Files
Socrates/apps/web/lib/sysml/breaks.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

148 lines
4.0 KiB
TypeScript

// Known model corruptions for demonstrating the validator end-to-end.
//
// Each "break" returns a model variant that should fire one specific rule
// (or a small cluster) without firing others. Used by the editor's `?break=...`
// query param to make the validator's behavior visible.
import type { SysMLModel } from "./model";
export type BreakName =
| "S1-dangling"
| "S2-dangling-constraint"
| "S3-untraced-satisfier"
| "M2-cycle"
| "M3-multi-system"
| "M5-dup-property"
| "T1-untraced-req"
| "T2-unused-block";
export const BREAKS: Record<BreakName, { label: string; description: string; apply: (m: SysMLModel) => SysMLModel }> = {
"S1-dangling": {
label: "S1 — dangling association endpoint",
description: "Add an association whose target block doesn't exist.",
apply(m) {
return {
...m,
associations: [
...m.associations,
{ id: "a-bad", fromBlockId: "aristotle", toBlockId: "ghost-block", label: "haunts", kind: "association" },
],
};
},
},
"S2-dangling-constraint": {
label: "S2 — constraint applies to unknown block",
description: "Add a constraint pointing at a block that doesn't exist.",
apply(m) {
return {
...m,
constraints: [
...m.constraints,
{ id: "c-bad", label: "Phantom rule", expression: "{}", appliesTo: ["ghost-block"] },
],
};
},
},
"S3-untraced-satisfier": {
label: "S3 — requirement satisfied by unknown block",
description: "Reroute REQ-001 to a non-existent block.",
apply(m) {
return {
...m,
requirements: m.requirements.map(r =>
r.tag === "REQ-001" ? { ...r, relations: [{ kind: "satisfy" as const, blockId: "ghost-block" }] } : r
),
};
},
},
"M2-cycle": {
label: "M2 — cyclic composition",
description: "Add a composition Assignment → Course (the inverse of the existing Course → Assignment).",
apply(m) {
return {
...m,
associations: [
...m.associations,
{
id: "a-cycle",
fromBlockId: "assignment",
toBlockId: "course",
label: "wraps",
kind: "composition",
},
],
};
},
},
"M3-multi-system": {
label: "M3 — two System-of-Interest blocks",
description: "Mark both Aristotle and Course as kind=system.",
apply(m) {
return {
...m,
blocks: m.blocks.map(b =>
b.id === "aristotle" || b.id === "course" ? { ...b, kind: "system" as const, stereotypes: ["system"] } : b
),
};
},
},
"M5-dup-property": {
label: "M5 — duplicate property name in a block",
description: "Add a second `interaction_style` property to Aristotle.",
apply(m) {
return {
...m,
blocks: m.blocks.map(b =>
b.id === "aristotle"
? {
...b,
properties: [
...b.properties,
{ id: "p-dup", name: "interaction_style", type: { kind: "string" as const }, multiplicity: "0..1" as const },
],
}
: b
),
};
},
},
"T1-untraced-req": {
label: "T1 — requirement with no satisfier",
description: "Already present in the fixture (REQ-003 has no satisfy relation).",
apply(m) {
// No mutation needed; the fixture already triggers T1 on REQ-003.
return m;
},
},
"T2-unused-block": {
label: "T2 — block with no incoming references",
description: "Add a Tutor block that nothing connects to.",
apply(m) {
return {
...m,
blocks: [
...m.blocks,
{
id: "tutor-orphan",
label: "TutorOrphan",
kind: "block",
stereotypes: ["block"],
properties: [],
},
],
};
},
},
};
export function applyBreaks(model: SysMLModel, names: BreakName[]): SysMLModel {
return names.reduce((m, name) => BREAKS[name].apply(m), model);
}