usecase e2e tests

This commit is contained in:
2026-04-26 12:45:49 +02:00
parent c03a1e93b0
commit a7f7113ce9
6 changed files with 736 additions and 0 deletions

View File

@@ -169,6 +169,9 @@ DELETE /repos/{id}/evidence/{evidence_id}
GET /abilities GET /abilities
GET /capabilities GET /capabilities
GET /search?q=... GET /search?q=...
GET /repository-comparisons?repository_ids=1&repository_ids=2
POST /capability-gaps
GET /repos/{id}/export
``` ```
## Agent API Loop ## 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. source/evidence context when the match comes from implementation evidence.
The generated OpenAPI schema at `/openapi.json` and docs at `/docs` include The generated OpenAPI schema at `/openapi.json` and docs at `/docs` include
typed response schemas and examples for the main agent-facing responses. 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
```

View File

@@ -1,6 +1,7 @@
from __future__ import annotations from __future__ import annotations
from collections.abc import Sequence from collections.abc import Sequence
from dataclasses import asdict
from repo_registry.core.models import ( from repo_registry.core.models import (
AbilitySummary, AbilitySummary,
@@ -778,6 +779,210 @@ class RegistryService:
def ability_map(self, repository_id: int) -> RepositoryAbilityMap: def ability_map(self, repository_id: int) -> RepositoryAbilityMap:
return self.store.get_ability_map(repository_id) 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( def search(
self, self,
query: str, query: str,

View File

@@ -4,6 +4,7 @@ from dataclasses import asdict
from pathlib import Path from pathlib import Path
from fastapi import Depends, FastAPI, HTTPException, Query from fastapi import Depends, FastAPI, HTTPException, Query
from fastapi.responses import PlainTextResponse
from pydantic import Field from pydantic import Field
from pydantic_settings import BaseSettings, SettingsConfigDict from pydantic_settings import BaseSettings, SettingsConfigDict
@@ -27,6 +28,8 @@ from repo_registry.web_api.schemas import (
CandidateGraphResponse, CandidateGraphResponse,
CandidateLeafRelink, CandidateLeafRelink,
CandidateRejection, CandidateRejection,
CapabilityGapRequest,
CapabilityGapResponse,
CapabilityCreate, CapabilityCreate,
CapabilitySummaryResponse, CapabilitySummaryResponse,
CapabilityUpdate, CapabilityUpdate,
@@ -38,6 +41,7 @@ from repo_registry.web_api.schemas import (
IdResponse, IdResponse,
ObservedFactResponse, ObservedFactResponse,
RepositoryAbilityMapResponse, RepositoryAbilityMapResponse,
RepositoryComparisonResponse,
RepositoryCreate, RepositoryCreate,
RepositoryResponse, RepositoryResponse,
RepositoryUpdate, RepositoryUpdate,
@@ -91,6 +95,7 @@ OPENAPI_TAGS = [
{"name": "review", "description": "Candidate graph approval and correction workflow."}, {"name": "review", "description": "Candidate graph approval and correction workflow."},
{"name": "registry", "description": "Approved ability maps and manual registry CRUD."}, {"name": "registry", "description": "Approved ability maps and manual registry CRUD."},
{"name": "search", "description": "Agent-facing discovery endpoints."}, {"name": "search", "description": "Agent-facing discovery endpoints."},
{"name": "discovery", "description": "Comparison, gap analysis, and export helpers."},
] ]
app = FastAPI( app = FastAPI(
@@ -951,6 +956,62 @@ def get_ability_map(
raise HTTPException(status_code=404, detail=str(exc)) from 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]) @app.get("/search", tags=["search"], response_model=list[SearchResultResponse])
def search( def search(
q: str = Query( q: str = Query(

View File

@@ -664,3 +664,85 @@ class CapabilitySummaryResponse(BaseModel):
description: str description: str
confidence: float confidence: float
confidence_label: str 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]

View File

@@ -19,6 +19,7 @@ def test_openapi_groups_agent_facing_endpoints():
"review", "review",
"registry", "registry",
"search", "search",
"discovery",
} }
search_operation = schema["paths"]["/search"]["get"] search_operation = schema["paths"]["/search"]["get"]
assert search_operation["tags"] == ["search"] assert search_operation["tags"] == ["search"]
@@ -190,6 +191,163 @@ def test_api_manual_registry_loop(tmp_path):
app.dependency_overrides.clear() 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): def test_api_registers_repository_from_url_metadata(tmp_path):
source = tmp_path / "metadata-api" source = tmp_path / "metadata-api"
source.mkdir() source.mkdir()
@@ -439,6 +597,179 @@ def test_api_analysis_run_loop(tmp_path):
app.dependency_overrides.clear() 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): def test_ui_register_analyze_and_approve_loop(tmp_path):
source = tmp_path / "repo" source = tmp_path / "repo"
source.mkdir() source.mkdir()

View File

@@ -262,6 +262,47 @@ Acceptance criteria:
- Responses are stable enough for agent/tooling integration - Responses are stable enough for agent/tooling integration
- OpenAPI docs describe all MVP endpoints - 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 ## 7. Initial Database Shape
Start with tables for: Start with tables for: