diff --git a/history/260802-render-port-challenge.md b/history/260802-render-port-challenge.md
new file mode 100644
index 0000000..6c3e7cd
--- /dev/null
+++ b/history/260802-render-port-challenge.md
@@ -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 `
`
+ 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.
diff --git a/history/260802-render-port-research.md b/history/260802-render-port-research.md
new file mode 100644
index 0000000..2e875c1
--- /dev/null
+++ b/history/260802-render-port-research.md
@@ -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.
diff --git a/history/260802-render-port-response.md b/history/260802-render-port-response.md
new file mode 100644
index 0000000..741b836
--- /dev/null
+++ b/history/260802-render-port-response.md
@@ -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 1–3 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.
diff --git a/research/CB-RES-0006-render-port.md b/research/CB-RES-0006-render-port.md
index b74e7c1..c3e9a74 100644
--- a/research/CB-RES-0006-render-port.md
+++ b/research/CB-RES-0006-render-port.md
@@ -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 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.
diff --git a/workplans/CB-WP-0012-render-port.md b/workplans/CB-WP-0012-render-port.md
index 1fc3d77..c6fbe46 100644
--- a/workplans/CB-WP-0012-render-port.md
+++ b/workplans/CB-WP-0012-render-port.md
@@ -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