Workspace - Pivot from "set of open panes" to a Finder-style miller column stack: TopBar / LeftSidebar / [section → entity → entity ...] / pinned text editor. openPanesStore is now an ordered Column[] with pushFrom / closeFrom / setStack; only one top-level section is rooted at a time. - New entity column panes: Term, Block, Association, Constraint, Requirement, Finding. Click-through navigation truncates deeper columns automatically. - LeftSidebar surfaces a pending-count chip per section (single-glance navigation cue) and spins its analyze ↻ via SVG Spinner whenever the LLM is working — including server-initiated runs caught by the runs poll, not just user-triggered ones. Analyze pipeline + persistence - Unified `concepts` pass (taxonomy + glossary in one LLM call) replaces the two-pass setup. Server still accepts ?section=taxonomy|glossary and normalizes them for back-compat. - model / requirements / detection (assumptions, risks, inconsistencies) + cross-layer validation rules (X1–X4: stale term link, unlinked formalism, undefined linked term, prose-only term). - Persistence: NarrativeDocument, ModelSnapshot, ChangelogEntry, TaxonomyTerm, RequirementEntry, Finding, AnalysisRun. Re-runs MERGE instead of replace: gentle update on existing items, suggested on new, deprecated on missing — same idiom for every artifact kind. User pins preserve "kept" decisions across re-analyses. - Migrations: pivot_text_first, add_requirement_linked_term, term_review_state, review_state_for_reqs_and_findings, add_term_definition_pinned. Concept ↔ ontology integration - linkedTermId on Block / Association / Constraint / Requirement. PromoteToolbar lets the user formalize a concept inline: + Block / + Association / + Constraint / + Requirement, all routed through applyOps so undo/redo and SSE work for free. - decideElement op for in-canvas keep/discard on review-pending model elements. User-authored definitions - TermColumn definition is click-to-edit. Save (Cmd-Enter / blur), Cancel (Esc), Reset to AI suggestion when pinned. - definitionPinned flag on TaxonomyTerm: future Analyze runs leave the user's text alone. setTermDefinition repo function + POST /api/projects/[id]/terms/[termId]/definition endpoint. - mergeTaxonomySuggestion + applyGlossaryDefinitions both pin-aware. UX/UI - StatusChip: single component for all state idioms (suggested, deprecated, accepted, dismissed, resolved, severity, validation code, confidence, warn). Replaces 5+ ad-hoc badge classes. - PaneControls (PaneViewTabs + PaneFilterChip): separates view-mode toggles from filter chips so toggling Pending no longer flips you off the current view. - PaneEmpty: unified empty-state with title + hint + action. - PaneDrawer: collapsible groups for Pending / Discarded review; cards group as Kept (top) → Pending (bottom drawer) → Discarded (Findings only, hidden when empty). Restore action recovers dismissed/resolved findings. - ConceptCard unifies Tree and A–Z views in Concepts; only Tree parents carry the chevron (no empty placeholder offset). - Type + spacing tokens (--text-xs..xl, --space-1..6, --lh-tight/ui/ prose, --radius-*) replace every ad-hoc value. - Buttons standardized to body sans 500 (was a mishmash of mono / display). - Card shells unified across Concepts / Requirements / Findings. Cleanup - Removed: LeftRail, FindingsPanel, IssuesPanel, SocratesDock, ProposalCard, SlashMenu, SlashExtension, slashSuggestion, CanvasHeader, TaxonomyPane, GlossaryPane, TermDetail (popover; now TermColumn). - Section ids in openPanesStore: dropped taxonomy/glossary, added concepts. localStorage migration runs on hydrate. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
259 lines
8.5 KiB
TypeScript
259 lines
8.5 KiB
TypeScript
// The canonical ModelOp alphabet — every mutation to a SysMLModel is
|
|
// expressible as one or more of these. See docs/sync.md §3 for the design.
|
|
//
|
|
// `tempId` lets clients optimistically create elements with a client-generated
|
|
// id; applyOps may rewrite to a canonical id and return the mapping. (M5
|
|
// in-memory: tempId IS the canonical id; the rewrite seam is here for the
|
|
// later DB / SSE iteration.)
|
|
|
|
import type {
|
|
Block,
|
|
BlockKind,
|
|
Association,
|
|
AssociationKind,
|
|
Constraint,
|
|
Property,
|
|
PropertyType,
|
|
Multiplicity,
|
|
Requirement,
|
|
RequirementRelation,
|
|
} from "../sysml/model";
|
|
|
|
// ─── Block ops ───────────────────────────────────────────────────────────
|
|
|
|
export interface AddBlockOp {
|
|
kind: "add-block";
|
|
block: Block;
|
|
tempId?: string;
|
|
}
|
|
|
|
export interface UpdateBlockOp {
|
|
kind: "update-block";
|
|
blockId: string;
|
|
patch: Partial<Pick<Block, "label" | "kind" | "stereotypes" | "description">>;
|
|
}
|
|
|
|
export interface RemoveBlockOp {
|
|
kind: "remove-block";
|
|
blockId: string;
|
|
}
|
|
|
|
// ─── Property ops ────────────────────────────────────────────────────────
|
|
|
|
export interface AddPropertyOp {
|
|
kind: "add-property";
|
|
blockId: string;
|
|
property: Property;
|
|
tempId?: string;
|
|
}
|
|
|
|
export interface UpdatePropertyOp {
|
|
kind: "update-property";
|
|
blockId: string;
|
|
propertyId: string;
|
|
patch: Partial<Pick<Property, "name" | "type" | "multiplicity" | "description">>;
|
|
}
|
|
|
|
export interface RemovePropertyOp {
|
|
kind: "remove-property";
|
|
blockId: string;
|
|
propertyId: string;
|
|
}
|
|
|
|
// ─── Association ops ─────────────────────────────────────────────────────
|
|
|
|
export interface AddAssociationOp {
|
|
kind: "add-association";
|
|
association: Association;
|
|
tempId?: string;
|
|
}
|
|
|
|
export interface UpdateAssociationOp {
|
|
kind: "update-association";
|
|
associationId: string;
|
|
patch: Partial<Pick<Association, "fromBlockId" | "toBlockId" | "label" | "kind" | "multiplicity">>;
|
|
}
|
|
|
|
export interface RemoveAssociationOp {
|
|
kind: "remove-association";
|
|
associationId: string;
|
|
}
|
|
|
|
// ─── Constraint ops ──────────────────────────────────────────────────────
|
|
|
|
export interface AddConstraintOp {
|
|
kind: "add-constraint";
|
|
constraint: Constraint;
|
|
tempId?: string;
|
|
}
|
|
|
|
export interface UpdateConstraintOp {
|
|
kind: "update-constraint";
|
|
constraintId: string;
|
|
patch: Partial<Pick<Constraint, "label" | "expression" | "appliesTo">>;
|
|
}
|
|
|
|
export interface RemoveConstraintOp {
|
|
kind: "remove-constraint";
|
|
constraintId: string;
|
|
}
|
|
|
|
// ─── Requirement ops ─────────────────────────────────────────────────────
|
|
|
|
export interface AddRequirementOp {
|
|
kind: "add-requirement";
|
|
requirement: Requirement;
|
|
tempId?: string;
|
|
}
|
|
|
|
export interface UpdateRequirementOp {
|
|
kind: "update-requirement";
|
|
requirementId: string;
|
|
patch: Partial<Pick<Requirement, "tag" | "text">>;
|
|
}
|
|
|
|
export interface RemoveRequirementOp {
|
|
kind: "remove-requirement";
|
|
requirementId: string;
|
|
}
|
|
|
|
export interface AddRelationOp {
|
|
kind: "add-relation";
|
|
requirementId: string;
|
|
relation: RequirementRelation;
|
|
}
|
|
|
|
export interface RemoveRelationOp {
|
|
kind: "remove-relation";
|
|
requirementId: string;
|
|
relationIndex: number;
|
|
}
|
|
|
|
// ─── Review ops ──────────────────────────────────────────────────────────
|
|
//
|
|
// `decide-element` confirms an analyzer-suggested or analyzer-deprecated
|
|
// model element. "Discard" simply uses the existing remove-* ops; for
|
|
// "keep" we need a way to flip reviewStatus → accepted (and pin if it was
|
|
// deprecated, so the next analyze pass doesn't re-deprecate the element).
|
|
|
|
export type ReviewableElementKind = "block" | "association" | "constraint" | "requirement";
|
|
|
|
export interface DecideElementOp {
|
|
kind: "decide-element";
|
|
element: { kind: ReviewableElementKind; id: string };
|
|
/** Only "keep" right now — discard goes through remove-*. */
|
|
decision: "keep";
|
|
}
|
|
|
|
// ─── Union ───────────────────────────────────────────────────────────────
|
|
|
|
export type ModelOp =
|
|
| AddBlockOp
|
|
| UpdateBlockOp
|
|
| RemoveBlockOp
|
|
| AddPropertyOp
|
|
| UpdatePropertyOp
|
|
| RemovePropertyOp
|
|
| AddAssociationOp
|
|
| UpdateAssociationOp
|
|
| RemoveAssociationOp
|
|
| AddConstraintOp
|
|
| UpdateConstraintOp
|
|
| RemoveConstraintOp
|
|
| AddRequirementOp
|
|
| UpdateRequirementOp
|
|
| RemoveRequirementOp
|
|
| AddRelationOp
|
|
| RemoveRelationOp
|
|
| DecideElementOp;
|
|
|
|
// ─── Op constructors (call sites stay readable) ─────────────────────────
|
|
|
|
export function addBlock(block: Block, tempId?: string): AddBlockOp {
|
|
return { kind: "add-block", block, tempId };
|
|
}
|
|
|
|
export function updateBlock(blockId: string, patch: UpdateBlockOp["patch"]): UpdateBlockOp {
|
|
return { kind: "update-block", blockId, patch };
|
|
}
|
|
|
|
export function removeBlock(blockId: string): RemoveBlockOp {
|
|
return { kind: "remove-block", blockId };
|
|
}
|
|
|
|
export function addProperty(blockId: string, property: Property, tempId?: string): AddPropertyOp {
|
|
return { kind: "add-property", blockId, property, tempId };
|
|
}
|
|
|
|
export function updateProperty(blockId: string, propertyId: string, patch: UpdatePropertyOp["patch"]): UpdatePropertyOp {
|
|
return { kind: "update-property", blockId, propertyId, patch };
|
|
}
|
|
|
|
export function removeProperty(blockId: string, propertyId: string): RemovePropertyOp {
|
|
return { kind: "remove-property", blockId, propertyId };
|
|
}
|
|
|
|
export function addAssociation(association: Association, tempId?: string): AddAssociationOp {
|
|
return { kind: "add-association", association, tempId };
|
|
}
|
|
|
|
export function updateAssociation(associationId: string, patch: UpdateAssociationOp["patch"]): UpdateAssociationOp {
|
|
return { kind: "update-association", associationId, patch };
|
|
}
|
|
|
|
export function removeAssociation(associationId: string): RemoveAssociationOp {
|
|
return { kind: "remove-association", associationId };
|
|
}
|
|
|
|
export function addConstraint(constraint: Constraint, tempId?: string): AddConstraintOp {
|
|
return { kind: "add-constraint", constraint, tempId };
|
|
}
|
|
|
|
export function updateConstraint(constraintId: string, patch: UpdateConstraintOp["patch"]): UpdateConstraintOp {
|
|
return { kind: "update-constraint", constraintId, patch };
|
|
}
|
|
|
|
export function removeConstraint(constraintId: string): RemoveConstraintOp {
|
|
return { kind: "remove-constraint", constraintId };
|
|
}
|
|
|
|
export function addRequirement(requirement: Requirement, tempId?: string): AddRequirementOp {
|
|
return { kind: "add-requirement", requirement, tempId };
|
|
}
|
|
|
|
export function updateRequirement(requirementId: string, patch: UpdateRequirementOp["patch"]): UpdateRequirementOp {
|
|
return { kind: "update-requirement", requirementId, patch };
|
|
}
|
|
|
|
export function removeRequirement(requirementId: string): RemoveRequirementOp {
|
|
return { kind: "remove-requirement", requirementId };
|
|
}
|
|
|
|
export function addRelation(requirementId: string, relation: RequirementRelation): AddRelationOp {
|
|
return { kind: "add-relation", requirementId, relation };
|
|
}
|
|
|
|
export function removeRelation(requirementId: string, relationIndex: number): RemoveRelationOp {
|
|
return { kind: "remove-relation", requirementId, relationIndex };
|
|
}
|
|
|
|
export function decideElement(
|
|
element: { kind: ReviewableElementKind; id: string }
|
|
): DecideElementOp {
|
|
return { kind: "decide-element", element, decision: "keep" };
|
|
}
|
|
|
|
// ─── Helpers ─────────────────────────────────────────────────────────────
|
|
|
|
let counter = 0;
|
|
/** Generate a temp id. Distinct prefix so applyOps can recognize them. */
|
|
export function tempId(prefix = "tmp"): string {
|
|
counter++;
|
|
return `${prefix}_${Date.now().toString(36)}_${counter}`;
|
|
}
|
|
|
|
/** Quick property factory used by clients producing add-property ops. */
|
|
export function newProperty(name: string, type: PropertyType = { kind: "string" }, multiplicity: Multiplicity = "0..1"): Property {
|
|
return { id: tempId("p"), name, type, multiplicity };
|
|
}
|