The application is a single Rust package named n3, with private implementation
modules. Its documentation adapter consumes the separately maintained
lgtmd SDK at an exact Git revision.
SDK sources and portable examples live in that repository. Read AGENTS.md for development principles and iteration
guidance. Testing indexes verification workflows, reference suites,
and benchmark resources. Start at the architecture map and
the milestone review. The old viewer-only
spike has been removed; the root package holds the maintained application.
Install Rust, just, and Python 3. Native app development on macOS also needs
the command-line developer tools. Docker is optional for reproducing Ubuntu CI. From the
repository root:
just run --empty
just build
just check
just cargo run --locked -- --check examples/cube.n3.json
just test
just verifyThe editor initially opens at 1440 × 900 logical points, capped to 90% of the primary monitor's full dimensions. This leaves desktop margins; winit does not expose the usable work area excluding the Dock and menu bar. The usual minimum is 820 × 500 points, reduced when necessary to fit the capped launch size. Executable guide captures keep their own fixed dimensions.
For focused component development, run just workbench. The internal
UI workbench skill owns the agent workflow
for fixture cases, deterministic evidence and component boundaries.
Rust 1.98.1 and just 1.46 are the pinned development tools. The repository's
rust-toolchain.toml selects Rust with Clippy and rustfmt without changing the
global toolchain. Cargo.toml retains Rust 1.95 as the minimum supported
version; the development pin and minimum serve different roles. The root
justfile serves local development first: just test, just docs check/update,
and just verify use the native toolchain and renderer. On macOS that is Metal;
on Linux it is an available Vulkan adapter. No Docker daemon or CI environment
marker is required. A missing native capture adapter still fails visibly.
just cargo ARGS... forwards other Cargo commands to the native host. Native
recipes use repository-local Cargo/cache directories and respect CARGO_HOME
and CARGO_TARGET_DIR overrides. After dependencies are available, set
CARGO_NET_OFFLINE=true for offline native runs. Dependency versions live in
Cargo.lock; verification uses --locked.
For an explicit Ubuntu CI reproduction, use just ci, just ci-test [ARGS...],
or just ci-docs check/update. Only these commands require Docker. Their runner
uses separate caches under .cache/ci/linux-amd64 and mounts source read-only
for checks. ci-docs update writes the separate Linux receipt and ignored review
captures while keeping the native guide read-only. No generated app bundle,
cache, or review capture belongs in Git.
Hosted Ubuntu CI runs two independent jobs: just ci checks performs formatting,
Clippy, tooling tests, and all Rust tests except the full-guide comparison;
just ci guide runs that exact comparison. Both discover the current Rust test
inventory and reject a missing or ignored guide test, overlapping partitions, or omitted
tests. just ci still runs the complete sequence in one container, and native
just verify, just test, and the pre-push hook always retain the full suite.
Hosted jobs cache dependency downloads and compiled build output under keys
that include their platform, pinned toolchain/environment, Cargo manifests, and
vendored dependency source.
Incremental compilation output, generated guide artifacts, renderer receipts,
diagnostics, and account files are excluded. Pull requests can restore caches;
only successful checks, native, and WASM jobs on pushes to main save them. The guide
job restores the shared Ubuntu cache without publishing it. Cache hits still
execute every check, including the exact guide replay. The macOS job also caches
the pinned just installation while retaining its ordinary installation command.
Containers retain the invoking user's numeric UID/GID so writable cache and review files keep their host ownership. The runner mounts isolated passwd/group entries read-only from its cache; the terminal backend needs a resolvable account even when its controlled shell is explicitly configured.
The browser target uses the same package with an additional WASM library entry point. The native application, documentation renderer, terminal backend, and filesystem host remain target-specific. Both hosts use the same workspace and user guide; host capabilities omit or disable unsupported controls. Read platform boundaries and performance before extending the port or changing host ownership.
For opt-in native/browser CPU-stage and frame-pacing measurements, use
just measure-viewport. It builds the
production hosts with contributor instrumentation and records raw samples;
ordinary builds do not include the recorder or measurement exports.
just web-setup
just web-check
just web-verify
just web-build
just web-build --profile release
just webweb-setup installs the wasm32-unknown-unknown target for the repository's Rust
toolchain and the matching wasm-bindgen-cli version, 0.2.129, under
.cache/web-tools. web-check checks the library for that target with the locked
dependency graph. web-build defaults to the web development Cargo profile;
--profile release selects the optimized production build. Both write the static
application to ignored build/web/, including pkg/n3.js, pkg/n3_bg.wasm, and
the bundled font licenses.
It requires no Node, bundler, React installation, Docker, or application server.
web-verify runs the wrapper lifecycle regressions with Node.js 24, Clippy with
warnings denied for the WASM library, and the complete static-site build. A
dedicated Ubuntu WASM CI job runs the same wrapper tests and lint, builds the
release artifact, and checks the optional measurement adapter. It supplements the
native jobs without requiring a browser GPU on that runner.
The pinned egui-winit 0.36.2 adapter requires narrow WASM compatibility patches
for its dropped-file trait and browser OS modifier mapping. The maintained source, upstream
licenses, and exact change are recorded in
vendor/egui-winit/N3-PATCH.md. Native behavior
is preserved; browser file contents enter through N3's selected-byte adapter.
just web builds and serves that directory on localhost port 8000, then opens
the browser. To select another port or leave browser opening to another tool:
just web --no-open --port 8001Keep the server running while using the application; Ctrl-C stops it. A desktop
browser with WebGPU enabled and available is required. Remote hosting requires
HTTPS; opening the generated HTML as a local file: URL is not the supported
launch path. The output is static; module and WASM URLs resolve relative to the
site, including when it is hosted under a repository subpath.
Target compilation and browser runtime checks are separate from just verify.
The native verification command continues to check shared behavior and the
strict documentation baseline. Browser runtime evidence must additionally record
successful WebGPU initialization, rendering, interaction, file round-tripping,
and lifecycle behavior on the browser actually tested. These host checks use the
same feature contract; they do not create a second guide or media baseline.
Include a fresh-load input check before switching focus away from the canvas or using controls in an embedding page. Move the pointer into the viewport, use Shift+I to open Insert, create an object, click empty space to deselect, and double-click the object to enter edit mode. Check held Z and Space through the shared shading and navigation behavior, then verify focus loss cancels held input and returning to the canvas restores ordinary interaction. Repeat with multiple objects to check selection changes. Using wrapper buttons first can hide a startup focus bug by causing an extra blur/focus transition.
The browser host initializes egui focus from the current window after asynchronous WebGPU setup; the canvas may already have received its initial focus event before the input adapter exists. Keep subsequent focus events ordered through the usual adapter. The local server disables HTTP caching, but an already-open tab retains its loaded WASM until reloaded after a rebuild.
The canonical repository publishes the web app through
the CI workflow. Its WASM job packages only
build/web/ as a GitHub Pages artifact. The deployment job runs for a push to
gridaco/n3's main branch after all four CI lanes succeed. Pull requests and
forks build and verify without publishing. Failed verification leaves the
previous deployment in place.
GitHub Pages uses the GitHub Actions source and the github-pages environment.
Only the deployment job receives Pages and OIDC write permissions. Main CI runs
finish before their successor starts, so a new push cannot interrupt an active
deployment. To retry a failed deployment, rerun the failed job while its artifact
is retained; otherwise rerun the entire main CI run to rebuild the artifact.
Generated WASM, JavaScript glue, and site copies stay in ignored build/ and
Actions artifacts. No deployment branch or generated commits are needed. The
published site tracks verified main development; it is not a versioned release.
See GitHub's custom Pages workflows
for the hosting contract. A new fork must explicitly configure its own site and
adjust the canonical-repository deployment conditions before publishing.
The browser host captures wheel and WebKit gesture events on its canvas, then passes them through browser_navigation into the shared navigation router. The router retains viewport, focus, popup, and active-edit ownership. Canvas capture prevents the browser's default page zoom for an editor pinch; it does not install page-wide gesture handlers. Ordinary scrolling over egui controls remains UI input rather than camera input.
Browsers encode trackpad pinch as a wheel event with ctrlKey, or as WebKit's
cumulative scale and rotation. The wheel flag identifies the zoom event; it
must not become a synthetic held keyboard Control modifier. A real Control-wheel
combination is indistinguishable from pinch and also zooms. WebKit samples become
relative zoom and rotation deltas; events from the same gesture must not also
reach the camera through winit's wheel path.
Pixel deltas use CSS pixels converted to egui logical points, independently of the display's backing-pixel ratio. Line deltas retain wheel-line units; page deltas use the canvas's CSS height. Browsers provide no reliable wheel-device identity: unmodified pixel scrolling uses the shared precise-scroll policy, including when a high-resolution mouse produces it. The adapter does not infer finger or momentum start/end phases from timing. WebKit exposes rotation; Chromium's pinch-wheel events contain no twist information.
The shared navigation scenario exercises these conversions and ownership rules. DOM delivery, browser default prevention, and physical trackpad feel require browser testing; native replay alone cannot establish them.
The framework-neutral wrapper exports
mountN3(container, { onState, onError }). It creates and sizes the canvas, awaits
GPU startup, and returns a controller:
| Method | Behavior |
|---|---|
command(id) |
Dispatch a semantic action, such as insert.cube or history.undo, then focus the canvas. |
open(file, { append }) |
Read a browser File and confirm replacement when needed; append defaults to false. |
download(filename) |
Request a document download; the default name is Untitled.n3.json. |
documentJSON() |
Return serialized document text. |
snapshot() |
Return the parsed status object. |
destroy() |
Stop the editor, disconnect observation, and remove listeners and the canvas. |
The controller exposes its canvas. onState receives status changes including
object and selection counts, dirty state, dimensions, and errors; callbacks run
after the Rust state borrow is released. A JavaScript or React application may
own surrounding UI while Rust retains workspace behavior and document editing.
This experimental bridge does not expose arbitrary document mutation, GPU
resources, or a stable UI-independent editor API.
import { mountN3 } from "/n3/n3.js";
const editor = await mountN3(document.querySelector("#editor"), {
onState: (state) => console.log(state.objects, state.dirty),
onError: (error) => console.error(error),
});Serve n3.js and its generated pkg/ together under /n3/ in this example.
Give the container an explicit usable size. Preserve the wrapper's file
confirmation and teardown behavior when extending it.
The lower-level generated start(canvas) export returns a WebApp handle and
initializes the GPU asynchronously. The canvas emits n3-ready or n3-error;
methods require a ready application. Its methods are command(id),
load_file(name, bytes, append), document_json(), new_document(),
settings_json(), snapshot(), resize(width, height), and destroy().
Direct callers must resolve unsaved changes before replacing the document.
Mounting is asynchronous: unmounting before startup completes must still destroy the eventual controller. The baseline supports one editor lifetime per WASM module; destroying it does not establish that the module can mount again. Direct React effects that remount, including development StrictMode, therefore require additional lifecycle work. Ordinary React mounting and removal can use an iframe, which gives each frame its own module lifetime:
export function N3Panel() {
return (
<iframe
title="N3 editor"
src="/n3/index.html"
style={{ width: "100%", height: "80vh", border: 0 }}
/>
);
}A cross-frame command/state bridge is not implemented. Direct mounting provides that integration in a persistent page; multiple editors within one module need a separate lifecycle design and verification.
The host accepts selected bytes for .n3.json, OBJ, and self-contained glTF/GLB.
Parsing and unit conversion remain in asset_io. Selected filenames are labels,
not directory access: external buffers, textures, and linked source files need a
resource resolution workflow. External asset resources fail explicitly; native
documents preserve unresolved references for recovery. Saving does not embed
imported assets or make those references portable.
Downloads cannot confirm durable storage or promise an in-place overwrite or
native save-conflict detection. The dirty indicator therefore remains set after
a download request. The wrapper confirms destructive replacement and requests a
beforeunload warning, subject to
browser restrictions;
this is not a recovery store.
Preferences use the shared typed controller and an origin-local store, separate from documents and history. A different host or port has a different store. Storage can be denied, cleared, or temporary; failures remain visible without preventing editing. The read-before-write merge check is optimistic and has no atomic cross-tab lock. Local edits, storage events from other tabs, and focus recovery trigger merges at safe input boundaries. Successful synchronization has no periodic polling; storage failures retry with a bounded backoff. The native filesystem store keeps its independent synchronization policy.
- WebGPU requires a secure context and a compatible browser, device, and policy. There is no WebGL fallback; initialization failures remain visible.
- The host cannot launch a local shell/PTY or open a settings file in an external editor. Those controls are omitted from the shared UI.
- Browser-reserved shortcuts may not reach the canvas. Focused-workspace input must preserve surrounding page controls. The gesture adapter above maps the events the browser exposes without claiming native phase or momentum parity.
- The egui adapter uses an internal clipboard fallback; external clipboard integration requires a browser adapter.
- The application runs on the main thread. Large decoding and geometry work can interrupt responsiveness; workers and shared memory require separate work.
- Touch, IME, accessibility, physical trackpads, and browser/GPU coverage remain review gates. A desktop smoke test does not establish these capabilities.
- One canvas is the presentation unit. Native multi-window delivery and multiple editors in one module remain outside the baseline.
Native contributor formatting additionally uses Node.js 24/npm with npx.
tools/format_docs.py holds the single Oxfmt version pin and invokes that
version through npx. Run just tools-install to warm its npm cache, or
just setup to do that and activate the Git hook. The default cache is
.cache/npm; set npm_config_cache to use another writable location. The
canonical CI image includes Node/npm and uses the same wrapper. No repository
package.json, package-lock.json, or local node_modules installation is
needed. The application and local guide preview do not require Node.
just fmt runs rustfmt and Oxfmt; just fmt-check checks both without writing.
The separate fmt-rust / fmt-rust-check and fmt-docs / fmt-docs-check recipes
are useful during focused work. Most Oxfmt input is Markdown; it also handles
the repository's supported configuration and browser-preview sources.
Oxfmt options and exclusions live in .oxfmtrc.json. TODO.md is deliberately
excluded from formatting but remains repository-owned and eligible for Git.
Generated guides, vendored preview code, the Cargo lockfile, and example
documents are also excluded. The wrapper's --check and --write modes format
authored sources; for .md.in templates it sends Markdown through Oxfmt's CLI
stdin, verifies that semantic bindings and their order survive, and validates
all templates before writing any of them. After formatting changes a guide
template, use just docs update and review the generated text; never format
generated pages independently.
The published guide baseline uses native macOS Metal. Local development compares
every artifact directly against that guide. Ubuntu CI replays the same scenarios
with the pinned lavapipe renderer and checks every artifact's byte length and
SHA-256 against docs/baselines/linux-vulkan-lavapipe.json. The receipt also pins
the entire native guide, and generated Markdown must match its published text.
Missing receipts, stale native guides, inventory changes and byte drift fail;
there are no image tolerances, skipped captures or automatic updates.
After deliberately updating and reviewing the native guide, run
just ci-docs update to generate the Linux review tree under
.cache/docs/linux-vulkan-lavapipe/ and update its receipt. Inspect that text,
stills and animation playback, then run just ci-docs check. Only this explicit
Ubuntu reproduction requires Docker; native development remains independent.
just setup installs contributor dependencies and configures
core.hooksPath=.githooks in local Git configuration, shared by the clone's linked
worktrees. just hooks-install can be run separately and refuses to replace
another configured hooks directory or disable existing active default hooks.
Each clone needs setup.
The pre-push hook checks the commit being pushed: non-deletion refs must resolve
to the current HEAD, with no staged, unstaged, or nonignored untracked changes.
It runs just verify and checks that the checkout did not change during the run.
Switch to the branch to be verified before pushing it. Deletion-only pushes have
no source to verify. The hook never rewrites files or updates generated baselines.
just verify runs Oxfmt and rustfmt checks, Clippy with warnings denied, Python
tooling tests, JavaScript wrapper regressions, and all Rust tests on the host. The Rust suite includes
required documentation replay and exact artifact comparisons. just test
forwards optional Cargo test arguments; just tools-test runs the Python and
JavaScript suites.
Checks never update the guide, including when a renderer or driver changes.
The optional CI image fixes Ubuntu 24.04 on
linux/amd64, a 2026-09-28 APT snapshot, and Mesa lavapipe software Vulkan with
sse2 CPU capabilities and 256-bit vectors, with its shader cache disabled. This
selects accurate sqrt for sRGB conversion and prevents cached 128-bit code from
bypassing that choice. The baseline contract records the
cross-host evidence and diagnostic commands. It requests N3_DOCS_RENDERER=lavapipe explicitly;
normal developer commands select the native backend without that setting.
The opt-in container uses one Rust test thread. Default parallel execution
reproduced allocator aborts and a segmentation fault among GPU-backed tests on
both the committed native baseline and the browser port. The underlying fault
is not established; serial test scheduling is the contained CI policy. All
tests and exact captures still execute, and native just verify retains the
host's ordinary concurrency. Explicit ci-test arguments can override test
thread count for diagnosis.
Updating a rendering profile requires reviewing its generated output. A Docker
build alone does not establish that the guide agrees with its baseline.
Hosted macOS CI separately runs just ci-macos: build the app, verify its ad-hoc
signature, bundled icon, and metadata, and run focused native:: tests. The
repository assembles the .app itself in just build; Cargo builds its executable
and the recipe supplies Info.plist, resources, and local ad-hoc signing. The
macOS icon is checked in at assets/logo/N3.icns and regenerated from the SVG
source with just tools icon when the logo changes. General CI stays on Ubuntu.
just test-metal is a focused capture check on a Mac with GPU access;
just test runs the complete native suite.
Hook tests use temporary repositories and a stub verifier to check rejection and
failure propagation without pushing or changing this checkout. just hooks-test
runs them directly. The CI workflow becomes live when the repository is pushed.
Every user-guide feature has one registered Rust scenario under
src/documentation/scenarios/ and exactly one narrative source. Registration
lives in src/documentation/features.rs: template: None means the scenario
authors the guide through the N3 Guide adapter; Some(...) selects its retained
Markdown template under docs/templates/. The hand-tool and navigation
features now author their prose in Rust alongside the replay. The remaining
26 features still use templates. Do not maintain both forms for one feature.
User-facing state and behavior come from the real workspace, semantic commands, navigation adapters, and scene renderer. Assertions verify the intended outcome. The portable SDK owns document composition, frozen evidence, and shared artifact publication; it does not own N3's input or renderer. See executable documents for the maintained boundary and the SDK README for its API.
just docs
just docs update
just docs checkjust docs starts a local guide preview and opens it in the default browser.
It requires Python 3, serves only on 127.0.0.1, and chooses an available port.
The terminal prints the URL; Ctrl-C stops the server. To choose a port or leave
browser opening to another tool:
just docs serve --no-open --port 3030The preview reads the checked-in Markdown and WebP media without rebuilding the
application, running GPU captures, or changing baselines. Run just docs update
in another terminal after intentionally changing the scenario's narrative or
its retained template, then refresh the browser. just docs update regenerates through the native renderer;
just docs check verifies there without writing. Neither requires Docker.
The small preview uses a vendored Docsify runtime, so it needs no network access,
Node installation, or package-manager setup. It is a local reading surface, not
a choice of framework for the eventual documentation website.
To inspect both audiences before updating the canonical baseline, build a fresh candidate in ignored local storage:
just docs build --out .cache/docs-candidateThis performs one complete replay, then writes reader/ and contributor/
subtrees from the same completed sessions and captured bytes. The two
Rust-authored guides add contributor notes; retained template pages are identical
across audiences. The destination must be new and separate from docs/guide and
docs/baselines. This command does not accept a baseline or change the roles of
just docs check/update.
Review changed text, stills, and animated clips before accepting regenerated
outputs. check does not rewrite or approve anything. The normal test suite
replays all scenarios and checks the complete generated tree, including animation
bytes and the manifest. Missing, orphaned, duplicate, and unreferenced artifacts
fail. A generation failure writes no new baseline.
Rust-authored guides take witnessed controls and canonical shortcut labels from
the Guide adapter, and insert typed handles into their authored Markdown.
Captures remain Session operations; the adapter registers their existing bytes
without replaying the action or advancing the clock. The current migrations keep
the published guide text, media paths, media bytes, and manifest.txt format.
Retained templates render application shortcuts with {{shortcut:semantic-id}} as
<kbd> keys from the canonical input bindings, and witnessed menu paths with
{{control:id}} as inline <code>. Literal text-entry keys and gesture modifiers
use {{key:Name}} and {{modifier:name}}. Do not copy remappable keys into prose
or scenario values. Replay illustrated shortcuts through Session::shortcut,
shortcut_down, and shortcut_up; raw keys remain appropriate for literal input
and explicit input-ownership probes.
Asserted values, stills, and clips use {{value:name}}, {{image:name}}, and
{{animation:name}}.
Callouts follow live control bounds and fail when their target disappears.
See authoring and replay contracts for
timing, input ownership, animation examples, and verification boundaries.
The manifest records the renderer profile that generated the guide; the full adapter and driver identity is reported at execution time. Exact byte checks require the same renderer, and driver changes can still cause pixel drift. A profile mismatch gives a specific diagnostic. Baselines stay strict: no hidden visual tolerance, automatic update, or skipped images. The scenarios explicitly select macOS styling, so the guide still depicts the Mac application when replay runs on Ubuntu. Synthetic replay proves application behavior, not physical trackpad feel, Finder delivery, macOS dialogs, or artistic approval.
just docs-preview-test checks the preview server's routes and browser asset
contract using Python's standard library. It also runs as part of just verify;
it does not replace the Rust replay and exact artifact checks.
SDK authoring, compiler/store work, and its portable Rust/React examples are
maintained and verified in the
lgtmd repository. Follow its contributor
instructions for SDK setup, independent package checks, and Node/Chromium gates.
N3 does not install or run the SDK's web example during application verification.
The N3 app's web/ host remains a separate consumer boundary.
N3 pins the SDK's full Git revision in Cargo.toml and retains its resolved
source in Cargo.lock. Updating that revision requires just verify, including
native replay and exact guide checks; portable SDK checks alone cannot establish
N3 compatibility. Cargo must fetch the pinned source and dependencies before an
offline run. Keep the dependency reproducible without sibling checkout discovery
or a developer-specific path. A registry dependency is deferred until an
explicitly authorized SDK release is ready.
Global preferences live at ~/Library/Application Support/N3/settings.json on
macOS. Preferences can open that file in a text editor. See the
user guide for keys and recovery, and the
settings boundary before changing persistence.
Settings tests and guide replay use isolated stores; they must never load or
modify the current user's file.
.gltf and .glb open as linked asset placements in an ordinary N3 document.
Native geometry and imported placements share selection, transforms, duplication,
deletion and Undo. Source meshes, materials and animation remain immutable;
decoded resources and playback stay outside authored history. See the
imported asset contract for its format profile,
rendering, animation and resource limits. Unsupported required extensions fail;
unsupported optional extensions appear in Properties diagnostics. Saving
.n3.json preserves source references and placement; editing asset contents,
converting them to authored meshes, and glTF/GLB export are not implemented.
File Open and startup replace the current document. File Import adds objects to
it. Dropping .n3.json opens that document; dropping OBJ/glTF/GLB imports into the
current document. OBJ geometry becomes editable, while imported scene placements
retain their linked sources. --read-only explicitly disables document editing
independently of input format.
just run fixtures/gltf/WaterBottle/glTF-Binary/WaterBottle.glb
just cargo run --locked -- --check fixtures/gltf/SimpleSkin/glTF/SimpleSkin.gltfThe --check command validates and evaluates the default scene without creating
a window; it does not establish GPU appearance or full format conformance.
N3 documents use versioned JSON (.n3.json) with stable object and vertex IDs.
Canonical lengths are centimeters; fractional values remain valid. Procedural
geometry is preserved until an effective vertex edit or explicit conversion.
Returning exactly to the edit-session baseline restores its parameters.
GPU buffers, render triangles,
selection, navigation, open menus, and unfinished sessions are not document data.
OBJ is an import format. Its raw coordinate numbers are interpreted as centimeters without guessing physical scale. Display framing normalizes derived render coordinates without rewriting source geometry. Y-up is the default; Z-up only changes presentation.
The importer preserves OBJ position indices and polygon boundaries without welding coincident vertices. Nonempty object/group sections become objects; shared positions across sections appear in each object's mesh. Positive and negative indices are supported. Concave polygons are triangulated for display. Warped faces use projected triangulation with a notice; degenerate or crossing projected boundaries are rejected.
Materials, textures, UVs, authored normals, standalone lines/points, and free-form geometry are not retained. Normals are generated for display. Missing material files do not block supported mesh import. OBJ export and lossless OBJ roundtripping remain unimplemented.
Import limits: 64 MiB input, 1,000,000 coordinate records per kind, 2,000,000 total face corners, 4,096 corners per face, 10,000 object/group records, and a polygon work budget of 50 million (sum of squared polygon corner counts). These limits are safety bounds, not a large-scene performance claim.
Keep canonical data, preview sessions, committed history, derived render data, and UI state distinct. The current Editor still uses egui for picking and painting; do not treat it as a ready external kernel API. A UI or keyboard shortcut should dispatch an existing semantic action when applicable.
Add behavioral assertions at the appropriate core/input boundary and update the owning guide scenario for visible interactions. Use production input events for what a tutorial illustrates; direct state setup may prepare a fixture but must not stand in for the behavior being demonstrated. Keep experiments and speculative UX proposals separate from implemented contracts.
Promotion is a source-layout milestone. It does not imply publication, release, performance qualification, full OBJ support, or final interaction acceptance.