From a7f7113ce98c8ed11da3f58b1e597f5484a5b13f Mon Sep 17 00:00:00 2001 From: tegwick Date: Sun, 26 Apr 2026 12:45:49 +0200 Subject: [PATCH] usecase e2e tests --- README.md | 16 ++ src/repo_registry/core/service.py | 205 +++++++++++++++++ src/repo_registry/web_api/app.py | 61 +++++ src/repo_registry/web_api/schemas.py | 82 +++++++ tests/test_web_api.py | 331 +++++++++++++++++++++++++++ workplans/ImplementationWorkplan.md | 41 ++++ 6 files changed, 736 insertions(+) diff --git a/README.md b/README.md index e957151..a802bf1 100644 --- a/README.md +++ b/README.md @@ -169,6 +169,9 @@ DELETE /repos/{id}/evidence/{evidence_id} GET /abilities GET /capabilities GET /search?q=... +GET /repository-comparisons?repository_ids=1&repository_ids=2 +POST /capability-gaps +GET /repos/{id}/export ``` ## Agent API Loop @@ -205,3 +208,16 @@ Search results include `match_type`, `matched_field`, `confidence`, source/evidence context when the match comes from implementation evidence. The generated OpenAPI schema at `/openapi.json` and docs at `/docs` include typed response schemas and examples for the main agent-facing responses. + +Discovery helpers are available for production-readiness workflows that compare +approved profiles, find simple capability gaps, or export a registry entry: + +```bash +curl 'http://127.0.0.1:8000/repository-comparisons?repository_ids=1&repository_ids=2' + +curl -X POST http://127.0.0.1:8000/capability-gaps \ + -H 'content-type: application/json' \ + -d '{"desired_ability":"Business Email Routing","desired_capabilities":["Classify Incoming Email","Route Email to Team"],"repository_ids":[1,2]}' + +curl http://127.0.0.1:8000/repos/1/export +``` diff --git a/src/repo_registry/core/service.py b/src/repo_registry/core/service.py index 34e8b6b..a84034c 100644 --- a/src/repo_registry/core/service.py +++ b/src/repo_registry/core/service.py @@ -1,6 +1,7 @@ from __future__ import annotations from collections.abc import Sequence +from dataclasses import asdict from repo_registry.core.models import ( AbilitySummary, @@ -778,6 +779,210 @@ class RegistryService: def ability_map(self, repository_id: int) -> RepositoryAbilityMap: return self.store.get_ability_map(repository_id) + def compare_repositories(self, repository_ids: Sequence[int]) -> dict[str, object]: + maps = [self.store.get_ability_map(repository_id) for repository_id in repository_ids] + ability_groups: dict[str, list[dict[str, object]]] = {} + capability_groups: dict[str, list[dict[str, object]]] = {} + for ability_map in maps: + repository = ability_map.repository + for ability in ability_map.abilities: + ability_groups.setdefault(ability.name.lower(), []).append( + { + "repository_id": repository.id, + "repository_name": repository.name, + "confidence": ability.confidence, + "confidence_label": ability.confidence_label, + "capabilities": [ + { + "name": capability.name, + "confidence": capability.confidence, + "confidence_label": capability.confidence_label, + "evidence_count": len(capability.evidence), + } + for capability in ability.capabilities + ], + "_name": ability.name, + } + ) + for capability in ability.capabilities: + capability_groups.setdefault(capability.name.lower(), []).append( + { + "repository_id": repository.id, + "repository_name": repository.name, + "ability_name": ability.name, + "capability_name": capability.name, + } + ) + + abilities = [ + { + "name": repositories[0]["_name"], + "repositories": [ + { + key: value + for key, value in repository.items() + if key != "_name" + } + for repository in repositories + ], + } + for repositories in ability_groups.values() + ] + unique_capabilities = [ + entries[0] + for entries in capability_groups.values() + if len({entry["repository_id"] for entry in entries}) == 1 + ] + return { + "repositories": [asdict(ability_map.repository) for ability_map in maps], + "abilities": sorted(abilities, key=lambda item: item["name"]), + "unique_capabilities": sorted( + unique_capabilities, + key=lambda item: (item["repository_name"], item["capability_name"]), + ), + } + + def detect_capability_gaps( + self, + *, + desired_ability: str, + desired_capabilities: Sequence[str], + repository_ids: Sequence[int] | None = None, + ) -> dict[str, object]: + repositories = ( + [self.store.get_repository(repository_id) for repository_id in repository_ids] + if repository_ids is not None + else self.store.list_repositories() + ) + maps = [self.store.get_ability_map(repository.id) for repository in repositories] + desired = [capability.strip() for capability in desired_capabilities if capability.strip()] + capability_matches: dict[str, list[dict[str, object]]] = {name.lower(): [] for name in desired} + duplicate_index: dict[str, set[str]] = {} + weak: list[dict[str, object]] = [] + + for ability_map in maps: + repository = ability_map.repository + for ability in ability_map.abilities: + for capability in ability.capabilities: + key = capability.name.lower() + duplicate_index.setdefault(key, set()).add(repository.name) + if key in capability_matches: + capability_matches[key].append( + { + "repository_id": repository.id, + "repository_name": repository.name, + "capability": capability, + } + ) + strengths = {evidence.strength for evidence in capability.evidence} + if "strong" not in strengths: + weak.append( + { + "capability": capability.name, + "repository_id": repository.id, + "repository_name": repository.name, + "evidence_count": len(capability.evidence), + "strongest_evidence": self._strongest_evidence(strengths), + "confidence": capability.confidence, + "confidence_label": capability.confidence_label, + } + ) + + matched = [ + { + "capability": name, + "repositories": [ + match["repository_name"] + for match in capability_matches[name.lower()] + ], + } + for name in desired + if capability_matches[name.lower()] + ] + missing = [name for name in desired if not capability_matches[name.lower()]] + duplicates = [ + { + "capability": capability, + "repositories": sorted(repositories), + } + for capability, repositories in duplicate_index.items() + if len(repositories) > 1 and capability in capability_matches + ] + return { + "desired_ability": desired_ability, + "matched_capabilities": matched, + "missing_capabilities": missing, + "weakly_evidenced_capabilities": weak, + "duplicate_capabilities": duplicates, + } + + def export_registry_entry(self, repository_id: int) -> str: + ability_map = self.store.get_ability_map(repository_id) + lines = [ + "repository:", + f" name: {self._yaml_scalar(ability_map.repository.name)}", + f" url: {self._yaml_scalar(ability_map.repository.url)}", + f" branch: {self._yaml_scalar(ability_map.repository.branch)}", + f" status: {self._yaml_scalar(ability_map.repository.status)}", + "abilities:", + ] + for ability in ability_map.abilities: + lines.extend( + [ + f" - name: {self._yaml_scalar(ability.name)}", + f" description: {self._yaml_scalar(ability.description)}", + f" confidence: {ability.confidence}", + f" confidence_label: {self._yaml_scalar(ability.confidence_label)}", + " capabilities:", + ] + ) + for capability in ability.capabilities: + lines.extend( + [ + f" - name: {self._yaml_scalar(capability.name)}", + f" description: {self._yaml_scalar(capability.description)}", + f" confidence: {capability.confidence}", + f" confidence_label: {self._yaml_scalar(capability.confidence_label)}", + f" inputs: {self._yaml_list(capability.inputs)}", + f" outputs: {self._yaml_list(capability.outputs)}", + " features:", + ] + ) + for feature in capability.features: + lines.extend( + [ + f" - name: {self._yaml_scalar(feature.name)}", + f" type: {self._yaml_scalar(feature.type)}", + f" location: {self._yaml_scalar(feature.location)}", + f" confidence: {feature.confidence}", + f" confidence_label: {self._yaml_scalar(feature.confidence_label)}", + ] + ) + lines.append(" evidence:") + for evidence in capability.evidence: + lines.extend( + [ + f" - type: {self._yaml_scalar(evidence.type)}", + f" reference: {self._yaml_scalar(evidence.reference)}", + f" strength: {self._yaml_scalar(evidence.strength)}", + ] + ) + return "\n".join(lines) + "\n" + + def _strongest_evidence(self, strengths: set[str]) -> str | None: + for strength in ("strong", "medium", "weak"): + if strength in strengths: + return strength + return None + + def _yaml_list(self, values: Sequence[str]) -> str: + return "[" + ", ".join(self._yaml_scalar(value) for value in values) + "]" + + def _yaml_scalar(self, value: object) -> str: + text = "" if value is None else str(value) + escaped = text.replace("\\", "\\\\").replace('"', '\\"') + return f'"{escaped}"' + def search( self, query: str, diff --git a/src/repo_registry/web_api/app.py b/src/repo_registry/web_api/app.py index 1daeb72..a15f6f4 100644 --- a/src/repo_registry/web_api/app.py +++ b/src/repo_registry/web_api/app.py @@ -4,6 +4,7 @@ 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 @@ -27,6 +28,8 @@ from repo_registry.web_api.schemas import ( CandidateGraphResponse, CandidateLeafRelink, CandidateRejection, + CapabilityGapRequest, + CapabilityGapResponse, CapabilityCreate, CapabilitySummaryResponse, CapabilityUpdate, @@ -38,6 +41,7 @@ from repo_registry.web_api.schemas import ( IdResponse, ObservedFactResponse, RepositoryAbilityMapResponse, + RepositoryComparisonResponse, RepositoryCreate, RepositoryResponse, RepositoryUpdate, @@ -91,6 +95,7 @@ OPENAPI_TAGS = [ {"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( @@ -951,6 +956,62 @@ def get_ability_map( 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( diff --git a/src/repo_registry/web_api/schemas.py b/src/repo_registry/web_api/schemas.py index aca6570..00b2533 100644 --- a/src/repo_registry/web_api/schemas.py +++ b/src/repo_registry/web_api/schemas.py @@ -664,3 +664,85 @@ class CapabilitySummaryResponse(BaseModel): description: str confidence: float confidence_label: str + + +class CapabilityGapRequest(BaseModel): + desired_ability: str + desired_capabilities: list[str] + repository_ids: list[int] | None = None + + model_config = { + "json_schema_extra": { + "examples": [ + { + "desired_ability": "Business Email Routing", + "desired_capabilities": [ + "Classify Incoming Email", + "Route Email to Team", + ], + "repository_ids": [1, 2], + } + ] + } + } + + +class ComparedCapabilityResponse(BaseModel): + name: str + confidence: float + confidence_label: str + evidence_count: int + + +class ComparedAbilityRepositoryResponse(BaseModel): + repository_id: int + repository_name: str + confidence: float + confidence_label: str + capabilities: list[ComparedCapabilityResponse] + + +class ComparedAbilityResponse(BaseModel): + name: str + repositories: list[ComparedAbilityRepositoryResponse] + + +class UniqueCapabilityResponse(BaseModel): + repository_id: int + repository_name: str + ability_name: str + capability_name: str + + +class RepositoryComparisonResponse(BaseModel): + repositories: list[RepositoryResponse] + abilities: list[ComparedAbilityResponse] + unique_capabilities: list[UniqueCapabilityResponse] + + +class CapabilityGapMatchResponse(BaseModel): + capability: str + repositories: list[str] + + +class WeakCapabilityEvidenceResponse(BaseModel): + capability: str + repository_id: int + repository_name: str + evidence_count: int + strongest_evidence: str | None = None + confidence: float + confidence_label: str + + +class DuplicateCapabilityResponse(BaseModel): + capability: str + repositories: list[str] + + +class CapabilityGapResponse(BaseModel): + desired_ability: str + matched_capabilities: list[CapabilityGapMatchResponse] + missing_capabilities: list[str] + weakly_evidenced_capabilities: list[WeakCapabilityEvidenceResponse] + duplicate_capabilities: list[DuplicateCapabilityResponse] diff --git a/tests/test_web_api.py b/tests/test_web_api.py index 00c75d0..f066d44 100644 --- a/tests/test_web_api.py +++ b/tests/test_web_api.py @@ -19,6 +19,7 @@ def test_openapi_groups_agent_facing_endpoints(): "review", "registry", "search", + "discovery", } search_operation = schema["paths"]["/search"]["get"] assert search_operation["tags"] == ["search"] @@ -190,6 +191,163 @@ def test_api_manual_registry_loop(tmp_path): app.dependency_overrides.clear() +def test_api_compare_gap_and_export_use_cases(tmp_path): + def override_settings(): + return Settings( + database_path=str(tmp_path / "discovery.sqlite3"), + checkout_root=str(tmp_path / "discovery-checkouts"), + ) + + app.dependency_overrides[get_settings] = override_settings + client = TestClient(app) + try: + first = client.post( + "/repos", + json={ + "name": "MailRouter", + "url": "https://example.com/mail-router.git", + "description": "Routes customer email.", + }, + ).json() + second = client.post( + "/repos", + json={ + "name": "SupportRouter", + "url": "https://example.com/support-router.git", + "description": "Routes support requests.", + }, + ).json() + + first_ability = client.post( + f"/repos/{first['id']}/abilities", + json={ + "name": "Business Email Routing", + "description": "Route inbound messages.", + "confidence": 0.92, + }, + ).json()["id"] + first_classify = client.post( + f"/repos/{first['id']}/capabilities", + json={ + "ability_id": first_ability, + "name": "Classify Incoming Email", + "description": "Classify messages by intent.", + "confidence": 0.88, + }, + ).json()["id"] + client.post( + f"/repos/{first['id']}/evidence", + json={ + "capability_id": first_classify, + "type": "unit_test", + "reference": "tests/test_classify.py", + "strength": "strong", + }, + ) + client.post( + f"/repos/{first['id']}/capabilities", + json={ + "ability_id": first_ability, + "name": "Route Email to Team", + "description": "Route messages to owning teams.", + "confidence": 0.7, + }, + ) + + second_ability = client.post( + f"/repos/{second['id']}/abilities", + json={ + "name": "Business Email Routing", + "description": "Support routing workflows.", + "confidence": 0.8, + }, + ).json()["id"] + second_classify = client.post( + f"/repos/{second['id']}/capabilities", + json={ + "ability_id": second_ability, + "name": "Classify Incoming Email", + "description": "Classify support requests.", + "confidence": 0.6, + }, + ).json()["id"] + client.post( + f"/repos/{second['id']}/evidence", + json={ + "capability_id": second_classify, + "type": "documentation", + "reference": "README.md", + "strength": "medium", + }, + ) + client.post( + f"/repos/{second['id']}/capabilities", + json={ + "ability_id": second_ability, + "name": "Archive Email", + "description": "Archive resolved messages.", + "confidence": 0.75, + }, + ) + + comparison_response = client.get( + "/repository-comparisons", + params=[ + ("repository_ids", first["id"]), + ("repository_ids", second["id"]), + ], + ) + assert comparison_response.status_code == 200 + comparison = comparison_response.json() + assert {repo["name"] for repo in comparison["repositories"]} == { + "MailRouter", + "SupportRouter", + } + ability_entry = comparison["abilities"][0] + assert ability_entry["name"] == "Business Email Routing" + assert {repo["repository_name"] for repo in ability_entry["repositories"]} == { + "MailRouter", + "SupportRouter", + } + assert { + item["capability_name"] for item in comparison["unique_capabilities"] + } >= {"Route Email to Team", "Archive Email"} + + gaps_response = client.post( + "/capability-gaps", + json={ + "desired_ability": "Business Email Routing", + "desired_capabilities": [ + "Classify Incoming Email", + "Route Email to Team", + "German Benchmark Evaluation", + ], + "repository_ids": [first["id"], second["id"]], + }, + ) + assert gaps_response.status_code == 200 + gaps = gaps_response.json() + assert gaps["missing_capabilities"] == ["German Benchmark Evaluation"] + assert gaps["matched_capabilities"][0]["capability"] == ( + "Classify Incoming Email" + ) + assert { + weak["capability"] for weak in gaps["weakly_evidenced_capabilities"] + } >= {"Classify Incoming Email", "Route Email to Team"} + assert gaps["duplicate_capabilities"][0]["capability"] == ( + "classify incoming email" + ) + + export_response = client.get(f"/repos/{first['id']}/export") + assert export_response.status_code == 200 + assert export_response.headers["content-type"].startswith("application/x-yaml") + assert 'name: "MailRouter"' in export_response.text + assert 'name: "Classify Incoming Email"' in export_response.text + assert 'reference: "tests/test_classify.py"' in export_response.text + finally: + app.dependency_overrides.clear() + + def test_api_registers_repository_from_url_metadata(tmp_path): source = tmp_path / "metadata-api" source.mkdir() @@ -439,6 +597,179 @@ def test_api_analysis_run_loop(tmp_path): app.dependency_overrides.clear() +def test_api_source_linked_candidate_and_repo_update_loop(tmp_path): + source = tmp_path / "source-linked" + source.mkdir() + (source / "README.md").write_text( + "# Source Linked\n\nProvides operational HTTP status checks.\n", + encoding="utf-8", + ) + (source / "docs").mkdir() + (source / "docs" / "usage.md").write_text( + "# Usage\n\nCall the status endpoint before routing traffic.\n", + encoding="utf-8", + ) + (source / "examples").mkdir() + (source / "examples" / "status_client.py").write_text( + "print('GET /status')\n", + encoding="utf-8", + ) + (source / "tests").mkdir() + (source / "tests" / "test_status.py").write_text( + "def test_status(): pass\n", + encoding="utf-8", + ) + (source / "requirements.txt").write_text("fastapi\npytest\n", encoding="utf-8") + app_file = source / "app.py" + app_file.write_text( + "from fastapi import FastAPI\n" + "app = FastAPI()\n" + '@app.get("/status")\n' + "def status():\n" + " return {'status': 'ok'}\n", + encoding="utf-8", + ) + + def override_settings(): + return Settings( + database_path=str(tmp_path / "source-linked.sqlite3"), + checkout_root=str(tmp_path / "source-linked-checkouts"), + ) + + app.dependency_overrides[get_settings] = override_settings + client = TestClient(app) + try: + repository_response = client.post( + "/repos", + json={"name": "Source Linked", "url": str(source)}, + ) + assert repository_response.status_code == 201 + repository_id = repository_response.json()["id"] + + first_run_response = client.post( + f"/repos/{repository_id}/analysis-runs", + json={}, + ) + assert first_run_response.status_code == 201 + first_run = first_run_response.json() + first_run_id = first_run["analysis_run"]["id"] + assert first_run["snapshot"]["file_count"] == 6 + + facts_response = client.get(f"/repos/{repository_id}/observed-facts") + assert facts_response.status_code == 200 + facts = facts_response.json() + fact_keys = {(fact["kind"], fact["name"], fact["path"]) for fact in facts} + assert ("documentation", "README", "README.md") in fact_keys + assert ("documentation", "usage.md", "docs/usage.md") in fact_keys + assert ("example", "status_client.py", "examples/status_client.py") in fact_keys + assert ("test", "test_status.py", "tests/test_status.py") in fact_keys + assert ("framework", "FastAPI", "requirements.txt") in fact_keys + assert ("interface", "python route decorator", "app.py") in fact_keys + + candidate_response = client.get( + f"/repos/{repository_id}/analysis-runs/{first_run_id}/candidate-graph" + ) + assert candidate_response.status_code == 200 + candidate_graph = candidate_response.json() + interface_capability = next( + capability + for capability in candidate_graph["abilities"][0]["capabilities"] + if capability["name"] == "Expose Repository Interface" + ) + feature = next( + item + for item in interface_capability["features"] + if item["name"] == "GET /status" + ) + assert feature["type"] == "API" + assert feature["location"] == "app.py" + assert feature["source_refs"][0]["path"] == "app.py" + assert feature["source_refs"][0]["line"] == 3 + evidence_refs = { + (evidence["type"], evidence["reference"], evidence["strength"]) + for evidence in interface_capability["evidence"] + } + assert ("test", "tests/test_status.py", "strong") in evidence_refs + assert ("example", "examples/status_client.py", "strong") in evidence_refs + assert ("documentation", "README.md", "medium") in evidence_refs + + approve_response = client.post( + f"/repos/{repository_id}/analysis-runs/{first_run_id}" + "/candidate-graph/approve", + json={"notes": "Approved source-linked e2e fixture"}, + ) + assert approve_response.status_code == 200 + approved_map = approve_response.json() + approved_capability = next( + capability + for capability in approved_map["abilities"][0]["capabilities"] + if capability["name"] == "Expose Repository Interface" + ) + approved_feature = next( + item + for item in approved_capability["features"] + if item["name"] == "GET /status" + ) + assert approved_feature["source_refs"][0]["line"] == 3 + assert { + evidence["reference"] for evidence in approved_capability["evidence"] + } >= {"tests/test_status.py", "examples/status_client.py", "README.md"} + + evidence_search = client.get( + "/search", + params={"q": "test_status.py", "status": "indexed"}, + ) + assert evidence_search.status_code == 200 + evidence_result = evidence_search.json()[0] + assert evidence_result["match_type"] == "evidence" + assert evidence_result["evidence_level"] == "strong" + assert evidence_result["source_reference"] == "tests/test_status.py" + + app_file.write_text( + "from fastapi import FastAPI\n" + "app = FastAPI()\n" + '@app.get("/status")\n' + "def status():\n" + " return {'status': 'ok'}\n\n" + '@app.get("/ready")\n' + "def ready():\n" + " return {'ready': True}\n", + encoding="utf-8", + ) + second_run_response = client.post( + f"/repos/{repository_id}/analysis-runs", + json={}, + ) + assert second_run_response.status_code == 201 + second_run_id = second_run_response.json()["analysis_run"]["id"] + assert second_run_id != first_run_id + + second_candidate_response = client.get( + f"/repos/{repository_id}/analysis-runs/{second_run_id}/candidate-graph" + ) + assert second_candidate_response.status_code == 200 + second_features = { + feature["name"] + for ability in second_candidate_response.json()["abilities"] + for capability in ability["capabilities"] + for feature in capability["features"] + } + assert {"GET /status", "GET /ready"} <= second_features + + approved_after_reanalysis = client.get(f"/repos/{repository_id}/ability-map") + assert approved_after_reanalysis.status_code == 200 + approved_features = { + feature["name"] + for ability in approved_after_reanalysis.json()["abilities"] + for capability in ability["capabilities"] + for feature in capability["features"] + } + assert "GET /status" in approved_features + assert "GET /ready" not in approved_features + finally: + app.dependency_overrides.clear() + + def test_ui_register_analyze_and_approve_loop(tmp_path): source = tmp_path / "repo" source.mkdir() diff --git a/workplans/ImplementationWorkplan.md b/workplans/ImplementationWorkplan.md index 8f4caa8..3b2de56 100644 --- a/workplans/ImplementationWorkplan.md +++ b/workplans/ImplementationWorkplan.md @@ -262,6 +262,47 @@ Acceptance criteria: - Responses are stable enough for agent/tooling integration - OpenAPI docs describe all MVP endpoints +## 6.1 Implemented Status Checkpoint + +Status date: 2026-04-26 + +Current implementation baseline: + +- Milestone 0: implemented. FastAPI app, SQLite migrations, settings, health endpoint, README development flow, and pytest harness are in place. +- Milestone 1: implemented. Repository CRUD, manual ability/capability/feature/evidence CRUD, ability-map API, and server-rendered repository profile UI are in place. +- Milestone 2: implemented for local paths and Git URLs. Registration can import metadata, analysis records snapshots and observed facts, and failures are captured on analysis runs. +- Milestone 3: implemented for deterministic extraction plus optional LLM-assisted extraction. Analysis stores content chunks, source-linked candidates, candidate evidence, confidence scores, and confidence labels. +- Milestone 4: implemented. Candidate approval, reject, edit, relink, merge, review decisions, and indexed repository publication are supported through API and UI paths. +- Milestone 5: partially implemented. Text search, filters, search UI, ability-map drill-down, and evidence/source context are implemented. pgvector-backed semantic search remains future work. +- Milestone 6: implemented for the MVP and review workflow. Agent-facing endpoints have typed OpenAPI response schemas, examples, tags, and docs smoke coverage. + +Use case coverage status: + +| ID | Use Case | Implementation Status | E2E Coverage Status | +| --- | --- | --- | --- | +| UC-01 | Register Git Repository | Implemented through API and UI. | Covered by API and UI registration loops. | +| UC-02 | Import Repository Metadata | Implemented from repository files when name/description are omitted. | Covered by API and service metadata tests. | +| UC-03 | Analyze Repository Structure | Implemented by deterministic scanner and analysis runs. | Covered by API, service, scanner, and UI analysis loops. | +| UC-04 | Extract Candidate Abilities | Implemented by deterministic generator and optional LLM mapper. | Covered by API/service analysis loops and LLM extraction tests. | +| UC-05 | Extract Candidate Capabilities | Implemented by deterministic generator and optional LLM mapper. | Covered by API/service analysis loops and LLM extraction tests. | +| UC-06 | Extract Candidate Features | Implemented with detected interfaces, languages, frameworks, docs, tests, and manifests. | Covered by API/service analysis loops plus source-linked fixture e2e assertions. | +| UC-07 | Link Features to Code Locations | Implemented through feature locations and source references. | Covered by service approval tests and API e2e assertions for source paths/lines. | +| UC-08 | Attach Evidence to Capabilities | Implemented for candidate and approved evidence. | Covered by API/UI review, manual registry tests, and source-linked approved evidence e2e assertions. | +| UC-09 | Review and Approve Analysis | Implemented through approve, edit, reject, relink, merge, and review decisions. | Covered by API/service/UI review tests. | +| UC-10 | Search Repositories by Need | Implemented with text search and structured filters. | Covered by API/service/UI search tests. Semantic search remains future work. | +| UC-11 | Inspect Repository Ability Map | Implemented through API and UI profile drill-down. | Covered by API/service/UI ability-map tests. | +| UC-12 | Compare Repositories | Implemented as a read-only API comparison over approved ability maps. | Covered by API e2e comparison test. | +| UC-13 | Detect Capability Gaps | Implemented as a read-only API gap report over desired capabilities and approved maps. | Covered by API e2e gap-analysis test. | +| UC-14 | Expose Registry via API | Implemented for MVP plus review workflow. | Covered by API contract, OpenAPI, and docs smoke tests. | +| UC-15 | Update Registry After Repo Change | Partially implemented by rerunning analysis; no explicit diff/change-review workflow yet. | Covered for rerun behavior by API e2e: second analysis records new candidates without corrupting approved profile. | +| UC-16 | Export Registry Entry | Implemented as YAML export for approved registry entries. | Covered by API e2e export test. | + +Immediate production-readiness test focus: + +1. If UC-15 becomes a production priority, add an explicit diff/change-review model instead of relying only on rerun analysis. +2. Broaden fixture coverage over time for README-only, Python CLI, FastAPI, JavaScript/TypeScript, tests/examples, and weak-doc repositories. +3. Add richer UI affordances for comparison, gap analysis, and export if these discovery endpoints become curator-facing workflows. + ## 7. Initial Database Shape Start with tables for: