from __future__ import annotations from dataclasses import asdict from pathlib import Path from fastapi import Depends, FastAPI, HTTPException, Query from fastapi.responses import PlainTextResponse from pydantic import Field from pydantic_settings import BaseSettings, SettingsConfigDict from repo_registry.core.service import RegistryService from repo_registry.llm_extraction import LLMCandidateExtractor, create_llm_connect_adapter from repo_registry.repo_ingestion.git import GitIngestionService from repo_registry.storage.sqlite import NotFoundError, RegistryStore from repo_registry.web_api.schemas import ( AbilityCreate, AbilitySummaryResponse, AbilityUpdate, AnalysisRunChangeApproval, AnalysisRunCreate, AnalysisRunDiffResponse, AnalysisRunResponse, CandidateAbilityMerge, CandidateCapabilityMerge, CandidateCapabilityRelink, CandidateEdit, CandidateEvidenceMerge, CandidateFeatureMerge, CandidateGraphApproval, CandidateGraphResponse, CandidateLeafRelink, CandidateRejection, CapabilityGapRequest, CapabilityGapResponse, CapabilityCreate, CapabilitySummaryResponse, CapabilityUpdate, ContentChunkResponse, EvidenceCreate, EvidenceUpdate, FeatureCreate, FeatureUpdate, IdResponse, ObservedFactResponse, RepositoryAbilityMapResponse, RepositoryComparisonResponse, RepositoryCreate, RepositoryResponse, RepositoryUpdate, ReviewDecisionResponse, ScanSummaryResponse, SearchResultResponse, ) class Settings(BaseSettings): model_config = SettingsConfigDict(env_prefix="REPO_REGISTRY_") database_path: str = Field(default="var/repo-registry.sqlite3") 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, ) 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."}, {"name": "discovery", "description": "Comparison, gap analysis, and export helpers."}, ] app = FastAPI( title="Repository Ability Registry", version="0.1.0", description=API_DESCRIPTION, openapi_tags=OPENAPI_TAGS, ) from repo_registry.web_ui.views import router as ui_router app.include_router(ui_router) @app.get("/health", tags=["health"]) def health() -> dict[str, str]: return {"status": "ok"} @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()) except (RuntimeError, ValueError) as exc: raise HTTPException(status_code=400, detail=str(exc)) from exc return asdict(repository) @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()] @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 @app.patch( "/repos/{repository_id}", tags=["repositories"], response_model=RepositoryResponse, ) 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 @app.delete("/repos/{repository_id}", status_code=204, tags=["repositories"]) 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 @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) @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 @app.get( "/repos/{repository_id}/analysis-runs/{analysis_run_id}", tags=["analysis"], response_model=AnalysisRunResponse, ) 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}/analysis-runs/{base_analysis_run_id}/diff/{target_analysis_run_id}", tags=["review"], response_model=AnalysisRunDiffResponse, ) def diff_analysis_runs( repository_id: int, base_analysis_run_id: int, target_analysis_run_id: int, service: RegistryService = Depends(get_service), ) -> dict[str, object]: try: return asdict( service.diff_analysis_runs( repository_id, base_analysis_run_id, target_analysis_run_id, ) ) except NotFoundError as exc: raise HTTPException(status_code=404, detail=str(exc)) from exc @app.get( "/repos/{repository_id}/review-decisions", tags=["review"], response_model=list[ReviewDecisionResponse], ) 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 @app.get( "/repos/{repository_id}/analysis-runs/{analysis_run_id}/review-decisions", tags=["review"], response_model=list[ReviewDecisionResponse], ) 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 @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 @app.get( "/repos/{repository_id}/content-chunks", tags=["analysis"], response_model=list[ContentChunkResponse], ) 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 @app.get( "/repos/{repository_id}/analysis-runs/{analysis_run_id}/content-chunks", tags=["analysis"], response_model=list[ContentChunkResponse], ) 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 @app.get( "/repos/{repository_id}/analysis-runs/{analysis_run_id}/candidate-graph", tags=["review"], response_model=CandidateGraphResponse, ) 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", tags=["review"], response_model=RepositoryAbilityMapResponse, ) 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}/changes/approve", tags=["review"], response_model=RepositoryAbilityMapResponse, ) def approve_analysis_run_changes( repository_id: int, analysis_run_id: int, payload: AnalysisRunChangeApproval, service: RegistryService = Depends(get_service), ) -> dict[str, object]: try: return asdict( service.approve_analysis_run_changes( 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", tags=["review"], response_model=CandidateGraphResponse, ) 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", tags=["review"], 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}" "/candidate-features/{candidate_feature_id}/reject", tags=["review"], 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}" "/candidate-evidence/{candidate_evidence_id}/reject", tags=["review"], 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}" "/candidate-abilities/{candidate_ability_id}", tags=["review"], 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}" "/candidate-capabilities/{candidate_capability_id}", tags=["review"], 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 @app.post( "/repos/{repository_id}/analysis-runs/{analysis_run_id}" "/candidate-capabilities/{candidate_capability_id}/relink", tags=["review"], response_model=CandidateGraphResponse, ) 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", tags=["review"], response_model=CandidateGraphResponse, ) 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", tags=["review"], response_model=CandidateGraphResponse, ) 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", tags=["review"], 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}" "/candidate-capabilities/{source_capability_id}/merge", tags=["review"], 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}" "/candidate-features/{source_feature_id}/merge", tags=["review"], 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}" "/candidate-evidence/{source_evidence_id}/merge", tags=["review"], 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 @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} @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 @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 @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} @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 @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 @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} @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 @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 @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} @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 @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 @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 @app.get( "/repos/{repository_id}/export", tags=["discovery"], response_class=PlainTextResponse, responses={ 200: { "content": {"application/x-yaml": {}}, "description": "YAML registry export suitable for repo-abilities.yaml.", } }, ) def export_repository_registry_entry( repository_id: int, service: RegistryService = Depends(get_service), ) -> PlainTextResponse: try: content = service.export_registry_entry(repository_id) except NotFoundError as exc: raise HTTPException(status_code=404, detail=str(exc)) from exc return PlainTextResponse(content, media_type="application/x-yaml") @app.get( "/repository-comparisons", tags=["discovery"], response_model=RepositoryComparisonResponse, ) def compare_repositories( repository_ids: list[int] = Query( ..., description="Repository ids to compare by approved abilities and capabilities.", min_length=2, ), service: RegistryService = Depends(get_service), ) -> dict[str, object]: try: return service.compare_repositories(repository_ids) except NotFoundError as exc: raise HTTPException(status_code=404, detail=str(exc)) from exc @app.post( "/capability-gaps", tags=["discovery"], response_model=CapabilityGapResponse, ) def detect_capability_gaps( payload: CapabilityGapRequest, service: RegistryService = Depends(get_service), ) -> dict[str, object]: try: return service.detect_capability_gaps(**payload.model_dump()) except NotFoundError as exc: raise HTTPException(status_code=404, detail=str(exc)) from exc @app.get("/search", tags=["search"], response_model=list[SearchResultResponse]) def search( 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]]: return [ asdict(result) for result in service.search( q, status=status, language=language, framework=framework, ability=ability, capability=capability, ) ] @app.get("/abilities", tags=["search"], response_model=list[AbilitySummaryResponse]) def list_abilities( service: RegistryService = Depends(get_service), ) -> list[dict[str, object]]: return [asdict(ability) for ability in service.list_abilities()] @app.get( "/capabilities", tags=["search"], response_model=list[CapabilitySummaryResponse], ) def list_capabilities( service: RegistryService = Depends(get_service), ) -> list[dict[str, object]]: return [asdict(capability) for capability in service.list_capabilities()]