generated from coulomb/repo-seed
Scope as first class root charactaristic
This commit is contained in:
@@ -123,6 +123,15 @@ CREATE TABLE IF NOT EXISTS review_decisions (
|
|||||||
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
|
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||||
);
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS repository_scopes (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
repository_id INTEGER NOT NULL UNIQUE REFERENCES repositories(id) ON DELETE CASCADE,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
description TEXT NOT NULL DEFAULT '',
|
||||||
|
confidence REAL NOT NULL DEFAULT 1.0,
|
||||||
|
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS approved_abilities (
|
CREATE TABLE IF NOT EXISTS approved_abilities (
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
repository_id INTEGER NOT NULL REFERENCES repositories(id) ON DELETE CASCADE,
|
repository_id INTEGER NOT NULL REFERENCES repositories(id) ON DELETE CASCADE,
|
||||||
@@ -183,6 +192,7 @@ CREATE INDEX IF NOT EXISTS idx_candidate_capabilities_repository ON candidate_ca
|
|||||||
CREATE INDEX IF NOT EXISTS idx_candidate_features_repository ON candidate_features(repository_id);
|
CREATE INDEX IF NOT EXISTS idx_candidate_features_repository ON candidate_features(repository_id);
|
||||||
CREATE INDEX IF NOT EXISTS idx_candidate_evidence_repository ON candidate_evidence(repository_id);
|
CREATE INDEX IF NOT EXISTS idx_candidate_evidence_repository ON candidate_evidence(repository_id);
|
||||||
CREATE INDEX IF NOT EXISTS idx_review_decisions_repository ON review_decisions(repository_id);
|
CREATE INDEX IF NOT EXISTS idx_review_decisions_repository ON review_decisions(repository_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_scopes_repository ON repository_scopes(repository_id);
|
||||||
CREATE INDEX IF NOT EXISTS idx_abilities_repository ON approved_abilities(repository_id);
|
CREATE INDEX IF NOT EXISTS idx_abilities_repository ON approved_abilities(repository_id);
|
||||||
CREATE INDEX IF NOT EXISTS idx_capabilities_repository ON approved_capabilities(repository_id);
|
CREATE INDEX IF NOT EXISTS idx_capabilities_repository ON approved_capabilities(repository_id);
|
||||||
CREATE INDEX IF NOT EXISTS idx_features_repository ON approved_features(repository_id);
|
CREATE INDEX IF NOT EXISTS idx_features_repository ON approved_features(repository_id);
|
||||||
|
|||||||
@@ -210,6 +210,15 @@ class Evidence:
|
|||||||
reference_id: int | None = None
|
reference_id: int | None = None
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class Scope:
|
||||||
|
id: int
|
||||||
|
name: str
|
||||||
|
description: str
|
||||||
|
confidence: float
|
||||||
|
confidence_label: str = ""
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
class Feature:
|
class Feature:
|
||||||
id: int
|
id: int
|
||||||
@@ -247,6 +256,7 @@ class Ability:
|
|||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
class RepositoryAbilityMap:
|
class RepositoryAbilityMap:
|
||||||
repository: Repository
|
repository: Repository
|
||||||
|
scope: Scope
|
||||||
abilities: list[Ability]
|
abilities: list[Ability]
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1214,6 +1214,22 @@ class RegistryService:
|
|||||||
self.store.delete_evidence(repository_id, evidence_id)
|
self.store.delete_evidence(repository_id, evidence_id)
|
||||||
return self.store.get_ability_map(repository_id)
|
return self.store.get_ability_map(repository_id)
|
||||||
|
|
||||||
|
def update_scope(
|
||||||
|
self,
|
||||||
|
repository_id: int,
|
||||||
|
*,
|
||||||
|
name: str | None = None,
|
||||||
|
description: str | None = None,
|
||||||
|
confidence: float | None = None,
|
||||||
|
) -> RepositoryAbilityMap:
|
||||||
|
self.store.update_scope(
|
||||||
|
repository_id,
|
||||||
|
name=name,
|
||||||
|
description=description,
|
||||||
|
confidence=confidence,
|
||||||
|
)
|
||||||
|
return self.store.get_ability_map(repository_id)
|
||||||
|
|
||||||
def ability_map(self, repository_id: int) -> RepositoryAbilityMap:
|
def ability_map(self, repository_id: int) -> RepositoryAbilityMap:
|
||||||
return self.store.get_ability_map(repository_id)
|
return self.store.get_ability_map(repository_id)
|
||||||
|
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ from repo_registry.core.models import (
|
|||||||
RepositorySnapshot,
|
RepositorySnapshot,
|
||||||
ReviewDecision,
|
ReviewDecision,
|
||||||
SearchResult,
|
SearchResult,
|
||||||
|
Scope,
|
||||||
SourceReference,
|
SourceReference,
|
||||||
confidence_label,
|
confidence_label,
|
||||||
)
|
)
|
||||||
@@ -47,6 +48,7 @@ class RegistryStore:
|
|||||||
with self.connect() as connection:
|
with self.connect() as connection:
|
||||||
connection.executescript(migration_path.read_text(encoding="utf-8"))
|
connection.executescript(migration_path.read_text(encoding="utf-8"))
|
||||||
self._ensure_content_chunks_table(connection)
|
self._ensure_content_chunks_table(connection)
|
||||||
|
self._ensure_repository_scopes_table(connection)
|
||||||
self._ensure_approved_source_ref_columns(connection)
|
self._ensure_approved_source_ref_columns(connection)
|
||||||
self._ensure_evidence_relationship_columns(connection)
|
self._ensure_evidence_relationship_columns(connection)
|
||||||
self._ensure_expectation_gaps_table(connection)
|
self._ensure_expectation_gaps_table(connection)
|
||||||
@@ -130,6 +132,23 @@ class RegistryStore:
|
|||||||
"CREATE INDEX IF NOT EXISTS idx_content_chunks_run ON content_chunks(analysis_run_id)"
|
"CREATE INDEX IF NOT EXISTS idx_content_chunks_run ON content_chunks(analysis_run_id)"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def _ensure_repository_scopes_table(self, connection: sqlite3.Connection) -> None:
|
||||||
|
connection.execute(
|
||||||
|
"""
|
||||||
|
CREATE TABLE IF NOT EXISTS repository_scopes (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
repository_id INTEGER NOT NULL UNIQUE REFERENCES repositories(id) ON DELETE CASCADE,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
description TEXT NOT NULL DEFAULT '',
|
||||||
|
confidence REAL NOT NULL DEFAULT 1.0,
|
||||||
|
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||||
|
)
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
connection.execute(
|
||||||
|
"CREATE INDEX IF NOT EXISTS idx_scopes_repository ON repository_scopes(repository_id)"
|
||||||
|
)
|
||||||
|
|
||||||
def _ensure_expectation_gaps_table(self, connection: sqlite3.Connection) -> None:
|
def _ensure_expectation_gaps_table(self, connection: sqlite3.Connection) -> None:
|
||||||
connection.execute(
|
connection.execute(
|
||||||
"""
|
"""
|
||||||
@@ -170,6 +189,14 @@ class RegistryStore:
|
|||||||
(name, url, description, branch),
|
(name, url, description, branch),
|
||||||
)
|
)
|
||||||
repository_id = int(cursor.lastrowid)
|
repository_id = int(cursor.lastrowid)
|
||||||
|
connection.execute(
|
||||||
|
"""
|
||||||
|
INSERT INTO repository_scopes
|
||||||
|
(repository_id, name, description, confidence)
|
||||||
|
VALUES (?, ?, ?, 1.0)
|
||||||
|
""",
|
||||||
|
(repository_id, name, description or ""),
|
||||||
|
)
|
||||||
return self.get_repository(repository_id)
|
return self.get_repository(repository_id)
|
||||||
|
|
||||||
def update_repository(
|
def update_repository(
|
||||||
@@ -1822,6 +1849,81 @@ class RegistryStore:
|
|||||||
row_id=evidence_id,
|
row_id=evidence_id,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def _ensure_scope(self, repository_id: int) -> Scope:
|
||||||
|
repository = self.get_repository(repository_id)
|
||||||
|
with self.connect() as connection:
|
||||||
|
row = connection.execute(
|
||||||
|
"""
|
||||||
|
SELECT id, name, description, confidence
|
||||||
|
FROM repository_scopes
|
||||||
|
WHERE repository_id = ?
|
||||||
|
""",
|
||||||
|
(repository_id,),
|
||||||
|
).fetchone()
|
||||||
|
if row is None:
|
||||||
|
cursor = connection.execute(
|
||||||
|
"""
|
||||||
|
INSERT INTO repository_scopes
|
||||||
|
(repository_id, name, description, confidence)
|
||||||
|
VALUES (?, ?, ?, 1.0)
|
||||||
|
""",
|
||||||
|
(repository_id, repository.name, repository.description or ""),
|
||||||
|
)
|
||||||
|
scope_id = int(cursor.lastrowid)
|
||||||
|
return Scope(
|
||||||
|
id=scope_id,
|
||||||
|
name=repository.name,
|
||||||
|
description=repository.description or "",
|
||||||
|
confidence=1.0,
|
||||||
|
confidence_label=confidence_label(1.0),
|
||||||
|
)
|
||||||
|
return self._scope_from_row(row)
|
||||||
|
|
||||||
|
def _get_scope(self, repository_id: int) -> Scope:
|
||||||
|
with self.connect() as connection:
|
||||||
|
row = connection.execute(
|
||||||
|
"""
|
||||||
|
SELECT id, name, description, confidence
|
||||||
|
FROM repository_scopes
|
||||||
|
WHERE repository_id = ?
|
||||||
|
""",
|
||||||
|
(repository_id,),
|
||||||
|
).fetchone()
|
||||||
|
if row is None:
|
||||||
|
return self._ensure_scope(repository_id)
|
||||||
|
return self._scope_from_row(row)
|
||||||
|
|
||||||
|
def _scope_from_row(self, row: sqlite3.Row) -> Scope:
|
||||||
|
return Scope(
|
||||||
|
id=row["id"],
|
||||||
|
name=row["name"],
|
||||||
|
description=row["description"],
|
||||||
|
confidence=row["confidence"],
|
||||||
|
confidence_label=confidence_label(row["confidence"]),
|
||||||
|
)
|
||||||
|
|
||||||
|
def update_scope(
|
||||||
|
self,
|
||||||
|
repository_id: int,
|
||||||
|
*,
|
||||||
|
name: str | None = None,
|
||||||
|
description: str | None = None,
|
||||||
|
confidence: float | None = None,
|
||||||
|
) -> Scope:
|
||||||
|
self._ensure_scope(repository_id)
|
||||||
|
self._update_approved_row(
|
||||||
|
table="repository_scopes",
|
||||||
|
label="scope",
|
||||||
|
repository_id=repository_id,
|
||||||
|
row_id=self._get_scope(repository_id).id,
|
||||||
|
values={
|
||||||
|
"name": name,
|
||||||
|
"description": description,
|
||||||
|
"confidence": confidence,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
return self._get_scope(repository_id)
|
||||||
|
|
||||||
def replace_approved_from_candidate_graph(
|
def replace_approved_from_candidate_graph(
|
||||||
self,
|
self,
|
||||||
repository_id: int,
|
repository_id: int,
|
||||||
@@ -1922,6 +2024,7 @@ class RegistryStore:
|
|||||||
|
|
||||||
def get_ability_map(self, repository_id: int) -> RepositoryAbilityMap:
|
def get_ability_map(self, repository_id: int) -> RepositoryAbilityMap:
|
||||||
repository = self.get_repository(repository_id)
|
repository = self.get_repository(repository_id)
|
||||||
|
scope = self._ensure_scope(repository_id)
|
||||||
with self.connect() as connection:
|
with self.connect() as connection:
|
||||||
ability_rows = connection.execute(
|
ability_rows = connection.execute(
|
||||||
"""
|
"""
|
||||||
@@ -2018,7 +2121,7 @@ class RegistryStore:
|
|||||||
)
|
)
|
||||||
for row in ability_rows
|
for row in ability_rows
|
||||||
]
|
]
|
||||||
return RepositoryAbilityMap(repository=repository, abilities=abilities)
|
return RepositoryAbilityMap(repository=repository, scope=scope, abilities=abilities)
|
||||||
|
|
||||||
def search(
|
def search(
|
||||||
self,
|
self,
|
||||||
@@ -2370,7 +2473,7 @@ class RegistryStore:
|
|||||||
label: str,
|
label: str,
|
||||||
repository_id: int,
|
repository_id: int,
|
||||||
row_id: int,
|
row_id: int,
|
||||||
values: dict[str, str | float | None],
|
values: dict[str, str | float | int | None],
|
||||||
) -> None:
|
) -> None:
|
||||||
assignments: list[str] = []
|
assignments: list[str] = []
|
||||||
params: list[str | float | int] = []
|
params: list[str | float | int] = []
|
||||||
|
|||||||
@@ -716,8 +716,17 @@ class AbilityResponse(BaseModel):
|
|||||||
capabilities: list[CapabilityResponse]
|
capabilities: list[CapabilityResponse]
|
||||||
|
|
||||||
|
|
||||||
|
class ScopeResponse(BaseModel):
|
||||||
|
id: int
|
||||||
|
name: str
|
||||||
|
description: str
|
||||||
|
confidence: float
|
||||||
|
confidence_label: str
|
||||||
|
|
||||||
|
|
||||||
class RepositoryAbilityMapResponse(BaseModel):
|
class RepositoryAbilityMapResponse(BaseModel):
|
||||||
repository: RepositoryResponse
|
repository: RepositoryResponse
|
||||||
|
scope: ScopeResponse
|
||||||
abilities: list[AbilityResponse]
|
abilities: list[AbilityResponse]
|
||||||
|
|
||||||
model_config = {
|
model_config = {
|
||||||
@@ -725,6 +734,13 @@ class RepositoryAbilityMapResponse(BaseModel):
|
|||||||
"examples": [
|
"examples": [
|
||||||
{
|
{
|
||||||
"repository": REPOSITORY_EXAMPLE,
|
"repository": REPOSITORY_EXAMPLE,
|
||||||
|
"scope": {
|
||||||
|
"id": 1,
|
||||||
|
"name": "MailRouter",
|
||||||
|
"description": "Scope root for the MailRouter repository.",
|
||||||
|
"confidence": 1.0,
|
||||||
|
"confidence_label": "high",
|
||||||
|
},
|
||||||
"abilities": [
|
"abilities": [
|
||||||
{
|
{
|
||||||
"id": 1,
|
"id": 1,
|
||||||
|
|||||||
@@ -647,6 +647,23 @@ def edit_repository_from_form(
|
|||||||
return RedirectResponse(f"/ui/repos/{repository_id}", status_code=303)
|
return RedirectResponse(f"/ui/repos/{repository_id}", status_code=303)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/ui/repos/{repository_id}/scope/edit")
|
||||||
|
def edit_scope_from_form(
|
||||||
|
repository_id: int,
|
||||||
|
name: str = Form(...),
|
||||||
|
description: str = Form(""),
|
||||||
|
confidence: float = Form(1.0),
|
||||||
|
service: RegistryService = Depends(get_service),
|
||||||
|
) -> RedirectResponse:
|
||||||
|
service.update_scope(
|
||||||
|
repository_id,
|
||||||
|
name=name,
|
||||||
|
description=description,
|
||||||
|
confidence=confidence,
|
||||||
|
)
|
||||||
|
return RedirectResponse(f"/ui/repos/{repository_id}", status_code=303)
|
||||||
|
|
||||||
|
|
||||||
@router.post("/ui/repos/{repository_id}/delete")
|
@router.post("/ui/repos/{repository_id}/delete")
|
||||||
def delete_repository_from_form(
|
def delete_repository_from_form(
|
||||||
repository_id: int,
|
repository_id: int,
|
||||||
@@ -2697,11 +2714,9 @@ def render_candidate_merge_form(
|
|||||||
|
|
||||||
def render_ability_map(ability_map: dict, repository_id: int) -> str:
|
def render_ability_map(ability_map: dict, repository_id: int) -> str:
|
||||||
abilities = ability_map.get("abilities", [])
|
abilities = ability_map.get("abilities", [])
|
||||||
if not abilities:
|
scope = ability_map["scope"]
|
||||||
return '<p class="muted">No approved entries yet.</p>'
|
scope_description = scope.get("description") or (
|
||||||
repository = ability_map["repository"]
|
f"Scope root for the approved characteristics of {scope['name']}."
|
||||||
scope_description = repository.get("description") or (
|
|
||||||
f"Scope root for the approved characteristics of {repository['name']}."
|
|
||||||
)
|
)
|
||||||
items = []
|
items = []
|
||||||
for ability in abilities:
|
for ability in abilities:
|
||||||
@@ -2745,17 +2760,32 @@ def render_ability_map(ability_map: dict, repository_id: int) -> str:
|
|||||||
return f"""
|
return f"""
|
||||||
<div class="tree">
|
<div class="tree">
|
||||||
<ul>
|
<ul>
|
||||||
<li id="scope-{repository['id']}">
|
<li id="scope-{scope['id']}">
|
||||||
<strong>{escape(repository['name'])}</strong>
|
<strong>{escape(scope['name'])}</strong>
|
||||||
<span class="pill">scope</span>
|
<span class="pill">scope</span>
|
||||||
|
<span class="pill">{scope['confidence']:.2f} {escape(scope['confidence_label'])}</span>
|
||||||
<p class="muted">{escape(scope_description)}</p>
|
<p class="muted">{escape(scope_description)}</p>
|
||||||
<ul>{"".join(items)}</ul>
|
{render_approved_scope_form(scope, repository_id)}
|
||||||
|
<ul>{"".join(items) or '<li class="muted">No approved characteristics yet.</li>'}</ul>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def render_approved_scope_form(scope: dict, repository_id: int) -> str:
|
||||||
|
return f"""
|
||||||
|
<form class="stack" method="post" action="/ui/repos/{repository_id}/scope/edit">
|
||||||
|
<label>Name <input name="name" value="{escape(scope['name'])}" required></label>
|
||||||
|
<label>Description <textarea name="description" rows="2">{escape(scope['description'])}</textarea></label>
|
||||||
|
<label>Confidence <input name="confidence" type="number" min="0" max="1" step="0.01" value="{scope['confidence']:.2f}" required></label>
|
||||||
|
<div class="actions">
|
||||||
|
<button class="secondary" type="submit">Save Scope</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
def render_approved_ability_forms(ability: dict, repository_id: int) -> str:
|
def render_approved_ability_forms(ability: dict, repository_id: int) -> str:
|
||||||
return f"""
|
return f"""
|
||||||
<form class="stack" method="post" action="/ui/repos/{repository_id}/abilities/{ability['id']}/edit">
|
<form class="stack" method="post" action="/ui/repos/{repository_id}/abilities/{ability['id']}/edit">
|
||||||
|
|||||||
@@ -82,6 +82,8 @@ def test_manual_registry_builds_ability_map(tmp_path):
|
|||||||
ability_map = service.ability_map(repository.id)
|
ability_map = service.ability_map(repository.id)
|
||||||
|
|
||||||
assert ability_map.repository.name == "MailRouter"
|
assert ability_map.repository.name == "MailRouter"
|
||||||
|
assert ability_map.scope.name == "MailRouter"
|
||||||
|
assert ability_map.scope.confidence_label == "high"
|
||||||
assert ability_map.abilities[0].name == "Business Email Routing"
|
assert ability_map.abilities[0].name == "Business Email Routing"
|
||||||
capability = ability_map.abilities[0].capabilities[0]
|
capability = ability_map.abilities[0].capabilities[0]
|
||||||
assert capability.name == "Classify Incoming Email"
|
assert capability.name == "Classify Incoming Email"
|
||||||
@@ -93,6 +95,15 @@ def test_manual_registry_builds_ability_map(tmp_path):
|
|||||||
assert capability.evidence[0].reference_kind == "fact"
|
assert capability.evidence[0].reference_kind == "fact"
|
||||||
assert capability.evidence[0].reference_id == 42
|
assert capability.evidence[0].reference_id == 42
|
||||||
|
|
||||||
|
updated_map = service.update_scope(
|
||||||
|
repository.id,
|
||||||
|
name="MailRouter Product Scope",
|
||||||
|
description="Email routing repository scope.",
|
||||||
|
confidence=0.9,
|
||||||
|
)
|
||||||
|
assert updated_map.scope.name == "MailRouter Product Scope"
|
||||||
|
assert updated_map.scope.description == "Email routing repository scope."
|
||||||
|
|
||||||
|
|
||||||
def test_manual_registry_updates_and_deletes_approved_entries(tmp_path):
|
def test_manual_registry_updates_and_deletes_approved_entries(tmp_path):
|
||||||
service = make_service(tmp_path)
|
service = make_service(tmp_path)
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ def test_initialize_is_idempotent_and_applies_expected_columns(tmp_path):
|
|||||||
} <= evidence_columns
|
} <= evidence_columns
|
||||||
assert "content_chunks" in tables
|
assert "content_chunks" in tables
|
||||||
assert "expectation_gaps" in tables
|
assert "expectation_gaps" in tables
|
||||||
|
assert "repository_scopes" in tables
|
||||||
|
|
||||||
|
|
||||||
def test_approved_registry_schema_allows_future_nullable_vocabulary_ref(tmp_path):
|
def test_approved_registry_schema_allows_future_nullable_vocabulary_ref(tmp_path):
|
||||||
@@ -152,6 +153,7 @@ def test_delete_repository_cascades_registry_and_review_rows(tmp_path):
|
|||||||
with service.store.connect() as connection:
|
with service.store.connect() as connection:
|
||||||
for table in (
|
for table in (
|
||||||
"approved_abilities",
|
"approved_abilities",
|
||||||
|
"repository_scopes",
|
||||||
"approved_capabilities",
|
"approved_capabilities",
|
||||||
"approved_features",
|
"approved_features",
|
||||||
"approved_evidence",
|
"approved_evidence",
|
||||||
|
|||||||
@@ -1611,6 +1611,21 @@ def test_ui_manual_registry_entry_loop(tmp_path):
|
|||||||
assert "Add Capability Support" in detail_response.text
|
assert "Add Capability Support" in detail_response.text
|
||||||
assert "Supported characteristic kind" in detail_response.text
|
assert "Supported characteristic kind" in detail_response.text
|
||||||
assert "Reference kind" in detail_response.text
|
assert "Reference kind" in detail_response.text
|
||||||
|
assert "Save Scope" in detail_response.text
|
||||||
|
|
||||||
|
scope_response = client.post(
|
||||||
|
f"{repository_path}/scope/edit",
|
||||||
|
data={
|
||||||
|
"name": "Manual Repository Scope",
|
||||||
|
"description": "Root product scope.",
|
||||||
|
"confidence": "0.91",
|
||||||
|
},
|
||||||
|
follow_redirects=False,
|
||||||
|
)
|
||||||
|
assert scope_response.status_code == 303
|
||||||
|
assert client.get(f"/repos/{repository_id}/ability-map").json()["scope"][
|
||||||
|
"name"
|
||||||
|
] == "Manual Repository Scope"
|
||||||
|
|
||||||
ability_response = client.post(
|
ability_response = client.post(
|
||||||
f"{repository_path}/abilities",
|
f"{repository_path}/abilities",
|
||||||
|
|||||||
@@ -231,3 +231,8 @@ Implementation note 2026-04-29: approved and candidate evidence now carry
|
|||||||
additive support metadata: `target_kind`, `target_id`, `reference_kind`, and
|
additive support metadata: `target_kind`, `target_id`, `reference_kind`, and
|
||||||
`reference_id`. Existing capability-bound evidence remains compatible, while the
|
`reference_id`. Existing capability-bound evidence remains compatible, while the
|
||||||
UI exposes these fields as supported-characteristic and reference metadata.
|
UI exposes these fields as supported-characteristic and reference metadata.
|
||||||
|
|
||||||
|
Implementation note 2026-04-29: repository scope is now first-class. A
|
||||||
|
`repository_scopes` row is created for new repositories and backfilled lazily for
|
||||||
|
existing repositories. The ability-map model and API include `scope`, and the UI
|
||||||
|
allows editing the scope root above approved abilities.
|
||||||
|
|||||||
Reference in New Issue
Block a user