Files
Socrates/apps/web/lib/sysml/depgraph.ts
dtoro 2052a280b1 MVP M4: SysML metamodel + validator + dependency graph + UI surfacing
apps/web/lib/sysml — pure engine (no UI deps)
- model.ts: canonical types ported from phase-0 (Block, Property,
  Association, Constraint, Requirement, SysMLModel)
- validate.ts: 12 rules grouped Structural (S1–S5) / Semantic (M1–M5) /
  Traceability (T1–T2). Pure function returning element-anchored issues.
  Cycle detection via DFS with canonical-key dedup. T3 deferred to Phase 1.5
  (depends on narrative chip references).
- depgraph.ts: directed graph over Blocks / Properties / Constraints /
  Requirements with `dependentsOf()` and `neighborhood()` for impact analysis
  (substrate for M7).
- fromFixture.ts: loose FixtureData → strict SysMLModel converter (interim
  until M5 unifies state).
- breaks.ts: 8 named model corruptions (S1/S2/S3/M2/M3/M5/T1/T2) for
  ?break=... demonstrations.

UI integration in EditorShell + LeftRail + DiagramCanvas + IssuesPanel
- Floating IssuesPanel (bottom-right) with severity-grouped counts and
  collapse. Click an issue to focus its anchor across the rail and diagram.
- LeftRail: severity dots on each block in the Model section and on each
  requirement entry. Tooltips show full issue text.
- DiagramCanvas: red/blue/dotted ring around offending blocks via
  issuesByElement → BlockNode.data.issueSeverity.
- ?break=S1-dangling,M2-cycle,M5-dup-property,... query param injects
  corruptions for verification of the "done when" criterion.
- Suspense boundary added at the editor route for useSearchParams.

Two pre-existing TipTap typing fixes (editor.storage cast through unknown).
2026-04-28 23:22:24 +02:00

151 lines
4.5 KiB
TypeScript

// 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<string, DepNode>;
/** Adjacency list, indexed by `from`. */
out: Map<string, DepEdge[]>;
/** Reverse adjacency, indexed by `to`. Useful for "what depends on this?". */
in: Map<string, DepEdge[]>;
}
export function buildDepGraph(model: SysMLModel): DepGraph {
const nodes = new Map<string, DepNode>();
const out = new Map<string, DepEdge[]>();
const inEdges = new Map<string, DepEdge[]>();
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<string> {
const visited = new Set<string>();
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<string> {
const visited = new Set<string>([seedId]);
let frontier = new Set<string>([seedId]);
for (let i = 0; i < radius; i++) {
const next = new Set<string>();
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;
}