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:
73
apps/web/components/editor/IssuesPanel.tsx
Normal file
73
apps/web/components/editor/IssuesPanel.tsx
Normal file
@@ -0,0 +1,73 @@
|
||||
// Floating panel listing current validation issues — shown next to the
|
||||
// status bar. Click an issue to focus its anchored element across the rail
|
||||
// + diagram (via setFocusBlockId). Lets the user verify the validator is
|
||||
// actually firing on the broken-fixture demos.
|
||||
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import type { ValidationIssue } from "../../lib/sysml/validate";
|
||||
|
||||
interface IssuesPanelProps {
|
||||
issues: ValidationIssue[];
|
||||
onSelectAnchor?: (id: string) => void;
|
||||
}
|
||||
|
||||
const SEV_GLYPH: Record<ValidationIssue["severity"], string> = {
|
||||
error: "●",
|
||||
warning: "▲",
|
||||
soft: "·",
|
||||
};
|
||||
|
||||
export function IssuesPanel({ issues, onSelectAnchor }: IssuesPanelProps) {
|
||||
const [collapsed, setCollapsed] = useState(false);
|
||||
|
||||
const grouped = {
|
||||
error: issues.filter(i => i.severity === "error"),
|
||||
warning: issues.filter(i => i.severity === "warning"),
|
||||
soft: issues.filter(i => i.severity === "soft"),
|
||||
};
|
||||
|
||||
if (issues.length === 0) {
|
||||
return (
|
||||
<div className={`issues-panel issues-panel-clean ${collapsed ? "issues-panel-collapsed" : ""}`}>
|
||||
<button className="issues-panel-head" onClick={() => setCollapsed(c => !c)} type="button">
|
||||
<span className="issues-panel-clean-glyph">✓</span>
|
||||
<span className="issues-panel-title">Model is clean</span>
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`issues-panel ${collapsed ? "issues-panel-collapsed" : ""}`}>
|
||||
<button className="issues-panel-head" onClick={() => setCollapsed(c => !c)} type="button">
|
||||
<span className="issues-panel-counts">
|
||||
{grouped.error.length > 0 && <span className="issues-count issues-count-error">● {grouped.error.length}</span>}
|
||||
{grouped.warning.length > 0 && <span className="issues-count issues-count-warning">▲ {grouped.warning.length}</span>}
|
||||
{grouped.soft.length > 0 && <span className="issues-count issues-count-soft">· {grouped.soft.length}</span>}
|
||||
</span>
|
||||
<span className="issues-panel-title">{issues.length} validation issue{issues.length === 1 ? "" : "s"}</span>
|
||||
<span className="issues-panel-caret">{collapsed ? "▴" : "▾"}</span>
|
||||
</button>
|
||||
|
||||
{!collapsed && (
|
||||
<ul className="issues-panel-list">
|
||||
{(["error", "warning", "soft"] as const).flatMap(sev =>
|
||||
grouped[sev].map((i, idx) => (
|
||||
<li
|
||||
key={`${sev}-${idx}`}
|
||||
className={`issues-item issues-item-${i.severity}`}
|
||||
onClick={() => onSelectAnchor?.(i.anchor.kind === "property" ? i.anchor.blockId : (i.anchor as { id: string }).id ?? "")}
|
||||
>
|
||||
<span className="issues-item-sev">{SEV_GLYPH[i.severity]}</span>
|
||||
<span className="issues-item-code">{i.code}</span>
|
||||
<span className="issues-item-msg">{i.message}</span>
|
||||
</li>
|
||||
))
|
||||
)}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user