UI can now build an approved profile by hand

This commit is contained in:
2026-04-26 02:26:34 +02:00
parent 6da0e8966b
commit 6b7c6443ae
7 changed files with 794 additions and 1 deletions

View File

@@ -121,6 +121,14 @@ GET /repos/{id}/analysis-runs/{run_id}
GET /repos/{id}/analysis-runs/{run_id}/candidate-graph
POST /repos/{id}/analysis-runs/{run_id}/candidate-graph/approve
GET /repos/{id}/ability-map
PATCH /repos/{id}/abilities/{ability_id}
DELETE /repos/{id}/abilities/{ability_id}
PATCH /repos/{id}/capabilities/{capability_id}
DELETE /repos/{id}/capabilities/{capability_id}
PATCH /repos/{id}/features/{feature_id}
DELETE /repos/{id}/features/{feature_id}
PATCH /repos/{id}/evidence/{evidence_id}
DELETE /repos/{id}/evidence/{evidence_id}
GET /abilities
GET /capabilities
GET /search?q=...

View File

@@ -545,6 +545,32 @@ class RegistryService:
confidence=confidence,
)
def update_ability(
self,
repository_id: int,
ability_id: int,
*,
name: str | None = None,
description: str | None = None,
confidence: float | None = None,
) -> RepositoryAbilityMap:
self.store.update_ability(
repository_id,
ability_id,
name=name,
description=description,
confidence=confidence,
)
return self.store.get_ability_map(repository_id)
def delete_ability(
self,
repository_id: int,
ability_id: int,
) -> RepositoryAbilityMap:
self.store.delete_ability(repository_id, ability_id)
return self.store.get_ability_map(repository_id)
def add_capability(
self,
repository_id: int,
@@ -567,6 +593,36 @@ class RegistryService:
confidence=confidence,
)
def update_capability(
self,
repository_id: int,
capability_id: int,
*,
name: str | None = None,
description: str | None = None,
inputs: Sequence[str] | None = None,
outputs: Sequence[str] | None = None,
confidence: float | None = None,
) -> RepositoryAbilityMap:
self.store.update_capability(
repository_id,
capability_id,
name=name,
description=description,
inputs=list(inputs) if inputs is not None else None,
outputs=list(outputs) if outputs is not None else None,
confidence=confidence,
)
return self.store.get_ability_map(repository_id)
def delete_capability(
self,
repository_id: int,
capability_id: int,
) -> RepositoryAbilityMap:
self.store.delete_capability(repository_id, capability_id)
return self.store.get_ability_map(repository_id)
def add_feature(
self,
repository_id: int,
@@ -587,6 +643,34 @@ class RegistryService:
confidence=confidence,
)
def update_feature(
self,
repository_id: int,
feature_id: int,
*,
name: str | None = None,
type: str | None = None,
location: str | None = None,
confidence: float | None = None,
) -> RepositoryAbilityMap:
self.store.update_feature(
repository_id,
feature_id,
name=name,
type=type,
location=location,
confidence=confidence,
)
return self.store.get_ability_map(repository_id)
def delete_feature(
self,
repository_id: int,
feature_id: int,
) -> RepositoryAbilityMap:
self.store.delete_feature(repository_id, feature_id)
return self.store.get_ability_map(repository_id)
def add_evidence(
self,
repository_id: int,
@@ -605,6 +689,32 @@ class RegistryService:
strength=strength,
)
def update_evidence(
self,
repository_id: int,
evidence_id: int,
*,
type: str | None = None,
reference: str | None = None,
strength: str | None = None,
) -> RepositoryAbilityMap:
self.store.update_evidence(
repository_id,
evidence_id,
type=type,
reference=reference,
strength=strength,
)
return self.store.get_ability_map(repository_id)
def delete_evidence(
self,
repository_id: int,
evidence_id: int,
) -> RepositoryAbilityMap:
self.store.delete_evidence(repository_id, evidence_id)
return self.store.get_ability_map(repository_id)
def ability_map(self, repository_id: int) -> RepositoryAbilityMap:
return self.store.get_ability_map(repository_id)

View File

@@ -1196,6 +1196,35 @@ class RegistryStore:
f"ability {ability_id} was not found for repository {repository_id}"
)
def update_ability(
self,
repository_id: int,
ability_id: int,
*,
name: str | None = None,
description: str | None = None,
confidence: float | None = None,
) -> None:
self._update_approved_row(
table="approved_abilities",
label="ability",
repository_id=repository_id,
row_id=ability_id,
values={
"name": name,
"description": description,
"confidence": confidence,
},
)
def delete_ability(self, repository_id: int, ability_id: int) -> None:
self._delete_approved_row(
table="approved_abilities",
label="ability",
repository_id=repository_id,
row_id=ability_id,
)
def create_capability(
self,
repository_id: int,
@@ -1240,6 +1269,39 @@ class RegistryStore:
f"capability {capability_id} was not found for repository {repository_id}"
)
def update_capability(
self,
repository_id: int,
capability_id: int,
*,
name: str | None = None,
description: str | None = None,
inputs: list[str] | None = None,
outputs: list[str] | None = None,
confidence: float | None = None,
) -> None:
self._update_approved_row(
table="approved_capabilities",
label="capability",
repository_id=repository_id,
row_id=capability_id,
values={
"name": name,
"description": description,
"inputs": json.dumps(inputs) if inputs is not None else None,
"outputs": json.dumps(outputs) if outputs is not None else None,
"confidence": confidence,
},
)
def delete_capability(self, repository_id: int, capability_id: int) -> None:
self._delete_approved_row(
table="approved_capabilities",
label="capability",
repository_id=repository_id,
row_id=capability_id,
)
def create_feature(
self,
repository_id: int,
@@ -1270,6 +1332,37 @@ class RegistryStore:
)
return int(cursor.lastrowid)
def update_feature(
self,
repository_id: int,
feature_id: int,
*,
name: str | None = None,
type: str | None = None,
location: str | None = None,
confidence: float | None = None,
) -> None:
self._update_approved_row(
table="approved_features",
label="feature",
repository_id=repository_id,
row_id=feature_id,
values={
"name": name,
"type": type,
"location": location,
"confidence": confidence,
},
)
def delete_feature(self, repository_id: int, feature_id: int) -> None:
self._delete_approved_row(
table="approved_features",
label="feature",
repository_id=repository_id,
row_id=feature_id,
)
def create_evidence(
self,
repository_id: int,
@@ -1298,6 +1391,35 @@ class RegistryStore:
)
return int(cursor.lastrowid)
def update_evidence(
self,
repository_id: int,
evidence_id: int,
*,
type: str | None = None,
reference: str | None = None,
strength: str | None = None,
) -> None:
self._update_approved_row(
table="approved_evidence",
label="evidence",
repository_id=repository_id,
row_id=evidence_id,
values={
"type": type,
"reference": reference,
"strength": strength,
},
)
def delete_evidence(self, repository_id: int, evidence_id: int) -> None:
self._delete_approved_row(
table="approved_evidence",
label="evidence",
repository_id=repository_id,
row_id=evidence_id,
)
def get_ability_map(self, repository_id: int) -> RepositoryAbilityMap:
repository = self.get_repository(repository_id)
with self.connect() as connection:
@@ -1715,6 +1837,81 @@ class RegistryStore:
],
)
def _update_approved_row(
self,
*,
table: str,
label: str,
repository_id: int,
row_id: int,
values: dict[str, str | float | None],
) -> None:
assignments: list[str] = []
params: list[str | float | int] = []
for column, value in values.items():
if value is None:
continue
assignments.append(f"{column} = ?")
params.append(value)
if not assignments:
self._ensure_approved_row(
table=table,
label=label,
repository_id=repository_id,
row_id=row_id,
)
return
params.extend([row_id, repository_id])
with self.connect() as connection:
cursor = connection.execute(
f"""
UPDATE {table}
SET {", ".join(assignments)}
WHERE id = ? AND repository_id = ?
""",
params,
)
if cursor.rowcount == 0:
raise NotFoundError(
f"{label} {row_id} was not found for repository {repository_id}"
)
def _delete_approved_row(
self,
*,
table: str,
label: str,
repository_id: int,
row_id: int,
) -> None:
with self.connect() as connection:
cursor = connection.execute(
f"DELETE FROM {table} WHERE id = ? AND repository_id = ?",
(row_id, repository_id),
)
if cursor.rowcount == 0:
raise NotFoundError(
f"{label} {row_id} was not found for repository {repository_id}"
)
def _ensure_approved_row(
self,
*,
table: str,
label: str,
repository_id: int,
row_id: int,
) -> None:
with self.connect() as connection:
row = connection.execute(
f"SELECT id FROM {table} WHERE id = ? AND repository_id = ?",
(row_id, repository_id),
).fetchone()
if row is None:
raise NotFoundError(
f"{label} {row_id} was not found for repository {repository_id}"
)
def _source_refs_to_json(self, source_refs: list[SourceReference]) -> str:
return json.dumps(
[

View File

@@ -84,6 +84,12 @@ class AbilityCreate(BaseModel):
}
class AbilityUpdate(BaseModel):
name: str | None = None
description: str | None = None
confidence: float | None = Field(default=None, ge=0.0, le=1.0)
class CapabilityCreate(BaseModel):
ability_id: int
name: str
@@ -108,6 +114,14 @@ class CapabilityCreate(BaseModel):
}
class CapabilityUpdate(BaseModel):
name: str | None = None
description: str | None = None
inputs: list[str] | None = None
outputs: list[str] | None = None
confidence: float | None = Field(default=None, ge=0.0, le=1.0)
class FeatureCreate(BaseModel):
capability_id: int
name: str
@@ -130,6 +144,13 @@ class FeatureCreate(BaseModel):
}
class FeatureUpdate(BaseModel):
name: str | None = None
type: str | None = None
location: str | None = None
confidence: float | None = Field(default=None, ge=0.0, le=1.0)
class EvidenceCreate(BaseModel):
capability_id: int
type: str
@@ -150,6 +171,12 @@ class EvidenceCreate(BaseModel):
}
class EvidenceUpdate(BaseModel):
type: str | None = None
reference: str | None = None
strength: str | None = None
class AnalysisRunCreate(BaseModel):
source_path: str | None = None
@@ -793,6 +820,37 @@ def create_ability(
return {"id": ability_id}
@app.patch("/repos/{repository_id}/abilities/{ability_id}")
def update_ability(
repository_id: int,
ability_id: int,
payload: AbilityUpdate,
service: RegistryService = Depends(get_service),
) -> dict[str, object]:
try:
return asdict(
service.update_ability(
repository_id,
ability_id,
**payload.model_dump(exclude_unset=True),
)
)
except NotFoundError as exc:
raise HTTPException(status_code=404, detail=str(exc)) from exc
@app.delete("/repos/{repository_id}/abilities/{ability_id}")
def delete_ability(
repository_id: int,
ability_id: int,
service: RegistryService = Depends(get_service),
) -> dict[str, object]:
try:
return asdict(service.delete_ability(repository_id, ability_id))
except NotFoundError as exc:
raise HTTPException(status_code=404, detail=str(exc)) from exc
@app.post("/repos/{repository_id}/capabilities", status_code=201)
def create_capability(
repository_id: int,
@@ -806,6 +864,37 @@ def create_capability(
return {"id": capability_id}
@app.patch("/repos/{repository_id}/capabilities/{capability_id}")
def update_capability(
repository_id: int,
capability_id: int,
payload: CapabilityUpdate,
service: RegistryService = Depends(get_service),
) -> dict[str, object]:
try:
return asdict(
service.update_capability(
repository_id,
capability_id,
**payload.model_dump(exclude_unset=True),
)
)
except NotFoundError as exc:
raise HTTPException(status_code=404, detail=str(exc)) from exc
@app.delete("/repos/{repository_id}/capabilities/{capability_id}")
def delete_capability(
repository_id: int,
capability_id: int,
service: RegistryService = Depends(get_service),
) -> dict[str, object]:
try:
return asdict(service.delete_capability(repository_id, capability_id))
except NotFoundError as exc:
raise HTTPException(status_code=404, detail=str(exc)) from exc
@app.post("/repos/{repository_id}/features", status_code=201)
def create_feature(
repository_id: int,
@@ -819,6 +908,37 @@ def create_feature(
return {"id": feature_id}
@app.patch("/repos/{repository_id}/features/{feature_id}")
def update_feature(
repository_id: int,
feature_id: int,
payload: FeatureUpdate,
service: RegistryService = Depends(get_service),
) -> dict[str, object]:
try:
return asdict(
service.update_feature(
repository_id,
feature_id,
**payload.model_dump(exclude_unset=True),
)
)
except NotFoundError as exc:
raise HTTPException(status_code=404, detail=str(exc)) from exc
@app.delete("/repos/{repository_id}/features/{feature_id}")
def delete_feature(
repository_id: int,
feature_id: int,
service: RegistryService = Depends(get_service),
) -> dict[str, object]:
try:
return asdict(service.delete_feature(repository_id, feature_id))
except NotFoundError as exc:
raise HTTPException(status_code=404, detail=str(exc)) from exc
@app.post("/repos/{repository_id}/evidence", status_code=201)
def create_evidence(
repository_id: int,
@@ -832,6 +952,37 @@ def create_evidence(
return {"id": evidence_id}
@app.patch("/repos/{repository_id}/evidence/{evidence_id}")
def update_evidence(
repository_id: int,
evidence_id: int,
payload: EvidenceUpdate,
service: RegistryService = Depends(get_service),
) -> dict[str, object]:
try:
return asdict(
service.update_evidence(
repository_id,
evidence_id,
**payload.model_dump(exclude_unset=True),
)
)
except NotFoundError as exc:
raise HTTPException(status_code=404, detail=str(exc)) from exc
@app.delete("/repos/{repository_id}/evidence/{evidence_id}")
def delete_evidence(
repository_id: int,
evidence_id: int,
service: RegistryService = Depends(get_service),
) -> dict[str, object]:
try:
return asdict(service.delete_evidence(repository_id, evidence_id))
except NotFoundError as exc:
raise HTTPException(status_code=404, detail=str(exc)) from exc
@app.get("/repos/{repository_id}/ability-map")
def get_ability_map(
repository_id: int,

View File

@@ -308,6 +308,45 @@ def repository_detail(
{render_ability_map(asdict(ability_map))}
</section>
</div>
<section class="panel" style="margin-top:18px">
<h2>Manual Registry Entry</h2>
<div class="grid">
<form class="stack" method="post" action="/ui/repos/{repository_id}/abilities">
<h3>Add Ability</h3>
<label>Name <input name="name" required></label>
<label>Description <textarea name="description" rows="2"></textarea></label>
<label>Confidence <input name="confidence" type="number" min="0" max="1" step="0.01" value="1.0" required></label>
<button type="submit">Add Ability</button>
</form>
<form class="stack" method="post" action="/ui/repos/{repository_id}/capabilities">
<h3>Add Capability</h3>
<label>Ability ID <input name="ability_id" type="number" min="1" required></label>
<label>Name <input name="name" required></label>
<label>Description <textarea name="description" rows="2"></textarea></label>
<label>Inputs <input name="inputs" placeholder="Comma-separated"></label>
<label>Outputs <input name="outputs" placeholder="Comma-separated"></label>
<label>Confidence <input name="confidence" type="number" min="0" max="1" step="0.01" value="1.0" required></label>
<button type="submit">Add Capability</button>
</form>
<form class="stack" method="post" action="/ui/repos/{repository_id}/features">
<h3>Add Feature</h3>
<label>Capability ID <input name="capability_id" type="number" min="1" required></label>
<label>Name <input name="name" required></label>
<label>Type <input name="type" required></label>
<label>Location <input name="location"></label>
<label>Confidence <input name="confidence" type="number" min="0" max="1" step="0.01" value="1.0" required></label>
<button type="submit">Add Feature</button>
</form>
<form class="stack" method="post" action="/ui/repos/{repository_id}/evidence">
<h3>Add Evidence</h3>
<label>Capability ID <input name="capability_id" type="number" min="1" required></label>
<label>Type <input name="type" required></label>
<label>Reference <input name="reference" required></label>
<label>Strength <input name="strength" value="medium" required></label>
<button type="submit">Add Evidence</button>
</form>
</div>
</section>
<section class="panel" style="margin-top:18px">
<h2>Review Decisions</h2>
{render_review_decisions(decisions)}
@@ -316,6 +355,86 @@ def repository_detail(
return page(repository.name, body)
@router.post("/ui/repos/{repository_id}/abilities")
def create_ability_from_form(
repository_id: int,
name: str = Form(...),
description: str = Form(""),
confidence: float = Form(1.0),
service: RegistryService = Depends(get_service),
) -> RedirectResponse:
service.add_ability(
repository_id,
name=name,
description=description,
confidence=confidence,
)
return RedirectResponse(f"/ui/repos/{repository_id}", status_code=303)
@router.post("/ui/repos/{repository_id}/capabilities")
def create_capability_from_form(
repository_id: int,
ability_id: int = Form(...),
name: str = Form(...),
description: str = Form(""),
inputs: str = Form(""),
outputs: str = Form(""),
confidence: float = Form(1.0),
service: RegistryService = Depends(get_service),
) -> RedirectResponse:
service.add_capability(
repository_id,
ability_id,
name=name,
description=description,
inputs=split_csv(inputs),
outputs=split_csv(outputs),
confidence=confidence,
)
return RedirectResponse(f"/ui/repos/{repository_id}", status_code=303)
@router.post("/ui/repos/{repository_id}/features")
def create_feature_from_form(
repository_id: int,
capability_id: int = Form(...),
name: str = Form(...),
type: str = Form(...),
location: str = Form(""),
confidence: float = Form(1.0),
service: RegistryService = Depends(get_service),
) -> RedirectResponse:
service.add_feature(
repository_id,
capability_id,
name=name,
type=type,
location=location,
confidence=confidence,
)
return RedirectResponse(f"/ui/repos/{repository_id}", status_code=303)
@router.post("/ui/repos/{repository_id}/evidence")
def create_evidence_from_form(
repository_id: int,
capability_id: int = Form(...),
type: str = Form(...),
reference: str = Form(...),
strength: str = Form("medium"),
service: RegistryService = Depends(get_service),
) -> RedirectResponse:
service.add_evidence(
repository_id,
capability_id,
type=type,
reference=reference,
strength=strength,
)
return RedirectResponse(f"/ui/repos/{repository_id}", status_code=303)
@router.post("/ui/repos/{repository_id}/analysis-runs")
def create_analysis_run_from_form(
repository_id: int,
@@ -751,6 +870,10 @@ def render_repository_facts(languages: list[str], frameworks: list[str]) -> str:
return f'<p class="actions">{language_pills}{framework_pills}</p>'
def split_csv(value: str) -> list[str]:
return [item.strip() for item in value.split(",") if item.strip()]
def render_review_decisions(decisions: list) -> str:
if not decisions:
return '<p class="muted">No review decisions yet.</p>'
@@ -967,6 +1090,7 @@ def render_ability_map(ability_map: dict) -> str:
f"""
<li id="capability-{capability['id']}">
<strong>{escape(capability['name'])}</strong>
<span class="pill">ID {capability['id']}</span>
<p class="muted">{escape(capability['description'])}</p>
<ul>{features}{evidence}</ul>
</li>
@@ -976,6 +1100,7 @@ def render_ability_map(ability_map: dict) -> str:
f"""
<li id="ability-{ability['id']}">
<strong>{escape(ability['name'])}</strong>
<span class="pill">ID {ability['id']}</span>
<p class="muted">{escape(ability['description'])}</p>
<ul>{''.join(capabilities)}</ul>
</li>

View File

@@ -61,6 +61,65 @@ def test_manual_registry_builds_ability_map(tmp_path):
assert capability.evidence[0].strength == "strong"
def test_manual_registry_updates_and_deletes_approved_entries(tmp_path):
service = make_service(tmp_path)
repository = service.register_repository(
name="Manual",
url="https://example.com/manual.git",
description="Manual registry fixture.",
)
ability_id = service.add_ability(repository.id, name="Original Ability")
capability_id = service.add_capability(
repository.id,
ability_id,
name="Original Capability",
)
feature_id = service.add_feature(
repository.id,
capability_id,
name="Original Feature",
type="API",
)
evidence_id = service.add_evidence(
repository.id,
capability_id,
type="test",
reference="tests/test_original.py",
)
service.update_ability(repository.id, ability_id, name="Updated Ability")
service.update_capability(
repository.id,
capability_id,
name="Updated Capability",
inputs=["request"],
outputs=["response"],
)
service.update_feature(repository.id, feature_id, location="src/api.py")
ability_map = service.update_evidence(
repository.id,
evidence_id,
strength="strong",
)
ability = ability_map.abilities[0]
capability = ability.capabilities[0]
assert ability.name == "Updated Ability"
assert capability.name == "Updated Capability"
assert capability.inputs == ["request"]
assert capability.outputs == ["response"]
assert capability.features[0].location == "src/api.py"
assert capability.evidence[0].strength == "strong"
service.delete_feature(repository.id, feature_id)
service.delete_evidence(repository.id, evidence_id)
ability_map = service.delete_capability(repository.id, capability_id)
assert ability_map.abilities[0].capabilities == []
ability_map = service.delete_ability(repository.id, ability_id)
assert ability_map.abilities == []
def test_repository_update_and_delete(tmp_path):
service = make_service(tmp_path)
repository = service.register_repository(

View File

@@ -66,13 +66,63 @@ def test_api_manual_registry_loop(tmp_path):
},
)
assert feature_response.status_code == 201
feature_id = feature_response.json()["id"]
evidence_response = client.post(
f"/repos/{repository_id}/evidence",
json={
"capability_id": capability_id,
"type": "unit_test",
"reference": "tests/test_email_classification.py",
},
)
assert evidence_response.status_code == 201
evidence_id = evidence_response.json()["id"]
ability_update_response = client.patch(
f"/repos/{repository_id}/abilities/{ability_id}",
json={"name": "Business Email Routing Updated"},
)
assert ability_update_response.status_code == 200
assert ability_update_response.json()["abilities"][0]["name"] == (
"Business Email Routing Updated"
)
capability_update_response = client.patch(
f"/repos/{repository_id}/capabilities/{capability_id}",
json={"name": "Classify Incoming Email Updated"},
)
assert capability_update_response.status_code == 200
assert capability_update_response.json()["abilities"][0]["capabilities"][0][
"name"
] == "Classify Incoming Email Updated"
feature_update_response = client.patch(
f"/repos/{repository_id}/features/{feature_id}",
json={"location": "src/routes/updated.py"},
)
assert feature_update_response.status_code == 200
evidence_update_response = client.patch(
f"/repos/{repository_id}/evidence/{evidence_id}",
json={"strength": "strong"},
)
assert evidence_update_response.status_code == 200
map_response = client.get(f"/repos/{repository_id}/ability-map")
assert map_response.status_code == 200
ability_map = map_response.json()
assert ability_map["repository"]["name"] == "MailRouter Updated"
assert ability_map["abilities"][0]["capabilities"][0]["name"] == (
"Classify Incoming Email"
"Classify Incoming Email Updated"
)
assert (
client.delete(f"/repos/{repository_id}/features/{feature_id}").status_code
== 200
)
assert (
client.delete(f"/repos/{repository_id}/evidence/{evidence_id}").status_code
== 200
)
search_response = client.get("/search", params={"q": "email"})
@@ -367,6 +417,99 @@ def test_ui_register_analyze_and_approve_loop(tmp_path):
app.dependency_overrides.clear()
def test_ui_manual_registry_entry_loop(tmp_path):
source = tmp_path / "manual-repo"
source.mkdir()
(source / "README.md").write_text("# Manual Repo\n", encoding="utf-8")
def override_settings():
return Settings(
database_path=str(tmp_path / "ui-manual.sqlite3"),
checkout_root=str(tmp_path / "ui-manual-checkouts"),
)
app.dependency_overrides[get_settings] = override_settings
client = TestClient(app)
try:
create_response = client.post(
"/ui/repos",
data={"url": str(source), "branch": "main"},
follow_redirects=False,
)
assert create_response.status_code == 303
repository_path = create_response.headers["location"]
repository_id = int(repository_path.rsplit("/", 1)[-1])
detail_response = client.get(repository_path)
assert detail_response.status_code == 200
assert "Manual Registry Entry" in detail_response.text
ability_response = client.post(
f"{repository_path}/abilities",
data={
"name": "Manual Ability",
"description": "Curated by hand.",
"confidence": "0.95",
},
follow_redirects=False,
)
assert ability_response.status_code == 303
ability_id = client.get(f"/repos/{repository_id}/ability-map").json()[
"abilities"
][0]["id"]
capability_response = client.post(
f"{repository_path}/capabilities",
data={
"ability_id": str(ability_id),
"name": "Manual Capability",
"description": "Curated capability.",
"inputs": "request, context",
"outputs": "response",
"confidence": "0.9",
},
follow_redirects=False,
)
assert capability_response.status_code == 303
capability_id = client.get(f"/repos/{repository_id}/ability-map").json()[
"abilities"
][0]["capabilities"][0]["id"]
feature_response = client.post(
f"{repository_path}/features",
data={
"capability_id": str(capability_id),
"name": "Manual API",
"type": "REST endpoint",
"location": "src/manual.py",
"confidence": "0.88",
},
follow_redirects=False,
)
assert feature_response.status_code == 303
evidence_response = client.post(
f"{repository_path}/evidence",
data={
"capability_id": str(capability_id),
"type": "documentation",
"reference": "README.md",
"strength": "medium",
},
follow_redirects=False,
)
assert evidence_response.status_code == 303
detail_response = client.get(repository_path)
assert "Manual Ability" in detail_response.text
assert "Manual Capability" in detail_response.text
assert "Manual API" in detail_response.text
assert "README.md" in detail_response.text
assert "ID " in detail_response.text
finally:
app.dependency_overrides.clear()
def test_api_rejects_candidate_capability_feature_and_evidence(tmp_path):
source = tmp_path / "repo"
source.mkdir()