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:
88
apps/web/lib/sysml/fromFixture.ts
Normal file
88
apps/web/lib/sysml/fromFixture.ts
Normal file
@@ -0,0 +1,88 @@
|
||||
// Convert the loose Aristotle FixtureData (M1 prototype port) into a strict
|
||||
// SysMLModel that the validator and dep-graph can operate on.
|
||||
|
||||
import type {
|
||||
SysMLModel,
|
||||
Block,
|
||||
Association,
|
||||
Constraint,
|
||||
Requirement,
|
||||
AssociationKind,
|
||||
} from "./model";
|
||||
import type { FixtureData } from "../fixtures/aristotle";
|
||||
|
||||
export function fromFixture(data: FixtureData): SysMLModel {
|
||||
const constraintBlocks = data.blocks.filter(b => b.kind === "constraint");
|
||||
const constraints: Constraint[] = constraintBlocks.map(b => ({
|
||||
id: b.id,
|
||||
label: b.label,
|
||||
expression: "{ tenancy = institutional }",
|
||||
// The fixture infers "what does this constraint apply to?" from its
|
||||
// outgoing/incoming `kind: "constraint"` associations.
|
||||
appliesTo: data.associations
|
||||
.filter(a => a.kind === "constraint" && (a.from === b.id || a.to === b.id))
|
||||
.map(a => (a.from === b.id ? a.to : a.from)),
|
||||
}));
|
||||
|
||||
const blocks: Block[] = data.blocks
|
||||
.filter(b => b.kind !== "constraint")
|
||||
.map(b => ({
|
||||
id: b.id,
|
||||
label: b.label,
|
||||
kind: b.kind,
|
||||
stereotypes: [b.kind],
|
||||
properties: b.properties.map((name, i) => ({
|
||||
id: `p${i + 1}`,
|
||||
name,
|
||||
type: { kind: "string" } as const,
|
||||
multiplicity: "0..1" as const,
|
||||
})),
|
||||
}));
|
||||
|
||||
const associations: Association[] = data.associations
|
||||
.filter(a => a.kind !== "constraint")
|
||||
.map(a => ({
|
||||
id: a.id,
|
||||
fromBlockId: a.from,
|
||||
toBlockId: a.to,
|
||||
label: a.label,
|
||||
kind: mapAssocKind(a.kind),
|
||||
}));
|
||||
|
||||
// Requirements come from the narrative; the fixture chips reference REQ-001..003
|
||||
// but the requirements themselves aren't stored. Fabricate them here so the
|
||||
// validator has something meaningful to operate on.
|
||||
const requirements: Requirement[] = [
|
||||
{
|
||||
id: "req-001",
|
||||
tag: "REQ-001",
|
||||
text: "Must never output a complete solution to a graded problem.",
|
||||
relations: [{ kind: "satisfy", blockId: "aristotle" }],
|
||||
},
|
||||
{
|
||||
id: "req-002",
|
||||
tag: "REQ-002",
|
||||
text: "Response latency under 1.2s P50 to preserve flow.",
|
||||
relations: [{ kind: "satisfy", blockId: "aristotle" }],
|
||||
},
|
||||
{
|
||||
id: "req-003",
|
||||
tag: "REQ-003",
|
||||
text: "Operates within FERPA boundaries; coursework never leaves institutional tenancy.",
|
||||
// Intentionally untraced in the fixture — see the rail's "untraced" dot
|
||||
relations: [],
|
||||
},
|
||||
];
|
||||
|
||||
return {
|
||||
blocks,
|
||||
associations,
|
||||
constraints,
|
||||
requirements,
|
||||
};
|
||||
}
|
||||
|
||||
function mapAssocKind(k: "association" | "composition" | "constraint"): AssociationKind {
|
||||
if (k === "constraint") return "constraintApplies";
|
||||
return k;
|
||||
}
|
||||
Reference in New Issue
Block a user