Delivers ADR-0007 Decision 1: visualization, drag-to-propose and hot-seat
play, at a measured marginal AM-4a cost of zero.
games-ground shipped: 23 third-party crates
cb-render-html: 23 third-party crates
new crates introduced: 0
Measured, not asserted — the survey's own lesson. AM-4a is unmoved at
246,250; own source is 7,636 -> 9,652.
What shipped:
crates/cb-render-html doc.rs (HTML/SVG emission, incl. the relationship
graph), input.rs (pointer facts -> commands),
serve.rs (Guard, Request, loopback bind)
tools/cb-play hotseat.rs + `--serve PORT`
Per ADR-0007 Decision 2 there is NO cb-render-api and NO cb-render-null.
The renderer targets the existing Project trait; the port waits for
stage 2's wgpu implementation to be its second use.
The six controls, all live, all mutation-checked (8 mutations, each red
for its stated reason):
1-3 token / Origin+Sec-Fetch-Site / explicit 127.0.0.1 bind
4 a token-less request is refused, in the unit AND over a real socket
5 JS may not construct commands — the page reports pointer facts, Rust
resolves them against the legal list the aggregate already offered,
and a test asserts the emitted script contains no game vocabulary
6 the coverage gate crosses the language boundary: it walks the
serialized view for leaf paths and requires each token to appear in
the PARSED emitted document, with a test that the parse really is a
parse (script/style contents must not count as rendered)
The gate fired on its author again, on its first run: ground_choices.*.
choice, ground_choices.*.problem and players.*.blame_from were in neither
list. The last is the one worth keeping — an EMPTY vector is a leaf path
of its own, and it now renders as an explicit absence.
Also, a mutation that did not go red: removing the Sec-Fetch-Site arm
alone left the cross-site test green, because the Origin check caught it
independently. Both had to be removed before the control bit. Recorded
because a control that passes for a reason you did not intend has not
been demonstrated.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
16 KiB
CB-RES-0006 — the render port, and what a 2D table costs
capability: render.table.2d status: approved (v2, after adversarial review — v1 was not approvable, with four conceded findings; see §2, §4, §5, §7) tier: L (structural L — the declaration was made when this pass would create a capability port; chaos d4=1 → no override. Review then removed the port, and the tier was deliberately not re-derived: a tier that changes because review shrank the work is a function of the outcome)
CB-WP-0012 T01. Step 1 of the inner loop. Measured 2026-08-02 at
331e7e9.
The question: INTENT stage 1 needs cb-render-api and a first real
implementation. AM-4a reports 3,750 lines of headroom (246,250 of a
250,000 target). Is that a real obstacle, or an artifact of the
instrument?
Both, and not in the proportions I expected.
1. Method
Every figure below comes from the same method tools/dep-weight.py uses
for AM-4a: resolve a real dependency graph with
cargo tree --edges normal --prefix none, drop path dependencies, and
count \n in every .rs file of the vendored source. Reusing the method
verbatim is the point — a survey that measures candidates one way and the
budget another produces two numbers that cannot be subtracted.
Probe crates were built in a scratch directory, one per candidate, and resolved against the live registry. The scripts are not committed, because a throwaway probe promoted to a tool is how the second-use rule gets broken by accident.
Positive control. source_lines() returns 0 for a crate it cannot
locate, so a registry path change would report every candidate as costing
nothing — the direction that flatters the recommendation. Every figure
below was produced under two guards copied from dep-weight.py: an
unlocatable crate must measure zero, a real one must measure non-zero, and
any candidate containing an unlocated crate is reported rather than summed.
No candidate contained one. The review (T02) found that the second batch of
measurements had copied the measurement function without the guards; they
were re-run under them before being cited here, and reproduced unchanged.
Marginal, not total. A candidate's headline size overstates its cost by whatever it already shares with the base graph. What AM-4a charges is the delta.
2. What a 2D table costs
Base: games-ground --no-default-features, 23 third-party crates,
246,250 lines.
| candidate | total | shared with base | marginal | vs 3,750 headroom |
|---|---|---|---|---|
egui + eframe |
2,946,121 | 163,272 | 2,782,849 | 742× |
wgpu + winit |
1,866,820 | 124,841 | 1,741,979 | 465× |
softbuffer + tiny-skia + winit |
1,271,922 | 124,841 | 1,147,081 | 306× |
ratatui + crossterm |
1,146,363 | 79,350 | 1,067,013 | 285× |
egui alone (no windowing) |
778,067 | 110,542 | 667,525 | 178× |
macroquad |
480,729 | 228 | 480,501 | 128× |
| HTML/SVG emission, no Rust toolkit | — | — | 0 by construction | — |
Revised after review (T02). The first draft of this section concluded from the table above that no windowed toolkit could fit, and called it "two orders of magnitude, not a near miss." That was drawn from a sample containing no small windowed toolkits. Every candidate above is a GPU-era stack, because that is what one reaches for; cheap bindings to system libraries were structurally absent. Measured, same method, same base, under the positive control:
| candidate | marginal | vs 92,798 corrected headroom (§3) |
|---|---|---|
tiny-skia alone (rasterizer, no window) |
83,956 | fits, at 90% |
termion |
133,130 | 1.4× |
fltk |
140,079 | 1.5× |
sdl2 |
191,973 | 2.1× |
sdl3 |
276,445 | 3.0× |
raqote |
286,852 | 3.1× |
minifb |
774,389 | 8.3× |
crossterm alone |
802,677 | 8.6× |
softbuffer alone |
909,416 | 9.8× |
pixels |
863,528 | 9.3× |
speedy2d |
1,328,008 | 14.3× |
The real floor for a windowed toolkit is fltk at 1.5× the corrected
headroom, and tiny-skia fits outright. The "two orders of magnitude"
claim is withdrawn, and with it the survey's original argument:
dependency cost alone does not decide this. What replaces it is §4.
The 0 in the first table is by construction, not by measurement —
there is no crate to fail to locate. It is relabelled because a zero
meaning "costs nothing" and a zero meaning "measured nothing" print
identically, and every other row is a measurement.
A surprise worth recording: ratatui — a terminal library, the
option one reaches for expecting it to be the cheap one — costs more than
macroquad, a 2D game framework with a GPU backend. The reason is
linux-raw-sys at 479,901 lines and libc at 129,990, pulled through
rustix. Neither is code anyone audits by hand; both are largely
generated constant and type definitions. Which raises the next question.
3. The instrument is wrong, and has been all along
AM-4a counts the --edges normal graph. That graph includes proc-macro
crates, which run in the compiler and never reach a shipped binary.
games-ground --no-default-features |
crates | lines |
|---|---|---|
| as AM-4a measures it | 23 | 246,250 |
excluding proc-macro crates (--edges normal,no-proc-macro) |
18 | 157,202 |
| the difference | 5 | 89,048 (36.2%) |
The five are syn, quote, proc-macro2, unicode-ident and
serde_derive. syn alone is 66,916 lines — the single largest entry in
the shipped-runtime figure, and it is a parser for Rust source that exists
only at build time.
So 36.2% of what AM-4a calls "what a game ships" is not shipped. Under a corrected metric the headroom is not 3,750 but 92,798. Every pass that has cited the 3,750 figure — including CB-WP-0011's own reasoning for deferring this declaration — cited a number that was wrong in the conservative direction.
This does not change the recommendation, and that is the point of
checking it before recommending. Under the corrected metric macroquad
still costs 5.2× the available headroom, and every other windowed
candidate is worse. The dependency argument survives the correction of the
number it rests on, which is the only condition under which a survey may
propose correcting a number in its own favour.
The correction is proposed as a separate, narrow change — the metric should measure what ships — and it must not be bundled with the render decision, because a budget correction that arrives attached to the request it unblocks is indistinguishable from motivated reasoning even when it is right.
4. What stage 1 actually asks for
card/token/hand/relationship-graph visualization, drag-to-propose, debug inspector, hot-seat play
Read plainly: draw a table, let a human point at it, and let several
humans take turns at one machine. It does not ask for a GPU, a frame
loop, physics, or 60fps. Those are stage 2 ("Physical 3D tabletop —
wgpu renderer, Rapier-backed physics"), and stage 2 is where the 1.7M
lines of wgpu + winit get argued for on their merits.
The allocation argument, which replaces the affordability one
fltk at 140,079 is affordable if AM-4a's target moves a little. The
reason not to buy it is not cost but allocation:
| marginal lines | |
|---|---|
| corrected headroom (§3) | 92,798 |
cheapest windowed stage-1 toolkit (fltk) |
140,079 |
wgpu + winit, which INTENT stage 2 names by name |
1,741,979 |
Stage 2's renderer is measured, unavoidable, named in INTENT, and twelve times the cost of the stage-1 toolkit it would replace. Spending 1.5× the entire remaining budget on a windowing stack that stage 2 discards is the wrong allocation regardless of whether the budget can be stretched to cover it. That argument does not depend on AM-4a's target being exactly 250,000 — only on stage 2's bill exceeding stage 1's benefit, which is measured and true by a factor of twelve.
AM-4a cannot survive stage 2, and that is not this pass's to decide
Following the above honestly produces a finding this survey did not set out to look for. Stage 2 exceeds AM-4a's entire target by 7× and the remaining headroom by 19×. No sequencing, feature-gating, or metric correction closes a gap that size: AM-4a as targeted is incompatible with INTENT as written, and has been since both were written.
Either the target moves, or the render port stays outside the AM-4a configuration by an argued rule, or stage 2 changes. That is a maintainer's decision. It is raised here and deliberately not resolved — a pass that discovers a budget conflict and also settles it has reviewed nothing.
5. The option that is not a Rust toolkit
Emit HTML with inline SVG and inline JavaScript; open it in the browser the machine already has.
- visualization — SVG draws cards, tokens and a relationship graph natively, and the relationship graph is the one element every Rust 2D toolkit would have made us hand-roll anyway.
- drag-to-propose — pointer events in the emitted page.
- debug inspector — already shipped (CB-WP-0011), and its renderer is already a total function from a projection to text; HTML is a second output format for the same walk.
- hot-seat play — one browser tab, seats taking turns, the same commit/reveal the CLI player uses.
The rule that makes "zero" honest (added by review)
Scoring the browser at zero is true under AM-4a and suspect in substance: a browser is tens of millions of lines of unaudited third-party code. If a proposal can satisfy a dependency target by relocating the dependency somewhere the target does not look, the target constrains nothing.
The same critique convicts sdl2 and fltk above — thin Rust bindings
whose .rs counts exclude the C library that does the work. The metric
already permits the move; this survey simply never measured a binding
before, so it never noticed.
One rule, applied to all three, belonging in AM-4's definition rather than in this pass's reasoning:
AM-4 counts third-party code the project causes to be acquired. It does not count runtimes the user already has independently of us — the kernel, the system C library, the shell, the terminal, a web browser. It does count a library our build or install instructions cause to be fetched, pinned, or linked, whether or not its source is Rust.
Under it: a browser is not counted; sdl2 and fltk are counted at more
than their Rust binding, not less, because they oblige a user to install
a -dev package they would not otherwise have. The rule makes the metric
harder to satisfy by relocation, which is the direction a rule proposed
by the party it benefits has to run before it is worth anything.
Marginal AM-4a cost: zero. Not "small" — zero. Emitting HTML is string
formatting; the 2,897-line svg crate measured above is not needed and is
not proposed. The input path costs no dependency either: a loopback
HTTP/1.1 listener sufficient for GET / and POST /command is
std::net::TcpListener plus roughly 250 lines of our own code, and our
own code is not what AM-4a governs.
This is the central rule applied literally — own the semantics; assimilate the implementation. The browser is the most thoroughly assimilated 2D renderer available, and it is the one implementation we are guaranteed not to have to vendor, audit, or keep on a target.
Own-code cost is a real cost. ~250 lines of hand-rolled HTTP is code we own, test and carry, and hand-rolled HTTP has a bad security history. It listens on loopback only and speaks to a page it emitted itself, which bounds the exposure but does not erase it. That trade — 250 own lines against 480,501 third-party ones — is the decision ADR-0007 has to make explicitly rather than inherit from this survey.
6. What this cannot do
Stated concretely, because the adversarial review (T02) should not have to discover it:
- No frame loop. Animation, drag feedback at pointer-event rates, and anything requiring sustained redraw are the browser's problem, not the port's — the Rust side emits state and receives commands. If stage 1 later wants tweened card motion, it is written in the emitted JS, in a language this repo otherwise does not use and has no gates for.
- No offline binary. A game that "ships" ships a process that wants a browser. For a headless-first rules engine that is nearly free; for a consumer product at stage 4 it is not.
- A second language enters the build. JavaScript in emitted strings is
untested by
cargo testand invisible to every gate this repo has. This is the strongest argument against, and the one T02 should press. - It is not stage 2's renderer. Nothing here carries forward to wgpu except the port interface — which is the argument for doing it, under the second-use rule, but only if the interface is written to be implemented twice rather than written around HTML.
7. Recommendation (revised after review)
The first draft recommended declaring cb-render-api and cb-render-null
now, while also saying the interface becomes canonical only at stage 2's
second use. Those two cannot both hold, and INTENT's rule is the one
that survives: "No concept becomes canonical merely because it looks
general. It becomes canonical after surviving a second concrete use." A
port written against exactly one implementation that emits whole documents
will acquire a document's shape — retained mode, full state per update,
string identity, no frame timing — and stage 2 will find it
unimplementable. CommitWindow is already in this repo with a delete-by
date for precisely this mistake.
- No port this pass.
cb-render-apiandcb-render-nullare withdrawn. A null implementation of an interface that does not exist is a null implementation of nothing. cb-render-htmlships against the existingProjecttrait — a real interface with real implementations, not a speculative one. Marginal AM-4a cost zero, honestly zero under §5's rule.- The port is declared at stage 2, derived from two implementations instead of imagined from one.
- AM-4a's proc-macro defect is filed separately, and is not a precondition for any of the above.
- AM-4a's incompatibility with stage 2 (§4) goes to the maintainer.
Six controls the implementation must carry
From the review, and binding on ADR-0007:
| # | control |
|---|---|
| 1 | an unguessable token minted per process, required on every request |
| 2 | Origin / Sec-Fetch-Site checked, rejected by default |
| 3 | listener bound explicitly to 127.0.0.1 |
| 4 | a test that a token-less request is refused, with an M-D1-MUT mutation removing the check and turning it red |
| 5 | JavaScript may not construct commands — the page reports raw pointer facts, Rust decides what command they mean |
| 6 | the coverage gate crosses the language boundary — the HTML counterpart of every_view_field_is_classified asserts over the parsed emitted document, not over the Rust that emits it |
Controls 1–3 without 4 are three claims and no evidence. Control 6 exists because asserting over the emitting code would reproduce CB-WP-0011's original defect one layer up.
The honest summary
Two of this survey's three arguments did not survive review. The budget constraint that appeared to force the decision was overstated by 36% by a mis-instrumented metric, and then overstated again by a candidate list that contained no cheap windowed toolkits — the real floor is 1.5× the corrected headroom, not 128×. The port the survey proposed to build contradicted the rule the same survey cited.
What survives is narrower and better founded: stage 1 should not spend stage 2's money on a toolkit stage 2 discards, and it should not canonize an interface it can only implement once. The recommendation is unchanged. The reasoning that reached it has been replaced, which is what the review step is for.