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).
145 lines
5.2 KiB
TypeScript
145 lines
5.2 KiB
TypeScript
// The dual-canvas workspace shell.
|
|
// Composes TopBar / SocratesDock / LeftRail / TextCanvas / DiagramCanvas / StatusBar.
|
|
// M4 adds: SysML model derivation + validation, IssuesPanel, optional ?break=...
|
|
// query param to demonstrate the validator surfacing rule violations.
|
|
|
|
"use client";
|
|
|
|
import { useMemo, useState } from "react";
|
|
import { useSearchParams } from "next/navigation";
|
|
import { TopBar } from "./TopBar";
|
|
import { LeftRail } from "./LeftRail";
|
|
import { CanvasHeader } from "./CanvasHeader";
|
|
import { StatusBar } from "./StatusBar";
|
|
import { IssuesPanel } from "./IssuesPanel";
|
|
import { TextCanvas } from "../text-canvas/TextCanvas";
|
|
import { DiagramCanvas, type DiagramVariant } from "../diagram-canvas/DiagramCanvas";
|
|
import { SocratesDock, type Density, type SocratesPresence } from "../socrates/SocratesDock";
|
|
import type { MarkupStyle } from "../text-canvas/Chip";
|
|
import type { FixtureData } from "../../lib/fixtures/aristotle";
|
|
import { fromFixture } from "../../lib/sysml/fromFixture";
|
|
import { validate, type ValidationIssue } from "../../lib/sysml/validate";
|
|
import { applyBreaks, BREAKS, type BreakName } from "../../lib/sysml/breaks";
|
|
|
|
interface EditorShellProps {
|
|
data: FixtureData;
|
|
density?: Density;
|
|
markupStyle?: MarkupStyle;
|
|
diagramStyle?: DiagramVariant;
|
|
presence?: SocratesPresence;
|
|
}
|
|
|
|
export function EditorShell({
|
|
data,
|
|
density = "comfortable",
|
|
markupStyle = "color",
|
|
diagramStyle = "softened",
|
|
presence = "default",
|
|
}: EditorShellProps) {
|
|
const [focusBlockId, setFocusBlockId] = useState<string | null>(null);
|
|
const searchParams = useSearchParams();
|
|
|
|
// Parse `?break=S1-dangling,M2-cycle&...` into a list of named corruptions.
|
|
const breaks = useMemo<BreakName[]>(() => {
|
|
const raw = searchParams?.get("break") ?? "";
|
|
if (!raw) return [];
|
|
return raw
|
|
.split(",")
|
|
.map(s => s.trim())
|
|
.filter((s): s is BreakName => s in BREAKS);
|
|
}, [searchParams]);
|
|
|
|
// Derive the SysML model from the fixture, optionally apply breaks, validate.
|
|
const { issues, issuesByElement } = useMemo(() => {
|
|
const baseModel = fromFixture(data);
|
|
const broken = breaks.length > 0 ? applyBreaks(baseModel, breaks) : baseModel;
|
|
const issues = validate(broken);
|
|
const issuesByElement = new Map<string, ValidationIssue[]>();
|
|
for (const i of issues) {
|
|
const key = anchorKey(i.anchor);
|
|
if (!key) continue;
|
|
const prev = issuesByElement.get(key) ?? [];
|
|
prev.push(i);
|
|
issuesByElement.set(key, prev);
|
|
}
|
|
return { issues, issuesByElement };
|
|
}, [data, breaks]);
|
|
|
|
return (
|
|
<div className={`shell shell-density-${density} shell-presence-${presence}`}>
|
|
<TopBar data={data} />
|
|
|
|
<div className="shell-body">
|
|
<SocratesDock thread={data.socratesThread} presence={presence} density={density} />
|
|
<LeftRail
|
|
data={data}
|
|
focusBlockId={focusBlockId}
|
|
setFocusBlockId={setFocusBlockId}
|
|
issuesByElement={issuesByElement}
|
|
/>
|
|
|
|
<main className="canvases">
|
|
<section className="canvas canvas-text">
|
|
<CanvasHeader title="Narrative" subtitle="Markup-augmented prose · synced to model" />
|
|
<div className="canvas-scroll">
|
|
<TextCanvas
|
|
data={data}
|
|
density={density}
|
|
markupStyle={markupStyle}
|
|
focusBlockId={focusBlockId}
|
|
setFocusBlockId={setFocusBlockId}
|
|
/>
|
|
</div>
|
|
</section>
|
|
|
|
<div className="canvas-divider" />
|
|
|
|
<section className="canvas canvas-diagram">
|
|
<CanvasHeader
|
|
title="Model"
|
|
subtitle={breaks.length > 0
|
|
? `SysML · breaks active: ${breaks.join(", ")}`
|
|
: "SysML · 6 blocks · 6 associations · 1 constraint"}
|
|
right={
|
|
<div className="canvas-actions">
|
|
<span className="canvas-mode-pill">Fit</span>
|
|
<span className="canvas-mode-pill canvas-mode-active">100%</span>
|
|
<span className="canvas-mode-pill">Layout</span>
|
|
</div>
|
|
}
|
|
/>
|
|
<div className="canvas-scroll canvas-scroll-diagram">
|
|
<DiagramCanvas
|
|
data={data}
|
|
density={density}
|
|
variant={diagramStyle}
|
|
focusBlockId={focusBlockId}
|
|
onSelect={setFocusBlockId}
|
|
issuesByElement={issuesByElement}
|
|
/>
|
|
</div>
|
|
</section>
|
|
</main>
|
|
</div>
|
|
|
|
<StatusBar data={data} />
|
|
|
|
<IssuesPanel issues={issues} onSelectAnchor={id => setFocusBlockId(id)} />
|
|
</div>
|
|
);
|
|
}
|
|
|
|
/** Map an issue's anchor to a single string key matching either a block id or
|
|
* a synthetic key (`assoc:a1`, `req:req-001`, `constraint:ferpa`). The UI uses
|
|
* block ids most often, so block anchors return the bare id. */
|
|
function anchorKey(anchor: ValidationIssue["anchor"]): string | null {
|
|
switch (anchor.kind) {
|
|
case "block": return anchor.id;
|
|
case "association": return `assoc:${anchor.id}`;
|
|
case "constraint": return `constraint:${anchor.id}`;
|
|
case "requirement": return `req:${anchor.id}`;
|
|
case "property": return anchor.blockId;
|
|
case "model": return null;
|
|
}
|
|
}
|