163 lines
6.1 KiB
Python
163 lines
6.1 KiB
Python
"""Traceability: return graph edges + node summaries for the React Flow view.
|
|
|
|
Endpoint returns a list of ``nodes`` (artifacts) and ``edges`` (relationships)
|
|
that the frontend can feed directly into React Flow to render an
|
|
interactive traceability graph.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from typing import Optional
|
|
from uuid import UUID
|
|
|
|
from fastapi import APIRouter, Depends, HTTPException, Query
|
|
|
|
from pydantic import BaseModel
|
|
from .deps import artifact_service, relationship_service
|
|
from ...services import ArtifactService, RelationshipService
|
|
|
|
router = APIRouter()
|
|
|
|
|
|
class FlowNode(BaseModel):
|
|
id: str
|
|
type: str # artifact type: requirement, task, ...
|
|
label: str
|
|
project_id: Optional[str] = None
|
|
status: Optional[str] = None
|
|
version: int = 1
|
|
source: Optional[str] = None
|
|
|
|
|
|
class FlowEdge(BaseModel):
|
|
id: str
|
|
source: str
|
|
target: str
|
|
label: str # relationship_type, e.g. "implemented_by"
|
|
|
|
|
|
class TraceabilityGraph(BaseModel):
|
|
nodes: list[FlowNode]
|
|
edges: list[FlowEdge]
|
|
mock_fields: list[str] = []
|
|
|
|
|
|
@router.get(
|
|
"/graph",
|
|
response_model=TraceabilityGraph,
|
|
summary="Return a React-Flow-ready graph (nodes+edges) starting from a seed artifact, project, or the entire tenant.",
|
|
)
|
|
async def traceability_graph(
|
|
artifact_id: Optional[UUID] = Query(
|
|
None,
|
|
description="Seed artifact. Edges will be traversed outward (outgoing + incoming).",
|
|
),
|
|
project_id: Optional[UUID] = Query(
|
|
None,
|
|
description="Constrain the graph to a project. When set without artifact_id, returns up to `limit` artifacts.",
|
|
),
|
|
max_depth: int = Query(2, ge=1, le=5, description="BFS depth when seed artifact_id is provided."),
|
|
limit: int = Query(200, ge=1, le=1000, description="Max total nodes returned."),
|
|
artifacts: ArtifactService = Depends(artifact_service),
|
|
relationships: RelationshipService = Depends(relationship_service),
|
|
) -> TraceabilityGraph:
|
|
nodes: dict[UUID, FlowNode] = {}
|
|
edges: list[FlowEdge] = []
|
|
|
|
async def _add_art(ids: list[UUID]) -> None:
|
|
needed = [i for i in ids if i not in nodes and len(nodes) < limit]
|
|
if not needed:
|
|
return
|
|
for art in await artifacts.get_many(needed):
|
|
nodes[art.id] = FlowNode(
|
|
id=str(art.id),
|
|
type=art.type if isinstance(art.type, str) else art.type.value,
|
|
label=art.title,
|
|
project_id=str(art.project_id),
|
|
status=art.status,
|
|
version=art.version,
|
|
source=art.source,
|
|
)
|
|
|
|
if artifact_id is not None:
|
|
# BFS around the seed
|
|
frontier: set[UUID] = {artifact_id}
|
|
visited: set[UUID] = set()
|
|
for _ in range(max_depth):
|
|
current = list(frontier - visited)
|
|
if not current:
|
|
break
|
|
await _add_art(current)
|
|
visited.update(current)
|
|
next_front: set[UUID] = set()
|
|
for aid in current:
|
|
out = await relationships.outgoing(aid, limit=50)
|
|
for rel in out:
|
|
edges.append(
|
|
FlowEdge(
|
|
id=f"e-{rel.id}",
|
|
source=str(rel.source_artifact_id),
|
|
target=str(rel.target_artifact_id),
|
|
label=rel.relationship_type
|
|
if isinstance(rel.relationship_type, str)
|
|
else rel.relationship_type.value,
|
|
)
|
|
)
|
|
next_front.add(rel.target_artifact_id)
|
|
inc = await relationships.incoming(aid, limit=50)
|
|
for rel in inc:
|
|
edges.append(
|
|
FlowEdge(
|
|
id=f"e-{rel.id}",
|
|
source=str(rel.source_artifact_id),
|
|
target=str(rel.target_artifact_id),
|
|
label=rel.relationship_type
|
|
if isinstance(rel.relationship_type, str)
|
|
else rel.relationship_type.value,
|
|
)
|
|
)
|
|
next_front.add(rel.source_artifact_id)
|
|
frontier = next_front - visited
|
|
elif project_id is not None:
|
|
arts = await artifacts.list(project_id=project_id, limit=limit)
|
|
await _add_art([a.id for a in arts])
|
|
# relationships for every artifact we loaded
|
|
for a in arts:
|
|
out = await relationships.outgoing(a.id, limit=50)
|
|
for rel in out:
|
|
edges.append(
|
|
FlowEdge(
|
|
id=f"e-{rel.id}",
|
|
source=str(rel.source_artifact_id),
|
|
target=str(rel.target_artifact_id),
|
|
label=rel.relationship_type
|
|
if isinstance(rel.relationship_type, str)
|
|
else rel.relationship_type.value,
|
|
)
|
|
)
|
|
# filter edges to those whose endpoints are both in our node set
|
|
node_ids = set(nodes.keys())
|
|
edges = [e for e in edges if UUID(e.source) in node_ids and UUID(e.target) in node_ids]
|
|
|
|
else:
|
|
# No seed, no project — return tenant-wide recent artifacts (bounded)
|
|
arts = await artifacts.list(limit=limit)
|
|
await _add_art([a.id for a in arts])
|
|
for a in arts:
|
|
out = await relationships.outgoing(a.id, limit=20)
|
|
for rel in out:
|
|
edges.append(
|
|
FlowEdge(
|
|
id=f"e-{rel.id}",
|
|
source=str(rel.source_artifact_id),
|
|
target=str(rel.target_artifact_id),
|
|
label=rel.relationship_type
|
|
if isinstance(rel.relationship_type, str)
|
|
else rel.relationship_type.value,
|
|
)
|
|
)
|
|
node_ids = set(nodes.keys())
|
|
edges = [e for e in edges if UUID(e.source) in node_ids and UUID(e.target) in node_ids]
|
|
|
|
return TraceabilityGraph(nodes=list(nodes.values()), edges=edges)
|