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).
148 lines
4.0 KiB
TypeScript
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);
|
|
}
|