repository-scoped dependency graph view profile persistence and interactive exploration features

This commit is contained in:
2026-05-04 11:26:25 +02:00
parent 98a65581ac
commit 4f04491734
11 changed files with 1365 additions and 32 deletions

View File

@@ -44,6 +44,11 @@ from repo_registry.web_api.schemas import (
CapabilitySummaryResponse,
CapabilityUpdate,
ContentChunkResponse,
DependencyGraphAdHocFilters,
DependencyGraphProfileCreate,
DependencyGraphProfileDuplicate,
DependencyGraphProfileResponse,
DependencyGraphProfileUpdate,
EvidenceCreate,
EvidenceUpdate,
ErrorResponse,
@@ -125,6 +130,7 @@ OPENAPI_TAGS = [
{"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."},
]
@@ -1141,12 +1147,13 @@ def get_ability_map(
@app.get(
"/repos/{repository_id}/dependency-graph",
tags=["registry"],
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),
service: RegistryService = Depends(get_service),
) -> dict[str, object]:
try:
@@ -1154,6 +1161,7 @@ def get_dependency_graph(
repository_id,
base_analysis_run_id=base_analysis_run_id,
target_analysis_run_id=target_analysis_run_id,
profile_id=profile_id,
)
except NotFoundError as exc:
raise HTTPException(status_code=404, detail=str(exc)) from exc
@@ -1161,6 +1169,158 @@ def get_dependency_graph(
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,
)
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"],

View File

@@ -308,6 +308,49 @@ class CharacteristicRebuildResponse(BaseModel):
candidate_counts: dict[str, int]
class DependencyGraphRule(BaseModel):
action: str
name: str | None = None
match: dict[str, Any] = Field(default_factory=dict)
class DependencyGraphAdHocFilters(BaseModel):
rules: list[dict[str, Any]] = Field(default_factory=list)
manual_overrides: dict[str, str] = Field(default_factory=dict)
class DependencyGraphProfileCreate(BaseModel):
name: str
description: str = ""
default_mode: str = "full"
filter_rules: list[dict[str, Any]] = Field(default_factory=list)
manual_overrides: dict[str, str] = Field(default_factory=dict)
class DependencyGraphProfileUpdate(BaseModel):
name: str | None = None
description: str | None = None
default_mode: str | None = None
filter_rules: list[dict[str, Any]] | None = None
manual_overrides: dict[str, str] | None = None
class DependencyGraphProfileDuplicate(BaseModel):
name: str | None = None
class DependencyGraphProfileResponse(BaseModel):
id: int
repository_id: int
name: str
description: str
default_mode: str
filter_rules: list[dict[str, Any]]
manual_overrides: dict[str, str]
created_at: str
updated_at: str
class CandidateRejection(BaseModel):
notes: str = ""