Skip to content

RUST-0/1: Rust core feasibility and isolated WebAssembly foundation - #130

Merged
DJAscendance merged 92 commits into
mainfrom
architecture/rust-1-wasm-boundary
Oct 12, 2026
Merged

DJAscendance merged 92 commits into
mainfrom
architecture/rust-1-wasm-boundary

Conversation

@DJAscendance

@DJAscendance DJAscendance commented Oct 8, 2026 •

Copy link
Copy Markdown
Collaborator

This is an isolated foundation and research PR. No production Rust integration is included.
Draft until the owner explicitly authorizes a status change. Do not merge.

Summary

  • RUST-0 — architecture feasibility research: a Rust core is feasible only as a pure computation core behind the existing Electron shell, reached through WebAssembly, with UTF-16 code units as the public offset unit. Includes the std-only spike and its 167,316-case differential.
  • RUST-1 — isolated boundary foundation: a Rust workspace (crates/), a real wasm32-unknown-unknown release artifact, a lossless UTF-16 boundary, one isolated session type, a reusable differential framework, Node and Electron renderer load proofs, and measured boundary costs.

Nothing in main.js, preload.js, renderer/, src/, package.json/lockfile, CI, packaging or release workflows changes. No production module loads anything from crates/.

RUST-0 findings (docs/architecture/RUST_0_*.md)

  • The VRML core runs in the renderer (esbuild bundle); a native N-API addon cannot reach it → WebAssembly is the only bridge that keeps one implementation. Electron stays (APP-ARCH-0 O1); Tauri/Wry and Qt not recommended.
  • No measured performance problem that Rust solves. The case is correctness and type safety, not speed.
  • The JS baseline has pre-existing text-fidelity gaps T1–T4 (non-UTF-8 decode, BOM diagnostics, CodeMirror line-ending normalization, surrogate-splitting edits) → separate TEXT-1 lane.
  • Staged roadmap RUST-1 … RUST-8, each with its own owner gate.

RUST-1 scope

Crates

crate contents deps
wrlforge-text UTF-16↔UTF-8 offsets, strict UTF-16 decode, WD1.2 edit algebra (applyEdits, mapOffset, mapRange, UTF-16 canonical tie-break, same-offset insertion refusal), line index (LF/CRLF/lone CR, UTF-16 columns), immutable snapshots std only
wrlforge-wasm wasm-bindgen boundary, UTF-16 gate, RawSession wasm-bindgen, js-sys

Both crates #![forbid(unsafe_code)]; no filesystem, network, clock, environment or process access. JS facade: crates/wrlforge-wasm/js/wrlforge-text.mjs.

WebAssembly bridge

  • wasm-bindgen --target web; the facade takes an initialized module and is environment-neutral. Node loads with initSync({module: bytes}); the renderer was proven with three loaders (fetch + initSync, glue default async, bytes embedded in a JS module — the shape an esbuild binary loader produces).
  • Raw wasm-bindgen objects (which expose __wbg_ptr) are never returned. Facade objects are frozen with no own properties.
  • Any uncoded failure (a wasm trap) poisons the engine; all later calls refuse with EENGINE.

UTF-16 safety rules

  • wasm-bindgen's default JS→Rust string conversion replaces unpaired surrogates with U+FFFD. Proven by experiment: a mutant artifact without the gate turns "\uDC00" into "�" silently.
  • Every JS string arrives as JsString; Rust calls native String.prototype.isWellFormed() before conversion; ill-formed → EENCODING (field, unit = first unpaired surrogate, index); method missing → EENGINE. Never substituted, never normalized.
  • A legitimate U+FFFD is ordinary text (tested separately). Leading BOM survives Rust→JS (TextDecoder ignoreBOM: true, tested). CRLF, lone CR, mixed endings and NUL are preserved.
  • Offsets inside a surrogate pair are refused (EEDITBOUNDARY / EOFFSETSURROGATE), never rounded.

Sessions

Proof is module-private WeakMap membership, never a number. Rust re-checks a per-session serial and revisions from a never-reset counter capped at Number.MAX_SAFE_INTEGER. Stale, foreign, forged, prototype-clone, cross-engine and disposed handles fail closed. Transaction verification compares the full text; no hash is trusted. WD1.4 object identity untouched; nothing written into VRML source.

Evidence (Linux x86_64; re-run from the committed tree)

gate result
Rust fmt + clippy clean (host and wasm32); 27/27 tests
Release artifact wrlforge_wasm_bg.wasm 65,892 B, SHA-256 a1e99690bf787e8542464f39fe1a1571cb05002e6912dd8ee3c4de0171910963 (reproducible across rebuilds)
Node 24.21.0 18/18 tests
Electron 41.7.1 renderer 12/12 checks; contextIsolation: true, nodeIntegration: false, sandbox: true, no preload, no --no-sandbox; driven by VisualQaRunner, no leaked process
CSP proof page uses renderer/editor.html's CSP byte-for-byte; loads. Same CSP minus 'wasm-unsafe-eval': all three loaders blocked. Existing source is necessary and sufficient; eval stays blocked. No CSP change.
Differential 177,582 cases; RUST-0 subset reproduces RUST-0's input and baseline SHA-256 exactly; exact 169,938; approved 7,419 (RUST0-BOUNDARY 3,120, RUST0-ENCODING 4,299); pending owner 225 (RUST1-PRECISION); 0 unexplained regressions
Negative controls all 4 detected: UTF-8 tie-break 646, no ambiguity check 1,597, no boundary check 2,723, lossy UTF-16 1,108
npm run check 2,625 tests: 2,621 pass, 0 fail, 4 pre-existing skips; 327 files parsed

Registry ids are harness classifications, not owner decisions D1–D9 (RUST-0's internal "D1"/"D2" labels are kept only as rust0Label). No entry approves a production change.

Performance (through the real boundary; median ms)

op 6 KB 327 KB 1.6 MB
Node: Rust applyEdits (full text in + out) 0.032 1.37 7.56
Node: JS edit.js applyEdits 0.004 0.13 0.69
Node: UTF-16 gate + copy (check_text) 0.013 0.50 3.31
Node: resident toUtf8 / lineCol ≤0.001 ≤0.001 ≤0.001
Renderer: Rust applyEdits stateless 0.1 3.2 16.4
Renderer: openSession 0.1 2.7 13.5
  • Full-text transfer makes Rust 5–11× slower than edit.js for a stateless edit. At 1.6 MB a renderer main-thread transfer costs 7–17 ms (≈ one 60 Hz frame). No speed claim is made; no X_ITE rendering claim.
  • Init 0.18 ms (Node). wasm memory 1.1 → 12.3 MB with ~3.6 MB of text open; linear memory does not shrink.
  • Details: docs/architecture/RUST_1_PERFORMANCE.md.

Licences and dependencies

wasm-bindgen =0.2.129 and js-sys =0.3.106 (default features off — drops the unsafe-eval feature and futures-util). 13 third-party crates total, all MIT OR Apache-2.0 (unicode-ident also Unicode-3.0) — GPL-3.0-or-later compatible. crates/Cargo.lock committed; toolchain pinned to 1.95.0. Build tool wasm-bindgen-cli 0.2.129 installed lane-locally (not committed). No White Dune or other upstream code. Production package.json dependency graph unchanged.

Commits

SHA subject
9c97a05 docs(architecture): record Rust core feasibility and migration plan
e92748f feat(rust): add isolated WebAssembly boundary foundation

Both DCO signed-off.

Files

Commit 1 (13, RUST-0; hash-verified against the original RUST-0 worktree): docs/architecture/RUST_0_CORE_FEASIBILITY.md, RUST_0_MIGRATION_MATRIX.md, RUST_0_MIGRATION_ROADMAP.md; spikes/rust-core-feasibility/{.gitignore, Cargo.lock, Cargo.toml, README.md, differential.js, src/{edit,lib,main,offsets,wire}.rs}.

Commit 2 (36, RUST-1):

  • crates/{.gitignore, Cargo.lock, Cargo.toml, rust-toolchain.toml}
  • crates/wrlforge-text/{Cargo.toml, src/{edit,lib,line_index,offsets,snapshot}.rs}
  • crates/wrlforge-wasm/{Cargo.toml, js/wrlforge-text.mjs, src/{lib,session}.rs}
  • docs/architecture/RUST_1_{BOUNDARY_CONTRACT,PERFORMANCE,WASM_QA}.md
  • spikes/rust-1-wasm-boundary/{.gitignore, README.md, build.mjs}
  • spikes/rust-1-wasm-boundary/electron/{main.cjs, proof.html, proof-no-wasm-eval.html, proof.mjs, run.cjs}
  • spikes/rust-1-wasm-boundary/harness/{framework.mjs, registry.json, run.mjs, stages/edit-algebra.mjs, stages/planned.mjs}
  • spikes/rust-1-wasm-boundary/{loaders/node.mjs, test/node-proof.test.mjs}
  • spikes/rust-1-wasm-boundary/perf/{bench-core.mjs, inputs.mjs, node-bench.mjs, summarize.mjs}

Not committed: target/, generated out/, local tool installs.

Known limitations

  • Linux only. macOS 15+ not tested; no macOS result is claimed.
  • Edit-algebra stage only; tokenize/parse/source-map/semantics/identity/transaction stages are interfaces (harness/stages/planned.mjs), not implementations.
  • Synthetic 1.6 MB input; private corpus not used. Renderer timer resolution ≈ 0.1 ms. Worker execution not tested.
  • Windows platform-policy conflict (repository still ships Windows) recorded, not changed.

Draft blockers (owner decisions)

  1. macOS 15+ WebAssembly and Electron QA is outstanding.
  2. The 225 RUST1-PRECISION cases need owner adjudication (status stays proposed-pending-owner).
  3. TEXT-1 source-fidelity policy (T1–T4; decisions D1–D3) remains separate and unresolved.
  4. RUST-2 needs a design that avoids repeated full-document transfers (resident text + deltas, or a Worker).
  5. Performance must be measured through the actual renderer boundary for any RUST-2 claim.
  6. Electron maintenance (41 reported EOL by RUST-0, not verified here) remains a separate lane.

Future requirements, documented only (not implemented here): UX-LOADING-0 — WRL Forge logo during X_ITE scene loading (prefer an application-owned overlay; do not assume splashScreen can replace the image); APP-SPLASH-0 — optional startup splash, evaluated near product completion, with no artificial delay.

Reproduce

See spikes/rust-1-wasm-boundary/README.md.


RUST-1A Follow-up

Commit 63f668038c4c80ae4f853e9a7076021cba15c6ba (DCO signed-off), fast-forward on top of e92748f. Spike-only: zero production changes (src/, renderer/, main.js, preload.js, package*.json, CI, packaging untouched); no dependency added (encoding_rs@=0.8.42 evaluated, not added). Full report: docs/architecture/RUST_1A_BOUNDARY_HARDENING.md. Linux only.

Confirmed defects and corrections

defect correction
A The UTF-16 gate trusted String.prototype.isWellFormed, looked up at call time. A replaced method (() => true, 1, inverted, or swapped after a session opened) let malformed UTF-16 become U+FFFD silently: 144/324 calls per lying mode, facade and every raw string export. take_text treats the method's answer as a claim only. After conversion, each U+FFFD must sit over a genuine 0xFFFD source unit (UTF-16 positions are preserved by the WHATWG encoder). Substituted surrogate → EENCODING with unit = first unpaired surrogate; lying false or a changed non-surrogate unit → EENGINE. Missing/throwing method still EENGINE. Reads k units for k U+FFFD, not n.
B A wasm trap poisoned only the engine that saw it; a second engine on the same trapped instance kept answering (reproduced with a real allocation-failure trap). Poison is keyed to the wasm instance (glue.RawSession, module-level WeakMap). Every engine, existing session handle, and new engine on a trapped instance refuses with EENGINE. Separately loaded instances stay healthy.

New regression tests

  • test/utf16-gate-adversarial.test.mjs — 16 tests: 18 entry points (9 facade, 9 raw) × 8 malformed + 10 valid inputs × 11 method modes (native, missing, deleted after init, true, false, throws, 1/2, 'yes'/{}/undefined, replaced after acceptance, genuine U+FFFD + surrogate, astral/pairs, inverted). Against the lossy-utf16 mutant: 15/16 fail (as required).
  • test/session-lifetime.test.mjs — 10 tests + 1 opt-in heavy real-trap test: open/dispose reuse, revision growth, stale/foreign/forged/disposed handles, instance poisoning, independent instances.
  • harness/precision-review.mjs — independent BigInt oracle over the 225 RUST1-PRECISION cases.
  • Electron renderer proof: 1 new check (hostile isWellFormed, facade + raw).

Results (re-run at publication, Linux x86_64, Node v24.21.0, Electron 41.7.1)

gate result
cargo fmt --all --check pass
Clippy host / wasm32-unknown-unknown (-D warnings) pass / pass
cargo test --workspace --locked 28 pass (25 + 3)
Node proof test/*.test.mjs 44 pass, 1 skip (heavy, opt-in)
Heavy real-trap test (RUST1A_HEAVY=1, run in a 10 GB memory-capped scope) pass (~11 s)
Electron renderer proof 13/13; 3/3 loaders; CSP identical to editor.html; eval blocked; no-wasm-unsafe-eval control blocked as expected
Differential 177,582 cases: 169,938 exact, 7,419 approved, 225 pending, 0 unexplained regressions; candidate digest eb44188d…7480 unchanged from RUST-1
Mutation controls all 4 detected: utf8-tiebreak 646, no-ambiguity 1,597, no-boundary 2,723, lossy-utf16 1,108
Precision review exit 0; 225/225 safe refusals; 0 genuine regressions; 0 taxonomy changes
npm run check 2,621 pass, 4 pre-existing skips; 327 files parsed

Artifact

Release wrlforge_wasm_bg.wasm: SHA-256 56f678b0ac92af6f0c0698831e40280267613ae82ff2c7bb05ec475c8f609455, 66,609 bytes (RUST-1 was a1e99690…0963). Glue wrlforge_wasm.js unchanged (aabd7363…760c). Reproduced exactly from a rebuild. Build output is not committed.

Performance (Node 24, median ms, RUST-1 → RUST-1A)

input op RUST-1 RUST-1A
~1.6 MB checkText 3.45 3.52
~1.6 MB stateless applyEdits 5.89 5.93
~1.6 MB with genuine U+FFFD checkText 2.50 3.81
~1.6 MB with genuine U+FFFD openSession + dispose 4.11 5.40

Electron renderer, 1.6 MB stateless edit: 16.6 / 17.3 ms median/p95 (RUST-1: 16.4 / 16.8). Rust is still not faster than edit.js; full-document transfer dominates (unchanged RUST-2 blocker).

Precision review findings

225 cases reviewed: P1 89 · P2 104 · P3 14 · P4 18. All 225 are safe EOFFSETPRECISION refusals; 122 (P2 + P4) are inputs where the existing JavaScript returns a rounded, wrong value. 0 genuine regressions. All 54,437 Rust mapping successes equal the exact BigInt answer. No case is reachable from a real document (no case has all inputs below 2^30). Registry unchanged: RUST1-PRECISION stays proposed-pending-owner (SHA-256 b7af1f67…c681). Recommendation only: approve as approved-experimental (harness scope).

Known limitations (not fixed)

  • Raw-handle boundary. The facade enforces session ownership for callers using the public facade API. Raw generated WebAssembly exports are trusted internal implementation details and do not independently enforce equivalent ownership (a forged __wbg_ptr read another session's text). Before production integration, the application must prevent untrusted code from obtaining or invoking raw handles, or must strengthen the raw boundary. Today there is no production caller, so no untrusted boundary exposes it.
  • Glue-intrinsic trust root. wasm-bindgen's own string transfer uses String.prototype.charCodeAt and TextEncoder.prototype.encodeInto. Verified at publication: a lying charCodeAt alone turns "hello" into "AAAAA" silently (RUST-1 and RUST-1A alike); lying isWellFormed plus lying charCodeAt passes a substitution. No same-realm check can close this; it is documented, not fixed.
  • Revision-record growth. ~21.6 B of retained metadata per issued revision until dispose (200,000 revisions → 4.3 MB). This is live session state, distinct from the reusable wasm memory high-water mark. A bounded-history or equivalent strategy is required before production integration.

Still outstanding

  • macOS 15+: UNTESTED. No authorized macOS host. No PASS is inferred from Linux or platform-neutral wasm.
  • Independent QA: outstanding. Scope in RUST_1A_BOUNDARY_HARDENING.md §13; owner selects the tool.
  • Owner decisions: RUST1-PRECISION approval; revision-record strategy; acceptance of "facade is the isolation boundary, glue stays private" as a RUST-2 requirement.

All RUST-0/RUST-1 blockers above remain in force. PR stays Draft.

RUST-0 research lane: architecture feasibility report, migration matrix,
staged migration roadmap, and the isolated std-only spike that ports the
UTF-16 offset boundary and the WD1.2 span-patch algebra to Rust.

The spike's differential harness runs 167,316 cases against
src/vrml/edit.js with 0 regressions; three mutation controls are detected.

Research and evidence only. No production caller, no production
dependency, no Electron, CI or release change.

Signed-off-by: Ryan Bundy <ascendance@skate.fm>
RUST-1: a Rust workspace under crates/ with two crates:

- wrlforge-text (std only): UTF-16 <-> UTF-8 offsets, strict UTF-16
  decoding, WD1.2 edit algebra (applyEdits, mapOffset, mapRange) with
  UTF-16 canonical ordering, line index, immutable text snapshots.
- wrlforge-wasm: wasm-bindgen 0.2.129 boundary. Every JS string is gated
  by native String.prototype.isWellFormed before conversion, so an
  unpaired surrogate is refused (EENCODING) and never becomes U+FFFD.
  One isolated session type with WeakMap-branded facade objects;
  stale, foreign, forged and disposed handles fail closed.

spikes/rust-1-wasm-boundary/ adds the reproducible build, a Node 24
proof (18 tests), a reusable differential framework (177,582 cases,
RUST-0 digests reproduced, 0 unexplained regressions, 4 negative
controls detected), boundary benchmarks, and an isolated Electron
renderer proof under the unchanged editor CSP (12/12 checks).

Isolated: no production caller, no package.json change, no change to
main.js, preload.js, renderer, CI or packaging. macOS 15+ untested.
RUST1-PRECISION remains proposed-pending-owner.

Signed-off-by: Ryan Bundy <ascendance@skate.fm>
RUST-1A follow-up to RUST-1 (Draft PR #130). Spike-only; no production
caller, no production file changed, no dependency added.

Defect A: the UTF-16 gate trusted String.prototype.isWellFormed, which a
caller can replace. A lying method let malformed UTF-16 become U+FFFD
silently (144/324 calls per lying mode, facade and raw exports). The
gate now treats the method's answer as a claim and checks the converted
text against the source code units: each U+FFFD must sit over a genuine
0xFFFD unit. Substituted surrogate -> EENCODING; lying false claim or a
changed non-surrogate unit -> EENGINE.

Defect B: a wasm trap poisoned only the engine that saw it; another
engine on the same instance kept answering. Poison is now keyed to the
wasm instance (glue.RawSession), so every engine over a trapped
instance refuses. Separately loaded instances are unaffected.

Adds adversarial UTF-16 gate and session-lifetime tests, a BigInt
precision review of the 225 RUST1-PRECISION cases (registry unchanged,
still proposed-pending-owner), and documents the raw-handle trust
boundary, the charCodeAt/encodeInto trust root, and per-revision
metadata growth as open limits.

Release wasm SHA-256 56f678b0ac92af6f0c0698831e40280267613ae82ff2c7bb05ec475c8f609455
(66,609 bytes). Differential: 177,582 cases, 0 unexplained regressions,
candidate digest unchanged. Linux only; macOS 15+ untested.

Signed-off-by: Ryan Bundy <ascendance@skate.fm>

This comment was marked as resolved.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Poison enforcement, differential independence, precision gating, and Electron CSP proof checks have unresolved correctness gaps.

6 open findings

🧠 Review effort: Balanced

Comment thread crates/wrlforge-wasm/js/wrlforge-text.mjs
Comment thread spikes/rust-1-wasm-boundary/electron/run.cjs Outdated
Comment thread spikes/rust-1-wasm-boundary/electron/run.cjs Outdated
Comment thread spikes/rust-1-wasm-boundary/harness/precision-review.mjs Outdated
Comment thread spikes/rust-1-wasm-boundary/harness/stages/edit-algebra.mjs Outdated
Comment thread spikes/rust-1-wasm-boundary/perf/node-bench.mjs Outdated
TAURI-RUST-MIGRATION-1. A new WRL Forge desktop app in apps/desktop-tauri
with no Electron and no Node.js runtime. The Electron app is unchanged and
remains the behaviour reference.

- crates/wrlforge-vrml: direct Rust port of src/vrml tokenizer and parser
  plus Scene Tree / Inspector read projections. Parity with the JS parser
  on 673/673 files (65 fixtures + 608 new-items/item-categories).
- crates/wrlforge-document: one canonical source buffer, revisions,
  undo/redo as exact inverse span edits through wrlforge-text (WD1.2), and
  a line-break view mapping so CRLF / lone CR / BOM survive a textarea.
- src-tauri: port of src/editor/file-io.js (gzip by magic bytes, verified
  temp write, backup, atomic rename, external-change refusal, gzip
  preservation), Rust-owned sessions, native dialogs, no path from the
  WebView, in-window --smoke harness.
- ui: Leptos 0.8 CSR compiled to Wasm; X_ITE 15.1.10 preview through a
  narrow, temporary JS adapter.

Status and module map: docs/architecture/TAURI_RUST_MIGRATION_1.md.

Signed-off-by: Ryan Bundy <ascendance@skate.fm>
TAURI-RUST-MIGRATION-2. Port src/vrml/node-schema.js to
wrlforge-vrml::node_schema: the query API is hand-translated; the data
is a field-for-field projection of the committed JS file, generated by
scripts/build-rust-node-schema.js (with --check). No field, type,
default or constraint is invented.

scripts/check-rust-node-schema-parity.js proves value equality against
the JS module (54 nodes, 544 fields, 10 node classes, all counts).

Signed-off-by: Ryan Bundy <ascendance@skate.fm>
…ctions

TAURI-RUST-MIGRATION-2. Port src/vrml/field-edit.js (WD2-B) to
wrlforge-vrml::field_edit: the same nine SF types, stable reason ids,
Annex A lexical gates, schema numeric bounds (never clamped),
token-span-only patches and re-parse round-trip proof.

Node identity without a WD1.4/WD1.5 port: the caller names a node by its
exact UTF-16 span in one revision; exactly one node must sit there, and
after the edit the re-parse must find exactly one node at the shifted
span with the same type, DEF, field and values. Stricter than JS: nodes
in PROTO, EXTERNPROTO or interface scope refuse, and SFString line
breaks refuse.

wrlforge-document gains apply_source_transaction (one undo step; the
result must equal the planned text) and map_span, which carries a span
through the exact changes of an undo/redo or loses it, never guesses.

The Rust tokenizer now skips a leading U+FEFF as signature trivia while
spans still count it, so BOM files parse cleanly. Deliberate difference
from the JS tokenizer; JS/Rust parity on test/fixtures stays 65/65.

Signed-off-by: Ryan Bundy <ascendance@skate.fm>
TAURI-RUST-MIGRATION-2. Node items in the Inspector show schema-typed
fields with per-component controls. The UI sends raw text with the item
id and the revision the Inspector was built from (doc_edit_field); Rust
validates the revision, node, field, type and value, applies one
verified transaction and returns the new view and node id. The editor,
Scene Tree, Inspector and live preview adopt only Rust's reply.
Refusals are shown in the Inspector and leave the document unchanged.

Undo/redo carry the selected node through the exact changes
(map_span); inputs bind the live value property so undo shows the old
value.

smoke.sh --inspector drives a real Inspector edit through the DOM
(refusals, apply, preview, undo, redo, save); Rust independently checks
the saved bytes differ from the typed-edit expectation by exactly the
one intended token. Verified headless on copies of
new-items/item-categories/decorative/velvet-thornwing.wrl in gzip, LF,
BOM+CRLF and lone-CR forms (27/27 steps each, debug and release).

Signed-off-by: Ryan Bundy <ascendance@skate.fm>
Only allowed-items-to-test/cars/ and allowed-items-to-test/misc/ are
approved for ad hoc real-item testing. Nothing below that root may be
tracked or committed.

Signed-off-by: Ryan Bundy <ascendance@skate.fm>
UI-THEME-1. themes.css defines three built-in themes over one semantic
--wf-* token contract: tokyo-night (default and the no-attribute CSS
fallback), tokyo-night-storm and tokyo-night-light. Colour values are
adapted from the MIT Tokyo Night VS Code theme (commit 7c0f11e, v1.1.2);
the licence ships beside the stylesheet and the adaptation is recorded
in OPEN_SOURCE_PROVENANCE.md (TN-1) and THIRD_PARTY_NOTICES.md.

style.css now reads only tokens: surfaces, text, borders, buttons and
their hover/pressed/selected/disabled states, tabs, tree, focus rings,
status states, badges, scrollbars, editor selection/caret and viewport
chrome. Syntax and transform-axis roles are reserved, unused. The
rendered VRML scene is not themed.

protocol::theme holds the theme registry and REQUIRED_TOKENS. Its tests
fail on a missing or extra token, a raw colour in style.css, an
undefined token reference, an external resource, or a text/control
pair below WCAG AA (4.5:1 / 3:1) in any theme.

Signed-off-by: Ryan Bundy <ascendance@skate.fm>
…moke

UI-THEME-1. A labelled native <select> in the main toolbar lists the
three built-in themes. A change sets <html data-theme> in the same event
turn, then persists through the narrow theme_get / theme_set commands.
It never touches the document, revision, dirty flag, undo stack,
selection, Inspector or X_ITE scene.

src-tauri/settings.rs owns settings.json in Tauri's app config dir
({"schemaVersion":1,"themeId":...}). Only registry ids are accepted.
Missing, corrupt, oversized, wrong-version or unknown-theme files fall
back to Tokyo Night with a visible notice. Writes are temp + fsync +
read-back + rename and keep unknown keys; a failed write leaves the old
file byte-identical and is reported, non-fatally, in the toolbar. No
path crosses IPC and no capability or CSP change is made.

The UI body stays hidden until the persisted theme is applied (1.5 s
fallback), so a Storm/Light choice never flashes Tokyo Night widgets.
Diagnostics show severity in words and tree rows expose aria-selected,
so neither state is colour-only.

Smoke: --smoke-theme / --smoke-theme-expect / --smoke-theme-notice /
--smoke-theme-save-fails and --config-dir. The in-window run checks 23
surfaces per theme against their tokens, focus-ring visibility, document
invariants, no preview reload, backend rejection of unknown ids, and
Rust re-reads the settings file. smoke.sh always uses a per-file /tmp
config dir, never the user's preferences.

Signed-off-by: Ryan Bundy <ascendance@skate.fm>
UI-SYNTAX-1. wrlforge_vrml::highlight ports the classification of
src/editor/language.js to Rust, from ONE parse result: lexical classes
from the tokenizer (header, comment, keyword, ROUTE/TO, TRUE/FALSE/NULL,
string, number, punctuation, invalid number / unterminated string) and
identifier roles from exact AST ranges (node type, PROTO/EXTERNPROTO
name, field, interface member, IS target, ROUTE event, DEF name, USE /
ROUTE node reference, interface field type). An identifier the tree does
not place keeps no class. No second grammar, no regex.

Spans are half-open UTF-16 source offsets, sorted and disjoint. Tests
cover headers, comments, DEF/USE, PROTO/EXTERNPROTO interfaces, IS,
ROUTE, Script text inside strings, multiline strings, numbers, invalid
numbers, unterminated strings, recovery, CRLF / lone CR / BOM and astral
characters, and every committed vrml fixture (each span is exactly a
token or comment of the parse).

Dev-time parity harness (Node oracle only, not shipped):
spikes/tauri-rust-migration-1/highlight-parity.sh compares the Rust
spans with language.js; 65/65 committed fixtures consistent.

Signed-off-by: Ryan Bundy <ascendance@skate.fm>
UI-SYNTAX-1. Service::analyze now returns highlight spans from the SAME
parse as the Scene Tree and diagnostics (no second parse), mapped
through ViewMap to UTF-16 view offsets and sent [gap, len, class]-encoded
(protocol::syntax) together with session, revision, view hash and view
length. protocol::syntax::SYNTAX_CLASSES maps each class to an existing
--wf-syntax-* token; no new colour token and no raw colour.

The <textarea> stays the ONLY editing control: its glyphs are
transparent, caret, selection, IME, clipboard and scrolling are native,
and every edit still goes through the Rust document protocol. An
aria-hidden, pointer-events:none layer behind it paints the widget's
current text with identical text metrics, follows scrolling by
transform, and is built from whole-line block chunks (<= 64 lines /
~8K units) so an edit relays out one chunk. Only chunks near the
viewport carry colour spans (16,000-element budget).

Safety: an analysis colours the text only when session, revision, view
hash and view length all match the screen; stale or foreign replies are
counted and discarded, and an older analysis no longer replaces a newer
Scene Tree / Diagnostics. Between an edit and the next analysis, spans
that touch the edit are dropped and later spans move over identical
characters. Diagnostics get wavy (error) / dashed (warning) underlines
from the same revision; the panel names that revision. A theme change
is CSS only: no reparse, no repaint, no document change.

Tests: protocol encode/decode, class registry, stylesheet mapping, and
WCAG pairs for the underline colours; backend span mapping over BOM,
CRLF, lone CR, multiline CRLF strings and astral characters; UI shift /
segment / chunk logic. Every smoke run adds 21 syntax steps (colours,
registration at four scroll positions, metrics, input interception,
typing, paste, rapid input, stale-reply rejection, malformed source,
undo / redo, theme recolouring, timing, reload). Opt-in
WRLFORGE_SMOKE_ALIGN_HOLD_MS pauses for registration screenshots.

Signed-off-by: Ryan Bundy <ascendance@skate.fm>
…evision

Two stale-result races found while proving UI-SYNTAX-1 on real items.

Preview: the debounced preview and a forced one can overlap on a large
file. X_ITE aborts the superseded load ("Replacing world aborted"), and
that late result overwrote the status of the newer, successful load.
Each load now takes a sequence number; only the newest may report.

Scene Tree: the <ul> carries data-revision, the analysis revision its
items and offsets belong to. Root cause of the UI-THEME-1 cyclone smoke
failure: the harness set the analysis signal and clicked at once, but
Leptos re-renders a tick later, so it clicked an item from the startup
analysis -- (77, 122) is NavigationInfo in the original text, before the
22-unit smoke insert; the current span is (99, 144). The harness now
waits for the tree DOM to carry the current revision and reports it as
its own step, and waits for the newest preview load. Product note: a
user click that lands inside the analysis debounce after an edit still
uses the previous revision's offsets (pre-existing; not changed here).

Signed-off-by: Ryan Bundy <ascendance@skate.fm>
Document::last_changes() now holds the SpanChanges applied by the most
recent change of any kind -- a typed edit, an Inspector transaction, undo
or redo -- so a selection can be carried through typing exactly as it is
through history (map_span; never guessed). last_history_changes() stays
as the same data for existing callers.

Signed-off-by: Ryan Bundy <ascendance@skate.fm>
A Scene Tree item id is a source span of one revision. A click or Enter
on the tree is now refused (nothing selected, nothing changed) unless the
rendered tree, the held analysis and the document are at the same
revision with no edit in flight; the tree shows an updating state and is
re-analyzed at once. Diagnostics and Inspector links follow the same rule.

- doc_inspect takes the revision and answers Stale for any other one.
- The selection carries its session and revision; Rust maps it through
  each typed edit (EditRequest.item) as it already did for undo/redo, or
  clears it. An id is never re-read against newer text.
- Inspector replies land only for the newest request, the same selection
  and its revision; Apply on an older Inspector is refused locally.
- doc_analyze / doc_inspect are async and parse a copy outside the
  session lock, so edits never wait behind a parse.
- Input path: the pump diffs and hashes UTF-16 units once (no clones or
  re-encoding), line/column comes from the color layer's line index, and
  the window title is updated only when the dirty mark changes.
- Smoke: 11 selection-safety steps and a per-stage keystroke breakdown.
- American spelling throughout (color, behavior, canceled, ...).

Measured: large-file keystroke cost is the WebKitGTK textarea's own
relayout (cyclone ~188 ms, buswagon ~555 ms); our own share is 11 / 57 ms.

Signed-off-by: Ryan Bundy <ascendance@skate.fm>
wrlforge_vrml::create plans ONE new Box / Sphere / Cylinder / Cone object
as a pure append to the document: a DEF'd Transform (translation,
rotation, scale authored) holding a Shape with Appearance / Material /
diffuseColor and the primitive geometry. Input is a closed Primitive
choice; no caller-supplied text or template.

Refuses non-'#VRML V2.0 utf8' text, blocking syntax errors, recovered
nodes and capped parses, so the end of the text is provably top level.
The DEF name is one no identifier token in the file uses. The result is
re-parsed: old top-level spans unchanged, the exact structure at the
planned span, or Refused. New lines use the document's dominant ending.

Signed-off-by: Ryan Bundy <ascendance@skate.fm>
File -> New World opens a path-less session over the canonical document
(no file, no temp file). A dirty document is replaced only after a native
Rust-run confirmation; Cancel changes nothing. Save on an untitled world
runs Save As; later saves are the normal backup-first save. Close added.

Toolbar Create menu (keyboard accessible, theme tokens) sends only the
primitive and base revision to doc_create, which applies the planner's
insert as one transaction (one undo step) and returns the new
Transform's Scene Tree id for the new revision. The UI selects it in the
tree, Inspector and source editor and loads the preview at once. A newly
loaded document no longer shows the previous document's Scene Tree.

./smoke.sh --create drives the whole workflow through the real controls
under Xvfb; Rust answers the dialogs from its plan and verifies the saved
file byte for byte, the backup and the confirmation count. A read-only
X_ITE pixel probe proves each object is drawn.

Signed-off-by: Ryan Bundy <ascendance@skate.fm>
PRODUCT_VISION.md records the owner's approved goal: one studio that
combines direct visual 3D authoring (Spazz3D-inspired) and VRML source
authoring (VrmlPad-inspired), built clean-room from public standards over
one canonical document. Not a claim of completed features.

The architecture tracker now states the approved future custom Rust
renderer (not started; X_ITE stays temporary), documents VISUAL-1 and
records the two unexplained advenbed2 smoke timeouts.

Signed-off-by: Ryan Bundy <ascendance@skate.fm>
Remove the obsolete "never build a custom renderer" rule. X_ITE is the
temporary working preview renderer; the owner has approved a custom Rust
VRML97/X3D renderer as the future engine. It needs its own lane, replaces
X_ITE only after compatibility tests pass and the owner approves, and
must fit the canonical-document architecture. The README separates what
ships today (X_ITE in Electron and the Rust/Tauri app) from the planned
renderer, which is not implemented.

Signed-off-by: Ryan Bundy <ascendance@skate.fm>
wrlforge_vrml::pick translates the WD2-D resolver (src/editor/viewport-pick.js,
PR #117) to Rust with the same steps, statuses and reason ids. It joins every
link of an X_ITE hit snapshot to exactly one AST node by exact provenance span
(shifted for a stripped BOM), checks node type and the containment chain by
identity, and refuses USE / several parents / PROTO bodies / Inline scenes /
sensors and Anchors / syntax errors / capped parses. A Shape promotes only to
its own simple-object Transform (Box, Sphere, Cylinder, Cone). No name, type,
index, offset-proximity or position fallback.

Tests cover one Box, two primitives, identical and nested twins, anonymous
nodes, DEF without and with USE, repeated USEs, PROTO, Inline, sensors,
detached nodes, inexact spans, type disagreement, chain disagreement, and
LF / CRLF / lone CR / BOM / Unicode / comments / repeated structures.

Signed-off-by: Ryan Bundy <ascendance@skate.fm>
A Select toggle in the viewport toolbar arms an explicit selection mode. A
primary-button click (press + release within 4 px) asks the WD2-D X_ITE
adapter, copied verbatim into dist/ by build-ui.sh, for a plain-data hit
snapshot; doc_pick resolves it in Rust against the exact rendered text
(revision + preview hash) and the UI selects the item through the same
selection authority as the Scene Tree. Selection never moves the caret,
edits the source, marks dirty, adds undo, reloads or saves.

Preview generations (session, revision, hash, load number) are retired when
a load starts or fails, the document changes, a document opens or closes,
or the viewport browser is replaced; late replies are refused. Refusals show
a short message and keep the existing selection.

Capture listeners only observe the pointer; the pick runs one task after the
release because X_ITE's viewer still counts the button as down during the
capture phase (found with real X input).

Test: smoke.sh --pick runs, per Tokyo Night theme, New World -> Box, Sphere,
Cylinder, Cone -> real-viewport clicks -> Scene Tree / Inspector checks ->
Transform and Material edits -> stale, late and last-valid-scene refusals ->
Undo / Redo / Save As, then the WD2-C0 oracle matrix (P1-P22 + CRLF, CR,
BOM, Unicode). Real X clicks are sent only inside the harness's own Xvfb,
to a focused window of the app's own process.

Signed-off-by: Ryan Bundy <ascendance@skate.fm>
Signed-off-by: Ryan Bundy <ascendance@skate.fm>
crates/wrlforge-vrml manipulate: proves a top-level Transform target (root statement index, optional unique unrouted DEF, readable frame, authored translation) and plans a one-axis translation edit through the typed field-edit planner. Other components keep their exact lexemes; a zero-distance move is Unchanged; non-finite and out-of-range values are refused.

Signed-off-by: Ryan Bundy <ascendance@skate.fm>
Records the inventory of PRs #139-#143, the status, fix commit, evidence
and limits of each finding, separates fixes on this branch from older PR
#143 fixes, and lists suggested (unposted) GitHub replies. Also corrects
the from_native_menu path in APP_SHELL_0.md.

Signed-off-by: Ryan Bundy <ascendance@skate.fm>
… copy

STACK-REVIEW-1 follow-up to Copilot findings r4242714025 and r4242714046.

check-xite-adapters.sh matched lines, so a valid declaration inside a
/* ... */ block comment, a template literal or a dead block passed while
the real declaration lacked the pinned version. Each adapter is now masked
by xite-js-mask.awk (POSIX awk, no Node.js): comments, strings and template
literals cannot match, the declaration must be the only write to its name
and a direct statement of the one top-level IIFE, and anything the lexer
cannot lex with certainty refuses.

build-ui.sh verified the X_ITE source and then copied it unchecked. It now
embeds through fetch-xite.sh --install, which verifies the copy itself
(--verify-copy: pinned SHA-256 per file, no symlink, no unlisted entry) and
removes dist/ on refusal.

test-xite-guards.sh: 77 offline cases (28 unchanged + 33 declaration + 16
copy). With the previous scripts 18 of the 33 new declaration cases fail.

Signed-off-by: Ryan Bundy <ascendance@skate.fm>
PR #144 review 5486909537 (Copilot):

r4244998855: fetch-xite.sh copied each manifest file with cp -P and
verified only the copy. cp -P keeps only a final link and follows a link in
a parent directory, so a linked package/dist/assets became regular files
that passed. The fetch now verifies the EXTRACTED tree (version,
package.json, a link in any component, hashes) before copying, copies
through install_copy (which also refuses a linked source) and verifies the
result. --from-tarball runs the same path on a local tarball under the same
SHA-256 pin, so the fetch path has offline tests: 14 new cases, 91 total.

r4244998880: about_dialog adds six steps, not five; the record says 6.
Signed-off-by: Ryan Bundy <ascendance@skate.fm>
Independent QA HOLD on e3a1496: build-ui.sh checked the source adapters
but never the copies it staged in dist/.

- build-ui.sh hashes each adapter source before the source check, checks
  the sources before copying, and after every copy runs
  check-xite-adapters.sh --staged: each staged adapter must be a regular
  file (no symlink, not missing), carry the pre-check SHA-256, be
  byte-identical to its source, and pass the same declaration check.
- Any build-ui.sh failure removes dist/ and exits non-zero.
- test-xite-guards.sh: 60 new cases (151 total), including the real
  build-ui.sh in a temp layout with a test-only cp shim; each refusal
  must leave no dist/.
- gate-linux.sh: one repeatable Linux gate with the exact test inventory.
- STACK_COPILOT_REVIEW_1.md: the HOLD, the fix, the tests, the count
  reconciliation.

X_ITE pin (16.5.0) and declaration rules unchanged; no Node.js in the build.

Signed-off-by: Ryan Bundy <ascendance@skate.fm>
The first gate-linux.sh run on 4f3fa65 failed at the backend build:
tauri.conf.json bundles target/pkg-licenses, which a clean worktree does
not have. The gate now takes it from TOOLS, like wasm-bindgen and the
X_ITE cache, and documents the node_modules/x_ite prerequisite.

Signed-off-by: Ryan Bundy <ascendance@skate.fm>
The 8dba2a1 gate run misread three passing GUI reports:
- texture writes texture-report.json, not report.json;
- the native harness lists its checks ({name, pass, detail}), it does
  not count them;
- the pick report writes "wrong 0", menu and dock write "0 wrong".
Re-scored on that run's real reports: texture 46/46, native 46/46,
WRONG_SOURCE_SELECTIONS 3 steps, 0 wrong. A nonzero count still fails.

Signed-off-by: Ryan Bundy <ascendance@skate.fm>
PR #130 review: TextSession.current() never checked the instance poison
record, so a live session kept minting snapshots after a wasm trap. The
poison check now runs first in every session, snapshot and engine call,
facade-only ones included. Only the labels engine.poisoned,
session.disposed and snapshot.revision stay readable.

Test 8b drives two engines over one poisoned instance through every
operation and proves a separate instance stays healthy.

Signed-off-by: Ryan Bundy <ascendance@skate.fm>
…tive control

PR #130 review, two findings in electron/run.cjs:
- the production verdict ignored evalBlocked, so the proof passed even
  if (0, eval)('1') succeeded; it now requires evalBlocked === 'EvalError';
- the negative-control verdict was Object.values(loaders).every(!ok),
  which is vacuously true for an empty record and ignored neg.fatal; it
  now requires a non-fatal result that reports fetchThenInitSync,
  glueDefaultAsync and embeddedBytesAsync, every one blocked.

The decision moves to a pure electron/verdict.cjs so it is testable
without Electron. The launch, lock, CSP comparison and cleanup are
unchanged; renderer/editor.html is untouched.

Signed-off-by: Ryan Bundy <ascendance@skate.fm>
PR #130 review: precision-review.mjs counted rustSuccess.oracleRefused
(Rust mapped an edit set the independent BigInt oracle refuses) but the
exit condition ignored it. Reproduced on the no-ambiguity negative-control
build: 1,049 such successes and exit 0. The gate now also requires zero
oracle-refused successes; the inexact-success and genuine-regression
gates are unchanged.

The oracle moves byte-for-byte (function bodies diffed) to
harness/bigint-oracle.mjs so the stage check can share it. review(),
summarize() and exitCode() are exported for tests; RUST1A_PKG selects
another artifact, as in utf16-gate-adversarial. The release review output
is byte-identical to the starting SHA.

Signed-off-by: Ryan Bundy <ascendance@skate.fm>
…t the JS answer

PR #130 review: unsafeIntegerInvolved accepted a case when the JavaScript
baseline's own answer contained an unsafe value, so the answer under test
justified its own classification. A lying or rounded baseline could turn
a safe-in, safe-out Rust refusal into a pending precision difference, and
a rounded safe-looking answer could hide a real precision case.

The check now reads unsafe INPUT offsets from the case, and proves an
unsafe RESULT with the independent BigInt oracle (harness/bigint-oracle.mjs)
on well-formed mappings only. It ignores both answers. On the real run
the harness summary is identical to the starting SHA apart from timing
(225 pending, 0 regressions); the registry is unchanged.

Signed-off-by: Ryan Bundy <ascendance@skate.fm>
PR #130 review: loadInputs returns five inputs since the U+FFFD case was
added, but the memory note still said four inputs, ~3.6 MB. The note,
input count, UTF-16 unit count and UTF-8 byte count now come from
describeInputs(inputs): 5 inputs, 5,237,166 units = 5,237,186 bytes,
~5.2 MB (10^6 B). A unitsNote states that the memory figures are MiB.
The workload and the memory measurements are unchanged.

Signed-off-by: Ryan Bundy <ascendance@skate.fm>
Adds docs/architecture/RUST_1_PR130_REVIEW_FIX.md: the six Copilot
findings, root causes, corrections, fix commits, test evidence,
remaining risks and suggested (unposted) thread replies. The spike
README lists the new modules, tests and the negative precision run.

Signed-off-by: Ryan Bundy <ascendance@skate.fm>
fix(rust-1): resolve the six PR #130 Copilot findings
…ing-1

STACK-REVIEW-1: resolve Copilot findings across PRs #140–#143
UI-MENU-1: desktop menus and command integration (1.4.5 Beta)
…workspace

UI-DOCK-1: magnetic docking and resizable workspace (1.4.5 Beta)
APP-SHELL-0: version 1.4.5 Beta, command registry, docking and menu contracts (Draft)
…tion

XITE-16-UPGRADE-2: integrate X_ITE 16.5.0 into the Rust/Tauri app (Draft)
…references

RUST-STAB-2B: typed ROUTE, IS, and PROTO reference analysis
RUST-STAB-2A: Rust scope graph and safe reference analysis
RUST-STAB-1: crash recovery and source safety
VISUAL-4A: source-safe Extrusion authoring (draft)
TEXTURE-LOCAL-1: local textures in the packaged Tauri preview
PKG-LINUX-1: Linux AppImage and .deb for the Tauri app (draft)
NATIVE-RENDER-1: hidden native wgpu viewport (draft, not accepted)
feat(desktop): Rust/Tauri 2 desktop application — first vertical slice (TAURI-RUST-MIGRATION-1)
@DJAscendance
DJAscendance marked this pull request as ready for review October 12, 2026 12:33
@DJAscendance
DJAscendance merged commit 67b9cc2 into main Oct 12, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants