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

@@ -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,

View File

@@ -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(

View File

@@ -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]