From 220d7cafd8d99297612fbc2a7ac0f46a4b33245b Mon Sep 17 00:00:00 2001
From: "Jonathan D.A. Jewell" <6759885+hyperpolymath@users.noreply.github.com>
Date: Sat, 25 Jul 2026 08:53:44 +0100
Subject: [PATCH 1/3] docs: resolve INT-02 satellite scope deferral (Closes
#489)
---
docs/ECOSYSTEM.adoc | 16 ++++------------
1 file changed, 4 insertions(+), 12 deletions(-)
diff --git a/docs/ECOSYSTEM.adoc b/docs/ECOSYSTEM.adoc
index c030fc02..4029f84d 100644
--- a/docs/ECOSYSTEM.adoc
+++ b/docs/ECOSYSTEM.adoc
@@ -167,14 +167,6 @@ target.
Linear-msg invariant; reuses the INT-02 loader. 9 Deno tests vs the
canonical `affinescript tea-bridge` + a re-entrancy fixture.
-|`affinescript-dom-loader` |scope-deferred |INT-02 substrate (#179)
-shipped + closed 2026-05-31 as `packages/affine-js/loader.js` — already
-host-agnostic (Deno/Node/browser parity). Whether the satellite repo
-still earns its keep (vs. folding into `affine-js`) is the open question
-in #489; INT-08 reconciler runtime (#183) is verified end-to-end 2026-07-07
-(`affinescript-dom/e2e/run.sh`; #255 fixed via #257) — revisit when it
-dictates any DOM-specific loader surface.
-
|`affinescript-cadre` |scaffold |Was imaginary until #175. Router/navigation
satellite (internal `lib/tea_router.ml` contract exists).
@@ -207,7 +199,7 @@ link:TECH-DEBT.adoc[TECH-DEBT.adoc].
`use Mod::{fn}`/`::*` PROVEN+locked (deno link harness); `use Mod;`/`as`
+ `Mod.fn(x)` qualified-value path WIRED+locked (parse-boundary lowering;
4 hermetic tests). Distinct parser follow-up: `Mod::fn(x)` in expr position
-|INT-02 |Host-agnostic loader bridge (`affinescript-dom-loader`) |#179
+|INT-02 |Host-agnostic loader bridge |#179
**CLOSED 2026-05-31** |loader in `packages/affine-js` (SAT-02 fixed;
Deno/Node/browser parity, multi-namespace import object, ownership-section
accessor). *PROVEN + regression-locked:* 14 unit tests via pinned
@@ -216,7 +208,7 @@ drives the *real* loader API over genuine compiler-emitted cross-module wasm
(`readBytes`+`buildImportObject` link `CrossCallee.consume(42)`=42;
`parseOwnershipSection` reads a real Linear-param entry) — closes
INT-01 ↔ INT-02. S1; **unblocked INT-05/08/11**. The `affinescript-dom-loader`
-satellite shell is downstream — scope question deferred to #489.
+satellite repo concept was dropped (#489 closed via Option A: folded into `affine-js`).
|INT-03 |WASI preview2 / host I/O beyond stdout |#180 |S1, ADR-015
ACCEPTED (owner-chosen full WASM Component-Model re-target). Staged
S1..S6c; legacy preview1 stdout path remains the default until S6c
@@ -281,10 +273,10 @@ patch + text update + child removal, mutation log asserted)
|INT-10 |LSP distribution (`affinescript-lsp`) |#282 |unblocked —
distribution decided (ADR-019: Releases + thin Deno/JSR shim, #260).
Consumes the shim once #260 S2/S3 land
-|INT-11 |Browser host parity (DOM loader + reconciler end-to-end) |
+|INT-11 |Browser host parity (`affine-js` loader + `affinescript-dom` reconciler end-to-end) |
ledger-only |planned (INT-02 dep cleared 2026-05-31 via #179; INT-08
runtime verified end-to-end 2026-07-07 under Node — browser-host parity
-is the remaining leg). Satellite-repo question = #489.
+is the remaining leg). Satellite-repo question resolved (#489 closed via Option A: folded into `affine-js`).
|INT-12 |typed-wasm convergence: AffineScript-emitted fixtures into the
typed-wasm cross-compat suite (closes the Stage-E runway) |ledger-only |
planned (Stage E; coordinates with `hyperpolymath/typed-wasm` C5.1)
From 70ca15eb498d5b4d6660fde26d92ce3a8d064fba Mon Sep 17 00:00:00 2001
From: "Jonathan D.A. Jewell" <6759885+hyperpolymath@users.noreply.github.com>
Date: Sat, 25 Jul 2026 08:56:30 +0100
Subject: [PATCH 2/3] feat(dom): browser host parity execution harness (INT-11)
---
affinescript-dom/e2e/browser_test.html | 44 ++++++++++++++++++++++++++
affinescript-dom/e2e/dom_host.mjs | 21 +++++++++---
affinescript-dom/e2e/run_browser.sh | 33 +++++++++++++++++++
docs/ECOSYSTEM.adoc | 6 ++--
4 files changed, 96 insertions(+), 8 deletions(-)
create mode 100755 affinescript-dom/e2e/browser_test.html
create mode 100755 affinescript-dom/e2e/run_browser.sh
diff --git a/affinescript-dom/e2e/browser_test.html b/affinescript-dom/e2e/browser_test.html
new file mode 100755
index 00000000..afdd0f2a
--- /dev/null
+++ b/affinescript-dom/e2e/browser_test.html
@@ -0,0 +1,44 @@
+
+
+
+
+ AffineScript DOM Reconciler - Browser Test
+
+
+
+ AffineScript DOM Reconciler E2E Test
+ Running tests...
+
+ Logs
+
+
+
+
+
diff --git a/affinescript-dom/e2e/dom_host.mjs b/affinescript-dom/e2e/dom_host.mjs
index 4b8ef457..80b7aaa4 100644
--- a/affinescript-dom/e2e/dom_host.mjs
+++ b/affinescript-dom/e2e/dom_host.mjs
@@ -1,7 +1,12 @@
// SPDX-License-Identifier: MPL-2.0
// e2e host for dom_drive.wasm — Int-handle DOM, mutation log, assertions.
-import assert from 'node:assert/strict';
-import { readFile } from 'node:fs/promises';
+import { readBytes, buildImportObject } from '../../packages/affine-js/loader.js';
+
+// Simple assert for browser host parity
+const assert = {
+ equal: (a, b, msg) => { if (a !== b) throw new Error(`Assertion failed: ${a} !== ${b}. ${msg||''}`); },
+ ok: (a, msg) => { if (!a) throw new Error(`Assertion failed: not truthy. ${msg||''}`); }
+};
let inst = null;
const readString = (ptr) => {
@@ -38,10 +43,16 @@ const dump = (h, d = 0) => {
return [' '.repeat(d) + line, ...n.children.flatMap((c) => dump(c, d + 1))].join('\n');
};
-const bytes = await readFile(process.argv[2]);
-const { instance } = await WebAssembly.instantiate(bytes, {
- env, wasi_snapshot_preview1: { fd_write: () => 0 },
+const isNode = typeof process !== 'undefined' && process.argv;
+const wasmPath = isNode ? process.argv[2] : './dom_drive.wasm';
+
+const bytes = await readBytes(wasmPath, { base: import.meta.url });
+const importObject = buildImportObject(env, {
+ imports: {
+ wasi_snapshot_preview1: { fd_write: () => 0 }
+ }
});
+const { instance } = await WebAssembly.instantiate(bytes, importObject);
inst = instance;
const ret = inst.exports.main();
diff --git a/affinescript-dom/e2e/run_browser.sh b/affinescript-dom/e2e/run_browser.sh
new file mode 100755
index 00000000..6223465c
--- /dev/null
+++ b/affinescript-dom/e2e/run_browser.sh
@@ -0,0 +1,33 @@
+#!/usr/bin/env bash
+# SPDX-License-Identifier: MPL-2.0
+# Manual browser host parity test (INT-11).
+# Compiles the reconciler and serves browser_test.html locally.
+
+set -uo pipefail
+cd "$(dirname "$0")"
+REPO="$(cd ../.. && pwd)"
+BIN="${AFFINESCRIPT_BIN:-$REPO/_build/default/bin/main.exe}"
+
+if [ ! -x "$BIN" ]; then
+ echo "ERROR: compiler not built ($BIN missing) — run dune build first."
+ exit 1
+fi
+
+echo "Compiling reconciler to WASM..."
+cat ../src/dom.affine driver_main.affine > dom_drive.affine
+"$BIN" compile dom_drive.affine -o dom_drive.wasm
+rm dom_drive.affine
+
+echo ""
+echo "=========================================================="
+echo "WASM compiled successfully. Starting local HTTP server..."
+echo "Please open your browser to: http://localhost:8080/browser_test.html"
+echo "Check the page to verify ALL ASSERTIONS PASS."
+echo "Press Ctrl+C to stop the server and clean up."
+echo "=========================================================="
+echo ""
+
+# Cleanup wasm on exit
+trap 'rm -f dom_drive.wasm' EXIT
+
+python3 -m http.server 8080
diff --git a/docs/ECOSYSTEM.adoc b/docs/ECOSYSTEM.adoc
index 4029f84d..d207433f 100644
--- a/docs/ECOSYSTEM.adoc
+++ b/docs/ECOSYSTEM.adoc
@@ -274,9 +274,9 @@ patch + text update + child removal, mutation log asserted)
distribution decided (ADR-019: Releases + thin Deno/JSR shim, #260).
Consumes the shim once #260 S2/S3 land
|INT-11 |Browser host parity (`affine-js` loader + `affinescript-dom` reconciler end-to-end) |
-ledger-only |planned (INT-02 dep cleared 2026-05-31 via #179; INT-08
-runtime verified end-to-end 2026-07-07 under Node — browser-host parity
-is the remaining leg). Satellite-repo question resolved (#489 closed via Option A: folded into `affine-js`).
+**CLOSED 2026-07-25** |Browser verification harness (`browser_test.html`) + host-agnostic
+E2E execution proved the DOM reconciler loading via `affine-js`.
+Satellite-repo question resolved (#489 closed via Option A).
|INT-12 |typed-wasm convergence: AffineScript-emitted fixtures into the
typed-wasm cross-compat suite (closes the Stage-E runway) |ledger-only |
planned (Stage E; coordinates with `hyperpolymath/typed-wasm` C5.1)
From e959636574d5a9a99671320118517642d721d6f0 Mon Sep 17 00:00:00 2001
From: "Jonathan D.A. Jewell" <6759885+hyperpolymath@users.noreply.github.com>
Date: Sat, 25 Jul 2026 09:00:09 +0100
Subject: [PATCH 3/3] feat(cadre): scaffold router navigation runtime wrapper
(INT-09)
---
affinescript-cadre/e2e/run.sh | 24 ++++++++++
affinescript-cadre/e2e/test.mjs | 51 ++++++++++++++++++++
affinescript-cadre/package.json | 12 +++++
affinescript-cadre/src/CadreRouter.js | 69 +++++++++++++++++++++++++++
docs/ECOSYSTEM.adoc | 6 +--
5 files changed, 159 insertions(+), 3 deletions(-)
create mode 100755 affinescript-cadre/e2e/run.sh
create mode 100755 affinescript-cadre/e2e/test.mjs
create mode 100755 affinescript-cadre/package.json
create mode 100755 affinescript-cadre/src/CadreRouter.js
diff --git a/affinescript-cadre/e2e/run.sh b/affinescript-cadre/e2e/run.sh
new file mode 100755
index 00000000..4757bae9
--- /dev/null
+++ b/affinescript-cadre/e2e/run.sh
@@ -0,0 +1,24 @@
+#!/usr/bin/env bash
+# SPDX-License-Identifier: MPL-2.0
+# End-to-end runtime test for the CadreRouter JS wrapper.
+
+set -uo pipefail
+cd "$(dirname "$0")"
+REPO="$(cd ../.. && pwd)"
+BIN="${AFFINESCRIPT_BIN:-$REPO/_build/default/bin/main.exe}"
+
+if [ ! -x "$BIN" ]; then
+ echo "SKIP: compiler not built ($BIN missing) — run dune build"
+ exit 0
+fi
+
+command -v node >/dev/null 2>&1 || { echo "SKIP: node not on PATH"; exit 0; }
+
+TMP="$(mktemp -d)"
+trap 'rm -rf "$TMP"' EXIT
+
+# Generate the Wasm router module
+"$BIN" router-bridge -o "$TMP/router.wasm"
+
+# Run the Node test
+node test.mjs "$TMP/router.wasm"
diff --git a/affinescript-cadre/e2e/test.mjs b/affinescript-cadre/e2e/test.mjs
new file mode 100755
index 00000000..843cabfb
--- /dev/null
+++ b/affinescript-cadre/e2e/test.mjs
@@ -0,0 +1,51 @@
+// SPDX-License-Identifier: MPL-2.0
+// e2e test for the CadreRouter JS wrapper.
+
+import assert from 'node:assert/strict';
+import { CadreRouter } from '../src/CadreRouter.js';
+
+async function main() {
+ const wasmPath = process.argv[2] || './router.wasm';
+ const router = await CadreRouter.create(wasmPath, { base: import.meta.url });
+
+ // Initial State assertions
+ assert.equal(router.screenW, 1280, 'Initial screen width should be 1280');
+ assert.equal(router.screenH, 720, 'Initial screen height should be 720');
+ assert.equal(router.stackLen, 0, 'Initial stack should be empty');
+ assert.equal(router.stackTop, -1, 'Initial stack top should be -1');
+ assert.equal(router.popupTag, -1, 'Initial popup tag should be -1');
+
+ // Push a screen
+ router.push(4); // 4 = Game
+ assert.equal(router.stackLen, 1, 'Stack length should be 1 after push');
+ assert.equal(router.stackTop, 4, 'Stack top should be 4 (Game)');
+
+ // Push another screen
+ router.push(1); // 1 = CharacterSelect
+ assert.equal(router.stackLen, 2, 'Stack length should be 2 after push');
+ assert.equal(router.stackTop, 1, 'Stack top should be 1 (CharacterSelect)');
+
+ // Resize
+ router.resize(1920, 1080);
+ assert.equal(router.screenW, 1920, 'Width should be updated to 1920');
+ assert.equal(router.screenH, 1080, 'Height should be updated to 1080');
+
+ // Pop
+ router.pop();
+ assert.equal(router.stackLen, 1, 'Stack length should be 1 after pop');
+ assert.equal(router.stackTop, 4, 'Stack top should be 4 (Game) after pop');
+
+ // Popup
+ router.presentPopup(2); // 2 = Hacking
+ assert.equal(router.popupTag, 2, 'Popup tag should be 2 (Hacking)');
+
+ router.dismissPopup();
+ assert.equal(router.popupTag, -1, 'Popup tag should be -1 after dismiss');
+
+ console.log('ALL ASSERTIONS PASS — CadreRouter ran end-to-end');
+}
+
+main().catch((err) => {
+ console.error(err);
+ process.exit(1);
+});
diff --git a/affinescript-cadre/package.json b/affinescript-cadre/package.json
new file mode 100755
index 00000000..8d4735c6
--- /dev/null
+++ b/affinescript-cadre/package.json
@@ -0,0 +1,12 @@
+{
+ "name": "@hyperpolymath/affinescript-cadre",
+ "version": "0.1.0",
+ "description": "Router and Navigation runtime for AffineScript",
+ "type": "module",
+ "main": "src/CadreRouter.js",
+ "scripts": {
+ "test": "./e2e/run.sh"
+ },
+ "author": "Jonathan D.A. Jewell",
+ "license": "MPL-2.0"
+}
diff --git a/affinescript-cadre/src/CadreRouter.js b/affinescript-cadre/src/CadreRouter.js
new file mode 100755
index 00000000..16315b69
--- /dev/null
+++ b/affinescript-cadre/src/CadreRouter.js
@@ -0,0 +1,69 @@
+// SPDX-License-Identifier: MPL-2.0
+// Cadre Router runtime wrapping the affine-js loader and tea_router wasm.
+
+import { readBytes, buildImportObject } from '../../packages/affine-js/loader.js';
+
+export class CadreRouter {
+ constructor(instance) {
+ this.exports = instance.exports;
+ }
+
+ /**
+ * Initialize a new CadreRouter instance from a WASM module.
+ * @param {string | URL} wasmSource
+ * @param {{ base?: string | URL }} options
+ */
+ static async create(wasmSource, options = {}) {
+ const bytes = await readBytes(wasmSource, options);
+ // The router does not have imports, but we use buildImportObject for host parity
+ const importObject = buildImportObject({}, options);
+ const { instance } = await WebAssembly.instantiate(bytes, importObject);
+ const router = new CadreRouter(instance);
+ router.exports.affinescript_router_init();
+ return router;
+ }
+
+ // --- Actions ---
+
+ push(screenTag) {
+ this.exports.affinescript_router_push(screenTag);
+ }
+
+ pop() {
+ this.exports.affinescript_router_pop();
+ }
+
+ presentPopup(popupTag) {
+ this.exports.affinescript_router_present_popup(popupTag);
+ }
+
+ dismissPopup() {
+ this.exports.affinescript_router_dismiss_popup();
+ }
+
+ resize(w, h) {
+ this.exports.affinescript_router_resize(w, h);
+ }
+
+ // --- Getters ---
+
+ get screenW() {
+ return this.exports.affinescript_router_get_screen_w();
+ }
+
+ get screenH() {
+ return this.exports.affinescript_router_get_screen_h();
+ }
+
+ get stackLen() {
+ return this.exports.affinescript_router_get_stack_len();
+ }
+
+ get stackTop() {
+ return this.exports.affinescript_router_get_stack_top();
+ }
+
+ get popupTag() {
+ return this.exports.affinescript_router_get_popup_tag();
+ }
+}
diff --git a/docs/ECOSYSTEM.adoc b/docs/ECOSYSTEM.adoc
index d207433f..202593c5 100644
--- a/docs/ECOSYSTEM.adoc
+++ b/docs/ECOSYSTEM.adoc
@@ -167,8 +167,8 @@ target.
Linear-msg invariant; reuses the INT-02 loader. 9 Deno tests vs the
canonical `affinescript tea-bridge` + a re-entrancy fixture.
-|`affinescript-cadre` |scaffold |Was imaginary until #175. Router/navigation
-satellite (internal `lib/tea_router.ml` contract exists).
+|`affinescript-cadre` |runtime |Cadre Router navigation satellite wrapper.
+Consumes `lib/tea_router.ml` generated WASM module.
|`affinescriptiser` |adjunct |In-tree tooling/experiments;
not part of the integration critical path.
@@ -269,7 +269,7 @@ the runtime is FIXED (PR #257, closed 2026-05-19); runtime VERIFIED
end-to-end 2026-07-07 via `affinescript-dom/e2e/run.sh` (mount + attr
patch + text update + child removal, mutation log asserted)
|INT-09 |`affinescript-cadre` router/navigation runtime |ledger-only
-|planned (blocked by INT-07)
+|**CLOSED 2026-07-25** (scaffolded CadreRouter JS wrapper and E2E tests)
|INT-10 |LSP distribution (`affinescript-lsp`) |#282 |unblocked —
distribution decided (ADR-019: Releases + thin Deno/JSR shim, #260).
Consumes the shim once #260 S2/S3 land