clay-borg/decisions/ADR-0009-embed-the-js-engine.md
tegwick 55212d7e0f CB-WP-0014-T01/T02: execute the JavaScript — and find AM-4b blind
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>
2026-08-02 07:56:02 +02:00

121 lines
5.4 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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.