# CB-RES-0006 — the render port, and what a 2D table costs capability: render.table.2d status: approved (v2, after adversarial review — v1 was **not approvable**, with four conceded findings; see §2, §4, §5, §7) tier: L (structural L — the declaration was made when this pass would create a capability port; chaos d4=1 → no override. Review then removed the port, and the tier was deliberately **not** re-derived: a tier that changes because review shrank the work is a function of the outcome) CB-WP-0012 T01. Step 1 of the inner loop. Measured 2026-08-02 at `331e7e9`. The question: INTENT stage 1 needs `cb-render-api` and a first real implementation. AM-4a reports **3,750 lines of headroom** (246,250 of a 250,000 target). Is that a real obstacle, or an artifact of the instrument? Both, and not in the proportions I expected. --- ## 1. Method Every figure below comes from the same method `tools/dep-weight.py` uses for AM-4a: resolve a real dependency graph with `cargo tree --edges normal --prefix none`, drop path dependencies, and count `\n` in every `.rs` file of the vendored source. Reusing the method 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 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. ## 2. What a 2D table costs Base: `games-ground --no-default-features`, 23 third-party crates, 246,250 lines. | candidate | total | shared with base | **marginal** | vs 3,750 headroom | |---|---:|---:|---:|---:| | `egui` + `eframe` | 2,946,121 | 163,272 | **2,782,849** | 742× | | `wgpu` + `winit` | 1,866,820 | 124,841 | **1,741,979** | 465× | | `softbuffer` + `tiny-skia` + `winit` | 1,271,922 | 124,841 | **1,147,081** | 306× | | `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 by construction** | — | **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 `macroquad`, a 2D game framework with a GPU backend. The reason is `linux-raw-sys` at 479,901 lines and `libc` at 129,990, pulled through `rustix`. Neither is code anyone audits by hand; both are largely generated constant and type definitions. Which raises the next question. ## 3. The instrument is wrong, and has been all along AM-4a counts the `--edges normal` graph. That graph includes **proc-macro crates**, which run in the compiler and never reach a shipped binary. | `games-ground --no-default-features` | crates | lines | |---|---:|---:| | as AM-4a measures it | 23 | **246,250** | | excluding proc-macro crates (`--edges normal,no-proc-macro`) | 18 | **157,202** | | the difference | 5 | **89,048** (36.2%) | The five are `syn`, `quote`, `proc-macro2`, `unicode-ident` and `serde_derive`. `syn` alone is 66,916 lines — the single largest entry in the shipped-runtime figure, and it is a parser for Rust source that exists only at build time. **So 36.2% of what AM-4a calls "what a game ships" is not shipped.** Under a corrected metric the headroom is not 3,750 but **92,798**. Every pass that has cited the 3,750 figure — including CB-WP-0011's own reasoning for deferring this declaration — cited a number that was wrong in the conservative direction. **This does not change the recommendation, and that is the point of checking it before recommending.** Under the corrected metric `macroquad` still costs 5.2× the available headroom, and every other windowed candidate is worse. The dependency argument survives the correction of the number it rests on, which is the only condition under which a survey may propose correcting a number in its own favour. The correction is proposed as a separate, narrow change — the metric should measure what ships — and it must not be bundled with the render decision, because a budget correction that arrives attached to the request it unblocks is indistinguishable from motivated reasoning even when it is right. ## 4. What stage 1 actually asks for > *card/token/hand/relationship-graph visualization, drag-to-propose, > debug inspector, hot-seat play* Read plainly: draw a table, let a human point at it, and let several humans take turns at one machine. It does **not** ask for a GPU, a frame 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. ### 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 Emit HTML with inline SVG and inline JavaScript; open it in the browser the machine already has. - **visualization** — SVG draws cards, tokens and a relationship graph natively, and the relationship graph is the one element every Rust 2D toolkit would have made us hand-roll anyway. - **drag-to-propose** — pointer events in the emitted page. - **debug inspector** — already shipped (CB-WP-0011), and its renderer is already a total function from a projection to text; HTML is a second output format for the same walk. - **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 HTTP/1.1 listener sufficient for `GET /` and `POST /command` is `std::net::TcpListener` plus roughly 250 lines of our own code, and our own code is not what AM-4a governs. This is the central rule applied literally — *own the semantics; assimilate the implementation*. The browser is the most thoroughly assimilated 2D renderer available, and it is the one implementation we are guaranteed not to have to vendor, audit, or keep on a target. **Own-code cost is a real cost.** ~250 lines of hand-rolled HTTP is code we own, test and carry, and hand-rolled HTTP has a bad security history. It listens on loopback only and speaks to a page it emitted itself, which bounds the exposure but does not erase it. That trade — 250 own lines against 480,501 third-party ones — is the decision ADR-0007 has to make explicitly rather than inherit from this survey. ## 6. What this cannot do Stated concretely, because the adversarial review (T02) should not have to discover it: - **No frame loop.** Animation, drag *feedback* at pointer-event rates, and anything requiring sustained redraw are the browser's problem, not the port's — the Rust side emits state and receives commands. If stage 1 later wants tweened card motion, it is written in the emitted JS, in a language this repo otherwise does not use and has no gates for. - **No offline binary.** A game that "ships" ships a process that wants a browser. For a headless-first rules engine that is nearly free; for a consumer product at stage 4 it is not. - **A second language enters the build.** JavaScript in emitted strings is untested by `cargo test` and invisible to every gate this repo has. This is the strongest argument against, and the one T02 should press. - **It is not stage 2's renderer.** Nothing here carries forward to wgpu except the port interface — which is the argument *for* doing it, under the second-use rule, but only if the interface is written to be implemented twice rather than written around HTML. ## 7. Recommendation (revised after review) 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.** ### 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.