# ADR-0009: embed the JS engine; do not put `node` in the toolchain status: accepted date: 2026-08-02 decided by: agent, under the standing loop authorization tier: M (structural M — adds an external dependency to the toolchain, InnerLoop v1.6; chaos d4=2 → no override). Tier M merges survey and decision into one document, which this is. references: [CB-WP-0014](../workplans/CB-WP-0014-execute-the-javascript.md), [ADR-0007](ADR-0007-render-html-not-a-port.md) D3 (the acquisition rule) and D5 (control 5), [ADR-0008](ADR-0008-instrument-corrections.md) D2 (AM-4b left uncorrected), [CB-EV-0010](../evidence/CB-EV-0010-render-port.md) §4 ## Context ADR-0007 control 5 says the emitted page **may not construct commands** — it reports raw pointer facts and Rust decides what they mean. That contract is currently held up 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."** And the script has never been executed at all, which is the sole remaining reason INTENT stage 1 is open. ## The survey `node` v24.11.1 is on this machine, so the obvious move is to shell out to it. Measured alternatives, same method as `dep-weight.py`, marginal against the 29-crate dev-toolchain graph, under the positive control: | option | marginal Rust lines | notes | |---|---:|---| | `boa_engine` | 896,410 | 27× AM-4b's headroom | | `rquickjs` | 69,985 | 2.1× headroom | | **`quick-js`** | **11,434** | bindings + vendored QuickJS C | | `node`, shelled out | **0** | and that zero is the problem | AM-4b headroom is **32,979** (317,021 of 350,000), so `quick-js` fits at 35% of it and `rquickjs` does not. ## Decision — embed `quick-js`; `node` is refused **This is ADR-0007 Decision 3's acquisition rule biting its author, which is the only real test of whether it was written honestly.** The rule: > AM-4 counts third-party code the project causes to be **acquired**. It > does not count runtimes the user already has independently of us. It > **does** count a library our build or install instructions cause to be > fetched, pinned, or linked, whether or not its source is Rust. A browser is not counted because a developer has one regardless of us. But **CI runs on `rust:1.97`, which has no `node`** — so adding this test would make our CI fetch a JavaScript runtime. That is our build causing an acquisition, of tens of millions of lines nobody here will audit, scoring **zero** on the only instrument that governs dependencies. Taking `node` would mean using the rule to exempt a browser we do not install while also exempting a runtime we do. `quick-js` costs 11,434 lines that are vendored, pinned, auditable, and counted. The secondary reasons matter less but all point the same way: the test runs anywhere `cargo test` runs, needs no CI change, cannot skip because a binary is missing, and pins one engine version rather than whatever the image happens to ship. **`python3` is not a precedent for `node`.** It is already a toolchain dependency, and the honest reading is that it was never argued — it predates the acquisition rule. Leaning on it would be using an unexamined decision to license a second one. It is left alone here and noted as owed. ### What is bought, and what is not Executing the script proves the **input contract**: what the page puts on the wire in response to a pointer gesture. It does **not** prove the SVG renders legibly, that a drag feels like a drag, or that anyone can play a game. QuickJS has no layout engine and this ADR claims no rendering evidence. ### Cost accepted — and a correction, measured after the fact The paragraph originally here read *"35% of AM-4b's remaining headroom, for a test."* **That was wrong, and finding out how wrong is the more important result of this pass.** After landing, `make dep-weight` reported AM-4b **unchanged at 317,021**. `quick-js` is a dev-dependency of `cb-render-html`, and AM-4b measures `cargo tree -p games-ground --edges normal` — one package, no dev edges. It cannot see it. Measured: | | crates | lines | |---|---:|---:| | AM-4b as instrumented (`games-ground`, normal) | 29 | 317,021 | | the whole workspace, including dev edges | 57 | 725,258 | | **uncounted by AM-4b** | **28** | **408,237** | **The dev-toolchain budget is blind to more source than its entire target** — `criterion`, `clap`, `ciborium`, and now `quick-js`. This is the same defect this ADR refuses `node` for: a real acquisition scoring zero because the instrument does not look there. The difference is that `quick-js` is *auditable and pinned* and `node` is neither, so the decision stands. But it stands on the acquisition rule, **not** on an affordability argument, because there is no affordability argument to be had until AM-4b can see what it is buying. Recorded as owed, and it is now the third defect in the AM-4 family: the shipped-runtime proc-macro count (fixed, ADR-0008 D2), AM-4b's own proc-macro share (owed), and AM-4b's scope (this). ## Consequences - `quick-js` is a **dev-dependency of `cb-render-html` only**. It must never reach the shipped-runtime configuration; AM-4a would catch that, and now measures 157,202 against 161,000 with no room for it. - The grep test stays. It is cheap, and it fails faster and more legibly than an execution test when someone adds a word to the script. - If `quick-js` becomes unmaintained, the fallback is not `node` — it is `rquickjs` plus an AM-4b decision, or dropping the execution test and saying so.