CB-WP-0012-T03: ADR-0007 — render to HTML, and do not declare the port yet

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>
This commit is contained in:
tegwick 2026-08-02 04:13:35 +02:00
parent c5fa610e59
commit 883b608f36
2 changed files with 185 additions and 1 deletions

View file

@ -0,0 +1,173 @@
# 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 | 13 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.

View file

@ -161,7 +161,7 @@ outcome. Recorded as the CHAOS window's second entry.
```task
id: CB-WP-0012-T03
status: todo
status: done
priority: high
```
@ -169,6 +169,17 @@ Record the decision, the rejected alternatives with their measured costs,
and the controls the implementation must carry. No implementation code
lands before this commits.
**Done 2026-08-02.**
[ADR-0007](../decisions/ADR-0007-render-html-not-a-port.md) — *render to
HTML, and do not declare the port yet.* Five decisions, eight rejected
alternatives with measured costs, six binding controls, and **two items
reserved for the maintainer**: AM-4a's 7× incompatibility with stage 2, and
whether the acquisition rule proposed by the pass that benefits from it is
the right rule.
The hard gate held: no render implementation code existed before this
commit.
## Task: the port and its first implementation
```task