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>
173 lines
9.8 KiB
Markdown
173 lines
9.8 KiB
Markdown
# 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](../research/CB-RES-0006-render-port.md),
|
||
`history/260802-render-port-{research,challenge,response}.md`,
|
||
[INTENT.md](../INTENT.md) §Implementation order,
|
||
[ArchitectureBlueprint.md](../specs/ArchitectureBlueprint.md) §Port pattern,
|
||
[ADR-0004](ADR-0004-am4-ratification.md) (AM-4 budgets),
|
||
[CB-EV-0009](../evidence/CB-EV-0009-inspectable-table.md) (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
|
||
|
||
1. **AM-4a is incompatible with INTENT stage 2.** `wgpu` + `winit` is
|
||
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.
|
||
2. **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 `wgpu` implementation), 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-play` acquires 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.
|