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).
This commit is contained in:
2026-04-28 23:22:24 +02:00
parent a0566ce64c
commit 2052a280b1
14 changed files with 1131 additions and 45 deletions

View File

@@ -35,6 +35,7 @@ import { NodeInspector } from "./NodeInspector";
import { fixtureToFlow } from "./fixtureToFlow";
import type { Density } from "../socrates/SocratesDock";
import type { FixtureData, AssociationKind, BlockKind } from "../../lib/fixtures/aristotle";
import type { ValidationIssue } from "../../lib/sysml/validate";
export type DiagramVariant = "softened" | "formal" | "graph";
@@ -44,6 +45,7 @@ interface DiagramCanvasProps {
variant?: DiagramVariant;
focusBlockId: string | null;
onSelect?: (id: string | null) => void;
issuesByElement?: Map<string, ValidationIssue[]>;
}
const nodeTypes = { sysmlBlock: BlockNode };
@@ -54,6 +56,13 @@ let nextEdgeId = 1000;
function freshNodeId(): string { return `n${nextNodeId++}`; }
function freshEdgeId(): string { return `e${nextEdgeId++}`; }
function pickWorst(issues: ValidationIssue[]): "error" | "warning" | "soft" | undefined {
if (issues.some(i => i.severity === "error")) return "error";
if (issues.some(i => i.severity === "warning")) return "warning";
if (issues.some(i => i.severity === "soft")) return "soft";
return undefined;
}
export function DiagramCanvas(props: DiagramCanvasProps) {
return (
<ReactFlowProvider>
@@ -62,7 +71,7 @@ export function DiagramCanvas(props: DiagramCanvasProps) {
);
}
function DiagramInner({ data, focusBlockId, onSelect }: DiagramCanvasProps) {
function DiagramInner({ data, focusBlockId, onSelect, issuesByElement }: DiagramCanvasProps) {
const initial = useMemo(() => fixtureToFlow(data), [data]);
const [nodes, setNodes] = useState<Node<BlockNodeData>[]>(initial.nodes);
const [edges, setEdges] = useState<Edge<SysmlEdgeData>[]>(initial.edges);
@@ -90,14 +99,20 @@ function DiagramInner({ data, focusBlockId, onSelect }: DiagramCanvasProps) {
}, []);
// Highlight focusBlockId from sibling components (e.g. narrative chip hover).
// Also surface validation severity onto each node so BlockNode can ring it.
useEffect(() => {
setNodes(ns =>
ns.map(n => ({
...n,
selected: n.id === focusBlockId,
}))
ns.map(n => {
const issues = issuesByElement?.get(n.id) ?? [];
const severity = pickWorst(issues);
return {
...n,
selected: n.id === focusBlockId,
data: { ...(n.data as BlockNodeData), issueSeverity: severity },
};
})
);
}, [focusBlockId]);
}, [focusBlockId, issuesByElement]);
const onNodeClick: NodeMouseHandler = useCallback(
(_event, node) => {

View File

@@ -12,6 +12,8 @@ export interface BlockNodeData extends Record<string, unknown> {
properties: string[];
/** For constraint kinds, optional one-line expression. */
expression?: string;
/** Highest severity of any validation issue anchored to this block. */
issueSeverity?: "error" | "warning" | "soft";
}
const STEREO: Record<string, string> = {
@@ -30,6 +32,7 @@ export function BlockNode({ data, selected }: NodeProps) {
"sysml-node",
`sysml-node-${kind}`,
selected ? "sysml-node-selected" : "",
d.issueSeverity ? `sysml-node-issue-${d.issueSeverity}` : "",
]
.filter(Boolean)
.join(" ");