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

1472 lines
39 KiB
Python
Raw Normal View History

from __future__ import annotations
from dataclasses import asdict
from pathlib import Path
2026-04-26 09:12:27 +02:00
from typing import Any
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 09:12:27 +02:00
class RepositoryResponse(BaseModel):
id: int
name: str
url: str
description: str | None
branch: str
status: str
class RepositorySnapshotResponse(BaseModel):
id: int
repository_id: int
commit_hash: str
branch: str
source_path: str
file_count: int
class AnalysisRunResponse(BaseModel):
id: int
repository_id: int
snapshot_id: int | None
status: str
started_at: str
completed_at: str | None
error_message: str | None
scanner_version: str
class ReviewDecisionResponse(BaseModel):
id: int
repository_id: int
analysis_run_id: int | None
action: str
notes: str
created_at: str
class ObservedFactResponse(BaseModel):
id: int
repository_id: int
analysis_run_id: int
snapshot_id: int | None
kind: str
path: str
name: str
value: str
metadata: dict[str, Any]
class ContentChunkResponse(BaseModel):
id: int
repository_id: int
analysis_run_id: int
snapshot_id: int | None
path: str
kind: str
start_line: int
end_line: int
text: str
class ScanSummaryResponse(BaseModel):
analysis_run: AnalysisRunResponse
snapshot: RepositorySnapshotResponse | None
facts: list[ObservedFactResponse]
class SourceReferenceResponse(BaseModel):
fact_id: int | None
path: str
kind: str
name: str
line: int | None = None
class CandidateEvidenceResponse(BaseModel):
id: int
type: str
reference: str
strength: str
status: str
source_refs: list[SourceReferenceResponse]
class CandidateFeatureResponse(BaseModel):
id: int
name: str
type: str
location: str
confidence: float
status: str
source_refs: list[SourceReferenceResponse]
confidence_label: str
class CandidateCapabilityResponse(BaseModel):
id: int
name: str
description: str
inputs: list[str]
outputs: list[str]
confidence: float
status: str
source_refs: list[SourceReferenceResponse]
confidence_label: str
features: list[CandidateFeatureResponse]
evidence: list[CandidateEvidenceResponse]
class CandidateAbilityResponse(BaseModel):
id: int
name: str
description: str
confidence: float
status: str
source_refs: list[SourceReferenceResponse]
confidence_label: str
capabilities: list[CandidateCapabilityResponse]
class CandidateGraphResponse(BaseModel):
repository: RepositoryResponse
analysis_run: AnalysisRunResponse
abilities: list[CandidateAbilityResponse]
class EvidenceResponse(BaseModel):
id: int
type: str
reference: str
strength: str
source_refs: list[SourceReferenceResponse]
class FeatureResponse(BaseModel):
id: int
name: str
type: str
location: str
confidence: float
confidence_label: str
source_refs: list[SourceReferenceResponse]
class CapabilityResponse(BaseModel):
id: int
name: str
description: str
inputs: list[str]
outputs: list[str]
confidence: float
confidence_label: str
features: list[FeatureResponse]
evidence: list[EvidenceResponse]
class AbilityResponse(BaseModel):
id: int
name: str
description: str
confidence: float
confidence_label: str
capabilities: list[CapabilityResponse]
class RepositoryAbilityMapResponse(BaseModel):
repository: RepositoryResponse
abilities: list[AbilityResponse]
class IdResponse(BaseModel):
id: int
class SearchResultResponse(BaseModel):
repository_id: int
repository_name: str
match_type: str
match_name: str
confidence: float
confidence_label: str
match_description: str
matched_field: str
ability_id: int | None = None
ability_name: str | None = None
capability_id: int | None = None
capability_name: str | None = None
evidence_level: str | None = None
source_reference: str | None = None
class AbilitySummaryResponse(BaseModel):
id: int
repository_id: int
repository_name: str
name: str
description: str
confidence: float
confidence_label: str
class CapabilitySummaryResponse(BaseModel):
id: int
repository_id: int
repository_name: str
ability_id: int
ability_name: str
name: str
description: str
confidence: float
confidence_label: str
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 09:12:27 +02:00
@app.post(
"/repos",
status_code=201,
tags=["repositories"],
response_model=RepositoryResponse,
)
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 09:12:27 +02:00
@app.get("/repos", tags=["repositories"], response_model=list[RepositoryResponse])
def list_repositories(
service: RegistryService = Depends(get_service),
) -> list[dict[str, object]]:
return [asdict(repository) for repository in service.list_repositories()]
2026-04-26 09:12:27 +02:00
@app.get(
"/repos/{repository_id}",
tags=["repositories"],
response_model=RepositoryResponse,
)
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 09:12:27 +02:00
@app.patch(
"/repos/{repository_id}",
tags=["repositories"],
response_model=RepositoryResponse,
)
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 09:12:27 +02:00
@app.post(
"/repos/{repository_id}/analysis-runs",
status_code=201,
tags=["analysis"],
response_model=ScanSummaryResponse,
)
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 09:12:27 +02:00
@app.get(
"/repos/{repository_id}/analysis-runs",
tags=["analysis"],
response_model=list[AnalysisRunResponse],
)
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 09:12:27 +02:00
@app.get(
"/repos/{repository_id}/analysis-runs/{analysis_run_id}",
tags=["analysis"],
response_model=AnalysisRunResponse,
)
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 09:12:27 +02:00
@app.get(
"/repos/{repository_id}/review-decisions",
tags=["review"],
response_model=list[ReviewDecisionResponse],
)
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 09:12:27 +02:00
response_model=list[ReviewDecisionResponse],
2026-04-26 08:45:00 +02:00
)
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 09:12:27 +02:00
@app.get(
"/repos/{repository_id}/observed-facts",
tags=["analysis"],
response_model=list[ObservedFactResponse],
)
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 09:12:27 +02:00
@app.get(
"/repos/{repository_id}/content-chunks",
tags=["analysis"],
response_model=list[ContentChunkResponse],
)
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 09:12:27 +02:00
response_model=list[ContentChunkResponse],
2026-04-26 08:45:00 +02:00
)
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"],
2026-04-26 09:12:27 +02:00
response_model=CandidateGraphResponse,
2026-04-26 08:45:00 +02:00
)
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-26 09:12:27 +02:00
response_model=RepositoryAbilityMapResponse,
2026-04-26 08:45:00 +02:00
)
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-26 09:12:27 +02:00
response_model=CandidateGraphResponse,
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"],
2026-04-26 09:12:27 +02:00
response_model=CandidateGraphResponse,
)
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"],
2026-04-26 09:12:27 +02:00
response_model=CandidateGraphResponse,
)
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"],
2026-04-26 09:12:27 +02:00
response_model=CandidateGraphResponse,
)
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"],
2026-04-26 09:12:27 +02:00
response_model=CandidateGraphResponse,
)
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"],
2026-04-26 09:12:27 +02:00
response_model=CandidateGraphResponse,
)
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-26 09:12:27 +02:00
response_model=CandidateGraphResponse,
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-26 09:12:27 +02:00
response_model=CandidateGraphResponse,
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-26 09:12:27 +02:00
response_model=CandidateGraphResponse,
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"],
2026-04-26 09:12:27 +02:00
response_model=CandidateGraphResponse,
)
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"],
2026-04-26 09:12:27 +02:00
response_model=CandidateGraphResponse,
)
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"],
2026-04-26 09:12:27 +02:00
response_model=CandidateGraphResponse,
)
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"],
2026-04-26 09:12:27 +02:00
response_model=CandidateGraphResponse,
)
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 09:12:27 +02:00
@app.post(
"/repos/{repository_id}/abilities",
status_code=201,
tags=["registry"],
response_model=IdResponse,
)
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 09:12:27 +02:00
@app.patch(
"/repos/{repository_id}/abilities/{ability_id}",
tags=["registry"],
response_model=RepositoryAbilityMapResponse,
)
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 09:12:27 +02:00
@app.delete(
"/repos/{repository_id}/abilities/{ability_id}",
tags=["registry"],
response_model=RepositoryAbilityMapResponse,
)
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 09:12:27 +02:00
@app.post(
"/repos/{repository_id}/capabilities",
status_code=201,
tags=["registry"],
response_model=IdResponse,
)
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 09:12:27 +02:00
@app.patch(
"/repos/{repository_id}/capabilities/{capability_id}",
tags=["registry"],
response_model=RepositoryAbilityMapResponse,
)
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 09:12:27 +02:00
@app.delete(
"/repos/{repository_id}/capabilities/{capability_id}",
tags=["registry"],
response_model=RepositoryAbilityMapResponse,
)
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 09:12:27 +02:00
@app.post(
"/repos/{repository_id}/features",
status_code=201,
tags=["registry"],
response_model=IdResponse,
)
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 09:12:27 +02:00
@app.patch(
"/repos/{repository_id}/features/{feature_id}",
tags=["registry"],
response_model=RepositoryAbilityMapResponse,
)
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 09:12:27 +02:00
@app.delete(
"/repos/{repository_id}/features/{feature_id}",
tags=["registry"],
response_model=RepositoryAbilityMapResponse,
)
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 09:12:27 +02:00
@app.post(
"/repos/{repository_id}/evidence",
status_code=201,
tags=["registry"],
response_model=IdResponse,
)
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 09:12:27 +02:00
@app.patch(
"/repos/{repository_id}/evidence/{evidence_id}",
tags=["registry"],
response_model=RepositoryAbilityMapResponse,
)
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 09:12:27 +02:00
@app.delete(
"/repos/{repository_id}/evidence/{evidence_id}",
tags=["registry"],
response_model=RepositoryAbilityMapResponse,
)
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 09:12:27 +02:00
@app.get(
"/repos/{repository_id}/ability-map",
tags=["registry"],
response_model=RepositoryAbilityMapResponse,
)
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 09:12:27 +02:00
@app.get("/search", tags=["search"], response_model=list[SearchResultResponse])
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 09:12:27 +02:00
@app.get("/abilities", tags=["search"], response_model=list[AbilitySummaryResponse])
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 09:12:27 +02:00
@app.get(
"/capabilities",
tags=["search"],
response_model=list[CapabilitySummaryResponse],
)
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()]