repo-scoping/src/repo_registry/web_api/app.py

1142 lines
33 KiB
Python
Raw Normal View History

from __future__ import annotations
from dataclasses import asdict
from pathlib import Path
2026-04-26 08:45:00 +02:00
from fastapi import Depends, FastAPI, HTTPException, Query
from pydantic import BaseModel, Field
2026-04-26 03:16:46 +02:00
from pydantic_settings import BaseSettings, SettingsConfigDict
from repo_registry.core.service import RegistryService
from repo_registry.llm_extraction import LLMCandidateExtractor, create_llm_connect_adapter
2026-04-25 22:37:06 +02:00
from repo_registry.repo_ingestion.git import GitIngestionService
from repo_registry.storage.sqlite import NotFoundError, RegistryStore
2026-04-26 03:16:46 +02:00
class Settings(BaseSettings):
model_config = SettingsConfigDict(env_prefix="REPO_REGISTRY_")
database_path: str = Field(default="var/repo-registry.sqlite3")
2026-04-25 22:37:06 +02:00
checkout_root: str = Field(default="var/checkouts")
llm_provider: str | None = Field(default=None)
llm_model: str | None = Field(default=None)
def get_settings() -> Settings:
return Settings()
def get_service(settings: Settings = Depends(get_settings)) -> RegistryService:
database_path = Path(settings.database_path)
database_path.parent.mkdir(parents=True, exist_ok=True)
store = RegistryStore(database_path)
store.initialize()
llm_extractor = None
if settings.llm_provider:
adapter = create_llm_connect_adapter(
settings.llm_provider,
model=settings.llm_model,
)
llm_extractor = LLMCandidateExtractor(adapter)
return RegistryService(
store,
ingestion=GitIngestionService(settings.checkout_root),
llm_extractor=llm_extractor,
)
class RepositoryCreate(BaseModel):
url: str
2026-04-25 23:04:15 +02:00
name: str | None = None
description: str | None = None
branch: str = "main"
2026-04-25 23:56:19 +02:00
model_config = {
"json_schema_extra": {
"examples": [
{
"url": "https://github.com/example/repository.git",
"name": "Example Repository",
"description": "Optional human-readable repository summary.",
"branch": "main",
}
]
}
}
2026-04-26 00:29:07 +02:00
class RepositoryUpdate(BaseModel):
name: str | None = None
description: str | None = None
branch: str | None = None
model_config = {
"json_schema_extra": {
"examples": [
{
"name": "Renamed Repository",
"description": "Updated curator-facing summary.",
"branch": "main",
}
]
}
}
class AbilityCreate(BaseModel):
name: str
description: str = ""
confidence: float = Field(default=1.0, ge=0.0, le=1.0)
2026-04-25 23:56:19 +02:00
model_config = {
"json_schema_extra": {
"examples": [
{
"name": "Business Email Routing",
"description": "Route inbound messages to the right team.",
"confidence": 0.92,
}
]
}
}
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
description: str = ""
inputs: list[str] = Field(default_factory=list)
outputs: list[str] = Field(default_factory=list)
confidence: float = Field(default=1.0, ge=0.0, le=1.0)
2026-04-25 23:56:19 +02:00
model_config = {
"json_schema_extra": {
"examples": [
{
"ability_id": 1,
"name": "Classify Incoming Email",
"description": "Classify messages by intent.",
"inputs": ["subject", "body"],
"outputs": ["intent", "confidence"],
"confidence": 0.88,
}
]
}
}
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
type: str
location: str = ""
confidence: float = Field(default=1.0, ge=0.0, le=1.0)
2026-04-25 23:56:19 +02:00
model_config = {
"json_schema_extra": {
"examples": [
{
"capability_id": 1,
"name": "POST /api/classify-email",
"type": "REST endpoint",
"location": "src/routes/classify_email.py",
"confidence": 0.84,
}
]
}
}
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
reference: str
strength: str = "medium"
2026-04-25 23:56:19 +02:00
model_config = {
"json_schema_extra": {
"examples": [
{
"capability_id": 1,
"type": "unit_test",
"reference": "tests/test_email_classification.py",
"strength": "strong",
}
]
}
}
class EvidenceUpdate(BaseModel):
type: str | None = None
reference: str | None = None
strength: str | None = None
class AnalysisRunCreate(BaseModel):
source_path: str | None = None
2026-04-25 23:56:19 +02:00
model_config = {
"json_schema_extra": {
"examples": [
{},
{"source_path": "/path/to/local/repository"},
]
}
}
2026-04-25 22:46:22 +02:00
class CandidateGraphApproval(BaseModel):
notes: str = ""
2026-04-25 23:56:19 +02:00
model_config = {
"json_schema_extra": {
"examples": [{"notes": "Approved after curator review."}]
}
}
2026-04-25 22:46:22 +02:00
2026-04-25 23:27:28 +02:00
class CandidateRejection(BaseModel):
notes: str = ""
2026-04-25 23:56:19 +02:00
model_config = {
"json_schema_extra": {
"examples": [{"notes": "Rejected because the claim is too generic."}]
}
}
2026-04-25 23:27:28 +02:00
class CandidateEdit(BaseModel):
name: str
description: str = ""
confidence: float = Field(default=0.5, ge=0.0, le=1.0)
notes: str = ""
2026-04-25 23:56:19 +02:00
model_config = {
"json_schema_extra": {
"examples": [
{
"name": "Service Health Monitoring",
"description": "Expose health state for operational checks.",
"confidence": 0.9,
"notes": "Renamed from generated review seed.",
}
]
}
}
2026-04-25 23:44:25 +02:00
class CandidateCapabilityRelink(BaseModel):
target_ability_id: int
notes: str = ""
2026-04-25 23:56:19 +02:00
model_config = {
"json_schema_extra": {
"examples": [
{"target_ability_id": 2, "notes": "Move under operational ability."}
]
}
}
2026-04-25 23:44:25 +02:00
class CandidateLeafRelink(BaseModel):
target_capability_id: int
notes: str = ""
2026-04-25 23:56:19 +02:00
model_config = {
"json_schema_extra": {
"examples": [
{
"target_capability_id": 3,
"notes": "Evidence supports a different capability.",
}
]
}
}
2026-04-25 23:44:25 +02:00
class CandidateAbilityMerge(BaseModel):
target_ability_id: int
notes: str = ""
2026-04-25 23:56:19 +02:00
model_config = {
"json_schema_extra": {
"examples": [
{"target_ability_id": 2, "notes": "Duplicate ability wording."}
]
}
}
class CandidateCapabilityMerge(BaseModel):
target_capability_id: int
notes: str = ""
2026-04-25 23:56:19 +02:00
model_config = {
"json_schema_extra": {
"examples": [
{"target_capability_id": 3, "notes": "Duplicate capability."}
]
}
}
class CandidateFeatureMerge(BaseModel):
target_feature_id: int
notes: str = ""
2026-04-25 23:56:19 +02:00
model_config = {
"json_schema_extra": {
"examples": [{"target_feature_id": 4, "notes": "Duplicate route."}]
}
}
class CandidateEvidenceMerge(BaseModel):
target_evidence_id: int
notes: str = ""
2026-04-25 23:56:19 +02:00
model_config = {
"json_schema_extra": {
"examples": [{"target_evidence_id": 5, "notes": "Duplicate evidence."}]
}
}
2026-04-26 08:45:00 +02:00
API_DESCRIPTION = (
"Register repositories, analyze their observable implementation facts, "
"curate reviewable ability graphs, and search approved repository abilities."
)
OPENAPI_TAGS = [
{"name": "health", "description": "Service health checks."},
{"name": "repositories", "description": "Repository registration and metadata."},
{"name": "analysis", "description": "Repository scans and extracted review inputs."},
{"name": "review", "description": "Candidate graph approval and correction workflow."},
{"name": "registry", "description": "Approved ability maps and manual registry CRUD."},
{"name": "search", "description": "Agent-facing discovery endpoints."},
]
app = FastAPI(
title="Repository Ability Registry",
version="0.1.0",
description=API_DESCRIPTION,
openapi_tags=OPENAPI_TAGS,
)
2026-04-25 23:04:15 +02:00
from repo_registry.web_ui.views import router as ui_router
app.include_router(ui_router)
2026-04-26 08:45:00 +02:00
@app.get("/health", tags=["health"])
def health() -> dict[str, str]:
return {"status": "ok"}
2026-04-26 08:45:00 +02:00
@app.post("/repos", status_code=201, tags=["repositories"])
def create_repository(
payload: RepositoryCreate,
service: RegistryService = Depends(get_service),
) -> dict[str, object]:
try:
repository = service.register_repository(**payload.model_dump())
2026-04-25 23:04:15 +02:00
except (RuntimeError, ValueError) as exc:
raise HTTPException(status_code=400, detail=str(exc)) from exc
return asdict(repository)
2026-04-26 08:45:00 +02:00
@app.get("/repos", tags=["repositories"])
def list_repositories(
service: RegistryService = Depends(get_service),
) -> list[dict[str, object]]:
return [asdict(repository) for repository in service.list_repositories()]
2026-04-26 08:45:00 +02:00
@app.get("/repos/{repository_id}", tags=["repositories"])
def get_repository(
repository_id: int,
service: RegistryService = Depends(get_service),
) -> dict[str, object]:
try:
return asdict(service.get_repository(repository_id))
except NotFoundError as exc:
raise HTTPException(status_code=404, detail=str(exc)) from exc
2026-04-26 08:45:00 +02:00
@app.patch("/repos/{repository_id}", tags=["repositories"])
2026-04-26 00:29:07 +02:00
def update_repository(
repository_id: int,
payload: RepositoryUpdate,
service: RegistryService = Depends(get_service),
) -> dict[str, object]:
try:
return asdict(
service.update_repository(
repository_id,
**payload.model_dump(exclude_unset=True),
)
)
except NotFoundError as exc:
raise HTTPException(status_code=404, detail=str(exc)) from exc
2026-04-26 08:45:00 +02:00
@app.delete("/repos/{repository_id}", status_code=204, tags=["repositories"])
2026-04-26 00:29:07 +02:00
def delete_repository(
repository_id: int,
service: RegistryService = Depends(get_service),
) -> None:
try:
service.delete_repository(repository_id)
except NotFoundError as exc:
raise HTTPException(status_code=404, detail=str(exc)) from exc
2026-04-26 08:45:00 +02:00
@app.post("/repos/{repository_id}/analysis-runs", status_code=201, tags=["analysis"])
def create_analysis_run(
repository_id: int,
payload: AnalysisRunCreate,
service: RegistryService = Depends(get_service),
) -> dict[str, object]:
try:
summary = service.analyze_repository(
repository_id,
source_path=payload.source_path,
)
except NotFoundError as exc:
raise HTTPException(status_code=404, detail=str(exc)) from exc
return asdict(summary)
2026-04-26 08:45:00 +02:00
@app.get("/repos/{repository_id}/analysis-runs", tags=["analysis"])
def list_analysis_runs(
repository_id: int,
service: RegistryService = Depends(get_service),
) -> list[dict[str, object]]:
try:
return [asdict(run) for run in service.list_analysis_runs(repository_id)]
except NotFoundError as exc:
raise HTTPException(status_code=404, detail=str(exc)) from exc
2026-04-26 08:45:00 +02:00
@app.get("/repos/{repository_id}/analysis-runs/{analysis_run_id}", tags=["analysis"])
2026-04-25 23:56:19 +02:00
def get_analysis_run(
repository_id: int,
analysis_run_id: int,
service: RegistryService = Depends(get_service),
) -> dict[str, object]:
try:
return asdict(service.get_analysis_run(repository_id, analysis_run_id))
except NotFoundError as exc:
raise HTTPException(status_code=404, detail=str(exc)) from exc
2026-04-26 08:45:00 +02:00
@app.get("/repos/{repository_id}/review-decisions", tags=["review"])
2026-04-26 00:24:02 +02:00
def list_repository_review_decisions(
repository_id: int,
service: RegistryService = Depends(get_service),
) -> list[dict[str, object]]:
try:
return [
asdict(decision)
for decision in service.list_review_decisions(repository_id)
]
except NotFoundError as exc:
raise HTTPException(status_code=404, detail=str(exc)) from exc
2026-04-26 08:45:00 +02:00
@app.get(
"/repos/{repository_id}/analysis-runs/{analysis_run_id}/review-decisions",
tags=["review"],
)
2026-04-26 00:24:02 +02:00
def list_analysis_run_review_decisions(
repository_id: int,
analysis_run_id: int,
service: RegistryService = Depends(get_service),
) -> list[dict[str, object]]:
try:
return [
asdict(decision)
for decision in service.list_review_decisions(
repository_id,
analysis_run_id,
)
]
except NotFoundError as exc:
raise HTTPException(status_code=404, detail=str(exc)) from exc
2026-04-26 08:45:00 +02:00
@app.get("/repos/{repository_id}/observed-facts", tags=["analysis"])
def list_observed_facts(
repository_id: int,
analysis_run_id: int | None = None,
service: RegistryService = Depends(get_service),
) -> list[dict[str, object]]:
try:
return [
asdict(fact)
for fact in service.list_observed_facts(repository_id, analysis_run_id)
]
except NotFoundError as exc:
raise HTTPException(status_code=404, detail=str(exc)) from exc
2026-04-26 08:45:00 +02:00
@app.get("/repos/{repository_id}/content-chunks", tags=["analysis"])
2026-04-26 02:47:10 +02:00
def list_content_chunks(
repository_id: int,
analysis_run_id: int | None = None,
service: RegistryService = Depends(get_service),
) -> list[dict[str, object]]:
try:
return [
asdict(chunk)
for chunk in service.list_content_chunks(repository_id, analysis_run_id)
]
except NotFoundError as exc:
raise HTTPException(status_code=404, detail=str(exc)) from exc
2026-04-26 08:45:00 +02:00
@app.get(
"/repos/{repository_id}/analysis-runs/{analysis_run_id}/content-chunks",
tags=["analysis"],
)
2026-04-26 02:47:10 +02:00
def list_analysis_run_content_chunks(
repository_id: int,
analysis_run_id: int,
service: RegistryService = Depends(get_service),
) -> list[dict[str, object]]:
try:
return [
asdict(chunk)
for chunk in service.list_content_chunks(repository_id, analysis_run_id)
]
except NotFoundError as exc:
raise HTTPException(status_code=404, detail=str(exc)) from exc
2026-04-26 08:45:00 +02:00
@app.get(
"/repos/{repository_id}/analysis-runs/{analysis_run_id}/candidate-graph",
tags=["review"],
)
def get_candidate_graph(
repository_id: int,
analysis_run_id: int,
service: RegistryService = Depends(get_service),
) -> dict[str, object]:
try:
return asdict(service.candidate_graph(repository_id, analysis_run_id))
except NotFoundError as exc:
raise HTTPException(status_code=404, detail=str(exc)) from exc
2026-04-26 08:45:00 +02:00
@app.post(
"/repos/{repository_id}/analysis-runs/{analysis_run_id}/candidate-graph/approve",
tags=["review"],
)
2026-04-25 22:46:22 +02:00
def approve_candidate_graph(
repository_id: int,
analysis_run_id: int,
payload: CandidateGraphApproval,
service: RegistryService = Depends(get_service),
) -> dict[str, object]:
try:
return asdict(
service.approve_candidate_graph(
repository_id,
analysis_run_id,
notes=payload.notes,
)
)
except NotFoundError as exc:
raise HTTPException(status_code=404, detail=str(exc)) from exc
2026-04-25 23:27:28 +02:00
@app.post(
"/repos/{repository_id}/analysis-runs/{analysis_run_id}"
2026-04-26 08:45:00 +02:00
"/candidate-abilities/{candidate_ability_id}/reject",
tags=["review"],
2026-04-25 23:27:28 +02:00
)
def reject_candidate_ability(
repository_id: int,
analysis_run_id: int,
candidate_ability_id: int,
payload: CandidateRejection,
service: RegistryService = Depends(get_service),
) -> dict[str, object]:
try:
return asdict(
service.reject_candidate_ability(
repository_id,
analysis_run_id,
candidate_ability_id,
notes=payload.notes,
)
)
except NotFoundError as exc:
raise HTTPException(status_code=404, detail=str(exc)) from exc
@app.post(
"/repos/{repository_id}/analysis-runs/{analysis_run_id}"
2026-04-26 08:45:00 +02:00
"/candidate-capabilities/{candidate_capability_id}/reject",
tags=["review"],
)
def reject_candidate_capability(
repository_id: int,
analysis_run_id: int,
candidate_capability_id: int,
payload: CandidateRejection,
service: RegistryService = Depends(get_service),
) -> dict[str, object]:
try:
return asdict(
service.reject_candidate_capability(
repository_id,
analysis_run_id,
candidate_capability_id,
notes=payload.notes,
)
)
except NotFoundError as exc:
raise HTTPException(status_code=404, detail=str(exc)) from exc
@app.post(
"/repos/{repository_id}/analysis-runs/{analysis_run_id}"
2026-04-26 08:45:00 +02:00
"/candidate-features/{candidate_feature_id}/reject",
tags=["review"],
)
def reject_candidate_feature(
repository_id: int,
analysis_run_id: int,
candidate_feature_id: int,
payload: CandidateRejection,
service: RegistryService = Depends(get_service),
) -> dict[str, object]:
try:
return asdict(
service.reject_candidate_feature(
repository_id,
analysis_run_id,
candidate_feature_id,
notes=payload.notes,
)
)
except NotFoundError as exc:
raise HTTPException(status_code=404, detail=str(exc)) from exc
@app.post(
"/repos/{repository_id}/analysis-runs/{analysis_run_id}"
2026-04-26 08:45:00 +02:00
"/candidate-evidence/{candidate_evidence_id}/reject",
tags=["review"],
)
def reject_candidate_evidence(
repository_id: int,
analysis_run_id: int,
candidate_evidence_id: int,
payload: CandidateRejection,
service: RegistryService = Depends(get_service),
) -> dict[str, object]:
try:
return asdict(
service.reject_candidate_evidence(
repository_id,
analysis_run_id,
candidate_evidence_id,
notes=payload.notes,
)
)
except NotFoundError as exc:
raise HTTPException(status_code=404, detail=str(exc)) from exc
@app.patch(
"/repos/{repository_id}/analysis-runs/{analysis_run_id}"
2026-04-26 08:45:00 +02:00
"/candidate-abilities/{candidate_ability_id}",
tags=["review"],
)
def edit_candidate_ability(
repository_id: int,
analysis_run_id: int,
candidate_ability_id: int,
payload: CandidateEdit,
service: RegistryService = Depends(get_service),
) -> dict[str, object]:
try:
return asdict(
service.edit_candidate_ability(
repository_id,
analysis_run_id,
candidate_ability_id,
**payload.model_dump(),
)
)
except NotFoundError as exc:
raise HTTPException(status_code=404, detail=str(exc)) from exc
@app.patch(
"/repos/{repository_id}/analysis-runs/{analysis_run_id}"
2026-04-26 08:45:00 +02:00
"/candidate-capabilities/{candidate_capability_id}",
tags=["review"],
)
def edit_candidate_capability(
repository_id: int,
analysis_run_id: int,
candidate_capability_id: int,
payload: CandidateEdit,
service: RegistryService = Depends(get_service),
) -> dict[str, object]:
try:
return asdict(
service.edit_candidate_capability(
repository_id,
analysis_run_id,
candidate_capability_id,
**payload.model_dump(),
)
)
except NotFoundError as exc:
raise HTTPException(status_code=404, detail=str(exc)) from exc
2026-04-25 23:44:25 +02:00
@app.post(
"/repos/{repository_id}/analysis-runs/{analysis_run_id}"
2026-04-26 08:45:00 +02:00
"/candidate-capabilities/{candidate_capability_id}/relink",
tags=["review"],
2026-04-25 23:44:25 +02:00
)
def relink_candidate_capability(
repository_id: int,
analysis_run_id: int,
candidate_capability_id: int,
payload: CandidateCapabilityRelink,
service: RegistryService = Depends(get_service),
) -> dict[str, object]:
try:
return asdict(
service.relink_candidate_capability(
repository_id,
analysis_run_id,
candidate_capability_id,
**payload.model_dump(),
)
)
except NotFoundError as exc:
raise HTTPException(status_code=404, detail=str(exc)) from exc
@app.post(
"/repos/{repository_id}/analysis-runs/{analysis_run_id}"
2026-04-26 08:45:00 +02:00
"/candidate-features/{candidate_feature_id}/relink",
tags=["review"],
2026-04-25 23:44:25 +02:00
)
def relink_candidate_feature(
repository_id: int,
analysis_run_id: int,
candidate_feature_id: int,
payload: CandidateLeafRelink,
service: RegistryService = Depends(get_service),
) -> dict[str, object]:
try:
return asdict(
service.relink_candidate_feature(
repository_id,
analysis_run_id,
candidate_feature_id,
**payload.model_dump(),
)
)
except NotFoundError as exc:
raise HTTPException(status_code=404, detail=str(exc)) from exc
@app.post(
"/repos/{repository_id}/analysis-runs/{analysis_run_id}"
2026-04-26 08:45:00 +02:00
"/candidate-evidence/{candidate_evidence_id}/relink",
tags=["review"],
2026-04-25 23:44:25 +02:00
)
def relink_candidate_evidence(
repository_id: int,
analysis_run_id: int,
candidate_evidence_id: int,
payload: CandidateLeafRelink,
service: RegistryService = Depends(get_service),
) -> dict[str, object]:
try:
return asdict(
service.relink_candidate_evidence(
repository_id,
analysis_run_id,
candidate_evidence_id,
**payload.model_dump(),
)
)
except NotFoundError as exc:
raise HTTPException(status_code=404, detail=str(exc)) from exc
@app.post(
"/repos/{repository_id}/analysis-runs/{analysis_run_id}"
2026-04-26 08:45:00 +02:00
"/candidate-abilities/{source_ability_id}/merge",
tags=["review"],
)
def merge_candidate_ability(
repository_id: int,
analysis_run_id: int,
source_ability_id: int,
payload: CandidateAbilityMerge,
service: RegistryService = Depends(get_service),
) -> dict[str, object]:
try:
return asdict(
service.merge_candidate_ability(
repository_id,
analysis_run_id,
source_ability_id,
**payload.model_dump(),
)
)
except (NotFoundError, ValueError) as exc:
raise HTTPException(status_code=400, detail=str(exc)) from exc
@app.post(
"/repos/{repository_id}/analysis-runs/{analysis_run_id}"
2026-04-26 08:45:00 +02:00
"/candidate-capabilities/{source_capability_id}/merge",
tags=["review"],
)
def merge_candidate_capability(
repository_id: int,
analysis_run_id: int,
source_capability_id: int,
payload: CandidateCapabilityMerge,
service: RegistryService = Depends(get_service),
) -> dict[str, object]:
try:
return asdict(
service.merge_candidate_capability(
repository_id,
analysis_run_id,
source_capability_id,
**payload.model_dump(),
)
)
except (NotFoundError, ValueError) as exc:
raise HTTPException(status_code=400, detail=str(exc)) from exc
@app.post(
"/repos/{repository_id}/analysis-runs/{analysis_run_id}"
2026-04-26 08:45:00 +02:00
"/candidate-features/{source_feature_id}/merge",
tags=["review"],
)
def merge_candidate_feature(
repository_id: int,
analysis_run_id: int,
source_feature_id: int,
payload: CandidateFeatureMerge,
service: RegistryService = Depends(get_service),
) -> dict[str, object]:
try:
return asdict(
service.merge_candidate_feature(
repository_id,
analysis_run_id,
source_feature_id,
**payload.model_dump(),
)
)
except (NotFoundError, ValueError) as exc:
raise HTTPException(status_code=400, detail=str(exc)) from exc
@app.post(
"/repos/{repository_id}/analysis-runs/{analysis_run_id}"
2026-04-26 08:45:00 +02:00
"/candidate-evidence/{source_evidence_id}/merge",
tags=["review"],
)
def merge_candidate_evidence(
repository_id: int,
analysis_run_id: int,
source_evidence_id: int,
payload: CandidateEvidenceMerge,
service: RegistryService = Depends(get_service),
) -> dict[str, object]:
try:
return asdict(
service.merge_candidate_evidence(
repository_id,
analysis_run_id,
source_evidence_id,
**payload.model_dump(),
)
)
except (NotFoundError, ValueError) as exc:
raise HTTPException(status_code=400, detail=str(exc)) from exc
2026-04-26 08:45:00 +02:00
@app.post("/repos/{repository_id}/abilities", status_code=201, tags=["registry"])
def create_ability(
repository_id: int,
payload: AbilityCreate,
service: RegistryService = Depends(get_service),
) -> dict[str, int]:
try:
ability_id = service.add_ability(repository_id, **payload.model_dump())
except NotFoundError as exc:
raise HTTPException(status_code=404, detail=str(exc)) from exc
return {"id": ability_id}
2026-04-26 08:45:00 +02:00
@app.patch("/repos/{repository_id}/abilities/{ability_id}", tags=["registry"])
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
2026-04-26 08:45:00 +02:00
@app.delete("/repos/{repository_id}/abilities/{ability_id}", tags=["registry"])
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
2026-04-26 08:45:00 +02:00
@app.post("/repos/{repository_id}/capabilities", status_code=201, tags=["registry"])
def create_capability(
repository_id: int,
payload: CapabilityCreate,
service: RegistryService = Depends(get_service),
) -> dict[str, int]:
try:
capability_id = service.add_capability(repository_id, **payload.model_dump())
except NotFoundError as exc:
raise HTTPException(status_code=404, detail=str(exc)) from exc
return {"id": capability_id}
2026-04-26 08:45:00 +02:00
@app.patch("/repos/{repository_id}/capabilities/{capability_id}", tags=["registry"])
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
2026-04-26 08:45:00 +02:00
@app.delete("/repos/{repository_id}/capabilities/{capability_id}", tags=["registry"])
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
2026-04-26 08:45:00 +02:00
@app.post("/repos/{repository_id}/features", status_code=201, tags=["registry"])
def create_feature(
repository_id: int,
payload: FeatureCreate,
service: RegistryService = Depends(get_service),
) -> dict[str, int]:
try:
feature_id = service.add_feature(repository_id, **payload.model_dump())
except NotFoundError as exc:
raise HTTPException(status_code=404, detail=str(exc)) from exc
return {"id": feature_id}
2026-04-26 08:45:00 +02:00
@app.patch("/repos/{repository_id}/features/{feature_id}", tags=["registry"])
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
2026-04-26 08:45:00 +02:00
@app.delete("/repos/{repository_id}/features/{feature_id}", tags=["registry"])
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
2026-04-26 08:45:00 +02:00
@app.post("/repos/{repository_id}/evidence", status_code=201, tags=["registry"])
def create_evidence(
repository_id: int,
payload: EvidenceCreate,
service: RegistryService = Depends(get_service),
) -> dict[str, int]:
try:
evidence_id = service.add_evidence(repository_id, **payload.model_dump())
except NotFoundError as exc:
raise HTTPException(status_code=404, detail=str(exc)) from exc
return {"id": evidence_id}
2026-04-26 08:45:00 +02:00
@app.patch("/repos/{repository_id}/evidence/{evidence_id}", tags=["registry"])
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
2026-04-26 08:45:00 +02:00
@app.delete("/repos/{repository_id}/evidence/{evidence_id}", tags=["registry"])
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
2026-04-26 08:45:00 +02:00
@app.get("/repos/{repository_id}/ability-map", tags=["registry"])
def get_ability_map(
repository_id: int,
service: RegistryService = Depends(get_service),
) -> dict[str, object]:
try:
return asdict(service.ability_map(repository_id))
except NotFoundError as exc:
raise HTTPException(status_code=404, detail=str(exc)) from exc
2026-04-26 08:45:00 +02:00
@app.get("/search", tags=["search"])
def search(
2026-04-26 08:45:00 +02:00
q: str = Query(
...,
description="Natural-language or keyword query over approved registry entries.",
examples=["classify email", "FastAPI health endpoint"],
),
status: str | None = Query(
default=None,
description="Filter repositories by registry status, such as indexed.",
),
language: str | None = Query(
default=None,
description="Filter by observed programming language.",
),
framework: str | None = Query(
default=None,
description="Filter by observed framework hint.",
),
ability: str | None = Query(
default=None,
description="Filter to results under an approved ability name.",
),
capability: str | None = Query(
default=None,
description="Filter to results under an approved capability name.",
),
service: RegistryService = Depends(get_service),
) -> list[dict[str, object]]:
2026-04-26 00:04:19 +02:00
return [
asdict(result)
for result in service.search(
q,
status=status,
language=language,
framework=framework,
2026-04-26 00:08:55 +02:00
ability=ability,
capability=capability,
2026-04-26 00:04:19 +02:00
)
]
2026-04-25 23:56:19 +02:00
2026-04-26 08:45:00 +02:00
@app.get("/abilities", tags=["search"])
2026-04-25 23:56:19 +02:00
def list_abilities(
service: RegistryService = Depends(get_service),
) -> list[dict[str, object]]:
return [asdict(ability) for ability in service.list_abilities()]
2026-04-26 08:45:00 +02:00
@app.get("/capabilities", tags=["search"])
2026-04-25 23:56:19 +02:00
def list_capabilities(
service: RegistryService = Depends(get_service),
) -> list[dict[str, object]]:
return [asdict(capability) for capability in service.list_capabilities()]