CB-WP-0012-T02: adversarial review — not approvable as written

Four of six challenges conceded; the survey's two main arguments are
replaced rather than clarified.

C1  The survey concluded the sub-100k region was empty without ever
    measuring it. It is not empty: tiny-skia fits at 83,956, and the
    real windowed floor is fltk at 140,079 — 1.5x corrected headroom,
    not 128x. "Two orders of magnitude" is withdrawn.

C1b Following that concession: wgpu+winit is 1,741,979 marginal lines
    against a 250,000 target. AM-4a is incompatible with INTENT stage 2
    by 7x, and has been since both were written. Raised for the
    maintainer; deliberately not decided by this pass.

C2  "Marginal cost zero" was scored on an axis chosen to produce zero,
    and the same relocation already flattered sdl2 and fltk in the
    survey's own table. One acquisition rule now covers all three, and
    it raises two candidates' cost rather than lowering the winner's.

C3  The survey contradicted itself on the second-use rule. cb-render-api
    and cb-render-null are withdrawn; cb-render-html ships against the
    existing Project trait, and the port waits for stage 2's second use.

C6  The candidate measurements did carry a positive control. The C1
    batch did not — it copied dep-weight.py's measurement function
    without its guards — and was re-measured under them before being
    cited. Figures reproduced unchanged. The HTML row is relabelled
    0 by construction, not by measurement.

C4/C5 become six controls binding on ADR-0007.

The recommendation stands. The reasoning that reached it does not.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
tegwick 2026-08-02 04:12:22 +02:00
parent 53abbaf68f
commit c5fa610e59
5 changed files with 665 additions and 27 deletions

View file

@ -22,11 +22,20 @@ 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 script is
`$CLAUDE_JOB_DIR/tmp/survey/measure.py`; it is not committed, because a
throwaway probe promoted to a tool is how the second-use rule gets broken
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.
@ -44,11 +53,39 @@ Base: `games-ground --no-default-features`, 23 third-party crates,
| `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** | 0× |
| HTML/SVG emission, no Rust toolkit | — | — | **0 by construction** | — |
The cheapest candidate that opens a window costs **128 times** the entire
remaining budget. This is not a near miss to be negotiated; it is two
orders of magnitude.
**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
@ -103,10 +140,37 @@ 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.
Buying a GPU toolkit now would mean paying stage 2's dependency bill to
satisfy stage 1's requirements, one stage early, against a budget that
cannot cover it, and before the port interface has ever had a second
implementation to be validated against.
### 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
@ -123,6 +187,33 @@ the machine already has.
- **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
@ -163,21 +254,59 @@ discover it:
the second-use rule, but only if the interface is written to be
implemented twice rather than written around HTML.
## 7. Recommendation
## 7. Recommendation (revised after review)
1. **`cb-render-api`** — a canonical port over the existing `Project`ion:
state in, drawable primitives out, proposed commands back. No
assimilated types cross it (ArchitectureBlueprint §Never leak).
2. **`cb-render-null`** — the reference no-op, so tests and agents need
neither GPU nor browser.
3. **`cb-render-html`** — the first real implementation, marginal AM-4a
cost zero.
4. **`cb-render-wgpu` is stage 2's**, and is where the port interface gets
its second use and only then becomes canonical.
5. **AM-4a's proc-macro defect is filed separately** and is not a
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.**
**The honest summary:** the constraint that looked like it would force
this decision turns out to be overstated by 36%, and the decision does not
change. The recommendation is what it would have been at 92,798 lines of
headroom, which is the only reason it is worth trusting at 3,750.
### 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 13 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.