From c5fa610e597628409489dc698dfa41585efd95f6 Mon Sep 17 00:00:00 2001 From: tegwick Date: Sun, 2 Aug 2026 04:12:22 +0200 Subject: [PATCH] =?UTF-8?q?CB-WP-0012-T02:=20adversarial=20review=20?= =?UTF-8?q?=E2=80=94=20not=20approvable=20as=20written?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- history/260802-render-port-challenge.md | 204 ++++++++++++++++++++++++ history/260802-render-port-research.md | 75 +++++++++ history/260802-render-port-response.md | 193 ++++++++++++++++++++++ research/CB-RES-0006-render-port.md | 181 ++++++++++++++++++--- workplans/CB-WP-0012-render-port.md | 39 ++++- 5 files changed, 665 insertions(+), 27 deletions(-) create mode 100644 history/260802-render-port-challenge.md create mode 100644 history/260802-render-port-research.md create mode 100644 history/260802-render-port-response.md 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