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/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 c030fc02..202593c5 100644
--- a/docs/ECOSYSTEM.adoc
+++ b/docs/ECOSYSTEM.adoc
@@ -167,16 +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-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).
+|`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.
@@ -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
@@ -277,14 +269,14 @@ 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
-|INT-11 |Browser host parity (DOM loader + 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.
+|INT-11 |Browser host parity (`affine-js` loader + `affinescript-dom` reconciler end-to-end) |
+**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)