diff --git a/decisions/ADR-0007-render-html-not-a-port.md b/decisions/ADR-0007-render-html-not-a-port.md new file mode 100644 index 0000000..264d44b --- /dev/null +++ b/decisions/ADR-0007-render-html-not-a-port.md @@ -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. diff --git a/workplans/CB-WP-0012-render-port.md b/workplans/CB-WP-0012-render-port.md index c6fbe46..8708ab7 100644 --- a/workplans/CB-WP-0012-render-port.md +++ b/workplans/CB-WP-0012-render-port.md @@ -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