diff --git a/README.md b/README.md index afc4191..0e4147c 100644 --- a/README.md +++ b/README.md @@ -48,6 +48,7 @@ OpenMBEE View Editor brings source editing, named model views and reviewed graph - **Edit highlighted SysML source.** CodeMirror provides line numbers, bracket matching/closing, indentation, find/replace and draft undo/redo. Apply or discard changes, undo/redo accepted source transactions, and keep the last valid model when a draft fails validation. - **Query the draft model.** Request engine-backed completion, symbol information and project-file definitions against the complete current source set. +- **Inspect requirement graphs.** Browse requirement definitions/usages, declared identifiers and documentation, typing, hierarchy and positive/negated satisfaction links. Follow exact source declarations; diagram links do not report executed test results. - **Open source-defined views.** Navigate named SysML views independently of the active source file. Inspect nested parts, typed inherited ports and resolved connection endpoints. - **Navigate source issues.** Open an issue’s reported file and line in the source editor. Missing or stale locations are explained while drafts and the last valid diagram are retained. - **Browse the model hierarchy.** Expand packages, definitions and declared members in the Model tree. Inspect type/kind labels; use arrow keys, type-ahead and Enter/Space to navigate and select. @@ -58,7 +59,7 @@ OpenMBEE View Editor brings source editing, named model views and reviewed graph - **Arrange your workspace.** Resize or collapse Explorer, Inspector, source/diagram panes and Issues using dividers, keyboard controls or panel options. - **Choose your appearance.** Use Light, Dark or Follow system, six diagram palettes and custom muted element colors. Theme and panel preferences stay in the browser. -Structural workflows have real-engine browser coverage; behavior, use-case, state, sequence and geometry renderers remain unavailable. See [capabilities and limits](docs/capabilities.md) and [local verification](docs/verification.md) for the implemented scope. +Structural and native requirement workflows have real-engine browser coverage; behavior, use-case, state, sequence and geometry renderers remain unavailable. See [capabilities and limits](docs/capabilities.md) and [local verification](docs/verification.md) for the implemented scope. In the source editor, use **Ctrl+F** / **Cmd+F** for find/replace and **Ctrl+Z** / **Cmd+Z** for draft editing undo; **Ctrl+Y** / **Cmd+Shift+Z** redo draft edits. **Tab** moves focus out of the editor. Toolbar **Undo source** / **Redo source** operate on accepted model revisions. Highlighting is lexical; the pinned engines still validate source when you apply it. Use **Ctrl+Space** for engine-backed completion, **F1** for symbol information and **F12** for project-file definitions; pointing at a symbol also requests hover information. Queries use the complete current draft source set. Unsupported completion edits and stale replies are rejected. Formatting and inline diagnostics remain future work. @@ -177,7 +178,7 @@ npm run test:e2e # fresh build, real engines and visual compa npm run preview -- --host 127.0.0.1 ``` -PR CI runs all fast checks plus twelve critical browser workflows, adding visual comparisons for code changes. Full browser coverage runs weekly, manually and for release/main/hotfix PRs and pushes. Run `npm run test:e2e -- --project=functional --grep @critical` to reproduce the smaller browser gate; `npm run test:e2e` still runs everything. See [CI coverage](docs/testing.md#ci-coverage) for selection and deferred coverage. +PR CI runs all fast checks plus thirteen critical browser workflows, adding visual comparisons for code changes. Full browser coverage runs weekly, manually and for release/main/hotfix PRs and pushes. Run `npm run test:e2e -- --project=functional --grep @critical` to reproduce the smaller browser gate; `npm run test:e2e` still runs everything. See [CI coverage](docs/testing.md#ci-coverage) for selection and deferred coverage. Functional checks can use installed Chrome with `PLAYWRIGHT_BROWSER_CHANNEL=chrome npm run test:e2e -- --project=functional`. Visual comparisons always use bundled Chromium and reviewed platform-specific baselines. The browser suite starts its own fresh production server; port **4175** must be free. See [browser testing and baseline review](docs/testing.md). @@ -193,7 +194,7 @@ Start with the [documentation index](docs/README.md) for current capabilities, a This implementation supports a bounded source/structure editing workflow. It does not claim complete SysML v2 language, notation, execution or interchange conformance. Owned-port boundary notation, effective conjugated directions, behavior renderers and broader editing commands remain explicit gaps. -Flexo MMS checkout and publishing are planned; the current workbench uses local project files. Read the [integration research](docs/research/openmbee-flexo-mms.md) and [roadmap](docs/next-steps.md#3-flexo-checkout-then-reviewed-publication) for the proposed direction. +Flexo MMS checkout and publishing are planned; the current workbench uses local project files. Read the [integration research](docs/research/openmbee-flexo-mms.md) and [roadmap](docs/next-steps.md#4-flexo-checkout-then-reviewed-publication) for the proposed direction. ## Contributing diff --git a/docs/README.md b/docs/README.md index 5df8c1b..9a93239 100644 --- a/docs/README.md +++ b/docs/README.md @@ -16,15 +16,16 @@ OpenMBEE View Editor is a sandbox browser workspace for SysML v2 source and name ## Implementation contracts -| Guide | Purpose | -| --------------------------------------------------------------------------- | -------------------------------------------------------------- | -| [Application shell](architecture/application-shell.md) | Landing, launcher, navigation and session preservation | -| [Workbench layout](architecture/workbench-layout.md) | Panels, toolbar alignment, model tree and editor lifetime | -| [Source editing and recovery](architecture/source-assistance-recovery.md) | Draft queries, issue navigation and browser recovery | -| [Structural views](architecture/structural-views.md) | Hierarchy, ports, connections and geometry projection | -| [Graphical authoring](architecture/graphical-authoring.md) | Reviewed semantic commands and supported endpoint rules | -| [Identity and layout preservation](architecture/identity-reconciliation.md) | Identity domains, verified edits and conservative invalidation | -| [Project format](architecture/project-format.md) | Portable version-1 data and compatibility rules | +| Guide | Purpose | +| --------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- | +| [Application shell](architecture/application-shell.md) | Landing, launcher, navigation and session preservation | +| [Workbench layout](architecture/workbench-layout.md) | Panels, toolbar alignment, model tree and editor lifetime | +| [Source editing and recovery](architecture/source-assistance-recovery.md) | Draft queries, issue navigation and browser recovery | +| [Structural views](architecture/structural-views.md) | Hierarchy, ports, connections and geometry projection | +| [Requirements and satisfaction](architecture/requirements-views.md) | Native graphs, identifiers/docs, relationship polarity, source reveal and bounded limits | +| [Graphical authoring](architecture/graphical-authoring.md) | Reviewed semantic commands and supported endpoint rules | +| [Identity and layout preservation](architecture/identity-reconciliation.md) | Identity domains, verified edits and conservative invalidation | +| [Project format](architecture/project-format.md) | Portable version-1 data and compatibility rules | ## Planned work and research diff --git a/docs/architecture/README.md b/docs/architecture/README.md index 6cd5a45..c8cdc02 100644 --- a/docs/architecture/README.md +++ b/docs/architecture/README.md @@ -40,6 +40,8 @@ Named views are actual SysML usages. Open existing views without authoring anoth Semantic subject identity, usage context and symbol identity remain distinct. Geometry changes never imply semantic containment or connectivity. The bounded structural projection consumes engine parent IDs, typed inherited ports and explicit connection endpoints; port selection retains its usage context. `moveSymbols` validates a complete presentation batch before publishing it, so moving a group and its descendants keeps version-1 absolute coordinates consistent without a source transaction. [Structural projection, engine trace and limits](structural-views.md). +Native `requirement` artifacts have a dedicated read-only projection with exact identifiers/documentation, selected/contextual membership, typing, direct/type-based ownership and declared satisfaction polarity. Node/edge correspondence supports source reveal, including anonymous relationships. The default compressor requirements tree is retained; native requirement rendering does not imply automatic system/usage expansion or execution results. [Requirements contract and limits](requirements-views.md). + Reviewed `createPart` and `connectPorts` commands share this transaction path with rename and view creation. The workspace verifies explicit endpoint direction, identical resolved port type and actual evaluated connection endpoints; Toolkit edit acceptance alone is insufficient. [Graphical authoring contracts](graphical-authoring.md). ## TypeBox diff --git a/docs/architecture/requirements-views.md b/docs/architecture/requirements-views.md new file mode 100644 index 0000000..3135a81 --- /dev/null +++ b/docs/architecture/requirements-views.md @@ -0,0 +1,117 @@ +# Requirements and satisfaction diagrams + +Implemented bounded projection, 2026-11-03, on `feature/requirements-traceability`. Native requirement artifacts render requirement definitions/usages, declared identifiers and documentation, typing, direct/type-based ownership and declared positive/negated satisfaction. Model semantics remain read-only through this diagram; presentation movement and source navigation are permitted. The session gates below retain the implementation scope; [verification](../verification.md) records actual checks. + +## User outcome + +Open the requirements example or the compressor project, choose an appropriate authored view, and understand: + +- Which requirement definitions and usages are shown, including available declared identifiers, names and documentation. +- Which requirements contain other requirements, independently of definition/usage typing. +- Which exact model elements have declared, resolved satisfaction relationships to those requirements. +- Where each element and relationship is declared in source, including repeated usages with distinct context. + +A satisfaction declaration is not proof that a requirement is met. Do not display pass/fail, verified status or global coverage percentages. An absent visible link means no satisfaction link is shown in this scope; it does not establish that the whole model has no satisfier. + +## Engine and representation boundary + +`Diagram.tsx` accepts tree, interconnection, definition and native `requirement` artifacts. The capacity and explicit traceability fixtures now render dedicated requirement graphs. The compressor requirements view remains a partial generic `tree`; the frontend does not coerce every requirement-bearing tree into a richer graph. A tested alternative compressor exposure would also introduce verification context and declaration-owned satisfier endpoints rather than full instance paths, so that model is unchanged pending a separate expansion contract. + +The retained `engine-bridge/opensysml/relationships.go` implements `EvaluateRelationships` for resolved allocation, satisfaction and verification endpoints, including declaration/source/target origins. Its retained Go tests cover aliases, cross-file endpoints, deterministic results and refusal of invalid/incomplete evidence. This code is a useful seam, not fresh execution evidence for this session. The browser semantic worker currently exposes parse and named-view evaluation only. + +The existing `EvaluateView` path supplies the normalized artifact; no additional worker operation is needed. Optional node fields type native `detail`, exact `shortName`, full AST-backed `documentation` and selected/contextual `exposure`. `detail` remains an opaque native excerpt; the frontend never splits it into identifiers or documentation. Satisfaction edges carry explicit `negated` and `asserted` booleans (`asserted` records the explicit source `assert` prefix, never an execution result); composition/reference edges carry `ownership: direct | via-type` from exact origins. Native requirement validation requires selected subjects, valid topology, exact node/edge source correspondence, consistent exposure, explicit satisfaction polarity and noncyclic direct ownership before publication. Engine pins and archives are unchanged; overlay/test bytes update derived-WASM provenance. + +Preserve authored view exposure and filters. The capacity view filters for requirements but its recorded native graph includes `storage`; compare selected membership, artifact nodes and correspondence to understand engine-provided related context. Do not assume all artifact nodes are directly exposed, hide native context without explanation, or synthesize additional nodes ourselves. For truly absent endpoints, show explicitly outside-view context or use a deliberately authored traceability view exposing both endpoint families. Never inject hidden view declarations. + +## Implementation checkpoints + +Each checkpoint must leave a reviewable, working subset. Record actual commands, outcomes and gaps in verification; keep transient traces under ignored `sandbox/`. + +| Checkpoint | Deliverable | Completion gate | +| --------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| 1. Prove the engine contract | Retained captures from the actual pinned browser workers for capacity and compressor requirements, resolved satisfaction, nested ownership, aliases and repeated usages | Exact endpoints, origins and complete/invalid/indeterminate states are demonstrated. Identify how view exposure/filter membership and requirement metadata are obtained. Stop the renderer design from assuming missing evidence. | +| 2. Define the projection boundary | Owning TypeBox schemas, normalized requirement data and relationship correspondence; browser transport for the proven read-only operation | Workspace stays independent of React, DOM and engine implementations. Replies bind to the applied revision; malformed, stale, cancelled and partial results cannot replace valid state. Preserve engine pins, checksums and provenance. | +| 3. Render requirements | Dedicated requirement cards and hierarchy/typing notation for the bounded fixture set | Definition versus usage, containment versus typing, identifiers, names and available documentation are distinguishable. Missing metadata has an honest fallback. Existing diagram families still render correctly. | +| 4. Add satisfaction traceability | Declared satisfaction edges with exact endpoint identity and declaration reveal | Direction and labels follow verified engine evidence. Several satisfiers, several requirements and repeated usage names are unambiguous. Hidden endpoints are explained rather than silently dropped or invented. | +| 5. Make browsing usable | Selection, Inspector details, source reveal, keyboard navigation, fit/zoom and bounded layout movement | Browsing changes no source, drafts or semantic history. Switching views/files retains the intended context. Labels and edges remain usable at desktop and narrow widths in light/dark. | +| 6. Verify persistence and failure | Exact save/reopen and compatible geometry; deliberate failure and stale-response scenarios | Existing named-view positions are not borrowed for another representation. Preserve version-1 compatibility where possible; any saved-format change requires an explicit schema/version decision and compatibility fixtures before implementation. Rejected work retains the live project and drafts. | +| 7. Review and integrate | Updated example catalog, capabilities, owning contracts, screenshots and PR to develop | Fast checks, fresh production build, focused real-engine regressions and full visual project pass. Inspect intentional baseline changes. Hosted CI is reported separately. Keep manual deployment as a separate authorized step. | + +Suggested atomic commit boundaries are engine evidence/bridge, validated projection, renderer, satisfaction interactions, preservation regressions and documentation. Combine boundaries when splitting would leave broken transport or callers. Do not publish placeholder UI for an unproven contract. + +## Subagents and worktrees + +The session uses one coordinator/integration checkout and three bounded coding worktrees, all started at committed planning baseline `a097907`. The engine contract landed as `af5d1f3`, the renderer as `cf73d95` with compact follow-up `ba14383`, and focused functional tests as `455d0ed`. The coordinator integrates shared changes; dependent branches consume explicit compiling checkpoints rather than independently advancing `develop`. Actual commits remain identifiable in Git history. + +| Owner | Branch / sibling worktree suffix | Exclusive implementation ownership | +| ------------------ | ---------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Coordinator | `feature/requirements-traceability` / primary checkout | Owning workspace schemas/validation/reconciliation, shared worker request schema, `Diagram.tsx` dispatch and node/edge selection, Workbench/Inspector integration, `ui/theme.css`, shared test helpers, persistence decisions and capability/architecture docs | +| Engine agent | `feature/requirements-engine` / `-requirements-engine` | Retained OpenSysML bridge overlays and their Go tests, `engines/opensysml/` adapter/worker changes, engine preparation/provenance updates only when necessary, engine-specific probe fixtures/captures | +| Renderer agent | `feature/requirements-renderer` / `-requirements-renderer` | New `RequirementNode.tsx`, `RequirementEdge.tsx` only if needed, `requirements.ts`, `requirements.test.ts`, and `ui/requirements.css`; no edits to the shared shell or contract | +| Fixture/test agent | `test/requirements-diagrams` / `-requirements-tests` | New `examples/requirements-traceability/` fixtures, `tests/e2e/requirements.spec.ts`, `requirements-support.ts`, requirements additions to `visual.spec.ts` and new snapshots, example catalog/index updates after real captures | + +Planning audits may inspect the primary checkout read-only. Coding agents must edit only their assigned worktree and paths. Shared-file needs go to the coordinator; an agent must not widen ownership silently. The three older dirty language/recovery/editor-test worktrees are unrelated and remain untouched. + +### Execution waves + +1. **Evidence first.** Engine agent probes the existing capacity and compressor artifacts, native `detail`, selected versus contextual nodes, source correspondence, nested ownership versus native owner-to-type composition, repeated usages, and positive/negated satisfaction. Fixture agent prepares small model inputs and assertion inventories; renderer agent studies notation and dimensions. No UI assumptions are frozen from invented data. +2. **Contract checkpoint.** Coordinator resolves the returned evidence into the smallest validated artifact contract and shares its exact commit/schema. Agree on relation direction, source mapping, optional metadata, incomplete evidence and exposure/context handling. If negated satisfaction cannot be distinguished, do not display it as a positive satisfaction assertion. +3. **Parallel implementation.** Engine, renderer and test agents build their isolated slices against that contract. Controlled renderer/unit fixtures establish UI behavior only; they do not stand in for pinned-engine acceptance. Any changed contract is an explicit coordinator checkpoint that dependent branches consume before continuing. +4. **Integrate incrementally.** Coordinator reviews and integrates engine/contract changes first, renderer files next, then fixtures/tests, wiring shared selection and Inspector paths. Keep atomic compiling commits when commits are authorized. Preserve branch ancestry; no force pushes or overwriting another agent's changes. +5. **Regression and visual review.** Coordinator alone runs production browser builds, focused requirements/preservation checks and baseline generation/comparison. Agents can run isolated unit/type checks. Inspect all intentional screenshots and demonstrate important assertions failing under a bounded fault before restored passing runs. +6. **Close the session.** Update actual capabilities and verification, run the required shared-code regression scope, open one feature PR to `develop` and report hosted checks when push/PR work is authorized. Manual Vercel publication remains separate. Remove only clean merged coding worktrees after preserving their evidence and checking for unpublished work. + +Each agent's handoff must state its base commit and contract version, files changed, executed commands/results, skipped coverage, artifacts, unresolved risks and its proposed integration commit(s). Do not claim successful conformance from a build or screenshot alone. + +### Shared integration decisions + +- Prefer rendering the proven native `requirement` artifact before inventing a new requirement extraction endpoint. Full documentation now comes from AST documentation spans; a combined abbreviated `detail` string is not structured metadata. +- Ground exact relationship meanings and notation in the formal SysML v2 specification alongside pinned-engine evidence. The imported tutorial series supports the shared definition/usage mental model, not detailed satisfaction/use-case conformance. +- A native `composition` edge can connect an owner to a type while labeling an undrawn owned usage. Do not call that literal ownership of the drawn type; prove hierarchy through resolved membership and edge correspondence. +- Native edge correspondence uses `edge:` while current generic rendered edges use `edge-`. Align selectable identity and source correspondence in coordinator-owned code; never match edges by label. +- Requirement graphs need kind-aware topology validation, not only the existing interconnection validation. Reject duplicate IDs, dangling endpoints and unsupported/ambiguous correspondence without replacing a valid diagram. +- Keep requirement creation/satisfaction editing disabled. The broadly available New part action must not appear to author requirements. +- A tree-to-requirement representation change must not silently borrow incompatible positions. Keep absolute coordinate ownership in the existing presentation flow; any persistence change follows the project-format gate. +- Do not broaden diagram support by treating every tree containing a requirement as a full requirements projection. The compressor view needs its own evidence and an explicit delivered scope. + +### Test/build coordination + +Install dependencies from the committed lockfile per coding worktree using the selected Node runtime and context checks. Do not commit `node_modules`, link it as an untracked source file, or share mutable engine preparation outputs among concurrent builds. Keep each worktree's generated files in its own ignored `sandbox/` and `static/vendor/`. + +The production browser test runner builds the current source and reserves port 4175. Run only one such build/test sequence at a time under the coordinator. The fixture/test agent is the sole writer of `visual.spec.ts` and its requirements snapshots; do not add a separate visual spec without changing project matching deliberately. Add one coherent `@critical` requirements workflow; extended failure cases stay in the full suite. Request Context7 documentation before library-dependent implementation, follow current shadcn conventions, and add no production dependencies without approval. + +## Look and interaction + +Reuse the current shadcn/Radix controls and theme tokens. Requirements should have a recognizable restrained header, a small definition/usage label, prominent declared identifier/name and readable documentation. Use compartments only for metadata actually supplied by the engine. Long text needs deliberate wrapping or an explicit expand action; it must not cover edges or handles. + +Use separate visual treatments and text labels for containment, typing and satisfaction. Do not use color alone. Distinguish contextual satisfying model elements from requirements. Prefer a clear automatic layout and existing fit/zoom controls over introducing a general-purpose routing editor in this slice. + +Selection should reveal requirement details or a relationship's declaration and resolved endpoints. Provide accessible names, visible keyboard focus and focus return for any dialog. Selection is transient; any future create/edit/delete/reconnect action needs the reviewed semantic transaction path. + +## Fixtures and acceptance evidence + +Follow the [example-library workflow](../../examples/README.md). Extend small independent fixtures deliberately rather than treating the large compressor model as the only proof. + +- Capacity example: declared identifier, documentation, requirement definition/usage and the exact storage-to-capacity satisfaction relationship. +- Hierarchy fixture: nested requirement usages and typing, proving that ownership and typing do not collapse into one edge meaning. +- Traceability fixture: multiple requirements/satisfiers, identical display names under different owners, aliases and cross-file references, and negated satisfaction that must never be labeled positive. Author an explicit view exposing the intended nodes. +- Failure fixtures: unknown references, malformed/partial transport, engine asset failure, cancellation and superseded evaluation. Test valid empty relationships separately from unavailable evidence. +- Compressor integration: retain all nine source files, drafts, independent source/view choice and existing structural layouts while browsing its requirements. + +For important new regressions, demonstrate failure under a bounded fault, restore the behavior and rerun. Assertions should establish exact semantic endpoint identity, source location and project preservation, not only node counts or screenshots. Add a focused critical real-engine path for the new supported workflow without promoting every extended matrix case into PR CI. + +Run `npm run check`, `npm run build`, relevant functional browser scenarios and the full visual project. Inspect desktop/narrow and light/dark captures, keyboard access and any dialog focus containment/return. Full semantic regressions are required when shared projection, reconciliation or transaction behavior changes. No skipped assertion, relaxed mismatch threshold or automatic baseline update substitutes for diagnosing a failure. + +## Notation scope + +The [SysML v2 language specification](https://www.omg.org/spec/SysML/2.0/Language/PDF), requirement satisfaction in section 7.21.1, distinguishes positive and negated satisfaction assertions. These are model declarations; this UI does not execute them or report evaluation results. Text labels distinguish typing, direct containment, owned-feature-via-type and positive/negated satisfaction. Shapes and styling are a bounded editor presentation, not a claim of complete standardized graphical notation. The [OMG issue tracker](https://issues.omg.org/issues/spec/SysML/2.0) records SYSML21-671 concerning missing graphical productions for negated satisfaction; explicit text preserves that distinction without inventing a standard symbol. + +Requirement cards use stable bounds by native kind, a bounded documentation preview and full available text in the Inspector. Related model elements have compact neutral cards. Nodes and relationships are keyboard selectable; `edge:` aligns with revision-bound native correspondence, including anonymous relationship declarations. Source/target details have separately named accessible groups. The diagram hides part/connection authoring and Inspector semantic edit controls in requirement mode. Invalid drafts retain the last applied graph and disable geometry edits. + +Positions remain absolute model-space coordinates in project format version 1. Requirement pointer and keyboard movement use the existing validated presentation update. Source-driven representation changes invalidate old geometry explicitly instead of borrowing another representation's layout. Some typing edges share smoothstep runs; selection/source reveal distinguishes the exact declarations. Camera persistence, full constraint/subject compartments, manual overlap avoidance and obstacle-aware/parallel routing remain outside this slice. + +## Deferred work + +Verification-case diagrams and result ingestion, coverage dashboards, requirement/satisfaction graphical authoring, automatic declaration-free exploration, new general routing controls, matrices/tables and additional behavior diagram families are separate follow-ups. Verification declarations must never be presented as executed tests. + +This plan does not promise complete SysML requirements notation or language conformance. If the first checkpoint cannot establish identifiers, hierarchy or satisfaction endpoints for a fixture, record the limitation and narrow the delivered subset explicitly rather than infer semantics. diff --git a/docs/architecture/structural-views.md b/docs/architecture/structural-views.md index ec34508..da66762 100644 --- a/docs/architecture/structural-views.md +++ b/docs/architecture/structural-views.md @@ -37,11 +37,11 @@ This slice establishes the fixture's nested interconnection workflow. GeneralVie ## Planned edge interactions and routing -This follow-up is not implemented. Context7 MCP documentation was checked against the installed React Flow implementation on 2026-11-01. +The broader structural/tree interaction and routing follow-up remains planned. Native requirement graphs now support pointer/keyboard relationship selection and exact endpoint/source inspection. Context7 MCP documentation was checked against the installed React Flow implementation on 2026-11-01. Current projections use ELK node placement and React Flow `smoothstep` edges; they do not consume ELK edge sections. The structural layout does not pass actual port geometry to ELK, and saved node positions can replace the initially calculated coordinates. Adopting initial ELK routes without reconciling those positions would leave stale paths. -Start with controlled edge selection, keyboard focus/selection styling and descriptive endpoint labels across structural and tree/definition views. `Diagram.tsx` currently wires node changes but not `onEdgesChange`; verify edge selection through the controlled state using [applyEdgeChanges](https://reactflow.dev/api-reference/utils/apply-edge-changes). Keep selection transient and source/history/presentations unchanged. Any future reconnect/delete action must use a reviewed semantic command rather than direct canvas mutation; those commands are not implemented. +Extend controlled edge selection, keyboard focus/selection styling and descriptive endpoint labels across structural and tree/definition views. `Diagram.tsx` now applies edge changes to controlled state; requirement-specific selection also updates the Inspector through recorded correspondence. Verify the broader views independently before extending that guarantee. Keep selection transient and source/history/presentations unchanged. Any future reconnect/delete action must use a reviewed semantic command rather than direct canvas mutation; those commands are not implemented. Built-in smoothstep edges already pass the [path helper's label coordinates](https://reactflow.dev/api-reference/utils/get-smooth-step-path) to SVG labels, and [BaseEdge](https://reactflow.dev/api-reference/components/base-edge) already supplies an invisible 20 px hit area. Measure selection at representative zoom levels before choosing a larger hit width. Keep plain labels in SVG; use [EdgeLabelRenderer](https://reactflow.dev/api-reference/components/edge-label-renderer) only for richer or interactive HTML, with explicit pointer events and `nodrag nopan`. Complete [edge descriptions and focus behavior](https://reactflow.dev/learn/advanced-use/accessibility) without implying full accessibility coverage. diff --git a/docs/capabilities.md b/docs/capabilities.md index 946e18b..54b20af 100644 --- a/docs/capabilities.md +++ b/docs/capabilities.md @@ -2,27 +2,29 @@ This catalogue describes the current implementation. Check [verification](verification.md) for checks actually performed; a feature description alone is not test evidence. -| Workflow | Support | Limit | -| -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Entry and shell | OpenMBEE landing, explicit project launcher, example/new/import/recovery actions; live-session resume and browser navigation | Folder write-back, remote checkout and automatic declaration-free exploration remain planned | -| Project sources | Multiple imported SysML files resolve together | 20 files / 5 MiB per source set; one active source editor | -| Source editing | CodeMirror lexical highlighting, line numbers, bracket matching/closing, indentation, find/replace, draft history, apply/discard and accepted source history | No formatting, inline diagnostics or syntax-aware folding; unsafe declaration/context matches lose affected positions | -| Source assistance | Toolkit completion, hover and project-file definitions bound to the full current draft source set | Explicit plain-text completion only; snippets/additional edits, ambiguous and library-only definition targets are unavailable | -| Named views | Open declarations; create GeneralView/InterconnectionView usages in a package | Custom filters/definitions remain editable in source | -| Diagram rendering | Flat tree views; nested interconnection parts, typed inherited ports, explicit directions and resolved port endpoints through React Flow/ELK | Owned-port boundary notation, effective conjugated directions, modeled layout/routes and behavior/sequence/geometry renderers remain future work | -| Semantic editing | Reviewed part definition/usage rename, native view creation, typed part creation and directed same-type port connections | Simple identifiers and qualified names; explicit out/inout to in/inout with identical resolved types; conjugated, ambiguous or undirected endpoints, reconnect, delete, duplicate and behavior editing are unavailable | -| Model navigation | Expandable ownership tree, declared members, type/kind labels, keyboard navigation and diagram-selection reveal | View declarations use the separate Views list; inherited members are not cloned beneath usages; no full-model search or virtualized rendering | -| Source navigation | Recorded origin range from diagram/port selection, snapshot-bound Issues file/line navigation and explicit file-only handling | Explorer-only selection does not yet resolve a source range; stale, unknown and out-of-range diagnostic locations are explained without guessing | -| Diagram presentation | Move nested groups, retain descendant positions, reconcile compatible positions across reviewed edits and bounded source changes, save/reopen and refit panes | Changed bindings, headers, owners or ambiguous correspondence invalidate affected positions; manual overlap avoidance, parent resizing, camera persistence, route edits and independent layout undo remain future work | -| Workbench panels | Resizable Explorer, Inspector, source/diagram split and Issues; collapse/restore, keyboard and button resizing, reset and browser layout preferences | Full docking/floating windows are unavailable; preferences do not recover source work | -| Save/reopen | Version-1 bundle of source, drafts and positions; local recovery with explicit restore/discard and active file/view | One IndexedDB recovery copy per browser origin; storage can be cleared or unavailable; no camera persistence or folder write-back | -| Runtime validation | TypeBox project/command/message/engine schemas | Schema validity is separate from SysML semantic validity | -| Appearance | Shared shadcn/Radix controls; Light/Dark/System, six palettes and custom muted package/part fills | Browser preferences, separate from project files; detailed port color controls remain unavailable | -| Execution | No simulation controls | Rendered behavior or declared verify links would not imply execution results | +| Workflow | Support | Limit | +| -------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| Entry and shell | OpenMBEE landing, explicit project launcher, example/new/import/recovery actions; live-session resume and browser navigation | Folder write-back, remote checkout and automatic declaration-free exploration remain planned | +| Project sources | Multiple imported SysML files resolve together | 20 files / 5 MiB per source set; one active source editor | +| Source editing | CodeMirror lexical highlighting, line numbers, bracket matching/closing, indentation, find/replace, draft history, apply/discard and accepted source history | No formatting, inline diagnostics or syntax-aware folding; unsafe declaration/context matches lose affected positions | +| Source assistance | Toolkit completion, hover and project-file definitions bound to the full current draft source set | Explicit plain-text completion only; snippets/additional edits, ambiguous and library-only definition targets are unavailable | +| Named views | Open declarations; create GeneralView/InterconnectionView usages in a package | Custom filters/definitions remain editable in source | +| Diagram rendering | Native requirement graphs with identifiers, documentation, typing, direct/type-based ownership and declared positive/negated satisfaction; flat tree views; nested interconnection parts, ports and resolved endpoints | Requirement constraints/subject compartments, generalized typed-instance trace expansion, owned-port boundary notation, effective conjugated directions, modeled layout/routes and behavior/sequence/geometry renderers remain future work | +| Semantic editing | Reviewed part definition/usage rename, native view creation, typed part creation and directed same-type port connections | Simple identifiers and qualified names; explicit out/inout to in/inout with identical resolved types; conjugated, ambiguous or undirected endpoints, reconnect, delete, duplicate and behavior editing are unavailable | +| Model navigation | Expandable ownership tree, declared members, type/kind labels, keyboard navigation and diagram-selection reveal | View declarations use the separate Views list; inherited members are not cloned beneath usages; no full-model search or virtualized rendering | +| Source navigation | Recorded origin range from diagram/port selection, snapshot-bound Issues file/line navigation and explicit file-only handling | Explorer-only selection does not yet resolve a source range; stale, unknown and out-of-range diagnostic locations are explained without guessing | +| Diagram presentation | Move nested groups, retain descendant positions, reconcile compatible positions across reviewed edits and bounded source changes, save/reopen and refit panes | Changed bindings, headers, owners or ambiguous correspondence invalidate affected positions; manual overlap avoidance, parent resizing, camera persistence, route edits and independent layout undo remain future work | +| Workbench panels | Resizable Explorer, Inspector, source/diagram split and Issues; collapse/restore, keyboard and button resizing, reset and browser layout preferences | Full docking/floating windows are unavailable; preferences do not recover source work | +| Save/reopen | Version-1 bundle of source, drafts and positions; local recovery with explicit restore/discard and active file/view | One IndexedDB recovery copy per browser origin; storage can be cleared or unavailable; no camera persistence or folder write-back | +| Runtime validation | TypeBox project/command/message/engine schemas | Schema validity is separate from SysML semantic validity | +| Appearance | Shared shadcn/Radix controls; Light/Dark/System, six palettes and custom muted package/part fills | Browser preferences, separate from project files; detailed port color controls remain unavailable | +| Execution | No simulation controls | Rendered behavior or declared verify links would not imply execution results | This repository does not claim full SysML v2 language, notation, execution or interchange conformance. -Use **New part** and **Connect ports** in a diagram, or the corresponding Inspector actions, to review source-backed creation commands. Part creation chooses an explicit source owner and existing definition; connections choose actual usage-qualified endpoints. Toolkit supplies source edits, OpenSysML validates the complete candidate, and the workspace checks ownership, bindings and exact connection endpoints before accepting one transaction. New symbols receive automatic layout; source exposure still determines whether they appear in an open view. +Requirements diagrams are read-only for graphical model edits. They retain native selected versus related-context elements, expose full available documentation in the Inspector, and reveal exact node/relationship source ranges through pointer or keyboard selection. Satisfaction and negated satisfaction are declarations, not executed verification results. Their positions use the existing portable presentation flow; keyboard movement is retained as well as pointer movement. See [requirements projection and limits](architecture/requirements-views.md). + +Use **New part** and **Connect ports** in supported structural diagrams, or the corresponding Inspector actions, to review source-backed creation commands. Part creation chooses an explicit source owner and existing definition; connections choose actual usage-qualified endpoints. Toolkit supplies source edits, OpenSysML validates the complete candidate, and the workspace checks ownership, bindings and exact connection endpoints before accepting one transaction. New symbols receive automatic layout; source exposure still determines whether they appear in an open view. Layout reconciliation checks every saved view, including inactive views. Verified commands use source-bound UTF-8 splices; free-form edits use conservative replacement ranges and require unchanged engine-recorded declaration headers, complete usage contexts and compatible resolved bindings. Unknown or ambiguous mappings are invalidated explicitly. This is bounded preservation, not a general persistent-identity guarantee. See [the implemented policy](architecture/identity-reconciliation.md) and [authoring contracts](architecture/graphical-authoring.md). diff --git a/docs/git-workflow.md b/docs/git-workflow.md index 8e7f30b..9c9f208 100644 --- a/docs/git-workflow.md +++ b/docs/git-workflow.md @@ -30,7 +30,7 @@ Implement a focused change, run the relevant checks, commit with a Conventional Documentation and ordinary fixes also target `develop`. Reserve `hotfix/*` for an urgent correction to code already released on `main`. -CI validates every PR. Pushes to `main`, `develop`, `release/**` and `hotfix/**` also run validation. The existing workflow still checks formatting, architecture, types, unit/script tests, dependency audit and the pinned-engine browser suite. Local success and a hosted CI result are separate evidence. +CI validates every PR. Pushes to `main`, `release/**` and `hotfix/**` run the full suite, as do release-targeted PRs, weekly schedules and manual dispatch. There is no duplicate validation push after merging into `develop`. Development PRs run all fast checks and critical browser workflows, adding visual comparisons for code changes; see [CI coverage](testing.md#ci-coverage). Local success and a hosted CI result are separate evidence. ## Prepare a release diff --git a/docs/next-steps.md b/docs/next-steps.md index 3e7464d..2b98205 100644 --- a/docs/next-steps.md +++ b/docs/next-steps.md @@ -1,14 +1,18 @@ # Next steps -Updated 2026-11-01. This is the forward plan; [capabilities](capabilities.md) describes shipped behavior and [verification](verification.md) records actual checks. No delivery dates are assigned. +Updated 2026-11-04. This is the forward plan; [capabilities](capabilities.md) describes shipped behavior and [verification](verification.md) records actual checks. No delivery dates are assigned. ## Current position The editor supports reviewed rename/view/part/connection commands, bounded layout preservation, CodeMirror source editing and draft language queries, issue navigation, resizable panels and explicit browser recovery. The OpenMBEE landing → launcher → workbench flow is implemented. Opening the app does not load an example implicitly. Current diagrams require authored named views. -## 1. Read-only automatic exploration +## 1. Requirements and satisfaction diagrams -Next feature priority: inspect an imported model without adding view declarations. Use a project/package overview when a system subject is ambiguous; offer structural exploration only where the engine demonstrates the intended definition or usage expansion. +The active feature branch `feature/requirements-traceability` implements a bounded [native requirements renderer](architecture/requirements-views.md), with identifiers/documentation, typing/ownership, declared satisfaction polarity, source reveal and presentation preservation. The capacity and traceability fixtures establish this subset; the compressor requirements view remains a generic tree. Complete review and validation before integrating. Declared satisfaction is not a test result. Verification coverage and graphical relationship authoring remain separate follow-ups. + +## 2. Read-only automatic exploration + +Separate planned track: inspect an imported model without adding view declarations. Use a project/package overview when a system subject is ambiguous; offer structural exploration only where the engine demonstrates the intended definition or usage expansion. The [engine investigation](research/automatic-exploration-contract.md) is complete for its bounded probes: native `RenderExposed` is promising, but the browser worker currently exposes only named-view evaluation. The tested typed usage did not unfold its definition's internals. Implement and validate that contract before promising a consolidated system diagram. @@ -22,13 +26,13 @@ Acceptance is owned by the [exploration plan](architecture/model-exploration.md) Then add reviewed named-view creation from an exploration and define browsing-context persistence. Portable bundles and recovery do not currently store exploration scope/representation; any format change needs schema/version and compatibility fixtures. -## 2. Local folder workflow +## 3. Local folder workflow Start with explicit import semantics: membership, exclusions, source limits, permission cancellation and unsupported-browser fallback. Existing Open imports copies and Download exports a bundle. Direct file saving is a separate gate: identify written files, validate permission, detect external changes and retain local work on failed or ambiguous writes. Do not label imported copies as folder-backed editing. See [shell capability gates](architecture/application-shell.md#future-opening-workflows). -## 3. Flexo checkout, then reviewed publication +## 4. Flexo checkout, then reviewed publication Follow the [integration research](research/openmbee-flexo-mms.md): prove unchanged interchange and foreign element identity preservation on a pinned deployment, then read an immutable commit selected through project/branch. Identify generated authoring text explicitly. diff --git a/docs/testing.md b/docs/testing.md index cb49742..283cfff 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -28,12 +28,12 @@ The browser-channel override applies only to functional checks. Visual checks al The required `checks` job always runs formatting, architecture, TypeScript, unit/script tests and the dependency audit. Browser coverage is selected separately: -| Trigger | Browser coverage | -| ----------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Every PR | Twelve `@critical` real-engine scenarios: entry, source-defined views, invalid drafts, rename and cancellation, native view creation, typed part/connection authoring, source undo/redo, exact save/reopen, engine failure recovery and IndexedDB restore | -| PR with code, assets, examples, tooling or unknown paths | All visual comparisons as well as the critical scenarios | -| Documentation-only PR | Critical scenarios; no visual comparisons | -| PR targeting `main`, `release/**`, `hotfix/**`; weekly Monday at 04:00 UTC; manual dispatch; or push to `main`, `release/**`, `hotfix/**` | Complete functional and visual suites | +| Trigger | Browser coverage | +| ----------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Non-release PR | Thirteen `@critical` real-engine scenarios: entry, source-defined views, invalid drafts, rename and cancellation, native view creation, typed part/connection authoring, source undo/redo, exact save/reopen, engine failure recovery, IndexedDB restore and exact requirements/satisfaction browsing | +| Non-release PR with code, assets, examples, tooling or unknown paths | All visual comparisons as well as the critical scenarios | +| Documentation-only non-release PR | Critical scenarios; no visual comparisons | +| PR targeting `main`, `release/**`, `hotfix/**`; weekly Monday at 04:00 UTC; manual dispatch; or push to `main`, `release/**`, `hotfix/**` | Complete functional and visual suites | There is no duplicate run on a merge push to `develop`; its PR must pass `checks`. Scheduled runs use the repository's default branch. The full run must pass before release, and remains available manually on any branch. Scheduled failures need triage; they do not retroactively block an already merged PR. The reduced PR suite intentionally defers the extended preservation/failure matrix, language assistance, engine identity investigations and example-library sweeps to full runs. @@ -72,12 +72,14 @@ Run `npm run test:e2e -- --project=functional language-assistance.spec.ts recove Run `npm run test:e2e -- --project=functional shell-entry.spec.ts recovery.spec.ts default-example.spec.ts` for entry and replacement behavior. These scenarios check landing/launcher without implicit model work, explicit example and direct app links, New/Open, modal focus, cancellation, actual parser failure and exact retained work through launcher/home/Back/Forward navigation. Recovery scenarios start from the genuinely empty launcher after reload instead of relying on an automatically opened example. The diagram's Named view selector is checked independently of the active source document. Visual entry scenarios capture landing/launcher at desktop, narrow and mobile widths in both themes, alongside the workbench and its dialogs. -`semanticPositions()` compares saved doubles exactly using semantic subject names plus complete usage contexts. `expectRenderedPositions()` compares actual node transforms with the browser's own CSS serialization of expected coordinates; CSSOM rounds transforms to six significant digits, so raw JavaScript doubles cannot be compared directly with parsed CSS values. The helper retains its original 0.0001 coordinate threshold after normalization. This does not relax the exact portable-project comparison or test identity through renderer IDs. +`semanticPositions()` compares saved doubles exactly using semantic subject names plus complete usage contexts. Anonymous declarations must have a zero-length name, a matching recorded origin identity and in-bounds UTF-8 source bytes; their keys retain that exact source identity/content and full context instead of inventing a name or ignoring the saved position. `expectRenderedPositions()` compares actual node transforms with the browser's own CSS serialization of expected coordinates; CSSOM rounds transforms to six significant digits, so raw JavaScript doubles cannot be compared directly with parsed CSS values. The helper retains its original 0.0001 coordinate threshold after normalization. This does not relax the exact portable-project comparison or test identity through renderer IDs. Write a concrete expected outcome before implementation. Explore the actual UI to choose accessible locators; prefer role/name or label over CSS structure. Keep special renderer coordinate reads in `tests/e2e/support.ts`. Compare model-space positions, since camera fit/zoom can change screen coordinates without changing saved geometry. Use real engines for supported semantic command acceptance, source application, and save/reopen. Controlled workspace responses remain useful for stale/forged proposals and cancellation. Network failure tests must establish that the injected failure occurred and check retained work plus recovery after removing the fault. A mocked engine result is not evidence of engine conformance. +`requirements.spec.ts` exercises the native capacity requirement graph and a warning-free traceability fixture through the actual pinned workers. Checks distinguish typing, direct ownership, positive and asserted-negated satisfaction, preserve repeated satisfier identity, reveal exact node/edge source, retain invalid drafts and independent layouts through fresh reopen, and recover from an injected engine asset failure with source undo/redo. Its critical scenario checks declared relationships without interpreting them as execution results. The compressor control remains a generic requirements tree and preserves all nine files plus its structural layout. Four requirement visual scenarios cover desktop/narrow widths in both themes, retaining six captures: two fitted overviews and four focused, readable card/Inspector states. + `diagram-style.spec.ts` checks structural label hierarchy, selection outline, keyboard port selection and physical connection endpoints through appearance changes, comparing the exact portable project before/after. It checks OUT-label contrast across all six palettes in light/dark. Selected structural-card baselines cover desktop/narrow widths in both themes. Visual checks complement source, identity and geometry assertions. Current baselines cover the default palette in light/dark workbenches, expanded Appearance dialogs and nested assembly diagrams, plus a 768 px workbench and dialog. Resized panels with the Issues dock have desktop and narrow captures in both themes. Part and connection forms have desktop and narrow snapshots in both light and dark themes; the connection form repeats complete selected endpoints in wrapping text below its selectors. Functional checks exercise all palettes and custom colors, structural containment, physical connector endpoints, keyboard port selection and group/child movement through save/reopen. Full visual parity across every palette and browser is not established. Keyboard checks cover Appearance, inherited-port/source interaction, authoring-dialog focus containment/return and a stable workspace focus fallback when an accepted edit removes its original trigger. They do not cover the complete editor. Automated accessibility scanning and Firefox/WebKit coverage are not currently configured. diff --git a/docs/verification.md b/docs/verification.md index d65b1cb..31008c9 100644 --- a/docs/verification.md +++ b/docs/verification.md @@ -1,5 +1,25 @@ # Verification record +## Native requirements and satisfaction checkpoint + +Implementation recorded 2026-11-03; final validation continued 2026-11-04 on `feature/requirements-traceability`, using Node 26.3.1, npm 11.16.0 and the pinned engines. Native requirement graphs now retain declared short identifiers, full available AST documentation, selected/contextual membership, satisfaction assertion/negation and direct versus type-based ownership. The app adds bounded requirement/model-element cards, labeled relationships, pointer/keyboard selection, exact source reveal and existing version-1 presentation persistence. No production dependencies, upstream engine pins, archive checksums or saved-format version changed. + +`npm run check` passed formatting, architecture, TypeScript, 141 Vitest tests and ten Node checks. `npm audit --audit-level=low` reported no vulnerabilities. Fresh production builds passed, retaining the existing bundle-size warning. + +Engine work retained fresh production semantic-worker/WASM captures for capacity, compressor, nested requirements, explicit traceability and compact owner-to-type examples. Fourteen capture-validation checks passed; native graph topology/order was retained while metadata was enriched. The retained Go bridge tests and derived WASM preparation passed, including anonymous satisfaction declarations, negative assertions, full documentation and direct/via-type relationships. Evidence copied to `sandbox/requirements-engine-evidence/`; the root fresh build log is `sandbox/cleanup-design/requirements-integration-build.log`. The default compressor view intentionally remains a generic tree: optional source-copy probes exposed additional verification context and declaration-owned rather than full instance-path satisfier identities, so its source was not changed. + +The warning-free traceability fixture uses explicitly exposed requirement members, distinct requirement usage names and repeated `tank` satisfier names under different owners. Actual engine evidence establishes ten requirement-graph nodes and nine edges, plus a separate thirty-node/fifteen-edge overview. Subject binding alone creates no satisfaction link to the unlinked requirement. Positive and asserted-negated satisfaction have distinct metadata and labels; neither is reported as an executed result. + +Focused checks: `npm run test:e2e -- --project=functional requirements.spec.ts assembly.spec.ts --output=sandbox/playwright/requirements-native-assembly --reporter=list --max-failures=1` passed four scenarios. The five requirements workflows then passed in 39.8 seconds under `sandbox/playwright/requirements-functional-final`, including exact endpoints/source, independent layouts, invalid drafts, fresh reopen, injected real engine asset failure/retry and source undo/redo. The later keyboard movement assertion passed its focused preservation run in 17.6 seconds under `sandbox/playwright/requirements-keyboard`; it compares the exact saved coordinate increment and fresh rendered positions. + +The initial preservation run exposed a test-helper assumption: valid anonymous constraint/satisfaction declarations had source origins but no qualified name. The helper now requires zero-length names, matching origin identities, bounded UTF-8 source bytes and complete context for those elements; it still compares every saved point. A separate failed movement run exposed an application Fit limit: the default 0.5 minimum zoom clipped the larger requirement graph. A new visible-bounds assertion failed with a node 72.17 px outside the canvas, then passed after requirement-only minimum zoom was set to 0.1. Other representation families retain their prior zoom limits. Evidence: `requirements-functional.log`, `requirements-functional-corrected-helper.log`, `requirements-fit-before.log`, and `sandbox/playwright/requirements-fit-before` under the primary ignored sandbox. + +Bounded regression demonstrations: removing the representation-change safeguard failed the reconciliation assertion, then the restored unit run passed (`sandbox/cleanup-design/requirements-representation-fault.log`, `requirements-reconciliation-restored.log`). Swapping relationship Inspector Source/Target failed the critical browser assertion on the exact source endpoint; restoration passed in 10.7 seconds (`requirements-endpoints-fault.log`, `requirements-endpoints-restored.log`, `sandbox/playwright/requirements-endpoints-*`). The renderer agent also removed negation text in an isolated mutation; the controlled layout regression failed, then passed after restoration. This controlled renderer evidence is separate from the actual-engine browser paths. + +Four new visual scenarios cover light/dark at 1440/768 px. Six new baselines retain fitted desktop overviews and focused, readable card/Inspector states. Focused setup uses real zoom/pan controls and compares all model-space node transforms before/after panning. Baseline generation is separate from normal comparison results. The complete functional run, `npm run test:e2e -- --project=functional --output=sandbox/playwright/requirements-full-functional --reporter=list --max-failures=1`, passed all 92 scenarios in 12.6 minutes without retries or skips. The final four-scenario baseline review ran separately under `sandbox/playwright/requirements-visual-final-review`; all six new images were inspected at their intended overview/detail scales. The first full visual attempt stopped during engine startup: the trace recorded `net::ERR_NETWORK_IO_SUSPENDED` on a WASM fetch before any screenshot assertion. A fresh run passed 48 scenarios before a new narrow requirement capture differed in scale. Its trace showed complete 1.2× zoom steps but a different starting fit. The test now normalizes through the visible maximum-zoom endpoint and five verified zoom-out actions, then pans without changing model-space positions. Eight isolated theme/width/repeat diagnostics, including deliberately different starting zooms, produced the same 273.2768 px card width and unchanged portable data. The corrected four-case baseline generation passed in 37.3 seconds under `sandbox/playwright/requirements-visual-normalized-review`; all six captures were inspected again. No existing baseline or mismatch threshold was changed. The final normal comparison, `npm run test:e2e -- --project=visual --output=sandbox/playwright/requirements-full-visual-normalized --reporter=list --max-failures=1`, passed all 51 scenarios in 3.7 minutes without baseline updates, retries or skips. Its log is `sandbox/cleanup-design/requirements-full-visual-normalized.log`. + +Firefox/WebKit, complete accessibility certification, generalized typed-instance trace expansion, full constraint/subject compartments and execution/verification results remain outside this checkpoint. Local results do not establish hosted CI; ignored evidence may be absent from another checkout. + ## Mobile header integration fix Recorded 2026-11-02. Review found that the base header rules overrode the earlier mobile breakpoint. At 390 px the 62 px banner contained actions extending to 83 px; the new containment assertion failed with a 21 px overflow. Moving the mobile rules after the base and wider breakpoints lets the header grow with its wrapped actions. diff --git a/engine-bridge/README.md b/engine-bridge/README.md index 5e3ba82..2f83332 100644 --- a/engine-bridge/README.md +++ b/engine-bridge/README.md @@ -17,6 +17,8 @@ For an engine upgrade, update explicit pins/checksums, review overlay seams, com The structural bridge retains engine hierarchy, inherited-port correspondence and exact connection endpoints. It adds explicit declaration directions and reference modifiers by recorded origin, without inferring effective directions from types or conjugation. The assembly sources under `examples/assembly/` are copied into the Go bridge test package and included in preparation's input digest. [Structural contract trace and scope](../docs/architecture/structural-views.md). +Native requirement graphs retain the engine's selected subjects and related context. The local overlay adds declared short names, complete owned documentation bodies, satisfaction assertion/negation flags and direct-versus-type-context ownership by looking up recorded AST origins. It does not split rendered detail text, infer satisfaction from subjects, or change the native graph's nodes, edges or order. Requirement transport validation checks exact correspondence, endpoint membership, exposure classification, polarity and direct ownership before publication. A declared relationship is not an execution result. + `insertMember` supports typed part and connection creation. Creation may insert within the recorded owner or promote only its terminal semicolon to a body; existing surrounding source is retained. Toolkit accepts some directionally incompatible connections, so the workspace separately checks explicit out/inout-to-in/inout direction, identical resolved type and exact evaluated usage/port endpoints. Conjugated or ambiguous endpoint paths remain unavailable for graphical creation. [Authoring scope and evidence](../docs/architecture/graphical-authoring.md). Toolkit also supplies read-only completion, hover and definition queries through a separate disposable language worker. Queries use the complete draft source set, negotiate UTF-16 positions and normalize only supported results. The workbench rejects stale replies; completion edits remain unapplied draft changes until the normal source transaction validates them. Engine pins, prepared assets and provenance are unchanged. See [source assistance](../docs/architecture/source-assistance-recovery.md). diff --git a/engine-bridge/opensysml/evaluate_view.go b/engine-bridge/opensysml/evaluate_view.go index 3092319..6e27bfd 100644 --- a/engine-bridge/opensysml/evaluate_view.go +++ b/engine-bridge/opensysml/evaluate_view.go @@ -145,6 +145,12 @@ func (e *Engine) evaluateView(req *JRenderViewRequest) ([]byte, error) { return nil, err } } + if data.Kind == view.KindRequirement { + out.Artifact, err = cached.requirementNotation(out.Artifact, data, out.Selected, declarations) + if err != nil { + return nil, err + } + } out.RenderStatus = "available" contexts := map[string][]string{} for _, node := range data.Nodes { diff --git a/engine-bridge/opensysml/requirements_view.go b/engine-bridge/opensysml/requirements_view.go new file mode 100644 index 0000000..ea5a40a --- /dev/null +++ b/engine-bridge/opensysml/requirements_view.go @@ -0,0 +1,108 @@ +package engine + +import ( + "encoding/json" + + "github.com/Open-MBEE/OpenSysML/internal/ir/view" + "github.com/Open-MBEE/OpenSysML/internal/semantic/symbols" + "github.com/Open-MBEE/OpenSysML/internal/syntax/ast" + "github.com/Open-MBEE/OpenSysML/internal/syntax/source" +) + +// requirementNotation enriches the native graph by its recorded origins. It +// does not change the native exposed set, related context, topology or order. +func (m *cachedModel) requirementNotation(raw json.RawMessage, data view.Data, selected []ViewSubject, declarations map[string]*symbols.Symbol) (json.RawMessage, error) { + var base JRenderViewResponse + if err := json.Unmarshal(raw, &base); err != nil { + return nil, err + } + type node struct { + JRenderNode + ShortName string `json:"shortName,omitempty"` + Documentation []string `json:"documentation,omitempty"` + Exposure string `json:"exposure"` + } + type edge struct { + JRenderEdge + Negated *bool `json:"negated,omitempty"` + Asserted *bool `json:"asserted,omitempty"` + Ownership string `json:"ownership,omitempty"` + } + out := struct { + *JRenderViewResponse + Nodes []node `json:"nodes"` + Edges []edge `json:"edges"` + }{JRenderViewResponse: &base, Nodes: []node{}, Edges: []edge{}} + exposed := map[string]bool{} + for _, item := range selected { + exposed[item.ID] = true + } + // Anonymous satisfy/assert declarations are scope members but are not + // registered by a qualified name. Resolve their recorded origins as well. + seen := map[*symbols.Symbol]bool{} + var visit func(*symbols.Symbol) + visit = func(sym *symbols.Symbol) { + if sym == nil || seen[sym] || m.Index.IsLibraryDocument(sym.DocName) { + return + } + seen[sym] = true + key := originKey(sym.Origin()) + if key != "" && declarations[key] == nil { + declarations[key] = sym + } + if sym.Scope != nil { + for _, member := range sym.Scope.AllMembers() { + visit(member) + } + } + } + m.Index.Registered(func(_ string, syms []*symbols.Symbol) { + for _, sym := range syms { + visit(sym) + } + }) + text := m.sourceText() + origins := map[string]string{} + for i, native := range data.Nodes { + origin := originKey(native.Origin) + origins[native.ID] = origin + item := node{JRenderNode: base.Nodes[i], Exposure: "contextual"} + if exposed[origin] { + item.Exposure = "selected" + } + if sym := declarations[origin]; sym != nil { + item.ShortName = sym.ShortName + if sym.Scope != nil && text != nil { + sym.Scope.ForEachMember(func(member *symbols.Symbol) bool { + if doc, ok := member.Decl.(*ast.Documentation); ok { + if body := source.CommentProse(text(member.DocName, doc.BodySpan)); body != "" { + item.Documentation = append(item.Documentation, body) + } + } + return true + }) + } + } + out.Nodes = append(out.Nodes, item) + } + for i, native := range data.Edges { + item := edge{JRenderEdge: base.Edges[i]} + sym := declarations[originKey(native.Origin)] + if sym != nil { + if usage, ok := sym.Decl.(*ast.Usage); ok { + switch item.Kind { + case "satisfy": + negated, asserted := usage.IsNegated, usage.PrefixKeyword == "assert" + item.Negated, item.Asserted = &negated, &asserted + case "composition", "reference": + item.Ownership = "via-type" + if origins[item.To] == originKey(native.Origin) { + item.Ownership = "direct" + } + } + } + } + out.Edges = append(out.Edges, item) + } + return json.Marshal(out) +} diff --git a/engine-bridge/opensysml/requirements_view_test.go b/engine-bridge/opensysml/requirements_view_test.go new file mode 100644 index 0000000..279b512 --- /dev/null +++ b/engine-bridge/opensysml/requirements_view_test.go @@ -0,0 +1,96 @@ +package combined + +import ( + "context" + "encoding/json" + "strings" + "testing" +) + +func TestStudioRequirementsNotation(t *testing.T) { + server, err := New("v0.10.0") + if err != nil { + t.Fatal(err) + } + call := func(method string, params any) map[string]any { + t.Helper() + body, _ := json.Marshal(params) + raw, err := server.Call(context.Background(), method, body) + if err != nil { + t.Fatal(err) + } + var out map[string]any + if err = json.Unmarshal(raw, &out); err != nil { + t.Fatal(err) + } + return out + } + prose := "The capacity requirement shall retain its full café documentation even when it exceeds sixty characters." + model := `package P { + part def Device; part a : Device; + requirement def <'REQ-BASE'> Base { doc /* ` + prose + ` */ } + requirement spec { requirement <'REQ-ONE'> one : Base; requirement two : Base; } + satisfy spec.one by a; + assert not satisfy spec.two by a; +} +package V { private import StandardViewDefinitions::*; + view complete : GeneralView { filter @SysML::RequirementDefinition or @SysML::RequirementUsage; expose P::**; } + view compact : GeneralView { filter @SysML::RequirementDefinition or @SysML::RequirementUsage; expose P::Base; expose P::spec; } +}` + parsed := call("ParseSources", map[string]any{"documents": []any{map[string]any{"name": "requirements.sysml", "content": model}}, "strictConformance": true}) + for _, view := range []string{"V::complete", "V::compact"} { + result := call("EvaluateView", map[string]any{"modelHash": parsed["modelHash"], "view": view}) + if result["status"] != "complete" || result["renderStatus"] != "available" { + t.Fatalf("%v", result) + } + artifact := result["artifact"].(map[string]any) + if artifact["kind"] != "requirement" { + t.Fatalf("%v", artifact) + } + names := map[string]string{} + for _, raw := range artifact["nodes"].([]any) { + node := raw.(map[string]any) + names[node["id"].(string)] = node["name"].(string) + if node["name"] == "P::Base" { + if node["shortName"] != "REQ-BASE" || node["documentation"].([]any)[0] != prose { + t.Fatalf("Full own AST metadata missing: %v", node) + } + if !strings.Contains(node["detail"].(string), "...") { + t.Fatalf("Expected native truncated detail separately: %v", node) + } + } + if node["name"] == "P::a" && node["exposure"] != "contextual" { + t.Fatalf("Satisfier falsely selected: %v", node) + } + if strings.HasPrefix(node["name"].(string), "P::") && node["name"] != "P::a" && node["exposure"] != "selected" { + t.Fatalf("Requirement selection lost: %v", node) + } + } + direct, via, positive, negative := false, false, false, false + for _, raw := range artifact["edges"].([]any) { + edge := raw.(map[string]any) + if edge["kind"] == "composition" { + if edge["ownership"] == "direct" { + direct = true + } + if edge["ownership"] == "via-type" { + via = true + } + } + if edge["kind"] == "satisfy" { + switch names[edge["to"].(string)] { + case "P::spec::one": + positive = edge["negated"] == false && edge["asserted"] == false + case "P::spec::two": + negative = edge["negated"] == true && edge["asserted"] == true + } + } + } + if view == "V::complete" && (!direct || via || !positive || !negative) { + t.Fatalf("Lost hierarchy/polarity: %v", artifact) + } + if view == "V::compact" && !via { + t.Fatalf("Hidden member to type was not distinguished: %v", artifact) + } + } +} diff --git a/examples/README.md b/examples/README.md index 81c7ca0..129f793 100644 --- a/examples/README.md +++ b/examples/README.md @@ -6,21 +6,22 @@ The “blocks” examples use SysML v2 `part def` and `part` declarations. Lexic Status is per view: **rendered** covers the stated fixture scope; **partial** shows a useful subset with recorded gaps; **unavailable** means the engine evaluates the view but the app reports that its renderer is unavailable. No examples are silently skipped for missing renderers. Targets describe intended visual quality and need visual review; passing source/engine checks alone does not satisfy every target. -| Project | Topics | Named views and current app status | -| --------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| [Comments and documentation](comments/model.sysml) | comments, documentation, annotations, unicode, long-labels | `overview`: partial; `maintenance`: partial | -| [Packages and imports](packages/model.sysml) | packages, imports, ownership, multiple-files | `outline`: partial; `dependencies`: unavailable | -| [Parts and definitions (blocks)](parts/model.sysml) | parts, blocks, definitions, usages, specialization, references, repeated-names | `structure`: rendered; `definitions`: partial | -| [Ports and conjugation](ports/model.sysml) | ports, interfaces, connections, conjugation | `link`: partial | -| [Use cases and actors](use-cases/model.sysml) | use-cases, actors, subjects, include, objectives | `scenarios`: unavailable; `definitions`: unavailable | -| [Requirements and satisfaction](requirements/model.sysml) | requirements, documentation, constraints, satisfaction | `requirements`: unavailable | -| [Actions and successions](actions/model.sysml) | actions, behavior, succession, parameters | `process`: unavailable | -| [States and transitions](states/model.sysml) | states, nested-states, transitions, initial-state | `states`: unavailable | -| [Messages and sequence ordering](sequences/model.sysml) | sequences, messages, occurrences, ordering | `exchange`: unavailable | -| [Filtered and empty views](filters/model.sysml) | filters, metadata, recursive-exposure, empty-view | `reviewed`: rendered; `empty`: rendered | -| [Workshop foundation](workshop/model.sysml) | multiple-files, shared-types, rename, tree | `overview`: rendered; `compressor`: rendered | -| [Nested assembly](assembly/model.sysml) | assembly, nested-parts, directed-ports, connections, references, usage-context | `structure`: rendered; `devices`: partial | -| [Smart air workshop](air-compressor/system.sysml) | multiple-files, specialization, directed-ports, conjugation, connections, attributes, constraints, requirements, actions, states, use-cases, sequences, geometry | `overview`: partial; `structure`: partial; `installation`: partial; `pneumatics`: partial; `definitions`: partial; `requirements`: partial; `actions`: unavailable; `maintenance`: unavailable; `useCases`: unavailable; `states`: unavailable; `startupSequence`: unavailable; `demandSequence`: unavailable; `faultSequence`: unavailable; `envelopeGeometry`: unavailable | +| Project | Topics | Named views and current app status | +| -------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| [Comments and documentation](comments/model.sysml) | comments, documentation, annotations, unicode, long-labels | `overview`: partial; `maintenance`: partial | +| [Packages and imports](packages/model.sysml) | packages, imports, ownership, multiple-files | `outline`: partial; `dependencies`: unavailable | +| [Parts and definitions (blocks)](parts/model.sysml) | parts, blocks, definitions, usages, specialization, references, repeated-names | `structure`: rendered; `definitions`: partial | +| [Ports and conjugation](ports/model.sysml) | ports, interfaces, connections, conjugation | `link`: partial | +| [Use cases and actors](use-cases/model.sysml) | use-cases, actors, subjects, include, objectives | `scenarios`: unavailable; `definitions`: unavailable | +| [Requirements and satisfaction](requirements/model.sysml) | requirements, documentation, constraints, satisfaction | `requirements`: partial | +| [Requirement hierarchy and satisfaction](requirements-traceability/system.sysml) | requirements, hierarchy, typing, satisfaction, negation, aliases, multiple-files, repeated-names | `requirements`: partial; `overview`: partial | +| [Actions and successions](actions/model.sysml) | actions, behavior, succession, parameters | `process`: unavailable | +| [States and transitions](states/model.sysml) | states, nested-states, transitions, initial-state | `states`: unavailable | +| [Messages and sequence ordering](sequences/model.sysml) | sequences, messages, occurrences, ordering | `exchange`: unavailable | +| [Filtered and empty views](filters/model.sysml) | filters, metadata, recursive-exposure, empty-view | `reviewed`: rendered; `empty`: rendered | +| [Workshop foundation](workshop/model.sysml) | multiple-files, shared-types, rename, tree | `overview`: rendered; `compressor`: rendered | +| [Nested assembly](assembly/model.sysml) | assembly, nested-parts, directed-ports, connections, references, usage-context | `structure`: rendered; `devices`: partial | +| [Smart air workshop](air-compressor/system.sysml) | multiple-files, specialization, directed-ports, conjugation, connections, attributes, constraints, requirements, actions, states, use-cases, sequences, geometry | `overview`: partial; `structure`: partial; `installation`: partial; `pneumatics`: partial; `definitions`: partial; `requirements`: partial; `actions`: unavailable; `maintenance`: unavailable; `useCases`: unavailable; `states`: unavailable; `startupSequence`: unavailable; `demandSequence`: unavailable; `faultSequence`: unavailable; `envelopeGeometry`: unavailable | ## Visual targets and known gaps @@ -114,13 +115,33 @@ Current gaps: The engine emits a case artifact; the application has no case rend Files: [model.sysml](requirements/model.sysml), [views.sysml](requirements/views.sysml). -**CapacityViews::requirements** — engine artifact: `requirement`; app: **unavailable**. +**CapacityViews::requirements** — engine artifact: `requirement`; app: **partial**. - Display requirement identifiers and readable requirement text. - Distinguish requirement typing from the storage satisfaction relationship. - Treat the declared constraint as model content, without claiming an execution result. -Current gaps: The engine emits a requirement graph; the application has no requirement renderer. +Current gaps: Requirement identifiers, documentation and relationships are shown; formal constraint and subject-parameter compartments are not drawn. + +### Requirement hierarchy and satisfaction + +Files: [system.sysml](requirements-traceability/system.sysml), [requirements.sysml](requirements-traceability/requirements.sysml), [views.sysml](requirements-traceability/views.sysml). + +**TraceViews::requirements** — engine artifact: `requirement`; app: **partial**. + +- Distinguish requirement identifiers, documentation, usage typing and direct ownership. +- Keep the two capacity usages and same-name tank satisfiers separately traceable. +- Render asserted negative satisfaction distinctly; a subject binding alone is not satisfaction. +- Label satisfying parts as engine-supplied related context without claiming executed verification. + +Current gaps: Requirement identifiers, documentation and relationships are shown; formal constraint and subject-parameter compartments are not drawn. + +**TraceViews::overview** — engine artifact: `tree`; app: **partial**. + +- Retain the same source model as an independent generic overview. +- Keep requirement-view and overview positions separate through save and reopen. + +Current gaps: This GeneralView produces a generic flat tree rather than dedicated requirement notation. ### Actions and successions diff --git a/examples/catalog.json b/examples/catalog.json index 1365189..415868d 100644 --- a/examples/catalog.json +++ b/examples/catalog.json @@ -264,7 +264,7 @@ { "id": "CapacityViews::requirements", "engineKind": "requirement", - "app": "unavailable", + "app": "partial", "nodeNames": [ "CapacityRequirements::MinimumCapacity", "CapacityRequirements::usableCapacity", @@ -277,7 +277,124 @@ "Treat the declared constraint as model content, without claiming an execution result." ], "gaps": [ - "The engine emits a requirement graph; the application has no requirement renderer." + "Requirement identifiers, documentation and relationships are shown; formal constraint and subject-parameter compartments are not drawn." + ], + "notices": [] + } + ] + }, + { + "id": "requirements-traceability", + "title": "Requirement hierarchy and satisfaction", + "topics": [ + "requirements", + "hierarchy", + "typing", + "satisfaction", + "negation", + "aliases", + "multiple-files", + "repeated-names" + ], + "files": ["system.sysml", "requirements.sysml", "views.sysml"], + "views": [ + { + "id": "TraceViews::requirements", + "engineKind": "requirement", + "app": "partial", + "nodeNames": [ + "TraceRequirements::MinimumCapacity", + "TraceRequirements::leftSpecification", + "TraceRequirements::rightSpecification", + "TraceRequirements::unlinkedCapacity", + "TraceRequirements::excludedCapacity", + "TraceRequirements::leftSpecification::leftCapacity", + "TraceRequirements::rightSpecification::rightCapacity", + "TraceSystem::spare", + "TraceSystem::LeftPlant::tank", + "TraceSystem::RightPlant::tank" + ], + "edgeKinds": [ + "typing", + "typing", + "typing", + "composition", + "typing", + "composition", + "satisfy", + "satisfy", + "satisfy" + ], + "targets": [ + "Distinguish requirement identifiers, documentation, usage typing and direct ownership.", + "Keep the two capacity usages and same-name tank satisfiers separately traceable.", + "Render asserted negative satisfaction distinctly; a subject binding alone is not satisfaction.", + "Label satisfying parts as engine-supplied related context without claiming executed verification." + ], + "gaps": [ + "Requirement identifiers, documentation and relationships are shown; formal constraint and subject-parameter compartments are not drawn." + ], + "notices": [] + }, + { + "id": "TraceViews::overview", + "engineKind": "tree", + "app": "partial", + "nodeNames": [ + "TraceRequirements", + "MinimumCapacity", + "tank", + "", + "leftSpecification", + "installation", + "leftCapacity", + "tank", + "rightSpecification", + "installation", + "rightCapacity", + "tank", + "unlinkedCapacity", + "tank", + "excludedCapacity", + "tank", + "", + "", + "", + "TraceSystem", + "Tank", + "capacity", + "LeftPlant", + "tank", + "capacity", + "RightPlant", + "tank", + "capacity", + "spare", + "capacity" + ], + "edgeKinds": [ + "typing", + "typing", + "typing", + "typing", + "typing", + "typing", + "typing", + "specialization", + "specialization", + "specialization", + "composition", + "specialization", + "composition", + "specialization", + "specialization" + ], + "targets": [ + "Retain the same source model as an independent generic overview.", + "Keep requirement-view and overview positions separate through save and reopen." + ], + "gaps": [ + "This GeneralView produces a generic flat tree rather than dedicated requirement notation." ], "notices": [] } diff --git a/examples/requirements-traceability/requirements.sysml b/examples/requirements-traceability/requirements.sysml new file mode 100644 index 0000000..28600cb --- /dev/null +++ b/examples/requirements-traceability/requirements.sysml @@ -0,0 +1,34 @@ +package TraceRequirements { + private import TraceSystem::*; + requirement def <'REQ-CAP'> MinimumCapacity { + doc /* The storage tank shall provide at least ten units of usable + capacity. This declared requirement and its satisfaction links + are model content, not evidence that a test has passed. */ + subject tank : Tank; + require constraint { tank.capacity >= 10.0 } + } + alias CapacityRule for MinimumCapacity; + requirement <'REQ-LEFT'> leftSpecification { + doc /* Capacity requirements for the left installation. */ + subject installation : Tank; + requirement <'REQ-LEFT-CAP'> leftCapacity : CapacityRule { + subject tank = TraceSystem::LeftPlant::tank; + } + } + requirement <'REQ-RIGHT'> rightSpecification { + doc /* Capacity requirements for the right installation. */ + subject installation : Tank; + requirement <'REQ-RIGHT-CAP'> rightCapacity : CapacityRule { + subject tank = TraceSystem::RightPlant::tank; + } + } + requirement <'REQ-UNLINKED'> unlinkedCapacity : MinimumCapacity { + subject tank = TraceSystem::spare; + } + requirement <'REQ-EXCLUDED'> excludedCapacity : MinimumCapacity { + subject tank = TraceSystem::spare; + } + assert not satisfy excludedCapacity by TraceSystem::spare; + satisfy leftSpecification.leftCapacity by TraceSystem::LeftPlant::tank; + satisfy rightSpecification.rightCapacity by TraceSystem::RightPlant::tank; +} diff --git a/examples/requirements-traceability/system.sysml b/examples/requirements-traceability/system.sysml new file mode 100644 index 0000000..7eeaa56 --- /dev/null +++ b/examples/requirements-traceability/system.sysml @@ -0,0 +1,18 @@ +package TraceSystem { + part def Tank { + attribute capacity : ScalarValues::Real; + } + package LeftPlant { + part tank : Tank { + attribute :>> capacity = 20.0; + } + } + package RightPlant { + part tank : Tank { + attribute :>> capacity = 30.0; + } + } + part spare : Tank { + attribute :>> capacity = 40.0; + } +} diff --git a/examples/requirements-traceability/views.sysml b/examples/requirements-traceability/views.sysml new file mode 100644 index 0000000..92909c2 --- /dev/null +++ b/examples/requirements-traceability/views.sysml @@ -0,0 +1,17 @@ +package TraceViews { + private import StandardViewDefinitions::*; + view requirements : GeneralView { + filter @SysML::RequirementDefinition or @SysML::RequirementUsage; + expose TraceRequirements::MinimumCapacity; + expose TraceRequirements::leftSpecification; + expose TraceRequirements::rightSpecification; + expose TraceRequirements::unlinkedCapacity; + expose TraceRequirements::excludedCapacity; + expose TraceRequirements::leftSpecification::leftCapacity; + expose TraceRequirements::rightSpecification::rightCapacity; + } + view overview : GeneralView { + expose TraceRequirements; + expose TraceSystem; + } +} diff --git a/scripts/prepare-opensysml.mjs b/scripts/prepare-opensysml.mjs index 804e760..b4f1ff6 100644 --- a/scripts/prepare-opensysml.mjs +++ b/scripts/prepare-opensysml.mjs @@ -15,6 +15,8 @@ const overlays = [ 'validated_view_test.go', 'geometry_pose.go', 'geometry_view.go', + 'requirements_view.go', + 'requirements_view_test.go', 'relationships.go', 'relationships_test.go', ]; @@ -95,6 +97,17 @@ for (const [name, folder] of [ name.endsWith('_test.go') ? 'studio_view_test.go' : 'studio_view.go', ), ); +await cp( + resolve(root, 'engine-bridge/opensysml/requirements_view.go'), + resolve(source, 'internal/frontend/engine/studio_requirements_view.go'), +); +await cp( + resolve(root, 'engine-bridge/opensysml/requirements_view_test.go'), + resolve( + source, + 'internal/frontend/combined/studio_requirements_view_test.go', + ), +); await cp( resolve(root, 'engine-bridge/opensysml/relationships.go'), resolve(source, 'internal/frontend/engine/studio_relationships.go'), diff --git a/src/lib/ui/requirements.css b/src/lib/ui/requirements.css new file mode 100644 index 0000000..b79a399 --- /dev/null +++ b/src/lib/ui/requirements.css @@ -0,0 +1,157 @@ +/* Native requirements graphs; colors express presentation, never verdicts. */ +.requirement-node { + --requirement-heading-bg: var(--sysml-usage-bg); + display: flex; + flex-direction: column; + padding: 0; + text-align: left; +} +.requirement-node.requirement-definition { + --requirement-heading-bg: var(--sysml-definition-bg); +} +.requirement-node.requirement-element, +.requirement-node.requirement-contextual { + --requirement-heading-bg: var(--sysml-surface); +} +.requirement-node.requirement-contextual { + border-style: dashed; +} +.requirement-heading { + max-height: 112px; + flex-shrink: 0; + overflow: hidden; + padding: 8px 12px; + background: var(--requirement-heading-bg); + border-bottom: 1px solid var(--sysml-border); + border-radius: var(--sysml-control-radius) var(--sysml-control-radius) 0 0; +} +.requirement-meta { + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; + color: var(--sysml-muted); + font-size: 11px; + line-height: 14px; +} +.requirement-meta .model-node-kind { + display: inline-flex; + margin: 0; + padding: 1px 5px; + border-radius: 5px; + background: color-mix(in srgb, var(--sysml-stroke) 8%, transparent); +} +.requirement-identifier { + display: block; + margin-top: 4px; + font-family: var(--sysml-font-mono); + font-size: 12px; + line-height: 16px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} +.requirement-name { + display: -webkit-box; + -webkit-box-orient: vertical; + -webkit-line-clamp: 2; + overflow: hidden; + overflow-wrap: anywhere; + margin-top: 4px; + font-size: 14px; + font-weight: 650; + line-height: 18px; +} +.requirement-type { + display: block; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + margin-top: 2px; + color: var(--sysml-muted); + font-size: 12px; + line-height: 16px; +} +.requirement-body { + flex: 1; + min-height: 0; + padding: 6px 12px; + overflow: hidden; +} +.requirement-detail { + display: -webkit-box; + -webkit-box-orient: vertical; + -webkit-line-clamp: 2; + overflow: hidden; + overflow-wrap: anywhere; + margin: 0; + white-space: pre-line; + font-size: 13px; + line-height: 18px; +} +.requirement-no-detail { + color: var(--sysml-muted); +} +.requirement-inspect { + flex-shrink: 0; + align-self: flex-start; + margin: auto 12px 6px; + height: 24px; + padding: 0; + color: var(--sysml-muted); + font-size: 12px; + text-decoration: underline; + text-underline-offset: 3px; +} +.requirement-inspect:hover { + color: var(--sysml-text); +} +.requirement-inspect:focus-visible { + outline: 2px solid var(--sysml-accent); + outline-offset: 3px; + border-radius: 2px; +} +.react-flow .react-flow__node.requirement-node.selected, +.react-flow .react-flow__node.requirement-node:focus-visible, +.react-flow .react-flow__node.requirement-node:focus-within { + outline: 2px solid var(--sysml-accent); + outline-offset: 2px; + box-shadow: 0 0 0 6px color-mix(in srgb, var(--sysml-accent) 12%, transparent); +} +.requirement-edge .react-flow__edge-path { + stroke: var(--sysml-stroke); + stroke-width: 1.5; +} +.requirement-satisfaction .react-flow__edge-path { + stroke-dasharray: 6 4; +} +.requirement-negated .react-flow__edge-path { + stroke-dasharray: 2 4; +} +.requirement-edge .react-flow__edge-text { + fill: var(--sysml-text); + font-size: 12px; +} +.requirement-edge .react-flow__edge-textbg { + fill: var(--sysml-surface); + fill-opacity: 1; +} +.react-flow .react-flow__edge.requirement-edge.selected .react-flow__edge-path, +.react-flow + .react-flow__edge.requirement-edge:focus-visible + .react-flow__edge-path { + stroke: var(--sysml-accent); + stroke-width: 2.5; +} +@media (forced-colors: active) { + .requirement-node { + border-color: CanvasText; + } + .react-flow .react-flow__node.requirement-node.selected, + .react-flow .react-flow__node.requirement-node:focus-within { + outline-color: Highlight; + } + .requirement-edge .react-flow__edge-path { + stroke: CanvasText; + } +} diff --git a/src/lib/ui/theme.css b/src/lib/ui/theme.css index 621a66d..6ef3c54 100644 --- a/src/lib/ui/theme.css +++ b/src/lib/ui/theme.css @@ -8,3 +8,4 @@ @import './tools.css'; @import './dialogs.css'; @import './entry.css'; +@import './requirements.css'; diff --git a/src/lib/workbench/diagrams/Diagram.tsx b/src/lib/workbench/diagrams/Diagram.tsx index 1cde987..30be3e9 100644 --- a/src/lib/workbench/diagrams/Diagram.tsx +++ b/src/lib/workbench/diagrams/Diagram.tsx @@ -6,6 +6,7 @@ import { ReactFlowProvider, Background, applyNodeChanges, + applyEdgeChanges, useReactFlow, useNodesInitialized, useStore, @@ -21,7 +22,9 @@ import { } from '$lib/ui/primitives/native-select'; import { layoutStructure, absolutePositions } from './structure'; import { StructureNode, SelectDiagramSymbol } from './StructureNode'; -const nodeTypes = { structure: StructureNode }; +import { RequirementNode } from './RequirementNode'; +import { layoutRequirements } from './requirements'; +const nodeTypes = { structure: StructureNode, requirement: RequirementNode }; function FitAvailableSpace({ padding, layoutMode, @@ -106,7 +109,14 @@ export function Diagram({ view?.status === 'complete' && view.renderStatus === 'available' && view.artifact && - ['tree', 'interconnection', 'definition'].includes(view.artifact.kind); + ['tree', 'interconnection', 'definition', 'requirement'].includes( + view.artifact.kind, + ); + const fitPadding = ['interconnection', 'requirement'].includes( + view?.artifact?.kind ?? '', + ) + ? 0.1 + : 0.2; const [initialPresentation] = useState(presentation); useEffect(() => { const artifact = view?.artifact; @@ -135,8 +145,13 @@ export function Diagram({ initialPresentation?.modelHash === view?.modelHash ? initialPresentation.positions : {}; - if (artifact.kind === 'interconnection') { - void layoutStructure(artifact, stored) + if ( + artifact.kind === 'interconnection' || + artifact.kind === 'requirement' + ) { + const layout = + artifact.kind === 'requirement' ? layoutRequirements : layoutStructure; + void layout(artifact, stored) .then((result) => { if (cancelled) return; setNodes(result.nodes); @@ -268,9 +283,11 @@ export function Diagram({ {view.artifact.kind === 'interconnection' ? 'Structure' - : view.artifact.kind === 'tree' - ? 'Overview tree' - : 'Named view'} + : view.artifact.kind === 'requirement' + ? 'Requirements' + : view.artifact.kind === 'tree' + ? 'Overview tree' + : 'Named view'} )} @@ -282,24 +299,26 @@ export function Diagram({ -
- - -
+ {view?.artifact?.kind !== 'requirement' && ( +
+ + +
+ )} {!view ? (
@@ -335,39 +354,70 @@ export function Diagram({ nodes={nodes} edges={edges} nodeTypes={nodeTypes} - onNodesChange={(changes) => - setNodes((current) => applyNodeChanges(changes, current)) - } + onNodesChange={(changes) => { + setNodes((current) => applyNodeChanges(changes, current)); + if (view?.artifact?.kind === 'requirement') { + for (const change of changes) + if (change.type === 'select' && change.selected) + onSelect(change.id); + if ( + !disabled && + !locked && + changes.some( + (change) => + change.type === 'position' && + change.position && + change.dragging === false, + ) + ) + onMove( + absolutePositions(applyNodeChanges(changes, nodes)), + ); + } + }} + onEdgesChange={(changes) => { + setEdges((current) => applyEdgeChanges(changes, current)); + if (view?.artifact?.kind === 'requirement') { + for (const change of changes) + if (change.type === 'select' && change.selected) + onSelect(change.id); + } + }} + edgesFocusable={!locked} + onEdgeClick={(_event, edge) => { + if (view?.artifact?.kind === 'requirement') onSelect(edge.id); + }} fitView - fitViewOptions={{ padding: 0.2, maxZoom: 1 }} + fitViewOptions={{ + padding: view?.artifact?.kind === 'requirement' ? 0.1 : 0.2, + maxZoom: 1, + }} + minZoom={view?.artifact?.kind === 'requirement' ? 0.1 : 0.5} nodesConnectable={false} nodesDraggable={!disabled && !locked} elementsSelectable={!locked} deleteKeyCode={null} onNodeClick={(_event, node) => onSelect(node.id)} - onNodeDragStop={(_event, node) => - onMove( - absolutePositions( - nodes.map((current) => - current.id === node.id ? node : current, + onNodeDragStop={(_event, node) => { + if (view?.artifact?.kind !== 'requirement') + onMove( + absolutePositions( + nodes.map((current) => + current.id === node.id ? node : current, + ), ), - ), - ) - } + ); + }} > diff --git a/src/lib/workbench/diagrams/RequirementNode.tsx b/src/lib/workbench/diagrams/RequirementNode.tsx new file mode 100644 index 0000000..b304cf2 --- /dev/null +++ b/src/lib/workbench/diagrams/RequirementNode.tsx @@ -0,0 +1,76 @@ +import { useContext } from 'react'; +import { Handle, Position, type NodeProps } from '@xyflow/react'; +import { Button } from '$lib/ui/primitives/button'; +import { SelectDiagramSymbol } from './StructureNode'; +import { + isRequirement, + type RequirementNode as RequirementNodeType, +} from './requirements'; + +export function RequirementNode({ data }: NodeProps) { + const select = useContext(SelectDiagramSymbol); + const { item } = data; + const documentation = item.documentation?.join('\n\n'); + const detail = documentation || item.detail; + return ( + <> + +
+
+ {item.kind} + {item.exposure === 'contextual' && Related context} +
+ {item.shortName && ( + + {item.shortName} + + )} + + {item.name} + + {item.type && ( + + : {item.type} + + )} +
+ {isRequirement(item) && ( +
+ {detail ? ( +

+ {detail} +

+ ) : ( +

+ No additional details available. +

+ )} +
+ )} + + + + ); +} diff --git a/src/lib/workbench/diagrams/requirements.test.ts b/src/lib/workbench/diagrams/requirements.test.ts new file mode 100644 index 0000000..3b131d6 --- /dev/null +++ b/src/lib/workbench/diagrams/requirements.test.ts @@ -0,0 +1,169 @@ +import { expect, it } from 'vitest'; +import type { ViewArtifact } from '$lib/workspace/views'; +import { absolutePositions } from './structure'; +import { + layoutRequirements, + requirementEdgeLabel, + requirementHeight, + relatedElementHeight, + requirementWidth, +} from './requirements'; + +// Controlled renderer fixtures establish projection behavior, not engine conformance. +const artifact: ViewArtifact = { + kind: 'requirement', + nodes: [ + { + id: 'definition', + name: 'Capacity::Minimum', + kind: 'requirement def', + shortName: 'REQ-1', + documentation: ['The tank shall store at least ten units.'], + exposure: 'selected', + }, + { + id: 'left', + name: 'Capacity::primary::capacity', + kind: 'requirement', + type: 'Capacity::Minimum', + exposure: 'selected', + }, + { + id: 'right', + name: 'Capacity::standby::capacity', + kind: 'requirement', + type: 'Capacity::Minimum', + exposure: 'selected', + }, + { id: 'tank', name: 'System::tank', kind: 'part', exposure: 'contextual' }, + ], + edges: [ + { from: 'left', to: 'definition', kind: 'typing' }, + { from: 'right', to: 'definition', kind: 'typing' }, + { from: 'tank', to: 'left', kind: 'satisfy' }, + { + from: 'tank', + to: 'right', + kind: 'satisfy', + negated: true, + asserted: true, + }, + ], +}; + +it('retains exact native symbol and relationship-index identities with distinct endpoint descriptions', async () => { + const result = await layoutRequirements(artifact, {}); + expect(result.nodes.map((node) => node.id)).toEqual([ + 'definition', + 'left', + 'right', + 'tank', + ]); + expect(result.edges[2]).toMatchObject({ + id: 'edge:2', + source: 'tank', + target: 'left', + label: 'satisfy', + sourceHandle: 'requirement-source', + targetHandle: 'requirement-target', + }); + expect(result.edges[3]).toMatchObject({ + id: 'edge:3', + source: 'tank', + target: 'right', + label: 'assert not satisfy', + }); + expect(result.edges[2]!.ariaLabel).toContain('Capacity::primary::capacity'); + expect(result.edges[3]!.ariaLabel).toContain('Capacity::standby::capacity'); + expect(result.nodes[0]!.ariaLabel).toContain('REQ-1'); + expect(result.nodes[3]!.ariaLabel).toContain('related context'); + expect(result.nodes[3]!.style?.height).toBe(relatedElementHeight); + expect(result.nodes[0]!.style?.height).toBe(requirementHeight); +}); + +it('keeps display-name duplicates separate and restores absolute saved positions without mutating the artifact', async () => { + const duplicated = { + ...artifact, + nodes: artifact.nodes.map((node) => + node.id === 'left' || node.id === 'right' + ? { ...node, name: 'capacity' } + : node, + ), + }; + const saved = { + left: { x: -143.75, y: 21.125 }, + right: { x: 900.5, y: 1200.25 }, + }; + const before = JSON.stringify(duplicated); + const result = await layoutRequirements(duplicated, saved); + expect( + result.nodes.filter((node) => node.data.item.name === 'capacity'), + ).toHaveLength(2); + expect(absolutePositions(result.nodes)).toMatchObject(saved); + expect(result.nodes.every((node) => node.parentId === undefined)).toBe(true); + expect(result.nodes[0]!.style).toMatchObject({ + width: requirementWidth, + height: requirementHeight, + }); + expect(JSON.stringify(duplicated)).toBe(before); + expect(result.notice).toBe(''); +}); + +it('distinguishes native direct ownership, via-type features, typing and negated satisfaction', () => { + const edge = { from: 'a', to: 'b', kind: 'composition' }; + expect(requirementEdgeLabel({ ...edge, ownership: 'direct' })).toBe( + 'contains', + ); + expect(requirementEdgeLabel({ ...edge, ownership: 'via-type' })).toBe( + 'owned feature (via type)', + ); + expect(requirementEdgeLabel(edge)).toBe('composition'); + expect(requirementEdgeLabel({ ...edge, kind: 'typing' })).toBe('typed by'); + expect( + requirementEdgeLabel({ ...edge, kind: 'satisfy', negated: true }), + ).toBe('not satisfy'); + expect( + requirementEdgeLabel({ ...edge, kind: 'satisfy', asserted: true }), + ).toBe('assert satisfy'); +}); + +it('rejects unresolved and duplicate identities rather than silently discarding relations', async () => { + await expect( + layoutRequirements( + { + ...artifact, + edges: [{ from: 'tank', to: 'missing', kind: 'satisfy' }], + }, + {}, + ), + ).rejects.toThrow('unresolved'); + await expect( + layoutRequirements( + { ...artifact, nodes: [...artifact.nodes, artifact.nodes[0]!] }, + {}, + ), + ).rejects.toThrow('duplicate'); + await expect( + layoutRequirements({ ...artifact, kind: 'tree' }, {}), + ).rejects.toThrow('native requirement'); +}); + +it('keeps bounded sizes independent of metadata length and exposure membership', async () => { + const verbose: ViewArtifact = { + ...artifact, + nodes: artifact.nodes.map((node) => ({ + ...node, + name: node.name.repeat(10), + documentation: ['Long documentation. '.repeat(100)], + exposure: node.exposure === 'contextual' ? 'selected' : 'contextual', + })), + }; + const original = await layoutRequirements(artifact, {}); + const expanded = await layoutRequirements(verbose, {}); + expect(expanded.nodes.map((node) => node.style)).toEqual( + original.nodes.map((node) => node.style), + ); + expect(expanded.nodes[0]!.data.item.documentation![0]).toContain( + 'Long documentation. ', + ); +}); diff --git a/src/lib/workbench/diagrams/requirements.ts b/src/lib/workbench/diagrams/requirements.ts new file mode 100644 index 0000000..27cfd9b --- /dev/null +++ b/src/lib/workbench/diagrams/requirements.ts @@ -0,0 +1,115 @@ +import ELK from 'elkjs/lib/elk.bundled.js'; +import { MarkerType, type Edge, type Node } from '@xyflow/react'; +import type { ArtifactNode, Point, ViewArtifact } from '$lib/workspace/views'; + +export const requirementWidth = 340; +export const requirementHeight = 196; +export const relatedElementHeight = 124; +export const isRequirement = (item: ArtifactNode) => + item.kind === 'requirement' || item.kind === 'requirement def'; +export type RequirementNode = Node<{ item: ArtifactNode }, 'requirement'>; +type Relationship = ViewArtifact['edges'][number]; + +export function requirementNodeLabel(item: ArtifactNode) { + return `${item.kind} ${item.shortName ? item.shortName + ' ' : ''}${item.name}${item.type ? ' : ' + item.type : ''}${item.exposure === 'contextual' ? ', related context' : ''}`; +} + +/** Labels reflect only the native relationship and explicit bridge evidence. */ +export function requirementEdgeLabel(edge: Relationship) { + if (edge.kind === 'satisfy') + return `${edge.asserted ? 'assert ' : ''}${edge.negated ? 'not ' : ''}satisfy`; + if (edge.kind === 'typing') return 'typed by'; + if (edge.kind === 'composition') { + if (edge.ownership === 'via-type') return 'owned feature (via type)'; + if (edge.ownership === 'direct') return 'contains'; + } + if (edge.kind === 'reference' && edge.ownership === 'via-type') + return 'reference (via type)'; + return edge.kind; +} + +/** Flat graph positions cross the workspace boundary as absolute coordinates. */ +export async function layoutRequirements( + artifact: ViewArtifact, + stored: Readonly>, +) { + if (artifact.kind !== 'requirement') + throw new Error('Expected a native requirement graph.'); + const byId = new Map(artifact.nodes.map((item) => [item.id, item])); + if (byId.size !== artifact.nodes.length) + throw new Error('Requirement graph has duplicate symbol identities.'); + for (const edge of artifact.edges) + if (!byId.has(edge.from) || !byId.has(edge.to)) + throw new Error( + 'Requirement graph has an unresolved relationship endpoint.', + ); + const graph = await new ELK().layout({ + id: 'requirements-root', + layoutOptions: { + 'elk.algorithm': 'layered', + 'elk.direction': 'DOWN', + 'elk.spacing.nodeNode': '88', + 'elk.layered.spacing.nodeNodeBetweenLayers': '72', + }, + children: artifact.nodes.map((item) => ({ + id: item.id, + width: requirementWidth, + height: isRequirement(item) ? requirementHeight : relatedElementHeight, + })), + edges: artifact.edges.map((edge, index) => ({ + id: `edge:${index}`, + sources: [edge.from], + targets: [edge.to], + })), + }); + const nodes: RequirementNode[] = artifact.nodes.map((item) => { + const point = + stored[item.id] ?? graph.children?.find((n) => n.id === item.id); + if (!point || !Number.isFinite(point.x) || !Number.isFinite(point.y)) + throw new Error('Requirement graph has no finite node position.'); + return { + id: item.id, + type: 'requirement', + data: { item }, + position: { x: point.x!, y: point.y! }, + ariaLabel: requirementNodeLabel(item), + className: `requirement-node${!isRequirement(item) ? ' requirement-element' : ''}${item.kind === 'requirement def' ? ' requirement-definition' : ''}${item.exposure === 'contextual' ? ' requirement-contextual' : ''}`, + style: { + width: requirementWidth, + height: isRequirement(item) ? requirementHeight : relatedElementHeight, + }, + }; + }); + const edges: Edge[] = artifact.edges.map((edge, index) => { + const relationship = requirementEdgeLabel(edge); + const label = + edge.label && edge.label !== edge.kind + ? `${relationship}: ${edge.label}` + : relationship; + return { + id: `edge:${index}`, + source: edge.from, + target: edge.to, + sourceHandle: 'requirement-source', + targetHandle: 'requirement-target', + type: 'smoothstep', + label, + ariaLabel: `${label}: ${requirementNodeLabel(byId.get(edge.from)!)} to ${requirementNodeLabel(byId.get(edge.to)!)}`, + className: `requirement-edge${edge.kind === 'satisfy' ? ' requirement-satisfaction' : ''}${edge.kind === 'typing' ? ' requirement-typing' : ''}${edge.negated ? ' requirement-negated' : ''}`, + ...(edge.kind === 'typing' || edge.kind === 'satisfy' + ? { + markerEnd: { + type: + edge.kind === 'typing' + ? MarkerType.Arrow + : MarkerType.ArrowClosed, + color: 'var(--sysml-stroke)', + }, + } + : {}), + interactionWidth: 24, + deletable: false, + }; + }); + return { nodes, edges, notice: '' }; +} diff --git a/src/lib/workbench/properties/Properties.tsx b/src/lib/workbench/properties/Properties.tsx index c8383c2..ce3d469 100644 --- a/src/lib/workbench/properties/Properties.tsx +++ b/src/lib/workbench/properties/Properties.tsx @@ -3,8 +3,14 @@ import { Input } from '$lib/ui/primitives/input'; import { Button } from '$lib/ui/primitives/button'; import { useState } from 'react'; import type { ModelElement } from '$lib/workspace/project'; +import type { ArtifactNode } from '$lib/workspace/views'; +import type { DiagramRelationshipSelection } from './diagram-selection'; export function Properties({ element, + artifactNode, + relationship, + sourceAvailable = true, + readOnlyDiagram = false, usageContext, disabled, onRename, @@ -14,6 +20,10 @@ export function Properties({ onConnectPorts, }: { element?: ModelElement; + artifactNode?: ArtifactNode; + relationship?: DiagramRelationshipSelection; + sourceAvailable?: boolean; + readOnlyDiagram?: boolean; usageContext?: readonly string[]; disabled: boolean; onRename: (name: string) => void; @@ -25,7 +35,55 @@ export function Properties({ return (