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