Dependency graph vizualization

This commit is contained in:
2026-05-03 12:52:51 +02:00
parent 60ebef0281
commit 8a5e38ce68
7 changed files with 543 additions and 1 deletions

View File

@@ -1090,6 +1090,115 @@ class RegistryService:
graph=graph,
)
def dependency_graph_elements(
self,
repository_id: int,
*,
base_analysis_run_id: int | None = None,
target_analysis_run_id: int | None = None,
) -> dict[str, object]:
impact = None
if base_analysis_run_id is not None or target_analysis_run_id is not None:
if base_analysis_run_id is None or target_analysis_run_id is None:
raise ValueError(
"base_analysis_run_id and target_analysis_run_id must be provided together"
)
impact = self.analyze_dependency_impact(
repository_id,
base_analysis_run_id,
target_analysis_run_id,
)
graph = impact.graph
else:
graph = self.build_dependency_graph(repository_id)
impact_by_key = (
{item.item_key: item for item in impact.impacts} if impact is not None else {}
)
changed_fact_keys = set(impact.changed_fact_keys) if impact is not None else set()
nodes: dict[str, dict[str, object]] = {}
def ensure_node(kind: str, key: str, item_id: int | None) -> None:
if key in nodes:
return
impact_item = impact_by_key.get(key)
is_changed_fact = key in changed_fact_keys
nodes[key] = {
"data": {
"id": key,
"kind": kind,
"label": self._dependency_node_label(repository_id, kind, key, item_id),
"ownership": self._ownership_for_kind(kind),
"freshnessState": (
impact_item.freshness_state
if impact_item is not None
else "changed"
if is_changed_fact
else "current"
),
"recommendedAction": (
impact_item.recommended_action if impact_item is not None else ""
),
"impactDepth": (
impact_item.impact_depth if impact_item is not None else None
),
"reasons": impact_item.reasons if impact_item is not None else [],
},
"classes": " ".join(
class_name
for class_name in (
kind,
"stale" if impact_item is not None else "current",
"changed" if is_changed_fact else "",
)
if class_name
),
}
for edge in graph.edges:
ensure_node(edge.source_kind, edge.source_key, edge.source_id)
ensure_node(edge.target_kind, edge.target_key, edge.target_id)
edges = [
{
"data": {
"id": f"{edge.source_key}->{edge.target_key}:{index}",
"source": edge.source_key,
"target": edge.target_key,
"dependencyType": edge.dependency_type,
"strength": edge.strength,
"edgeSource": edge.source,
"sameLayer": edge.same_layer,
"label": edge.dependency_type,
},
"classes": " ".join(
class_name
for class_name in (
edge.dependency_type,
edge.strength,
"same-layer" if edge.same_layer else "",
)
if class_name
),
}
for index, edge in enumerate(graph.edges)
]
return {
"repository": asdict(graph.repository),
"scope": asdict(graph.scope),
"mode": "impact" if impact is not None else "full",
"metrics": {
"node_count": len(nodes),
"edge_count": len(edges),
"propagation_breadth": impact.propagation_breadth if impact else 0,
"max_depth": impact.max_depth if impact else 0,
"scope_impacted": impact.scope_impacted if impact else False,
},
"changed_fact_keys": impact.changed_fact_keys if impact else [],
"elements": [*nodes.values(), *edges],
"impacts": [asdict(item) for item in impact.impacts] if impact else [],
}
def approve_analysis_run_changes(
self,
repository_id: int,
@@ -2300,6 +2409,24 @@ class RegistryService:
return evidence.reference
return f"{kind}:{item_id}"
def _dependency_node_label(
self,
repository_id: int,
kind: str,
key: str,
item_id: int | None,
) -> str:
if item_id is not None and kind != "fact":
return self._dependency_display_name(repository_id, kind, item_id)
if kind == "fact":
parts = key.split(":", 3)
if len(parts) == 4:
_, fact_kind, path, name = parts
return f"{name} ({fact_kind}, {path})"
if item_id is not None:
return f"{kind}:{item_id}"
return key
def _chunk_index(
self,
chunks: Sequence[ContentChunk],