// Dependency graph over a SysML model. // // Nodes: every Block, Property, Constraint, Requirement. // Edges encode the structural and traceability relationships from // docs/sysml-modeling.md §8. // // Used by the UI for cross-surface highlighting now (M4) and by Socrates' // impact analysis later (M7). import type { SysMLModel } from "./model"; export type DepNodeKind = "block" | "property" | "constraint" | "requirement"; export interface DepNode { id: string; // unique within graph; for properties: `${blockId}.${propertyId}` kind: DepNodeKind; label: string; } export type DepEdgeKind = | "contains" // block → property | "relates" // block → block (per association.kind) | "applies-to" // constraint → block | "satisfies" // requirement → block | "derives-from"; // requirement → requirement export interface DepEdge { from: string; to: string; kind: DepEdgeKind; /** For "relates" edges, the underlying association kind. */ associationKind?: string; } export interface DepGraph { nodes: Map; /** Adjacency list, indexed by `from`. */ out: Map; /** Reverse adjacency, indexed by `to`. Useful for "what depends on this?". */ in: Map; } export function buildDepGraph(model: SysMLModel): DepGraph { const nodes = new Map(); const out = new Map(); const inEdges = new Map(); function addNode(node: DepNode) { nodes.set(node.id, node); if (!out.has(node.id)) out.set(node.id, []); if (!inEdges.has(node.id)) inEdges.set(node.id, []); } function addEdge(edge: DepEdge) { const o = out.get(edge.from); if (o) o.push(edge); else out.set(edge.from, [edge]); const i = inEdges.get(edge.to); if (i) i.push(edge); else inEdges.set(edge.to, [edge]); } for (const b of model.blocks) { addNode({ id: b.id, kind: "block", label: b.label }); for (const p of b.properties) { const pid = `${b.id}.${p.id}`; addNode({ id: pid, kind: "property", label: p.name }); addEdge({ from: b.id, to: pid, kind: "contains" }); } } for (const a of model.associations) { addEdge({ from: a.fromBlockId, to: a.toBlockId, kind: "relates", associationKind: a.kind, }); } for (const c of model.constraints) { addNode({ id: c.id, kind: "constraint", label: c.label }); for (const target of c.appliesTo) { addEdge({ from: c.id, to: target, kind: "applies-to" }); } } for (const r of model.requirements) { addNode({ id: r.id, kind: "requirement", label: r.tag }); for (const rel of r.relations) { if (rel.kind === "satisfy") { addEdge({ from: r.id, to: rel.blockId, kind: "satisfies" }); } else if (rel.kind === "derive") { addEdge({ from: r.id, to: rel.fromReqId, kind: "derives-from" }); } } } return { nodes, out, in: inEdges }; } /** * BFS the dep graph downstream from `seedId`, returning the closure (excluding * the seed itself by default). Use for "if I change X, what's affected?". */ export function dependentsOf(graph: DepGraph, seedId: string): Set { const visited = new Set(); const queue: string[] = [seedId]; while (queue.length) { const cur = queue.shift()!; const out = graph.out.get(cur) ?? []; for (const edge of out) { if (!visited.has(edge.to)) { visited.add(edge.to); queue.push(edge.to); } } // Reverse direction too — things that depend ON this element should also // appear in the closure (an Assumption linked to this block, etc., once // those are added in M8). const inEdges = graph.in.get(cur) ?? []; for (const edge of inEdges) { if (!visited.has(edge.from)) { visited.add(edge.from); queue.push(edge.from); } } } visited.delete(seedId); return visited; } export function neighborhood(graph: DepGraph, seedId: string, radius = 2): Set { const visited = new Set([seedId]); let frontier = new Set([seedId]); for (let i = 0; i < radius; i++) { const next = new Set(); for (const id of frontier) { for (const e of graph.out.get(id) ?? []) { if (!visited.has(e.to)) { visited.add(e.to); next.add(e.to); } } for (const e of graph.in.get(id) ?? []) { if (!visited.has(e.from)) { visited.add(e.from); next.add(e.from); } } } frontier = next; } visited.delete(seedId); return visited; }