diff --git a/CONTEXT.md b/CONTEXT.md new file mode 100644 index 0000000..6fccbcb --- /dev/null +++ b/CONTEXT.md @@ -0,0 +1,37 @@ +# Model exploration and authored views + +Language for understanding a SysML project and choosing how to inspect its content. These terms distinguish model content from its storage and graphical presentation. + +## Language + +**Source file**: +An authoring document containing SysML declarations. A file boundary is not necessarily a package or system boundary. +_Avoid_: Model, system, diagram when referring to a file. + +**Project model**: +The logical model resolved from a project's source documents together. It can contain several systems, reusable definitions and packages. +_Avoid_: Full system when referring to all project content. + +**Applied model**: +The last accepted, validated model revision. Unapplied draft source can differ from the source of this revision. + +**Scope**: +The model content being inspected, such as the project, a package or a particular system usage. A definition and its individual usages are distinct subjects. + +**Representation**: +The graphical form used to inspect a scope, such as an overview or internal structure. Representation choice does not define the underlying system. +_Avoid_: Named view when only the graphical form is meant. + +**Exploration**: +A temporary browsing context consisting of a scope and representation, without requiring an authored SysML view declaration. +_Avoid_: View usage, saved view. + +**Named view**: +An authored SysML view usage specifying a repeatable presentation of model content through its declared scope, exposure, filters and representation semantics. +_Avoid_: Diagram tab, temporary exploration. + +**Diagram presentation**: +The visual arrangement of symbols for a particular graphical context. Geometry does not establish semantic ownership, connectivity or usage identity. + +**Camera**: +The viewport position and zoom through which a diagram is inspected. Camera movement does not change model-space geometry. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 9dd6706..9d1ecb6 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,6 +1,6 @@ # Contributing -Start with [current capabilities](docs/capabilities.md), [next steps](docs/next-steps.md), [architecture](docs/architecture/README.md) and the [design system](docs/design-system.md). The current milestone is a small assembly that can be understood and edited from source or diagram while preserving layout and unrelated source content. Reviewed part renames, bounded source-edit preservation and typed part/connection creation now share the transaction path. Start with their [acceptance criteria](docs/next-steps.md#21-preserve-layout-through-reviewed-part-renames) and [authoring limits](docs/architecture/graphical-authoring.md) before extending the next everyday-editing tasks. +Start with the [documentation index](docs/README.md), [current capabilities](docs/capabilities.md) and [next steps](docs/next-steps.md). The source/assembly editing workflow and explicit entry shell are implemented within their documented limits. Automatic read-only model exploration is the next feature priority; follow its [acceptance criteria](docs/architecture/model-exploration.md#first-implementation-slice). Before extending semantic editing, read the [authoring limits](docs/architecture/graphical-authoring.md) and [identity policy](docs/architecture/identity-reconciliation.md). ## Get started diff --git a/README.md b/README.md index 868963e..508855c 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@

- SysML v2 View Editor — author source, review changes and explore connected views + OpenMBEE View Editor — author source, review changes and explore connected views

-

SysML v2 View Editor

+

OpenMBEE View Editor

Work with SysML v2 source and diagrams in your browser.
@@ -31,7 +31,7 @@ > **Sandbox stage** — Actively iterating on the design and implementation. Features, interfaces and workflows may change. Not intended for production use. -SysML v2 View Editor brings source editing, named model views and reviewed graphical authoring into one local workbench. Open a multi-file model, inspect its structure and connections, review supported edits, then download the source and compatible diagram positions as one project. +OpenMBEE View Editor brings source editing, named model views and reviewed graphical authoring into one local workbench. Open a multi-file model, inspect its structure and connections, review supported edits, then download the source and compatible diagram positions as one project. **Source owns model semantics. Presentation owns geometry.** Renaming a part or connecting ports produces a reviewed source transaction; moving a symbol changes its position. The application runs locally in browser workers and requires no application backend. @@ -72,18 +72,22 @@ npm run dev -- --host 127.0.0.1 Open the URL printed by Vite. The first dev/build verifies and prepares checksum-pinned browser engines. It needs network access, `tar` and disk space for the Go build cache. Linux x64 can download the pinned Go 1.25.11 toolchain into ignored `sandbox/`; on other platforms, set `SYSML_GO` to that Go version's binary. Later runs verify and reuse prepared assets. See [engine preparation](engine-bridge/README.md). -### 2. Explore the startup model +Vite excludes generated `sandbox/` contents and prepared `static/vendor/` binaries from development watching; app sources remain watched. If an older dev process reported `ENOSPC` from those directories, restart it with the current configuration. -**Smart air workshop** opens automatically in Diagram mode. Its nine source files and fourteen named views cover reusable definitions, specialization, attributes, constraints, requirements, actions, states, use cases, sequences and geometry. +### 2. Open the app or try an example + +The landing page offers **Open app** and **Try an example**. `/app` opens a project launcher with **New project**, **Open files or project**, **Load example** and **Local recovery**. No model loads implicitly. New projects stay marked modified until downloaded. + +Choose **Load example** to open **Smart air workshop** in Diagram mode. Its nine source files and fourteen named views cover reusable definitions, specialization, attributes, constraints, requirements, actions, states, use cases, sequences and geometry. The starting **pneumatics** view shows the pump, tank, regulator, relief valve and pressure sensor, with thirteen ports and four connections: -1. Select another named view in Explorer to explore its contents. +1. Select another named view in Explorer or the diagram’s **Named view** selector to explore its contents. 2. Choose **Split** to see source beside the diagram; selecting a source file keeps the active view open. 3. Edit source and choose **Apply source**. Invalid drafts retain the previous applied model and appear in **Issues**. 4. Choose **Download project** to save your work before closing or reloading. -Six startup views have partial structural/definition/tree rendering. The other eight evaluate through the engine and display an explicit unavailable-renderer message. The [model guide](examples/air-compressor/README.md) lists each view's current support. +Six example views have partial structural/definition/tree rendering. The other eight evaluate through the engine and display an explicit unavailable-renderer message. The [model guide](examples/air-compressor/README.md) lists each view's current support. ### 3. Try graphical authoring @@ -102,7 +106,7 @@ Open all `.sysml` files from one example folder together. Each project has its o | Project | Explore | | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- | -| [Smart air workshop](examples/air-compressor/README.md) | A complete nine-file example spanning structure, requirements, behavior and geometry; the startup project | +| [Smart air workshop](examples/air-compressor/README.md) | A complete nine-file example spanning structure, requirements, behavior and geometry; the built-in launcher example | | [Nested assembly](examples/assembly/) | Nested ownership, repeated usages, inherited ports, a connection and reference notation; the focused authoring fixture | | [Parts](examples/parts/), [ports](examples/ports/) and [packages](examples/packages/) | Shared definitions, usage contexts, conjugation, imports and exposure | | [Requirements](examples/requirements/), [use cases](examples/use-cases/), [actions](examples/actions/), [states](examples/states/) and [sequences](examples/sequences/) | Source constructs and named views, including explicit unavailable-renderer cases | @@ -127,6 +131,10 @@ React Flow draws the interactive diagrams and ELK supplies automatic layout. The ## Projects and workspace +Use **Projects** to return to the launcher and **Resume project** to continue. The live workbench stays mounted while you browse the launcher or landing page, retaining source drafts, accepted history, selection and diagram state. A full reload starts at the launcher; existing browser recovery is offered explicitly. Landing and app share one appearance preference controller. + +Folder write-back, Flexo project/branch checkout and automatic diagrams for models without named views remain planned. Opening source imports copies; it does not connect to a folder or remote branch. + **Download project** saves one `.sysml-project.json`; **Open** accepts that bundle or multiple `.sysml` files. Downloads do not write back to the imported folder. **Local recovery** keeps one recovery copy in this browser using IndexedDB, including applied source, unapplied drafts, diagram positions and the active file/view. On reload, choose **Restore recovery** or **Discard recovery** before new work can replace that copy. The dialog can disable local saving or retry storage failures. Browser storage can be cleared; retain downloaded projects as portable backups. Restored work remains modified until downloaded. With saving enabled, returning to a clean project clears this session’s older recovery; a competing tab cannot silently overwrite a newer copy. | Data | Where it lives | @@ -168,34 +176,25 @@ npm run preview -- --host 127.0.0.1 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). +Production hosting must serve `index.html` for direct `/app` navigation while retaining normal asset paths; use the SPA fallback for your hosting provider. `/app/?example=1` is canonicalized to `/app` before the explicitly requested example opens, and reload does not repeat that opening action. + For example changes, run `npm run test:examples`; use `npm run examples:index` after updating the catalog. `npm run format` formats owned source and docs; `npm run lint` checks formatting and architectural imports. Keep temporary files, reports and captures under ignored `sandbox/`. ## Documentation -| Guide | What you will find | -| ------------------------------------------------------------------- | ------------------------------------------------------------------------ | -| [Capabilities](docs/capabilities.md) | Supported workflows and current limits | -| [Architecture](docs/architecture/README.md) | Module ownership, engine boundaries and source transactions | -| [Project format](docs/architecture/project-format.md) | Source, drafts, identity and presentation serialization | -| [Graphical authoring](docs/architecture/graphical-authoring.md) | Reviewed creation/rename, command validation and failure behavior | -| [Layout preservation](docs/architecture/identity-reconciliation.md) | Verified correspondence, bounded source edits and unsafe mappings | -| [Workbench panels](docs/architecture/workbench-layout.md) | Resizing, focus, toolbar layout and browser preferences | -| [Design system](docs/design-system.md) | Shared controls, typography, appearance and registry workflow | -| [Testing](docs/testing.md) | Real-engine scenarios, visual comparisons and failure artifacts | -| [Verification](docs/verification.md) | Local results, retained evidence and skipped coverage | -| [Next steps](docs/next-steps.md) | Implemented milestones and planned work, including Flexo MMS integration | +Start with the [documentation index](docs/README.md) for current capabilities, architecture contracts, design conventions, testing evidence and planned work. Engine preparation and provenance live in the [engine guide](engine-bridge/README.md); model fixtures and visualization targets live in the [example library](examples/README.md). ## Scope 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#future-integrations-planned) 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#3-flexo-checkout-then-reviewed-publication) for the proposed direction. ## Contributing The default branch, `develop`, holds ongoing sandbox work. Feature PRs target `develop`; releases and hotfixes follow the [Git Flow workflow](docs/git-workflow.md). `main` requires a code-owner approval and passing CI. -Start with the [contribution guide](CONTRIBUTING.md), [architecture decisions](docs/decisions/0001-foundation.md) and the acceptance criteria in [next steps](docs/next-steps.md). Keep changes focused, preserve license/provenance notices and request approval before adding production dependencies. Use the [project repository](https://github.com/Open-MBEE/sysml_view_editor) for bugs and feature requests. +Start with the [contribution guide](CONTRIBUTING.md), [architecture](docs/architecture/README.md) and the acceptance criteria in [next steps](docs/next-steps.md). Keep changes focused, preserve license/provenance notices and request approval before adding production dependencies. Use the [project repository](https://github.com/Open-MBEE/sysml_view_editor) for bugs and feature requests. ## Security diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..5df8c1b --- /dev/null +++ b/docs/README.md @@ -0,0 +1,37 @@ +# Documentation + +OpenMBEE View Editor is a sandbox browser workspace for SysML v2 source and named views. This index separates current behavior, implementation contracts and planned work. Setup and everyday use live in the [project README](../README.md). + +## Start here + +| Guide | Purpose | +| ------------------------------------------ | ---------------------------------------------------------------- | +| [Capabilities and limits](capabilities.md) | What the application supports today | +| [Next steps](next-steps.md) | Current priorities and the gates for future work | +| [Architecture](architecture/README.md) | Module ownership, engine boundaries and transaction rules | +| [Design system](design-system.md) | Shared shadcn controls, themes and UI conventions | +| [Testing](testing.md) | Running checks, reviewing baselines and diagnosing failures | +| [Verification](verification.md) | Latest recorded results, regression evidence and coverage limits | +| [Git workflow](git-workflow.md) | Branches, PR targets and release integration | + +## 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 | + +## Planned work and research + +- [Model exploration](architecture/model-exploration.md): agreed behavior for browsing models without authored views; not implemented. +- [Exploration engine investigation](research/automatic-exploration-contract.md): pinned renderer/WASM evidence and the next contract boundary. +- [Flexo MMS integration](research/openmbee-flexo-mms.md): dated source research and unresolved interchange/concurrency gates; not implemented. + +Related sources: [domain glossary](../CONTEXT.md), [engine preparation and provenance](../engine-bridge/README.md), [example library](../examples/README.md), [contribution guide](../CONTRIBUTING.md). + +Keep current behavior in capabilities and owning contracts; put future acceptance criteria in the roadmap or exploration plan. Verification records observations, not promises. Superseded milestone narratives remain available in Git history rather than parallel archive documents. Local `sandbox/` evidence paths are ignored artifacts and may be absent from another checkout. diff --git a/docs/architecture/README.md b/docs/architecture/README.md index 33a0ac2..6cd5a45 100644 --- a/docs/architecture/README.md +++ b/docs/architecture/README.md @@ -1,5 +1,7 @@ # Architecture +See the [documentation index](../README.md) for the detailed implementation contracts and planned work. + One static browser application contains a framework-independent workspace and a React workbench. There is one npm project. Internal modules expose small interfaces; extraction into separately published packages requires a concrete second consumer. ## Ownership and direction @@ -32,6 +34,8 @@ The [identity investigation](identity-reconciliation.md) compares real worker ou ## Views +The [agreed exploration direction](model-exploration.md) separates temporary scope/representation browsing from authored named views. It is planned; the current implementation described below requires a named view. The [domain glossary](../../CONTEXT.md) defines these terms. + Named views are actual SysML usages. Open existing views without authoring another declaration. New view takes a subject, representation and name; package ownership is advanced configuration. Definitions, exposure and filters remain source content. The canvas is a derived artifact and does not serialize the model. 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). @@ -51,3 +55,5 @@ Unit tests exercise workspace interfaces with controlled engine responses. These The [panel shell](workbench-layout.md) composes resizable tools and editors, with validated browser layout preferences and accessible icon actions. It owns no semantic transactions or portable-project fields. Divider movement retains camera/model-space geometry; explicit editor mode changes retain the existing fit-to-pane behavior. [Source assistance and local recovery](source-assistance-recovery.md) describes draft-bound language queries, supported edits, the independent recovery envelope and concurrency policy. + +The [application shell](application-shell.md) separates the public landing, explicit launcher and live workbench. The workbench session remains mounted during internal launcher/home navigation; full reload offers recovery explicitly. Entry pages use shared appearance state and defer engine work until a project-opening action. diff --git a/docs/architecture/application-shell.md b/docs/architecture/application-shell.md new file mode 100644 index 0000000..431559c --- /dev/null +++ b/docs/architecture/application-shell.md @@ -0,0 +1,40 @@ +# Application shell + +Current implementation, updated 2026-11-01. The application uses the shared Geist/Lucide/shadcn controls and appearance tokens. Product identity is **OpenMBEE View Editor**, at **Sandbox** stage; legal and third-party attribution remains in the repository notices. + +## Entry and opening + +- `/` presents the product, documentation/feedback/license links, **Open app** and **Try an example**. +- `/app` starts an empty launcher with **Load example**, **New project**, **Open files or project** and **Local recovery**. An existing live session adds **Resume project**. +- `/app/?example=1` requests the example once and is canonicalized to `/app` before opening; reload does not repeat the example action. Static hosting must serve the SPA fallback for `/app` while preserving asset paths. + +No model loads implicitly. The built-in example opens its pneumatic named view in Diagram mode. New projects open source and remain modified until downloaded. Imports are copies; Download exports a portable bundle. Recovery is a separate browser copy, not a disk save. + +Applied source is validated before replacing the project. Replacing dirty work requires confirmation. Cancelled or failed replacement retains current source, drafts, model, history and compatible presentations. Failed operations remain visible and can be retried. + +## Session and focus + +The workbench is created on entry, but source/diagram panels mount only after a successful opening. Once opened, they remain mounted while the launcher or landing is displayed; hidden surfaces are inert. Home, Resume and browser Back/Forward retain the live source editor/selection, history, Inspector selection, camera and diagram positions. A full reload creates a new session and offers stored recovery explicitly; it does not restore session history or camera. + +One appearance provider owns preferences across all surfaces. Dialogs close when their owning surface becomes inactive; focus returns to a visible trigger or the current heading/workspace. The workbench Save shortcut is active only while the workspace is visible and ready. Unsaved-work unload protection remains active while internal navigation hides the workspace. + +## Workbench grouping + +The app bar groups product/home navigation, Projects, project name/origin and persistence/settings actions. Source transaction history, apply/discard and Code/Split/Diagram controls use the measured toolbar layout. Sidebar toggles belong to their tools. The diagram header selects an actual **Named view**, displays its supported representation and applied/draft status, and keeps authoring actions nearby. Source-file selection does not replace the diagram's named view. + +See [layout contracts](workbench-layout.md), [source/recovery policies](source-assistance-recovery.md) and [project format](project-format.md). [Testing](../testing.md) and [verification](../verification.md) own checks and observed results. + +## Future opening workflows + +These are capability gates, not shipped controls: + +| Workflow | Required before delivery | +| ----------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Automatic exploration | Validated read-only projection with scope/representation independent of files and authored views; see the [exploration plan](model-exploration.md) | +| Local folder import | Explicit membership/exclusions/limits, permission handling and file-import fallback | +| Folder write-back | Exact write set, permissions, external-change detection and preservation on failure | +| Flexo project/branch | Select a deployment/auth flow, prove interchange/identity, resolve branch to immutable commit and label generated source; see [integration research](../research/openmbee-flexo-mms.md) | +| Remote publication | Reviewed change set, repository identity retention, server-enforced concurrency and ambiguous-outcome recovery | +| Recent projects/context | Validated persistence, permission renewal and schema compatibility; navigation references are not backups | + +The [roadmap](../next-steps.md) defines the next sequence. Resolve folder scope and Flexo deployment/authentication when their implementation slices begin. diff --git a/docs/architecture/engine-identity-sources.md b/docs/architecture/engine-identity-sources.md deleted file mode 100644 index fc7b896..0000000 --- a/docs/architecture/engine-identity-sources.md +++ /dev/null @@ -1,49 +0,0 @@ -# Engine identity source findings - -Recorded 2026-10-26 for the identity/layout investigation. This note records the original source inspection at `fd54b67`; runtime comparisons and implemented reconciliation supply separate evidence. The current production policy is documented in [identity reconciliation](identity-reconciliation.md). The inspected engines are OpenSysML `0.10.0` at `08ef397841767b829e492bbecf6d723483b4421e` and SysML Toolkit `0.10.10` at `339ef0e073d06e553e81c079755ddfcd224e4b92`. The [engine preparation guide](../../engine-bridge/README.md) and preparation scripts own those pins and generated provenance. - -## The application has several different identity domains - -| Value | What the inspected source defines | Consequence for matching revisions | -| ------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| OpenSysML `modelHash` | SHA-256 of conformance mode, document count and each ordered document's name, language and full content | Identifies the complete parser input. Comments, filenames, document order and unrelated file changes can change it. It is not an element identity. | -| Normalized model element `id` | `GetSymbol` returns the symbol's fully qualified name; the worker copies it into the model record | Renaming an element or its owner changes this name. Reusing the name can identify a different declaration. | -| Evaluated `correspondence.subject.id` | The bridge formats a located declaration as `file@offset:length` | Identifies a recorded source span within a revision. Insertion, replacement, file moves and changes inside enclosing declarations can change it. A same-length replacement can reuse it. | -| Evaluated `correspondence.context` | The bridge collects ancestor declaration origins and appends the node's origin; inherited-port correspondence adds its declaration origin | Distinguishes occurrences of the same inherited port in a particular usage chain within a view. The origin keys remain sensitive to source edits. | -| Diagram node/port `symbol` | A renderer traversal counter (`n0`, `n1`, …), with port ordinal appended to the node ID | Identifies an artifact entry in that rendering. A new traversal can assign the same string to a different element. | -| Toolkit `idMap` | An edit result's old/new IDs in Toolkit's own session; its public element APIs also expose interchange IDs | The inspected application does not expose a mapping from these IDs to OpenSysML origins, model names or diagram symbols. A join must be demonstrated rather than assumed. | - -Sources for the hash and model names are the pinned upstream [parser input key](https://github.com/Open-MBEE/OpenSysML/blob/08ef397841767b829e492bbecf6d723483b4421e/internal/frontend/engine/engine.go#L527), [hash function](https://github.com/Open-MBEE/OpenSysML/blob/08ef397841767b829e492bbecf6d723483b4421e/internal/frontend/engine/engine.go#L1035), [GetSymbol lookup](https://github.com/Open-MBEE/OpenSysML/blob/08ef397841767b829e492bbecf6d723483b4421e/internal/frontend/core/core.go#L335) and [symbol facts](https://github.com/Open-MBEE/OpenSysML/blob/08ef397841767b829e492bbecf6d723483b4421e/internal/frontend/symbolfacts/facts.go#L110). The worker preserves `symbol.id`, ownership and resolved type IDs in [semantic.worker.ts](../../src/lib/engines/opensysml/semantic.worker.ts#L96). - -The retained bridge defines [subject/origin fields](../../engine-bridge/opensysml/evaluate_view.go#L18), [origin lookup and subject construction](../../engine-bridge/opensysml/evaluate_view.go#L88), [usage-context construction](../../engine-bridge/opensysml/evaluate_view.go#L149) and [origin-key spelling](../../engine-bridge/opensysml/evaluate_view.go#L209). `QualifiedName` is looked up using a source origin; it is not a persistent ID assigned by this bridge. - -The upstream renderer [starts a new structural counter for a rendering](https://github.com/Open-MBEE/OpenSysML/blob/08ef397841767b829e492bbecf6d723483b4421e/internal/ir/view/interconnection.go#L20), [allocates IDs while visiting features](https://github.com/Open-MBEE/OpenSysML/blob/08ef397841767b829e492bbecf6d723483b4421e/internal/ir/view/interconnection.go#L111), [emits counter names](https://github.com/Open-MBEE/OpenSysML/blob/08ef397841767b829e492bbecf6d723483b4421e/internal/ir/view/view.go#L769) and [names ports by ordinal](https://github.com/Open-MBEE/OpenSysML/blob/08ef397841767b829e492bbecf6d723483b4421e/internal/ir/view/behavior.go#L820). Therefore stability observed in one fixture must not be promoted to a guarantee that those IDs persist across edits or views. - -The cached upstream sources were inspected under `sandbox/opensysml-build/source/` with the paths shown in those immutable links. That directory is ignored and reconstructed by preparation. Bridge overlays are retained separately in this repository; they are not claimed to exist in upstream source at that commit. - -## Toolkit edit evidence at the original investigation revision - -The prepared Toolkit JavaScript wrapper documents `checkEdit` as a dry run of splice, reparse, rebuild and reference checks, returning `wouldCommit`, `idMap`, findings and splices. It documents successful `edit` as rebuilding the session, invalidating outstanding handles, and returning old/new ID pairs plus splices in **pre-commit byte offsets per unit**. These descriptions were inspected at generated `static/vendor/sysml-toolkit/sysmlv2.js:465–476` and `:746–763`. The prepared wrapper's SHA-256 is `3c4d015fdce693a103c5a3d6c880c5d930a72bb6fb8cd54ce638abc38aaab52b`, recorded in its generated provenance and verified by [Toolkit preparation](../../scripts/prepare-sysml-toolkit.mjs). Its `elementById`/`elementId` APIs describe interchange UUIDs at `:787–805`; this does not prove that any such UUID is an OpenSysML ID. - -At `fd54b67`, the transformation worker checked refusal/findings and returned only full sources; its interface was `Promise`. The current [transformation result](../../src/lib/workspace/editing/evidence.ts) now includes validated source-bound splices and optional owner extent. The [adapter](../../src/lib/engines/toolkit/transform.worker.ts) still does not treat Toolkit UUID maps as OpenSysML identity. The semantic worker reparses source in a new disposable worker for each request; [worker lifetime](../../src/lib/engines/workers/request.ts#L1) terminates the worker on completion. The application cannot rely on retaining an engine session's in-memory handles across these operations. - -Source inspection establishes an available direction for a rename investigation: capture the actual edit report, associate each splice unit with a filename, replay the byte splices against the exact before sources, and compare the result with the returned after sources. The resulting offset relation can then be checked against both versions of the OpenSysML origins. This is a proposed use of the evidence; these sources alone do not establish that every declaration or context can be reconciled. - -JavaScript string offsets count UTF-16 code units, so using them directly for the wrapper's documented byte positions would be unsafe for Unicode source. Any adapter must prove UTF-8 byte handling, offset bounds, unit/file association, nonoverlapping splice replay and the exact before/after source binding before publishing correspondence. - -## Requirements implied by these contracts - -These are deductions from the inspected contracts, not a claim that reconciliation is implemented: - -1. Keep model revision, view identity, declaration identity, usage chain and artifact symbol distinct. The same inherited-port declaration appears under multiple usage chains; declaration-only matching can attach the right port to the wrong instance. -2. Establish declaration lineage from verified edit evidence before using names or origin spans to join old/new artifacts. Exact names and equal spans are useful checks, but neither proves continuity after arbitrary source replacement. -3. Require a unique match in both directions, and validate all relevant ancestor contexts. When origins are missing, duplicated or ambiguous, discard the affected mapping rather than choosing a same-label candidate. -4. Resolve old and new symbols separately in each view. The renderer-local IDs and traversal ordering provide no cross-view identity guarantee. -5. Distinguish a verified transformation from a free-form draft. Toolkit can report how it performed a known edit; the current source-apply interface receives replacement text without an edit report. A conservative policy is still needed for arbitrary drafts, deleted/recreated declarations, moved files and changed owners. -6. Recheck source/model/view compatibility after mapping and before accepting positions. A successful source transformation does not prove a complete or renderable view. - -The investigation revision deliberately cleared presentations on source application and proposal acceptance. The current [workspace](../../src/lib/workspace/createWorkspace.ts) reconciles compatible positions using the [implemented policy](identity-reconciliation.md). The [version-1 project format](project-format.md) remains unchanged; source inspection alone is still not evidence of application preservation. - -## Runtime evidence and remaining work - -The [runtime investigation](identity-reconciliation.md) now compares unchanged reparses, Toolkit definition/usage renames, an unrelated-file edit, offset-shifting edits, reordering, insertion, file/owner moves and changed types. It retains Unicode/splice evidence and tests distinct repeated-port contexts in both views. Reordering equal-length usages supplies a concrete same-location identity counterexample. The implementation now adds a conservative draft policy, stale-source/presentation rejection and separate browser preservation/authoring checks through undo/redo and fresh import. General arbitrary-replacement identity and independent layout undo remain gaps. See [verification](../verification.md) for actual application results. diff --git a/docs/architecture/identity-reconciliation.md b/docs/architecture/identity-reconciliation.md index 632cc2e..0fc6096 100644 --- a/docs/architecture/identity-reconciliation.md +++ b/docs/architecture/identity-reconciliation.md @@ -1,6 +1,6 @@ # Identity across source revisions -Investigation recorded 2026-10-26; implementation policy updated 2026-10-29. The original comparisons below establish the identity hazards and bounded rename join. Production preservation now uses that evidence for reviewed commands and a conservative policy for free-form source edits; actual checks are recorded in [verification](../verification.md). +Investigation recorded 2026-10-26; current implementation reviewed 2026-11-01. The original comparisons below establish the identity hazards and bounded rename join. Production preservation now uses that evidence for reviewed commands and a conservative policy for free-form source edits; actual checks are recorded in [verification](../verification.md). ## Findings @@ -8,7 +8,22 @@ No currently exposed identifier is a persistent cross-revision identity. An unch There is a viable bounded path for verified renames: Toolkit supplies actual edit splices and changed interchange IDs. Replaying the splices against the exact old UTF-8 sources reproduces the worker's returned sources. Transporting recorded OpenSysML declaration spans and every ancestor context through those verified name edits identifies unique counterparts in both assembly views, including repeated inherited ports. The probe checks model rename postconditions as well. The original experiment proves its tested rename. The implementation policy below defines the production subset; it does not establish persistent identity for arbitrary text replacement. -The [source-contract findings](engine-identity-sources.md) explain the distinct identity domains and cite the pinned engine sources. The retained [browser investigation](../../tests/e2e/identity.spec.ts) supplies runtime evidence with production worker bundles and the actual prepared WASM engines. It sends validated worker requests and validates model/evaluation replies; it does not substitute engine responses. +The retained [browser investigation](../../tests/e2e/identity.spec.ts) supplies runtime evidence with production worker bundles and the actual prepared WASM engines. It sends validated worker requests and validates model/evaluation replies; it does not substitute engine responses. + +## Identity domains + +| Value | Meaning and limit | +| --------------------------- | --------------------------------------------------------------------------------------------- | +| `modelHash` | Hash of the complete ordered parser input; comments and unrelated edits can change it | +| Model element `id` | Qualified name; changes on rename and can be reused by another declaration | +| `correspondence.subject.id` | Located source declaration, spelled `file@offset:length`; revision-bound | +| `correspondence.context` | Complete ancestor/usage origin chain; distinguishes repeated occurrences within a revision | +| Diagram `symbol` | Renderer-local traversal counter/port ordinal; no cross-view or cross-edit identity guarantee | +| Toolkit UUID / `idMap` | Toolkit's interchange/session identity domain; not an OpenSysML origin or renderer symbol | + +The retained [bridge](../../engine-bridge/opensysml/evaluate_view.go), [semantic worker](../../src/lib/engines/opensysml/semantic.worker.ts) and [transformation worker](../../src/lib/engines/toolkit/transform.worker.ts) own normalization. Immutable upstream references describe the [input hash](https://github.com/Open-MBEE/OpenSysML/blob/08ef397841767b829e492bbecf6d723483b4421e/internal/frontend/engine/engine.go#L1035) and [renderer IDs](https://github.com/Open-MBEE/OpenSysML/blob/08ef397841767b829e492bbecf6d723483b4421e/internal/ir/view/view.go#L769). Engine pins/checksums are owned by [preparation and provenance](../../engine-bridge/README.md), not duplicated here. + +Toolkit splice units name files; numeric `session.units()` handles are separate. Splices use pre-edit UTF-8 byte offsets, while JavaScript strings use UTF-16. Exact replay, valid code-point boundaries and source digests must be verified before using edit evidence. Disposable workers provide no persistent engine handles. The demonstrated join below uses verified source edits and complete OpenSysML origins; it does not establish a general UUID join or arbitrary-replacement continuity. ## Comparison matrix @@ -58,7 +73,7 @@ Mapped presentations are prepared with the semantic candidate, then published at The version-1 bundle shape is unchanged. It stores sources/drafts/compatible positions, while edit evidence and origin joins remain transient. Fresh import reparses source and restores only hash-compatible positions. New symbols receive deterministic automatic layout until moved; existing renderer bounds checks may arrange incompatible coordinates again with a notice. Manual overlap avoidance, interactive parent resizing, file-moving controls and general persistent identity remain outside this implementation. -The [structural authoring contract](graphical-authoring.md) describes part and connection commands built on this path. The [roadmap](../next-steps.md) distinguishes delivered slices from remaining notation, language-assistance and recovery work. +The [structural authoring contract](graphical-authoring.md) describes part and connection commands built on this path. The [roadmap](../next-steps.md) distinguishes delivered slices from remaining exploration, integration and usability work. ## Reproduction and evidence diff --git a/docs/architecture/model-exploration.md b/docs/architecture/model-exploration.md new file mode 100644 index 0000000..090920c --- /dev/null +++ b/docs/architecture/model-exploration.md @@ -0,0 +1,44 @@ +# Model exploration and named views + +Agreed direction, 2026-11-01. This document describes planned behavior, not an implemented capability. Current diagrams still require named SysML view usages. + +Opening is exploration; creating a named view is authorship. Users should be able to inspect an imported project without first changing its source. The [domain glossary](../../CONTEXT.md) distinguishes source files, the project model, scopes, representations, explorations, named views, diagram presentations and cameras. + +## Opening experience + +- For a new import with one unambiguous top-level system subject, open its supported structural representation. +- For multiple systems, packages or ambiguous roots, open a project overview and let the user choose a scope. Do not equate all packages with one instantiated system. +- When reopening saved work, restore its valid previous browsing context when that context is supported by the saved format; otherwise use the appropriate opening fallback. + +Expose scope and representation as separate choices. Switching the active source file must not silently replace the diagram scope. Existing named views remain directly accessible. Browsing can lead to an explicit, reviewed action to create a named view where the exploration's scope and representation can be expressed faithfully in SysML. + +The system-root criterion needs evidence from the resolved model and example fixtures. Do not guess a system from its filename, display name or first package. Where the criterion is uncertain, prefer the project overview. The initial overview may show project/package contents; it must not imply that unrelated packages form one connected system. + +## Ownership and preservation + +Files organize source; the applied model resolves their meaning together. Unapplied or invalid drafts remain distinct from the last valid model shown by diagrams. Browsing should identify the revision it represents. + +Temporary scope and representation are workbench navigation state. Named-view declarations, exposures and filters remain source-owned semantics. Diagram geometry and camera are separate concerns. Exploring must not add hidden declarations to project sources, mark a project modified, add semantic history or overwrite saved named-view geometry. + +Use shared evaluation, rendering, correspondence and selection contracts for exploration and named views. Trace the actual engine contracts before choosing a read-only exploration interface. If evaluation requires temporary engine input, isolate it from applied source, drafts and serialization, and verify its identity correspondence rather than assuming synthetic IDs are equivalent. Scope changes and stale or failed evaluations must retain current work. + +## First implementation slice + +Implement read-only automatic exploration for an imported model with no view declarations, using only representations supported by the pinned engines and current renderers. Prove a project/package overview and a bounded structural subject before extending representation families or enabling graphical semantic edits within exploration. + +Acceptance: + +- A model without view usages opens a useful overview or an unambiguous system structure immediately. +- A multi-file fixture resolves as one project model; a multi-system fixture offers an overview rather than choosing an arbitrary system. +- Scope and representation controls distinguish definitions from repeated usages. Unsupported representations explain their limits. +- Existing named views still open normally and retain independent saved positions. Source-file navigation remains independent of diagram scope. +- Browsing and failed or superseded requests retain exact applied source, drafts, semantic history and saved presentations; valid scope changes do not inherit another subject's geometry. +- Real-engine browser checks establish the projection contract and preservation outcomes. Inspect desktop/narrow, light/dark captures and keyboard focus behavior. + +The first slice keeps exploration navigation transient. Persisting an exploration across reloads or portable reopen is a later explicit storage decision: the current recovery envelope records only an active file and named-view identity, and the portable bundle has no exploration field. Any persistence change must follow the owning schema, versioning and compatibility-fixture requirements. + +Follow-up slices are explicit creation of a named view from exploration, storage/restoration of browsing context, and additional representation families supported by actual engine and renderer evidence. State and action representations require independent engine and renderer verification. + +## Engine evidence + +The [pinned-engine investigation](../research/automatic-exploration-contract.md) executed the current WASM boundary and native renderer. Declaration-free project/package rendering has a promising underlying seam, but the browser contract still requires named views. A typed system usage did not automatically unfold its definition’s internals. The first delivered exploration must therefore use an overview until structural subject/usage expansion is proven, rather than promise an inferred full-system diagram. diff --git a/docs/architecture/source-assistance-recovery.md b/docs/architecture/source-assistance-recovery.md index 4b7552e..9b0ac6b 100644 --- a/docs/architecture/source-assistance-recovery.md +++ b/docs/architecture/source-assistance-recovery.md @@ -1,9 +1,21 @@ -# Source assistance and local recovery +# Source editing, assistance and recovery The workbench composes a separate Toolkit language adapter in `shell/session.ts`. Language queries are read-only and use every current draft source file, including unapplied text. Each request runs in a disposable worker against the pinned Toolkit library and negotiates UTF-16 positions. Normalization converts between raw source offsets and protocol positions, including CRLF and astral Unicode characters. Completion is explicit with Control+Space. Only supported single plain-text edits are offered; snippets, additional edits and commands are excluded. Hover and F1 display plain text. F12 navigates to one exact project-file definition; unavailable, ambiguous and library-only targets explain their limits. The editor rejects results after draft, session, file or busy-state changes and rechecks completion validity when applying it. Queries do not apply source, change model history or replace views. +## Issue navigation + +OpenSysML's pinned ParseSources response supplies diagnostic `span.file` and one-based `span.startLine`. The existing semantic worker normalizes those to optional `file` and `line`. The workbench selects a whole reported line, retaining raw UTF-16 offsets and CRLF/CR/LF boundaries; it does not claim a column or token range that this transport does not carry. + +`WorkspaceState.diagnosticDocuments` records the exact source documents associated with visible diagnostics. Failed draft application and rejected semantic candidates retain their respective inputs; accepted model diagnostics use applied documents. This transient state is independent of the version-1 portable project. Draft edits clear obsolete diagnostics and their inputs. + +`workbench/issues/navigation.ts` resolves a diagnostic only when its file belongs to the current project and that file's current draft text exactly matches the diagnosed text. A known file without a line opens at the beginning with no text selected and an explicit explanation. Unknown files, unavailable snapshots and out-of-range lines expose a reason instead of a source action. A candidate error must not lend its location to the old live source. + +The click handler rechecks current diagnostic membership and resolves against the current workspace snapshot before changing source focus. Source reveal preserves Code/Split mode, opening Split when the source was hidden in Diagram mode. The open named view, selected semantic element, source text and saved geometry remain independent of issue browsing. Explicit editor-mode changes retain the existing diagram-fit policy. + +The browser scenarios combine actual pinned-engine errors with controlled normalized responses for unavailable locations and rejected-candidate evidence. Unit checks cover raw line bounds, Unicode, line endings and stale snapshots. These are navigation and preservation guarantees, not complete diagnostic or SysML conformance. + ## Recovery storage Local recovery uses one IndexedDB record per browser origin: database `sysml-view-recovery`, store `recovery`, key `latest`. A separately validated version-1 recovery envelope contains a serialized portable project, save time and active file/view. The portable project format remains version 1. Recovery includes applied source, drafts and saved presentation geometry; it does not include canvas camera or undo history. diff --git a/docs/architecture/source-navigation.md b/docs/architecture/source-navigation.md deleted file mode 100644 index fc2564c..0000000 --- a/docs/architecture/source-navigation.md +++ /dev/null @@ -1,11 +0,0 @@ -# Source issue navigation - -OpenSysML's pinned ParseSources response supplies diagnostic `span.file` and one-based `span.startLine`. The existing semantic worker normalizes those to optional `file` and `line`. The workbench selects a whole reported line, retaining raw UTF-16 offsets and CRLF/CR/LF boundaries; it does not claim a column or token range that this transport does not carry. - -`WorkspaceState.diagnosticDocuments` records the exact source documents associated with visible diagnostics. Failed draft application and rejected semantic candidates retain their respective inputs; accepted model diagnostics use applied documents. This transient state is independent of the version-1 portable project. Draft edits clear obsolete diagnostics and their inputs. - -`workbench/issues/navigation.ts` resolves a diagnostic only when its file belongs to the current project and that file's current draft text exactly matches the diagnosed text. A known file without a line opens at the beginning with no text selected and an explicit explanation. Unknown files, unavailable snapshots and out-of-range lines expose a reason instead of a source action. A candidate error must not lend its location to the old live source. - -The click handler rechecks current diagnostic membership and resolves against the current workspace snapshot before changing source focus. Source reveal preserves Code/Split mode, opening Split when the source was hidden in Diagram mode. The open named view, selected semantic element, source text and saved geometry remain independent of issue browsing. Explicit editor-mode changes retain the existing diagram-fit policy. - -The browser scenarios combine actual pinned-engine errors with controlled normalized responses for unavailable locations and rejected-candidate evidence. Unit checks cover raw line bounds, Unicode, line endings and stale snapshots. These are navigation and preservation guarantees, not complete diagnostic or SysML conformance. diff --git a/docs/architecture/structural-views.md b/docs/architecture/structural-views.md index e2346cc..ec34508 100644 --- a/docs/architecture/structural-views.md +++ b/docs/architecture/structural-views.md @@ -33,4 +33,16 @@ Children remain within their displayed owner's content area. Group movement carr Browser tests load the shared sources through the actual pinned workers. They check nested bounds, reference notation, explicit port labels, SVG endpoints against the correct physical port handles, keyboard port selection/source reveal, distinct inherited-port contexts, parent and child movement, view switching and save/reopen with exact source retention. Light/dark snapshots cover the assembly in diagram-only mode. Unit tests exercise malformed topology, projection and absolute-coordinate restoration; the retained Go test checks the engine contract directly. -This slice establishes the fixture's nested interconnection workflow. GeneralView remains a flat tree. Broader owned-port boundary notation, effective directions through conjugated types, modeled layouts/routes, behavior/sequence/geometry rendering, large-model performance and full notation/language conformance remain outside the verified scope. Reviewed typed part creation and explicit directed, same-type port connections are available through diagram/Inspector actions; [the authoring contract](graphical-authoring.md) records restrictions. See [verification](../verification.md) for commands actually run and [next steps](../next-steps.md) for the remaining milestone. +This slice establishes the fixture's nested interconnection workflow. GeneralView remains a flat tree. Broader owned-port boundary notation, effective directions through conjugated types, modeled layouts/routes, behavior/sequence/geometry rendering, large-model performance and full notation/language conformance remain outside the verified scope. Reviewed typed part creation and explicit directed, same-type port connections are available through diagram/Inspector actions; [the authoring contract](graphical-authoring.md) records restrictions. See [verification](../verification.md) for commands actually run and [next steps](../next-steps.md) for planned extensions. + +## 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. + +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. + +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. + +Obstacle-aware routing requires a separate contract: accurate port coordinates/directions, nested coordinate transforms, parallel edges, labels on sufficiently long clear segments and rerouting after saved-position restoration or movement. Rerouting must retain manually positioned nodes. Test exact endpoint attachment, source/history preservation, pointer/keyboard movement and save/reopen before adopting those routes. Rounded corners alone do not provide obstacle avoidance. diff --git a/docs/architecture/workbench-layout.md b/docs/architecture/workbench-layout.md index 314c14c..4f1e30e 100644 --- a/docs/architecture/workbench-layout.md +++ b/docs/architecture/workbench-layout.md @@ -20,7 +20,7 @@ The canvas waits for measured dimensions and initialized nodes before fitting. E Reads and writes tolerate blocked browser storage. A failed save retains the live layout for the visit and reports the failure under Workspace layout. Reset applies responsive defaults. Preferences are specific to a browser origin; they are not part of the portable project. -Panel changes do not invoke workspace transactions, reparse source, alter diagram geometry, mark the project modified or add undo history. Source and Diagram remain mounted through mode changes. Project bundles retain their existing version-1 format and still require download to preserve source/drafts across reloads; remembering panel preferences is not work recovery. +Panel changes do not invoke workspace transactions, reparse source, alter diagram geometry, mark the project modified or add undo history. Source and Diagram remain mounted through mode changes. Project bundles retain their existing version-1 format. Download provides a portable copy; the separate local recovery workflow offers explicit restore/discard after reload. Remembering panel preferences is not work recovery. ## Source editor @@ -55,7 +55,7 @@ Lucide icons use the shared shadcn Button sizes, theme roles and focus indicator Apply and Download retain visible labels; frequent compact actions retain precise names such as Undo source. CodeMirror draft undo/redo is distinct from accepted source transaction history. Standard editor/search shortcuts are documented in the README; Tab retains focus navigation. Existing kind labels remain visible alongside Explorer icons; those icons do not establish SysML notation support. -[Browser testing](../testing.md) describes verification; [verification](../verification.md) records actual outcomes and remaining gaps. Full tab docking, floating windows, semantic language assistance and complete accessibility certification remain separate work. +[Browser testing](../testing.md) describes verification; [verification](../verification.md) records actual outcomes and remaining gaps. Full tab docking, floating windows and complete accessibility auditing remain separate work. Engine-backed language queries are documented in [source editing and recovery](source-assistance-recovery.md). ## Model ownership tree @@ -66,3 +66,11 @@ Apply and Download retain visible labels; frequent compact actions retain precis Navigation follows the [WAI-ARIA tree pattern](https://www.w3.org/WAI/ARIA/apg/patterns/treeview/): arrows move through visible rows and fold/unfold branches, Home/End reach the visible extremes, typed prefixes find visible names, and Enter/Space select the focused declaration. Focus and selection are separate. One treeitem participates in Tab order; shared focus styling distinguishes it from the inspected row. Diagram selection opens the selected declaration's owner chain without taking focus from the diagram. A UI request counter handles a second selection of the same symbol after its branch has been folded. These are reveal requests, not semantic revisions or identity reconciliation. Selecting a tree row retains the existing Inspector behavior; source-range reveal still requires diagram correspondence. Expansion is not portable project data or automatic work recovery. + +## Entry and session lifetime + +The public landing lives at `/`; `/app` starts with a project launcher. Source/diagram panels are created on first successful project opening and remain mounted while the launcher or landing is displayed. Inactive workbench surfaces are hidden and inert, retaining the live source editor, history and camera. Appearance state belongs to one application provider. Modal controls close when their owning route becomes inactive, and focus returns to visible controls or the current route heading. A full browser reload is a new session with an explicit recovery choice. + +## Shell code ownership + +`Workbench.tsx` retains the live session, project-opening validation, selection and reviewed-command orchestration. `ProjectHeader.tsx` and `SourceToolbar.tsx` render shell controls through explicit callbacks. `projectFiles.ts` owns local bundle construction, file reading and browser download preparation; workspace validation and saved-state tracking retain their existing contracts. Panel controls and preferences remain in `WorkbenchPanels.tsx` and `layout.ts`. Surface CSS is imported through `ui/theme.css`; see the [design-system ownership table](../design-system.md#ownership). diff --git a/docs/assets/readme-header.svg b/docs/assets/readme-header.svg index 435f9e8..166361f 100644 --- a/docs/assets/readme-header.svg +++ b/docs/assets/readme-header.svg @@ -1,5 +1,5 @@ - SysML v2 View Editor + OpenMBEE View Editor Author source, review changes and explore connected views. A browser workbench with local project files and pinned WebAssembly engines. @@ -12,7 +12,7 @@ SYSML V2 / BROWSER WORKSPACE - SysML v2 View Editor + OpenMBEE View Editor Author source. Review changes. Explore connected views. SOURCE & DIAGRAMS diff --git a/docs/capabilities.md b/docs/capabilities.md index 2f21b9c..946e18b 100644 --- a/docs/capabilities.md +++ b/docs/capabilities.md @@ -1,9 +1,10 @@ # Capabilities and known limits -This catalogue describes the current implementation. Check `docs/verification.md` for checks actually performed; a feature description alone is not test evidence. +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 | @@ -16,15 +17,15 @@ This catalogue describes the current implementation. Check `docs/verification.md | 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 controls and full old-UI parity remain future work | +| 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 | -The previous app remains the reference for features to evaluate and reintroduce through complete vertical slices. This repository does not claim full SysML v2 language, notation, execution or interchange conformance. +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. 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). -The startup project is [Smart air workshop](../examples/air-compressor/README.md): nine source files and fourteen named views, opening its pneumatic assembly in Diagram mode. Six views render partial structural/definition/tree representations; eight behavior/sequence/geometry views evaluate but report unavailable renderers. The simpler Workshop remains a separate example. +The explicit **Load example** project is [Smart air workshop](../examples/air-compressor/README.md): nine source files and fourteen named views, opening its pneumatic assembly in Diagram mode. Six views render partial structural/definition/tree representations; eight behavior/sequence/geometry views evaluate but report unavailable renderers. The simpler Workshop remains a separate example. The [visualization example library](../examples/README.md) records representative models, named views, intended visual quality and current gaps. Its checks distinguish successful engine evaluation from application rendering; use-case, package-graph and other unsupported renderer fixtures remain explicit development targets. diff --git a/docs/decisions/0001-foundation.md b/docs/decisions/0001-foundation.md deleted file mode 100644 index 77ff7c4..0000000 --- a/docs/decisions/0001-foundation.md +++ /dev/null @@ -1,18 +0,0 @@ -# 0001: Browser workspace with explicit source transactions - -Date: 2026-10-26. Status: initial implementation direction. - -## Decisions - -- React, TypeScript and Vite provide the browser workbench. React was selected during the fresh-start discussion for ecosystem reuse. Domain/workspace code remains independent of React. -- Retain React Flow and ELK for graph interaction/layout. Build SysML operations and adapters around them. -- OpenSysML supplies authoritative interpretation and native view evaluation; Toolkit proposes source transformations. The two engines have distinct interfaces and are not interchangeable semantic authorities. -- Source owns model declarations and native view semantics. Presentation owns geometry. Every supported graphical semantic edit produces a reviewed source transaction. -- TypeBox defines runtime data contracts and inferred types for persistence and worker/engine messages. Semantic invariants remain explicit workspace rules. -- Keep a single npm project and enforce import direction. Add packages/plugins/backends only for demonstrated requirements. - -## Consequences - -The initial scope is smaller than the predecessor application and is recorded in capabilities. Reusing pinned engine preparation/overlays preserves proven integration work and provenance while the application is implemented afresh. Whole-project worker requests are initially disposable and bounded by timeouts; persistent/incremental sessions require measured evidence and compatibility tests. - -The project format includes source/drafts and compatible positions from the outset. Conservative layout invalidation is preferable to unverified identity matching. Source selection, renderer support and editing capability remain separate outcomes internally, while users open named views through one list. diff --git a/docs/design-system.md b/docs/design-system.md index b91a57f..6baae9a 100644 --- a/docs/design-system.md +++ b/docs/design-system.md @@ -1,6 +1,6 @@ # Workbench design system -The React workbench carries forward the previous editor's appearance system. The visual language is maintained separately from the workspace and engine modules. +The OpenMBEE landing, launcher and React workbench share one appearance system, separate from workspace semantics and engine modules. ## Ownership @@ -13,7 +13,14 @@ The React workbench carries forward the previous editor's appearance system. The | `src/lib/ui/appearance.ts` | Framework-independent preference validation, storage helpers and safe custom-color blending | | `src/lib/ui/AppearanceSettings.tsx` | Browser storage, system-theme subscription and appearance controls | | `src/lib/ui/Dialog.tsx` | Shared controlled modal composition and focus return for workbench actions | -| `src/lib/ui/theme.css` | Workbench arrangement and panel/diagram styling | +| `src/lib/ui/theme.css` | Stylesheet entry point; imports the owning surface styles | +| `src/lib/ui/base.css` | Application defaults and shared product identity | +| `src/lib/ui/workbench.css` | Shell, toolbar and resizable panels | +| `src/lib/ui/source.css` | CodeMirror and language-query presentation | +| `src/lib/ui/diagram.css` | Canvas, structural notation, ports and diagram controls | +| `src/lib/ui/tools.css` | Explorer/model tree, Inspector and Issues | +| `src/lib/ui/dialogs.css` | Modal forms, source review and appearance settings | +| `src/lib/ui/entry.css` | Landing and project launcher | Use the installed shadcn primitives for new controls. The Button defaults to compact `sm` sizing with 12 px labels; callers choose an explicit variant. Do not introduce competing button styles or independent palette stores. Component sources are owned code: review registry updates rather than overwriting local changes blindly. @@ -32,15 +39,15 @@ Light with Muted element colors is the fresh-browser default. Dark and Follow sy Preferences use the existing `sysml-appearance-v1` browser key and parser. This preserves compatible preferences when hosted on the same origin; a different origin does not share storage. Invalid fields fall back independently. Denied reads use defaults; failed writes retain settings for the visit and show a message. -Muted base colors can be customized for packages, part definitions and part usages, and reset. Existing direction-specific port preferences are retained by the parser, but port color controls remain unavailable until detailed port rendering exists. Color changes produce blended fills; readable text/border roles remain controlled by the theme. +Muted base colors can be customized for packages, part definitions and part usages, and reset. Existing direction-specific port preferences are retained by the parser, but detailed port color controls are not exposed. Color changes produce blended fills; readable text/border roles remain controlled by the theme. -The current graph adapter maps exact engine notation kinds `package`, `part def` and `part` to palette roles. Other kinds keep neutral surfaces and their engine kind label. Detailed notation, custom connection routing and compound elements remain documented limits. +The current graph adapter maps exact engine notation kinds `package`, `part def` and `part` to palette roles. Other kinds keep neutral surfaces and their engine kind label. Nested structures are supported; broader notation and custom connection routing remain documented limits. Appearance is a user preference. It does not enter project bundles, mark projects modified, reparse source or add model undo history. Source application reconciles compatible presentation mappings according to the workspace contract; appearance never invokes reconciliation or changes geometry. ## Registry workflow -`components.json` configures the official React registry and local aliases. The shadcn MCP server can inspect components, usage examples and installation commands; it is development tooling and is not bundled into the app. It was already available in the development session; this repository does not modify personal MCP configuration. +`components.json` configures the official React registry and local aliases. The shadcn MCP server can inspect components, usage examples and installation commands; it is development tooling and is not bundled into the app. After reviewing an item and its dependency changes, add only the controls needed for a concrete workflow: @@ -59,3 +66,15 @@ Resizable, Tooltip and Popover are owned official registry primitives. `react-re Tool headers, dividers, collapse controls and preference handling live in the workbench, outside semantic workspace state. Focus transfer follows committed visibility, hidden tools are inert, and panel geometry is a browser preference. See [panel contracts](architecture/workbench-layout.md). The Model tree uses the shared Button through `asChild` for its rows and IconButton for expand/collapse-all controls. Native `tree`, `treeitem` and `group` roles express ownership; one roving tab stop, explicit selection and arrow-key navigation belong to the tree component. Chevron targets fold branches without changing the inspected selection. Kind icons are decorative; text labels and full declaration/type tooltips provide context. + +## Entry surfaces and shared appearance + +The OpenMBEE landing and project launcher use the existing Geist/Lucide/shadcn foundation and appearance roles. `ui/entry.css` owns their responsive arrangements. A single `AppearanceProvider` owns theme/palette preferences and root tokens across landing, launcher and retained workbench; individual Appearance buttons own only their dialog visibility. Navigating between surfaces must not reset appearance or create competing preference stores. The product uses a typographic OpenMBEE identity; no invented brand mark or copied predecessor license claims are introduced. + +Entry screens remain useful at 390, 768 and 1440 px. The launcher is a full app surface, not an empty workbench behind a modal. Contextual source and diagram actions remain in their own workbench rows; the Named view selector exposes actual authored views and its supported representation, without implying automatic model exploration. + +## Structural diagram cards + +Structural cards use a subtle palette-driven header, a small kind badge, a prominent declaration/instance name and quieter type text. Port rows separate name/type labels and place explicit direction text beside the corresponding square handle; directions remain engine-provided, including undirected/inout cases. Selected cards use an accent outline and halo without tinting their text. Compound containers use a lighter blend of the definition/canvas roles so their child cards stand out. Light/dark and forced-color styles share the same semantic tokens. + +The styling retains renderer node dimensions and absolute saved positions. Leaf port rows share the existing body space; the real handles remain the source of connection endpoint coordinates. UI labels do not infer effective direction through conjugation. diff --git a/docs/next-steps.md b/docs/next-steps.md index ed1b558..3e7464d 100644 --- a/docs/next-steps.md +++ b/docs/next-steps.md @@ -1,167 +1,45 @@ # Next steps -Updated 2026-10-30 from the development sequence recorded on 2026-10-26. Planned work below is not implemented unless explicitly marked. [Capabilities](capabilities.md) records current behavior; [verification](verification.md) records checks actually performed. No delivery dates or individual owners are assigned here. +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. ## Current position -The foundation and identity investigation are committed in `a2d0d91` and `fd54b67`; the assembly editing milestone is committed in `ead6337` on `feat/clean-foundation`. It implements reviewed rename layout preservation, bounded source-edit preservation, and typed part/port-connection authoring. The current usability pass adds resizable panels, contextual Lucide controls, measured toolbar alignment and an expandable ownership tree with keyboard navigation and diagram-selection reveal. [Smart air workshop](../examples/air-compressor/README.md) replaces the small Workshop as startup: nine source files and fourteen named views, opening its pneumatic assembly in Diagram mode. The smaller examples remain focused workflow fixtures. [Verification](verification.md) records exact local results, inspected captures and deliberate regression faults; hosted CI has not been run for this pass. +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. -The [identity investigation](architecture/identity-reconciliation.md) compares fresh parses, definition/usage renames, an unrelated-file edit, offset shifts, reordered/inserted usages, file/owner moves and changed types. It records a counterexample where unchanged symbol/origin/context IDs identify the wrong usage. Its original complete splice-based join covered a definition rename. The new production browser scenarios verify both definition and usage renames, including distinct saved views and fresh import; broader comparison cases remain explicit invalidation targets. +## 1. Read-only automatic exploration -The assembly milestone is locally complete within the documented bounded policy. The retained steps and acceptance criteria below record its implemented slices and verification gates. Source edits use a bounded unchanged-header policy, rather than general persistent identity. Remaining work after these slices concerns everyday editing in step 4 and the explicit notation, accessibility and performance gaps; broad source-reconciliation support is not implied. +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 semantic command contract now supports `rename`, `createView`, `createPart` and `connectPorts`. Diagram/Inspector forms review exact source and layout changes before acceptance. Connections require explicit direction, identical resolved port types and verified usage endpoints. See [authoring contracts](architecture/graphical-authoring.md); the example library still records renderer/notation gaps. +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. -## Assembly milestone: edit a small assembly in either presentation +Acceptance is owned by the [exploration plan](architecture/model-exploration.md): -An engineer should be able to open a small assembly, understand its ownership and connections, create a typed part and connect compatible ports through the diagram, inspect/apply the resulting source changes, and continue in source without losing unrelated content or compatible layout. +- No-view and multi-file/multi-system imports open useful, correctly scoped projections. +- Scope, representation and source-file navigation remain independent; repeated usages retain their context. +- Browsing and failed/superseded evaluation preserve exact source, drafts, semantic history and saved named-view positions. +- Temporary exploration does not modify source or borrow another subject's geometry. +- Actual-engine browser, keyboard and desktop/narrow light/dark checks establish the delivered subset. -Use one small, non-sensitive assembly fixture throughout the milestone. It should include shared part definitions, multiple usages, nested ownership, directed ports, a valid connection, two named views, comments and at least one source construct outside the graphical editor's supported subset. Grow the fixture only as each workflow becomes supported; retain a clear distinction between valid source and renderable content. +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. -The priorities are bidirectional editing, simpler views and everyday usability. Extend the working foundation through complete user workflows rather than importing the previous application wholesale. +## 2. Local folder workflow -## 1. Make structural diagrams useful +Start with explicit import semantics: membership, exclusions, source limits, permission cancellation and unsupported-browser fallback. Existing Open imports copies and Download exports a bundle. -Implemented bounded slice: existing assemblies render with the fixture coverage recorded below. GeneralView remains a flat tree. Broader notation gaps are tracked separately from the assembly workflow. +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). -Work: +## 3. Flexo checkout, then reviewed publication -- Preserve ownership and usage context when projecting nested structures. -- Show supported ports, their direction and unambiguous connection endpoints. -- Distinguish part definitions, usages, reference features and ownership through notation and labels as well as color. -- Keep symbol selection tied to recorded semantic correspondence across the diagram, Explorer and Inspector. -- Report unsupported artifacts or notation explicitly, retaining their source. +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. -Acceptance: +Publication follows only after one reviewed command preserves repository UUIDs and unedited semantics, with server-enforced stale-head rejection and defined retry/ambiguous-outcome handling. Authentication, conversion and publication failures must retain local work. No deployment/authentication target has been selected here. -- The fixture's assembly, repeated usages, supported ports and connections render with the correct context. -- Selecting a symbol identifies the intended subject/usage, including when a definition is used more than once. -- Dragging a symbol changes geometry without changing ownership, connectivity or source. -- Switching between the two named views preserves their separate presentation state. -- The workflow is checked with the actual pinned browser engines, plus keyboard and light/dark visual checks. +## Follow-on usability and rendering -The first bounded implementation task was to trace one nested assembly artifact through the engine adapter and renderer, document missing fields, and add only the contract/projection support required to render that fixture faithfully. Do not infer containment from a node's position. +The next diagram-polish slice is [edge interaction and routing](architecture/structural-views.md#planned-edge-interactions-and-routing): verify controlled selection, accessible endpoint descriptions, readable labels and hit targets before adopting obstacle-aware routes. Existing smoothstep labels and hit areas already provide part of that behavior. -Progress recorded 2026-10-26: the [first structural slice](architecture/structural-views.md) implements that bounded task using `examples/assembly/`. It covers nested groups, repeated Device usages, explicitly directed inherited ports and their connection, reference notation, contextual port/source selection, group/child movement, two views, unsupported-action notices and compatible save/reopen. The engine trace and retained tests define its scope; broader owned-port notation and effective directions through conjugation remain gaps. The subsequent identity investigation and production reconciliation/authoring code are now present; the acceptance criteria below govern their completion. +- Consistent selection and source/canvas reveal, including Explorer-only selection; model-wide search and discoverable language shortcuts. +- Broader structural notation, effective conjugated directions and new representation families, each backed by actual engine/renderer evidence. +- Accessibility and large-model performance work based on measured workflows. -## 2. Preserve identity and layout through source changes - -The implementation reconciles compatible presentations per view before publishing source changes. Unsafe matches are invalidated explicitly. The [implemented policy](architecture/identity-reconciliation.md) defines source-bound edit evidence and bounded free-form changes; the criteria below retain the intended scope. - -Work: - -- Distinguish semantic identity, usage context and per-view symbol identity in the mapping contract. -- Retain positions for elements whose correspondence is verified across revisions. -- Support safe rename and unrelated-file edits without unnecessarily rearranging unaffected symbols. -- Define behavior for new/deleted elements, file moves, duplicate names, changed owners and ambiguous correspondence. -- Keep revision checks, stale-proposal rejection and invalid-draft preservation intact. - -Acceptance: - -- A verified rename or unrelated edit retains compatible symbol positions in both views. -- Duplicate labels and repeated usages never borrow another symbol's identity or layout. -- A new symbol gets a deliberate placement; deleted or unmatched mappings are not silently reassigned. -- When identity cannot be established safely, affected layout is invalidated explicitly while source remains intact. -- Undo/redo behavior and save/reopen results are defined and tested. Any project-format change includes versioning and compatibility fixtures. - -The original worker/SDK investigation remains distinct from application tests. Production retention uses verified evidence and full usage contexts; it does not add a competing semantic model or a label-based identity scheme. - -### 2.1 Preserve layout through reviewed part renames - -User-visible outcome: move parts in both assembly views, review and apply a part definition or usage rename, and keep each intended usage at its compatible position. The resulting source and references must remain correct through undo/redo and a fresh save/reopen. - -Implemented sequence: - -1. Extend the transformation result and owning TypeBox transport schemas to carry rename edit evidence alongside the complete returned sources. Bind the evidence to the exact before/after source sets. Validate filename-based splice units, UTF-8 byte ranges, nonoverlap and exact replay. Malformed evidence must refuse the proposal and preserve the applied state. -2. Reconcile old/new evaluated artifacts for every view with saved positions, including views that are not currently open. Match the transported declaration origin and complete usage context uniquely in both directions, check ownership and resolved bindings, and resolve each view's new symbol IDs. Toolkit UUIDs, unchanged renderer IDs and equal labels are not substitutes for that mapping. -3. Prepare the source and presentation changes together in an immutable proposal. Reject or reprepare it if its bound source revision or captured presentation state changes during review, so acceptance cannot overwrite newer symbol movement. Keep `accept()` as one atomic publication and source-history entry. -4. Retain only compatible absolute positions of matched artifact nodes, including nested groups and descendants. Check port correspondence and connection references for identity correctness; saved positions remain node geometry. Invalid or partial semantic candidates must never publish. For valid source with an unsupported or ambiguous view mapping, report which affected layout will be invalidated during review and preserve other verified views. - -Keep workspace reconciliation independent of React, the DOM, canvas libraries and engine implementations. Engine adapters own evidence normalization; workspace modules own proposal validation and acceptance. Prefer the existing version-1 bundle shape when transient proposal evidence is sufficient. Any necessary saved-format change must be justified, versioned and covered by compatibility fixtures. - -Acceptance and evidence: - -- Use `examples/assembly/` and the actual pinned browser engines. Save distinct moved positions in `structure` and `devices`, then exercise definition and usage renames separately. Check every saved view, including one left unopened during the transaction. -- Verify exact intended source changes, resolved usage types, updated connection references and retained comments/unsupported source. Selecting repeated usages and inherited ports must identify the correct declaration and usage context after each rename. -- Assert compatible absolute positions after acceptance, view switching, undo, redo and a fresh page reload/import. Parent/descendant movement must not change ownership or borrow another usage's position. -- Cover Unicode before edited spans, malformed or mismatched edit evidence, stale proposals, geometry changes during review, missing/ambiguous correspondence and unsupported view evaluation. Required transformation or semantic-validation failures retain applied source, model, layout and history; invalid drafts retain their text and last valid diagram. -- Keep controlled workspace cases separate from real-engine browser evidence. Demonstrate that an important new retention/identity assertion fails with the targeted behavior deliberately broken, then restore it and rerun. -- Run `npm run check`, `npm run build` and the relevant browser scenarios. For workflow or appearance changes, follow the keyboard, focus and visual requirements in [browser testing](testing.md). Update capabilities and verification with actual outcomes and remaining gaps. - -### 2.2 Preserve layout through bounded source edits - -The source-apply implementation derives conservative replacement ranges and retains only uniquely matched declarations whose recorded headers, complete usage contexts and resolved owner/type bindings remain compatible. The existing unexposed-marker experiment is a starting fixture; it does not prove that edits in another file leave imports, type resolution, exposure, filters or other dependencies unchanged. - -Establish declaration and dependency continuity before retaining positions, then verify both views, repeated usages, undo/redo and fresh save/reopen. Retain explicit counterexamples for reordered declarations, reused names/spans, changed owners, file moves, retyping, additions and deletions. Define deliberate placement for new nodes and explicit invalidation for unmatched or unsafe mappings. Keep arbitrary replacements conservative until the evidence supports a broader policy. Step 2 is complete only when its broader acceptance criteria above are met. - -## 3. Complete one graphical authoring workflow - -Implemented bounded slice: create a typed part and connect compatible ports using the rendering and identity contracts above. Semantic commands remain in the framework-independent workspace; canvas and Inspector actions share them. - -Work: - -- Create a part usage in an explicit owner using an existing compatible definition. -- Create a supported connection between explicit port endpoints, with engine-backed validation. -- Preview the source transformation, validate the complete candidate, and accept one atomic transaction per command. -- Update source, diagram and Inspector together after acceptance. -- Preserve comments, unsupported constructs and unrelated source formatting. - -Acceptance: - -- An engineer can create the usage, connect supported ports, review/apply each change, and then continue editing source. -- The resulting source reparses and resolves through the pinned engines; applicable command postconditions hold. -- Invalid endpoints, invalid candidates and stale proposals leave the applied source/model unchanged and produce actionable feedback. -- Undo/redo and save/reopen reproduce the accepted source and compatible layout. -- Rejected edits and temporary invalid source retain the last valid diagram and the user's draft. -- Browser evidence covers the actual end-to-end workflow, not only controlled unit-test responses. - -The pinned Toolkit audit confirmed `insertMember` for typed parts and connections, including promotion of a typed semicolon owner. Toolkit accepts some directionally invalid links; the workspace therefore checks direction, canonical type and exact evaluated endpoints independently. Unsupported endpoint paths remain unavailable. [Authoring scope](architecture/graphical-authoring.md). - -## 4. Improve everyday editing - -The usability pass adds [resizable panels and Lucide action icons](architecture/workbench-layout.md), with browser-only arrangement preferences and preservation checks. - -CodeMirror 6 now supplies lexical SysML highlighting, line numbers, bracket matching/closing, indentation, current-file find/replace and local draft undo/redo. It is a workbench integration; semantic validity remains with the pinned engines. External file/source replacements reset local editor history; toolbar history continues to own accepted model revisions. - -Navigable diagnostics are implemented through the existing source editor. Each issue is bound to the exact source documents that produced it. Available file/line locations select the whole reported line; file-only locations open without selecting unrelated text. Missing files, out-of-range lines and rejected candidate snapshots are explained instead of targeting current draft text. Editing clears obsolete issues. Real-engine and controlled policy scenarios verify drafts, saved geometry, keyboard focus, undo/redo and reopen. See [source navigation](architecture/source-navigation.md). - -Engine-backed completion, hover and go-to-definition through the pinned Toolkit language server and explicit browser recovery are implemented. See the [verification record](verification.md) for the actual local checks and remaining coverage limits. The source editor's lexical mode supplies no semantic evidence. Bind each query to its full draft source set, reject stale results and unsupported partial edits, and retain project/view state on engine failures. Recovery uses a separate validated storage envelope, explicit restore/discard choice and atomic conflict checks. See [implementation policy](architecture/source-assistance-recovery.md). - -Alongside those tasks, prioritize usability issues observed while completing the assembly workflow: - -- Consistent selection across Explorer, diagram and Inspector, with explicit source/canvas reveal actions. -- Model-wide search and discoverable keyboard shortcuts. -- Clear applied/draft/modified/download feedback. -- Improve discoverability of the implemented completion, hover and definition shortcuts without conflating lexical highlighting with semantic validation. -- Extend recovery coverage only where observed failures justify it; retain explicit storage/versioning and conflict policies. - -Acceptance should describe a concrete task: navigate an issue, find a part, reveal its source, correct it and preserve the work. New editor or recovery dependencies require review; browser preferences, recovery data and portable project files have different responsibilities. - -## Keep views simple throughout - -Keep one list of named SysML views and a contextual **New view of selection** action. Existing views open directly. New views ask for a subject, representation and name; ownership, exposure and filters use advanced settings or source until richer controls earn their place. - -Source owns view declarations and semantic scope. Presentation owns symbol geometry; camera and editor arrangement are separate workspace concerns. Switching source files does not implicitly replace the open view. Avoid adding parallel view catalogues or requiring new declarations just to inspect an existing one. - -## Future integrations planned - -### OpenMBEE / Flexo MMS - -Planned beyond the current assembly milestone; not implemented or scheduled. Support editing both existing MMS models created by other tools and models first created in this editor. The intended workflow is **open project and branch → edit locally → review changes → publish a commit**, using Flexo's SysML v2 API as the primary semantic interface. - -Proposed sequence: - -1. Prove interchange with the pinned Toolkit against a disposable Flexo deployment. An unchanged import/export must preserve element identities, properties and references, including content outside the graphical editor's supported subset. -2. Add read-only checkout of a complete model at a fixed commit, with project/branch selection and explicit handling of generated source and unsupported content. -3. Enable one reviewed writeback command, initially rename, preserving repository element UUIDs. Require server-enforced rejection of stale writes and retain local drafts and the applied model on failure. -4. Extend to publishing models created here and further supported commands. Define versioned associations for exact source files and per-view presentation data; keep private invalid drafts and local undo history separate from published repository history. - -The first interoperability milestone should open an externally authored model, rename a part, publish it and reopen it in another client with the intended identities and references intact; repeat with a model created here. Authentication, deployment compatibility, source reconstruction, identity mapping and concurrency guarantees remain design questions. See the [Flexo MMS integration research](research/openmbee-flexo-mms.md) for primary sources, existing OpenMBEE client work and unresolved contracts. No live MMS round trip has been verified. - -## Scope and completion - -Broader behavior/sequence/geometry editing, simulation, collaboration services, deployment changes and wholesale legacy feature parity are outside this milestone. Record newly discovered gaps in [capabilities](capabilities.md); evaluate them against the target workflow before expanding scope. - -The milestone is complete when the assembly workflow above works through source and diagram, preservation/failure cases have retained fixtures, appropriate local checks pass, and documentation matches the supported behavior. Hosted CI, publication and upstream acceptance are separate evidence and decisions. - -Use the [contribution guide](../CONTRIBUTING.md), [workspace architecture](architecture/README.md), [project format](architecture/project-format.md) and [design system](design-system.md) when turning each bounded task into a change. Update this sequence when implementation evidence changes a dependency or priority. +Keep arbitrary source reconciliation conservative; general persistent identity, independent layout undo, camera persistence, reconnect/delete/duplicate and behavior editing remain gaps. Use the [authoring contract](architecture/graphical-authoring.md), [identity policy](architecture/identity-reconciliation.md) and [example targets](../examples/README.md) when extending them. Completed assembly milestones and earlier delivery sequences are retained in Git history, not repeated as pending work. diff --git a/docs/research/automatic-exploration-contract.md b/docs/research/automatic-exploration-contract.md new file mode 100644 index 0000000..9ccdae5 --- /dev/null +++ b/docs/research/automatic-exploration-contract.md @@ -0,0 +1,88 @@ +# Automatic exploration: pinned engine contract investigation + +Recorded 2026-11-01. This is a bounded investigation for the [exploration plan](../architecture/model-exploration.md), not an implemented application capability. No engine pins, production dependencies, worker protocols or project formats changed during this investigation. + +## Finding + +The pinned OpenSysML renderer already has a source-preserving `RenderExposed` entry point for rendering selected model elements without a view declaration. It is the preferred initial seam for a validated read-only exploration adapter because it uses the same parser, origins and rendering machinery as named views. It is not currently exposed through the application's combined WASM/worker contract. + +The actual Toolkit WASM also produces a whole-project graph without view declarations. That establishes an alternative projection capability, but its UUID graph identities and source locations do not satisfy the existing OpenSysML correspondence contract automatically. Switching renderers would require a separately verified identity join. + +## What the application exposes today + +- `SemanticEngine` in `src/lib/workspace/editing/index.ts` exposes `parse` and `evaluate(documents, modelHash, view, signal)`. +- `src/lib/engines/workers/schema.ts` allows only parse/evaluate semantic operations. The adapter in `src/lib/engines/opensysml/index.ts` validates an evaluation against the requested model hash and named-view identity. +- `semantic.worker.ts` reparses the complete requested source set, checks the model hash and calls combined WASM `EvaluateView`. +- `engine-bridge/opensysml/validated_view.go` checks semantic diagnostics before forwarding evaluation. `evaluate_view.go` then requires one unambiguous `SymbolViewUsage`; it builds selected subjects, renderer artifacts, source origins, complete parent contexts, port correspondence and explicit structural modifiers. +- `workspace.openView` independently rejects subjects that are not declared view usages. `moveSymbols` stores geometry in `project.presentations[view.view]`, so temporary exploration cannot safely be passed through this path unchanged. + +These are several separate boundaries. Adding a dropdown or passing a package name to the existing evaluate method does not implement automatic exploration. + +## Executed evidence + +The inputs contain two files and no view declarations: package P has a Motor definition, a System definition containing primary/standby usages, and a typed system usage; package Q has another definition/usage. Native renderer inputs also include an astral Unicode character before the declarations. Probes and raw outputs are retained locally in ignored `sandbox/exploration-contract/`. + +### Existing browser WASM hosted by Node + +`node sandbox/exploration-contract/wasm-probe.mjs` instantiated the current `static/vendor/opensysml/sysml-wasm.wasm` with its retained Go runtime and invoked the same `sysmlWasm.call` host entry point used by the worker. It did not run the browser worker or UI. + +| Request | Observed result | +| ------------------------------------------- | --------------------------------------------------------------- | +| ParseSources over the model without views | Parsed successfully | +| RenderView with `#tree:P` | Error code 12: RenderView is not served by combined sysml-wasm | +| EvaluateView with `#tree:P` | Invalid/unavailable; expected one unambiguous native view usage | +| EvaluateView with `P::System` | Same named-view requirement | +| ParseSources again over unchanged documents | Identical model hash | + +The prepared provenance identifies OpenSysML commit `08ef397841767b829e492bbecf6d723483b4421e`, Go 1.25.11 and the locally derived bridge binary. The full WASM replies are in `wasm-probe.json`. + +### Native pinned renderer + +A temporary Go overlay added only `TestExplorationContractProbe` to the cached pinned source's `internal/ir/view` test package. No source or overlay tracked by the application was modified. The test used the existing native test loaders and called `Renderer.RenderExposed` directly. + +| Scope and representation | Observed result | +| ---------------------------------------- | ---------------------------------- | +| Both package roots, tree | 9 nodes, 2 edges | +| Package P, tree | 6 nodes, 2 edges | +| Definition `P::System`, interconnection | 3 nodes: root, primary and standby | +| Typed usage `P::system`, interconnection | 1 node: the usage itself | + +All four cases passed. Rendered view names were empty, confirming these renderings do not name authored views. Nodes carried original document/span origins, including the distinct primary and standby declaration spans. The typed-usage result does not establish inherited-member expansion: it did not display the definition's two internals. Do not promise a complete internal-system picture merely because a top-level usage has a resolved type. + +The probe ran with the retained Go 1.25.11 binary and caches, `go test -overlay … ./internal/ir/view -run '^TestExplorationContractProbe$' -v`. Exact test/overlay inputs, `native-probe.json` and `native-probe.log` are retained in the probe directory. This is native engine evidence, not a production browser-path test. + +### Existing Toolkit WASM hosted by Node + +`node sandbox/exploration-contract/toolkit-probe.mjs` instantiated the pinned Toolkit WASM, constructed a `Session.fromSources` over the two simple source files, and called `toGraph('{}')`. It produced a graph with 9 nodes. `session.units()` was byte-for-byte unchanged after projection. + +The returned graph has `nodes`, `edges` and `view` fields; nodes include UUID IDs, qualified names, file/line/column locations, metaclasses, parents and compartment rows. These are different data and identity domains from current OpenSysML `EvaluatedView` objects. The probe used the simple source-only Session, not a prepared-library session, and did not prove library-dependent projections or browser integration. Raw evidence is in `toolkit-probe.json`. + +## Inspected source, beyond the executed probes + +The cached upstream source matches the pin recorded by `scripts/prepare-opensysml.mjs`: + +- [`internal/ir/view/view.go`](https://github.com/Open-MBEE/OpenSysML/blob/08ef397841767b829e492bbecf6d723483b4421e/internal/ir/view/view.go): `RenderExposed` renders explicit symbols without adding them or a view to the index. It has no declared-view identity. +- `internal/ir/view/resolve.go`: `RenderNamed` recognizes targeted pseudo-view syntax and forwards selected content to `RenderExposed`. It chooses the first match after lookup; a production exploration API must instead reject ambiguity explicitly. +- `internal/frontend/engine/engine.go`: raw engine RenderView uses that pseudo-view-aware resolver. The combined WASM server's dispatch does not expose raw RenderView, as the executed WASM probe confirms. +- `internal/ir/view/view.go`: the upstream renderer recognizes more representation families than the current application renders. This inspection is not application support evidence for states, actions or other families. +- The pinned Toolkit declaration file's `Session.toGraph` documents graph output and optional summary behavior. Only the default source-only graph was executed in this investigation; summary options and unsupported kinds were not exercised. + +## Recommended first implementation task + +Add a separate, validated read-only exploration operation around OpenSysML `RenderExposed`, initially limited to project/package tree and a proven bounded structural subject. Keep the existing named-view operation and saved geometry semantics intact. + +1. Define a workspace-owned exploration request/result contract distinguishing project roots, a package and an explicit element subject, plus the supported representation. Bind it to the applied source/model revision. A diagram layout mode such as Code/Split/Diagram is not a representation. +2. Extend combined bridge dispatch and worker transport deliberately. Validate the complete applied source set and selected subjects, reject missing/ambiguous/library-only targets, and call `RenderExposed` without source augmentation. Reuse the named-view bridge's origin/context and structural-notation normalization where valid rather than returning an unvalidated raw artifact. +3. Tag temporary exploration separately from named-view identity in workspace state. Share artifact rendering and selection correspondence, but keep transient geometry/camera outside portable named-view presentations. The first read-only slice must not invoke source commands or permit graphical mutation against an unproven exploration identity. +4. Select project roots from actual resolved project membership. For multiple systems or uncertain usage expansion, show the project/package overview and make the selected scope explicit. Establish system-root detection separately; filenames and first-package order are not evidence of a single system. +5. Add the actual pinned browser-worker path before advertising the feature. Cover multi-file roots, repeated usages, Unicode origins, definitions versus typed usages, cancelled/stale requests and renderer failures. Compare exact applied source, drafts, history and every saved named-view presentation before/after exploration. + +## Preservation risks and remaining proof + +- Renderer IDs such as n0 are local to a rendering and may repeat across scopes. Never reuse geometry solely by those IDs. A scope/representation/revision association must prevent one picture's positions from attaching to another subject. +- Declaration origins alone do not distinguish every occurrence when shared definitions are expanded. Preserve and verify complete usage contexts, consistent with the existing identity reconciliation policy. +- Pseudo-view strings are internal renderer syntax, not a source-backed view usage and not an automatic persistence key. Passing them into the current open/move/save path would blur that distinction. +- A raw render operation does not inherit the current validated bridge's semantic error gate, correspondence normalization or stale-source validation merely because the underlying renderer is shared. +- Creating a synthetic view in copied sources changes the parsed source set, model hash and potentially origins/selection. The direct renderer seam avoids that requirement; no synthetic-source fallback is justified by this investigation. +- Library-dependent input, large project bounds, empty roots, unresolved or anonymous subjects, ambiguous targets, imported exposures, effective inherited ports, and renderer-supported versus application-supported kinds need explicit retained fixtures. +- No application code was changed, and no new browser scenario, saved-data migration, graphical command, shell screen or hosted CI run was executed for this document. Root check/build suites were not repeated for this research-only change; native/WASM probe results above are the executed evidence. diff --git a/docs/research/openmbee-flexo-mms.md b/docs/research/openmbee-flexo-mms.md index 7e7ef7a..9003474 100644 --- a/docs/research/openmbee-flexo-mms.md +++ b/docs/research/openmbee-flexo-mms.md @@ -26,7 +26,7 @@ Source reviewed at `b173564276814ca84007915852a3a80fb637bcd5` shows repository r The [workspace architecture](../architecture/README.md) makes applied source authoritative, keeps drafts separate, and validates complete semantic proposals before acceptance. The [version-1 bundle](../architecture/project-format.md) preserves exact applied documents, drafts and compatible presentation positions. An MMS graph checkout supplies semantic elements; it may have no original source files to recover. Generated authoring text must therefore be identified as generated, with a demonstrated mapping back to repository identities. -The application's normalized model and rendered symbols are insufficient as full SysML interchange. Names, source spans and renderer counters are revision-scoped. They must not become MMS element UUIDs. The [identity investigation](../architecture/engine-identity-sources.md) records the separate identity domains and lack of a demonstrated Toolkit/OpenSysML identity join. +The application's normalized model and rendered symbols are insufficient as full SysML interchange. Names, source spans and renderer counters are revision-scoped. They must not become MMS element UUIDs. The [identity investigation](../architecture/identity-reconciliation.md) records the separate identity domains and the bounded source-edit join; that join does not establish general repository UUID preservation. There is already a promising local seam: the pinned Toolkit `0.10.10` wrapper exposes `Session.fromInterchangeJson`, `toCompactJson`, `toFullJsonStrict` and `toFullJson`. Its comments explicitly mention Flexo change records. These methods are not currently exposed through the application's worker contract, and comments are not conformance evidence. Inspect the prepared wrapper under `static/vendor/sysml-toolkit/`; [preparation](../../scripts/prepare-sysml-toolkit.mjs) and the [engine guide](../../engine-bridge/README.md) own pin/checksum provenance. Test this existing capability before adding another parser service or dependency. diff --git a/docs/testing.md b/docs/testing.md index 3bd9d30..4fe119a 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -30,7 +30,7 @@ The [example catalog](../examples/README.md) supplies independent source project ## Test outcomes -`default-example.spec.ts` checks the actual startup project: Diagram mode, the pneumatic assembly’s exact parts/ports/connections, fourteen named views, all nine unchanged source files and independent file/view selection. `ready()` waits for this real-engine startup; focused history and palette tests explicitly import the smaller Workshop fixture through `openWorkshop()`. The example-library scenario exercises all fourteen compressor views and exact source download/reopen, including unavailable renderer messages. +`default-example.spec.ts` explicitly loads the built-in project and checks: Diagram mode, the pneumatic assembly’s exact parts/ports/connections, fourteen named views, all nine unchanged source files and independent file/view selection. `ready()` enters `/app`, explicitly chooses **Load example**, then waits for the real-engine model; `waitForStartup()` is only a readiness assertion, and reload scenarios choose their fixture deliberately; focused history and palette tests explicitly import the smaller Workshop fixture through `openWorkshop()`. The example-library scenario exercises all fourteen compressor views and exact source download/reopen, including unavailable renderer messages. The [identity investigation](architecture/identity-reconciliation.md) runs the compiled production workers directly to compare model/view identities across source revisions. Run `npm run test:e2e -- --project=functional identity.spec.ts` for the source-change matrix and Toolkit UTF-8 splice/correspondence probe. These are worker/SDK contract checks; they do not assert that the UI retains layout after applying source. Full source inputs, evaluations, edit reports and candidate mappings are retained as JSON attachments in each scenario's output directory. @@ -46,12 +46,16 @@ Run `npm run test:e2e -- --project=functional diagnostics.spec.ts` for real-engi Run `npm run test:e2e -- --project=functional language-assistance.spec.ts recovery.spec.ts` for the source assistance and recovery workflows. Language scenarios use the pinned Toolkit server for symbol information, cross-file definitions with CRLF/Unicode offsets and completion followed by real-engine application and source history. Held and failed engine assets exercise cancellation, retained drafts and a successful fresh query after the fault is removed. Recovery scenarios read the actual IndexedDB record, restore exact drafts and saved layouts after reload, discard malformed envelopes explicitly, retry denied storage and retain both live drafts during a competing-tab conflict. The recovery dialog also exercises keyboard focus containment/return and the persisted enabled preference. Additional cases verify that recovered clean geometry stays unsaved until download, discarded drafts are removed from the durable record, and a real-engine rejected restore retains the current project and pending saved copy. See [storage and query policies](architecture/source-assistance-recovery.md). +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. 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. +`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. ## Investigate failures and review baselines diff --git a/docs/verification.md b/docs/verification.md index d6d669c..d65b1cb 100644 --- a/docs/verification.md +++ b/docs/verification.md @@ -1,289 +1,53 @@ # Verification record -## Source assistance and recovery checkpoint +## Mobile header integration fix -Recorded 2026-11-01 on `feature/source-assistance-recovery`, using Node 26.3.1, npm 11.16.0 and the pinned browser engines. No production dependencies, engine pins or portable project fields changed. +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. -- `npm run check`: passed formatting, architecture, TypeScript, 118 Vitest tests and 5 Node tests. -- Fresh production builds passed through the focused Playwright runs, retaining the existing bundle-size warning. -- `language-assistance.spec.ts`: all 4 actual-engine scenarios passed in `sandbox/playwright/language-integration-options`. -- `recovery.spec.ts`: all 9 scenarios passed across `sandbox/playwright/recovery-integration`, `recovery-integration-remaining`, `recovery-integration-clean` and `recovery-integration-validation`; see the final full-suite record for the combined run. +`npm run test:e2e -- --project=functional toolbar.spec.ts appearance.spec.ts --output=sandbox/playwright/mobile-header-functional --reporter=list --max-failures=1` passed all eight scenarios after the fix. The regression covers 390, 600 and 601 px, toolbar separation, keyboard Appearance dismissal/focus return, Projects/Resume focus return and an exact project-bundle comparison. Red evidence: `sandbox/cleanup-design/mobile-header-before.log` and `sandbox/playwright/mobile-header-before`. -Queries retain the full draft source set, raw CRLF/Unicode ranges and project-only target policy. Real-engine completion is applied and undone/redone; held and failed actual library assets verify stale cancellation, exact retained work and retry. Recovery checks compare durable IndexedDB payloads, both view layouts, invalid drafts, active view, explicit discard, storage denial, competing-tab conflicts, persisted settings and keyboard focus containment/return. Restored clean geometry stays modified until download and requires replacement confirmation. A real-engine rejected recovery retains the current bundle and stored record. Discarded drafts disappear from the durable recovery record. +Four new header captures at 390/600 px in light/dark were inspected. The normal comparison run, `npm run test:e2e -- --project=visual --output=sandbox/playwright/mobile-header-visual-final --reporter=list --max-failures=1`, passed all 47 comparisons without baseline updates, retries or skips. `npm run check` passed, including 118 Vitest tests and six Node checks; fresh production builds passed through these browser runs with the existing bundle-size warning. -Deliberate faults removed the stale-completion guard, startup-record protection, clean-transition clearing and restored-unsaved baseline. Each targeted assertion failed; restored code passed. Logs: `sandbox/language-stale-fault.log`, `sandbox/recovery-pending-fault.log`, `sandbox/recovery-clean-fault.log`, `sandbox/recovery-unsaved-fault.log`. Controlled storage/asset failures establish failure-policy behavior; successful semantic operations still use the real pinned engines. +[Validate run 41336740488](https://github.com/Open-MBEE/sysml_view_editor/actions/runs/41336740488) passed all 128 browser scenarios for the preceding head `ddd5196`. That hosted result predates this mobile fix; its new full CI run is separate. Firefox/WebKit and a complete accessibility audit were not run for this fix. -`npm run test:e2e -- --project=visual --output sandbox/playwright/editor-workflows-visual-full --reporter=list`: all 33 normal visual comparisons passed. Eight new recovery-dialog/symbol-information captures cover desktop/narrow widths in both themes. Fifteen existing baselines changed intentionally for the Local recovery header action; pixel comparisons confirm every difference is within y=23–39, with content below the header unchanged. New images and before/after/diff sheets were inspected. Evidence: `sandbox/language-recovery-new-contact-sheet.jpg`, `sandbox/recovery-header-comparison-1.jpg`, `sandbox/recovery-header-comparison-2.jpg`, `sandbox/recovery-header-diff-evidence.json`. +## Shell cleanup and structural card checkpoint -The final full functional run is in progress; its result will be recorded separately. Firefox/WebKit and complete accessibility auditing remain outside this checkpoint. Hosted CI is separate from local evidence. +Recorded 2026-11-01 on `feature/openmbee-shell`, using Node 26.3.1, npm 11.16.0 and the existing pinned browser engines. The private package is now `@openmbee/sysml-view-editor`; dependency versions, engine pins and portable project fields are unchanged. TypeScript now checks unused locals/parameters; two unused imports were removed. -## Navigable diagnostics checkpoint +`npm run check` passed formatting, architecture, the stricter TypeScript configuration, 118 Vitest tests and 6 Node checks. Fresh production builds passed through the browser runs, retaining the existing bundle-size warning. -Recorded 2026-11-01 on `feature/source-diagnostics`; commit `a4c544b`. +Shell rendering is split into `ProjectHeader` and `SourceToolbar`, with local file construction/import/download in `projectFiles.ts`. The Workbench retains session lifetime, project replacement policy and semantic orchestration. CSS is grouped by surface through `ui/theme.css`. Before the diagram redesign, `npm run test:e2e -- --project=visual --output=sandbox/playwright/cleanup-css-equivalence --reporter=list --max-failures=1` passed all 39 existing visual comparisons without updates, establishing visual equivalence for that refactor. -- `npm run check`: passed formatting, architecture, TypeScript, 87 Vitest tests and 5 Node tests. -- `npm run build`: passed, with the existing bundle-size warning. -- `npm run test:e2e -- --project=functional diagnostics.spec.ts preservation.spec.ts --max-failures=1 --output sandbox/playwright/diagnostics-final-functional --reporter=list`: 8 passed. -- `npm run test:e2e -- --project=visual --output sandbox/playwright/diagnostics-final-visual --reporter=list --max-failures=1`: all 25 passed. +Structural cards now have palette-tinted headers, kind badges, distinct name/type hierarchy, aligned port rows, 12 px square handles, explicit direction text, selection outline/halo and quieter containers. Renderer node dimensions and saved positions retain their existing contract. Scoped selection styles override React Flow's focus outline reset. The markup retains the original `name : Type` text format as well as accessible names. -The first diagnostic scenario uses the real engine. Three controlled normalized-response scenarios establish missing/file-only/stale-candidate policy. Four new desktop/narrow light/dark diagnostic captures were inspected; existing 21 baselines stayed unchanged at this checkpoint. A deliberate removal of the diagnosed-text equality guard caused the stale-location unit regression to fail; the restored implementation and focused browser checks passed. Evidence: `sandbox/diagnostic-stale-fault.log`, `sandbox/diagnostic-visual-review.jpg` and the browser output directories above. The other functional scenarios were not rerun locally for this checkpoint. +The new actual-engine `diagram-style.spec.ts` checks name/type emphasis, selection outline while focused, keyboard port selection with its resolved declaration/usage context, pump-to-tank endpoint alignment and an exact bundle comparison across appearance changes. It failed against the original combined header markup, then passed with the redesign. Red evidence: `sandbox/cleanup-design/diagram-style-before.log` and `sandbox/playwright/diagram-style-before`. Three assembly scenarios passed; the final focused diagram/entry run passed all 11 scenarios in 53.5 seconds under `sandbox/playwright/cleanup-design-focused-final`, including source DOM/selection, drafts, history, camera, both layouts, cancellation and superseded requests. -## CodeMirror source editor checkpoint +Seventeen affected/new visual scenarios generated baselines in `sandbox/playwright/cleanup-design-visual-review`. Nine existing images intentionally change only for the structural notation; four selected-node images add desktop/narrow light/dark coverage. All before/after/diff sheets and new captures were inspected, alongside running-app desktop/narrow captures. Evidence: `sandbox/cleanup-design/comparison-1.jpg` through `comparison-3.jpg`, `selected-review.jpg` and `visual-diff-evidence.json`. Baseline generation is separate from normal comparison results. -Recorded 2026-11-01 on `feat/source-editor`, using Node 26.3.1, npm 11.16.0, locked Linux Chromium and the existing pinned browser engines. The approved CodeMirror dependencies are pinned in the npm manifest/lockfile; engine pins and the portable project format are unchanged. +`npm run test:e2e -- --project=functional --output=sandbox/playwright/cleanup-design-functional --reporter=list --max-failures=1`: all 85 scenarios passed in one fresh-build run (11.3 minutes), without retries or skips. This preceded the final foreground-only contrast adjustment, whose focused results are recorded separately. -The editor provides lexical SysML highlighting, line numbers, bracket matching/closing, brace indentation, current-file find/replace and local draft undo/redo. External file/source replacements reset local history; busy operations retain the view and block edits. A resize-callback check also repairs the blank strip left by the hidden pane when changing from Split to a single-editor mode. +The extended direction-contrast assertion failed before the final foreground adjustment (3.1858:1 in light/neutral), then passed across all six palettes in light/dark. `npm run test:e2e -- --project=functional diagram-style.spec.ts appearance.spec.ts --output=sandbox/playwright/diagram-contrast-final --reporter=list --max-failures=1`: five passed in 42.9 seconds after that adjustment. Measured OUT-label contrast is at least 6.77:1 across the twelve combinations. Evidence: `sandbox/cleanup-design/contrast-before.log`, `contrast-final.log`, `palette-before.log`, `palette-final.log` and the inspected `palette-review.jpg`. This tests the direction text, not complete accessibility conformance. -| Command/check | Observed result | -| ------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| `npm run check` | Passed formatting, architecture, TypeScript, 83 Vitest tests and 5 Node script tests | -| `npm run build` | Passed standalone and through fresh Playwright production builds; the existing bundle-size warning remains | -| `npm audit --audit-level=low` | Passed; 0 vulnerabilities | -| `npm run test:e2e -- --project=functional source-editor.spec.ts --max-failures=1` | All 7 editor scenarios passed, including desktop/narrow single-editor sizing and split restoration | -| `npm run test:e2e -- --max-failures=1` | 57 functional and 5 visual scenarios passed; the first New part visual comparison found 8 changed pixels at its rounded corner, so 15 later visual scenarios did not run | -| `npm run test:e2e -- --project=visual --output sandbox/playwright/source-editor-final-visual --reporter=list` | All 21 visual scenarios passed without snapshot updates or skips (57 functional + 21 visual scenarios covered across the two normal runs) | -| Visual review | Inspected all 4 new source/search captures and the 8 changed split-panel/assembly/form PNGs; light/dark and desktop/narrow source layouts are covered | +`npm run test:e2e -- --project=visual --output=sandbox/playwright/cleanup-design-visual-final --reporter=list --max-failures=1`: all 43 normal visual comparisons passed in 2.5 minutes after the final contrast adjustment, without baseline updates, retries or skips. -The editor regressions verify exact text through actual select-all/copy commands, including a 1,500-line virtualized draft. Portable downloads separately establish raw source and CRLF preservation. Find/replace is applied through the real engines, with accepted-source undo/redo; file switching, discard, local undo and a fresh reopen retain their explicit outcomes. Holding an actual OpenSysML asset request verifies blocked typing/undo during pending application. Existing semantic checks retain exact accepted sources, resolved usage identity and compatible per-view geometry through save/reopen. +Firefox/WebKit, complete accessibility auditing and large-model performance remain outside this checkpoint. Local results do not establish hosted CI. Ignored `sandbox/` evidence may be absent from other checkouts. -A deliberate fault removed the raw-UTF-16/CRLF-to-editor position conversion. The CRLF/Unicode browser regression failed at its source-selection assertion: expected `part primary : Motor`, received `primary : Motor;` plus whitespace. The original helper was restored and the seven-scenario focused run passed afterward. The fault log and original helper are in ignored `sandbox/source-editor-fault.log` and `sandbox/source-text-before-fault.ts`; failure traces/screenshots remain under `sandbox/playwright/source-editor-fault-results/`. +## Development watcher regression -Baseline generation was separate from comparison. Four changed panel PNGs show CodeMirror in Split mode and two changed assembly PNGs show the corrected Diagram area after a mode change. Two existing desktop New part captures changed only at the rounded corner where the source underlay is visible through the backdrop: inspected pixel deltas greater than 20 were confined to a 7 × 4 px corner. The dialog dimensions, content and controls are unchanged; the other six authoring captures were identical. The four new PNGs show source/search in Code mode. All 78 defined browser scenarios are covered across the normal functional/full run and the final complete visual comparison. The initial normal-run failure trace, actual and diff are retained in `sandbox/playwright/results/visual-structural-authoring-dialogs-in-light-visual/`. The final visual run has its own output directory; the HTML report remains under `sandbox/playwright/report/`. +The reported Linux `ENOSPC` arose while Vite watched generated engine sources under `sandbox/opensysml-build`. The development configuration now excludes `sandbox/` and prepared `static/vendor/` binaries without disabling source watching or changing operating-system limits. `node --test scripts/vite-watch.test.mjs` exercises the actual Vite watcher: the original configuration failed because sandbox directories were watched; the fixed configuration passed, with source watching retained and both generated trees excluded. Evidence: `sandbox/shell-session/vite-watch-before.log` and `vite-watch-after.log`. -These are local results. Hosted CI, Firefox/WebKit, complete screen-reader/accessibility coverage and large-model performance were not established by this checkpoint. Semantic completion, hover, definition navigation, formatting and inline diagnostics remain future work; navigable Issues are the next bounded task. +`npm run dev -- --host 127.0.0.1 --port 4176 --strictPort` started successfully after engine preparation. HTTP smoke checks returned 200 for the landing page, `/src/main.tsx` and the OpenSysML WASM asset. The dev server remained running without the reported watcher error. Excluding vendor binaries from watching does not prevent serving them; regenerated binaries require a page reload. -## Model ownership tree checkpoint +## Retained regression evidence -Recorded 2026-10-30 on `feat/clean-foundation`. Explorer's Model list now projects recorded `ownerId` relationships into a collapsible tree. Rows retain full declaration/type identifiers, show kind/type labels and use shared shadcn Button/IconButton controls. Native tree/group semantics and one roving tab stop support visible-row arrows, Home/End, type-ahead and explicit Enter/Space selection. Diagram selection reveals the owner chain without taking focus, including a second click on the same symbol after folding its branch. No engine pin, semantic transaction or portable-project format changed. +The latest browser coverage above includes the existing identity, authoring, diagnostics, source assistance and recovery suites. Earlier deliberate-fault records explain why critical preservation checks exist: -`npm run check` passed formatting, architecture, TypeScript, 77 Vitest tests and 5 Node script tests. Fresh Playwright web-server builds passed with the existing chunk-size warning. `npm run test:e2e -- --project=functional model-tree.spec.ts workspace.spec.ts panels.spec.ts preservation.spec.ts authoring.spec.ts assembly.spec.ts --max-failures=1 --output sandbox/playwright/tree-workflows-results --reporter=list` passed all 27 scenarios in 4.2 minutes. These include the three new real-engine tree paths and the affected existing selection, authoring, preservation and panel workflows. +| Invariant tested | Deliberate fault and observed failure | Local evidence | +| --------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------- | +| Live workbench preservation | Unmounted the workbench when opening the launcher; source-editor DOM identity assertion failed, restored implementation passed | `sandbox/shell-session/launcher-lifetime-fault.log`, `sandbox/playwright/shell-lifetime-restored` | +| Draft query freshness | Removed the stale-completion guard; targeted regression failed, restored code passed | `sandbox/language-stale-fault.log` | +| Recovery preservation | Removed startup-copy protection, clean clearing or restored-unsaved baseline; each targeted assertion failed, restored code passed | `sandbox/recovery-pending-fault.log`, `sandbox/recovery-clean-fault.log`, `sandbox/recovery-unsaved-fault.log` | +| Issue/source binding | Removed diagnosed-text equality guard; stale-location regression failed, restored code passed | `sandbox/diagnostic-stale-fault.log` | +| Raw source navigation | Removed UTF-16/CRLF translation; source-selection regression failed, restored editor scenarios passed | `sandbox/source-editor-fault.log` | -Visual comparison: `npm run test:e2e -- --project=visual --output sandbox/playwright/tree-visual-final-results --reporter=list` passed all 17 scenarios in 1.2 minutes. The four new tree captures were then scoped to the visible Explorer viewport, excluding content outside its scrolling boundary. Desktop/narrow light/dark images were inspected and regenerated separately; the final normal comparison `npm run test:e2e -- --project=visual --grep 'model ownership tree' --max-failures=1 --output sandbox/playwright/tree-capture-final-results --reporter=list` passed all four in 19.4 seconds without snapshot updates. The other 13 visual baselines are unchanged. This pass covers 27 functional and 17 distinct visual scenarios against the restored production code. - -The tree cases distinguish identically named action parameters by actual owner and resolved type; retain invalid drafts, source-file choice, applied view, camera and exact project data through browsing; exercise focus separately from selection; and verify repeated diagram selection. Existing model-selection helpers expand hidden branches explicitly and assert `aria-selected`; semantic source, occurrence identity and saved-position assertions remain intact. - -The deliberate regression runner `python3 sandbox/tree-faults/run.py` removed the UI's repeated-selection reveal request. The first diagram selection revealed the usage; after folding its owner, the second heading click failed the intended selected-usage assertion. The retained trace records two pump-heading clicks. The runner recorded `observedExit: 1` and `restored: true`; restored source digest and original source are retained with `sandbox/tree-faults/outcome.json`. Failure evidence is in `sandbox/playwright/tree-fault-results/`. - -An initial test selected the pump's inlet button by clicking the node's centre, as confirmed by its trace and Inspector identity. The test now clicks the visible pump heading to select the intended usage. Evidence remains in `sandbox/playwright/tree-selection-setup-results/`. A screenshot setup race also exposed the panel-options popover restoring focus after the tree was focused; the trace showed Explorer panel options active and the branch still closed. The setup now waits for that focus return before navigating the tree. Evidence remains in `sandbox/playwright/tree-visual-focus-results/`. Neither correction relaxes a source, identity, focus or geometry assertion. - -The unrelated example-library, worker-identity, appearance and default-startup functional suites were not rerun for this UI slice. Firefox/WebKit, hosted CI, complete accessibility/screen-reader coverage and large-model performance were not run. Declared members are shown under their recorded owners; inherited members are not cloned beneath typed usages, and Explorer selection does not add a new source-range contract. - -## Public-content review - -Recorded 2026-10-30. Reviewed all 186 tracked files and, before cleanup, 265 reachable Git blobs (32 binary). Pattern scans found no absolute home-directory paths, private-key/token markers, authenticated credential URLs or quoted credential assignments. Tracked PNGs contained no text or EXIF metadata chunks. This was a targeted local content review, not a comprehensive security audit. - -Removed a personal requester name and workstation-specific context/SSH instructions from current documentation. Contributor instructions now describe portable environment checks, and README names the current repository. Required copyright/provenance notices and standard Git author/committer attribution are retained. Earlier document versions remain in Git history; no history rewrite was performed. Ignored local artifacts and local Git/SSH configuration are outside the tracked repository. - -The final scan of all 186 tracked files found no personal/workstation-reference, home-path or credential-marker matches. Commit messages contained no home-path or credential-marker matches. URL hosts were public services or the documented loopback development address. `npm run check` passed formatting, architecture, TypeScript, 73 Vitest tests and 5 script tests; `npm run build` passed with the existing chunk-size warning. Browser checks were not rerun for these documentation-only changes. - -## Panels, icons and richer startup checkpoint - -Recorded 2026-10-30 on `feat/clean-foundation`. The usability pass adds shadcn-based resizable panels, shared Lucide icon buttons, contextual panel edge rails and measured toolbar alignment. The startup project is now [Smart air workshop](../examples/air-compressor/README.md), opening Pneumatics in Diagram mode. These are local checks; hosted CI and publication are separate. - -| Command/check | Observed result | -| ------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- | -| `npm run check` | Passed formatting, architecture, TypeScript, 73 Vitest tests and 5 Node script tests | -| `npm run build` through fresh Playwright web-server starts | Passed prepared-engine verification, TypeScript and static build; existing chunk-size warning remains | -| `npm run test:e2e -- --max-failures=1` | 36 functional scenarios passed; stopped at the tooltip setup failure explained below | -| `npm run test:e2e -- --project=visual --update-snapshots --output sandbox/playwright/toolbar-visual-results --reporter=list` | 13 scenarios generated/reviewed in 53.9 seconds; generation is separate from comparison | -| `npm run test:e2e -- panels.spec.ts toolbar.spec.ts workspace.spec.ts visual.spec.ts --max-failures=1 --output sandbox/playwright/ui-final-results --reporter=list` | Final comparison: 29 passed in 2.8 minutes, including 16 functional and all 13 visual scenarios, without snapshot updates or skips | - -Together the 36 passing functional scenarios and final 29 cover all 60 defined browser scenarios (47 functional, 13 visual), with the five panel scenarios repeated against the final CSS. The final run includes the corrected tooltip setup, wide/narrow alignment, single-editor spacing/wrapping and all remaining workspace cases. Earlier semantic/engine scenarios were retained after the final CSS-only adjustment; the first full command did not finish green. Final artifacts are in `sandbox/playwright/ui-final-results/` and its command log is `sandbox/final-browser.log`. - -Changed captures were inspected for the startup workbench at desktop/narrow sizes, nested assembly diagrams and resized panels in both themes; the Appearance narrow capture was also reviewed. The final comparisons retain the inspected baselines. Cropping after resizing preserves the current camera; Fit view remains an explicit action. - -The startup scenario checks six exact engine node names (the assembly and five components), thirteen ports, four named connections, fourteen view declarations, unchanged download of all nine source files and independent source-file/view selection. The catalog adds all fourteen views with explicit targets and gaps. Six structural/definition/tree views render partial representations; eight behavior/sequence/geometry views report unavailable renderers. Engine evaluation does not imply application rendering, simulation, verification or conformance. All nine imported source files were byte-compared with the legacy model; provenance and license are retained in NOTICE. - -Panel scenarios cover pointer and keyboard resizing, button alternatives, minimums, collapse/restore and focus, preference reload/reset, blocked/corrupt storage and invalid-draft preservation. The real-engine assembly path compares exact sources/drafts, compatible rendered and saved geometry in both views, selection, source DOM identity and camera state. Toolbar scenarios check accessible names/tooltips, source history, zoom/fit/lock, actual pane/group centre lines and SVG centring. Focused Workshop history/palette scenarios explicitly import the small fixture rather than treating it as startup. - -Two deliberate faults established that the regressions detect their intended failures before production code was restored: - -- `python3 sandbox/panel-faults/run.py`: removing the fit-once guard made divider resizing change the camera; the preservation scenario failed its camera comparison. The runner restored the original file and recorded `observedExit: 1` and `restored: true`. -- `python3 sandbox/panel-faults/run-centering.py`: moving source commands 40 px off centre made the toolbar assertion fail at 39.71875 px against its unchanged 1 px limit. The runner restored the original file and recorded `observedExit: 1` and `restored: true`. - -An initial full run exposed an existing saved-coordinate boundary problem: fractional ancestor offsets could put a valid child a floating-point rounding increment beyond its owner, discarding its saved point on reopen. Three of five added fractional-offset cases failed before the fix. Commit `6359216` makes owner-bound validation allow a `1e-7` numerical epsilon; a point `0.001` outside remains rejected. All ten structure tests and the three real-engine assembly browser scenarios passed after the fix. Portable saved-coordinate equality and browser assertion tolerances were retained. - -The first run after switching startup exposed a palette test that still expected Workshop immediately after reload. Its retained trace showed the correct Smart air workshop startup. Palette tests now explicitly reimport their Workshop fixture after startup, and the project-reopen test waits for the new startup before importing its saved bundle; their original source, color and coordinate assertions are retained. The failure trace remains in `sandbox/playwright/default-migration-results/`, with its log in `sandbox/default-migration-browser.log`. - -The subsequent full run passed 36 functional scenarios, then stopped at a tooltip test: startup setup had already clicked and focused Split, so calling `focus()` again did not produce a new focus event. Its trace showed this sequence. The test now focuses source before focusing Split, retaining its tooltip text, Escape dismissal and focus-return assertions. That run also exposed a separately measured 10 px overlap between Diagram and Workspace layout in single-editor mode. Separate columns with an 8 px gap fix it; constrained desktop editors wrap those groups to another row. The toolbar test checks both separation and wrapping. Its original failure trace and the 36 passing scenarios remain in `sandbox/playwright/results/`, with the log in `sandbox/pre-toolbar-browser.log`. - -A follow-up alignment run caught a 29 px source-action offset at a 768 px viewport: the compact flex row fitted history beside source commands. The compact source group now takes a full row, keeping its centre aligned with the pane. The original 1 px assertion is unchanged. Evidence is retained in `sandbox/playwright/toolbar-narrow-results/` and `sandbox/toolbar-narrow-browser.log`. - -Fault inputs, original sources, logs and restoration records are in ignored `sandbox/panel-faults/`; failure traces/screenshots are in `sandbox/playwright/panels-fault-results/` and `sandbox/playwright/toolbar-fault-results/`. The exploratory direct-worker model capture is `sandbox/air-compressor-evidence.json`, separate from the application tests. Snapshot generation and visual comparison are separate checks; intentional baselines represent the new startup project, controls and panel layout. - -Firefox/WebKit, hosted CI, complete accessibility/screen-reader coverage and large-model performance were not run. Keyboard/focus checks and image inspection do not establish complete accessibility. No engine pins or portable project format changed. - -## Assembly editing milestone checkpoint - -Recorded 2026-10-29 on `feat/clean-foundation`, after the identity investigation commit `fd54b67`. The working tree implements reviewed definition/usage rename preservation, bounded source-edit preservation and typed part/port-connection authoring. These are local results for the supported assembly subset; no new commit, hosted CI result or general SysML conformance is implied. - -Environment: Node 26.3.1, npm 11.16.0, locked Playwright with bundled Linux Chromium and the existing pinned/prepared engines. No dependencies, engine pins, checksums, bridge overlays or version-1 project format changed. - -| Command/check | Observed result | -| ------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `npm run check` | Passed formatting, architecture, TypeScript, 64 Vitest tests and 5 Node script tests | -| `npm run build` (standalone and Playwright web-server builds) | Passed prepared-engine verification, TypeScript and the static build; the existing chunk-size warning remains | -| `npm run test:e2e -- --max-failures=1` | 45 passed in 5.1 minutes: all 37 functional scenarios and the then-current 8 visual scenarios, without snapshot updates or skips | -| `npm run test:e2e -- --project=visual --output sandbox/playwright/visual-final-results --reporter=list` | Final comparison after adding narrow dark-theme coverage: all 9 visual scenarios passed in 31.3 seconds, without snapshot updates or skips | -| Changed/new baseline inspection | Inspected the 5 changed workbench/assembly PNGs and all 8 new part/connection form PNGs, including desktop and narrow forms in both themes. Complete selected endpoints wrap below narrow selectors; the narrow workbench retains its existing scrollable layout | -| Deliberate position-transfer fault, checked by the definition-rename regression using `sandbox/milestone-faults/playwright.config.ts` | Deliberately disabling position transfer failed at the intended retention assertion: structure retained 0 instead of 5 positions and devices retained 0 instead of 3. Original code was restored; subsequent focused and full normal runs passed | - -The 37 functional and final 9 visual scenarios cover all 46 currently defined browser scenarios across the two normal commands above. Baseline generation for the added narrow dark case was a separate review step; generation is not counted as a successful comparison. - -The preservation scenarios check exact accepted source, updated resolved bindings/references, full repeated-usage identity, saved coordinates in both views, actual rendered coordinates, undo/redo and fresh reload/import. They cover definition and usage renames separately, Unicode prefix insertion, an unexposed declaration edit in another file, invalid draft retention and explicit invalidation after usage reordering. Controlled reconciliation cases separately reject file/owner/type changes, deleted/replaced declarations and ambiguous correspondence without lending new symbols another usage's position. - -The authoring workflow creates an existing-definition-typed usage, moves it independently in both views, connects its inherited output to the intended other usage's input, and checks actual evaluated endpoints through history and fresh import. A second creation path promotes a typed semicolon owner while preserving surrounding source. Duplicate-name refusal, injected Toolkit asset failure/recovery, cancelled reviews and cancellation during preparation preserve source, layout and history. Keyboard checks cover focus containment, trigger return and workspace fallback when acceptance removes the original port trigger. - -An earlier reopen assertion exposed CSSOM transform rounding: saved `974.911688741722` becomes computed CSS `974.912`, a difference of `0.000311258278`. The helper now normalizes expected coordinates through the same browser serialization before applying its unchanged `0.0001` threshold; portable-project coordinates still require exact equality. The isolated reproduction is retained in `sandbox/coordinate-precision.json`. This is a test representation correction, not a renderer-coordinate change or widened tolerance. - -The full HTML report and retained engine inputs/evaluations are in ignored `sandbox/playwright/report/` and `sandbox/playwright/results/`; the final visual comparison uses `sandbox/playwright/visual-final-results/`. Fault configuration, original source and restoration outcome remain in `sandbox/milestone-faults/`; the trace, screenshot and failed assertion are in `sandbox/playwright/milestone-fault-results/` with the earlier fault report in `sandbox/playwright/milestone-fault-report/`. Isolated pinned-SDK insertion/direction probes are retained under `sandbox/graphical-authoring-audit.*` and `sandbox/owned-insertion-audit.*`; they are contract evidence separate from application tests. - -The security audit, Firefox/WebKit, hosted CI, complete accessibility/screen-reader coverage and large-model performance were not run for this milestone. Conjugated/undirected or ambiguous connection paths, general subtype compatibility, arbitrary persistent identity, manual overlap avoidance and parent resizing remain unsupported. [Capabilities](capabilities.md), [preservation policy](architecture/identity-reconciliation.md), [authoring scope](architecture/graphical-authoring.md) and [next steps](next-steps.md) describe the bounds and follow-up work. - -## Commit preparation - -Recorded 2026-10-30. Refreshed the README's explicit port direction/path limits and the roadmap's completed milestone wording. The repository context, Git identity, SSH agent and remote were verified on `feat/clean-foundation`. - -`npm run check` passed formatting, architecture, TypeScript, all 64 Vitest tests and 5 Node script tests. `npm run build` passed engine preparation and the static build with the existing chunk-size warning. Only documentation changed after the assembly checkpoint's final runtime/browser verification; its 37 functional and 9 visual results above were retained rather than rerun. Hosted CI and other browsers were not run for commit preparation. - -## Identity investigation checkpoint - -Recorded 2026-10-26 after the foundation commit. The [identity investigation](architecture/identity-reconciliation.md) and its [source-contract notes](architecture/engine-identity-sources.md) cover source-revision comparisons and the conditions for matching repeated usages. This pass adds documentation and worker/SDK browser checks; automatic layout reconciliation is not implemented. - -Environment: Node 26.3.1, npm 11.16.0, the locked Playwright installation and bundled Chromium, with the existing pinned/prepared browser engines. - -| Command/check | Observed result | -| ----------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `npm run check` | Passed formatting, architecture and TypeScript checks, 44 Vitest tests and 5 Node script tests | -| `npm run build` | Passed prepared-engine verification, TypeScript and static build; the existing main-chunk size warning remains | -| `npm run test:e2e -- --project=functional identity.spec.ts --max-failures=1 --output sandbox/playwright/identity-results --reporter=list` | Final normal run: 2 passed in 47.3 seconds, no skipped tests | -| `npm run test:e2e -- --config sandbox/identity-faults/playwright.config.ts --grep 'investigates identity' --max-failures=1` | Deliberately faulted copy failed at the distinct-usage-context assertion: expected 3 contexts, received 1. The normal spec was unchanged by this fault; its subsequent normal run passed | - -The first scenario captures 11 source sets and both assembly views for each. The second records actual Toolkit UUID changes and filename-keyed UTF-8 byte splices, verifies exact replay against the transformation worker's sources, and joins all 15/10 view correspondences through transported origins and complete usage contexts. It also checks the existing rename postconditions. JSON inputs, models, evaluations, provenance, edit reports and matches are retained under `sandbox/playwright/identity-results/`. - -The fault experiment erases repeated-port context arrays in an isolated copied assertion helper after receiving real engine replies. This checks the regression assertion's sensitivity; it is not an engine defect or evidence of application layout retention. Its copied spec/config, trace and report remain under `sandbox/identity-faults/`, `sandbox/playwright/identity-fault-results/` and `sandbox/playwright/identity-fault-report/`. - -Full UI/visual suites, the security audit, Firefox/WebKit and hosted CI were not rerun for these documentation/test-only changes. No production source, dependencies, engine pins, project formats or visual baselines changed. Earlier full-suite results remain historical records below. - -## Foundation commit checkpoint - -Recorded 2026-10-26. The foundation, structural rendering slice and example library are committed in `a2d0d91` (`feat: establish SysML v2 view editor foundation`) on `feat/clean-foundation`. The working tree was clean immediately after that commit. - -The pre-commit verification used Node 26.3.1 and npm 11.16.0: - -- `npm run check` passed formatting, architecture and TypeScript checks, 44 Vitest tests and 5 Node script tests. -- `npm run build` passed prepared-engine verification, TypeScript and the static build. Vite reported the existing chunk-size warning; the main JavaScript chunk was approximately 2.10 MB before gzip. -- Browser scenarios and the security audit were not rerun for this commit. Their earlier recorded results appear below and remain historical evidence. - -The commit was created locally; no push, hosted CI run or deployment was performed in this checkpoint. The sections below retain the checks reported during development before the initial commit, including their original scope and limitations. - -## Initial foundation - -Recorded 2026-10-26 against the uncommitted `feat/clean-foundation` working tree. These are local results, not hosted CI or publication evidence. - -Environment: Node 26.3.1, npm 11.16.0, React/React DOM 19.3.1, TypeScript 6.0.3, Vite 8.2.4, TypeBox 1.4.2, React Flow 12.12.0, Playwright 1.64.1 and installed Google Chrome through Playwright's `chrome` channel. - -| Command/check | Observed result | -| ------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `npm run check` | Passed formatting, architecture import/platform rules and TypeScript checks; 32 Vitest tests and 2 Node architecture-rule tests passed | -| `npm run engines:prepare` | Verified prepared Toolkit assets; built the derived OpenSysML WASM with pinned Go 1.25.11; retained combined bridge tests passed; upstream engine package reported no test files | -| `npm run build` | Passed; static output written to ignored `build/` | -| `PLAYWRIGHT_BROWSER_CHANNEL=chrome npm run test:e2e -- --max-failures=1` | 8 passed in 24.6 seconds on the final run after the shadcn/appearance integration; see the scenarios below | -| `npm audit --audit-level=low` | Zero reported vulnerabilities | -| `git diff --check` | No tracked-diff whitespace findings; formatting also checked owned untracked source | -| Browser capture review | Desktop and narrow captures retained in ignored `sandbox/captures/`; capture session reported no page errors | - -The browser scenarios exercise the actual WASM engines: - -1. Load source-defined views; switching source files retains the selected view. -2. Retain invalid drafts and the previous valid diagram, then discard drafts. -3. Prepare/apply a Toolkit part-definition rename, inspect updated usage references and undo the source transaction. -4. Create a native view declaration through a reviewed source transaction and open it. -5. Move a symbol, save a project with an unfinished draft and reopen its sources/drafts/presentation. -6. Switch all six palettes in both themes, follow live system changes only when selected, and keep appearance out of model history/project serialization. -7. Retain/reset custom muted colors across palette changes and reload; dismiss a dialog with Escape and restore focus to its opener. -8. Apply appearance settings when browser storage is denied, with a visible persistence message. - -An earlier browser run exposed decorative glyphs in accessible button names. Labels were corrected; the final five-scenario run passed. No tests were removed or skipped to obtain that result. - -The appearance pass added three browser scenarios and retained the original five. Visual captures of Light, Dark, the Appearance dialog and a 768 px viewport are retained under `sandbox/captures/design-system-*.png`. The capture session reported no page errors. It exposed fixed-height/centered shadcn navigation rows; the workbench now uses content-sized, left-aligned rows. The development server was restarted after installing the Tailwind plugin to clear its earlier missing-module state. - -## Remaining limits - -- Vite reports a chunk-size warning: the main JavaScript bundle is approximately 2.09 MB before gzip, including the graph/layout stack and UI primitives. Further loading/layout-worker improvements need measurement; no performance budget or large-model benchmark was verified. -- Browser checks used desktop Chrome. Other browsers, narrow-screen editing parity, screen-reader behavior and complete keyboard accessibility were not certified. Narrow captures are visual inspection only. -- Automatic browser recovery, source-language assistance, compound/port notation, other renderer families and broader semantic commands remain documented in capabilities. -- The source/model/engine contract is tested for this initial slice; this does not establish full SysML conformance. -- Hosted CI, deployment, Git commits and pushes were not performed. - -## Frontend regression strengthening - -Recorded 2026-10-26 after the initial foundation results above, against the same uncommitted feature branch. These are local results on Ubuntu 24.04.5 with Node 26.3.1, npm 11.16.0, Playwright 1.64.1 and its bundled Chromium 156.0.8078.4. See [the browser testing workflow](testing.md) for reproduction and baseline review. - -| Command/check | Observed result | -| ------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `npm run check` | Passed formatting, architecture and TypeScript checks; 32 Vitest and 2 Node tests passed | -| `npm run build` | Passed engine asset verification, TypeScript and the production build; the existing main-chunk size warning remains | -| `npm run test:e2e -- --max-failures=1` | 15 passed in 1.1 minutes: 12 functional scenarios and 3 visual scenarios; no tests skipped | -| Visual baseline review | Inspected all six new Linux Chromium PNGs: light/dark workbench and expanded Appearance dialog, plus 768 px workbench and keyboard-focused dialog; the final run compared them without updating snapshots | -| `npm run test:e2e -- --config sandbox/fault-checks/playwright.config.ts --grep 'downloads and reopens\|workbench and appearance dialog in light'` | Two deliberately faulted copies failed at the intended assertions: missing imported positions restored the default coordinates instead of the saved coordinates; forced magenta node fills produced a screenshot mismatch. The production tests and baselines were unchanged by these experiments | - -The strengthened scenarios verify both resolved rename references and exact source undo/redo, cancellation without source/history changes, independent positions in two named views through a reload/import round trip, retained invalid drafts and diagram identities, repeated-usage selection/source ranges, engine asset failure with retained work and successful retry, rendered custom fills, explicit versus system theme behavior, and keyboard focus wrapping/return. - -The repeated-usage test exposed an existing focus defect: clicking **Open source** in an already-open file retained focus on the button. The reveal action now reissues the recorded range so the editor focuses it; the original failing assertion passes after the fix. An early position test also caught a test synchronization problem: an old view's nodes can remain visible while the next view evaluates. The helper now waits for the requested view to be active and no longer busy before interacting with its symbols. - -The final normal HTML report is in ignored `sandbox/playwright/report/`. Deliberately failing experiment traces, screenshots and the visual diff are retained separately in `sandbox/playwright/fault-results/` and `sandbox/playwright/fault-report/`; the isolated copied specs/config are under `sandbox/fault-checks/`. The CI workflow is configured to retain browser evidence for seven days, but hosted execution and artifact upload were not performed here. - -No dependencies, engine pins or project formats changed. Firefox/WebKit, automated accessibility scanning, complete keyboard/screen-reader coverage and large-model performance checks remain unverified. The narrow screenshot records the existing scrollable layout; it does not establish narrow-screen editing parity. - -## First structural rendering slice - -Recorded 2026-10-26 after the frontend regression work above, on the same uncommitted feature branch and Linux/Node/npm/Chromium environment. The shared source fixture and scope are documented in [structural views](architecture/structural-views.md). - -| Command/check | Observed result | -| ------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `npm run check` | Passed formatting, architecture and TypeScript checks; 44 Vitest tests and 2 Node architecture-rule tests passed | -| `npm run build` | Passed asset/provenance verification, TypeScript and static build; the existing main-chunk size warning remains | -| `npm run test:e2e -- --max-failures=1` | Final normal run: 20 passed in 1.3 minutes, with no skipped tests. Includes 15 functional scenarios and 5 visual scenarios comparing eight reviewed baselines without snapshot updates | -| Pinned Go 1.25.11: `go test -count=1 ./internal/frontend/combined ./internal/frontend/engine` | Passed from the prepared source with local build/module caches; combined tests include the shared assembly fixture. The upstream engine package reported no test files | -| Pinned Go 1.25.11: `go test -count=1 -run TestStudioStructuralAssembly -v ./internal/frontend/combined` | The dedicated structural contract test passed: nested ownership, explicit inherited-port directions, reference modifier, resolved endpoints, distinct usage contexts and unsupported-action notice | -| Isolated browser fault checks | Two copied scenarios deliberately failed: shifting source-port handles away from the SVG endpoint failed the physical endpoint assertion, and constraining the canvas width failed the diagram-expansion/refit assertion. Normal production tests were unchanged by those experiments | - -The three new functional browser scenarios load `examples/assembly/` through the real pinned workers. They check nested bounds, reference notation, explicit direction labels, actual SVG/port attachment points, keyboard selection/source reveal for shared inherited ports with distinct contexts, moving an assembly plus a child, switching views and fresh reload/import with absolute positions and exact source retention. The devices view reports the exposed unsupported action. The two new assembly snapshots cover light and dark in diagram-only mode. - -Visual review exposed stale split-mode zoom after expanding the diagram pane. The canvas now refits when its available dimensions change; the new assertion checks that the assembly grows into the expanded pane. Existing six workbench/dialog baselines passed unchanged. Endpoint assertions use the outward handle edge where React Flow attaches the line, not the handle center. - -The original and updated raw engine traces are in ignored `sandbox/assembly-before-trace.json` and `sandbox/assembly-trace.json`; the latter includes generated provenance. Deliberately failing scenarios and reports are isolated under `sandbox/structure-faults/`, `sandbox/playwright/structure-fault-results/` and `sandbox/playwright/structure-fault-report/`. They were run with: - -```sh -npm run test:e2e -- --config sandbox/structure-faults/playwright.config.ts --grep 'real port endpoints|canvas refit fault' -``` - -Upstream pins/checksums/licenses and the version-1 project shape remain unchanged. The bridge/builder/input digests and derived WASM were regenerated, including the shared example sources in preparation's digest. No dependencies were added. Source revisions still invalidate layout; graphical part/connection authoring, broader owned-port/conjugation notation, modeled geometry/routes, performance budgets, other browsers and full accessibility remain outside verified scope. Hosted CI, commits and pushes were not performed. - -## Visualization example library - -Recorded 2026-10-26 in the same local environment and uncommitted feature branch. The [library index](../examples/README.md) is generated from a TypeBox-validated catalog. It contains 12 independent projects, 24 source files and 19 named views: 6 scoped as rendered, 6 partial and 7 with unavailable application renderers. All 19 evaluate as complete/available engine artifacts; application availability is checked separately. - -| Command/check | Observed result | -| ---------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `npm run examples:index` and `node scripts/examples-index.mjs --check` | Generated the index and confirmed synchronization with catalog targets/support/gaps | -| `npm run check` | Passed formatting, architecture and TypeScript checks; 44 Vitest tests and 5 Node checks passed, including example inventory, metadata invariants and generated-index synchronization | -| `npm run test:examples -- --max-failures=1` | Catalog checks passed; all 12 dedicated browser scenarios passed in 1.5 minutes | -| `npm run test:e2e -- --max-failures=1` | All 32 scenarios passed in 3.8 minutes, with no skipped tests; includes 27 functional and 5 visual scenarios. Existing eight visual baselines passed without updates | -| `npm run build` | Production build and prepared-engine verification passed; the existing chunk-size warning remains | - -Library browser checks import every project, select every cataloged view, observe real worker replies without replacing them, check exact node-name/edge-kind multisets, compare rendered edge counts, verify expected availability/notices, and download/reopen exact applied/draft sources. Repeated motor names in the parts fixture are selected by engine correspondence and checked against their distinct qualified identities. Comment/documentation text includes Unicode and remains byte-equivalent in the exported source strings. - -Current per-view PNGs and engine JSON are retained under `sandbox/playwright/results/examples-example-library-*/` and attached to the normal HTML report. Inspected captures include comments, the package outline and unavailable package graph, nested parts and definition relationships, conjugated ports, unavailable use-case rendering and the valid empty view. The catalog records visible tree-label overflow, incomplete relationship notation and port-label routing alongside missing comment callouts and renderer families. These captures are current-state evidence, not new golden comparisons or certification that all visual targets are met. - -An isolated copy removed the `satisfy usableCapacity by storage` relationship before import. The source still evaluated, but the expected requirement graph lost its storage node; the catalog assertion failed as intended. The original source fixtures were untouched. The normal requirements scenario then passed again. Commands: - -```sh -npm run test:e2e -- --config sandbox/example-library-fault/playwright.config.ts --grep 'example library: requirements' -npm run test:e2e -- --project=functional examples.spec.ts --grep 'example library: requirements' --output sandbox/playwright/library-restored-results --reporter=list -``` - -The first command deliberately failed; the second passed. Fault evidence is isolated in `sandbox/playwright/library-fault-results/` and `sandbox/playwright/library-fault-report/`; the restored scenario retained its captures separately. The normal 32-scenario HTML report remains in `sandbox/playwright/report/`. - -No production dependencies, engine pins, project formats or application renderers changed for the library. Package/case/requirement/action/state/sequence rendering, comment callouts and the recorded partial-notation gaps remain development targets. Firefox/WebKit, hosted CI and complete accessibility were not checked in this pass. +Historical hosted evidence: [Validate run 41314513339](https://github.com/Open-MBEE/sysml_view_editor/actions/runs/41314513339) passed for code head `f708b84`. That result does not establish CI for the later shell branch. Older milestone commands, counts and captures are available in Git history; they do not describe the current test inventory. Local artifacts under ignored `sandbox/` may be absent from other checkouts. diff --git a/examples/air-compressor/README.md b/examples/air-compressor/README.md index 4905d33..f7a26f4 100644 --- a/examples/air-compressor/README.md +++ b/examples/air-compressor/README.md @@ -1,8 +1,8 @@ # Smart air workshop -This is the editor's startup project. Its nine SysML files and 14 named views model the compressor's electrical and pneumatic assemblies, cooling, two installed compressor usages, tools, operator/technician roles, requirements, behaviour and enclosure geometry. +This is the built-in project opened by **Load example** in the launcher or **Try an example** on the landing page. Its nine SysML files and 14 named views model the compressor's electrical and pneumatic assemblies, cooling, two installed compressor usages, tools, operator/technician roles, requirements, behaviour and enclosure geometry. -The default opens **pneumatics** in Diagram mode: pump, tank, regulator, relief valve and pressure sensor, with 13 displayed ports and four authored connections. **Split** reveals the source; other named views remain available in the sidebar. +Loading the example opens **pneumatics** in Diagram mode: pump, tank, regulator, relief valve and pressure sensor, with 13 displayed ports and four authored connections. **Split** reveals the source; other named views remain available in the sidebar. | File | Features | | -------------------- | -------------------------------------------------------------------------------------------------------- | diff --git a/index.html b/index.html index 6196ef2..f91345e 100644 --- a/index.html +++ b/index.html @@ -5,9 +5,9 @@ - SysML View Editor + OpenMBEE View Editor | SysML v2 workspace

diff --git a/package-lock.json b/package-lock.json index 4b2f017..b2c7ccc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,11 +1,11 @@ { - "name": "@dsg/sysmlv2-view-editor", + "name": "@openmbee/sysml-view-editor", "version": "0.1.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "@dsg/sysmlv2-view-editor", + "name": "@openmbee/sysml-view-editor", "version": "0.1.0", "license": "Apache-2.0", "dependencies": { diff --git a/package.json b/package.json index 884baaf..2ca09d9 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "name": "@dsg/sysmlv2-view-editor", + "name": "@openmbee/sysml-view-editor", "version": "0.1.0", "private": true, "type": "module", diff --git a/scripts/vite-watch.test.mjs b/scripts/vite-watch.test.mjs new file mode 100644 index 0000000..4c59cd0 --- /dev/null +++ b/scripts/vite-watch.test.mjs @@ -0,0 +1,43 @@ +import assert from 'node:assert/strict'; +import { mkdir, mkdtemp, rm } from 'node:fs/promises'; +import { setTimeout } from 'node:timers/promises'; +import { resolve, sep } from 'node:path'; +import test from 'node:test'; +import { createServer } from 'vite'; + +// Exercise Vite's real watcher: generated engine checkouts and test artifacts +// must never consume watchers or trigger reloads, while app source stays live. +test('dev watcher excludes generated directories and retains source watching', async () => { + // Keep this meaningful on a fresh checkout without prepared engines. + const fixtures = []; + for (const directory of ['sandbox', 'static/vendor']) { + await mkdir(directory, { recursive: true }); + fixtures.push(await mkdtemp(resolve(directory, 'watch-probe-'))); + } + const server = await createServer({ + server: { middlewareMode: true, hmr: false }, + logLevel: 'silent', + }); + try { + const deadline = Date.now() + 30_000; + while (!server.watcher.getWatched()[resolve('src')]) { + assert.ok(Date.now() < deadline, 'source watcher must become ready'); + await setTimeout(20); + } + const watched = Object.keys(server.watcher.getWatched()); + for (const directory of ['sandbox', 'static/vendor']) { + const generated = resolve(directory); + assert.equal( + watched.some( + (path) => path === generated || path.startsWith(generated + sep), + ), + false, + `${directory} must be excluded from development watching`, + ); + } + assert.ok(watched.includes(resolve('src'))); + } finally { + await server.close(); + await Promise.all(fixtures.map((path) => rm(path, { recursive: true }))); + } +}); diff --git a/src/lib/engines/opensysml/semantic.worker.ts b/src/lib/engines/opensysml/semantic.worker.ts index e1a7496..b0f748d 100644 --- a/src/lib/engines/opensysml/semantic.worker.ts +++ b/src/lib/engines/opensysml/semantic.worker.ts @@ -1,8 +1,4 @@ -import type { - SourceDocument, - Diagnostic, - ModelElement, -} from '$lib/workspace/project'; +import type { Diagnostic, ModelElement } from '$lib/workspace/project'; import type { EvaluatedView } from '$lib/workspace/views'; interface GoRuntime { importObject: WebAssembly.Imports; diff --git a/src/lib/ui/AppearanceSettings.tsx b/src/lib/ui/AppearanceSettings.tsx index cddf655..8d2bcc3 100644 --- a/src/lib/ui/AppearanceSettings.tsx +++ b/src/lib/ui/AppearanceSettings.tsx @@ -4,7 +4,14 @@ import { NativeSelectOption, } from '$lib/ui/primitives/native-select'; import { Input } from '$lib/ui/primitives/input'; -import { useEffect, useLayoutEffect, useState } from 'react'; +import { + createContext, + useContext, + useEffect, + useLayoutEffect, + useState, + type ReactNode, +} from 'react'; import { Button } from './primitives/button'; import { Dialog } from './Dialog'; import { @@ -23,8 +30,7 @@ import { } from './appearance'; // This module owns browser preferences; no model/session operation is involved. -export function AppearanceSettings() { - const [open, setOpen] = useState(false); +function useAppearancePreferences() { const [appearance, setAppearance] = useState(() => { try { return loadAppearance(window.localStorage); @@ -74,6 +80,27 @@ export function AppearanceSettings() { } } + return { appearance, update, saveFailed }; +} +const AppearanceContext = createContext | null>(null); +export function AppearanceProvider({ children }: { children: ReactNode }) { + const preferences = useAppearancePreferences(); + return ( + + {children} + + ); +} +export function AppearanceSettings({ active = true }: { active?: boolean }) { + const [open, setOpen] = useState(false); + const preferences = useContext(AppearanceContext); + if (!preferences) throw new Error('AppearanceProvider is required.'); + const { appearance, update, saveFailed } = preferences; + useEffect(() => { + if (!active) setOpen(false); + }, [active]); return ( <> ))} diff --git a/src/lib/workbench/entry/Application.tsx b/src/lib/workbench/entry/Application.tsx new file mode 100644 index 0000000..9c3adf6 --- /dev/null +++ b/src/lib/workbench/entry/Application.tsx @@ -0,0 +1,75 @@ +import { lazy, Suspense, useEffect, useLayoutEffect, useState } from 'react'; +import { Landing } from './Landing'; +const Workbench = lazy(() => + import('../shell/Workbench').then((module) => ({ + default: module.Workbench, + })), +); +export type EntryRequest = { id: number; kind: 'launcher' | 'example' }; + +export function Application() { + const [inApp, setInApp] = useState(() => + ['/app', '/app/'].includes(window.location.pathname), + ); + const [started, setStarted] = useState(inApp); + const [entry, setEntry] = useState(() => ({ + id: 1, + kind: + new URL(window.location.href).searchParams.get('example') === '1' + ? 'example' + : 'launcher', + })); + useLayoutEffect(() => { + // An example link expresses one opening action, not an instruction to replace + // recovery with the example every time the page is reloaded. + if ( + inApp && + (window.location.pathname !== '/app' || + new URL(window.location.href).searchParams.has('example')) + ) + window.history.replaceState(null, '', '/app'); + const pop = () => { + const app = ['/app', '/app/'].includes(window.location.pathname); + setInApp(app); + if (app) setStarted(true); + }; + window.addEventListener('popstate', pop); + return () => window.removeEventListener('popstate', pop); + }, []); + useEffect(() => { + document.title = inApp + ? 'Workspace | OpenMBEE View Editor' + : 'OpenMBEE View Editor | SysML v2 workspace'; + }, [inApp]); + function open(example = false) { + window.history.pushState(null, '', '/app'); + setEntry((previous) => ({ + id: previous.id + 1, + kind: example ? 'example' : 'launcher', + })); + setStarted(true); + setInApp(true); + } + function home() { + window.history.pushState(null, '', '/'); + setInApp(false); + } + return ( + <> + {!inApp && } + {started && ( + + )} + + ); +} diff --git a/src/lib/workbench/entry/Brand.tsx b/src/lib/workbench/entry/Brand.tsx new file mode 100644 index 0000000..8f8c01b --- /dev/null +++ b/src/lib/workbench/entry/Brand.tsx @@ -0,0 +1,28 @@ +import { Button } from '$lib/ui/primitives/button'; + +export function Brand({ onHome }: { onHome: () => void }) { + return ( + + ); +} +export const repository = 'https://github.com/Open-MBEE/sysml_view_editor'; diff --git a/src/lib/workbench/entry/Landing.tsx b/src/lib/workbench/entry/Landing.tsx new file mode 100644 index 0000000..f5c1d57 --- /dev/null +++ b/src/lib/workbench/entry/Landing.tsx @@ -0,0 +1,110 @@ +import { useLayoutEffect, useRef } from 'react'; +import { ArrowUpRight, ArrowRight } from 'lucide-react'; +import { Button } from '$lib/ui/primitives/button'; +import { AppearanceSettings } from '$lib/ui/AppearanceSettings'; +import { Brand, repository } from './Brand'; + +export function Landing({ onOpen }: { onOpen: (example?: boolean) => void }) { + const heading = useRef(null); + useLayoutEffect(() => { + heading.current?.focus(); + }, []); + function link(event: React.MouseEvent, example = false) { + if ( + event.button !== 0 || + event.metaKey || + event.ctrlKey || + event.shiftKey || + event.altKey + ) + return; + event.preventDefault(); + onOpen(example); + } + return ( +
+ + Skip to content + +
+ heading.current?.focus()} /> + +
+
+
+
+ Sandbox +

+ OpenMBEE +
+ View Editor +

+

SysML v2 modeling workspace

+
+
+

+ Write SysML, explore connected views, and understand your system. +

+

+ Work with source, inspect named views, and review graphical + changes in one local workspace. +

+ +
+
+
+
+

Start with a model

+

+ Open SysML source files or a project bundle. Explore the built-in + example, or start with a new source project. +

+ + Read the guide +
+
+

Built to learn with you

+

+ This is a sandbox. Features and interfaces are still evolving. + Keep downloaded copies of your work and share reproducible + feedback. +

+ + Share feedback +
+
+

Open source, with attribution

+

+ The application is Apache-2.0 licensed. Bundled engines and + libraries retain their own licenses and notices. +

+ +
+
+
+
+ OpenMBEE + Source, model and presentation, connected. +
+
+ ); +} diff --git a/src/lib/workbench/entry/ProjectLauncher.tsx b/src/lib/workbench/entry/ProjectLauncher.tsx new file mode 100644 index 0000000..84be980 --- /dev/null +++ b/src/lib/workbench/entry/ProjectLauncher.tsx @@ -0,0 +1,144 @@ +import { useLayoutEffect, useRef, type ReactNode } from 'react'; +import { FilePlus2, FolderOpen, PanelsTopLeft, ArrowRight } from 'lucide-react'; +import { Button } from '$lib/ui/primitives/button'; +import { AppearanceSettings } from '$lib/ui/AppearanceSettings'; +import { Brand } from './Brand'; + +export function ProjectLauncher({ + onHome, + onExample, + onNew, + onOpen, + onResume, + projectName, + busy, + error, + recovery, + recoveryAction, +}: { + onHome: () => void; + onExample: () => void; + onNew: () => void; + onOpen: () => void; + onResume: () => void; + projectName?: string; + busy: boolean; + error: string; + recovery: ReactNode; + recoveryAction: ReactNode; +}) { + const heading = useRef(null); + useLayoutEffect(() => { + heading.current?.focus(); + }, []); + return ( +
+
+ + +
+
+
+
+

+ Open a project +

+

+ Choose a starting point. Your model stays in this browser until + you download it. +

+
+ {projectName && ( + + )} +
+ {projectName && ( +

+ Current project: {projectName}. Returning here + keeps its drafts, history and diagram in this session. +

+ )} + {recovery} + {error && ( +

+ {error} +

+ )} + {busy && ( +

+ Opening project… +

+ )} +
+ + + +
+
+

Local work, explicit saves

+

+ Opening imports a copy. Download project keeps source, drafts and + diagram positions together. Local recovery is a separate browser + copy. +

+
+
+ Folder and repository workflows +

+ Direct folder saving and Flexo project/branch checkout are planned. + Use source files or a project bundle to work locally today. +

+
+
+
+ OpenMBEE View Editor + Sandbox +
+
+ ); +} diff --git a/src/lib/workbench/recovery/RecoveryControls.tsx b/src/lib/workbench/recovery/RecoveryControls.tsx index 2a68134..7a1ff8b 100644 --- a/src/lib/workbench/recovery/RecoveryControls.tsx +++ b/src/lib/workbench/recovery/RecoveryControls.tsx @@ -132,6 +132,7 @@ export interface RecoveryControlsProps { /** Confirm replacement, open, then call recovery.acceptRestored synchronously before awaiting view setup. */ onRestore: (entry: RecoveryEntry) => Promise; busy?: boolean; + active?: boolean; } function RecoveryActions({ recovery, onRestore, busy }: RecoveryControlsProps) { @@ -237,6 +238,9 @@ export function RecoveryBanner(props: RecoveryControlsProps) { export function RecoveryButton(props: RecoveryControlsProps) { const { recovery } = props; const [open, setOpen] = useState(false); + useEffect(() => { + if (props.active === false) setOpen(false); + }, [props.active]); return ( <> +
+ + {state.project?.name ?? 'Workspace'} + {state.dirty ? ' · modified' : ''} + + {origin} +
+ + + ); +} diff --git a/src/lib/workbench/shell/SourceToolbar.tsx b/src/lib/workbench/shell/SourceToolbar.tsx new file mode 100644 index 0000000..acf364d --- /dev/null +++ b/src/lib/workbench/shell/SourceToolbar.tsx @@ -0,0 +1,108 @@ +import { + Undo2, + Redo2, + Check, + X, + CodeXml, + Columns2, + Network, +} from 'lucide-react'; +import { IconButton } from '$lib/ui/IconButton'; +import { Button } from '$lib/ui/primitives/button'; +import type { WorkspaceState } from '$lib/workspace/createWorkspace'; +import { WorkbenchToolbar } from './WorkbenchPanels'; +type Mode = 'code' | 'split' | 'diagram'; + +export function SourceToolbar({ + state, + mode, + onMode, + onUndo, + onRedo, + onDiscard, + onApply, +}: { + state: Pick; + mode: Mode; + onMode: (mode: Mode) => void; + onUndo: () => void; + onRedo: () => void; + onDiscard: () => void; + onApply: () => void; +}) { + return ( + + + + + } + sourceActions={ +
+ + {state.busy + ? 'Working…' + : state.unapplied + ? 'Draft not applied' + : 'Source applied'} + + + +
+ } + modes={ +
+ {(['code', 'split', 'diagram'] as const).map((value) => ( + onMode(value)} + /> + ))} +
+ } + /> + ); +} diff --git a/src/lib/workbench/shell/Workbench.tsx b/src/lib/workbench/shell/Workbench.tsx index 4b66334..65c604f 100644 --- a/src/lib/workbench/shell/Workbench.tsx +++ b/src/lib/workbench/shell/Workbench.tsx @@ -1,29 +1,13 @@ import { WorkbenchLayout, WorkbenchPanels, - WorkbenchToolbar, IssuesToggle, } from './WorkbenchPanels'; -import { - Undo2, - Redo2, - FilePlus2, - FolderOpen, - Download, - Check, - X, - CodeXml, - Columns2, - Network, -} from 'lucide-react'; -import { IconButton } from '$lib/ui/IconButton'; -import { Button } from '$lib/ui/primitives/button'; -import { AppearanceSettings } from '$lib/ui/AppearanceSettings'; -import { useEffect, useRef, useState } from 'react'; +import { useEffect, useLayoutEffect, useRef, useState } from 'react'; import { createBrowserSession } from './session'; import { exampleProject, exampleView } from './example'; import type { Workspace, WorkspaceState } from '$lib/workspace/createWorkspace'; -import { readProject, type ProjectBundle } from '$lib/workspace/persistence'; +import type { ProjectBundle } from '$lib/workspace/persistence'; import type { EditProposal, SemanticCommand } from '$lib/workspace/editing'; import type { ModelElement, Diagnostic } from '$lib/workspace/project'; import { Explorer } from '../explorer/Explorer'; @@ -39,6 +23,15 @@ import { RecoveryBanner, } from '../recovery/RecoveryControls'; import type { RecoveryEntry } from '../recovery/envelope'; +import { ProjectLauncher } from '../entry/ProjectLauncher'; +import { ProjectHeader } from './ProjectHeader'; +import { SourceToolbar } from './SourceToolbar'; +import { + createLocalProject, + readProjectFiles, + downloadProject, +} from './projectFiles'; +import type { EntryRequest } from '../entry/Application'; const initial: WorkspaceState = { project: null, @@ -55,7 +48,22 @@ const initial: WorkspaceState = { layoutNotices: [], }; type Mode = 'split' | 'code' | 'diagram'; -export function Workbench() { +export function Workbench({ + active, + entryRequest, + onHome, +}: { + active: boolean; + entryRequest: EntryRequest; + onHome: () => void; +}) { + const [readySession, setReadySession] = useState(null); + const [launcherOpen, setLauncherOpen] = useState(true); + const [everOpened, setEverOpened] = useState(false); + const [origin, setOrigin] = useState('Local project'); + const launcherTrigger = useRef(null); + const handledEntry = useRef<{ session: Workspace; id: number } | null>(null); + const resumed = useRef(false); const ref = useRef(null), picker = useRef(null); const languageRef = useRef< @@ -63,12 +71,13 @@ export function Workbench() { >(null); const workspaceSurface = useRef(null); const reviewGeneration = useRef(0); + const actionGeneration = useRef(0); const [state, setState] = useState(initial), [file, setFile] = useState(exampleProject.documents[0]!.name), [selected, setSelected] = useState(''), [treeSelectionRequest, setTreeSelectionRequest] = useState(0), [error, setError] = useState(''), - [message, setMessage] = useState('Opening example project…'); + [message, setMessage] = useState('Choose a project to begin.'); const [symbolSelection, setSymbolSelection] = useState<{ view: string; modelHash: string; @@ -106,6 +115,9 @@ export function Workbench() { setFocusRange(undefined); setMode('split'); setMessage('Local recovery restored.'); + setOrigin('Browser recovery'); + setEverOpened(true); + setLauncherOpen(false); await refreshView(session, entry.activeView ?? undefined); } const element = state.model?.elements.find((item) => item.id === selected); @@ -151,18 +163,8 @@ export function Workbench() { languageRef.current = language; ref.current = session; const unsubscribe = session.subscribe(setState); - let closed = false; - void session - .open(exampleProject) - .then(() => refreshView(session, exampleView)) - .then(() => { - if (!closed) setMessage('Project opened locally.'); - }) - .catch((reason) => { - if (!closed) setError(String(reason)); - }); + setReadySession(session); return () => { - closed = true; unsubscribe(); session.dispose(); if (ref.current === session) { @@ -171,23 +173,81 @@ export function Workbench() { } }; }, []); + useEffect(() => { + if ( + !readySession || + ref.current !== readySession || + (handledEntry.current?.session === readySession && + handledEntry.current.id === entryRequest.id) + ) + return; + handledEntry.current = { session: readySession, id: entryRequest.id }; + setLauncherOpen(true); + if (entryRequest.kind === 'example') void run(loadExample); + }, [readySession, entryRequest]); + useEffect(() => { + if (!active) { + reviewGeneration.current++; + setDialog(null); + setProposal(undefined); + } + }, [active]); + useLayoutEffect(() => { + if (launcherOpen || !active) return; + if (resumed.current) launcherTrigger.current?.focus(); + else workspaceSurface.current?.focus(); + resumed.current = false; + }, [launcherOpen, active]); + function resume() { + resumed.current = true; + setError(''); + setLauncherOpen(false); + } + async function loadExample() { + if (!replaceAllowed()) return; + await open(exampleProject, { + origin: 'Built-in example', + preferred: exampleView, + }); + setMode('diagram'); + } async function run(action: () => unknown | Promise) { + const generation = ++actionGeneration.current; setError(''); try { await action(); } catch (reason) { - setError(reason instanceof Error ? reason.message : String(reason)); + if (generation === actionGeneration.current) + setError(reason instanceof Error ? reason.message : String(reason)); } } - async function open(project: ProjectBundle) { + async function open( + project: ProjectBundle, + options: { + origin?: string; + preferred?: string; + mode?: Mode; + unsaved?: boolean; + } = {}, + ) { const session = workspace(); - await session.open(project); + await session.open(project, { unsaved: options.unsaved }); setSelected(''); setSymbolSelection(undefined); setFile(project.drafts[0]!.name); setFocusRange(undefined); setMessage('Project opened locally.'); - await refreshView(session); + setOrigin(options.origin ?? 'Imported copy'); + if (options.mode) setMode(options.mode); + else if ( + !session + .snapshot() + .model?.elements.some((item) => item.kind === 'viewUsage') + ) + setMode('split'); + setEverOpened(true); + setLauncherOpen(false); + await refreshView(session, options.preferred); } function replaceAllowed() { return ( @@ -198,21 +258,12 @@ export function Workbench() { ); } function newProject(projectName: string) { - const normalized = - projectName.trim().replace(/[^A-Za-z0-9_]/g, '') || 'Model', - name = /^[A-Za-z_]/.test(normalized) ? normalized : 'Model' + normalized; - const documents = [ - { name: 'model.sysml', text: `package ${name} {\n\n}\n` }, - ]; setDialog(null); void run(() => - open({ - format: 'sysmlv2-view-project', - version: 1, - name: projectName.trim() || 'My project', - documents, - drafts: documents, - presentations: {}, + open(createLocalProject(projectName), { + origin: 'New local project', + mode: 'code', + unsaved: true, }), ); } @@ -220,39 +271,10 @@ export function Workbench() { if (!files?.length) return; const chosen = Array.from(files); if (!replaceAllowed()) return; - if (chosen.length === 1 && chosen[0]!.name.endsWith('.json')) { - await open(readProject(await chosen[0]!.text())); - return; - } - const documents = await Promise.all( - chosen.map(async (item) => ({ - name: item.webkitRelativePath || item.name, - text: await item.text(), - })), - ); - await open({ - format: 'sysmlv2-view-project', - version: 1, - name: chosen[0]!.name.replace(/\.sysml$/, ''), - documents, - drafts: documents, - presentations: {}, - }); + await open(await readProjectFiles(chosen)); } function download() { - const session = workspace(), - serialized = session.serialize(), - url = URL.createObjectURL( - new Blob([serialized], { type: 'application/json' }), - ), - link = document.createElement('a'); - link.href = url; - link.download = - (session.snapshot().project?.name.replace(/[^A-Za-z0-9_-]/g, '-') || - 'model') + '.sysml-project.json'; - link.click(); - setTimeout(() => URL.revokeObjectURL(url), 1000); - session.markSaved(serialized); + downloadProject(workspace()); setMessage( 'Project download prepared. Keep the downloaded file to reopen your work.', ); @@ -265,13 +287,22 @@ export function Workbench() { } } function keyboard(event: KeyboardEvent) { + const current = ref.current?.snapshot(); if ( + active && + !launcherOpen && + !current?.busy && + current?.project && + !event.defaultPrevented && + !( + event.target instanceof Element && + event.target.closest('[role="dialog"]') + ) && (event.ctrlKey || event.metaKey) && - event.key.toLowerCase() === 's' && - ref.current?.snapshot().project + event.key.toLowerCase() === 's' ) { event.preventDefault(); - download(); + void run(download); } } window.addEventListener('beforeunload', unload); @@ -280,7 +311,7 @@ export function Workbench() { window.removeEventListener('beforeunload', unload); window.removeEventListener('keydown', keyboard); }; - }, [state.dirty]); + }, [state.dirty, active, launcherOpen]); function newView() { setDialog('view'); } @@ -413,7 +444,7 @@ export function Workbench() { return ( -
-
-
- SysML View Editor -
- - {state.project?.name ?? 'Workspace'} - {state.dirty ? ' · modified' : ''} - - -
- - - - void run(async () => { - const session = workspace(), - id = session.snapshot().view?.view; - session.undo(); - await refreshView(session, id); - }) - } - label="Undo source" - size="icon-xs" - icon={Undo2} - /> - - void run(async () => { - const session = workspace(), - id = session.snapshot().view?.view; - session.redo(); - await refreshView(session, id); - }) - } - label="Redo source" - size="icon-xs" - icon={Redo2} - /> -
- } - sourceActions={ -
- - {state.busy - ? 'Working…' - : state.unapplied - ? 'Draft not applied' - : 'Source applied'} - - - -
} - modes={ -
- {(['code', 'split', 'diagram'] as const).map((value) => ( - setMode(value)} - /> - ))} -
+ recoveryAction={ + } /> -
- { - setFile(name); - if (mode === 'diagram') setMode('split'); - }} - onView={(id) => void run(() => workspace().openView(id))} - onSelect={selectElement} - onNewView={newView} + )} + {everOpened && ( + + } + inspector={ + void run(() => reviewRename(name))} + onCreateView={newView} + onSource={revealSource} + onCreatePart={newPart} + onConnectPorts={newConnection} + /> + } + issues={ + + } + issueKey={ + state.diagnostics.length || (!dialog && error) + ? JSON.stringify([state.diagnostics, dialog ? '' : error]) + : '' + } + /> +
+
+ + + {message} + {state.layoutNotices.some((notice) => notice.invalidated) && + ` ${state.layoutNotices.reduce((total, notice) => total + notice.invalidated, 0)} saved positions were arranged again because their correspondence could not be verified.`} + + + {state.project?.documents.length ?? 0} files · revision{' '} + {state.revision} · local browser workspace + +
+ + )} { + if (chosen.length === 1 && chosen[0]!.name.endsWith('.json')) + return readProject(await chosen[0]!.text()); + const documents = await Promise.all( + chosen.map(async (item) => ({ + name: item.webkitRelativePath || item.name, + text: await item.text(), + })), + ); + return { + format: 'sysmlv2-view-project', + version: 1, + name: chosen[0]!.name.replace(/\.sysml$/, ''), + documents, + drafts: documents, + presentations: {}, + }; +} + +export function downloadProject(session: Workspace) { + const serialized = session.serialize(), + url = URL.createObjectURL( + new Blob([serialized], { type: 'application/json' }), + ), + link = document.createElement('a'); + link.href = url; + link.download = + (session.snapshot().project?.name.replace(/[^A-Za-z0-9_-]/g, '-') || + 'model') + '.sysml-project.json'; + link.click(); + setTimeout(() => URL.revokeObjectURL(url), 1000); + session.markSaved(serialized); +} diff --git a/src/main.tsx b/src/main.tsx index 8f5abde..7e0214b 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -1,13 +1,16 @@ import { StrictMode } from 'react'; import { createRoot } from 'react-dom/client'; -import { Workbench } from '$lib/workbench/shell/Workbench'; +import { Application } from '$lib/workbench/entry/Application'; +import { AppearanceProvider } from '$lib/ui/AppearanceSettings'; import '$lib/ui/theme.css'; import { TooltipProvider } from '$lib/ui/primitives/tooltip'; createRoot(document.getElementById('root')!).render( - + + + , ); diff --git a/tests/e2e/appearance.spec.ts b/tests/e2e/appearance.spec.ts index 3efb472..bf1997c 100644 --- a/tests/e2e/appearance.spec.ts +++ b/tests/e2e/appearance.spec.ts @@ -4,7 +4,7 @@ import { expect, openWorkshop, importWorkshop, - waitForStartup, + startExample, diagramNode, downloadProject, } from './support'; @@ -87,7 +87,7 @@ test('retains custom muted colors across palettes and reload, with keyboard dism ); await expect(trigger).toBeFocused(); await page.reload(); - await waitForStartup(page); + await startExample(page); await importWorkshop(page); await expect(definition).toBeVisible(); await expect(page.locator('html')).toHaveAttribute('data-palette', 'forest'); diff --git a/tests/e2e/assembly.spec.ts b/tests/e2e/assembly.spec.ts index ca04c7d..1d28beb 100644 --- a/tests/e2e/assembly.spec.ts +++ b/tests/e2e/assembly.spec.ts @@ -1,7 +1,7 @@ import { copySourceSelection, test, - waitForStartup, + startExample, expect, ready, importAssembly, @@ -151,7 +151,7 @@ test('moves an assembly with its descendants and reopens absolute positions with await openView(page, 'structure'); await expect.poll(() => nodePosition(primary)).toEqual(childAfter); await page.reload(); - await waitForStartup(page); + await startExample(page); await page.locator('input[type=file]').setInputFiles({ name: 'assembly.sysml-project.json', mimeType: 'application/json', diff --git a/tests/e2e/authoring.spec.ts b/tests/e2e/authoring.spec.ts index ebfdae0..26c5ed5 100644 --- a/tests/e2e/authoring.spec.ts +++ b/tests/e2e/authoring.spec.ts @@ -1,6 +1,6 @@ import { test, - waitForStartup, + startExample, expect, ready, importAssembly, @@ -197,7 +197,7 @@ test('creates a typed part and connects its actual inherited usage ports through } const saved = await downloadProject(page); await page.reload(); - await waitForStartup(page); + await startExample(page); await page.locator('input[type=file]').setInputFiles({ name: 'authored.sysml-project.json', mimeType: 'application/json', diff --git a/tests/e2e/default-example.spec.ts b/tests/e2e/default-example.spec.ts index ebc8cc7..910f026 100644 --- a/tests/e2e/default-example.spec.ts +++ b/tests/e2e/default-example.spec.ts @@ -5,9 +5,10 @@ import { ready, observeEvaluations, downloadProject, + namedViewButton, } from './support'; -test('opens the complete Smart air workshop in its useful pneumatic view and preserves all nine source files', async ({ +test('explicitly loads the complete Smart air workshop in its useful pneumatic view and preserves all nine source files', async ({ page, }, testInfo) => { const evaluations = await observeEvaluations(page); @@ -97,7 +98,7 @@ test('opens the complete Smart air workshop in its useful pneumatic view and pre ); } await expect( - page.getByTitle('CompressorViews::pneumatics', { exact: true }), + namedViewButton(page, 'CompressorViews::pneumatics'), ).toHaveAttribute('aria-current', 'page'); expect((await downloadProject(page)).bundle).toEqual(saved.bundle); await testInfo.attach('default-pneumatics-engine', { diff --git a/tests/e2e/diagnostics.spec.ts b/tests/e2e/diagnostics.spec.ts index 263d14f..943c4a0 100644 --- a/tests/e2e/diagnostics.spec.ts +++ b/tests/e2e/diagnostics.spec.ts @@ -13,7 +13,7 @@ import { semanticPositions, selectModelElement, panDiagram, - waitForStartup, + startExample, } from './support'; test('opens a real diagnostic in the correct file by pointer and keyboard while retaining drafts and saved views', async ({ @@ -90,7 +90,7 @@ test('opens a real diagnostic in the correct file by pointer and keyboard while await page.getByRole('button', { name: 'Redo source', exact: true }).click(); await expect(source).toHaveSource(saved.bundle.documents[0]!.text); await page.reload(); - await waitForStartup(page); + await startExample(page); await page.locator('input[type=file]').setInputFiles({ name: 'diagnostics.sysml-project.json', mimeType: 'application/json', diff --git a/tests/e2e/diagram-style.spec.ts b/tests/e2e/diagram-style.spec.ts new file mode 100644 index 0000000..989eca9 --- /dev/null +++ b/tests/e2e/diagram-style.spec.ts @@ -0,0 +1,124 @@ +import type { Locator } from '@playwright/test'; +import { + test, + expect, + ready, + downloadProject, + connectionPoints, + portAnchor, +} from './support'; + +async function directionContrast(label: Locator, card: Locator) { + const luminance = (color: string) => + color + .match(/[0-9.]+/g)! + .slice(0, 3) + .map((value) => Number(value) / 255) + .map((value) => + value <= 0.04045 ? value / 12.92 : ((value + 0.055) / 1.055) ** 2.4, + ) + .reduce( + (sum, value, index) => sum + value * [0.2126, 0.7152, 0.0722][index]!, + 0, + ); + const foreground = luminance( + await label.evaluate((node) => getComputedStyle(node).color), + ); + const background = luminance( + await card.evaluate((node) => getComputedStyle(node).backgroundColor), + ); + return ( + (Math.max(foreground, background) + 0.05) / + (Math.min(foreground, background) + 0.05) + ); +} + +test('keeps structural labels, keyboard port selection and real endpoints readable through appearance changes', async ({ + page, +}) => { + await ready(page, { split: false }); + const before = (await downloadProject(page)).bundle; + const pump = page.getByRole('group', { + name: 'part pump : Pump', + exact: true, + }); + const name = pump.getByText('pump', { exact: true }); + const type = pump.getByText('Pump', { exact: true }); + await expect(name).toBeVisible(); + await expect(type).toBeVisible(); + expect( + await name.evaluate((node) => Number(getComputedStyle(node).fontWeight)), + ).toBeGreaterThan( + await type.evaluate((node) => Number(getComputedStyle(node).fontWeight)), + ); + await name.click(); + await expect(pump).toHaveCSS('outline-style', 'solid'); + const output = pump.getByRole('button', { + name: 'out port outlet : AirOutlet', + exact: true, + }); + await expect(output.getByText('OUT', { exact: true })).toBeVisible(); + await expect(output.getByText('outlet', { exact: true })).toBeVisible(); + await expect(output.getByText('AirOutlet', { exact: true })).toBeVisible(); + await output.focus(); + await output.press('Enter'); + await expect( + page.getByRole('complementary', { name: 'Properties', exact: true }), + ).toContainText('CompressorComponents::Pump::outlet'); + await expect( + page.getByRole('complementary', { name: 'Properties', exact: true }), + ).toContainText('CompressorSystem::PneumaticAssembly::pump'); + await expect(output).toBeFocused(); + const tank = page.getByRole('group', { + name: 'part tank : Tank', + exact: true, + }); + const inlet = tank.getByRole('button', { + name: 'in port inlet : ~AirOutlet', + exact: true, + }); + const from = await portAnchor(output, 'source'); + const to = await portAnchor(inlet, 'target'); + await expect + .poll(async () => + (await connectionPoints(page)).some( + (edge) => + Math.abs(edge.start.x - from.x) < 1 && + Math.abs(edge.start.y - from.y) < 1 && + Math.abs(edge.end.x - to.x) < 1 && + Math.abs(edge.end.y - to.y) < 1, + ), + ) + .toBe(true); + for (const theme of ['light', 'dark']) { + for (const palette of [ + 'neutral', + 'muted', + 'contrast', + 'forest', + 'blueprint', + 'paper', + ]) { + await page + .getByRole('button', { name: 'Appearance', exact: true }) + .click(); + const dialog = page.getByRole('dialog', { + name: 'Appearance', + exact: true, + }); + await dialog.getByLabel('Theme', { exact: true }).selectOption(theme); + await dialog + .getByLabel('Diagram palette', { exact: true }) + .selectOption(palette); + await dialog.getByRole('button', { name: 'Done', exact: true }).click(); + await expect(name).toBeVisible(); + const direction = output.getByText('OUT', { exact: true }); + await expect(direction).toBeVisible(); + expect( + await directionContrast(direction, pump), + `${theme}/${palette} output direction contrast`, + ).toBeGreaterThanOrEqual(4.5); + } + } + expect((await downloadProject(page)).bundle).toEqual(before); +}); diff --git a/tests/e2e/examples.spec.ts b/tests/e2e/examples.spec.ts index e28d3de..23ef706 100644 --- a/tests/e2e/examples.spec.ts +++ b/tests/e2e/examples.spec.ts @@ -1,12 +1,13 @@ import { test, - waitForStartup, + startExample, expect, ready, observeEvaluations, downloadProject, diagramSymbol, connectionPoints, + namedViewButton, } from './support'; import { loadCatalog, loadExample } from '../../examples/catalog'; import { writeFile } from 'node:fs/promises'; @@ -29,7 +30,7 @@ for (const entry of catalog.examples) { await expect(source).toHaveSource(documents[0]!.text); const diagram = page.getByRole('region', { name: 'Diagram', exact: true }); for (const view of entry.views) { - const button = page.getByTitle(view.id, { exact: true }); + const button = namedViewButton(page, view.id); await button.click(); await expect(button).toHaveAttribute('aria-current', 'page'); await expect(button).toBeEnabled(); @@ -104,16 +105,17 @@ for (const entry of catalog.examples) { expect(bundle.documents).toEqual(documents); expect(bundle.drafts).toEqual(documents); await page.reload(); - await waitForStartup(page); + await startExample(page); await page.locator('input[type=file]').setInputFiles({ name: entry.id + '.sysml-project.json', mimeType: 'application/json', buffer, }); await expect(source).toHaveSource(documents[0]!.text); - await expect( - page.getByTitle(entry.views[0]!.id, { exact: true }), - ).toHaveAttribute('aria-current', 'page'); + await expect(namedViewButton(page, entry.views[0]!.id)).toHaveAttribute( + 'aria-current', + 'page', + ); const reopened = (await downloadProject(page)).bundle; expect(reopened.documents).toEqual(documents); expect(reopened.drafts).toEqual(documents); diff --git a/tests/e2e/identity.spec.ts b/tests/e2e/identity.spec.ts index ab56384..79bf322 100644 --- a/tests/e2e/identity.spec.ts +++ b/tests/e2e/identity.spec.ts @@ -5,10 +5,7 @@ import type { SourceDocument, ModelSnapshot, } from '../../src/lib/workspace/project'; -import { - readModelSnapshot, - validateDocuments, -} from '../../src/lib/workspace/project'; +import { readModelSnapshot } from '../../src/lib/workspace/project'; import { validateEvaluation, type EvaluatedView, diff --git a/tests/e2e/model-tree.spec.ts b/tests/e2e/model-tree.spec.ts index 4c0291b..4332868 100644 --- a/tests/e2e/model-tree.spec.ts +++ b/tests/e2e/model-tree.spec.ts @@ -6,6 +6,7 @@ import { modelTreeItem, downloadProject, diagramCamera, + namedViewButton, } from './support'; test('nests actual action parameters by owner and retains an invalid draft, source file, applied view and geometry while browsing', async ({ @@ -86,7 +87,7 @@ test('nests actual action parameters by owner and retains an invalid draft, sour .getByText('system.sysml', { exact: true }), ).toBeVisible(); await expect( - page.getByTitle('CompressorViews::pneumatics', { exact: true }), + namedViewButton(page, 'CompressorViews::pneumatics'), ).toHaveAttribute('aria-current', 'page'); expect((await downloadProject(page)).bundle).toEqual(before); expect(await diagramCamera(page)).toEqual(camera); diff --git a/tests/e2e/panels.spec.ts b/tests/e2e/panels.spec.ts index 1bbc5ec..5e4a58e 100644 --- a/tests/e2e/panels.spec.ts +++ b/tests/e2e/panels.spec.ts @@ -2,7 +2,7 @@ import { fillSource, test, selectModelElement, - waitForStartup, + startExample, expect, ready, importAssembly, @@ -197,7 +197,7 @@ test('collapses and restores sizes with keyboard focus, persists UI preferences expect((await downloadProject(page)).bundle).toEqual(before); await expect(page.getByRole('banner')).not.toContainText('modified'); await page.reload(); - await waitForStartup(page); + await startExample(page); await expect .poll(() => regionSize(explorer, 'width')) .toBeCloseTo(expanded, 0); diff --git a/tests/e2e/preservation.spec.ts b/tests/e2e/preservation.spec.ts index f88a25e..f16010a 100644 --- a/tests/e2e/preservation.spec.ts +++ b/tests/e2e/preservation.spec.ts @@ -2,7 +2,7 @@ import { fillSource, test, selectModelElement, - waitForStartup, + startExample, expect, ready, observeEvaluations, @@ -99,7 +99,7 @@ for (const rename of [ ).toEqual(expected); const saved = await downloadProject(page); await page.reload(); - await waitForStartup(page); + await startExample(page); await page.locator('input[type=file]').setInputFiles({ name: 'retained.sysml-project.json', mimeType: 'application/json', @@ -223,7 +223,7 @@ test('retains both views through a Unicode source insertion and an unrelated-fil .click(); const saved = await downloadProject(page); await page.reload(); - await waitForStartup(page); + await startExample(page); await page.locator('input[type=file]').setInputFiles({ name: 'edited.sysml-project.json', mimeType: 'application/json', diff --git a/tests/e2e/recovery.spec.ts b/tests/e2e/recovery.spec.ts index edcfdee..ef4fa53 100644 --- a/tests/e2e/recovery.spec.ts +++ b/tests/e2e/recovery.spec.ts @@ -9,7 +9,9 @@ import { diagramNode, moveNode, openView, - waitForStartup, + waitForLauncher, + startExample, + namedViewButton, } from './support'; /** Read the real adapter's durable record, never a private application state. */ @@ -80,7 +82,7 @@ test('restores exact applied source, invalid drafts and both view layouts from I const { expected, draft } = await savedDraft(page); page.on('dialog', (dialog) => dialog.accept()); await page.reload(); - await waitForStartup(page); + await waitForLauncher(page); const recovery = page.getByRole('region', { name: 'Saved recovery', exact: true, @@ -95,7 +97,7 @@ test('restores exact applied source, invalid drafts and both view layouts from I }); await expect(source).toHaveSource(draft); await expect( - page.getByTitle('WorkshopViews::compressor', { exact: true }), + namedViewButton(page, 'WorkshopViews::compressor'), ).toHaveAttribute('aria-current', 'page'); expect((await downloadProject(page)).bundle).toEqual(expected); await openView(page, 'overview'); @@ -105,14 +107,16 @@ test('restores exact applied source, invalid drafts and both view layouts from I ); }); -test('explicitly discards stored recovery without changing the current startup project', async ({ +test('explicitly discards stored recovery while leaving the launcher empty', async ({ page, }) => { await savedDraft(page); page.on('dialog', (dialog) => dialog.accept()); await page.reload(); - await waitForStartup(page); - const before = (await downloadProject(page)).bundle; + await waitForLauncher(page); + await expect( + page.getByRole('button', { name: 'Download project', exact: true }), + ).not.toBeVisible(); const recovery = page.getByRole('region', { name: 'Saved recovery', exact: true, @@ -122,7 +126,13 @@ test('explicitly discards stored recovery without changing the current startup p .click(); await expect(recovery).not.toBeVisible(); await expect.poll(() => readRecovery(page)).toBeUndefined(); - expect((await downloadProject(page)).bundle).toEqual(before); + await waitForLauncher(page); + await expect( + page.getByRole('button', { name: 'Resume project', exact: true }), + ).not.toBeVisible(); + await expect( + page.getByRole('textbox', { name: 'SysML source', exact: true }), + ).not.toBeVisible(); }); test('retains malformed recovery until explicit discard and leaves the current project untouched', async ({ @@ -134,7 +144,8 @@ test('retains malformed recovery until explicit discard and leaves the current p await replaceRecovery(page, corrupt); page.on('dialog', (dialog) => dialog.accept()); await page.reload(); - await waitForStartup(page); + await waitForLauncher(page); + await startExample(page); const before = (await downloadProject(page)).bundle; const recovery = page.getByRole('region', { name: 'Saved recovery', @@ -225,8 +236,8 @@ test('a second tab cannot silently overwrite a newer recovery or lose either liv const errors: string[] = []; other.on('pageerror', (error) => errors.push(error.message)); try { - await other.goto('/'); - await waitForStartup(other); + await other.goto('/app'); + await waitForLauncher(other); await other .getByRole('region', { name: 'Saved recovery', exact: true }) .getByRole('button', { name: 'Restore recovery', exact: true }) @@ -237,7 +248,7 @@ test('a second tab cannot silently overwrite a newer recovery or lose either liv }); await expect(otherSource).toHaveSource(first.draft); await expect( - other.getByTitle('WorkshopViews::compressor', { exact: true }), + namedViewButton(other, 'WorkshopViews::compressor'), ).toHaveAttribute('aria-current', 'page'); await expect( other.getByRole('button', { name: 'Download project', exact: true }), @@ -335,14 +346,14 @@ test('contains keyboard focus in recovery settings, returns focus and persists t await expect(trigger).toBeFocused(); expect((await downloadProject(page)).bundle).toEqual(before); await page.reload(); - await waitForStartup(page); + await waitForLauncher(page); await trigger.click(); await expect(enabled).not.toBeChecked(); await enabled.check(); await done.click(); await expect(trigger).toBeFocused(); await page.reload(); - await waitForStartup(page); + await waitForLauncher(page); await trigger.click(); await expect(enabled).toBeChecked(); }); @@ -372,7 +383,7 @@ test('restored clean source and geometry remain unsaved until a portable downloa expect(expected.drafts).toEqual(before.documents); page.once('dialog', (dialog) => dialog.accept()); await page.reload(); - await waitForStartup(page); + await waitForLauncher(page); await page .getByRole('region', { name: 'Saved recovery', exact: true }) .getByRole('button', { name: 'Restore recovery', exact: true }) @@ -430,7 +441,7 @@ test('discarding the only unsaved draft removes its stale local recovery without await expect.poll(() => readRecovery(page)).toBeUndefined(); expect((await downloadProject(page)).bundle).toEqual(before); await page.reload(); - await waitForStartup(page); + await waitForLauncher(page); await expect( page.getByRole('region', { name: 'Saved recovery', exact: true }), ).not.toBeVisible(); @@ -450,7 +461,8 @@ test('failed real-engine recovery validation retains the live project, exact sto await replaceRecovery(page, invalid); page.once('dialog', (dialog) => dialog.accept()); await page.reload(); - await waitForStartup(page); + await waitForLauncher(page); + await startExample(page); const before = (await downloadProject(page)).bundle; const recovery = page.getByRole('region', { name: 'Saved recovery', diff --git a/tests/e2e/shell-entry.spec.ts b/tests/e2e/shell-entry.spec.ts new file mode 100644 index 0000000..5ea1805 --- /dev/null +++ b/tests/e2e/shell-entry.spec.ts @@ -0,0 +1,591 @@ +import { readFile } from 'node:fs/promises'; +import type { Page } from '@playwright/test'; +import { + test, + expect, + waitForLauncher, + waitForStartup, + openWorkshop, + downloadProject, + fillSource, + readSource, + copySourceSelection, + diagramNode, + openView, + moveNode, + panDiagram, + diagramCamera, + selectModelElement, + expectRenderedPositions, + namedViewButton, +} from './support'; + +async function chooseFiles(page: Page) { + const choice = page.waitForEvent('filechooser'); + await page + .getByRole('button', { name: 'Open files or project', exact: true }) + .click(); + return choice; +} + +async function workshopFiles() { + return Promise.all( + ['model.sysml', 'views.sysml'].map(async (name) => ({ + name, + mimeType: 'text/plain', + buffer: await readFile( + new URL('../../examples/workshop/' + name, import.meta.url), + ), + })), + ); +} + +test('opens the OpenMBEE landing and empty launcher without loading an example or model engine', async ({ + page, +}) => { + const engineRequests: string[] = []; + page.on('request', (request) => { + if (/\/vendor\/(opensysml|sysml-toolkit)\//.test(request.url())) + engineRequests.push(request.url()); + }); + await page.goto('/'); + await expect(page).toHaveTitle(/OpenMBEE View Editor/); + await expect( + page.getByRole('heading', { + name: 'OpenMBEE View Editor', + exact: true, + level: 1, + }), + ).toBeVisible(); + await expect( + page.getByRole('link', { name: 'Source', exact: true }), + ).toHaveAttribute('href', 'https://github.com/Open-MBEE/sysml_view_editor'); + await page.getByRole('link', { name: 'Open app', exact: true }).click(); + await expect(page).toHaveURL(/\/app$/); + await waitForLauncher(page); + await expect( + page.getByRole('heading', { name: 'Open a project', exact: true }), + ).toBeFocused(); + await expect( + page.getByRole('button', { name: 'Resume project', exact: true }), + ).not.toBeVisible(); + await expect( + page.getByRole('main', { name: 'Model workspace', exact: true }), + ).not.toBeVisible(); + await expect( + page.getByRole('button', { name: 'Download project', exact: true }), + ).not.toBeVisible(); + await expect( + page.getByRole('button', { name: 'New project', exact: true }), + ).toBeEnabled(); + await expect( + page.getByRole('button', { name: 'Open files or project', exact: true }), + ).toBeEnabled(); + await page + .getByText('Folder and repository workflows', { exact: true }) + .click(); + await expect( + page.getByText( + 'Direct folder saving and Flexo project/branch checkout are planned. Use source files or a project bundle to work locally today.', + { exact: true }, + ), + ).toBeVisible(); + expect(engineRequests).toEqual([]); +}); + +test('landing and direct example links open the real model once and reload returns to the empty launcher', async ({ + page, +}) => { + await page.goto('/'); + await page.getByRole('link', { name: 'Try an example', exact: true }).click(); + await waitForStartup(page, { split: false }); + await expect(page).toHaveURL(/\/app$/); + const bundle = (await downloadProject(page)).bundle; + expect(bundle.name).toBe('Smart air workshop'); + expect(bundle.documents).toHaveLength(9); + expect(bundle.drafts).toEqual(bundle.documents); + await page.reload(); + await waitForLauncher(page); + await expect( + page.getByRole('button', { name: 'Resume project', exact: true }), + ).not.toBeVisible(); + await expect( + page.getByRole('main', { name: 'Model workspace', exact: true }), + ).not.toBeVisible(); + // A directly opened example URL uses the same real vendor assets and consumes + // its one-shot instruction, including when the incoming route has a slash. + await page.goto('/app/?example=1'); + await waitForStartup(page, { split: false }); + await expect(page).toHaveURL(/\/app$/); + expect((await downloadProject(page)).bundle.documents).toEqual( + bundle.documents, + ); + await page.reload(); + await waitForLauncher(page); + await expect( + page.getByRole('button', { name: 'Resume project', exact: true }), + ).not.toBeVisible(); +}); + +test('creates a new source project from the launcher and returns dialog focus on cancellation', async ({ + page, +}) => { + await page.goto('/app'); + await waitForLauncher(page); + const create = page.getByRole('button', { name: 'New project', exact: true }); + await create.focus(); + await create.press('Enter'); + const dialog = page.getByRole('dialog', { name: 'New project', exact: true }); + const name = dialog.getByRole('textbox', { + name: 'Project name', + exact: true, + }); + await expect(name).toBeFocused(); + const close = dialog.getByRole('button', { name: 'Close', exact: true }); + await close.focus(); + await page.keyboard.press('Tab'); + await expect(name).toBeFocused(); + await page.keyboard.press('Shift+Tab'); + await expect(close).toBeFocused(); + await dialog.getByRole('button', { name: 'Cancel', exact: true }).click(); + await expect(dialog).not.toBeVisible(); + await expect(create).toBeFocused(); + await expect( + page.getByRole('button', { name: 'Resume project', exact: true }), + ).not.toBeVisible(); + await create.click(); + await name.fill('Bench'); + await dialog + .getByRole('button', { name: 'Create project', exact: true }) + .click(); + await expect(dialog).not.toBeVisible(); + const source = page.getByRole('textbox', { + name: 'SysML source', + exact: true, + }); + await expect(source).toHaveSource('package Bench {\n\n}\n'); + await expect( + page.getByRole('banner').getByText('Bench · modified', { exact: true }), + ).toBeVisible(); + await expect( + page.getByRole('button', { name: 'Code', exact: true }), + ).toHaveAttribute('aria-pressed', 'true'); + let replacement = ''; + page.once('dialog', async (confirmation) => { + replacement = confirmation.message(); + await confirmation.dismiss(); + }); + await create.click(); + await expect + .poll(() => replacement) + .toBe( + 'Replace this project and discard changes that have not been downloaded?', + ); + await expect(dialog).not.toBeVisible(); + await expect(source).toHaveSource('package Bench {\n\n}\n'); + await expect( + page.getByRole('banner').getByText('Bench · modified', { exact: true }), + ).toBeVisible(); + const bundle = (await downloadProject(page)).bundle; + await expect( + page.getByRole('banner').getByText('Bench', { exact: true }), + ).toBeVisible(); + await expect(page.getByRole('banner')).not.toContainText('modified'); + expect(bundle.name).toBe('Bench'); + expect(bundle.documents).toEqual([ + { name: 'model.sysml', text: 'package Bench {\n\n}\n' }, + ]); + expect(bundle.drafts).toEqual(bundle.documents); + expect(bundle.presentations).toEqual({}); +}); + +test('imports source files through the launcher file chooser without an implicit startup model', async ({ + page, +}) => { + await page.goto('/app'); + await waitForLauncher(page); + const files = await workshopFiles(); + const chooser = await chooseFiles(page); + await chooser.setFiles(files); + await expect(diagramNode(page, 'Workshop::Motor')).toBeVisible(); + await page.getByRole('button', { name: 'Split', exact: true }).click(); + const source = page.getByRole('textbox', { + name: 'SysML source', + exact: true, + }); + await expect(source).toHaveSource(files[0]!.buffer.toString('utf8')); + const bundle = (await downloadProject(page)).bundle; + expect(bundle.documents).toEqual( + files.map((file) => ({ + name: file.name, + text: file.buffer.toString('utf8'), + })), + ); + expect(bundle.drafts).toEqual(bundle.documents); + await expect( + page.getByRole('heading', { name: 'Open a project', exact: true }), + ).not.toBeVisible(); +}); + +test('launcher and browser navigation retain exact drafts, source DOM, camera, both layouts and accepted source history', async ({ + page, +}) => { + test.setTimeout(150_000); + await openWorkshop(page); + const source = page.getByRole('textbox', { + name: 'SysML source', + exact: true, + }); + const original = await readSource(source); + const applied = original + '\n// accepted before launcher navigation\n'; + await fillSource(source, applied); + await page.getByRole('button', { name: 'Apply source', exact: true }).click(); + await expect(page.getByText('Source applied', { exact: true })).toBeVisible(); + await expect( + page.getByRole('button', { name: 'Undo source', exact: true }), + ).toBeEnabled(); + await moveNode(page, diagramNode(page, 'Workshop::Motor')); + await openView(page, 'compressor'); + await moveNode(page, diagramNode(page, 'primary : Motor')); + await selectModelElement(page, 'Workshop::Compressor::primary'); + const before = (await downloadProject(page)).bundle; + const draft = applied + '\n// retained unfinished launcher draft\npart'; + await fillSource(source, draft); + const editor = await source.elementHandle(); + await source.press('ControlOrMeta+Home'); + await source.press('Shift+End'); + const selectedSource = await copySourceSelection(source); + expect(selectedSource).toBe('package Workshop {'); + await panDiagram(page); + const camera = await diagramCamera(page); + await expect(page.getByRole('banner')).toContainText('modified'); + + await page + .getByRole('link', { name: 'OpenMBEE View Editor home', exact: true }) + .click(); + await expect(page).toHaveURL(/\/$/); + await expect(source).not.toBeVisible(); + await page.goBack(); + await expect(page).toHaveURL(/\/app$/); + await expect(source).toBeVisible(); + await page.goForward(); + await expect(page).toHaveURL(/\/$/); + await expect(source).not.toBeVisible(); + await page.goBack(); + await expect(page).toHaveURL(/\/app$/); + await expect(source).toBeVisible(); + await page.getByRole('button', { name: 'Projects', exact: true }).click(); + await waitForLauncher(page); + await expect(source).not.toBeVisible(); + await page + .getByRole('button', { name: 'Resume project', exact: true }) + .click(); + await expect( + page.getByRole('button', { name: 'Projects', exact: true }), + ).toBeFocused(); + await expect.poll(() => copySourceSelection(source)).toBe(selectedSource); + expect( + await source.evaluate((node, previous) => node === previous, editor), + ).toBe(true); + await expect(source).toHaveSource(draft); + await expect(page.getByRole('banner')).toContainText('modified'); + await expect( + namedViewButton(page, 'WorkshopViews::compressor'), + ).toHaveAttribute('aria-current', 'page'); + await expect( + page.getByRole('complementary', { name: 'Properties', exact: true }), + ).toContainText('Workshop::Compressor::primary'); + expect(await diagramCamera(page)).toEqual(camera); + const retained = (await downloadProject(page)).bundle; + expect(retained).toEqual({ + ...before, + drafts: before.drafts.map((file) => + file.name === 'model.sysml' ? { ...file, text: draft } : file, + ), + }); + await page + .getByRole('button', { name: 'Discard draft', exact: true }) + .click(); + for (const view of ['overview', 'compressor']) { + await openView(page, view); + await expectRenderedPositions(page, before, 'WorkshopViews::' + view); + } + await page.getByRole('button', { name: 'Undo source', exact: true }).click(); + await expect(source).toHaveSource(original); + await page.getByRole('button', { name: 'Redo source', exact: true }).click(); + await expect(source).toHaveSource(applied); +}); + +test('cancelled replacement and failed real-engine import keep the current model, invalid draft and saved geometry', async ({ + page, +}) => { + await openWorkshop(page); + await moveNode(page, diagramNode(page, 'Workshop::Motor')); + const before = (await downloadProject(page)).bundle; + const source = page.getByRole('textbox', { + name: 'SysML source', + exact: true, + }); + const draft = before.documents[0]!.text + '\n// keep current work\npart'; + await fillSource(source, draft); + await page.getByRole('button', { name: 'Projects', exact: true }).click(); + await waitForLauncher(page); + let cancelled = ''; + page.once('dialog', async (dialog) => { + cancelled = dialog.message(); + await dialog.dismiss(); + }); + await page.getByRole('button', { name: 'Load example', exact: true }).click(); + await expect + .poll(() => cancelled) + .toBe( + 'Replace this project and discard changes that have not been downloaded?', + ); + await waitForLauncher(page); + page.once('dialog', (dialog) => dialog.accept()); + const chooser = await chooseFiles(page); + await chooser.setFiles({ + name: 'broken.sysml', + mimeType: 'text/plain', + buffer: Buffer.from('package Broken { part def'), + }); + await expect(page.getByRole('alert')).toHaveText( + 'Source has errors; the last applied model is retained.', + ); + await expect( + page.getByRole('button', { name: 'Resume project', exact: true }), + ).toBeEnabled(); + await page + .getByRole('button', { name: 'Resume project', exact: true }) + .click(); + await expect(source).toHaveSource(draft); + await expect(diagramNode(page, 'Workshop::Motor')).toBeVisible(); + await expect(page.getByRole('banner')).toContainText('modified'); + expect((await downloadProject(page)).bundle).toEqual({ + ...before, + drafts: before.drafts.map((file) => + file.name === 'model.sysml' ? { ...file, text: draft } : file, + ), + }); +}); + +test('rejects an unsupported project bundle in the empty launcher and permits a valid retry', async ({ + page, +}) => { + await page.goto('/app'); + await waitForLauncher(page); + const chooser = await chooseFiles(page); + await chooser.setFiles({ + name: 'future.sysml-project.json', + mimeType: 'application/json', + buffer: Buffer.from('{"format":"sysmlv2-view-project","version":99}'), + }); + await expect(page.getByRole('alert')).toContainText( + 'Select a supported version 1 project', + ); + await waitForLauncher(page); + await expect( + page.getByRole('button', { name: 'Resume project', exact: true }), + ).not.toBeVisible(); + const retry = await chooseFiles(page); + const files = await workshopFiles(); + await retry.setFiles(files); + await expect(diagramNode(page, 'Workshop::Motor')).toBeVisible(); + const bundle = (await downloadProject(page)).bundle; + expect(bundle.documents).toEqual( + files.map((file) => ({ + name: file.name, + text: file.buffer.toString('utf8'), + })), + ); + expect(bundle.drafts).toEqual(bundle.documents); +}); + +test('the diagram view selector keeps source-file choice, pending draft and both saved view layouts independent', async ({ + page, +}) => { + await openWorkshop(page); + await moveNode(page, diagramNode(page, 'Workshop::Motor')); + await openView(page, 'compressor'); + await moveNode(page, diagramNode(page, 'primary : Motor')); + const before = (await downloadProject(page)).bundle; + await page.getByRole('button', { name: 'views.sysml', exact: true }).click(); + const source = page.getByRole('textbox', { + name: 'SysML source', + exact: true, + }); + const draft = + before.documents.find((file) => file.name === 'views.sysml')!.text + + '\n// independent source draft\npart'; + await fillSource(source, draft); + const diagram = page.getByRole('region', { name: 'Diagram', exact: true }); + const selector = diagram.getByRole('combobox', { + name: 'Named view', + exact: true, + }); + await expect(selector).toHaveValue('WorkshopViews::compressor'); + for (const [view, representation] of [ + ['overview', 'Overview tree'], + ['compressor', 'Structure'], + ] as const) { + await selector.selectOption('WorkshopViews::' + view); + await expect( + namedViewButton(page, 'WorkshopViews::' + view), + ).toHaveAttribute('aria-current', 'page'); + await expect( + diagram.getByText(representation, { exact: true }), + ).toBeVisible(); + await expect( + diagram.getByText('Applied revision · draft pending', { exact: true }), + ).toBeVisible(); + await expect( + page.getByRole('button', { name: 'views.sysml', exact: true }), + ).toHaveAttribute('aria-current', 'page'); + await expect(source).toHaveSource(draft); + await expectRenderedPositions(page, before, 'WorkshopViews::' + view); + } + expect((await downloadProject(page)).bundle).toEqual({ + ...before, + drafts: before.drafts.map((file) => + file.name === 'views.sysml' ? { ...file, text: draft } : file, + ), + }); +}); + +test('hidden workbench save shortcuts do not download or mark work saved, while visible save still exports exact work', async ({ + page, +}) => { + await openWorkshop(page); + const source = page.getByRole('textbox', { + name: 'SysML source', + exact: true, + }); + const original = await readSource(source); + const applied = + original + '\n// accepted history before hidden save shortcuts\n'; + await fillSource(source, applied); + await page.getByRole('button', { name: 'Apply source', exact: true }).click(); + await expect(page.getByText('Source applied', { exact: true })).toBeVisible(); + await expect( + page.getByRole('button', { name: 'Undo source', exact: true }), + ).toBeEnabled(); + const before = (await downloadProject(page)).bundle; + const draft = applied + '\n// retain the dirty hidden draft\npart'; + await fillSource(source, draft); + await expect(page.getByRole('banner')).toContainText('modified'); + const downloads: string[] = []; + page.on('download', (download) => + downloads.push(download.suggestedFilename()), + ); + + await page.getByRole('button', { name: 'Projects', exact: true }).click(); + await waitForLauncher(page); + await page.keyboard.press('ControlOrMeta+s'); + await page + .getByRole('link', { name: 'OpenMBEE View Editor home', exact: true }) + .click(); + await expect(page).toHaveURL(/\/$/); + await page.keyboard.press('ControlOrMeta+s'); + await page.getByRole('link', { name: 'Open app', exact: true }).click(); + await waitForLauncher(page); + await page + .getByRole('button', { name: 'Resume project', exact: true }) + .click(); + await expect(source).toHaveSource(draft); + // Marking a hidden project saved would remove this real dirty-state outcome, + // even if the browser's download event had not yet reached the test process. + await expect(page.getByRole('banner')).toContainText('modified'); + expect(downloads).toEqual([]); + + const exported = page.waitForEvent('download'); + await page.keyboard.press('ControlOrMeta+s'); + const download = await exported; + expect(download.suggestedFilename()).toBe('Workshop.sysml-project.json'); + const path = await download.path(); + expect(path).not.toBeNull(); + const project = JSON.parse(await readFile(path!, 'utf8')); + expect(project).toEqual({ + ...before, + drafts: before.drafts.map((file) => + file.name === 'model.sysml' ? { ...file, text: draft } : file, + ), + }); + expect(downloads).toEqual(['Workshop.sysml-project.json']); + await expect(page.getByRole('banner')).not.toContainText('modified'); + await expect(source).toHaveSource(draft); + await page + .getByRole('button', { name: 'Discard draft', exact: true }) + .click(); + await expect( + page.getByRole('button', { name: 'Undo source', exact: true }), + ).toBeEnabled(); + await page.getByRole('button', { name: 'Undo source', exact: true }).click(); + await expect(source).toHaveSource(original); + await page.getByRole('button', { name: 'Redo source', exact: true }).click(); + await expect(source).toHaveSource(applied); +}); + +test('a newer example opening supersedes a held real engine request without publishing its cancellation error', async ({ + page, +}) => { + await page.goto('/app'); + await waitForLauncher(page); + let release!: () => void; + let held = false; + let requests = 0; + const pending = new Promise((resolve) => { + release = resolve; + }); + const asset = '**/vendor/opensysml/sysml-wasm.wasm'; + await page.route(asset, async (route) => { + requests++; + if (!held) { + held = true; + await pending; + } + await route.continue(); + }); + try { + await page + .getByRole('button', { name: 'Load example', exact: true }) + .click(); + await expect.poll(() => held).toBe(true); + await expect( + page.getByText('Opening project…', { exact: true }), + ).toBeVisible(); + await page + .getByRole('link', { name: 'OpenMBEE View Editor home', exact: true }) + .click(); + await page + .getByRole('link', { name: 'Try an example', exact: true }) + .click(); + await expect.poll(() => requests).toBeGreaterThan(1); + release(); + await waitForStartup(page, { split: false }); + await expect( + page.getByRole('button', { name: /^(Show|Hide) Issues$/ }), + ).toHaveText('Issues (0)'); + await expect( + page.getByText('Engine operation cancelled.', { exact: true }), + ).not.toBeVisible(); + await expect(page.getByRole('alert')).not.toBeVisible(); + const bundle = (await downloadProject(page)).bundle; + expect(bundle.name).toBe('Smart air workshop'); + expect(bundle.documents).toHaveLength(9); + for (const document of bundle.documents) { + expect(document.text).toBe( + await readFile( + new URL( + '../../examples/air-compressor/' + document.name, + import.meta.url, + ), + 'utf8', + ), + ); + } + expect(bundle.drafts).toEqual(bundle.documents); + } finally { + release(); + await page.unroute(asset); + } +}); diff --git a/tests/e2e/source-editor.spec.ts b/tests/e2e/source-editor.spec.ts index d075f15..d706be8 100644 --- a/tests/e2e/source-editor.spec.ts +++ b/tests/e2e/source-editor.spec.ts @@ -6,7 +6,7 @@ import { fillSource, copySourceSelection, downloadProject, - waitForStartup, + startExample, diagramState, diagramNode, } from './support'; @@ -128,7 +128,7 @@ test('retains a virtualized long draft across file switches, local undo and fres expect(saved.bundle.documents[0]!.text).toBe(original); expect(saved.bundle.drafts[0]!.text).toBe(draft); await page.reload(); - await waitForStartup(page); + await startExample(page); await page.locator('input[type=file]').setInputFiles({ name: 'draft.sysml-project.json', mimeType: 'application/json', diff --git a/tests/e2e/support.ts b/tests/e2e/support.ts index ec5ac07..ade8fda 100644 --- a/tests/e2e/support.ts +++ b/tests/e2e/support.ts @@ -112,12 +112,19 @@ export function diagramNode(page: Page, label: string) { .filter({ hasText: label }); } +/** A named-view navigation button, distinct from the diagram's active-view selector. */ +export function namedViewButton(page: Page, id: string) { + return page + .getByRole('complementary', { name: 'Project navigation', exact: true }) + .getByTitle(id, { exact: true }); +} + export async function waitForStartup(page: Page, { split = true } = {}) { await expect( page.getByRole('group', { name: 'part pump : Pump', exact: true }), ).toBeVisible(); await expect( - page.getByTitle('CompressorViews::pneumatics', { exact: true }), + namedViewButton(page, 'CompressorViews::pneumatics'), ).toHaveAttribute('aria-current', 'page'); await expect( page.getByRole('button', { name: 'Download project', exact: true }), @@ -127,11 +134,31 @@ export async function waitForStartup(page: Page, { split = true } = {}) { await page.evaluate(() => document.fonts.ready); } -export async function ready(page: Page, options: { split?: boolean } = {}) { - await page.goto('/'); +/** The app launcher is deliberately empty until an explicit project choice. */ +export async function waitForLauncher(page: Page) { + await expect( + page.getByRole('heading', { name: 'Open a project', exact: true }), + ).toBeVisible(); + await expect( + page.getByRole('button', { name: 'Load example', exact: true }), + ).toBeEnabled(); +} + +/** Explicitly choose the example; waiting for a model never creates one. */ +export async function startExample( + page: Page, + options: { split?: boolean } = {}, +) { + await waitForLauncher(page); + await page.getByRole('button', { name: 'Load example', exact: true }).click(); await waitForStartup(page, options); } +export async function ready(page: Page, options: { split?: boolean } = {}) { + await page.goto('/app'); + await startExample(page, options); +} + /** Keep focused history/appearance regressions explicit about their small fixture. */ export async function openWorkshop(page: Page) { await ready(page); @@ -164,7 +191,7 @@ export async function importWorkshop(page: Page) { }); await expect(diagramNode(page, 'Workshop::Motor')).toBeVisible(); await expect( - page.getByTitle('WorkshopViews::overview', { exact: true }), + namedViewButton(page, 'WorkshopViews::overview'), ).toHaveAttribute('aria-current', 'page'); await expect( page.getByRole('button', { name: 'Download project', exact: true }), diff --git a/tests/e2e/toolbar.spec.ts b/tests/e2e/toolbar.spec.ts index fd00bfd..1e7cbed 100644 --- a/tests/e2e/toolbar.spec.ts +++ b/tests/e2e/toolbar.spec.ts @@ -4,6 +4,7 @@ import { test, expect, ready, + waitForLauncher, openWorkshop, downloadProject, diagramCamera, @@ -147,3 +148,91 @@ test('centres source commands and mode icons over their panes while resizing', a .toBeGreaterThanOrEqual(0); await expect.poll(modeGap).toBeGreaterThanOrEqual(8); }); + +test('keeps wrapped mobile project actions inside the header and above source controls', async ({ + page, +}) => { + await page.setViewportSize({ width: 390, height: 900 }); + await page.goto('/app'); + await waitForLauncher(page); + await page.getByRole('button', { name: 'Load example', exact: true }).click(); + await expect( + page.getByRole('combobox', { name: 'Named view', exact: true }), + ).toHaveValue('CompressorViews::pneumatics'); + await expect( + page.getByRole('group', { name: 'part pump : Pump', exact: true }), + ).toBeVisible(); + await page.evaluate(() => document.fonts.ready); + const header = page.getByRole('banner'); + const actions = header.getByRole('navigation', { + name: 'Project actions', + exact: true, + }); + for (const width of [390, 600, 601]) { + await page.setViewportSize({ width, height: 900 }); + await expect + .poll( + async () => { + const [container, content] = await Promise.all([ + header.boundingBox(), + actions.boundingBox(), + ]); + return ( + container!.y + container!.height - content!.y - content!.height + ); + }, + { timeout: 5000 }, + ) + .toBeGreaterThanOrEqual(0); + await expect + .poll( + async () => { + const [container, history, source, modes] = await Promise.all([ + header.boundingBox(), + page + .getByRole('group', { + name: 'Source transaction history', + exact: true, + }) + .boundingBox(), + page + .getByRole('group', { name: 'Source actions', exact: true }) + .boundingBox(), + page + .getByRole('group', { name: 'Editor layout', exact: true }) + .boundingBox(), + ]); + return ( + Math.min(history!.y, source!.y, modes!.y) - + container!.y - + container!.height + ); + }, + { timeout: 5000 }, + ) + .toBeGreaterThanOrEqual(0); + } + await page.setViewportSize({ width: 390, height: 900 }); + const before = (await downloadProject(page)).bundle; + const appearance = actions.getByRole('button', { + name: 'Appearance', + exact: true, + }); + await appearance.focus(); + await appearance.press('Enter'); + const dialog = page.getByRole('dialog', { name: 'Appearance', exact: true }); + await expect( + dialog.getByRole('combobox', { name: 'Theme', exact: true }), + ).toBeFocused(); + await page.keyboard.press('Escape'); + await expect(dialog).not.toBeVisible(); + await expect(appearance).toBeFocused(); + await header.getByRole('button', { name: 'Projects', exact: true }).click(); + await page + .getByRole('button', { name: 'Resume project', exact: true }) + .click(); + await expect( + header.getByRole('button', { name: 'Projects', exact: true }), + ).toBeFocused(); + expect((await downloadProject(page)).bundle).toEqual(before); +}); diff --git a/tests/e2e/visual.spec.ts b/tests/e2e/visual.spec.ts index c96b89a..2c6c5c2 100644 --- a/tests/e2e/visual.spec.ts +++ b/tests/e2e/visual.spec.ts @@ -437,3 +437,114 @@ for (const theme of ['light', 'dark']) { }); } } + +for (const theme of ['light', 'dark']) { + for (const width of [1440, 768, 390]) { + test(`OpenMBEE entry surfaces in ${theme} at ${width}`, async ({ + page, + }) => { + await page.setViewportSize({ width, height: 900 }); + await page.goto('/'); + await page + .getByRole('button', { name: 'Appearance', exact: true }) + .click(); + const appearance = page.getByRole('dialog', { + name: 'Appearance', + exact: true, + }); + await appearance.getByLabel('Theme', { exact: true }).selectOption(theme); + await appearance + .getByRole('button', { name: 'Done', exact: true }) + .click(); + await expect(page.locator('html')).toHaveAttribute('data-theme', theme); + await page.evaluate(() => document.fonts.ready); + await page.mouse.move(0, 0); + await expect( + page.getByRole('heading', { + name: 'OpenMBEE View Editor', + exact: true, + level: 1, + }), + ).toBeVisible(); + await expect( + page.getByRole('link', { name: 'Open app', exact: true }), + ).toBeInViewport({ ratio: 1 }); + expect( + await page.evaluate(() => document.documentElement.scrollWidth), + ).toBeLessThanOrEqual(width); + await expect(page).toHaveScreenshot(`landing-${theme}-${width}.png`, { + fullPage: true, + }); + await page.getByRole('link', { name: 'Open app', exact: true }).click(); + await expect( + page.getByRole('heading', { name: 'Open a project', exact: true }), + ).toBeFocused(); + await expect( + page.getByRole('button', { name: 'Load example', exact: true }), + ).toBeEnabled(); + await expect(page.locator('html')).toHaveAttribute('data-theme', theme); + await page.mouse.move(0, 0); + expect( + await page.evaluate(() => document.documentElement.scrollWidth), + ).toBeLessThanOrEqual(width); + await expect(page).toHaveScreenshot(`launcher-${theme}-${width}.png`, { + fullPage: true, + }); + }); + } +} + +for (const theme of ['light', 'dark']) { + for (const narrow of [false, true]) { + test(`selected structural nodes in ${theme}${narrow ? ' narrow' : ''}`, async ({ + page, + }) => { + if (narrow) await page.setViewportSize({ width: 768, height: 900 }); + await ready(page, { split: false }); + await page + .getByRole('button', { name: 'Appearance', exact: true }) + .click(); + const dialog = page.getByRole('dialog', { + name: 'Appearance', + exact: true, + }); + await dialog.getByLabel('Theme', { exact: true }).selectOption(theme); + await dialog.getByRole('button', { name: 'Done', exact: true }).click(); + const tank = page.getByRole('group', { + name: 'part tank : Tank', + exact: true, + }); + await tank.getByText('tank', { exact: true }).click(); + await expect(tank).toHaveClass(/selected/); + await page.mouse.move(0, 0); + await expect( + page.getByRole('region', { name: 'Diagram', exact: true }), + ).toHaveScreenshot( + `selected-structure-${theme}${narrow ? '-narrow' : ''}.png`, + ); + }); + } +} + +for (const theme of ['light', 'dark']) { + for (const width of [390, 600]) { + test(`mobile project header in ${theme} at ${width}`, async ({ page }) => { + await ready(page, { split: false }); + await page.setViewportSize({ width, height: 900 }); + await page + .getByRole('button', { name: 'Appearance', exact: true }) + .click(); + const dialog = page.getByRole('dialog', { + name: 'Appearance', + exact: true, + }); + await dialog.getByLabel('Theme', { exact: true }).selectOption(theme); + await dialog.getByRole('button', { name: 'Done', exact: true }).click(); + await expect(dialog).not.toBeVisible(); + await page.mouse.move(0, 0); + await expect(page.getByRole('banner')).toHaveScreenshot( + `mobile-header-${theme}-${width}.png`, + ); + }); + } +} diff --git a/tests/e2e/visual.spec.ts-snapshots/assembly-dark-visual-linux.png b/tests/e2e/visual.spec.ts-snapshots/assembly-dark-visual-linux.png index 733fb8c..7b917df 100644 Binary files a/tests/e2e/visual.spec.ts-snapshots/assembly-dark-visual-linux.png and b/tests/e2e/visual.spec.ts-snapshots/assembly-dark-visual-linux.png differ diff --git a/tests/e2e/visual.spec.ts-snapshots/assembly-light-visual-linux.png b/tests/e2e/visual.spec.ts-snapshots/assembly-light-visual-linux.png index ce03d93..425994e 100644 Binary files a/tests/e2e/visual.spec.ts-snapshots/assembly-light-visual-linux.png and b/tests/e2e/visual.spec.ts-snapshots/assembly-light-visual-linux.png differ diff --git a/tests/e2e/visual.spec.ts-snapshots/diagnostics-dark-narrow-visual-linux.png b/tests/e2e/visual.spec.ts-snapshots/diagnostics-dark-narrow-visual-linux.png index 528d9da..313c2ff 100644 Binary files a/tests/e2e/visual.spec.ts-snapshots/diagnostics-dark-narrow-visual-linux.png and b/tests/e2e/visual.spec.ts-snapshots/diagnostics-dark-narrow-visual-linux.png differ diff --git a/tests/e2e/visual.spec.ts-snapshots/diagnostics-dark-visual-linux.png b/tests/e2e/visual.spec.ts-snapshots/diagnostics-dark-visual-linux.png index e8accb6..e656706 100644 Binary files a/tests/e2e/visual.spec.ts-snapshots/diagnostics-dark-visual-linux.png and b/tests/e2e/visual.spec.ts-snapshots/diagnostics-dark-visual-linux.png differ diff --git a/tests/e2e/visual.spec.ts-snapshots/diagnostics-light-narrow-visual-linux.png b/tests/e2e/visual.spec.ts-snapshots/diagnostics-light-narrow-visual-linux.png index 288fa41..4eaac3b 100644 Binary files a/tests/e2e/visual.spec.ts-snapshots/diagnostics-light-narrow-visual-linux.png and b/tests/e2e/visual.spec.ts-snapshots/diagnostics-light-narrow-visual-linux.png differ diff --git a/tests/e2e/visual.spec.ts-snapshots/diagnostics-light-visual-linux.png b/tests/e2e/visual.spec.ts-snapshots/diagnostics-light-visual-linux.png index 7a6c76e..5faf932 100644 Binary files a/tests/e2e/visual.spec.ts-snapshots/diagnostics-light-visual-linux.png and b/tests/e2e/visual.spec.ts-snapshots/diagnostics-light-visual-linux.png differ diff --git a/tests/e2e/visual.spec.ts-snapshots/landing-dark-1440-visual-linux.png b/tests/e2e/visual.spec.ts-snapshots/landing-dark-1440-visual-linux.png new file mode 100644 index 0000000..387a6ab Binary files /dev/null and b/tests/e2e/visual.spec.ts-snapshots/landing-dark-1440-visual-linux.png differ diff --git a/tests/e2e/visual.spec.ts-snapshots/landing-dark-390-visual-linux.png b/tests/e2e/visual.spec.ts-snapshots/landing-dark-390-visual-linux.png new file mode 100644 index 0000000..c753822 Binary files /dev/null and b/tests/e2e/visual.spec.ts-snapshots/landing-dark-390-visual-linux.png differ diff --git a/tests/e2e/visual.spec.ts-snapshots/landing-dark-768-visual-linux.png b/tests/e2e/visual.spec.ts-snapshots/landing-dark-768-visual-linux.png new file mode 100644 index 0000000..5cbc926 Binary files /dev/null and b/tests/e2e/visual.spec.ts-snapshots/landing-dark-768-visual-linux.png differ diff --git a/tests/e2e/visual.spec.ts-snapshots/landing-light-1440-visual-linux.png b/tests/e2e/visual.spec.ts-snapshots/landing-light-1440-visual-linux.png new file mode 100644 index 0000000..0c3e9ba Binary files /dev/null and b/tests/e2e/visual.spec.ts-snapshots/landing-light-1440-visual-linux.png differ diff --git a/tests/e2e/visual.spec.ts-snapshots/landing-light-390-visual-linux.png b/tests/e2e/visual.spec.ts-snapshots/landing-light-390-visual-linux.png new file mode 100644 index 0000000..f9712fe Binary files /dev/null and b/tests/e2e/visual.spec.ts-snapshots/landing-light-390-visual-linux.png differ diff --git a/tests/e2e/visual.spec.ts-snapshots/landing-light-768-visual-linux.png b/tests/e2e/visual.spec.ts-snapshots/landing-light-768-visual-linux.png new file mode 100644 index 0000000..75af6eb Binary files /dev/null and b/tests/e2e/visual.spec.ts-snapshots/landing-light-768-visual-linux.png differ diff --git a/tests/e2e/visual.spec.ts-snapshots/launcher-dark-1440-visual-linux.png b/tests/e2e/visual.spec.ts-snapshots/launcher-dark-1440-visual-linux.png new file mode 100644 index 0000000..e9ccf5f Binary files /dev/null and b/tests/e2e/visual.spec.ts-snapshots/launcher-dark-1440-visual-linux.png differ diff --git a/tests/e2e/visual.spec.ts-snapshots/launcher-dark-390-visual-linux.png b/tests/e2e/visual.spec.ts-snapshots/launcher-dark-390-visual-linux.png new file mode 100644 index 0000000..538441b Binary files /dev/null and b/tests/e2e/visual.spec.ts-snapshots/launcher-dark-390-visual-linux.png differ diff --git a/tests/e2e/visual.spec.ts-snapshots/launcher-dark-768-visual-linux.png b/tests/e2e/visual.spec.ts-snapshots/launcher-dark-768-visual-linux.png new file mode 100644 index 0000000..870d57a Binary files /dev/null and b/tests/e2e/visual.spec.ts-snapshots/launcher-dark-768-visual-linux.png differ diff --git a/tests/e2e/visual.spec.ts-snapshots/launcher-light-1440-visual-linux.png b/tests/e2e/visual.spec.ts-snapshots/launcher-light-1440-visual-linux.png new file mode 100644 index 0000000..f40e087 Binary files /dev/null and b/tests/e2e/visual.spec.ts-snapshots/launcher-light-1440-visual-linux.png differ diff --git a/tests/e2e/visual.spec.ts-snapshots/launcher-light-390-visual-linux.png b/tests/e2e/visual.spec.ts-snapshots/launcher-light-390-visual-linux.png new file mode 100644 index 0000000..9bf15c9 Binary files /dev/null and b/tests/e2e/visual.spec.ts-snapshots/launcher-light-390-visual-linux.png differ diff --git a/tests/e2e/visual.spec.ts-snapshots/launcher-light-768-visual-linux.png b/tests/e2e/visual.spec.ts-snapshots/launcher-light-768-visual-linux.png new file mode 100644 index 0000000..17b84aa Binary files /dev/null and b/tests/e2e/visual.spec.ts-snapshots/launcher-light-768-visual-linux.png differ diff --git a/tests/e2e/visual.spec.ts-snapshots/mobile-header-dark-390-visual-linux.png b/tests/e2e/visual.spec.ts-snapshots/mobile-header-dark-390-visual-linux.png new file mode 100644 index 0000000..26bd991 Binary files /dev/null and b/tests/e2e/visual.spec.ts-snapshots/mobile-header-dark-390-visual-linux.png differ diff --git a/tests/e2e/visual.spec.ts-snapshots/mobile-header-dark-600-visual-linux.png b/tests/e2e/visual.spec.ts-snapshots/mobile-header-dark-600-visual-linux.png new file mode 100644 index 0000000..c91daa9 Binary files /dev/null and b/tests/e2e/visual.spec.ts-snapshots/mobile-header-dark-600-visual-linux.png differ diff --git a/tests/e2e/visual.spec.ts-snapshots/mobile-header-light-390-visual-linux.png b/tests/e2e/visual.spec.ts-snapshots/mobile-header-light-390-visual-linux.png new file mode 100644 index 0000000..7e4e44b Binary files /dev/null and b/tests/e2e/visual.spec.ts-snapshots/mobile-header-light-390-visual-linux.png differ diff --git a/tests/e2e/visual.spec.ts-snapshots/mobile-header-light-600-visual-linux.png b/tests/e2e/visual.spec.ts-snapshots/mobile-header-light-600-visual-linux.png new file mode 100644 index 0000000..016e9a2 Binary files /dev/null and b/tests/e2e/visual.spec.ts-snapshots/mobile-header-light-600-visual-linux.png differ diff --git a/tests/e2e/visual.spec.ts-snapshots/panels-dark-narrow-visual-linux.png b/tests/e2e/visual.spec.ts-snapshots/panels-dark-narrow-visual-linux.png index bff5798..160d88a 100644 Binary files a/tests/e2e/visual.spec.ts-snapshots/panels-dark-narrow-visual-linux.png and b/tests/e2e/visual.spec.ts-snapshots/panels-dark-narrow-visual-linux.png differ diff --git a/tests/e2e/visual.spec.ts-snapshots/panels-dark-visual-linux.png b/tests/e2e/visual.spec.ts-snapshots/panels-dark-visual-linux.png index 170c451..d274296 100644 Binary files a/tests/e2e/visual.spec.ts-snapshots/panels-dark-visual-linux.png and b/tests/e2e/visual.spec.ts-snapshots/panels-dark-visual-linux.png differ diff --git a/tests/e2e/visual.spec.ts-snapshots/panels-light-narrow-visual-linux.png b/tests/e2e/visual.spec.ts-snapshots/panels-light-narrow-visual-linux.png index e8e1763..39b411c 100644 Binary files a/tests/e2e/visual.spec.ts-snapshots/panels-light-narrow-visual-linux.png and b/tests/e2e/visual.spec.ts-snapshots/panels-light-narrow-visual-linux.png differ diff --git a/tests/e2e/visual.spec.ts-snapshots/panels-light-visual-linux.png b/tests/e2e/visual.spec.ts-snapshots/panels-light-visual-linux.png index 2f394d3..5d06194 100644 Binary files a/tests/e2e/visual.spec.ts-snapshots/panels-light-visual-linux.png and b/tests/e2e/visual.spec.ts-snapshots/panels-light-visual-linux.png differ diff --git a/tests/e2e/visual.spec.ts-snapshots/selected-structure-dark-narrow-visual-linux.png b/tests/e2e/visual.spec.ts-snapshots/selected-structure-dark-narrow-visual-linux.png new file mode 100644 index 0000000..c181fab Binary files /dev/null and b/tests/e2e/visual.spec.ts-snapshots/selected-structure-dark-narrow-visual-linux.png differ diff --git a/tests/e2e/visual.spec.ts-snapshots/selected-structure-dark-visual-linux.png b/tests/e2e/visual.spec.ts-snapshots/selected-structure-dark-visual-linux.png new file mode 100644 index 0000000..1b3c6e2 Binary files /dev/null and b/tests/e2e/visual.spec.ts-snapshots/selected-structure-dark-visual-linux.png differ diff --git a/tests/e2e/visual.spec.ts-snapshots/selected-structure-light-narrow-visual-linux.png b/tests/e2e/visual.spec.ts-snapshots/selected-structure-light-narrow-visual-linux.png new file mode 100644 index 0000000..639c9e7 Binary files /dev/null and b/tests/e2e/visual.spec.ts-snapshots/selected-structure-light-narrow-visual-linux.png differ diff --git a/tests/e2e/visual.spec.ts-snapshots/selected-structure-light-visual-linux.png b/tests/e2e/visual.spec.ts-snapshots/selected-structure-light-visual-linux.png new file mode 100644 index 0000000..b452082 Binary files /dev/null and b/tests/e2e/visual.spec.ts-snapshots/selected-structure-light-visual-linux.png differ diff --git a/tests/e2e/visual.spec.ts-snapshots/source-editor-dark-narrow-visual-linux.png b/tests/e2e/visual.spec.ts-snapshots/source-editor-dark-narrow-visual-linux.png index b1a5655..42f160c 100644 Binary files a/tests/e2e/visual.spec.ts-snapshots/source-editor-dark-narrow-visual-linux.png and b/tests/e2e/visual.spec.ts-snapshots/source-editor-dark-narrow-visual-linux.png differ diff --git a/tests/e2e/visual.spec.ts-snapshots/source-editor-dark-visual-linux.png b/tests/e2e/visual.spec.ts-snapshots/source-editor-dark-visual-linux.png index 2191dd4..6838099 100644 Binary files a/tests/e2e/visual.spec.ts-snapshots/source-editor-dark-visual-linux.png and b/tests/e2e/visual.spec.ts-snapshots/source-editor-dark-visual-linux.png differ diff --git a/tests/e2e/visual.spec.ts-snapshots/source-editor-light-narrow-visual-linux.png b/tests/e2e/visual.spec.ts-snapshots/source-editor-light-narrow-visual-linux.png index d89463d..5dd2074 100644 Binary files a/tests/e2e/visual.spec.ts-snapshots/source-editor-light-narrow-visual-linux.png and b/tests/e2e/visual.spec.ts-snapshots/source-editor-light-narrow-visual-linux.png differ diff --git a/tests/e2e/visual.spec.ts-snapshots/source-editor-light-visual-linux.png b/tests/e2e/visual.spec.ts-snapshots/source-editor-light-visual-linux.png index bd4eabc..13893da 100644 Binary files a/tests/e2e/visual.spec.ts-snapshots/source-editor-light-visual-linux.png and b/tests/e2e/visual.spec.ts-snapshots/source-editor-light-visual-linux.png differ diff --git a/tests/e2e/visual.spec.ts-snapshots/symbol-information-dark-narrow-visual-linux.png b/tests/e2e/visual.spec.ts-snapshots/symbol-information-dark-narrow-visual-linux.png index 4e6cfd1..1f72102 100644 Binary files a/tests/e2e/visual.spec.ts-snapshots/symbol-information-dark-narrow-visual-linux.png and b/tests/e2e/visual.spec.ts-snapshots/symbol-information-dark-narrow-visual-linux.png differ diff --git a/tests/e2e/visual.spec.ts-snapshots/symbol-information-dark-visual-linux.png b/tests/e2e/visual.spec.ts-snapshots/symbol-information-dark-visual-linux.png index e3efb62..837612a 100644 Binary files a/tests/e2e/visual.spec.ts-snapshots/symbol-information-dark-visual-linux.png and b/tests/e2e/visual.spec.ts-snapshots/symbol-information-dark-visual-linux.png differ diff --git a/tests/e2e/visual.spec.ts-snapshots/symbol-information-light-narrow-visual-linux.png b/tests/e2e/visual.spec.ts-snapshots/symbol-information-light-narrow-visual-linux.png index 1d3faf7..63b8997 100644 Binary files a/tests/e2e/visual.spec.ts-snapshots/symbol-information-light-narrow-visual-linux.png and b/tests/e2e/visual.spec.ts-snapshots/symbol-information-light-narrow-visual-linux.png differ diff --git a/tests/e2e/visual.spec.ts-snapshots/symbol-information-light-visual-linux.png b/tests/e2e/visual.spec.ts-snapshots/symbol-information-light-visual-linux.png index 25e5276..2ee5eab 100644 Binary files a/tests/e2e/visual.spec.ts-snapshots/symbol-information-light-visual-linux.png and b/tests/e2e/visual.spec.ts-snapshots/symbol-information-light-visual-linux.png differ diff --git a/tests/e2e/visual.spec.ts-snapshots/workbench-dark-visual-linux.png b/tests/e2e/visual.spec.ts-snapshots/workbench-dark-visual-linux.png index b2d1f93..fd7c616 100644 Binary files a/tests/e2e/visual.spec.ts-snapshots/workbench-dark-visual-linux.png and b/tests/e2e/visual.spec.ts-snapshots/workbench-dark-visual-linux.png differ diff --git a/tests/e2e/visual.spec.ts-snapshots/workbench-light-visual-linux.png b/tests/e2e/visual.spec.ts-snapshots/workbench-light-visual-linux.png index 286748b..34b6781 100644 Binary files a/tests/e2e/visual.spec.ts-snapshots/workbench-light-visual-linux.png and b/tests/e2e/visual.spec.ts-snapshots/workbench-light-visual-linux.png differ diff --git a/tests/e2e/visual.spec.ts-snapshots/workbench-narrow-visual-linux.png b/tests/e2e/visual.spec.ts-snapshots/workbench-narrow-visual-linux.png index 3ed3041..b9af34e 100644 Binary files a/tests/e2e/visual.spec.ts-snapshots/workbench-narrow-visual-linux.png and b/tests/e2e/visual.spec.ts-snapshots/workbench-narrow-visual-linux.png differ diff --git a/tests/e2e/workspace.spec.ts b/tests/e2e/workspace.spec.ts index 09517e1..649475d 100644 --- a/tests/e2e/workspace.spec.ts +++ b/tests/e2e/workspace.spec.ts @@ -7,7 +7,7 @@ import { openWorkshop, selectModelElement, modelTreeItem, - waitForStartup, + startExample, openView, diagramNode, diagramState, @@ -24,9 +24,18 @@ test('loads real source-defined views and keeps the view stable when changing fi page.getByRole('textbox', { name: 'SysML source' }), ).toHaveSource(/package WorkshopViews/); const diagram = page.getByRole('region', { name: 'Diagram', exact: true }); - await expect(diagram.getByText('overview', { exact: true })).toBeVisible(); + const view = diagram.getByRole('combobox', { + name: 'Named view', + exact: true, + }); + await expect(view).toBeVisible(); + await expect(view).toHaveValue('WorkshopViews::overview'); + await expect(diagramNode(page, 'Workshop::Motor')).toBeVisible(); await openView(page, 'compressor'); - await expect(diagram.getByText('compressor', { exact: true })).toBeVisible(); + await expect(view).toHaveValue('WorkshopViews::compressor'); + await expect( + page.getByRole('textbox', { name: 'SysML source' }), + ).toHaveSource(/package WorkshopViews/); await expect(diagramNode(page, 'primary : Motor')).toBeVisible(); }); test('retains an invalid draft and the last valid diagram', async ({ @@ -108,11 +117,20 @@ test('creates a native view declaration through a reviewed transaction', async ( await expect( page.getByRole('button', { name: 'CompressorDetail', exact: true }), ).toBeVisible(); - await expect( - page - .getByRole('region', { name: 'Diagram', exact: true }) - .getByText('CompressorDetail', { exact: true }), - ).toBeVisible(); + const view = page + .getByRole('region', { name: 'Diagram', exact: true }) + .getByRole('combobox', { name: 'Named view', exact: true }); + await expect(view).toBeVisible(); + await expect(view).toHaveValue('Workshop::CompressorDetail'); + await expect(diagramNode(page, 'primary : Motor')).toBeVisible(); + await expect(diagramNode(page, 'standby : Motor')).toBeVisible(); + const bundle = (await downloadProject(page)).bundle; + expect( + bundle.documents.some((file) => + file.text.includes('view CompressorDetail'), + ), + ).toBe(true); + expect(bundle.drafts).toEqual(bundle.documents); }); test('downloads and reopens a project with the draft and diagram positions intact', async ({ page, @@ -163,7 +181,7 @@ test('downloads and reopens a project with the draft and diagram positions intac // Reopen into a fresh page so the old renderer cannot supply the position. await page.reload(); - await waitForStartup(page); + await startExample(page); await page.locator('input[type=file]').setInputFiles({ name: 'saved.sysml-project.json', mimeType: 'application/json', diff --git a/tsconfig.json b/tsconfig.json index b722a19..7f39a86 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -7,6 +7,8 @@ "jsx": "react-jsx", "strict": true, "noUncheckedIndexedAccess": true, + "noUnusedLocals": true, + "noUnusedParameters": true, "noEmit": true, "skipLibCheck": true, "resolveJsonModule": true, diff --git a/vite.config.ts b/vite.config.ts index 0e3e9de..29f18f4 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -5,6 +5,8 @@ import { defineConfig } from 'vitest/config'; export default defineConfig({ plugins: [react(), tailwindcss()], publicDir: 'static', + // Generated engine sources, caches and captures are not app inputs. + server: { watch: { ignored: ['**/sandbox/**', '**/static/vendor/**'] } }, resolve: { alias: { $lib: fileURLToPath(new URL('./src/lib', import.meta.url)) }, },