generated from coulomb/repo-seed
1776 lines
54 KiB
Python
1776 lines
54 KiB
Python
from __future__ import annotations
|
|
|
|
import logging
|
|
import json
|
|
from dataclasses import asdict
|
|
from pathlib import Path
|
|
from urllib.error import HTTPError, URLError
|
|
from urllib.request import urlopen
|
|
|
|
from fastapi import Depends, FastAPI, HTTPException, Query
|
|
from fastapi.responses import PlainTextResponse
|
|
from pydantic import Field
|
|
from pydantic_settings import BaseSettings, SettingsConfigDict
|
|
|
|
from repo_scoping.acceptance import (
|
|
criteria_registry_dict,
|
|
evaluate_candidate_graph_quality,
|
|
load_quality_criteria,
|
|
quality_gate_outcome_dicts,
|
|
)
|
|
from repo_scoping.core.service import RegistryService
|
|
from repo_scoping.llm_extraction import LLMCandidateExtractor, create_llm_connect_adapter
|
|
from repo_scoping.repo_ingestion.git import GitIngestionService
|
|
from repo_scoping.semantic import HashingEmbeddingProvider
|
|
from repo_scoping.scope import ScopeGenerator, ScopeValidator
|
|
from repo_scoping.storage.sqlite import NotFoundError, RegistryStore
|
|
from repo_scoping.web_api.schemas import (
|
|
AbilityCreate,
|
|
AbilitySummaryResponse,
|
|
AbilityUpdate,
|
|
AnalysisRunChangeApproval,
|
|
AnalysisRunCreate,
|
|
AnalysisRunDiffResponse,
|
|
AnalysisRunResponse,
|
|
CandidateAbilityMerge,
|
|
CandidateCapabilityMerge,
|
|
CandidateCapabilityRelink,
|
|
CandidateEdit,
|
|
CandidateEvidenceMerge,
|
|
CandidateFeatureMerge,
|
|
CandidateGraphApproval,
|
|
CandidateGraphResponse,
|
|
CharacteristicRebuildRequest,
|
|
CharacteristicRebuildResponse,
|
|
CandidateLeafRelink,
|
|
CandidateRejection,
|
|
CapabilityGapRequest,
|
|
CapabilityGapResponse,
|
|
CapabilityCreate,
|
|
CapabilitySummaryResponse,
|
|
CapabilityUpdate,
|
|
ContentChunkResponse,
|
|
DependencyGraphAdHocFilters,
|
|
DependencyGraphProfileCreate,
|
|
DependencyGraphProfileDuplicate,
|
|
DependencyGraphProfileResponse,
|
|
DependencyGraphProfileUpdate,
|
|
EvidenceCreate,
|
|
EvidenceUpdate,
|
|
ErrorResponse,
|
|
ExpectationGapCreate,
|
|
ExpectationGapResponse,
|
|
FeatureCreate,
|
|
FeatureUpdate,
|
|
IdResponse,
|
|
ObservedFactResponse,
|
|
QualityCriteriaRegistryResponse,
|
|
QualityGateOverrideCreate,
|
|
RepositoryAbilityMapResponse,
|
|
RepositoryComparisonResponse,
|
|
RepositoryCreate,
|
|
RepositoryResponse,
|
|
RepositoryScopeContextResponse,
|
|
RepositoryUpdate,
|
|
ReviewDecisionResponse,
|
|
ScanSummaryResponse,
|
|
SearchResultResponse,
|
|
TrustedAutoApprovalMigrationRecordResponse,
|
|
)
|
|
|
|
|
|
def slugify(value: str) -> str:
|
|
import re
|
|
|
|
return re.sub(r"[^a-z0-9]+", "-", value.lower()).strip("-")
|
|
|
|
|
|
class Settings(BaseSettings):
|
|
model_config = SettingsConfigDict(env_prefix="REPO_SCOPING_")
|
|
|
|
database_path: str = Field(default="var/repo-scoping.sqlite3")
|
|
checkout_root: str = Field(default="var/checkouts")
|
|
llm_enabled: bool = Field(default=True)
|
|
llm_provider: str | None = Field(default=None)
|
|
llm_model: str | None = Field(default=None)
|
|
embedding_provider: str | None = Field(default=None)
|
|
state_hub_base_url: str = Field(default="http://127.0.0.1:8000")
|
|
log_level: str = Field(default="INFO")
|
|
|
|
|
|
def get_settings() -> Settings:
|
|
return Settings()
|
|
|
|
|
|
def get_service(settings: Settings = Depends(get_settings)) -> RegistryService:
|
|
logging.getLogger("repo_scoping.operations").setLevel(
|
|
getattr(logging, settings.log_level.upper(), logging.INFO)
|
|
)
|
|
database_path = Path(settings.database_path)
|
|
database_path.parent.mkdir(parents=True, exist_ok=True)
|
|
store = RegistryStore(database_path)
|
|
store.initialize()
|
|
llm_extractor = None
|
|
if settings.llm_enabled and settings.llm_provider:
|
|
adapter = create_llm_connect_adapter(
|
|
settings.llm_provider,
|
|
model=settings.llm_model,
|
|
)
|
|
llm_extractor = LLMCandidateExtractor(adapter)
|
|
embedding_provider = None
|
|
if settings.embedding_provider == "hashing":
|
|
embedding_provider = HashingEmbeddingProvider()
|
|
return RegistryService(
|
|
store,
|
|
ingestion=GitIngestionService(settings.checkout_root),
|
|
llm_extractor=llm_extractor,
|
|
embedding_provider=embedding_provider,
|
|
)
|
|
|
|
|
|
def candidate_graph_payload(graph) -> dict[str, object]:
|
|
payload = asdict(graph)
|
|
payload["quality_gate_outcomes"] = quality_gate_outcome_dicts(
|
|
evaluate_candidate_graph_quality(graph)
|
|
)
|
|
return payload
|
|
|
|
|
|
API_DESCRIPTION = (
|
|
"Register repositories, analyze their observable implementation facts, "
|
|
"curate reviewable scope graphs, and search approved repository characteristics."
|
|
)
|
|
|
|
OPENAPI_TAGS = [
|
|
{"name": "health", "description": "Service health checks."},
|
|
{"name": "repositories", "description": "Repository registration and metadata."},
|
|
{"name": "analysis", "description": "Repository scans and extracted review inputs."},
|
|
{"name": "review", "description": "Candidate graph approval and correction workflow."},
|
|
{"name": "registry", "description": "Approved ability maps and manual registry CRUD."},
|
|
{"name": "scope", "description": "SCOPE.md generation, diffing, and writing."},
|
|
{"name": "visualization", "description": "Dependency graph exploration and view profiles."},
|
|
{"name": "search", "description": "Agent-facing discovery endpoints."},
|
|
{"name": "discovery", "description": "Comparison, gap analysis, and export helpers."},
|
|
]
|
|
|
|
app = FastAPI(
|
|
title="Repository Scoping",
|
|
version="0.1.0",
|
|
description=API_DESCRIPTION,
|
|
openapi_tags=OPENAPI_TAGS,
|
|
responses={
|
|
400: {"model": ErrorResponse, "description": "Bad request."},
|
|
404: {"model": ErrorResponse, "description": "Resource not found."},
|
|
},
|
|
)
|
|
|
|
|
|
from repo_scoping.web_ui.views import router as ui_router
|
|
|
|
app.include_router(ui_router)
|
|
|
|
|
|
@app.get("/health", tags=["health"])
|
|
def health(settings: Settings = Depends(get_settings)) -> dict[str, object]:
|
|
database_path = Path(settings.database_path)
|
|
checkout_root = Path(settings.checkout_root)
|
|
database_reachable = False
|
|
database_error = None
|
|
try:
|
|
database_path.parent.mkdir(parents=True, exist_ok=True)
|
|
store = RegistryStore(database_path)
|
|
store.initialize()
|
|
with store.connect() as connection:
|
|
connection.execute("SELECT 1").fetchone()
|
|
database_reachable = True
|
|
except Exception as exc:
|
|
database_error = str(exc)
|
|
|
|
return {
|
|
"status": "ok" if database_reachable else "degraded",
|
|
"database": {
|
|
"path": str(database_path),
|
|
"reachable": database_reachable,
|
|
"error": database_error,
|
|
},
|
|
"checkout_root": {
|
|
"path": str(checkout_root),
|
|
"exists": checkout_root.exists(),
|
|
},
|
|
}
|
|
|
|
|
|
@app.get(
|
|
"/quality-criteria",
|
|
tags=["review"],
|
|
response_model=QualityCriteriaRegistryResponse,
|
|
)
|
|
def list_quality_criteria() -> dict[str, object]:
|
|
return criteria_registry_dict(load_quality_criteria())
|
|
|
|
|
|
@app.get(
|
|
"/review/migrations/trusted-auto-approvals",
|
|
tags=["review"],
|
|
response_model=list[TrustedAutoApprovalMigrationRecordResponse],
|
|
)
|
|
def list_trusted_auto_approval_migration_records(
|
|
service: RegistryService = Depends(get_service),
|
|
) -> list[dict[str, object]]:
|
|
return [
|
|
asdict(record)
|
|
for record in service.list_trusted_auto_approval_migration_records()
|
|
]
|
|
|
|
|
|
@app.post(
|
|
"/repos",
|
|
status_code=201,
|
|
tags=["repositories"],
|
|
response_model=RepositoryResponse,
|
|
)
|
|
def create_repository(
|
|
payload: RepositoryCreate,
|
|
service: RegistryService = Depends(get_service),
|
|
) -> dict[str, object]:
|
|
try:
|
|
repository = service.register_repository(**payload.model_dump())
|
|
except (RuntimeError, ValueError) as exc:
|
|
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
|
return asdict(repository)
|
|
|
|
|
|
@app.get("/repos", tags=["repositories"], response_model=list[RepositoryResponse])
|
|
def list_repositories(
|
|
service: RegistryService = Depends(get_service),
|
|
) -> list[dict[str, object]]:
|
|
return [asdict(repository) for repository in service.list_repositories()]
|
|
|
|
|
|
@app.get(
|
|
"/repos/{repository_id}",
|
|
tags=["repositories"],
|
|
response_model=RepositoryResponse,
|
|
)
|
|
def get_repository(
|
|
repository_id: int,
|
|
service: RegistryService = Depends(get_service),
|
|
) -> dict[str, object]:
|
|
try:
|
|
return asdict(service.get_repository(repository_id))
|
|
except NotFoundError as exc:
|
|
raise HTTPException(status_code=404, detail=str(exc)) from exc
|
|
|
|
|
|
@app.patch(
|
|
"/repos/{repository_id}",
|
|
tags=["repositories"],
|
|
response_model=RepositoryResponse,
|
|
)
|
|
def update_repository(
|
|
repository_id: int,
|
|
payload: RepositoryUpdate,
|
|
service: RegistryService = Depends(get_service),
|
|
) -> dict[str, object]:
|
|
try:
|
|
return asdict(
|
|
service.update_repository(
|
|
repository_id,
|
|
**payload.model_dump(exclude_unset=True),
|
|
)
|
|
)
|
|
except NotFoundError as exc:
|
|
raise HTTPException(status_code=404, detail=str(exc)) from exc
|
|
|
|
|
|
@app.delete("/repos/{repository_id}", status_code=204, tags=["repositories"])
|
|
def delete_repository(
|
|
repository_id: int,
|
|
service: RegistryService = Depends(get_service),
|
|
) -> None:
|
|
try:
|
|
service.delete_repository(repository_id)
|
|
except NotFoundError as exc:
|
|
raise HTTPException(status_code=404, detail=str(exc)) from exc
|
|
|
|
|
|
@app.post(
|
|
"/repos/{repository_id}/analysis-runs",
|
|
status_code=201,
|
|
tags=["analysis"],
|
|
response_model=ScanSummaryResponse,
|
|
)
|
|
def create_analysis_run(
|
|
repository_id: int,
|
|
payload: AnalysisRunCreate,
|
|
service: RegistryService = Depends(get_service),
|
|
) -> dict[str, object]:
|
|
try:
|
|
summary = service.analyze_repository(
|
|
repository_id,
|
|
source_path=payload.source_path,
|
|
use_cached_checkout=payload.use_cached_checkout,
|
|
use_llm_assistance=payload.use_llm_assistance,
|
|
agentic_review=payload.agentic_review,
|
|
trusted_auto_approve=payload.trusted_auto_approve,
|
|
access_username=payload.access_username,
|
|
access_password=payload.access_password,
|
|
)
|
|
except NotFoundError as exc:
|
|
raise HTTPException(status_code=404, detail=str(exc)) from exc
|
|
return asdict(summary)
|
|
|
|
|
|
@app.post(
|
|
"/repos/{repository_id}/characteristics/rebuild",
|
|
tags=["analysis"],
|
|
response_model=CharacteristicRebuildResponse,
|
|
)
|
|
def rebuild_characteristics_from_scratch(
|
|
repository_id: int,
|
|
payload: CharacteristicRebuildRequest,
|
|
service: RegistryService = Depends(get_service),
|
|
) -> dict[str, object]:
|
|
try:
|
|
return asdict(
|
|
service.rebuild_characteristics_from_scratch(
|
|
repository_id,
|
|
dry_run=payload.dry_run,
|
|
confirm=payload.confirm,
|
|
source_path=payload.source_path,
|
|
use_cached_checkout=payload.use_cached_checkout,
|
|
use_llm_assistance=payload.use_llm_assistance,
|
|
)
|
|
)
|
|
except NotFoundError as exc:
|
|
raise HTTPException(status_code=404, detail=str(exc)) from exc
|
|
except ValueError as exc:
|
|
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
|
|
|
|
|
@app.get(
|
|
"/repos/{repository_id}/analysis-runs",
|
|
tags=["analysis"],
|
|
response_model=list[AnalysisRunResponse],
|
|
)
|
|
def list_analysis_runs(
|
|
repository_id: int,
|
|
service: RegistryService = Depends(get_service),
|
|
) -> list[dict[str, object]]:
|
|
try:
|
|
return [asdict(run) for run in service.list_analysis_runs(repository_id)]
|
|
except NotFoundError as exc:
|
|
raise HTTPException(status_code=404, detail=str(exc)) from exc
|
|
|
|
|
|
@app.get(
|
|
"/repos/{repository_id}/analysis-runs/{analysis_run_id}",
|
|
tags=["analysis"],
|
|
response_model=AnalysisRunResponse,
|
|
)
|
|
def get_analysis_run(
|
|
repository_id: int,
|
|
analysis_run_id: int,
|
|
service: RegistryService = Depends(get_service),
|
|
) -> dict[str, object]:
|
|
try:
|
|
return asdict(service.get_analysis_run(repository_id, analysis_run_id))
|
|
except NotFoundError as exc:
|
|
raise HTTPException(status_code=404, detail=str(exc)) from exc
|
|
|
|
|
|
@app.get(
|
|
"/repos/{repository_id}/expectation-gaps",
|
|
tags=["review"],
|
|
response_model=list[ExpectationGapResponse],
|
|
)
|
|
def list_expectation_gaps(
|
|
repository_id: int,
|
|
analysis_run_id: int | None = Query(default=None),
|
|
service: RegistryService = Depends(get_service),
|
|
) -> list[dict[str, object]]:
|
|
try:
|
|
return [
|
|
asdict(gap)
|
|
for gap in service.list_expectation_gaps(repository_id, analysis_run_id)
|
|
]
|
|
except NotFoundError as exc:
|
|
raise HTTPException(status_code=404, detail=str(exc)) from exc
|
|
|
|
|
|
@app.post(
|
|
"/repos/{repository_id}/expectation-gaps",
|
|
status_code=201,
|
|
tags=["review"],
|
|
response_model=ExpectationGapResponse,
|
|
)
|
|
def create_expectation_gap(
|
|
repository_id: int,
|
|
payload: ExpectationGapCreate,
|
|
service: RegistryService = Depends(get_service),
|
|
) -> dict[str, object]:
|
|
try:
|
|
return asdict(
|
|
service.record_expectation_gap(
|
|
repository_id,
|
|
analysis_run_id=payload.analysis_run_id,
|
|
expected_type=payload.expected_type,
|
|
expected_name=payload.expected_name,
|
|
source=payload.source,
|
|
notes=payload.notes,
|
|
)
|
|
)
|
|
except NotFoundError as exc:
|
|
raise HTTPException(status_code=404, detail=str(exc)) from exc
|
|
|
|
|
|
@app.get(
|
|
"/repos/{repository_id}/analysis-runs/{base_analysis_run_id}/diff/{target_analysis_run_id}",
|
|
tags=["review"],
|
|
response_model=AnalysisRunDiffResponse,
|
|
)
|
|
def diff_analysis_runs(
|
|
repository_id: int,
|
|
base_analysis_run_id: int,
|
|
target_analysis_run_id: int,
|
|
service: RegistryService = Depends(get_service),
|
|
) -> dict[str, object]:
|
|
try:
|
|
return asdict(
|
|
service.diff_analysis_runs(
|
|
repository_id,
|
|
base_analysis_run_id,
|
|
target_analysis_run_id,
|
|
)
|
|
)
|
|
except NotFoundError as exc:
|
|
raise HTTPException(status_code=404, detail=str(exc)) from exc
|
|
|
|
|
|
@app.get(
|
|
"/repos/{repository_id}/review-decisions",
|
|
tags=["review"],
|
|
response_model=list[ReviewDecisionResponse],
|
|
)
|
|
def list_repository_review_decisions(
|
|
repository_id: int,
|
|
service: RegistryService = Depends(get_service),
|
|
) -> list[dict[str, object]]:
|
|
try:
|
|
return [
|
|
asdict(decision)
|
|
for decision in service.list_review_decisions(repository_id)
|
|
]
|
|
except NotFoundError as exc:
|
|
raise HTTPException(status_code=404, detail=str(exc)) from exc
|
|
|
|
|
|
@app.get(
|
|
"/repos/{repository_id}/analysis-runs/{analysis_run_id}/review-decisions",
|
|
tags=["review"],
|
|
response_model=list[ReviewDecisionResponse],
|
|
)
|
|
def list_analysis_run_review_decisions(
|
|
repository_id: int,
|
|
analysis_run_id: int,
|
|
service: RegistryService = Depends(get_service),
|
|
) -> list[dict[str, object]]:
|
|
try:
|
|
return [
|
|
asdict(decision)
|
|
for decision in service.list_review_decisions(
|
|
repository_id,
|
|
analysis_run_id,
|
|
)
|
|
]
|
|
except NotFoundError as exc:
|
|
raise HTTPException(status_code=404, detail=str(exc)) from exc
|
|
|
|
|
|
@app.post(
|
|
"/repos/{repository_id}/analysis-runs/{analysis_run_id}/quality-gate-overrides",
|
|
tags=["review"],
|
|
response_model=ReviewDecisionResponse,
|
|
)
|
|
def create_quality_gate_override(
|
|
repository_id: int,
|
|
analysis_run_id: int,
|
|
payload: QualityGateOverrideCreate,
|
|
service: RegistryService = Depends(get_service),
|
|
) -> dict[str, object]:
|
|
try:
|
|
return asdict(
|
|
service.record_quality_gate_override(
|
|
repository_id,
|
|
analysis_run_id,
|
|
**payload.model_dump(),
|
|
)
|
|
)
|
|
except (NotFoundError, ValueError) as exc:
|
|
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
|
|
|
|
|
@app.get(
|
|
"/repos/{repository_id}/observed-facts",
|
|
tags=["analysis"],
|
|
response_model=list[ObservedFactResponse],
|
|
)
|
|
def list_observed_facts(
|
|
repository_id: int,
|
|
analysis_run_id: int | None = None,
|
|
service: RegistryService = Depends(get_service),
|
|
) -> list[dict[str, object]]:
|
|
try:
|
|
return [
|
|
asdict(fact)
|
|
for fact in service.list_observed_facts(repository_id, analysis_run_id)
|
|
]
|
|
except NotFoundError as exc:
|
|
raise HTTPException(status_code=404, detail=str(exc)) from exc
|
|
|
|
|
|
@app.get(
|
|
"/repos/{repository_id}/content-chunks",
|
|
tags=["analysis"],
|
|
response_model=list[ContentChunkResponse],
|
|
)
|
|
def list_content_chunks(
|
|
repository_id: int,
|
|
analysis_run_id: int | None = None,
|
|
service: RegistryService = Depends(get_service),
|
|
) -> list[dict[str, object]]:
|
|
try:
|
|
return [
|
|
asdict(chunk)
|
|
for chunk in service.list_content_chunks(repository_id, analysis_run_id)
|
|
]
|
|
except NotFoundError as exc:
|
|
raise HTTPException(status_code=404, detail=str(exc)) from exc
|
|
|
|
|
|
@app.get(
|
|
"/repos/{repository_id}/analysis-runs/{analysis_run_id}/content-chunks",
|
|
tags=["analysis"],
|
|
response_model=list[ContentChunkResponse],
|
|
)
|
|
def list_analysis_run_content_chunks(
|
|
repository_id: int,
|
|
analysis_run_id: int,
|
|
service: RegistryService = Depends(get_service),
|
|
) -> list[dict[str, object]]:
|
|
try:
|
|
return [
|
|
asdict(chunk)
|
|
for chunk in service.list_content_chunks(repository_id, analysis_run_id)
|
|
]
|
|
except NotFoundError as exc:
|
|
raise HTTPException(status_code=404, detail=str(exc)) from exc
|
|
|
|
|
|
@app.get(
|
|
"/repos/{repository_id}/analysis-runs/{analysis_run_id}/candidate-graph",
|
|
tags=["review"],
|
|
response_model=CandidateGraphResponse,
|
|
)
|
|
def get_candidate_graph(
|
|
repository_id: int,
|
|
analysis_run_id: int,
|
|
service: RegistryService = Depends(get_service),
|
|
) -> dict[str, object]:
|
|
try:
|
|
return candidate_graph_payload(
|
|
service.candidate_graph(repository_id, analysis_run_id)
|
|
)
|
|
except NotFoundError as exc:
|
|
raise HTTPException(status_code=404, detail=str(exc)) from exc
|
|
|
|
|
|
@app.post(
|
|
"/repos/{repository_id}/analysis-runs/{analysis_run_id}/candidate-graph/approve",
|
|
tags=["review"],
|
|
response_model=RepositoryAbilityMapResponse,
|
|
)
|
|
def approve_candidate_graph(
|
|
repository_id: int,
|
|
analysis_run_id: int,
|
|
payload: CandidateGraphApproval,
|
|
service: RegistryService = Depends(get_service),
|
|
) -> dict[str, object]:
|
|
try:
|
|
return asdict(
|
|
service.approve_candidate_graph(
|
|
repository_id,
|
|
analysis_run_id,
|
|
notes=payload.notes,
|
|
)
|
|
)
|
|
except NotFoundError as exc:
|
|
raise HTTPException(status_code=404, detail=str(exc)) from exc
|
|
|
|
|
|
@app.post(
|
|
"/repos/{repository_id}/analysis-runs/{analysis_run_id}/changes/approve",
|
|
tags=["review"],
|
|
response_model=RepositoryAbilityMapResponse,
|
|
)
|
|
def approve_analysis_run_changes(
|
|
repository_id: int,
|
|
analysis_run_id: int,
|
|
payload: AnalysisRunChangeApproval,
|
|
service: RegistryService = Depends(get_service),
|
|
) -> dict[str, object]:
|
|
try:
|
|
return asdict(
|
|
service.approve_analysis_run_changes(
|
|
repository_id,
|
|
analysis_run_id,
|
|
notes=payload.notes,
|
|
)
|
|
)
|
|
except NotFoundError as exc:
|
|
raise HTTPException(status_code=404, detail=str(exc)) from exc
|
|
|
|
|
|
@app.post(
|
|
"/repos/{repository_id}/analysis-runs/{analysis_run_id}"
|
|
"/candidate-abilities/{candidate_ability_id}/reject",
|
|
tags=["review"],
|
|
response_model=CandidateGraphResponse,
|
|
)
|
|
def reject_candidate_ability(
|
|
repository_id: int,
|
|
analysis_run_id: int,
|
|
candidate_ability_id: int,
|
|
payload: CandidateRejection,
|
|
service: RegistryService = Depends(get_service),
|
|
) -> dict[str, object]:
|
|
try:
|
|
return candidate_graph_payload(
|
|
service.reject_candidate_ability(
|
|
repository_id,
|
|
analysis_run_id,
|
|
candidate_ability_id,
|
|
notes=payload.notes,
|
|
)
|
|
)
|
|
except NotFoundError as exc:
|
|
raise HTTPException(status_code=404, detail=str(exc)) from exc
|
|
|
|
|
|
@app.post(
|
|
"/repos/{repository_id}/analysis-runs/{analysis_run_id}"
|
|
"/candidate-capabilities/{candidate_capability_id}/reject",
|
|
tags=["review"],
|
|
response_model=CandidateGraphResponse,
|
|
)
|
|
def reject_candidate_capability(
|
|
repository_id: int,
|
|
analysis_run_id: int,
|
|
candidate_capability_id: int,
|
|
payload: CandidateRejection,
|
|
service: RegistryService = Depends(get_service),
|
|
) -> dict[str, object]:
|
|
try:
|
|
return candidate_graph_payload(
|
|
service.reject_candidate_capability(
|
|
repository_id,
|
|
analysis_run_id,
|
|
candidate_capability_id,
|
|
notes=payload.notes,
|
|
)
|
|
)
|
|
except NotFoundError as exc:
|
|
raise HTTPException(status_code=404, detail=str(exc)) from exc
|
|
|
|
|
|
@app.post(
|
|
"/repos/{repository_id}/analysis-runs/{analysis_run_id}"
|
|
"/candidate-features/{candidate_feature_id}/reject",
|
|
tags=["review"],
|
|
response_model=CandidateGraphResponse,
|
|
)
|
|
def reject_candidate_feature(
|
|
repository_id: int,
|
|
analysis_run_id: int,
|
|
candidate_feature_id: int,
|
|
payload: CandidateRejection,
|
|
service: RegistryService = Depends(get_service),
|
|
) -> dict[str, object]:
|
|
try:
|
|
return candidate_graph_payload(
|
|
service.reject_candidate_feature(
|
|
repository_id,
|
|
analysis_run_id,
|
|
candidate_feature_id,
|
|
notes=payload.notes,
|
|
)
|
|
)
|
|
except NotFoundError as exc:
|
|
raise HTTPException(status_code=404, detail=str(exc)) from exc
|
|
|
|
|
|
@app.post(
|
|
"/repos/{repository_id}/analysis-runs/{analysis_run_id}"
|
|
"/candidate-evidence/{candidate_evidence_id}/reject",
|
|
tags=["review"],
|
|
response_model=CandidateGraphResponse,
|
|
)
|
|
def reject_candidate_evidence(
|
|
repository_id: int,
|
|
analysis_run_id: int,
|
|
candidate_evidence_id: int,
|
|
payload: CandidateRejection,
|
|
service: RegistryService = Depends(get_service),
|
|
) -> dict[str, object]:
|
|
try:
|
|
return candidate_graph_payload(
|
|
service.reject_candidate_evidence(
|
|
repository_id,
|
|
analysis_run_id,
|
|
candidate_evidence_id,
|
|
notes=payload.notes,
|
|
)
|
|
)
|
|
except NotFoundError as exc:
|
|
raise HTTPException(status_code=404, detail=str(exc)) from exc
|
|
|
|
|
|
@app.patch(
|
|
"/repos/{repository_id}/analysis-runs/{analysis_run_id}"
|
|
"/candidate-abilities/{candidate_ability_id}",
|
|
tags=["review"],
|
|
response_model=CandidateGraphResponse,
|
|
)
|
|
def edit_candidate_ability(
|
|
repository_id: int,
|
|
analysis_run_id: int,
|
|
candidate_ability_id: int,
|
|
payload: CandidateEdit,
|
|
service: RegistryService = Depends(get_service),
|
|
) -> dict[str, object]:
|
|
try:
|
|
return candidate_graph_payload(
|
|
service.edit_candidate_ability(
|
|
repository_id,
|
|
analysis_run_id,
|
|
candidate_ability_id,
|
|
**payload.model_dump(),
|
|
)
|
|
)
|
|
except NotFoundError as exc:
|
|
raise HTTPException(status_code=404, detail=str(exc)) from exc
|
|
|
|
|
|
@app.patch(
|
|
"/repos/{repository_id}/analysis-runs/{analysis_run_id}"
|
|
"/candidate-capabilities/{candidate_capability_id}",
|
|
tags=["review"],
|
|
response_model=CandidateGraphResponse,
|
|
)
|
|
def edit_candidate_capability(
|
|
repository_id: int,
|
|
analysis_run_id: int,
|
|
candidate_capability_id: int,
|
|
payload: CandidateEdit,
|
|
service: RegistryService = Depends(get_service),
|
|
) -> dict[str, object]:
|
|
try:
|
|
return candidate_graph_payload(
|
|
service.edit_candidate_capability(
|
|
repository_id,
|
|
analysis_run_id,
|
|
candidate_capability_id,
|
|
**payload.model_dump(),
|
|
)
|
|
)
|
|
except NotFoundError as exc:
|
|
raise HTTPException(status_code=404, detail=str(exc)) from exc
|
|
|
|
|
|
@app.post(
|
|
"/repos/{repository_id}/analysis-runs/{analysis_run_id}"
|
|
"/candidate-capabilities/{candidate_capability_id}/relink",
|
|
tags=["review"],
|
|
response_model=CandidateGraphResponse,
|
|
)
|
|
def relink_candidate_capability(
|
|
repository_id: int,
|
|
analysis_run_id: int,
|
|
candidate_capability_id: int,
|
|
payload: CandidateCapabilityRelink,
|
|
service: RegistryService = Depends(get_service),
|
|
) -> dict[str, object]:
|
|
try:
|
|
return candidate_graph_payload(
|
|
service.relink_candidate_capability(
|
|
repository_id,
|
|
analysis_run_id,
|
|
candidate_capability_id,
|
|
**payload.model_dump(),
|
|
)
|
|
)
|
|
except NotFoundError as exc:
|
|
raise HTTPException(status_code=404, detail=str(exc)) from exc
|
|
|
|
|
|
@app.post(
|
|
"/repos/{repository_id}/analysis-runs/{analysis_run_id}"
|
|
"/candidate-features/{candidate_feature_id}/relink",
|
|
tags=["review"],
|
|
response_model=CandidateGraphResponse,
|
|
)
|
|
def relink_candidate_feature(
|
|
repository_id: int,
|
|
analysis_run_id: int,
|
|
candidate_feature_id: int,
|
|
payload: CandidateLeafRelink,
|
|
service: RegistryService = Depends(get_service),
|
|
) -> dict[str, object]:
|
|
try:
|
|
return candidate_graph_payload(
|
|
service.relink_candidate_feature(
|
|
repository_id,
|
|
analysis_run_id,
|
|
candidate_feature_id,
|
|
**payload.model_dump(),
|
|
)
|
|
)
|
|
except NotFoundError as exc:
|
|
raise HTTPException(status_code=404, detail=str(exc)) from exc
|
|
|
|
|
|
@app.post(
|
|
"/repos/{repository_id}/analysis-runs/{analysis_run_id}"
|
|
"/candidate-evidence/{candidate_evidence_id}/relink",
|
|
tags=["review"],
|
|
response_model=CandidateGraphResponse,
|
|
)
|
|
def relink_candidate_evidence(
|
|
repository_id: int,
|
|
analysis_run_id: int,
|
|
candidate_evidence_id: int,
|
|
payload: CandidateLeafRelink,
|
|
service: RegistryService = Depends(get_service),
|
|
) -> dict[str, object]:
|
|
try:
|
|
return candidate_graph_payload(
|
|
service.relink_candidate_evidence(
|
|
repository_id,
|
|
analysis_run_id,
|
|
candidate_evidence_id,
|
|
**payload.model_dump(),
|
|
)
|
|
)
|
|
except NotFoundError as exc:
|
|
raise HTTPException(status_code=404, detail=str(exc)) from exc
|
|
|
|
|
|
@app.post(
|
|
"/repos/{repository_id}/analysis-runs/{analysis_run_id}"
|
|
"/candidate-abilities/{source_ability_id}/merge",
|
|
tags=["review"],
|
|
response_model=CandidateGraphResponse,
|
|
)
|
|
def merge_candidate_ability(
|
|
repository_id: int,
|
|
analysis_run_id: int,
|
|
source_ability_id: int,
|
|
payload: CandidateAbilityMerge,
|
|
service: RegistryService = Depends(get_service),
|
|
) -> dict[str, object]:
|
|
try:
|
|
return candidate_graph_payload(
|
|
service.merge_candidate_ability(
|
|
repository_id,
|
|
analysis_run_id,
|
|
source_ability_id,
|
|
**payload.model_dump(),
|
|
)
|
|
)
|
|
except (NotFoundError, ValueError) as exc:
|
|
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
|
|
|
|
|
@app.post(
|
|
"/repos/{repository_id}/analysis-runs/{analysis_run_id}"
|
|
"/candidate-capabilities/{source_capability_id}/merge",
|
|
tags=["review"],
|
|
response_model=CandidateGraphResponse,
|
|
)
|
|
def merge_candidate_capability(
|
|
repository_id: int,
|
|
analysis_run_id: int,
|
|
source_capability_id: int,
|
|
payload: CandidateCapabilityMerge,
|
|
service: RegistryService = Depends(get_service),
|
|
) -> dict[str, object]:
|
|
try:
|
|
return candidate_graph_payload(
|
|
service.merge_candidate_capability(
|
|
repository_id,
|
|
analysis_run_id,
|
|
source_capability_id,
|
|
**payload.model_dump(),
|
|
)
|
|
)
|
|
except (NotFoundError, ValueError) as exc:
|
|
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
|
|
|
|
|
@app.post(
|
|
"/repos/{repository_id}/analysis-runs/{analysis_run_id}"
|
|
"/candidate-features/{source_feature_id}/merge",
|
|
tags=["review"],
|
|
response_model=CandidateGraphResponse,
|
|
)
|
|
def merge_candidate_feature(
|
|
repository_id: int,
|
|
analysis_run_id: int,
|
|
source_feature_id: int,
|
|
payload: CandidateFeatureMerge,
|
|
service: RegistryService = Depends(get_service),
|
|
) -> dict[str, object]:
|
|
try:
|
|
return candidate_graph_payload(
|
|
service.merge_candidate_feature(
|
|
repository_id,
|
|
analysis_run_id,
|
|
source_feature_id,
|
|
**payload.model_dump(),
|
|
)
|
|
)
|
|
except (NotFoundError, ValueError) as exc:
|
|
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
|
|
|
|
|
@app.post(
|
|
"/repos/{repository_id}/analysis-runs/{analysis_run_id}"
|
|
"/candidate-evidence/{source_evidence_id}/merge",
|
|
tags=["review"],
|
|
response_model=CandidateGraphResponse,
|
|
)
|
|
def merge_candidate_evidence(
|
|
repository_id: int,
|
|
analysis_run_id: int,
|
|
source_evidence_id: int,
|
|
payload: CandidateEvidenceMerge,
|
|
service: RegistryService = Depends(get_service),
|
|
) -> dict[str, object]:
|
|
try:
|
|
return candidate_graph_payload(
|
|
service.merge_candidate_evidence(
|
|
repository_id,
|
|
analysis_run_id,
|
|
source_evidence_id,
|
|
**payload.model_dump(),
|
|
)
|
|
)
|
|
except (NotFoundError, ValueError) as exc:
|
|
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
|
|
|
|
|
@app.post(
|
|
"/repos/{repository_id}/abilities",
|
|
status_code=201,
|
|
tags=["registry"],
|
|
response_model=IdResponse,
|
|
)
|
|
def create_ability(
|
|
repository_id: int,
|
|
payload: AbilityCreate,
|
|
service: RegistryService = Depends(get_service),
|
|
) -> dict[str, int]:
|
|
try:
|
|
ability_id = service.add_ability(repository_id, **payload.model_dump())
|
|
except NotFoundError as exc:
|
|
raise HTTPException(status_code=404, detail=str(exc)) from exc
|
|
return {"id": ability_id}
|
|
|
|
|
|
@app.patch(
|
|
"/repos/{repository_id}/abilities/{ability_id}",
|
|
tags=["registry"],
|
|
response_model=RepositoryAbilityMapResponse,
|
|
)
|
|
def update_ability(
|
|
repository_id: int,
|
|
ability_id: int,
|
|
payload: AbilityUpdate,
|
|
service: RegistryService = Depends(get_service),
|
|
) -> dict[str, object]:
|
|
try:
|
|
return asdict(
|
|
service.update_ability(
|
|
repository_id,
|
|
ability_id,
|
|
**payload.model_dump(exclude_unset=True),
|
|
)
|
|
)
|
|
except NotFoundError as exc:
|
|
raise HTTPException(status_code=404, detail=str(exc)) from exc
|
|
|
|
|
|
@app.delete(
|
|
"/repos/{repository_id}/abilities/{ability_id}",
|
|
tags=["registry"],
|
|
response_model=RepositoryAbilityMapResponse,
|
|
)
|
|
def delete_ability(
|
|
repository_id: int,
|
|
ability_id: int,
|
|
service: RegistryService = Depends(get_service),
|
|
) -> dict[str, object]:
|
|
try:
|
|
return asdict(service.delete_ability(repository_id, ability_id))
|
|
except NotFoundError as exc:
|
|
raise HTTPException(status_code=404, detail=str(exc)) from exc
|
|
|
|
|
|
@app.post(
|
|
"/repos/{repository_id}/capabilities",
|
|
status_code=201,
|
|
tags=["registry"],
|
|
response_model=IdResponse,
|
|
)
|
|
def create_capability(
|
|
repository_id: int,
|
|
payload: CapabilityCreate,
|
|
service: RegistryService = Depends(get_service),
|
|
) -> dict[str, int]:
|
|
try:
|
|
capability_id = service.add_capability(repository_id, **payload.model_dump())
|
|
except NotFoundError as exc:
|
|
raise HTTPException(status_code=404, detail=str(exc)) from exc
|
|
return {"id": capability_id}
|
|
|
|
|
|
@app.patch(
|
|
"/repos/{repository_id}/capabilities/{capability_id}",
|
|
tags=["registry"],
|
|
response_model=RepositoryAbilityMapResponse,
|
|
)
|
|
def update_capability(
|
|
repository_id: int,
|
|
capability_id: int,
|
|
payload: CapabilityUpdate,
|
|
service: RegistryService = Depends(get_service),
|
|
) -> dict[str, object]:
|
|
try:
|
|
return asdict(
|
|
service.update_capability(
|
|
repository_id,
|
|
capability_id,
|
|
**payload.model_dump(exclude_unset=True),
|
|
)
|
|
)
|
|
except NotFoundError as exc:
|
|
raise HTTPException(status_code=404, detail=str(exc)) from exc
|
|
|
|
|
|
@app.delete(
|
|
"/repos/{repository_id}/capabilities/{capability_id}",
|
|
tags=["registry"],
|
|
response_model=RepositoryAbilityMapResponse,
|
|
)
|
|
def delete_capability(
|
|
repository_id: int,
|
|
capability_id: int,
|
|
service: RegistryService = Depends(get_service),
|
|
) -> dict[str, object]:
|
|
try:
|
|
return asdict(service.delete_capability(repository_id, capability_id))
|
|
except NotFoundError as exc:
|
|
raise HTTPException(status_code=404, detail=str(exc)) from exc
|
|
|
|
|
|
@app.post(
|
|
"/repos/{repository_id}/features",
|
|
status_code=201,
|
|
tags=["registry"],
|
|
response_model=IdResponse,
|
|
)
|
|
def create_feature(
|
|
repository_id: int,
|
|
payload: FeatureCreate,
|
|
service: RegistryService = Depends(get_service),
|
|
) -> dict[str, int]:
|
|
try:
|
|
feature_id = service.add_feature(repository_id, **payload.model_dump())
|
|
except NotFoundError as exc:
|
|
raise HTTPException(status_code=404, detail=str(exc)) from exc
|
|
return {"id": feature_id}
|
|
|
|
|
|
@app.patch(
|
|
"/repos/{repository_id}/features/{feature_id}",
|
|
tags=["registry"],
|
|
response_model=RepositoryAbilityMapResponse,
|
|
)
|
|
def update_feature(
|
|
repository_id: int,
|
|
feature_id: int,
|
|
payload: FeatureUpdate,
|
|
service: RegistryService = Depends(get_service),
|
|
) -> dict[str, object]:
|
|
try:
|
|
return asdict(
|
|
service.update_feature(
|
|
repository_id,
|
|
feature_id,
|
|
**payload.model_dump(exclude_unset=True),
|
|
)
|
|
)
|
|
except NotFoundError as exc:
|
|
raise HTTPException(status_code=404, detail=str(exc)) from exc
|
|
|
|
|
|
@app.delete(
|
|
"/repos/{repository_id}/features/{feature_id}",
|
|
tags=["registry"],
|
|
response_model=RepositoryAbilityMapResponse,
|
|
)
|
|
def delete_feature(
|
|
repository_id: int,
|
|
feature_id: int,
|
|
service: RegistryService = Depends(get_service),
|
|
) -> dict[str, object]:
|
|
try:
|
|
return asdict(service.delete_feature(repository_id, feature_id))
|
|
except NotFoundError as exc:
|
|
raise HTTPException(status_code=404, detail=str(exc)) from exc
|
|
|
|
|
|
@app.post(
|
|
"/repos/{repository_id}/evidence",
|
|
status_code=201,
|
|
tags=["registry"],
|
|
response_model=IdResponse,
|
|
)
|
|
def create_evidence(
|
|
repository_id: int,
|
|
payload: EvidenceCreate,
|
|
service: RegistryService = Depends(get_service),
|
|
) -> dict[str, int]:
|
|
try:
|
|
evidence_id = service.add_evidence(repository_id, **payload.model_dump())
|
|
except NotFoundError as exc:
|
|
raise HTTPException(status_code=404, detail=str(exc)) from exc
|
|
return {"id": evidence_id}
|
|
|
|
|
|
@app.patch(
|
|
"/repos/{repository_id}/evidence/{evidence_id}",
|
|
tags=["registry"],
|
|
response_model=RepositoryAbilityMapResponse,
|
|
)
|
|
def update_evidence(
|
|
repository_id: int,
|
|
evidence_id: int,
|
|
payload: EvidenceUpdate,
|
|
service: RegistryService = Depends(get_service),
|
|
) -> dict[str, object]:
|
|
try:
|
|
return asdict(
|
|
service.update_evidence(
|
|
repository_id,
|
|
evidence_id,
|
|
**payload.model_dump(exclude_unset=True),
|
|
)
|
|
)
|
|
except NotFoundError as exc:
|
|
raise HTTPException(status_code=404, detail=str(exc)) from exc
|
|
|
|
|
|
@app.delete(
|
|
"/repos/{repository_id}/evidence/{evidence_id}",
|
|
tags=["registry"],
|
|
response_model=RepositoryAbilityMapResponse,
|
|
)
|
|
def delete_evidence(
|
|
repository_id: int,
|
|
evidence_id: int,
|
|
service: RegistryService = Depends(get_service),
|
|
) -> dict[str, object]:
|
|
try:
|
|
return asdict(service.delete_evidence(repository_id, evidence_id))
|
|
except NotFoundError as exc:
|
|
raise HTTPException(status_code=404, detail=str(exc)) from exc
|
|
|
|
|
|
@app.get(
|
|
"/repos/{repository_id}/ability-map",
|
|
tags=["registry"],
|
|
response_model=RepositoryAbilityMapResponse,
|
|
)
|
|
def get_ability_map(
|
|
repository_id: int,
|
|
service: RegistryService = Depends(get_service),
|
|
) -> dict[str, object]:
|
|
try:
|
|
return asdict(service.ability_map(repository_id))
|
|
except NotFoundError as exc:
|
|
raise HTTPException(status_code=404, detail=str(exc)) from exc
|
|
|
|
|
|
@app.get(
|
|
"/repos/{repository_id}/dependency-graph",
|
|
tags=["visualization"],
|
|
)
|
|
def get_dependency_graph(
|
|
repository_id: int,
|
|
base_analysis_run_id: int | None = Query(default=None),
|
|
target_analysis_run_id: int | None = Query(default=None),
|
|
profile_id: int | None = Query(default=None),
|
|
use_latest_profile: bool = Query(default=True),
|
|
service: RegistryService = Depends(get_service),
|
|
) -> dict[str, object]:
|
|
try:
|
|
return service.dependency_graph_elements(
|
|
repository_id,
|
|
base_analysis_run_id=base_analysis_run_id,
|
|
target_analysis_run_id=target_analysis_run_id,
|
|
profile_id=profile_id,
|
|
use_latest_profile=use_latest_profile,
|
|
)
|
|
except NotFoundError as exc:
|
|
raise HTTPException(status_code=404, detail=str(exc)) from exc
|
|
except ValueError as exc:
|
|
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
|
|
|
|
|
@app.post(
|
|
"/repos/{repository_id}/dependency-graph/filter",
|
|
tags=["visualization"],
|
|
)
|
|
def filter_dependency_graph(
|
|
repository_id: int,
|
|
payload: DependencyGraphAdHocFilters,
|
|
base_analysis_run_id: int | None = Query(default=None),
|
|
target_analysis_run_id: int | None = Query(default=None),
|
|
profile_id: int | None = Query(default=None),
|
|
service: RegistryService = Depends(get_service),
|
|
) -> dict[str, object]:
|
|
try:
|
|
return service.dependency_graph_elements(
|
|
repository_id,
|
|
base_analysis_run_id=base_analysis_run_id,
|
|
target_analysis_run_id=target_analysis_run_id,
|
|
profile_id=profile_id,
|
|
rules=payload.rules,
|
|
manual_overrides=payload.manual_overrides,
|
|
use_latest_profile=profile_id is not None,
|
|
)
|
|
except NotFoundError as exc:
|
|
raise HTTPException(status_code=404, detail=str(exc)) from exc
|
|
except ValueError as exc:
|
|
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
|
|
|
|
|
@app.get(
|
|
"/repos/{repository_id}/dependency-graph/profiles",
|
|
tags=["visualization"],
|
|
response_model=list[DependencyGraphProfileResponse],
|
|
)
|
|
def list_dependency_graph_profiles(
|
|
repository_id: int,
|
|
service: RegistryService = Depends(get_service),
|
|
) -> list[dict[str, object]]:
|
|
try:
|
|
return [
|
|
asdict(profile)
|
|
for profile in service.list_dependency_graph_profiles(repository_id)
|
|
]
|
|
except NotFoundError as exc:
|
|
raise HTTPException(status_code=404, detail=str(exc)) from exc
|
|
|
|
|
|
@app.post(
|
|
"/repos/{repository_id}/dependency-graph/profiles",
|
|
tags=["visualization"],
|
|
status_code=201,
|
|
response_model=DependencyGraphProfileResponse,
|
|
)
|
|
def create_dependency_graph_profile(
|
|
repository_id: int,
|
|
payload: DependencyGraphProfileCreate,
|
|
service: RegistryService = Depends(get_service),
|
|
) -> dict[str, object]:
|
|
try:
|
|
return asdict(
|
|
service.create_dependency_graph_profile(
|
|
repository_id,
|
|
**payload.model_dump(),
|
|
)
|
|
)
|
|
except NotFoundError as exc:
|
|
raise HTTPException(status_code=404, detail=str(exc)) from exc
|
|
except ValueError as exc:
|
|
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
|
|
|
|
|
@app.get(
|
|
"/repos/{repository_id}/dependency-graph/profiles/{profile_id}",
|
|
tags=["visualization"],
|
|
response_model=DependencyGraphProfileResponse,
|
|
)
|
|
def get_dependency_graph_profile(
|
|
repository_id: int,
|
|
profile_id: int,
|
|
service: RegistryService = Depends(get_service),
|
|
) -> dict[str, object]:
|
|
try:
|
|
return asdict(service.get_dependency_graph_profile(repository_id, profile_id))
|
|
except NotFoundError as exc:
|
|
raise HTTPException(status_code=404, detail=str(exc)) from exc
|
|
|
|
|
|
@app.patch(
|
|
"/repos/{repository_id}/dependency-graph/profiles/{profile_id}",
|
|
tags=["visualization"],
|
|
response_model=DependencyGraphProfileResponse,
|
|
)
|
|
def update_dependency_graph_profile(
|
|
repository_id: int,
|
|
profile_id: int,
|
|
payload: DependencyGraphProfileUpdate,
|
|
service: RegistryService = Depends(get_service),
|
|
) -> dict[str, object]:
|
|
try:
|
|
return asdict(
|
|
service.update_dependency_graph_profile(
|
|
repository_id,
|
|
profile_id,
|
|
**payload.model_dump(exclude_unset=True),
|
|
)
|
|
)
|
|
except NotFoundError as exc:
|
|
raise HTTPException(status_code=404, detail=str(exc)) from exc
|
|
except ValueError as exc:
|
|
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
|
|
|
|
|
@app.post(
|
|
"/repos/{repository_id}/dependency-graph/profiles/{profile_id}/duplicate",
|
|
tags=["visualization"],
|
|
status_code=201,
|
|
response_model=DependencyGraphProfileResponse,
|
|
)
|
|
def duplicate_dependency_graph_profile(
|
|
repository_id: int,
|
|
profile_id: int,
|
|
payload: DependencyGraphProfileDuplicate,
|
|
service: RegistryService = Depends(get_service),
|
|
) -> dict[str, object]:
|
|
try:
|
|
return asdict(
|
|
service.duplicate_dependency_graph_profile(
|
|
repository_id,
|
|
profile_id,
|
|
name=payload.name,
|
|
)
|
|
)
|
|
except NotFoundError as exc:
|
|
raise HTTPException(status_code=404, detail=str(exc)) from exc
|
|
except ValueError as exc:
|
|
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
|
|
|
|
|
@app.delete(
|
|
"/repos/{repository_id}/dependency-graph/profiles/{profile_id}",
|
|
tags=["visualization"],
|
|
status_code=204,
|
|
)
|
|
def delete_dependency_graph_profile(
|
|
repository_id: int,
|
|
profile_id: int,
|
|
service: RegistryService = Depends(get_service),
|
|
) -> None:
|
|
try:
|
|
service.delete_dependency_graph_profile(repository_id, profile_id)
|
|
except NotFoundError as exc:
|
|
raise HTTPException(status_code=404, detail=str(exc)) from exc
|
|
|
|
|
|
@app.get(
|
|
"/repos/{repository_id}/export",
|
|
tags=["discovery"],
|
|
response_class=PlainTextResponse,
|
|
responses={
|
|
200: {
|
|
"content": {"application/x-yaml": {}},
|
|
"description": "YAML registry export suitable for repo-abilities.yaml.",
|
|
}
|
|
},
|
|
)
|
|
def export_repository_registry_entry(
|
|
repository_id: int,
|
|
service: RegistryService = Depends(get_service),
|
|
) -> PlainTextResponse:
|
|
try:
|
|
content = service.export_registry_entry(repository_id)
|
|
except NotFoundError as exc:
|
|
raise HTTPException(status_code=404, detail=str(exc)) from exc
|
|
return PlainTextResponse(content, media_type="application/x-yaml")
|
|
|
|
|
|
@app.get(
|
|
"/repos/{repo_slug}/scope/context",
|
|
tags=["scope"],
|
|
response_model=RepositoryScopeContextResponse,
|
|
)
|
|
def get_repository_scope_context(
|
|
repo_slug: str,
|
|
service: RegistryService = Depends(get_service),
|
|
settings: Settings = Depends(get_settings),
|
|
) -> dict[str, object]:
|
|
try:
|
|
repository = repository_by_slug(service, repo_slug)
|
|
ability_map = service.ability_map(repository.id)
|
|
except NotFoundError as exc:
|
|
raise HTTPException(status_code=404, detail=str(exc)) from exc
|
|
|
|
scope_path = inspectable_scope_file_path(service, repository, repo_slug, settings)
|
|
scope_md_exists = bool(scope_path and scope_path.is_file())
|
|
scope_summary = (
|
|
scope_summary_from_file(scope_path)
|
|
if scope_md_exists and scope_path is not None
|
|
else None
|
|
)
|
|
if not scope_summary:
|
|
scope_summary = (
|
|
ability_map.scope.description.strip()
|
|
or (repository.description.strip() if repository.description else None)
|
|
or None
|
|
)
|
|
|
|
return {
|
|
"repo_slug": slugify(repo_slug),
|
|
"capabilities": scope_context_capabilities(ability_map),
|
|
"tags": scope_context_tags(ability_map),
|
|
"scope_md_exists": scope_md_exists,
|
|
"scope_summary": scope_summary,
|
|
}
|
|
|
|
|
|
@app.get(
|
|
"/repos/{repo_slug}/scope",
|
|
tags=["scope"],
|
|
response_class=PlainTextResponse,
|
|
responses={
|
|
200: {
|
|
"content": {"text/markdown": {}},
|
|
"description": "Generated SCOPE.md preview from approved characteristics.",
|
|
}
|
|
},
|
|
)
|
|
def generate_repository_scope(
|
|
repo_slug: str,
|
|
service: RegistryService = Depends(get_service),
|
|
) -> PlainTextResponse:
|
|
try:
|
|
ensure_scope_generation_ready(service, repo_slug)
|
|
content = ScopeGenerator(service).generate(repo_slug)
|
|
except NotFoundError as exc:
|
|
raise HTTPException(status_code=404, detail=str(exc)) from exc
|
|
return PlainTextResponse(content, media_type="text/markdown")
|
|
|
|
|
|
@app.get(
|
|
"/repos/{repo_slug}/scope/diff",
|
|
tags=["scope"],
|
|
)
|
|
def diff_repository_scope(
|
|
repo_slug: str,
|
|
service: RegistryService = Depends(get_service),
|
|
settings: Settings = Depends(get_settings),
|
|
) -> dict[str, object]:
|
|
try:
|
|
repository = ensure_scope_generation_ready(service, repo_slug)
|
|
scope_path = scope_file_path(service, repository, repo_slug, settings)
|
|
diff = ScopeValidator(ScopeGenerator(service)).diff(repo_slug, scope_path)
|
|
except NotFoundError as exc:
|
|
raise HTTPException(status_code=404, detail=str(exc)) from exc
|
|
except ValueError as exc:
|
|
raise HTTPException(status_code=409, detail=str(exc)) from exc
|
|
return {
|
|
"sections": [asdict(section) for section in diff.sections],
|
|
"needs_update": diff.needs_update,
|
|
}
|
|
|
|
|
|
@app.post(
|
|
"/repos/{repo_slug}/scope/write",
|
|
tags=["scope"],
|
|
)
|
|
def write_repository_scope(
|
|
repo_slug: str,
|
|
service: RegistryService = Depends(get_service),
|
|
settings: Settings = Depends(get_settings),
|
|
) -> dict[str, object]:
|
|
try:
|
|
repository = ensure_scope_generation_ready(service, repo_slug)
|
|
scope_path = scope_file_path(service, repository, repo_slug, settings)
|
|
content = ScopeGenerator(service).generate(repo_slug)
|
|
except NotFoundError as exc:
|
|
raise HTTPException(status_code=404, detail=str(exc)) from exc
|
|
except ValueError as exc:
|
|
raise HTTPException(status_code=409, detail=str(exc)) from exc
|
|
scope_path.write_text(content, encoding="utf-8")
|
|
return {"written": True, "path": str(scope_path)}
|
|
|
|
|
|
def scope_context_capabilities(ability_map) -> list[str]:
|
|
return unique_preserving_order(
|
|
[
|
|
capability.name
|
|
for ability in ability_map.abilities
|
|
for capability in ability.capabilities
|
|
]
|
|
)
|
|
|
|
|
|
def scope_context_tags(ability_map) -> list[str]:
|
|
tags = []
|
|
for ability in ability_map.abilities:
|
|
tags.append(ability.primary_class)
|
|
tags.extend(ability.attributes)
|
|
for capability in ability.capabilities:
|
|
tags.append(capability.primary_class)
|
|
tags.extend(capability.attributes)
|
|
return sorted(unique_preserving_order(tags), key=str.lower)
|
|
|
|
|
|
def unique_preserving_order(values: list[str]) -> list[str]:
|
|
result: list[str] = []
|
|
seen: set[str] = set()
|
|
for value in values:
|
|
item = str(value).strip()
|
|
key = item.lower()
|
|
if not item or key in seen:
|
|
continue
|
|
seen.add(key)
|
|
result.append(item)
|
|
return result
|
|
|
|
|
|
def inspectable_scope_file_path(
|
|
service: RegistryService,
|
|
repository,
|
|
repo_slug: str,
|
|
settings: Settings,
|
|
) -> Path | None:
|
|
try:
|
|
state_hub_path = state_hub_scope_file_path(repo_slug, settings)
|
|
except ValueError:
|
|
state_hub_path = None
|
|
if state_hub_path is not None:
|
|
return state_hub_path
|
|
|
|
source_path = Path(repository.url)
|
|
if source_path.exists() and source_path.is_dir():
|
|
return source_path / "SCOPE.md"
|
|
|
|
checkout = service.ingestion.cached_checkout(repository.url)
|
|
if checkout is not None and checkout.source_path.exists():
|
|
return checkout.source_path / "SCOPE.md"
|
|
return None
|
|
|
|
|
|
def scope_summary_from_file(scope_path: Path) -> str | None:
|
|
try:
|
|
content = scope_path.read_text(encoding="utf-8")
|
|
except OSError:
|
|
return None
|
|
|
|
paragraph: list[str] = []
|
|
in_frontmatter = False
|
|
frontmatter_checked = False
|
|
for line in content.splitlines():
|
|
stripped = line.strip()
|
|
if not frontmatter_checked:
|
|
frontmatter_checked = True
|
|
if stripped == "---":
|
|
in_frontmatter = True
|
|
continue
|
|
if in_frontmatter:
|
|
if stripped == "---":
|
|
in_frontmatter = False
|
|
continue
|
|
if not stripped:
|
|
if paragraph:
|
|
return " ".join(paragraph)
|
|
continue
|
|
if stripped == "---" or stripped.startswith("#") or stripped.startswith(">"):
|
|
if paragraph:
|
|
return " ".join(paragraph)
|
|
continue
|
|
paragraph.append(stripped)
|
|
if paragraph:
|
|
return " ".join(paragraph)
|
|
return None
|
|
|
|
|
|
def ensure_scope_generation_ready(
|
|
service: RegistryService,
|
|
repo_slug: str,
|
|
):
|
|
repository = repository_by_slug(service, repo_slug)
|
|
ability_map = service.ability_map(repository.id)
|
|
if not ability_map.abilities:
|
|
raise NotFoundError(
|
|
f"repository {repo_slug!r} has no approved characteristics"
|
|
)
|
|
return repository
|
|
|
|
|
|
def repository_by_slug(service: RegistryService, repo_slug: str):
|
|
wanted = slugify(repo_slug)
|
|
for repository in service.list_repositories():
|
|
candidates = {
|
|
slugify(repository.name),
|
|
slugify(repository.url.rstrip("/").rsplit("/", 1)[-1].removesuffix(".git")),
|
|
}
|
|
if wanted in candidates:
|
|
return repository
|
|
raise NotFoundError(f"repository slug {repo_slug!r} was not found")
|
|
|
|
|
|
def scope_file_path(
|
|
service: RegistryService,
|
|
repository,
|
|
repo_slug: str,
|
|
settings: Settings,
|
|
) -> Path:
|
|
state_hub_path = state_hub_scope_file_path(repo_slug, settings)
|
|
if state_hub_path is not None:
|
|
return state_hub_path
|
|
source_path = Path(repository.url)
|
|
if source_path.exists() and source_path.is_dir():
|
|
return source_path / "SCOPE.md"
|
|
checkout = service.ingestion.cached_checkout(repository.url)
|
|
if checkout is not None and checkout.source_path.exists():
|
|
return checkout.source_path / "SCOPE.md"
|
|
raise ValueError(
|
|
"repository has no known local checkout path on this host"
|
|
)
|
|
|
|
|
|
def state_hub_scope_file_path(repo_slug: str, settings: Settings) -> Path | None:
|
|
base_url = settings.state_hub_base_url.rstrip("/")
|
|
if not base_url:
|
|
return None
|
|
try:
|
|
with urlopen(f"{base_url}/repos/{repo_slug}/", timeout=2) as response:
|
|
repo = json.loads(response.read().decode("utf-8"))
|
|
except HTTPError as exc:
|
|
if exc.code == 404:
|
|
return None
|
|
raise ValueError("state hub repository path lookup failed") from exc
|
|
except (URLError, TimeoutError, OSError, json.JSONDecodeError):
|
|
return None
|
|
local_path = repo.get("local_path")
|
|
if not local_path:
|
|
raise ValueError(
|
|
f"state hub repo {repo_slug!r} has no local path on this host"
|
|
)
|
|
path = Path(local_path)
|
|
if path.exists() and path.is_dir():
|
|
return path / "SCOPE.md"
|
|
raise ValueError(
|
|
f"state hub local path for repo {repo_slug!r} is not available: {path}"
|
|
)
|
|
|
|
|
|
@app.get(
|
|
"/repository-comparisons",
|
|
tags=["discovery"],
|
|
response_model=RepositoryComparisonResponse,
|
|
)
|
|
def compare_repositories(
|
|
repository_ids: list[int] = Query(
|
|
...,
|
|
description="Repository ids to compare by approved abilities and capabilities.",
|
|
min_length=2,
|
|
),
|
|
service: RegistryService = Depends(get_service),
|
|
) -> dict[str, object]:
|
|
try:
|
|
return service.compare_repositories(repository_ids)
|
|
except NotFoundError as exc:
|
|
raise HTTPException(status_code=404, detail=str(exc)) from exc
|
|
|
|
|
|
@app.post(
|
|
"/capability-gaps",
|
|
tags=["discovery"],
|
|
response_model=CapabilityGapResponse,
|
|
)
|
|
def detect_capability_gaps(
|
|
payload: CapabilityGapRequest,
|
|
service: RegistryService = Depends(get_service),
|
|
) -> dict[str, object]:
|
|
try:
|
|
return service.detect_capability_gaps(**payload.model_dump())
|
|
except NotFoundError as exc:
|
|
raise HTTPException(status_code=404, detail=str(exc)) from exc
|
|
|
|
|
|
@app.get("/search", tags=["search"], response_model=list[SearchResultResponse])
|
|
def search(
|
|
q: str = Query(
|
|
...,
|
|
description="Natural-language or keyword query over approved registry entries.",
|
|
examples=["classify email", "FastAPI health endpoint"],
|
|
),
|
|
status: str | None = Query(
|
|
default=None,
|
|
description="Filter repositories by registry status, such as indexed.",
|
|
),
|
|
language: str | None = Query(
|
|
default=None,
|
|
description="Filter by observed programming language.",
|
|
),
|
|
framework: str | None = Query(
|
|
default=None,
|
|
description="Filter by observed framework hint.",
|
|
),
|
|
ability: str | None = Query(
|
|
default=None,
|
|
description="Filter to results under an approved ability name.",
|
|
),
|
|
capability: str | None = Query(
|
|
default=None,
|
|
description="Filter to results under an approved capability name.",
|
|
),
|
|
service: RegistryService = Depends(get_service),
|
|
) -> list[dict[str, object]]:
|
|
return [
|
|
asdict(result)
|
|
for result in service.search(
|
|
q,
|
|
status=status,
|
|
language=language,
|
|
framework=framework,
|
|
ability=ability,
|
|
capability=capability,
|
|
)
|
|
]
|
|
|
|
|
|
@app.get("/abilities", tags=["search"], response_model=list[AbilitySummaryResponse])
|
|
def list_abilities(
|
|
service: RegistryService = Depends(get_service),
|
|
) -> list[dict[str, object]]:
|
|
return [asdict(ability) for ability in service.list_abilities()]
|
|
|
|
|
|
@app.get(
|
|
"/capabilities",
|
|
tags=["search"],
|
|
response_model=list[CapabilitySummaryResponse],
|
|
)
|
|
def list_capabilities(
|
|
service: RegistryService = Depends(get_service),
|
|
) -> list[dict[str, object]]:
|
|
return [asdict(capability) for capability in service.list_capabilities()]
|