174 lines
9.8 KiB
Markdown
174 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.
|