generated from coulomb/repo-seed
UI can now build an approved profile by hand
This commit is contained in:
@@ -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=...
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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(
|
||||
[
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user