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

5.4 KiB
Raw Blame History

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, ADR-0007 D3 (the acquisition rule) and D5 (control 5), ADR-0008 D2 (AM-4b left uncorrected), CB-EV-0010 §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 targetcriterion, 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.