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