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

@ -0,0 +1,204 @@
# 260802 — challenge to CB-RES-0006
Adversarial review, one round, per InnerLoop §Step 2.
**Fidelity note, first, because it caps everything below.** The spec asks
for a *separate session or agent*. This review was run in the same session
as the survey, because this environment's standing instruction is not to
spawn agents unless asked. So it does not have the property the step exists
to provide: the reviewer inherits the author's sampling, the author's
framing, and the author's blind spots. Every prior instance of this class in
the repo — CB-WP-0002's dedup sample, CB-WP-0005's AM-7 assertion — was a
shared blind spot between author and reviewer who sampled the same way.
Treat the challenges below as a lower bound on what a genuinely separate
reviewer would find.
The claim rests on numbers, so per the table in §Step 2 the reviewer's job
is to reproduce them independently and to mutate the assertions behind
them — not to argue with the prose.
---
## C1 — The survey concluded the sub-100k region was empty without looking
**This is the challenge that lands.**
The survey measures seven candidates. The cheapest windowed one costs
480,501. It then reports the corrected headroom as 92,798 and concludes
that no windowed toolkit fits — a conclusion about the interval
`0 < x < 92,798` drawn from a sample whose smallest windowed member is
480,501. Nothing was measured in the interval the conclusion is about.
The candidate list is not random, either. It is the list a 2026 Rust
developer reaches for, which is a list of GPU-era stacks: everything on it
pulls `wgpu`, `winit`, or `rustix`. Cheap, old, thin bindings to system
libraries were structurally absent from the sample.
Measured, same method, marginal against the same base:
| candidate | marginal lines | vs 92,798 corrected headroom |
|---|---:|---:|
| `tiny-skia` alone | **83,956** | **fits**, at 90% of it |
| `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× |
| `pixels` | 863,528 | 9.3× |
| `speedy2d` | 1,328,008 | 14.3× |
**The survey's central rhetorical move — "this is not a near miss to be
negotiated; it is two orders of magnitude" — is false.** It is two orders
of magnitude for the candidates the survey chose. The real floor for a
windowed toolkit is `fltk` at 140,079, which is **1.5×** the corrected
headroom. That is exactly a near miss to be negotiated.
`tiny-skia` at 83,956 actually fits — it is a pure-Rust CPU rasterizer with
no windowing, so it is not by itself a table, but it disproves the survey's
stated interval claim on its own.
**Required:** the survey must either withdraw the "two orders of magnitude"
framing and re-argue against a 1.5× floor, or concede that its
recommendation rests on something other than dependency cost.
## C2 — "Marginal cost zero" is scored on an axis chosen to produce zero
AM-4a measures vendored third-party **Rust**. The HTML option scores zero
because it relocates the renderer into a runtime AM-4a cannot see. The
browser is tens of millions of lines of unaudited third-party code. Calling
that zero is true under the metric and false in substance.
If a proposal may satisfy a dependency target by moving the dependency
somewhere the target does not look, then the target constrains nothing, and
this pass is the first to demonstrate it. That is a worse outcome for the
project than buying `macroquad` honestly.
**And the same critique convicts C1's cheap candidates.** `sdl2` at 191,973
and `fltk` at 140,079 are thin Rust bindings to large C libraries; their
`.rs` line counts exclude the actual implementation for exactly the same
reason the browser's is excluded. So the metric already permits this move,
and the survey's own table already rewards it — it simply did not notice,
because it never measured a binding.
**Required:** one rule, applied to all three. Either relocating an
implementation into an unmeasured runtime is permissible (and then `sdl2`
and `fltk` are legitimately cheap and must be compared on their merits, not
excluded on cost), or it is not (and then HTML is not free either). The
survey cannot have it one way for the browser and another for `sdl2`
without stating the distinction. It states none.
## C3 — The survey contradicts itself on the second-use rule
Recommendation 1 declares `cb-render-api` a canonical port now.
Recommendation 4 says `cb-render-wgpu` at stage 2 is "where the port
interface gets its second use and only then becomes canonical."
Both cannot hold. INTENT: *"No concept becomes canonical merely because it
looks general. It becomes canonical after surviving a second concrete
use."* A port interface written against exactly one implementation, which
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 and rewrite it. The survey names this risk in §6
and mitigates it with "write it to be implemented twice," which is a wish.
`CommitWindow` is already on record in this repo as the concept that looked
general, has a delete-by date, and has declined two second-use
opportunities.
**Required:** either defer `cb-render-api` until stage 2 supplies a second
implementation, and ship `cb-render-html` directly against the existing
`Project` trait — or state why this port is exempt from a rule INTENT
states without exception.
## C4 — The 250-line HTTP server is undersold, and "loopback only" is not a boundary
The survey bounds the risk with "listens on loopback only and speaks to a
page it emitted itself." Neither clause holds:
- Any process on the machine can reach a loopback listener. Hot-seat play
is explicitly *several people at one machine*.
- **Any web page the user visits can reach it too.** A page on the open
internet can issue requests to `127.0.0.1` — that is what DNS rebinding
and cross-origin POST are. The browser running the table is the same
browser reading the internet; that is the whole premise of the option.
- "A page it emitted itself" is an assumption the server cannot check
unless it is written to check it, and the survey specifies no such check.
An unauthenticated loopback endpoint that accepts `POST /command` and
mutates authoritative game state is a remote-controlled game from any tab
the user has open.
**Required, as concrete controls the survey lacks:** an unguessable token
minted per process and required on every request; `Origin` and
`Sec-Fetch-Site` checked and rejected by default; the listener bound to
`127.0.0.1` explicitly rather than `0.0.0.0`; and a test that a request
without the token is refused. The last one is the one that matters — the
other three are claims until something asserts them.
## C5 — The proposal regresses the finding the previous pass paid for
CB-WP-0011's finding, in its own words: a renderer's defect class is
**silent omission**, no natural renderer assertion catches it, and the fix
is to walk the shape of the input and make silence cost a build.
This proposal moves drag hit-testing and command construction into
JavaScript embedded in emitted strings — a language `cargo test` cannot
run, `clippy` cannot lint, and `M-D1-MUT` cannot mutate. The gate that
CB-WP-0011 built stops at the language boundary, and the survey proposes
putting the interactive half of stage 1 on the far side of it, one pass
after paying to learn why that is dangerous.
§6 lists this as "the strongest argument against, and the one T02 should
press." Naming a challenge in advance is not answering it.
**Required, as controls:**
- **JS may not construct commands.** The emitted page reports raw pointer
facts — "pointer down on element `id`, up on element `id`" — and Rust
decides what command that is. That confines JS to input transport and
makes the decision testable by feeding the Rust side synthetic events.
- **The coverage gate must cross the boundary.** `every_view_field_is_
classified` must have an HTML counterpart asserting over the *parsed
emitted document*, not over the Rust that emits it. Emitting a `<div>`
per field is not evidence the field is visible; the assertion must be
that the token is in the document.
## C6 — One number in the survey is reproduced; one is not independently checkable
Reproduced: the proc-macro delta. `--edges normal,no-proc-macro` gives 18
crates / 157,202 lines against 23 / 246,250. The 89,048 / 36.2% figure is
correct, and the five crates named are the five that differ.
**Not independently checkable:** every candidate figure, because the
reviewer used the author's script. Per §Step 2 this is precisely the
failure mode — "the reviewer re-derives on a different sample than the
author used" — and no different sample was used. The C1 table above is
*new* data from the same instrument, which tests the survey's coverage but
not its instrument.
**What the harness would report if it silently stopped:** `source_lines()`
returns 0 for a crate it cannot locate, and the probe scripts sum without
a positive control. A registry path change would report every candidate as
0 marginal lines — i.e. as *fitting comfortably* — which is the direction
that flatters the recommendation. `tools/dep-weight.py` has exactly this
control (`unlocatable crate measures zero (so the guard fires)`); the probe
scripts copied the measurement function and not the guard.
The `svg-emit` row in the survey's table reads `0` marginal for reasons
that are correct, but a reader cannot distinguish that 0 from a harness
that stopped. **A zero that means "costs nothing" and a zero that means
"measured nothing" are printed identically.** That is HDN, in the survey's
own instrument, in the row carrying the recommendation.
---
## Verdict
**Not approvable as written.** C1, C2, C3 and C6 each require a change to
the survey, not a clarification. C4 and C5 require controls that must land
in ADR-0007 as conditions on the implementation.
The recommendation may well survive — none of these establishes that a
windowed toolkit is affordable, only that the survey's argument for why it
is not was overstated and partly unmeasured. But the survey as written
argues from a false interval claim, an inconsistently applied metric rule,
an internal contradiction, and a zero that cannot be distinguished from a
dead harness.

View file

@ -0,0 +1,75 @@
# 260802 — how CB-RES-0006 was conducted
Unpolished trail for `research/CB-RES-0006-render-port.md`, per
InnerLoop §Step 2 documentation requirement.
## What was measured vs cited
**Everything quoted is measured.** No line count in the survey comes from
a README, a crates.io page, or memory. Method: a scratch cargo crate per
candidate, `cargo add`, then `cargo tree --edges normal --prefix none`
over the resolved graph, then `\n` counted in every `.rs` file of the
vendored source under `~/.cargo/registry/src/*/`.
That method is copied out of `tools/dep-weight.py` rather than
re-invented, because the survey's whole purpose is to subtract candidate
sizes from a budget produced by that tool, and two methods produce two
numbers that cannot be subtracted.
Scripts: `$CLAUDE_JOB_DIR/tmp/survey/{measure,breakdown,gap}.py`.
Deliberately not committed — a throwaway probe promoted to a repo tool is
how the second-use rule gets broken by accident. If a later pass needs
these numbers refreshed, it should re-derive them, and the fact that it
costs ten minutes is a feature.
## Dead ends and corrections during the survey
**First reading was on the wrong axis.** The initial run reported headline
totals: `egui+eframe` 2,946,121, `ratatui+crossterm` 1,146,363,
`macroquad` 480,729. Those are the numbers a survey normally quotes and
they are the wrong ones — AM-4a charges what a candidate *adds* to a graph
that already holds 23 crates. Recomputed as marginal against the real
base. For `macroquad` the difference is negligible (480,729 → 480,501);
for `egui+eframe` it is 163,272 lines of overlap.
**A network 403 that was not a network failure.** `curl` to crates.io
returned 403; the sandbox looked like it was blocking egress. It was a
missing user-agent — crates.io rejects requests without one. Two further
probes (sparse index, static.crates.io) returned 200. Worth recording
because "the environment is blocking me" is a conclusion that ends
surveys, and it was wrong here by one header.
**Checked whether `cargo tree` was including non-host targets.** The
`ratatui` figure was dominated by `linux-raw-sys` (479,901) and looked
like it might be an all-platform artifact — a Windows-bindings inflation
of a Linux measurement. Re-ran with `--target x86_64-unknown-linux-gnu`:
identical, 1,146,363 both ways. The number is real, and `ratatui` really
does cost more than a 2D game framework.
**The proc-macro finding was not what the survey set out to look for.** It
came from asking why `syn` was the largest single entry in a
"shipped-runtime" figure. `--edges normal,no-proc-macro` answered it:
89,048 lines, 36.2%, none of which reaches a binary.
Order matters here, and it went the right way by luck rather than design:
the candidate measurements were complete *before* the instrument defect
was found, so the recommendation could be checked against the corrected
number instead of being derived from it. Had it gone the other way the
survey would have been unusable — a budget correction discovered while
looking for headroom is not evidence, whatever it says.
## What the survey did not do
- **It did not sample the region between 0 and 480,501 lines.** The
candidate list was assembled from what one reaches for — modern GPU-era
Rust GUI stacks — and every one of them landed two orders of magnitude
over. The conclusion "no windowed toolkit fits" was therefore drawn from
a sample that contained no small windowed toolkits. This is the gap the
challenge (T02) attacks, and it is a real one.
- **It did not run any candidate.** No candidate was built, drawn with, or
benchmarked. Per InnerLoop §Step 1 the comparisons here are dependency
cost only; nothing in the survey claims one toolkit draws better than
another, and no `better` verdict is available on that axis.
- **It did not measure the browser.** The HTML option is scored zero on
AM-4a because AM-4a measures vendored Rust. What the browser costs on
any other axis is unmeasured and the survey says so in §5.

View file

@ -0,0 +1,193 @@
# 260802 — response to the challenge on CB-RES-0006
One round, per InnerLoop §Step 2. Four challenges conceded, two answered
with evidence, and one of the concessions turned up something larger than
this pass.
---
## C1 — the unmeasured interval — **conceded, and the framing withdrawn**
The challenge is right and the survey's rhetoric was wrong. "Two orders of
magnitude" described the candidates I picked, and I picked them from what a
2026 Rust developer reaches for, which is a list of GPU-era stacks. The
real floor for a windowed toolkit is `fltk` at **140,079** marginal lines —
**1.5×** the corrected headroom, not 128×.
The C1 table is folded into the survey as measured data, including
`tiny-skia` at 83,956, which fits inside 92,798 and disproves the interval
claim by itself.
**So dependency cost alone does not decide this.** The survey's argument as
written is withdrawn. What replaces it is an allocation argument, and it is
stronger:
INTENT stage 2 names `wgpu` explicitly. That is a **1,741,979-line**
marginal cost, already measured, already unavoidable, and it arrives one
stage from now. Against that, spending 1.5× the entire remaining budget on
a stage-1 windowing toolkit that stage 2 immediately discards is the wrong
allocation — not because it does not fit, but because it buys a thing with
a known expiry using money already committed elsewhere.
That argument does not need AM-4a's target to be exactly 250,000. It only
needs stage 2's bill to be larger than stage 1's benefit, which is
measured, and true by a factor of twelve.
## C1b — what the concession exposed: **AM-4a cannot survive stage 2**
Following C1 honestly produces a finding neither the survey nor the
challenge was looking for.
| | marginal lines |
|---|---:|
| AM-4a target, total | 250,000 |
| current shipped-runtime (corrected, proc-macro excluded) | 157,202 |
| headroom | 92,798 |
| `wgpu` + `winit`, which INTENT stage 2 names by name | **1,741,979** |
Stage 2 exceeds the entire AM-4a target by **7×**, and exceeds the
remaining headroom by **19×**. No sequencing, no feature-gating and no
metric correction closes a gap that size. **AM-4a as targeted is
incompatible with INTENT as written**, and has been since both were
written; nothing in this pass caused it.
That is a maintainer's decision, not mine — either the target moves, or the
port stays out of the AM-4a configuration by an argued rule, or stage 2
changes. It is recorded here and carried to the evidence file, and it is
deliberately **not** resolved by this pass, because a pass that discovers a
budget conflict and also decides it has reviewed nothing.
It does, however, retire one bad reason for the recommendation. "HTML,
because we cannot afford anything else" is false. "HTML, because stage 1
should not spend stage 2's money on something stage 2 throws away" is true.
## C2 — the metric is gameable by relocation — **conceded; one rule, stated**
The challenge is right that the survey scored the browser at zero on an
axis chosen to produce zero, and right that the same move already makes
`sdl2` and `fltk` look cheap in the survey's own table. I had not noticed,
because I had measured no bindings.
The rule, which belongs in AM-4's definition and not in this pass's
reasoning, applied consistently to all three:
> **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 that rule, consistently:
- a **browser** is not counted — nothing we ship or instruct causes it to
be acquired;
- **`sdl2`/`fltk`** are counted at more than their Rust binding, not less —
they oblige a user to install a `-dev` package they would not otherwise
have, so the survey's 191,973 and 140,079 **understate** them;
- and the rule makes the metric *harder* to satisfy by relocation, not
easier, which is the direction a rule proposed by the party it benefits
has to go before it is worth anything.
This makes the HTML row honest rather than merely favourable. It also
means the survey's table understates two candidates, and that correction is
folded in.
## C3 — the port contradicts the second-use rule — **conceded entirely**
Recommendations 1 and 4 cannot both stand, and 4 is the one INTENT
supports. `CommitWindow` is already in this repo with a delete-by date for
exactly this mistake.
**`cb-render-api` is withdrawn from this pass.** `cb-render-html` ships
against the existing `Project` trait, which is a real interface with real
implementations rather than a speculative one. The port gets declared at
stage 2, when `wgpu` supplies the second concrete use and the interface can
be derived from two implementations instead of imagined from one.
This also removes `cb-render-null`: a null implementation of an interface
that does not exist yet is a null implementation of nothing.
**And it changes the pass's tier.** The structural trigger for tier L was
"creates a new capability port." This pass no longer creates one. That is
recorded rather than acted on — the declaration and its roll stand, and the
pass runs at the weight it declared. Re-rolling a tier because review made
the work smaller would make the tier a function of the outcome, which is
the one thing a tier declaration must not be. **The CHAOS calibration
window gets a second entry from this**, and it is a more interesting one
than the first: tier L at full weight deleted its own structural trigger.
## C4 — the loopback server — **conceded; controls carried to ADR-0007**
The challenge is right that "loopback only" is not a boundary against a
browser that also reads the internet, and right that "a page it emitted
itself" is an assumption no code checks. All four controls are adopted as
conditions on the implementation, and the fourth is the load-bearing one:
1. an unguessable token minted per process, required on every request;
2. `Origin` / `Sec-Fetch-Site` checked, rejected by default;
3. bound explicitly to `127.0.0.1`;
4. **a test that a request without the token is refused** — and, per
M-D1-MUT, a mutation that removes the check and turns that test red.
Controls 13 without 4 are three claims and no evidence, which is the
class this project keeps finding in its own work.
## C5 — the gate stops at the language boundary — **conceded; both controls adopted**
Naming a challenge in the survey and calling it pressed was not an answer,
and the challenge says so correctly.
1. **JavaScript may not construct commands.** The page reports raw pointer
facts; Rust decides what command they mean. JS becomes input transport,
and the decision becomes testable in Rust against synthetic events.
2. **The coverage gate crosses the boundary.** The HTML counterpart to
`every_view_field_is_classified` asserts over the *parsed emitted
document* — the token is in the document, or the build fails. Asserting
over the Rust that emits the document would reproduce CB-WP-0011's
original defect one layer up.
## C6 — the zeros — **partly answered, partly conceded**
**Answered:** the candidate measurements did carry the positive control the
challenge says they lacked. `measure.py` tracks an `unlocated` list per
candidate and it is empty for all seven — verified in `results.json`, not
asserted. So no candidate figure is a silently-stopped harness.
**Conceded, twice over:**
- `gap.py`, which produced the C1 table, copied the measurement function
**without** the guard. The C1 numbers therefore have no positive control.
They are load-bearing now, since they are what withdrew the survey's main
argument, so they must be re-measured under the guard before ADR-0007
cites them.
- The survey's HTML row is **0 by construction, not by measurement**
there is no crate to fail to locate. The challenge is right that a
reader cannot tell that zero from a dead one, and the survey printed them
identically in a table whose other rows are measurements. It is relabelled
rather than left to be read as a measured figure.
The general observation is worth keeping: a probe script that copies a
tool's measurement function and not its controls is how a positive control
gets lost, and it happened here inside one pass, in a repo whose whole
discipline is positive controls.
---
## What changed in the survey
- The "two orders of magnitude" framing is **withdrawn**; the real floor is
`fltk` at 1.5× corrected headroom.
- The C1 candidates are folded in as measured rows, re-measured under the
positive control.
- The recommendation's justification changes from *affordability* to
*allocation against stage 2's known 1,741,979-line bill*.
- The AM-4 acquisition rule is stated, and it **raises** the cost of two
candidates rather than lowering the cost of the recommended one.
- **`cb-render-api` and `cb-render-null` are withdrawn from this pass.**
- The HTML row is relabelled *0 by construction*.
- C4 and C5's six controls become conditions in ADR-0007.
- **AM-4a's incompatibility with stage 2 is raised as a maintainer
decision** and explicitly not decided here.
**Approvable after these changes.** The recommendation stands; the argument
that reached it does not, and has been replaced.

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.

View file

@ -102,7 +102,7 @@ left to stage 2 where it becomes the interface's second use.
```task
id: CB-WP-0012-T02
status: todo
status: done
priority: high
```
@ -120,6 +120,43 @@ it. The review must name, at minimum:
A review that ratifies the survey without producing a control the survey
lacked has not run.
**Done 2026-08-02.** [challenge](../history/260802-render-port-challenge.md),
[response](../history/260802-render-port-response.md). **Not approvable as
written** — four of six challenges conceded, and the survey's two main
arguments replaced.
- **C1**: the survey concluded the sub-100k region was empty without
measuring it. It is not empty. `tiny-skia` fits at 83,956; the real
windowed floor is `fltk` at **140,079 — 1.5×** corrected headroom, not
128×. "Two orders of magnitude" withdrawn.
- **C1b**, which the concession exposed: `wgpu` + `winit` is **1,741,979**
marginal lines against a 250,000 target. **AM-4a is incompatible with
INTENT stage 2** — 7× the whole target — and has been since both were
written. Raised for the maintainer, deliberately not decided here.
- **C2**: "marginal cost zero" was scored on an axis chosen to produce
zero, and the same move already flattered `sdl2`/`fltk`. One acquisition
rule now covers all three, and it *raises* two candidates' cost.
- **C3**: the survey contradicted itself on the second-use rule.
**`cb-render-api` and `cb-render-null` are withdrawn from this pass.**
- **C4/C5**: six controls adopted as binding on ADR-0007 — a loopback
token with a mutation-backed refusal test, JS barred from constructing
commands, and the coverage gate crossing the language boundary.
- **C6**: the candidate measurements did carry a positive control (empty
`unlocated` for all seven, verified). The C1 batch did **not** — it
copied the measurement function without the guards — and was re-measured
under them before being cited. The HTML row is relabelled *0 by
construction, not by measurement*.
**Fidelity note:** the review ran in the same session as the survey rather
than a separate one, per this environment's standing instruction not to
spawn agents unasked. It therefore inherits the author's sampling and is a
lower bound on what a separate reviewer would find.
**Note on tier:** this pass no longer creates a capability port, which was
its structural trigger for tier L. The declaration and its roll stand — a
tier that changes because review shrank the work would be a function of the
outcome. Recorded as the CHAOS window's second entry.
## Task: ADR-0007 — the render port
```task