2026-04-25 22:00:26 +02:00
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
from dataclasses import asdict
|
|
|
|
|
from pathlib import Path
|
|
|
|
|
|
|
|
|
|
from fastapi import Depends, FastAPI, HTTPException
|
|
|
|
|
from pydantic import BaseModel, Field
|
|
|
|
|
|
|
|
|
|
from repo_registry.core.service import RegistryService
|
2026-04-25 22:37:06 +02:00
|
|
|
from repo_registry.repo_ingestion.git import GitIngestionService
|
2026-04-25 22:00:26 +02:00
|
|
|
from repo_registry.storage.sqlite import NotFoundError, RegistryStore
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class Settings(BaseModel):
|
|
|
|
|
database_path: str = Field(default="var/repo-registry.sqlite3")
|
2026-04-25 22:37:06 +02:00
|
|
|
checkout_root: str = Field(default="var/checkouts")
|
2026-04-25 22:00:26 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
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()
|
2026-04-25 22:37:06 +02:00
|
|
|
return RegistryService(store, ingestion=GitIngestionService(settings.checkout_root))
|
2026-04-25 22:00:26 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
class RepositoryCreate(BaseModel):
|
|
|
|
|
url: str
|
2026-04-25 23:04:15 +02:00
|
|
|
name: str | None = None
|
2026-04-25 22:00:26 +02:00
|
|
|
description: str | None = None
|
|
|
|
|
branch: str = "main"
|
|
|
|
|
|
2026-04-25 23:56:19 +02:00
|
|
|
model_config = {
|
|
|
|
|
"json_schema_extra": {
|
|
|
|
|
"examples": [
|
|
|
|
|
{
|
|
|
|
|
"url": "https://github.com/example/repository.git",
|
|
|
|
|
"name": "Example Repository",
|
|
|
|
|
"description": "Optional human-readable repository summary.",
|
|
|
|
|
"branch": "main",
|
|
|
|
|
}
|
|
|
|
|
]
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-25 22:00:26 +02:00
|
|
|
|
|
|
|
|
class AbilityCreate(BaseModel):
|
|
|
|
|
name: str
|
|
|
|
|
description: str = ""
|
|
|
|
|
confidence: float = Field(default=1.0, ge=0.0, le=1.0)
|
|
|
|
|
|
2026-04-25 23:56:19 +02:00
|
|
|
model_config = {
|
|
|
|
|
"json_schema_extra": {
|
|
|
|
|
"examples": [
|
|
|
|
|
{
|
|
|
|
|
"name": "Business Email Routing",
|
|
|
|
|
"description": "Route inbound messages to the right team.",
|
|
|
|
|
"confidence": 0.92,
|
|
|
|
|
}
|
|
|
|
|
]
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-25 22:00:26 +02:00
|
|
|
|
|
|
|
|
class CapabilityCreate(BaseModel):
|
|
|
|
|
ability_id: int
|
|
|
|
|
name: str
|
|
|
|
|
description: str = ""
|
|
|
|
|
inputs: list[str] = Field(default_factory=list)
|
|
|
|
|
outputs: list[str] = Field(default_factory=list)
|
|
|
|
|
confidence: float = Field(default=1.0, ge=0.0, le=1.0)
|
|
|
|
|
|
2026-04-25 23:56:19 +02:00
|
|
|
model_config = {
|
|
|
|
|
"json_schema_extra": {
|
|
|
|
|
"examples": [
|
|
|
|
|
{
|
|
|
|
|
"ability_id": 1,
|
|
|
|
|
"name": "Classify Incoming Email",
|
|
|
|
|
"description": "Classify messages by intent.",
|
|
|
|
|
"inputs": ["subject", "body"],
|
|
|
|
|
"outputs": ["intent", "confidence"],
|
|
|
|
|
"confidence": 0.88,
|
|
|
|
|
}
|
|
|
|
|
]
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-25 22:00:26 +02:00
|
|
|
|
|
|
|
|
class FeatureCreate(BaseModel):
|
|
|
|
|
capability_id: int
|
|
|
|
|
name: str
|
|
|
|
|
type: str
|
|
|
|
|
location: str = ""
|
|
|
|
|
confidence: float = Field(default=1.0, ge=0.0, le=1.0)
|
|
|
|
|
|
2026-04-25 23:56:19 +02:00
|
|
|
model_config = {
|
|
|
|
|
"json_schema_extra": {
|
|
|
|
|
"examples": [
|
|
|
|
|
{
|
|
|
|
|
"capability_id": 1,
|
|
|
|
|
"name": "POST /api/classify-email",
|
|
|
|
|
"type": "REST endpoint",
|
|
|
|
|
"location": "src/routes/classify_email.py",
|
|
|
|
|
"confidence": 0.84,
|
|
|
|
|
}
|
|
|
|
|
]
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-25 22:00:26 +02:00
|
|
|
|
|
|
|
|
class EvidenceCreate(BaseModel):
|
|
|
|
|
capability_id: int
|
|
|
|
|
type: str
|
|
|
|
|
reference: str
|
|
|
|
|
strength: str = "medium"
|
|
|
|
|
|
2026-04-25 23:56:19 +02:00
|
|
|
model_config = {
|
|
|
|
|
"json_schema_extra": {
|
|
|
|
|
"examples": [
|
|
|
|
|
{
|
|
|
|
|
"capability_id": 1,
|
|
|
|
|
"type": "unit_test",
|
|
|
|
|
"reference": "tests/test_email_classification.py",
|
|
|
|
|
"strength": "strong",
|
|
|
|
|
}
|
|
|
|
|
]
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-25 22:00:26 +02:00
|
|
|
|
2026-04-25 22:32:05 +02:00
|
|
|
class AnalysisRunCreate(BaseModel):
|
|
|
|
|
source_path: str | None = None
|
|
|
|
|
|
2026-04-25 23:56:19 +02:00
|
|
|
model_config = {
|
|
|
|
|
"json_schema_extra": {
|
|
|
|
|
"examples": [
|
|
|
|
|
{},
|
|
|
|
|
{"source_path": "/path/to/local/repository"},
|
|
|
|
|
]
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-25 22:32:05 +02:00
|
|
|
|
2026-04-25 22:46:22 +02:00
|
|
|
class CandidateGraphApproval(BaseModel):
|
|
|
|
|
notes: str = ""
|
|
|
|
|
|
2026-04-25 23:56:19 +02:00
|
|
|
model_config = {
|
|
|
|
|
"json_schema_extra": {
|
|
|
|
|
"examples": [{"notes": "Approved after curator review."}]
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-25 22:46:22 +02:00
|
|
|
|
2026-04-25 23:27:28 +02:00
|
|
|
class CandidateRejection(BaseModel):
|
|
|
|
|
notes: str = ""
|
|
|
|
|
|
2026-04-25 23:56:19 +02:00
|
|
|
model_config = {
|
|
|
|
|
"json_schema_extra": {
|
|
|
|
|
"examples": [{"notes": "Rejected because the claim is too generic."}]
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-25 23:27:28 +02:00
|
|
|
|
2026-04-25 23:32:18 +02:00
|
|
|
class CandidateEdit(BaseModel):
|
|
|
|
|
name: str
|
|
|
|
|
description: str = ""
|
|
|
|
|
confidence: float = Field(default=0.5, ge=0.0, le=1.0)
|
|
|
|
|
notes: str = ""
|
|
|
|
|
|
2026-04-25 23:56:19 +02:00
|
|
|
model_config = {
|
|
|
|
|
"json_schema_extra": {
|
|
|
|
|
"examples": [
|
|
|
|
|
{
|
|
|
|
|
"name": "Service Health Monitoring",
|
|
|
|
|
"description": "Expose health state for operational checks.",
|
|
|
|
|
"confidence": 0.9,
|
|
|
|
|
"notes": "Renamed from generated review seed.",
|
|
|
|
|
}
|
|
|
|
|
]
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-25 23:32:18 +02:00
|
|
|
|
2026-04-25 23:44:25 +02:00
|
|
|
class CandidateCapabilityRelink(BaseModel):
|
|
|
|
|
target_ability_id: int
|
|
|
|
|
notes: str = ""
|
|
|
|
|
|
2026-04-25 23:56:19 +02:00
|
|
|
model_config = {
|
|
|
|
|
"json_schema_extra": {
|
|
|
|
|
"examples": [
|
|
|
|
|
{"target_ability_id": 2, "notes": "Move under operational ability."}
|
|
|
|
|
]
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-25 23:44:25 +02:00
|
|
|
|
|
|
|
|
class CandidateLeafRelink(BaseModel):
|
|
|
|
|
target_capability_id: int
|
|
|
|
|
notes: str = ""
|
|
|
|
|
|
2026-04-25 23:56:19 +02:00
|
|
|
model_config = {
|
|
|
|
|
"json_schema_extra": {
|
|
|
|
|
"examples": [
|
|
|
|
|
{
|
|
|
|
|
"target_capability_id": 3,
|
|
|
|
|
"notes": "Evidence supports a different capability.",
|
|
|
|
|
}
|
|
|
|
|
]
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-25 23:44:25 +02:00
|
|
|
|
2026-04-25 23:50:58 +02:00
|
|
|
class CandidateAbilityMerge(BaseModel):
|
|
|
|
|
target_ability_id: int
|
|
|
|
|
notes: str = ""
|
|
|
|
|
|
2026-04-25 23:56:19 +02:00
|
|
|
model_config = {
|
|
|
|
|
"json_schema_extra": {
|
|
|
|
|
"examples": [
|
|
|
|
|
{"target_ability_id": 2, "notes": "Duplicate ability wording."}
|
|
|
|
|
]
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-25 23:50:58 +02:00
|
|
|
|
|
|
|
|
class CandidateCapabilityMerge(BaseModel):
|
|
|
|
|
target_capability_id: int
|
|
|
|
|
notes: str = ""
|
|
|
|
|
|
2026-04-25 23:56:19 +02:00
|
|
|
model_config = {
|
|
|
|
|
"json_schema_extra": {
|
|
|
|
|
"examples": [
|
|
|
|
|
{"target_capability_id": 3, "notes": "Duplicate capability."}
|
|
|
|
|
]
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-25 23:50:58 +02:00
|
|
|
|
|
|
|
|
class CandidateFeatureMerge(BaseModel):
|
|
|
|
|
target_feature_id: int
|
|
|
|
|
notes: str = ""
|
|
|
|
|
|
2026-04-25 23:56:19 +02:00
|
|
|
model_config = {
|
|
|
|
|
"json_schema_extra": {
|
|
|
|
|
"examples": [{"target_feature_id": 4, "notes": "Duplicate route."}]
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-25 23:50:58 +02:00
|
|
|
|
|
|
|
|
class CandidateEvidenceMerge(BaseModel):
|
|
|
|
|
target_evidence_id: int
|
|
|
|
|
notes: str = ""
|
|
|
|
|
|
2026-04-25 23:56:19 +02:00
|
|
|
model_config = {
|
|
|
|
|
"json_schema_extra": {
|
|
|
|
|
"examples": [{"target_evidence_id": 5, "notes": "Duplicate evidence."}]
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-25 23:50:58 +02:00
|
|
|
|
2026-04-25 22:00:26 +02:00
|
|
|
app = FastAPI(title="Repository Ability Registry", version="0.1.0")
|
|
|
|
|
|
|
|
|
|
|
2026-04-25 23:04:15 +02:00
|
|
|
from repo_registry.web_ui.views import router as ui_router
|
|
|
|
|
|
|
|
|
|
app.include_router(ui_router)
|
|
|
|
|
|
|
|
|
|
|
2026-04-25 22:00:26 +02:00
|
|
|
@app.get("/health")
|
|
|
|
|
def health() -> dict[str, str]:
|
|
|
|
|
return {"status": "ok"}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@app.post("/repos", status_code=201)
|
|
|
|
|
def create_repository(
|
|
|
|
|
payload: RepositoryCreate,
|
|
|
|
|
service: RegistryService = Depends(get_service),
|
|
|
|
|
) -> dict[str, object]:
|
|
|
|
|
try:
|
|
|
|
|
repository = service.register_repository(**payload.model_dump())
|
2026-04-25 23:04:15 +02:00
|
|
|
except (RuntimeError, ValueError) as exc:
|
2026-04-25 22:00:26 +02:00
|
|
|
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
|
|
|
|
return asdict(repository)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@app.get("/repos")
|
|
|
|
|
def list_repositories(
|
|
|
|
|
service: RegistryService = Depends(get_service),
|
|
|
|
|
) -> list[dict[str, object]]:
|
|
|
|
|
return [asdict(repository) for repository in service.list_repositories()]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@app.get("/repos/{repository_id}")
|
|
|
|
|
def get_repository(
|
|
|
|
|
repository_id: int,
|
|
|
|
|
service: RegistryService = Depends(get_service),
|
|
|
|
|
) -> dict[str, object]:
|
|
|
|
|
try:
|
|
|
|
|
return asdict(service.get_repository(repository_id))
|
|
|
|
|
except NotFoundError as exc:
|
|
|
|
|
raise HTTPException(status_code=404, detail=str(exc)) from exc
|
|
|
|
|
|
|
|
|
|
|
2026-04-25 22:32:05 +02:00
|
|
|
@app.post("/repos/{repository_id}/analysis-runs", status_code=201)
|
|
|
|
|
def create_analysis_run(
|
|
|
|
|
repository_id: int,
|
|
|
|
|
payload: AnalysisRunCreate,
|
|
|
|
|
service: RegistryService = Depends(get_service),
|
|
|
|
|
) -> dict[str, object]:
|
|
|
|
|
try:
|
|
|
|
|
summary = service.analyze_repository(
|
|
|
|
|
repository_id,
|
|
|
|
|
source_path=payload.source_path,
|
|
|
|
|
)
|
|
|
|
|
except NotFoundError as exc:
|
|
|
|
|
raise HTTPException(status_code=404, detail=str(exc)) from exc
|
|
|
|
|
return asdict(summary)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@app.get("/repos/{repository_id}/analysis-runs")
|
|
|
|
|
def list_analysis_runs(
|
|
|
|
|
repository_id: int,
|
|
|
|
|
service: RegistryService = Depends(get_service),
|
|
|
|
|
) -> list[dict[str, object]]:
|
|
|
|
|
try:
|
|
|
|
|
return [asdict(run) for run in service.list_analysis_runs(repository_id)]
|
|
|
|
|
except NotFoundError as exc:
|
|
|
|
|
raise HTTPException(status_code=404, detail=str(exc)) from exc
|
|
|
|
|
|
|
|
|
|
|
2026-04-25 23:56:19 +02:00
|
|
|
@app.get("/repos/{repository_id}/analysis-runs/{analysis_run_id}")
|
|
|
|
|
def get_analysis_run(
|
|
|
|
|
repository_id: int,
|
|
|
|
|
analysis_run_id: int,
|
|
|
|
|
service: RegistryService = Depends(get_service),
|
|
|
|
|
) -> dict[str, object]:
|
|
|
|
|
try:
|
|
|
|
|
return asdict(service.get_analysis_run(repository_id, analysis_run_id))
|
|
|
|
|
except NotFoundError as exc:
|
|
|
|
|
raise HTTPException(status_code=404, detail=str(exc)) from exc
|
|
|
|
|
|
|
|
|
|
|
2026-04-25 22:32:05 +02:00
|
|
|
@app.get("/repos/{repository_id}/observed-facts")
|
|
|
|
|
def list_observed_facts(
|
|
|
|
|
repository_id: int,
|
|
|
|
|
analysis_run_id: int | None = None,
|
|
|
|
|
service: RegistryService = Depends(get_service),
|
|
|
|
|
) -> list[dict[str, object]]:
|
|
|
|
|
try:
|
|
|
|
|
return [
|
|
|
|
|
asdict(fact)
|
|
|
|
|
for fact in service.list_observed_facts(repository_id, analysis_run_id)
|
|
|
|
|
]
|
|
|
|
|
except NotFoundError as exc:
|
|
|
|
|
raise HTTPException(status_code=404, detail=str(exc)) from exc
|
|
|
|
|
|
|
|
|
|
|
2026-04-25 22:42:13 +02:00
|
|
|
@app.get("/repos/{repository_id}/analysis-runs/{analysis_run_id}/candidate-graph")
|
|
|
|
|
def get_candidate_graph(
|
|
|
|
|
repository_id: int,
|
|
|
|
|
analysis_run_id: int,
|
|
|
|
|
service: RegistryService = Depends(get_service),
|
|
|
|
|
) -> dict[str, object]:
|
|
|
|
|
try:
|
|
|
|
|
return asdict(service.candidate_graph(repository_id, analysis_run_id))
|
|
|
|
|
except NotFoundError as exc:
|
|
|
|
|
raise HTTPException(status_code=404, detail=str(exc)) from exc
|
|
|
|
|
|
|
|
|
|
|
2026-04-25 22:46:22 +02:00
|
|
|
@app.post("/repos/{repository_id}/analysis-runs/{analysis_run_id}/candidate-graph/approve")
|
|
|
|
|
def approve_candidate_graph(
|
|
|
|
|
repository_id: int,
|
|
|
|
|
analysis_run_id: int,
|
|
|
|
|
payload: CandidateGraphApproval,
|
|
|
|
|
service: RegistryService = Depends(get_service),
|
|
|
|
|
) -> dict[str, object]:
|
|
|
|
|
try:
|
|
|
|
|
return asdict(
|
|
|
|
|
service.approve_candidate_graph(
|
|
|
|
|
repository_id,
|
|
|
|
|
analysis_run_id,
|
|
|
|
|
notes=payload.notes,
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
except NotFoundError as exc:
|
|
|
|
|
raise HTTPException(status_code=404, detail=str(exc)) from exc
|
|
|
|
|
|
|
|
|
|
|
2026-04-25 23:27:28 +02:00
|
|
|
@app.post(
|
|
|
|
|
"/repos/{repository_id}/analysis-runs/{analysis_run_id}"
|
|
|
|
|
"/candidate-abilities/{candidate_ability_id}/reject"
|
|
|
|
|
)
|
|
|
|
|
def reject_candidate_ability(
|
|
|
|
|
repository_id: int,
|
|
|
|
|
analysis_run_id: int,
|
|
|
|
|
candidate_ability_id: int,
|
|
|
|
|
payload: CandidateRejection,
|
|
|
|
|
service: RegistryService = Depends(get_service),
|
|
|
|
|
) -> dict[str, object]:
|
|
|
|
|
try:
|
|
|
|
|
return asdict(
|
|
|
|
|
service.reject_candidate_ability(
|
|
|
|
|
repository_id,
|
|
|
|
|
analysis_run_id,
|
|
|
|
|
candidate_ability_id,
|
|
|
|
|
notes=payload.notes,
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
except NotFoundError as exc:
|
|
|
|
|
raise HTTPException(status_code=404, detail=str(exc)) from exc
|
|
|
|
|
|
|
|
|
|
|
2026-04-25 23:39:29 +02:00
|
|
|
@app.post(
|
|
|
|
|
"/repos/{repository_id}/analysis-runs/{analysis_run_id}"
|
|
|
|
|
"/candidate-capabilities/{candidate_capability_id}/reject"
|
|
|
|
|
)
|
|
|
|
|
def reject_candidate_capability(
|
|
|
|
|
repository_id: int,
|
|
|
|
|
analysis_run_id: int,
|
|
|
|
|
candidate_capability_id: int,
|
|
|
|
|
payload: CandidateRejection,
|
|
|
|
|
service: RegistryService = Depends(get_service),
|
|
|
|
|
) -> dict[str, object]:
|
|
|
|
|
try:
|
|
|
|
|
return asdict(
|
|
|
|
|
service.reject_candidate_capability(
|
|
|
|
|
repository_id,
|
|
|
|
|
analysis_run_id,
|
|
|
|
|
candidate_capability_id,
|
|
|
|
|
notes=payload.notes,
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
except NotFoundError as exc:
|
|
|
|
|
raise HTTPException(status_code=404, detail=str(exc)) from exc
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@app.post(
|
|
|
|
|
"/repos/{repository_id}/analysis-runs/{analysis_run_id}"
|
|
|
|
|
"/candidate-features/{candidate_feature_id}/reject"
|
|
|
|
|
)
|
|
|
|
|
def reject_candidate_feature(
|
|
|
|
|
repository_id: int,
|
|
|
|
|
analysis_run_id: int,
|
|
|
|
|
candidate_feature_id: int,
|
|
|
|
|
payload: CandidateRejection,
|
|
|
|
|
service: RegistryService = Depends(get_service),
|
|
|
|
|
) -> dict[str, object]:
|
|
|
|
|
try:
|
|
|
|
|
return asdict(
|
|
|
|
|
service.reject_candidate_feature(
|
|
|
|
|
repository_id,
|
|
|
|
|
analysis_run_id,
|
|
|
|
|
candidate_feature_id,
|
|
|
|
|
notes=payload.notes,
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
except NotFoundError as exc:
|
|
|
|
|
raise HTTPException(status_code=404, detail=str(exc)) from exc
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@app.post(
|
|
|
|
|
"/repos/{repository_id}/analysis-runs/{analysis_run_id}"
|
|
|
|
|
"/candidate-evidence/{candidate_evidence_id}/reject"
|
|
|
|
|
)
|
|
|
|
|
def reject_candidate_evidence(
|
|
|
|
|
repository_id: int,
|
|
|
|
|
analysis_run_id: int,
|
|
|
|
|
candidate_evidence_id: int,
|
|
|
|
|
payload: CandidateRejection,
|
|
|
|
|
service: RegistryService = Depends(get_service),
|
|
|
|
|
) -> dict[str, object]:
|
|
|
|
|
try:
|
|
|
|
|
return asdict(
|
|
|
|
|
service.reject_candidate_evidence(
|
|
|
|
|
repository_id,
|
|
|
|
|
analysis_run_id,
|
|
|
|
|
candidate_evidence_id,
|
|
|
|
|
notes=payload.notes,
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
except NotFoundError as exc:
|
|
|
|
|
raise HTTPException(status_code=404, detail=str(exc)) from exc
|
|
|
|
|
|
|
|
|
|
|
2026-04-25 23:32:18 +02:00
|
|
|
@app.patch(
|
|
|
|
|
"/repos/{repository_id}/analysis-runs/{analysis_run_id}"
|
|
|
|
|
"/candidate-abilities/{candidate_ability_id}"
|
|
|
|
|
)
|
|
|
|
|
def edit_candidate_ability(
|
|
|
|
|
repository_id: int,
|
|
|
|
|
analysis_run_id: int,
|
|
|
|
|
candidate_ability_id: int,
|
|
|
|
|
payload: CandidateEdit,
|
|
|
|
|
service: RegistryService = Depends(get_service),
|
|
|
|
|
) -> dict[str, object]:
|
|
|
|
|
try:
|
|
|
|
|
return asdict(
|
|
|
|
|
service.edit_candidate_ability(
|
|
|
|
|
repository_id,
|
|
|
|
|
analysis_run_id,
|
|
|
|
|
candidate_ability_id,
|
|
|
|
|
**payload.model_dump(),
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
except NotFoundError as exc:
|
|
|
|
|
raise HTTPException(status_code=404, detail=str(exc)) from exc
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@app.patch(
|
|
|
|
|
"/repos/{repository_id}/analysis-runs/{analysis_run_id}"
|
|
|
|
|
"/candidate-capabilities/{candidate_capability_id}"
|
|
|
|
|
)
|
|
|
|
|
def edit_candidate_capability(
|
|
|
|
|
repository_id: int,
|
|
|
|
|
analysis_run_id: int,
|
|
|
|
|
candidate_capability_id: int,
|
|
|
|
|
payload: CandidateEdit,
|
|
|
|
|
service: RegistryService = Depends(get_service),
|
|
|
|
|
) -> dict[str, object]:
|
|
|
|
|
try:
|
|
|
|
|
return asdict(
|
|
|
|
|
service.edit_candidate_capability(
|
|
|
|
|
repository_id,
|
|
|
|
|
analysis_run_id,
|
|
|
|
|
candidate_capability_id,
|
|
|
|
|
**payload.model_dump(),
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
except NotFoundError as exc:
|
|
|
|
|
raise HTTPException(status_code=404, detail=str(exc)) from exc
|
|
|
|
|
|
|
|
|
|
|
2026-04-25 23:44:25 +02:00
|
|
|
@app.post(
|
|
|
|
|
"/repos/{repository_id}/analysis-runs/{analysis_run_id}"
|
|
|
|
|
"/candidate-capabilities/{candidate_capability_id}/relink"
|
|
|
|
|
)
|
|
|
|
|
def relink_candidate_capability(
|
|
|
|
|
repository_id: int,
|
|
|
|
|
analysis_run_id: int,
|
|
|
|
|
candidate_capability_id: int,
|
|
|
|
|
payload: CandidateCapabilityRelink,
|
|
|
|
|
service: RegistryService = Depends(get_service),
|
|
|
|
|
) -> dict[str, object]:
|
|
|
|
|
try:
|
|
|
|
|
return asdict(
|
|
|
|
|
service.relink_candidate_capability(
|
|
|
|
|
repository_id,
|
|
|
|
|
analysis_run_id,
|
|
|
|
|
candidate_capability_id,
|
|
|
|
|
**payload.model_dump(),
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
except NotFoundError as exc:
|
|
|
|
|
raise HTTPException(status_code=404, detail=str(exc)) from exc
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@app.post(
|
|
|
|
|
"/repos/{repository_id}/analysis-runs/{analysis_run_id}"
|
|
|
|
|
"/candidate-features/{candidate_feature_id}/relink"
|
|
|
|
|
)
|
|
|
|
|
def relink_candidate_feature(
|
|
|
|
|
repository_id: int,
|
|
|
|
|
analysis_run_id: int,
|
|
|
|
|
candidate_feature_id: int,
|
|
|
|
|
payload: CandidateLeafRelink,
|
|
|
|
|
service: RegistryService = Depends(get_service),
|
|
|
|
|
) -> dict[str, object]:
|
|
|
|
|
try:
|
|
|
|
|
return asdict(
|
|
|
|
|
service.relink_candidate_feature(
|
|
|
|
|
repository_id,
|
|
|
|
|
analysis_run_id,
|
|
|
|
|
candidate_feature_id,
|
|
|
|
|
**payload.model_dump(),
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
except NotFoundError as exc:
|
|
|
|
|
raise HTTPException(status_code=404, detail=str(exc)) from exc
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@app.post(
|
|
|
|
|
"/repos/{repository_id}/analysis-runs/{analysis_run_id}"
|
|
|
|
|
"/candidate-evidence/{candidate_evidence_id}/relink"
|
|
|
|
|
)
|
|
|
|
|
def relink_candidate_evidence(
|
|
|
|
|
repository_id: int,
|
|
|
|
|
analysis_run_id: int,
|
|
|
|
|
candidate_evidence_id: int,
|
|
|
|
|
payload: CandidateLeafRelink,
|
|
|
|
|
service: RegistryService = Depends(get_service),
|
|
|
|
|
) -> dict[str, object]:
|
|
|
|
|
try:
|
|
|
|
|
return asdict(
|
|
|
|
|
service.relink_candidate_evidence(
|
|
|
|
|
repository_id,
|
|
|
|
|
analysis_run_id,
|
|
|
|
|
candidate_evidence_id,
|
|
|
|
|
**payload.model_dump(),
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
except NotFoundError as exc:
|
|
|
|
|
raise HTTPException(status_code=404, detail=str(exc)) from exc
|
|
|
|
|
|
|
|
|
|
|
2026-04-25 23:50:58 +02:00
|
|
|
@app.post(
|
|
|
|
|
"/repos/{repository_id}/analysis-runs/{analysis_run_id}"
|
|
|
|
|
"/candidate-abilities/{source_ability_id}/merge"
|
|
|
|
|
)
|
|
|
|
|
def merge_candidate_ability(
|
|
|
|
|
repository_id: int,
|
|
|
|
|
analysis_run_id: int,
|
|
|
|
|
source_ability_id: int,
|
|
|
|
|
payload: CandidateAbilityMerge,
|
|
|
|
|
service: RegistryService = Depends(get_service),
|
|
|
|
|
) -> dict[str, object]:
|
|
|
|
|
try:
|
|
|
|
|
return asdict(
|
|
|
|
|
service.merge_candidate_ability(
|
|
|
|
|
repository_id,
|
|
|
|
|
analysis_run_id,
|
|
|
|
|
source_ability_id,
|
|
|
|
|
**payload.model_dump(),
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
except (NotFoundError, ValueError) as exc:
|
|
|
|
|
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@app.post(
|
|
|
|
|
"/repos/{repository_id}/analysis-runs/{analysis_run_id}"
|
|
|
|
|
"/candidate-capabilities/{source_capability_id}/merge"
|
|
|
|
|
)
|
|
|
|
|
def merge_candidate_capability(
|
|
|
|
|
repository_id: int,
|
|
|
|
|
analysis_run_id: int,
|
|
|
|
|
source_capability_id: int,
|
|
|
|
|
payload: CandidateCapabilityMerge,
|
|
|
|
|
service: RegistryService = Depends(get_service),
|
|
|
|
|
) -> dict[str, object]:
|
|
|
|
|
try:
|
|
|
|
|
return asdict(
|
|
|
|
|
service.merge_candidate_capability(
|
|
|
|
|
repository_id,
|
|
|
|
|
analysis_run_id,
|
|
|
|
|
source_capability_id,
|
|
|
|
|
**payload.model_dump(),
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
except (NotFoundError, ValueError) as exc:
|
|
|
|
|
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@app.post(
|
|
|
|
|
"/repos/{repository_id}/analysis-runs/{analysis_run_id}"
|
|
|
|
|
"/candidate-features/{source_feature_id}/merge"
|
|
|
|
|
)
|
|
|
|
|
def merge_candidate_feature(
|
|
|
|
|
repository_id: int,
|
|
|
|
|
analysis_run_id: int,
|
|
|
|
|
source_feature_id: int,
|
|
|
|
|
payload: CandidateFeatureMerge,
|
|
|
|
|
service: RegistryService = Depends(get_service),
|
|
|
|
|
) -> dict[str, object]:
|
|
|
|
|
try:
|
|
|
|
|
return asdict(
|
|
|
|
|
service.merge_candidate_feature(
|
|
|
|
|
repository_id,
|
|
|
|
|
analysis_run_id,
|
|
|
|
|
source_feature_id,
|
|
|
|
|
**payload.model_dump(),
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
except (NotFoundError, ValueError) as exc:
|
|
|
|
|
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@app.post(
|
|
|
|
|
"/repos/{repository_id}/analysis-runs/{analysis_run_id}"
|
|
|
|
|
"/candidate-evidence/{source_evidence_id}/merge"
|
|
|
|
|
)
|
|
|
|
|
def merge_candidate_evidence(
|
|
|
|
|
repository_id: int,
|
|
|
|
|
analysis_run_id: int,
|
|
|
|
|
source_evidence_id: int,
|
|
|
|
|
payload: CandidateEvidenceMerge,
|
|
|
|
|
service: RegistryService = Depends(get_service),
|
|
|
|
|
) -> dict[str, object]:
|
|
|
|
|
try:
|
|
|
|
|
return asdict(
|
|
|
|
|
service.merge_candidate_evidence(
|
|
|
|
|
repository_id,
|
|
|
|
|
analysis_run_id,
|
|
|
|
|
source_evidence_id,
|
|
|
|
|
**payload.model_dump(),
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
except (NotFoundError, ValueError) as exc:
|
|
|
|
|
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
|
|
|
|
|
|
|
|
|
|
2026-04-25 22:00:26 +02:00
|
|
|
@app.post("/repos/{repository_id}/abilities", status_code=201)
|
|
|
|
|
def create_ability(
|
|
|
|
|
repository_id: int,
|
|
|
|
|
payload: AbilityCreate,
|
|
|
|
|
service: RegistryService = Depends(get_service),
|
|
|
|
|
) -> dict[str, int]:
|
|
|
|
|
try:
|
|
|
|
|
ability_id = service.add_ability(repository_id, **payload.model_dump())
|
|
|
|
|
except NotFoundError as exc:
|
|
|
|
|
raise HTTPException(status_code=404, detail=str(exc)) from exc
|
|
|
|
|
return {"id": ability_id}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@app.post("/repos/{repository_id}/capabilities", status_code=201)
|
|
|
|
|
def create_capability(
|
|
|
|
|
repository_id: int,
|
|
|
|
|
payload: CapabilityCreate,
|
|
|
|
|
service: RegistryService = Depends(get_service),
|
|
|
|
|
) -> dict[str, int]:
|
|
|
|
|
try:
|
|
|
|
|
capability_id = service.add_capability(repository_id, **payload.model_dump())
|
|
|
|
|
except NotFoundError as exc:
|
|
|
|
|
raise HTTPException(status_code=404, detail=str(exc)) from exc
|
|
|
|
|
return {"id": capability_id}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@app.post("/repos/{repository_id}/features", status_code=201)
|
|
|
|
|
def create_feature(
|
|
|
|
|
repository_id: int,
|
|
|
|
|
payload: FeatureCreate,
|
|
|
|
|
service: RegistryService = Depends(get_service),
|
|
|
|
|
) -> dict[str, int]:
|
|
|
|
|
try:
|
|
|
|
|
feature_id = service.add_feature(repository_id, **payload.model_dump())
|
|
|
|
|
except NotFoundError as exc:
|
|
|
|
|
raise HTTPException(status_code=404, detail=str(exc)) from exc
|
|
|
|
|
return {"id": feature_id}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@app.post("/repos/{repository_id}/evidence", status_code=201)
|
|
|
|
|
def create_evidence(
|
|
|
|
|
repository_id: int,
|
|
|
|
|
payload: EvidenceCreate,
|
|
|
|
|
service: RegistryService = Depends(get_service),
|
|
|
|
|
) -> dict[str, int]:
|
|
|
|
|
try:
|
|
|
|
|
evidence_id = service.add_evidence(repository_id, **payload.model_dump())
|
|
|
|
|
except NotFoundError as exc:
|
|
|
|
|
raise HTTPException(status_code=404, detail=str(exc)) from exc
|
|
|
|
|
return {"id": evidence_id}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@app.get("/repos/{repository_id}/ability-map")
|
|
|
|
|
def get_ability_map(
|
|
|
|
|
repository_id: int,
|
|
|
|
|
service: RegistryService = Depends(get_service),
|
|
|
|
|
) -> dict[str, object]:
|
|
|
|
|
try:
|
|
|
|
|
return asdict(service.ability_map(repository_id))
|
|
|
|
|
except NotFoundError as exc:
|
|
|
|
|
raise HTTPException(status_code=404, detail=str(exc)) from exc
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@app.get("/search")
|
|
|
|
|
def search(
|
|
|
|
|
q: str,
|
2026-04-26 00:04:19 +02:00
|
|
|
status: str | None = None,
|
|
|
|
|
language: str | None = None,
|
|
|
|
|
framework: str | None = None,
|
2026-04-26 00:08:55 +02:00
|
|
|
ability: str | None = None,
|
|
|
|
|
capability: str | None = None,
|
2026-04-25 22:00:26 +02:00
|
|
|
service: RegistryService = Depends(get_service),
|
|
|
|
|
) -> list[dict[str, object]]:
|
2026-04-26 00:04:19 +02:00
|
|
|
return [
|
|
|
|
|
asdict(result)
|
|
|
|
|
for result in service.search(
|
|
|
|
|
q,
|
|
|
|
|
status=status,
|
|
|
|
|
language=language,
|
|
|
|
|
framework=framework,
|
2026-04-26 00:08:55 +02:00
|
|
|
ability=ability,
|
|
|
|
|
capability=capability,
|
2026-04-26 00:04:19 +02:00
|
|
|
)
|
|
|
|
|
]
|
2026-04-25 23:56:19 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
@app.get("/abilities")
|
|
|
|
|
def list_abilities(
|
|
|
|
|
service: RegistryService = Depends(get_service),
|
|
|
|
|
) -> list[dict[str, object]]:
|
|
|
|
|
return [asdict(ability) for ability in service.list_abilities()]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@app.get("/capabilities")
|
|
|
|
|
def list_capabilities(
|
|
|
|
|
service: RegistryService = Depends(get_service),
|
|
|
|
|
) -> list[dict[str, object]]:
|
|
|
|
|
return [asdict(capability) for capability in service.list_capabilities()]
|