Five decisions: 1. cb-render-html emits HTML/SVG/JS; the browser draws it. Rejected on measured marginal cost: egui+eframe 2,782,849; wgpu+winit 1,741,979; ratatui 1,067,013; macroquad 480,501; sdl2 191,973; fltk 140,079. The reason is allocation, not affordability — fltk is only 1.5x the corrected headroom, but wgpu is 12x fltk, unavoidable, and named by INTENT for stage 2. Stage 1 should not buy windowing stage 2 discards. 2. No cb-render-api and no cb-render-null. A port designed against one document-emitting implementation acquires a document's shape and stage 2 rewrites it. INTENT's second-use rule beats the blueprint's port pattern on timing. cb-render-html ships against Project. 3. AM-4 counts third-party code the project causes to be ACQUIRED, not runtimes the user already has. Browsers are not counted; sdl2 and fltk are counted at MORE than their Rust bindings. The rule costs more than it saves, which is the only reason to trust it here. 4. AM-4a's proc-macro defect (89,048 lines, 36.2%) is filed separately, not bundled with the request it unblocks. 5. Six controls bind the implementation, including a mutation-backed test that a token-less loopback request is refused, a bar on JS constructing commands, and a coverage gate asserting over the parsed emitted document rather than the Rust that emits it. Reserved for the maintainer: AM-4a exceeds INTENT stage 2 by 7x and cannot survive it; and whether decision 3's rule is the right one. The hard gate held — no render implementation code before this commit. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
9.8 KiB
ADR-0007: render to HTML, and do not declare the port yet
status: accepted
date: 2026-08-02
decided by: agent, under the standing loop authorization. Two items are
explicitly reserved for the maintainer and are not decided here — see
§Reserved.
tier: L (structural L — the declaration was made when this pass would
create a capability port; chaos d4=1 → no override. Review removed the
port, and the tier was not re-derived — see §Consequences)
references: CB-RES-0006,
history/260802-render-port-{research,challenge,response}.md,
INTENT.md §Implementation order,
ArchitectureBlueprint.md §Port pattern,
ADR-0004 (AM-4 budgets),
CB-EV-0009 (the inspector)
Context
INTENT stage 1 is "Inspectable 2D table — card/token/hand/relationship- graph visualization, drag-to-propose, debug inspector, hot-seat play." CB-WP-0011 shipped the debug inspector. This decides how the other three get drawn.
The obvious shape — cb-render-api / cb-render-null / cb-render-wgpu,
straight from ArchitectureBlueprint §Port pattern — did not survive review.
Two things went wrong with the survey that produced it, and both are
recorded because the decision below is only trustworthy given what was
wrong with the reasoning that first reached it.
Decision 1 — render by emitting HTML, SVG and JavaScript
cb-render-html emits a document; the browser draws it. SVG covers cards,
tokens and the relationship graph natively — the graph being the one
element every Rust 2D toolkit would have left us to hand-roll. Pointer
events cover drag-to-propose. One tab covers hot-seat.
Rejected alternatives, with measured marginal cost against the 23-crate shipped-runtime base:
| rejected | marginal lines | why not |
|---|---|---|
egui + eframe |
2,782,849 | 30× the corrected headroom |
wgpu + winit |
1,741,979 | stage 2's, and stage 2 should buy it |
softbuffer + tiny-skia + winit |
1,147,081 | windowing dominates the rasterizer |
ratatui + crossterm |
1,067,013 | costs more than a GPU game framework; rustix → linux-raw-sys at 479,901 |
macroquad |
480,501 | cheapest GPU-era stack, still 5.2× |
sdl2 |
191,973 + a C library | see Decision 3 |
fltk |
140,079 + a C library | the real floor. Affordable if the target moved — rejected on allocation, not cost |
tiny-skia alone |
83,956 | fits, but rasterizes without windowing or input |
The reason is allocation, not affordability. fltk is 1.5× the
corrected headroom — a negotiation, not an impossibility. But wgpu at
1,741,979 is named by INTENT for stage 2, is unavoidable, and is twelve
times the cost of the stage-1 toolkit it would replace. Spending 1.5× the
remaining budget on windowing that stage 2 discards is the wrong
allocation whether or not the budget can be stretched. This argument does
not depend on AM-4a's target being 250,000; it depends only on stage 2's
bill exceeding stage 1's benefit, which is measured and true by 12×.
The first draft of the survey argued affordability — "128×, two orders of magnitude, not a near miss." That was false, drawn from a candidate list containing no cheap windowed toolkits. It is recorded here because a decision whose stated reason was wrong once should carry the correction where the decision lives, not only where the survey does.
Decision 2 — do not declare cb-render-api or cb-render-null
INTENT: "No concept becomes canonical merely because it looks general. It becomes canonical after surviving a second concrete use."
A port designed against one implementation that emits whole documents
acquires a document's shape — retained mode, full state per update, string
identity, no frame timing — and stage 2 finds it unimplementable and
rewrites it. CommitWindow is already in this repo with a delete-by date
and two declined second-use opportunities, for exactly this error.
cb-render-html therefore ships against the existing Project trait,
which is a real interface with real implementations. The port is declared
at stage 2, derived from two implementations rather than imagined from one.
cb-render-null goes with it: a null implementation of an interface that
does not exist is a null implementation of nothing.
This overrides ArchitectureBlueprint §Port pattern for this capability, and only for its timing. The blueprint says every important capability ships null, reference and optimized implementations; it does not say when the interface is fixed. Where the two rules met, INTENT's second-use rule won, because it is the one INTENT states without exception.
Decision 3 — AM-4 counts what the project causes to be acquired
Scoring the browser at zero is true under AM-4a and suspect in substance.
If a target can be satisfied by relocating a dependency somewhere the
target does not look, it constrains nothing — and the same move was already
flattering sdl2 and fltk, whose .rs counts exclude the C libraries
that do the work.
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.
Consequences, applied consistently: a browser is not counted; sdl2 and
fltk are counted at more than their Rust bindings, since they oblige a
user to install a -dev package. The rule makes the target harder to
satisfy, not easier, which is the only direction a rule proposed by its
beneficiary should be trusted to run.
Decision 4 — AM-4a's proc-macro defect is a separate change
AM-4a counts --edges normal, which includes proc-macro crates that run in
the compiler and never reach a binary: syn (66,916), proc-macro2,
quote, unicode-ident, serde_derive — 89,048 lines, 36.2% of the
"shipped-runtime" figure. Real headroom is 92,798, not the 3,750 this
repo has cited in every pass that mentioned it, including CB-WP-0011's
reason for deferring this declaration.
The metric should count --edges normal,no-proc-macro. Filed as its own
change, not bundled here. A budget correction that arrives attached to the
request it unblocks is indistinguishable from motivated reasoning even when
it is right — and it is right, which is why it must arrive separately.
Decision 5 — six controls bind the implementation
| # | control | why |
|---|---|---|
| 1 | unguessable token minted per process, required on every request | a loopback listener is reachable by any process and any web page the user visits |
| 2 | Origin / Sec-Fetch-Site checked, rejected by default |
cross-origin POST to 127.0.0.1 is the attack, not a hypothetical |
| 3 | listener bound explicitly to 127.0.0.1 |
not 0.0.0.0 by default |
| 4 | a test that a token-less request is refused, plus an M-D1-MUT mutation removing the check and turning it red | 1–3 without 4 are three claims and no evidence |
| 5 | JavaScript may not construct commands — the page reports raw pointer facts (down on id, up on id); Rust decides what command they mean |
confines JS to input transport, making the decision testable in Rust against synthetic events |
| 6 | the coverage gate crosses the language boundary — the HTML counterpart of every_view_field_is_classified asserts over the parsed emitted document |
asserting over the Rust that emits the document reproduces CB-WP-0011's defect one layer up |
Controls 5 and 6 exist because CB-WP-0011 established that a renderer's
defect class is silent omission, and this decision moves the interactive
half of stage 1 into a language cargo test, clippy and M-D1-MUT
cannot reach. Without them, this ADR would spend a pass's finding one pass
after paying for it.
K13 binds unchanged. The renderer consumes a projection and can never feed back into validation; a drag that proposes a move goes through the same command path a CLI move takes.
Reserved for the maintainer — not decided here
- AM-4a is incompatible with INTENT stage 2.
wgpu+winitis 1,741,979 marginal lines against a 250,000 total target — 7× the whole budget, 19× the corrected headroom. No sequencing, feature-gating or metric correction closes that. Either the target moves, the render port sits outside the AM-4a configuration by an argued rule, or stage 2 changes. A pass that discovers a budget conflict and also settles it has reviewed nothing, so this pass does not settle it. - Whether Decision 3's acquisition rule is the right rule, given it is proposed by the pass that benefits from it. It is written to cost more than it saves, but that is an argument, not a ratification.
Consequences
- Stage 1 ships without a capability port. That is a deliberate deferral
with a named trigger (stage 2's
wgpuimplementation), not an omission. - The tier is now wrong and was not re-derived. Tier L's structural trigger was "creates a new capability port"; review removed the port. Re-rolling would make the tier a function of the outcome, which is the one thing a tier declaration must not be. The pass runs at L. This is the CHAOS calibration window's second entry, and unlike the first it comes from a non-override: full-weight review deleted its own trigger.
- A second language enters the build, gated only by control 6. If the emitted JavaScript grows beyond input transport, control 5 has failed and the decision should be revisited rather than the control widened.
cb-playacquires a third mode. CB-EV-0009 §5 flagged that a third mode is the second use at which its single-binary shape should be reconsidered; that reconsideration is now due.