from __future__ import annotations from dataclasses import asdict from pathlib import Path from fastapi import Depends, FastAPI, HTTPException from pydantic import BaseModel, Field from repo_registry.core.service import RegistryService from repo_registry.repo_ingestion.git import GitIngestionService from repo_registry.storage.sqlite import NotFoundError, RegistryStore class Settings(BaseModel): database_path: str = Field(default="var/repo-registry.sqlite3") checkout_root: str = Field(default="var/checkouts") 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() return RegistryService(store, ingestion=GitIngestionService(settings.checkout_root)) class RepositoryCreate(BaseModel): url: str name: str | None = None description: str | None = None branch: str = "main" model_config = { "json_schema_extra": { "examples": [ { "url": "https://github.com/example/repository.git", "name": "Example Repository", "description": "Optional human-readable repository summary.", "branch": "main", } ] } } class AbilityCreate(BaseModel): name: str description: str = "" confidence: float = Field(default=1.0, ge=0.0, le=1.0) model_config = { "json_schema_extra": { "examples": [ { "name": "Business Email Routing", "description": "Route inbound messages to the right team.", "confidence": 0.92, } ] } } 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) 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 FeatureCreate(BaseModel): capability_id: int name: str type: str location: str = "" confidence: float = Field(default=1.0, ge=0.0, le=1.0) 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 EvidenceCreate(BaseModel): capability_id: int type: str reference: str strength: str = "medium" model_config = { "json_schema_extra": { "examples": [ { "capability_id": 1, "type": "unit_test", "reference": "tests/test_email_classification.py", "strength": "strong", } ] } } class AnalysisRunCreate(BaseModel): source_path: str | None = None model_config = { "json_schema_extra": { "examples": [ {}, {"source_path": "/path/to/local/repository"}, ] } } class CandidateGraphApproval(BaseModel): notes: str = "" model_config = { "json_schema_extra": { "examples": [{"notes": "Approved after curator review."}] } } class CandidateRejection(BaseModel): notes: str = "" model_config = { "json_schema_extra": { "examples": [{"notes": "Rejected because the claim is too generic."}] } } class CandidateEdit(BaseModel): name: str description: str = "" confidence: float = Field(default=0.5, ge=0.0, le=1.0) notes: str = "" 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.", } ] } } class CandidateCapabilityRelink(BaseModel): target_ability_id: int notes: str = "" model_config = { "json_schema_extra": { "examples": [ {"target_ability_id": 2, "notes": "Move under operational ability."} ] } } class CandidateLeafRelink(BaseModel): target_capability_id: int notes: str = "" model_config = { "json_schema_extra": { "examples": [ { "target_capability_id": 3, "notes": "Evidence supports a different capability.", } ] } } class CandidateAbilityMerge(BaseModel): target_ability_id: int notes: str = "" model_config = { "json_schema_extra": { "examples": [ {"target_ability_id": 2, "notes": "Duplicate ability wording."} ] } } class CandidateCapabilityMerge(BaseModel): target_capability_id: int notes: str = "" model_config = { "json_schema_extra": { "examples": [ {"target_capability_id": 3, "notes": "Duplicate capability."} ] } } class CandidateFeatureMerge(BaseModel): target_feature_id: int notes: str = "" model_config = { "json_schema_extra": { "examples": [{"target_feature_id": 4, "notes": "Duplicate route."}] } } class CandidateEvidenceMerge(BaseModel): target_evidence_id: int notes: str = "" model_config = { "json_schema_extra": { "examples": [{"target_evidence_id": 5, "notes": "Duplicate evidence."}] } } app = FastAPI(title="Repository Ability Registry", version="0.1.0") from repo_registry.web_ui.views import router as ui_router app.include_router(ui_router) @app.get("/health") def health() -> dict[str, str]: return {"status": "ok"} @app.post("/repos", status_code=201) def create_repository( payload: RepositoryCreate, service: RegistryService = Depends(get_service), ) -> dict[str, object]: try: repository = service.register_repository(**payload.model_dump()) except (RuntimeError, ValueError) as exc: raise HTTPException(status_code=400, detail=str(exc)) from exc return asdict(repository) @app.get("/repos") def list_repositories( service: RegistryService = Depends(get_service), ) -> list[dict[str, object]]: return [asdict(repository) for repository in service.list_repositories()] @app.get("/repos/{repository_id}") 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 @app.post("/repos/{repository_id}/analysis-runs", status_code=201) 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) @app.get("/repos/{repository_id}/analysis-runs") 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 @app.get("/repos/{repository_id}/analysis-runs/{analysis_run_id}") 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 @app.get("/repos/{repository_id}/observed-facts") 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 @app.get("/repos/{repository_id}/analysis-runs/{analysis_run_id}/candidate-graph") 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 @app.post("/repos/{repository_id}/analysis-runs/{analysis_run_id}/candidate-graph/approve") 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 @app.post( "/repos/{repository_id}/analysis-runs/{analysis_run_id}" "/candidate-abilities/{candidate_ability_id}/reject" ) 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}" "/candidate-capabilities/{candidate_capability_id}/reject" ) 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}" "/candidate-features/{candidate_feature_id}/reject" ) 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}" "/candidate-evidence/{candidate_evidence_id}/reject" ) 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}" "/candidate-abilities/{candidate_ability_id}" ) 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}" "/candidate-capabilities/{candidate_capability_id}" ) 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 @app.post( "/repos/{repository_id}/analysis-runs/{analysis_run_id}" "/candidate-capabilities/{candidate_capability_id}/relink" ) 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}" "/candidate-features/{candidate_feature_id}/relink" ) 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}" "/candidate-evidence/{candidate_evidence_id}/relink" ) 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}" "/candidate-abilities/{source_ability_id}/merge" ) 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}" "/candidate-capabilities/{source_capability_id}/merge" ) 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}" "/candidate-features/{source_feature_id}/merge" ) 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}" "/candidate-evidence/{source_evidence_id}/merge" ) 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 @app.post("/repos/{repository_id}/abilities", status_code=201) 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} @app.post("/repos/{repository_id}/capabilities", status_code=201) 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} @app.post("/repos/{repository_id}/features", status_code=201) 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} @app.post("/repos/{repository_id}/evidence", status_code=201) 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} @app.get("/repos/{repository_id}/ability-map") 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 @app.get("/search") def search( q: str, status: str | None = None, language: str | None = None, framework: str | None = None, ability: str | None = None, capability: str | None = None, service: RegistryService = Depends(get_service), ) -> list[dict[str, object]]: return [ asdict(result) for result in service.search( q, status=status, language=language, framework=framework, ability=ability, capability=capability, ) ] @app.get("/abilities") def list_abilities( service: RegistryService = Depends(get_service), ) -> list[dict[str, object]]: return [asdict(ability) for ability in service.list_abilities()] @app.get("/capabilities") def list_capabilities( service: RegistryService = Depends(get_service), ) -> list[dict[str, object]]: return [asdict(capability) for capability in service.list_capabilities()]