--- id: CB-WP-0014 kind: product title: "Execute the JavaScript, and close or keep stage 1" status: done state_hub_workstream_id: "a8602f84-d073-4873-b845-afe9d2620a5d" --- # Purpose ``` structural tier M (adds an external dependency to the toolchain — InnerLoop v1.6 tier table) chaos d4 = 2 → no override declared tier M ``` Declaration 9 of 12. Tier M merges the survey and the ADR into one document; the adversarial review is optional. ## The one thing blocking stage 1 CB-EV-0010 §4, stated plainly at the time: > **The emitted JavaScript has never run.** Every test is on the Rust > side: the socket loop is driven by synthetic HTTP, and the page is > asserted against as a parsed document. No browser engine has executed > `SCRIPT`. All four of INTENT stage 1's named deliverables now exist. That sentence is the only reason the stage is still open, and it has been carried unexamined for one pass on the assumption that executing it was not possible here. **It is possible.** `node` v24.11.1 is on this machine. The assumption was never checked, which makes it the same shape as every other finding in the last three passes: a claim carried because nobody ran the one command that would settle it. ## What executing it actually proves Not that the table *looks* right — that needs a rendering engine and a human eye, and neither is available here. What it proves is the contract ADR-0007 control 5 rests on: > the page reports **raw pointer facts and nothing else**, and Rust > decides what command they mean. That contract is currently asserted by a test that greps the emitted script for game vocabulary. Grepping for the absence of words is a weak proxy for "this code cannot construct a command". Running it, feeding it synthetic pointer events, and observing exactly what it puts on the wire is the real check. ## Task: decide whether a JS engine joins the toolchain ```task id: CB-WP-0014-T01 status: done priority: high state_hub_task_id: "ad118b97-e59a-41ec-9054-e314aa9482f6" ``` Write `decisions/ADR-0009-*.md` (tier M: survey and decision in one). **This is a real dependency decision, not a formality.** CI runs on the `rust:1.97` image, which has no `node`. Adding a test that needs one means either installing it in CI or letting the test skip — and **a test that skips silently is the harness-does-nothing class this project has found five times.** If it can skip, it must fail loudly instead. Decide, and price it under ADR-0007 Decision 3's acquisition rule — the rule this project adopted precisely so that "it's only a dev tool" is not an automatic pass: - `python3` is already a toolchain dependency, so this is the second, not the first. Say whether that makes it cheaper or is merely a precedent being leaned on. - What the alternatives cost: no execution at all (status quo, and the reason stage 1 is open), or a JS interpreter as a Rust crate (measure it — do not estimate, and note that AM-4b now has an unmeasured proc-macro share of its own). - Whether the engine is required by `make all` or by a separate target. **AM-4b is the budget this lands in**, and it is the instrument CB-WP-0013 explicitly declined to correct. Do not use its uncorrected headroom as an argument. **Done 2026-08-02.** [ADR-0009](../decisions/ADR-0009-embed-the-js-engine.md) — **embed `quick-js`; `node` is refused.** Measured, marginal, under the control: `boa_engine` 896,410 · `rquickjs` 69,985 · **`quick-js` 11,434** · `node` **0**, and that zero is the problem. This is ADR-0007 D3's acquisition rule biting its author. CI runs on `rust:1.97`, which has no `node` — so the test would make *our build* fetch a JS runtime of tens of millions of unaudited lines, scoring zero on the only instrument that governs dependencies. A browser is exempt because a developer has one regardless of us; a CI-installed runtime is not. **And the cost argument in the first draft was wrong.** It claimed 35% of AM-4b's headroom. After landing, AM-4b did not move at all — it measures `games-ground --edges normal`, one package, no dev edges. Measured: the workspace including dev edges is **725,258** lines against AM-4b's **317,021**, so **408,237 lines are uncounted — more than the target itself**. The decision stands on the acquisition rule; the affordability argument is withdrawn, because there is none to be had until the instrument can see what it is buying. Filed as the third AM-4 defect. ## Task: run it, end to end, through a real socket ```task id: CB-WP-0014-T02 status: done priority: high state_hub_task_id: "56f09f33-cdb9-4d58-80c9-73e5ee1cf589" ``` Execute `cb_render_html::doc::SCRIPT` in a real JS engine against a minimal DOM, synthesize a pointer-down and pointer-up on two real element ids from a real emitted document, and let its `fetch` reach the **actual `Server`** — the same `next_choice` loop `cb-play --serve` runs. Acceptance: the seat's `Choice` comes back from a pointer gesture that originated in JavaScript. That is the first time anything in this project has closed that loop. **Controls, and they are the point.** The test must go red when: - the JS is mutated to POST a command name instead of element ids — this is **control 5 asserted rather than grepped**, and if it does not fire, the grep test was the only thing holding the contract up; - the pointer events are not delivered at all — the EXPECT-VACUOUS case, a harness that runs a script and asserts nothing about what it did; - the token is stripped from the endpoint the page was given. **Done 2026-08-02.** `crates/cb-render-html/src/jsrun.rs` and `hotseat::tests::a_gesture_in_javascript_becomes_a_move_in_the_game`. **The loop is closed.** The real server serves the real page; QuickJS runs *that page's own scripts*; the gesture it produces goes over a real socket; the seat's `Choice` comes back. Both `