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:
parent
c5fa610e59
commit
883b608f36
2 changed files with 185 additions and 1 deletions
173
decisions/ADR-0007-render-html-not-a-port.md
Normal file
173
decisions/ADR-0007-render-html-not-a-port.md
Normal 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 | 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.
|
||||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue