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>
320 lines
16 KiB
Markdown
320 lines
16 KiB
Markdown
# 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 test` and 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.
|
||
|
||
1. **No port this pass.** `cb-render-api` and `cb-render-null` are
|
||
withdrawn. A null implementation of an interface that does not exist is
|
||
a null implementation of nothing.
|
||
2. **`cb-render-html`** ships against the **existing `Project` trait** — a
|
||
real interface with real implementations, not a speculative one.
|
||
Marginal AM-4a cost zero, honestly zero under §5's rule.
|
||
3. **The port is declared at stage 2**, derived from two implementations
|
||
instead of imagined from one.
|
||
4. **AM-4a's proc-macro defect is filed separately**, and is not a
|
||
precondition for any of the above.
|
||
5. **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.
|