# Thing 2.0 build spec Status: proposal for review (PR), 2026-11-05. Written for Meta's ask t12-m0 (#cultlib, 2026-11-04) on top of `docs/thing2-target.md` (why, invariants) and `docs/thing2-map.md` (body facts P1-P22, model page, consumer contract, authority map, cut order). It does not restate them; it adds what the map leaves at deliverable level: **the pieces, each piece's contract, the build order, and the test that defends each piece**, with prior art for every design choice. Nothing here overrides a ruling. Where this spec makes a choice the rulings do not force, it is labelled **D** (decision, with confidence) and the ones it could not settle are in "Open" at the end, each with a recommendation. ## How to read the prior-art tags Each piece ends with **Prior art**. Tags: **[read]** = a source read in an earlier Eyes or Imagination pass, with its evidence pointer in `docs/research/*` (cited as R-F = `thing2-foundation-prior-art.md`, R-T = `thing2-text-input-a11y-prior-art.md`, R-K = `thing2-contract-forks-prior-art.md`, R-C = `thing2-consumer-census.md`); **[background]** = prior knowledge, not re-fetched for this spec, and the first thing to verify when its piece is mapped into a `cut_spec`. Confidence is stated per decision. Meta's rule, as passed to this run, is "prior-art sources cited"; if the rule has more to it (format, depth, a required venue) the Open list asks. ## 0. The machine in one page ``` provider ──publishes──▶ CultMesh state (typed .cc records) │ StatePort (subscribe / read) ▼ ┌───────────────────────── thing-core (one crate, internal modules) ─────────────────────────┐ │ resolve ─▶ bind ─▶ layout ─▶ text ─▶ render ─┐ │ │ (docs, (Bind, (Taffy) (Parley or ├─▶ FrameOutput { commands, a11y, outbound, │ │ compos.) Repeat) cells) │ ime, diagnostics } │ │ input (focus, edit, hit-test, scroll) ─▶ command dispatcher ─▶ outbound CommandInvocation │ │ a11y (AccessKit tree) │ └───────────────────────────────────────────────────────────────────────────────────────────┘ ▲ FrameInput { now, viewport, scale, events } │ commands + a11y │ ▼ host event loop ◀────── thin backend: TUI (crossterm) | web (wasm + JS) | later hosts ``` Core is a **function of (documents, state, input, time)**. The host owns the loop and calls `Core::frame`; core never spawns, sleeps, reads a clock or touches a socket except through a port. This is what makes every test below runnable without a daemon (`doctrine/gamecult.md`, "dependency injection is boring") and what makes goldens byte-stable (map rule 10). **D1. One crate for core, internal modules, not eight crates.** (Confidence medium-high.) Authority separation (resolve, bind, layout, text, render, command) is real, but the modules share one frame pipeline and one set of types; a crate per stage buys build fan-out and a public surface no consumer may use, which `narrow-consumer-contract` forbids. Splits that *do* earn a crate, each naming the invariant it protects: | Crate | Earns its boundary because | | --- | --- | | `thing-contract` (landed) | consumers depend on it alone (`narrow-consumer-contract`); three runtimes are held to its goldens | | `thing-core` | owns geometry, text, focus, binding, results; depends on no transport, no clock, no platform | | `thing-mesh` | the only crate that depends on CultMesh/CultNet: `StatePort`/discovery implementations. Keeps rule 7 and the mockable-port invariant checkable by a dependency-tree grep on `thing-core` | | `thing-tui` | backend; depends on `thing-core` output types and `crossterm`, never `taffy`/`parley` (rule 8) | | `thing-web` | wasm entry (`wasm-bindgen`) plus the JS host package; same dependency rule | | `thing-testkit` | fake ports, fixture loader, golden harness, shared by every crate's tests; never a dependency of a shipped artifact | Prior art: Xilem and egui split by renderer/backends and keep one core (R-K §1: `masonry_core`, `egui`+`epaint`, backends as crates) **[read]**; AccessKit and Parley are separate repos only where independently consumed or needing another toolchain (R-K §1) **[read]**; Flutter merged repos to end rolls (R-K §1) **[read]**. Same repo, many crates, is the dominant pattern. ## 1. Build order and dependency graph ``` 0 decisions ─▶ 1 contract-ts ┐ ├▶ 1 contract-cs ┴▶ 1x cross-store check └▶ 2 testkit+ports ─▶ 3 resolve ─▶ 4 bind ─▶ 5 layout ─▶ 6 render+goldens ─▶ 7 commands │ │ 8 TUI ◀────────┘ │ 9 text(Parley)/focus/edit ─▶ 10 a11y ─▶ 11 web ◀───┘ 12 thing-mesh (parallel from 4 on) 13 consumer ports (one per consumer) ─▶ 14 retirement ``` Rules for the order: (a) contract before core (map, rationale); (b) the pipeline is built **end to end thin first** (cut 8 draws a real surface in a terminal before text, IME or a11y exist), then deepened, because the first full run is where unknown unknowns show; (c) `thing-mesh` runs beside core from cut 4 because it is the other half of the `StatePort` contract and needs the same fixtures; (d) no consumer port starts before cut 7 and one backend (cut 8) work, so a port can be viewed. Cuts are sized to Hands' budget (about 150 calls, 0.5M tokens is the danger line): each row below is one `cut_spec`, or two where marked. Sizes are Self's estimates in thousand lines of owned code (excluding goldens), pressure not metric. | # | Cut | Waits on | Est. | Parallel with | | --- | --- | --- | --- | --- | | 1a/1b | contract-ts, contract-cs (specs `thing2:cut_spec:cut-contract-*.r2`) | contract-rs (landed `6ae2da0`) | mapped | each other | | 1x | cross-store check | 1a, 1b | 0.3 | 2 | | 2 | testkit + ports | contract-rs | 1.2 | 1a, 1b | | 3 | resolve | 2 | 1.8 | - | | 4 | bind | 3 | 1.5 | 12 | | 5 | layout | 4 | 1.8 | - | | 6 | render + goldens | 5 | 1.5 | - | | 7 | commands | 4 (needs 6 only for hit-test) | 1.6 | 8 | | 8 | TUI backend | 6, 7 | 1.5 | 7 tail | | 9a/9b | text: measure+shape, then focus+editing+IME state | 6 | 2 + 2 | 8 | | 10 | a11y tree | 9b | 1.0 | 11 start | | 11a/11b/11c | web: wasm+drawing, IME mirror + a11y mirror, shell+discovery | 6, 9b, 10 | 1.5+1.5+1.0 | - | | 12 | thing-mesh | 2, 4 | 1.5 | 5-8 | | 13.n | consumer ports | 7, 8 (web for browser consumers: 11c) | per consumer | one repo each | | 14 | retirement | last port | -1x | - | ## 2. Cross-cutting contracts ### 2.1 Core API (the one public surface core has) ``` Core::new(ports: Ports, config: Config) -> Core Core::frame(&mut self, input: FrameInput) -> FrameOutput FrameInput { now: Instant, viewport: Size, scale: f32, events: Vec, metrics: MetricsMode } FrameOutput { commands: Vec, a11y: Option, outbound: Vec, ime: Option, diagnostics: Vec, next_wake: Option } Ports { state: dyn StatePort, fonts: dyn FontPort, clock: dyn Clock, ids: dyn IdSource, host: dyn HostEffects } ``` - **One owner for time and ids.** `now` arrives in the frame input; `IdSource` mints invocation ids. Tests substitute both, so a run is a pure function and invocation ids in goldens are reproducible. - **Outbound is invocations only** (rule 7). `Ports.state` has `read`, `subscribe`, `list(schema, key_prefix)`; it has no `write`. The one write path is `FrameOutput.outbound`, which the host or `thing-mesh` forwards. - **`HostEffects`** has exactly the closed `effect` set of the contract: `open_surface(key)`, `open_external(url)`; plus `clipboard` read/write when the text piece needs it (see Open O7). - Metrics mode (`Pixels` | `Cells`) is a core input, because the TUI measures in cells inside the Taffy measure function (R-T §5: iocraft does exactly this **[read]**) and rule 8 forbids the backend from owning any layout input. The cell-metrics provider is part of core (small: `unicode-width` and `unicode-segmentation`), not of `thing-tui`. Prior art: Clay's per-frame contract (set dimensions → pointer state → begin layout → end layout returns the command array) is the model for `frame` **[read, R-F §2.1]**; egui and Bevy UI emit shapes or commands to thin backends **[read, R-F §2.4, R-T §1]**. Elm/Redux style "update is a pure function of state and event" **[background]**. Confidence high: no contrary evidence found. ### 2.2 The closed render-command list (D2) `FillRect{rect, radius, color}`, `StrokeRect{rect, radius, width, color}`, `Text{origin, run, color}` where `run` is `Glyphs{font, size, glyphs}` (Pixels mode) or `Cells{col, row, text, style}` (Cells mode), `Image{rect, catalog, asset, fit}`, `ClipPush{rect, radius}`, `ClipPop`, `Polyline{points, width, color}`, `Polygon{points, color}` (plot marks), `Embed{rect, slot_kind, slot_id}`. Nine variants. Selection highlights, carets, focus rings and scrollbars are `FillRect`/`StrokeRect` that core emits. Z order is emission order; there is no z-index, no layers, no custom escape. A new command is a reviewed change to core plus every backend and the goldens at once (that is the point of closing it). Colours in commands are resolved literals; theme tokens never cross to a backend. Coordinates are `f32` logical pixels in Pixels mode, integer cells in Cells mode. Prior art: Clay's complete enum is `NONE, RECTANGLE, BORDER, TEXT, IMAGE, SCISSOR_START, SCISSOR_END, OVERLAY_COLOR_START/END, CUSTOM`, with renderers for SDL, raylib, canvas2d, termbox2 and a terminal, all thin **[read, R-F §2.1]**. Thing's list differs on purpose: no `CUSTOM` (Embed is the closed host slot), no overlay-colour pair (an overlay scrim is a `FillRect`), plus polylines for `Plot`. Confidence medium: the count is a proposal; whether `Plot` should lower to `Polyline`/`Polygon` or be left to an `Embed` is O3. ### 2.3 Diagnostics (D3) A closed `Diagnostic { code, node, document, detail }`: `UnresolvedUse`, `BindSchemaMismatch`, `BindMemberMissing`, `UnknownNode`, `UndeclaredCommand`, `DuplicateNodeId`, `StaleVersionIgnored`, `MalformedResult`, `UnknownInvocation`, `InvocationTimedOut`, `DecodeRefused`. The same value drives (a) the placeholder or error node drawn in the frame, (b) a log line, (c) the a11y tree (an error node is announced), and (d) the tests. Rules 2, 4, 5 and 6 are asserted on these codes, so a rule that is deleted fails by name, not by a screenshot. Confidence high; the alternative (free-text logs) is what Thing 1's gap ledgers were. ### 2.4 Test layers and golden format (D4) Doctrine splits tests into local-authority unit tests, adjacent-organ pipeline smokes, and end-to-end Verse contract runs; every piece below names which layer. - **Fixtures** (`tests/fixtures/*.surface.cc`, committed): typed documents encoded through `thing-contract`, one per row of the fixture table (§2.5). Built by a `thing-testkit` builder in Rust, then frozen; the bytes are what is loaded. - **Geometry golden**: for a fixture + viewport + state script, the resolved node tree with `(node id, x, y, w, h)` in a stable text form. - **Render golden**: the `Vec` encoded with CultCache MessagePack through the same derive the contract uses, plus a line-oriented text rendering beside it for diffs. The binary form is the oracle (byte-identical, rule 10); the text form is only for review. - **Regeneration** is `cargo xtask bless`, runs only on a reviewed core change, and a backend crate cannot call it (model page: goldens are the core's). - **Layout correctness is inherited, tree building is tested.** Taffy is itself validated against Chrome with generated fixtures (`just gentest`, R-F §2.2 **[read]**), as is Yoga (`gentest/`, R-F §2.3 **[read]**); Thing does not re-prove flexbox. Thing's tests prove that *our* style mapping puts the right inputs into Taffy and that the output flows into commands unchanged. One extra check re-derives a small fixture's geometry from the raw Taffy API directly and compares (guards against our mapping drifting while goldens are re-blessed). - **Mutation**: `tools/cargo-mutants.sh` once per Hands pass, scoped to the cut's diff, per the Eureka rule. Each piece's "Test" below names the rules whose mutants must die; a survivor is triaged, not hidden. - **Property tests** (`proptest` [background]) only where a total function has a clear law: contract round-trip (exists), `resolve` never panics and either returns a tree whose ids are unique or a refusal, `bind` is idempotent for the same state, `frame` with no events and unchanged state returns an equal command list (stability). Fuzzing the decoder (`cargo-fuzz` [background]) is deferred until `decode-stack-safety` lands (the decode hazard is CultLib's, map P20). Prior art: golden command lists are not published by any project read (R-F §2.5: "not found, not searched exhaustively"), so this is the least-supported decision in the spec; the nearest supported techniques are Taffy/Yoga generated geometry tests and egui's `egui_kittest` snapshot crate (R-K §1 lists the crate; its snapshot method is **[background]**). Confidence medium: the idea is sound because commands are renderer-independent by construction; cost of churn on intentional change is the risk, mitigated by `bless` being one command. ### 2.5 Fixture set (the census sizes the floor) Twelve fixtures cover the node set and every rule; each is named for the consumer shape it models (R-C §7 cross-table is the source, and Thing is a general library, so a fixture exists for a capability a reasonable consumer would expect even with no caller today): | Fixture | Exercises | Rules | | --- | --- | --- | | `f01-swarm-dashboard` | VoidBot-shaped: Box grid, bound Text/Format, `Repeat` over a collection prefix, one Button | 6, 9 | | `f02-login-effect` | Ghostlight/Heimdall: TextInput, Button, `OpenExternal{resume}` result | 1, 2 | | `f03-input-target` | Muninn: `position: absolute` target over an `Image`, bound label (P6) | - | | `f04-form` | Toggle, Slider, Select, TextInput number, `captures` and `clear_drafts` | 1 | | `f05-modal-overlay` | `Overlay` modal with dismiss command, focus trap | - | | `f06-scroll-list` | `overflow: scroll`, `Repeat` 1k items, keyed insert/remove | - | | `f07-plot` | Stonks/StreamPixels-shaped `Plot` series | - | | `f08-embed-viewport` | `Embed` slot as a leaf of given size | - | | `f09-composition-stack` | nested `Use` with typed args and child slots, `thing.std` plus a publisher composition | 5 | | `f10-hostile` | unknown arm, undeclared `CommandRef`, duplicate id after expansion, unresolved `Use`, schema-mismatched `Bind` | 4, 5, 6 | | `f11-deep` | nesting at the 256 bound (decoded on a large-stack thread until CultLib's fix) | - | | `f12-text-edge` | bidi, CJK wide characters, combining marks, emoji ZWJ, long wrap | - | ## 3. The pieces Each piece lists **Owner** (one writer), **In**, **Out**, **Ports**, **Invariants** (what must die under mutation), **Failure**, **Test** (layer and the rule each pins), **Deletes/forbidden** (what the piece must not contain), and **Prior art**. Map rules are cited as M1-M10 (the ten in the map's "Rules that must die under their own mutation"). ### Piece 1: contract (packages for TS and C#, and the store check) Rust (`crates/thing-contract`) landed after two Soul passes; `contract-ts` and `contract-cs` are mapped (`thing2:cut_spec:cut-contract-ts.r2`, `-cs.r2`) and have branches (`hands/thing2-contract-ts`, `hands/thing2-contract-cs`) not yet merged. This spec adds no contract change. - **Owner:** the contract crate; packages own nothing (map authority). - **Test (per package):** the shared golden fixtures decode and re-encode byte-identical (M1, M3); the refusal goldens (`seq` 0, extra slot, unknown union arm) are refused; a hand-built object with seq 0 is refused at encode. - **1x cross-store check:** the C# package opens a `.cc` store the TS package wrote and the reverse, and both open Rust's. The check lives in `thing-testkit` as a scripted cross-runtime job run on Yggdrasil's verify stopgap (`tools/stopgap/ygg-verify.sh`), because only that image has all three runtimes. It proves the catalog type names agree (map cut 1 waiting line). - **Prior art:** CultCache wire parity against the C# reference is the standing CultLib rule; probe P12-P13 are the evidence **[read, map]**; golden-file interoperability is how Protobuf and Cap'n Proto test language bindings (`conformance` suites) **[background]**. Confidence high. ### Piece 2: testkit and ports - **Owner:** `thing-testkit` owns fakes; `thing-core` owns the port *traits* (a trait is the narrow port; the fake is a test crate's business). - **In/Out:** `StatePort { read(schema,key)->Option, list(schema,prefix), subscribe(schema,key|prefix) -> Stream }` where `Change` is `Upsert(entry)|Delete(key)`; `FontPort { resolve(family, weight, style) -> FontHandle, bytes(handle) }`; `Clock`; `IdSource`; `HostEffects`. All object-safe. - **Fakes:** `MemState` (a `CultCache`-backed map with a scripted change feed and a record of every call), `FixedFonts` (one bundled test font, so Parley output is deterministic across machines), `ManualClock`, `SeqIds`. - **Invariants:** `StatePort` has no write method (M7, checked by the trait's method list in a test that enumerates it and by a grep over `thing-core`); `MemState` returns entries only as decoded contract records, never `Value`. - **Failure:** a decode refusal from the port surfaces as a `Change::Refused` carrying the code, never as a missing record (a refusal must not look like absence). - **Test:** unit. `MemState` change ordering; fixture loader round-trips all twelve fixtures through the contract (a fixture that fails the contract is a bug in the fixture before it is a bug in core). - **Deletes/forbidden:** no global state, no `Instant::now()` in core (negative grep). - **Prior art:** the port style is the project's own doctrine; Xilem's `masonry_testing` and egui's `egui_kittest` are test-harness crates beside a core (R-K §1) **[read]**. Confidence high. ### Piece 3: resolve - **Owner:** `thing-core::resolve`. It is the only code that interprets Thing Script structure. - **In:** `SurfaceDocument`, a `CompositionStore` (std compiled in, plus publisher compositions read through `StatePort`). **Out:** `ResolvedTree` (arena of nodes, unique ids, every `Use` expanded or replaced by a placeholder node, every `CommandRef` checked against the document's `CommandDecl`s, bindings still unresolved), plus diagnostics. - **Invariants:** M4 (unknown kind, undeclared `CommandRef`, duplicate id after expansion refuse the document *whole*, and the old document stays displayed); M5 (unresolved `Use` is a visible placeholder with a diagnostic); expansion is substitution only (no conditionals, no arithmetic: a test that every `thing.std` composition expands under a parameter-less evaluator); expansion terminates (recursion in compositions is refused with a cycle diagnostic and a depth bound); expansion is deterministic and order-preserving; ids of expanded nodes are derived from the `Use` node id and the template id by one scheme, so they are stable across republishes (focus and scroll state key on them). - **Failure:** a referenced composition arrives later → the placeholder is replaced next frame without a republish. - **Test:** unit per rule on `f09`, `f10`; property (`resolve` total, ids unique or refusal); pipeline smoke: `resolve` output fed to `layout` (cut 5) in `f01`. Mutants that must die: skip the duplicate-id check; treat an undeclared `CommandRef` as a warning; drop the placeholder; expand `Use` without pinning the composition version. - **Deletes/forbidden:** no `kind` string dispatch anywhere (closed enum only); no fallback along a kind path (M4); no expression evaluator. No text-syntax parser outside `thing-core` (`thing-script-form`): `resolve` consumes only the typed document; a later `.thingscript` parser is core's and feeds it. - **Prior art:** Adaptive Cards separates template expansion from render (`adaptivecards-templating` expands a template against data before render) **[read, R-K §2]**; Thing's `Use` is the same split without an expression language, which R-K §2/P5 shows the failure of (the DSL's renderer-local `derive`) **[read]**. Slint compiles a text language to a typed tree at build/run time **[read, R-K §2]**. Confidence high on substitution-only; medium on id-derivation (no source read; it is the same problem React solves with keys **[background]**). ### Piece 4: bind - **Owner:** `thing-core::bind`. Only code that reads typed state. - **In:** `ResolvedTree`, `StatePort`. **Out:** `BoundTree` with every `Bind` replaced by a typed value or a binding-error value; `Repeat` expanded to one subtree per item, keyed by the item key; subscriptions established and released as bindings appear and disappear. - **Invariants:** M6 (different schema or missing member renders a typed error, never a coerced value); M9 (a version at or below the held one is ignored, and the ignore is a `StaleVersionIgnored` diagnostic at most once per document version, not per frame); a value change never triggers layout of anything but the nodes whose bound values changed (dirty tracking is observable through a counter the test reads); `Format` is total and locale-free in core (formats are the closed set; any locale is a host input, see O8); `Repeat` item keys must be unique, a duplicate key refuses the document at bind time as M4 does; keyed reorder preserves node identity, so focus and scroll survive a reorder. - **Subscription rule:** core subscribes to exactly the `(schema,key)` pairs and prefixes the live tree references; a document replacement diffs its subscription set. An unsubscribed record's arrival changes nothing. - **Failure:** record absent → a typed `Absent` value rendered as the node's declared empty text (the contract field), not an error; schema mismatch → error. - **Test:** unit per rule on `f01`, `f06`, `f10` with scripted `MemState` feeds; pipeline smoke `bind → layout` checks dirty-node count; mutants: coerce a mismatched schema; drop the version gate; resubscribe every frame; reorder by index instead of key. - **Deletes/forbidden:** no local `var`/`derive`/`collection` store (P5); no draft binding (BD2: editing state is `input`'s). - **Prior art:** BD1-BD9 collapse in the map **[read, map]**; keyed list reconciliation is React's `key` and Elm's `Html.Keyed` **[background]**; Clay retains element state by ID and requires `CLAY_IDI` for loops (R-F §2.1) **[read]**, the same constraint. Flutter's retained tree keeps state by key **[background]**. Confidence high. ### Piece 5: layout - **Owner:** `thing-core::layout`. The only crate-user of `taffy`. - **In:** `BoundTree`, viewport, metrics mode, `FontPort`. **Out:** `LaidTree`: per node a content box, scroll extents, clip, and absolute-positioned overlay boxes. - **Contract:** the mapping from contract `BoxStyle` to Taffy style is one function with an exhaustive match (a new contract field fails to compile until it is mapped). Leaf measure for `Text`, `TextInput`, `Image`, `Embed`, `Plot` goes through one `Measure` trait: Pixels → Parley (piece 9), Cells → cell metrics, `Image`/`Embed`/`Plot` → declared sizes. Overlay nodes are laid out as a separate root attached to an anchor node's final rectangle or the viewport. Scroll containers expose `content size` and clamp offsets that `input` owns. - **Invariants:** layout is a pure function of (tree, viewport, scale, metrics, fonts): same inputs, byte-identical geometry (M10); a style field with a literal and a token resolves via the one theme resolver before reaching Taffy; `fr`, `%`, `auto` and `content` sizes reach Taffy as the matching Taffy dimensions with no rewriting; an `Embed` is a leaf of exactly its declared size. - **Failure:** NaN or negative sizes from a document are refused at resolve (the contract already refuses at decode for `Count`); layout never panics on a constraint it cannot satisfy and instead overflows per CSS rules. - **Test:** unit geometry goldens on `f01`, `f03`, `f05`, `f06`, `f08`; the raw-Taffy rederivation on one small tree (§2.4); property: layout is deterministic across repeats and scale 1 vs 2 scales positions linearly when fonts are scale-independent (fixed font); Cells mode: `f12` widths equal `unicode-width` expectations. Mutants: swap row/column mapping; drop `gap`; round positions; ignore `position: absolute` insets. - **Deletes/forbidden:** `taffy` appears in no other crate's tree (M8, `cargo tree -i taffy` negative check on `thing-tui`, `thing-web`, `thing-mesh`, contract). - **Prior art:** Taffy implements Block, Flexbox and Grid, is used by Servo, Blitz, Bevy, Slint, iocraft, Zed/GPUI and is validated against Chrome (R-F §2.2) **[read]**; measure-function leaf text is the documented integration point (R-F §2.2, R-T §1) **[read]**; iocraft's measure for Cells (R-T §5) **[read]**. Ruling `thing:ruling:thing2-one-solver-taffy`. Pin: Taffy 0.14.0 (2026-08-24 [read]); Slint pins 0.10 flexbox-only and iocraft 0.5.2 (R-T §1, §5), so the Grid feature surface at 0.14 is the one to prove first (confidence medium). ### Piece 6: render and goldens - **Owner:** `thing-core::render`. Turns `LaidTree` + paint + input state into the command list; draws selection, caret, focus ring, scrollbars. - **Invariants:** M10 (byte-identical command list for equal inputs); painter order is document order, then overlays last; `ClipPush`/`ClipPop` balance (a validator over every golden); every `Embed` and `Image` node produces exactly one command or a visible missing-asset placeholder (diagnostic `AssetMissing` added to D3's list when this cut lands, O9); nothing outside the clip stack is drawn for `overflow: clip|scroll` nodes. - **Backends must prove consumption (M10 second half):** each backend's parity test replays a golden list and asserts one draw call per command; a backend that skips a command variant fails by name. - **Test:** golden suite described in §2.4 over all fixtures; pipeline smoke `resolve→bind→layout→render` on `f01`; mutants: drop `ClipPop`, reorder overlay, emit caret when unfocused, emit a command with a theme token not a literal. - **Prior art:** Clay's sorted command array and scissor start/end (R-F §2.1) **[read]**; egui/epaint clipped shapes to thin backends (R-F §2.4) **[read, background]**. Confidence high on structure, medium on exact variants (O3). ### Piece 7: commands (dispatcher and result validation) - **Owner:** `thing-core::command`. Mints invocations, tracks them, validates results. The only producer of `FrameOutput.outbound`. - **Lifecycle (D5):** `Issued → Pending* → Completed | Denied | Failed`, with two core-only terminal states, `Malformed` (result failed to decode, or named an unknown invocation) and `TimedOut`. Terminal states are final: a later result for a terminal invocation is a diagnostic, not a state change. - **In:** input events that activate a `CommandRef` node; results arriving as `StatePort` changes of schema `gamecult.thing.command_result.v1`. **Out:** `CommandInvocation` (id from `IdSource`, `captured` from `input`'s editing state for the nodes `CommandDecl.captures` lists, `surface` = key + held version, `issued_at` from the clock; **no caller identity field written**, the transport sets it), UI state for the issuing node (busy, denied-at-node), effects through `HostEffects`. - **Invariants:** M2 (a result that fails to decode, or an unknown `invocation_id`, becomes a visible `Malformed` state for that invocation and a log line; no invocation stays pending past its timeout without one); M7 (core writes nothing but invocations); idempotency: one user action mints exactly one id, and a re-delivered invocation carries the same id; `Completed{clear_drafts:true}` clears exactly the captured nodes' drafts and no others; `OpenExternal{resume}` is executed once per result `seq`; a `Denied{node}` marks that node and no other; a double-click on a busy Button mints no second invocation while the first is non-terminal (the Button node's enabled state is derived from lifecycle, not a flag a document sets). - **Timeout:** a core config value, default 15 s in `Config` (O5: it is a guess). - **Failure:** `StatePort` refusal on a result (piece 2's `Change::Refused`) maps to `Malformed`, which is the case P2 shows Thing 1 lacked (result rejected whole by `ajv` with nothing visible). - **Test:** unit lifecycle table (every transition, every illegal one), driven with `ManualClock` for timeouts; pipeline smoke with `MemState`: press a Button in `f02`, assert exactly one outbound invocation with the right captures, feed `Pending` then `Completed{OpenExternal}`, assert one host effect; negative: result with extra field (M1) never reaches the lifecycle. Mutants: no timeout; mint id per frame; `clear_drafts` clears all; accept results after terminal; write caller identity. - **Prior art:** idempotency keys for commands and retries (Stripe API idempotency, RFC draft "Idempotency-Key" header) **[background]**; command/result split with a pending phase (job/operation resources in Google AIP-151 long-running operations) **[background]**; Flutter's `TextInputConnection` shows what drafts the host must hold (R-T §4) **[read]**. Evidence of the failure it prevents: P2, P7 **[read, map]**. Confidence medium-high; the `Malformed` rule follows directly from P2. ### Piece 8: TUI backend (`thing-tui`) - **Owner:** `thing-tui`: terminal drawing, keyboard and mouse into `InputEvent`. First end-to-end consumer of core; also the agents' reader. - **In:** `FrameOutput.commands` in Cells mode. **Out:** terminal bytes via `crossterm`; `InputEvent`s. - **Invariants:** M8 (no `taffy`/`parley` in the tree); every `RenderCommand` variant is handled and each is either drawn or explicitly declared not-representable in cells (`Image` → block placeholder with alt text, `Polyline` → braille or block approximation): "not representable" is a table in the crate, tested; wide and zero-width characters advance the cursor as the `Cells` run says (the backend never re-measures); the screen diff writes only changed cells (testable on a `Vec` buffer without a terminal). - **Test:** unit on a virtual screen (`ratatui`-style test buffer is **[background]**; ours is a `Vec`): every golden command list renders to a stored screen dump; pipeline smoke: `f01` through the whole of cuts 3-7 to a screen dump; parity (M10): every command variant produces a cell effect. Mutants: skip `ClipPop`; mis-advance on a wide char; draw a command outside the clip; ignore `Text` colour. - **Forbidden:** layout of any kind. - **Prior art:** Clay has `termbox2` and terminal renderers as thin command consumers (R-F §2.1) **[read]**; iocraft is a Taffy-based TUI (R-T §5) **[read]**; ratatui uses Cassowary not Taffy and pins `unicode-width >= 0.2.0` (R-T §5) **[read]**, a version issue to carry into our own pin (confidence medium: width tables differ between 0.1.x and 0.2.x, so the cell widths in goldens must be pinned to a crate version, D6). ### Piece 9: text (9a measure+shape, 9b focus+editing+IME state) - **Owner:** `thing-core::text` (ruling `thing2-thing-owns-text`). - **9a In/Out:** string + `TextStyle` + width constraint → `TextLayout` with line boxes and glyph runs (Pixels, via Parley) or cell lines (Cells). One `Measure` implementation per metrics mode behind the layout `Measure` trait. - **9b In/Out:** `InputEvent`s targeted by hit-test/focus → editing state per `TextInput` node (value, selection, composition range), caret rectangle, `ImeRequest { rect, text_window, selection }` (the data a hidden host element needs), and edits fed to `command` as captured values. - **Focus (D7):** one focus owner in core. Tab order is document order of focusable nodes, overlays trap focus while modal, focus is keyed by node id and survives republish when the id survives, and a focused node removed moves focus to its previous sibling then parent. A backend never owns focus; it forwards host focus loss/gain only. - **Invariants (9a):** shaping and bidi never run in a backend (M8); a run's glyph positions are a pure function of (text, style, width, font bytes); wrap and `max_lines` are applied by core and appear in the command list. - **Invariants (9b):** the draft is core's state and dies with the session (model page row 5); composition text is displayed but not committed or captured until the host commits; an IME update never rewrites committed text; every selection edit is on grapheme-cluster boundaries (`unicode-segmentation`); `ImeRequest` windows the text rather than mirroring a whole large value, and coalesces to one per frame (R-T §2: GPUI's mirror coalesces writes because each write restarts the IME connection **[read]**). - **Test:** 9a: unit on `f12` (bidi order, CJK width, combining marks, ZWJ emoji clusters, wrap at several widths) with the bundled font; golden glyph runs. 9b: scripted event sequences (type, move, select, delete across a cluster, compose-update-commit, compose-cancel, undo if D8 includes it) assert value/selection/`ImeRequest` at each step; focus: traversal order on `f04`, trap on `f05`, removal on a republish. Mutants: byte-index deletion in a cluster; commit composition on update; focus not keyed by id; mirror whole value. - **Prior art:** Parley: rich-text layout with HarfRust shaping, ICU4X bidi and segmentation, `PlainEditor`, IME primitives, an AccessKit editor example (R-T §1) **[read]**; Bevy (taffy 0.14 + parley 0.11), Slint (`shared-parley`), Blitz (Stylo + Taffy + Parley, text input state in the DOM node), Masonry (R-T §1-2) **[read]**: four independent Taffy+Parley integrations. Cosmic-text is the alternative; Parley wins on the integrations above and AccessKit ties, not on measured quality (R-T §1; no benchmark read). IME-restart and mirror behaviour from GPUI web, egui `text_agent`, Flutter web `text_editing.dart` (R-T §2) **[read]**. Confidence medium: all of this is an assembly of read-but-not-built parts; the biggest risk is Parley under `wasm32` and `no_std`-adjacent builds (not established; O1). Slug/Vello GPU glyphs (R-T §6) is the operator's scope addition and is **out of this build order**: glyphs reach the backend as positioned runs, and how the web backend rasterizes them (canvas 2D first, GPU later) is O2. ### Piece 10: a11y tree - **Owner:** `thing-core::a11y`. Derives an AccessKit `TreeUpdate` per frame from the resolved tree, roles, labels, focus, text editing state and layout. - **Invariants:** every focusable node has a role and an accessible name (a document that offers none gets the diagnostic `NoAccessibleName`, O9); roles come from the contract's closed `Role` set mapped by one exhaustive match; bounds equal layout bounds; updates are incremental (unchanged nodes omitted), keyed by node id; an `Embed` is exposed as a generic node with its label; text input exposes value, selection and composition as AccessKit text-run data. - **Test:** unit: tree for `f01`, `f04`, `f05` compared to stored AccessKit dumps; incremental update test (change one bound value → exactly the changed node plus ancestors needed); mutants: drop `focus`; stale bounds after scroll; role map swap. An actual screen reader cannot be run in CI; it is an operator check at web cutover (O6). - **Prior art:** AccessKit as the schema, with Bevy, Blitz, Masonry, Slint, GPUI and egui integrating it (R-T §3) **[read]**; Blitz's `build_accessibility_tree` tests (`accessibility_roles.rs`) are the closest test style **[read]**. Web adapter: **planned only** in AccessKit (R-F §2.6, R-T §3), so the web backend mirrors the tree to DOM itself (piece 11b); Flutter web is the shipped precedent, and its open issues (semantics DOM stealing taps, R-T §3) are the failure catalogue for 11b. Confidence high on native, medium on web. ### Piece 11: web backend and shell (`thing-web`) - **Owner:** `thing-web` (wasm entry + a small JS host package). Replaces the Nightwing browser reference (`nightwing-eve-browser-reference`, P8). - **11a draw:** core compiled to wasm, `frame` called from `requestAnimationFrame` with batched events; commands drawn to one canvas 2D context (D9: canvas 2D first, because GPUI web already rasterizes through OffscreenCanvas 2D, R-T §2, and the contract is already glyph-run based so a GPU path swaps in under the same commands). The JS host holds **no layout, no text shaping, no focus state**. - **11b IME + a11y:** a hidden `