ADR-0009: embed quick-js; node is refused. Measured marginal cost against the dev-toolchain graph, under the positive control: boa_engine 896,410 rquickjs 69,985 quick-js 11,434 node 0 <- and that zero is the problem 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 while 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. The loop is now closed: the real server serves the real page, QuickJS runs that page's own scripts, the gesture goes over a real socket, and the seat's Choice comes back. Before this, every link was tested and the chain was not — a page whose JavaScript sent something else entirely would have passed everything. Three controls, each red for its stated reason: the JS posting a command name instead of ids, the gesture not being delivered (EXPECT-VACUOUS), and the token stripped from the endpoint. A wrong assertion worth keeping: the first draft required the body not to contain "attack". It legitimately does — action-attack is the id of an element a finger landed on. An element may name an action; that is not the page deciding. The real test is the shape: exactly two fields, down and up, carrying two ids and nothing derived from them. AND the ADR's own cost argument 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: 408,237 uncounted, MORE THAN THE TARGET ITSELF (criterion, clap, ciborium, quick-js). The decision stands on the acquisition rule; the affordability argument is withdrawn. Third defect in the AM-4 family. Also fixed structurally rather than by raising a limit: `make status` had grown past its 40-line readability gate as workplans accumulated. Closed workplans now collapse to one line, so the report is fixed-size. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
121 lines
5.4 KiB
Markdown
121 lines
5.4 KiB
Markdown
# 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.
|