Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 159ad486e5 | |||
|
|
b6e06cf89a | ||
|
|
366774dc4c | ||
|
|
9eb099be4d | ||
|
|
3136ee8b80 | ||
| fd7a48484d | |||
| 4a6c04a073 | |||
|
|
ee8eec41f3 | ||
|
|
ebafda432f | ||
|
|
e95f1ab5ba |
3 changed files with 835 additions and 1 deletions
5
.github/workflows/portable-contracts.yml
vendored
5
.github/workflows/portable-contracts.yml
vendored
|
|
@ -8,6 +8,11 @@ on:
|
||||||
pull_request:
|
pull_request:
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
# A newer push or PR update replaces the older run on the same ref; workflow_dispatch runs are never cancelled.
|
||||||
|
group: ${{ github.workflow }}-${{ github.ref }}
|
||||||
|
cancel-in-progress: ${{ github.event_name != 'workflow_dispatch' }}
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
contracts-and-browser:
|
contracts-and-browser:
|
||||||
name: ${{ matrix.os }} ${{ matrix.package }}
|
name: ${{ matrix.os }} ${{ matrix.package }}
|
||||||
|
|
|
||||||
825
docs/thing2-build-spec.md
Normal file
825
docs/thing2-build-spec.md
Normal file
|
|
@ -0,0 +1,825 @@
|
||||||
|
# Thing 2.0 build spec
|
||||||
|
|
||||||
|
Status: build spec, refreshed 2026-11-10 against the thing2 rulings in force, 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<n>** (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. The standard is "prior-art sources cited"; O12 asks whether it
|
||||||
|
also fixes a format.
|
||||||
|
|
||||||
|
## 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-ts.r3`, `-cs.r3`) | 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<InputEvent>, metrics: MetricsMode }
|
||||||
|
FrameOutput { commands: Vec<RenderCommand>, a11y: Option<TreeUpdate>, outbound: Vec<CommandInvocation>,
|
||||||
|
ime: Option<ImeRequest>, diagnostics: Vec<Diagnostic>, next_wake: Option<Instant> }
|
||||||
|
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<RenderCommand>` 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]) waits only on Eve taking the
|
||||||
|
CultLib pin that carries `decode-stack-safety` (Fixed, CultLib `bbb98eaf`,
|
||||||
|
`cultlib-gaps:resolution:follow_up.decode-stack-safety.n1`; 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 Eve's pin of CultLib `bbb98eaf` merges) | - |
|
||||||
|
| `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.r3`, `-cs.r3`; r2
|
||||||
|
is superseded) and have
|
||||||
|
branches (`hands/thing2-contract-ts`, `hands/thing2-contract-cs`) not yet merged.
|
||||||
|
This spec adds no contract change. Rulings that bind the packages:
|
||||||
|
|
||||||
|
- **Unknown union arms** are refused by CultCache in every runtime, the C#
|
||||||
|
reference included, and Thing packages carry no check of their own
|
||||||
|
(`thing2:ruling:contract-decode-tolerance`; the fix is CultLib's, follow-up
|
||||||
|
`thing2:follow_up:cultcache-union-parity`, resolved in the mind). The "unknown union arm is refused"
|
||||||
|
test below therefore exercises CultCache, not package code.
|
||||||
|
- **Nil in required slots** is refused by each package: constructors and factories
|
||||||
|
reject null, decoding nil into a slot the Rust contract does not make optional
|
||||||
|
is a typed refusal (`thing2:ruling:contract-refuses-nil-in-required-slots`,
|
||||||
|
Defaulted). In TS this is a `present(codec)` on every non-Option slot, and
|
||||||
|
results are validated and frozen at one factory (`cut-contract-ts.r3`).
|
||||||
|
- **Byte payloads** (`RecordPayload.Bytes`) are a copied read-only view in C#
|
||||||
|
(`ReadOnlyMemory<byte>`); Rust's `Vec<u8>` and the wire are unchanged
|
||||||
|
(`thing2:ruling:payload-bytes-read-only`, Defaulted). The catalog type name for
|
||||||
|
that slot changes with it, which the 1x store check must read from the C# set.
|
||||||
|
|
||||||
|
- **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<Entry>, list(schema,prefix),
|
||||||
|
subscribe(schema,key|prefix) -> Stream<Change> }` 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<Cell>` buffer without a terminal).
|
||||||
|
- **Test:** unit on a virtual screen (`ratatui`-style test buffer is
|
||||||
|
**[background]**; ours is a `Vec<Cell>`): 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` (`thing:ruling:thing2-thing-owns-text`, operator:
|
||||||
|
core owns shaping (Parley), editing and IME state, focus and the AccessKit tree;
|
||||||
|
backends only draw and bridge to platform protocols; glyph rasterization stays
|
||||||
|
backend-private and the render-command contract carries shaped glyph runs).
|
||||||
|
- **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 `<textarea>` placed from `ImeRequest` (the pattern
|
||||||
|
all three of egui, GPUI and Flutter web converge on, R-T §2), mirrored text
|
||||||
|
windowed and coalesced; a DOM accessibility mirror of the AccessKit update,
|
||||||
|
clipped so it cannot intercept pointer events (the exact Flutter failures in R-T
|
||||||
|
§3: #194011, #192466), off by default until an assistive-technology signal or an
|
||||||
|
explicit toggle, as Flutter does.
|
||||||
|
- **11c shell + discovery:** a web app that discovers surfaces through the Verse's
|
||||||
|
discovery owner (map: discovery is not Thing's, `advertisement-owner`; until the
|
||||||
|
owner's typed advertisement lands, `thing2:follow_up:discovery-owner-campaign`,
|
||||||
|
through one adapter over today's `gamecult.eve.provider_advertisement.v1`,
|
||||||
|
deleted when it does) and mounts core; reachable by `thing-mesh`'s transport or a gateway (O4).
|
||||||
|
- **Invariants:** M8 (no `taffy`/`parley` in the JS package or the wasm host
|
||||||
|
glue); the JS host calls only `Core::frame`, passes only the closed
|
||||||
|
`InputEvent`s, and draws only the closed commands; the hidden textarea never
|
||||||
|
scrolls the page into view (R-T §2: egui's park-over-canvas) and never receives
|
||||||
|
pointer events; one frame's work for `f06` (1k items) stays under a stated budget
|
||||||
|
(O10: a number is owed, none was measured).
|
||||||
|
- **Test:** unit in `wasm-bindgen-test` (headless Chrome and Firefox in the verify
|
||||||
|
image): replay goldens through the canvas and compare against a canvas-drawn
|
||||||
|
reference of the same list (command consumption, M10); IME: scripted composition
|
||||||
|
events at the textarea produce core edits equal to the 9b scripts; a11y: DOM
|
||||||
|
mirror equals the AccessKit tree for `f04`; click-through: a pointer test over a
|
||||||
|
mirror region reaches the canvas target (guards the Flutter failure). Real IME
|
||||||
|
and real screen readers are operator checks (O6).
|
||||||
|
- **Prior art:** R-T §2-3 as above; EditContext is Chrome-only (MDN, R-T §3) so it
|
||||||
|
is not the base path; `egui` web and Flutter web **[read]**; Slint web has no
|
||||||
|
accessibility and is not recommended for general web apps (R-T §2) **[read]** (the
|
||||||
|
failure this piece exists to avoid). Confidence medium: no Taffy+AccessKit
|
||||||
|
web combination exists in the sources read (R-T Gaps), so 11b is first of its
|
||||||
|
kind for this stack.
|
||||||
|
|
||||||
|
### Piece 12: thing-mesh (the CultMesh adapters)
|
||||||
|
|
||||||
|
- **Owner:** `thing-mesh`: `StatePort` and a discovery client over CultMesh/CultNet
|
||||||
|
and CultCache (the client is a port: Thing's contract has no advertisement of its own, `advertisement-owner`); the outbound invocation sender; the only crate with a transport
|
||||||
|
dependency.
|
||||||
|
- **Invariants:** a `StatePort` never returns `serde_json::Value` or an untyped
|
||||||
|
blob (P3); decode runs through the contract types and refuses on error
|
||||||
|
(`Change::Refused`), never a silently-dropped update; subscription uses the mesh's
|
||||||
|
own mechanism, never a poll loop (BD9 dies); the sender sets the caller identity
|
||||||
|
from the transport session and has no API to override it; decode needs no special thread once Eve pins CultLib `bbb98eaf`, which
|
||||||
|
carries `cultlib-gaps:follow_up:decode-stack-safety` (Fixed); until that pin
|
||||||
|
merges, decode runs on a thread with the stack it needs (P20).
|
||||||
|
- **Test:** pipeline smoke: a real CultMesh in-process loopback publishes
|
||||||
|
`f01`'s state and a document; core with `thing-mesh` shows it; then a hostile
|
||||||
|
record (unknown arm) yields `Change::Refused`; an invocation sent through the
|
||||||
|
sender arrives at a fake provider with the transport's identity and not any value
|
||||||
|
the core supplied. End-to-end (per Verse contract): later, with Odin's discovery.
|
||||||
|
- **Prior art:** the doctrine itself (CultMesh is the state substrate;
|
||||||
|
`doctrine/gamecult.md`); service registries are not owned by the UI toolkit
|
||||||
|
(R-K §3: Consul, Kubernetes Services **[background]**; Teams manifests vs cards
|
||||||
|
**[background]**). Confidence high on shape, unknown on the CultMesh API surface
|
||||||
|
it must wrap: that API is read when the cut is specced (O4).
|
||||||
|
|
||||||
|
### Piece 13: consumer ports (template)
|
||||||
|
|
||||||
|
One `cut_spec` per consumer, each carrying its rename (T, E1-E7 in
|
||||||
|
`thing-campaign.md`) and deleting its Thing 1 path in the same cut. Order per map
|
||||||
|
cut 5 (Odin, VoidBot, Ghostlight+Heimdall, Muninn/sleipnir, weksa, Stonks, Vili,
|
||||||
|
Gjallar, StreamPixels, gamecult-ops, CultLib), with cutover shape per
|
||||||
|
`thing2:ruling:port-cutover`: live consumers cut over one at a time, never all at
|
||||||
|
once.
|
||||||
|
|
||||||
|
Rulings that shape every port:
|
||||||
|
|
||||||
|
- **Naming and wire.** The eve-to-thing wire rename is frozen and folded into Thing
|
||||||
|
2.0; no wire, window or prose rename lands apart from a port. Thing 2.0 adopts
|
||||||
|
rename table T and edge table E1-E7 as its naming rule, so surviving kinds are
|
||||||
|
born `gamecult.thing.*` (`thing:ruling:wire-rename-freeze-fold`).
|
||||||
|
- **No compatibility ids.** No `CompatibleSchemaIds` on any renamed type. An
|
||||||
|
Eve-typed store is deleted and rebuilt by its daemon, conformance packs are
|
||||||
|
re-exported under thing ids, and a store found to be truth stops its cut as a
|
||||||
|
fork (`thing:ruling:operator-compat-rebuild`). A port therefore carries a
|
||||||
|
rebuild step, not a reader of old ids.
|
||||||
|
- **No new Eve surfaces.** Thing, formerly Eve, is deprecated; every new surface
|
||||||
|
waits for Thing 2.0 (`thing2:ruling:surfaces-wait-for-thing2`). Operator
|
||||||
|
dashboards are quarantined (parked tag, removed from main and the build) and
|
||||||
|
rebuilt on Thing 2 by follow-ups, e.g. `thing2:follow_up:ymir-operator-view-on-thing2`,
|
||||||
|
`weksa-`, `mimir-`, `muninn-operator-view-on-thing2`
|
||||||
|
(`thing2:ruling:eve-dashboards-quarantined-2`). Product UIs are decided case by
|
||||||
|
case: `Mimir.EveSensorReceiver` and the Pitch page's visual-novel component are
|
||||||
|
frozen until Thing 2, neither extended nor removed
|
||||||
|
(`thing2:ruling:mimir-sensor-receiver-frozen`, `thing2:ruling:pitch-component-frozen`).
|
||||||
|
- **Discovery is not Thing's.** A port reads the provider advertisement through the
|
||||||
|
`thing-mesh` port, not through a Thing-owned kind (`thing2:ruling:advertisement-owner`).
|
||||||
|
Until the owner lands (`thing2:follow_up:discovery-owner-campaign`), one adapter
|
||||||
|
over today's `gamecult.eve.provider_advertisement.v1` serves the port and is
|
||||||
|
deleted when the owner's typed advertisement arrives.
|
||||||
|
- **Repo set.** The code home is the Eve repo renamed Thing; Unity and Flutter
|
||||||
|
backends get their own repos only when a live consumer needs one
|
||||||
|
(`thing2:ruling:code-home`). The campaign waits for `cut-campaign-repos`, then
|
||||||
|
target r2 names the 13 repos before the first port
|
||||||
|
(`thing2:ruling:campaign-repo-limit`). The six retired family repos are archived
|
||||||
|
under their Eve names (`thing2:ruling:retired-repo-names`).
|
||||||
|
|
||||||
|
**Port-list gap.** gamecult-site's Thing 1 surface is tracked in
|
||||||
|
`void-body:follow_up:site-eve-present-tense` (Void's mind; folded there by
|
||||||
|
`thing2:resolution:follow_up.site-thing1-surface.n1`), and its Pitch component is
|
||||||
|
frozen until Thing 2 (`thing2:ruling:pitch-component-frozen`). The repo count
|
||||||
|
stays 13.
|
||||||
|
|
||||||
|
The template (every port cut has the same spec skeleton, so Soul can run the same
|
||||||
|
falsifications):
|
||||||
|
|
||||||
|
1. Fixture first: the consumer's current surface rebuilt as a Thing Script document
|
||||||
|
through its language package, loaded into `thing-testkit`, geometry-golden'd.
|
||||||
|
2. Deletes: its Thing 1 publisher path and every `gamecult.eve.*` reader/writer
|
||||||
|
(exact paths from the census, to `file:line`).
|
||||||
|
3. Commands: each existing command becomes a `CommandDecl` and a handler
|
||||||
|
returning an `Outcome` through the package (no state-as-string, M3).
|
||||||
|
4. Test: the consumer's own suite plus one pipeline smoke that renders the
|
||||||
|
consumer's real surface headlessly through core and asserts the geometry golden
|
||||||
|
and the command round-trip. Operator check: view it in the TUI/web.
|
||||||
|
5. Negative grep: no `gamecult.eve` and no `eve-browser-lowering` imports remain.
|
||||||
|
- **Prior art:** expand/contract (parallel change) and strangler-fig, with the
|
||||||
|
caveat in R-K §4 that Thing 2 kinds are new kinds not renames, so an aggregator
|
||||||
|
reads two kinds, not one under two ids **[read]**. Confidence high on the
|
||||||
|
template, per-consumer specs owe their own census read.
|
||||||
|
|
||||||
|
### Piece 14: retirement
|
||||||
|
|
||||||
|
Delete Thing 1's tree from the kernel repo after the last port. Test: negative
|
||||||
|
greps for `gamecult.eve`, `eve-browser-lowering`, `packages/eve-contracts`,
|
||||||
|
`web/eve-dsl.js`; workspace builds with the directories gone; README front door
|
||||||
|
(`doctrine`: first screen true) rewritten; Idunn targets (P15) re-pointed. Soul
|
||||||
|
pass before the tag.
|
||||||
|
|
||||||
|
## 4. What every cut's Soul pass attacks (derived, not new)
|
||||||
|
|
||||||
|
Rules M1-M10 are the checklist; in addition the shared failure shapes from this
|
||||||
|
spec: (i) a backend that grows layout, text or focus (compare dependency trees and
|
||||||
|
public fns); (ii) a decode refusal that reads as absence; (iii) a rule held by a
|
||||||
|
constructor but skipped by decode (the contract's P18 lesson, now applied to
|
||||||
|
`CommandInvocation` and results received by core); (iv) a golden re-blessed to
|
||||||
|
match a bug (Soul reruns generation on a clean checkout and diffs); (v) an `Embed`
|
||||||
|
or placeholder that silently drops.
|
||||||
|
|
||||||
|
## 5. Decisions made here (summary)
|
||||||
|
|
||||||
|
| ID | Decision | Confidence |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| D1 | One `thing-core` crate with modules; six crates in total | medium-high |
|
||||||
|
| D2 | Nine-variant closed render-command list | medium |
|
||||||
|
| D3 | Closed diagnostics enum drives placeholders, logs, a11y and tests | high |
|
||||||
|
| D4 | Byte-compared binary render and geometry goldens, `bless` task | medium |
|
||||||
|
| D5 | Invocation lifecycle with core-only `Malformed` and `TimedOut` | medium-high |
|
||||||
|
| D6 | Cell widths pinned to a `unicode-width` version in goldens | medium |
|
||||||
|
| D7 | One focus owner in core, id-keyed, modal trap | medium-high |
|
||||||
|
| D8 | (see O7) edit history scope | open |
|
||||||
|
| D9 | Web draws through canvas 2D first | medium |
|
||||||
|
|
||||||
|
## 6. Open: what this spec could not decide
|
||||||
|
|
||||||
|
Each with a recommendation; none blocks cuts 1-8.
|
||||||
|
|
||||||
|
- **O1. Parley (and AccessKit types) on `wasm32`.** No source read establishes
|
||||||
|
that Parley + fontique build and perform for wasm; GPUI web has its own text
|
||||||
|
system and Slint web uses femtovg, not Parley. Recommendation: one half-day
|
||||||
|
probe (build `thing-core` text module to wasm, shape `f12`) before cut 9a is
|
||||||
|
specced; the fallback is browser `Canvas.measureText` behind the `Measure` trait,
|
||||||
|
which weakens "core owns text" for the web only. `thing:ruling:thing2-thing-owns-text`
|
||||||
|
already puts shaping in core, so taking the fallback contradicts it and needs a
|
||||||
|
new operator ruling. Needs: the probe.
|
||||||
|
- **O2. Glyph rasterization path** (canvas 2D, Slug, Vello family). Partly ruled:
|
||||||
|
rasterization is backend-private and the first GPU backend picks Vello or a Slug
|
||||||
|
port by measurement (`thing:ruling:thing2-thing-owns-text`). Still open:
|
||||||
|
Recommendation: canvas 2D now; revisit after a measured frame budget (O10).
|
||||||
|
Needs: operator word on whether GPU glyphs gate web cutover.
|
||||||
|
- **O3. The exact command list**, in particular whether `Plot` lowers to
|
||||||
|
`Polyline`/`Polygon` in core or becomes an `Embed`. Recommendation: lower in core
|
||||||
|
(consumers expect charts; an `Embed` makes every host write one). Needs: a
|
||||||
|
decision on D2 from the spec's reviewer.
|
||||||
|
- **O4. CultMesh/CultNet surface for `thing-mesh` and how a browser reaches it**
|
||||||
|
(direct WebTransport/QUIC, or a gateway). Not read in this pass. Who owns the
|
||||||
|
provider advertisement is ruled (`thing2:ruling:advertisement-owner`: discovery
|
||||||
|
leaves Thing, its owner is CultMesh or Odin, follow-up
|
||||||
|
`thing2:follow_up:discovery-owner-campaign`); the transport question stays open.
|
||||||
|
Needs: a short
|
||||||
|
Eyes pass over `cultmesh-ts`/`cultnet-ts` in CultLib before cut 12 and 11c are
|
||||||
|
specced.
|
||||||
|
- **O5. Defaults that are guesses:** invocation timeout 15 s, frame budget, scrollbar
|
||||||
|
style, overlay z policy. Recommendation: land as `Config` with these defaults and
|
||||||
|
record a follow-up to measure.
|
||||||
|
- **O6. What only the operator can check:** real IME on Windows/macOS/iOS/Android
|
||||||
|
browsers, a real screen reader on the web mirror. No CI substitute exists; the
|
||||||
|
spec names them as operator checks at cut 11 and at each consumer cutover.
|
||||||
|
- **O7. Editing scope (D8):** undo/redo, clipboard, drag-select auto-scroll in 9b.
|
||||||
|
Recommendation: undo and clipboard are in (a general library's reasonable
|
||||||
|
expectation; clipboard goes through `HostEffects`), word/line navigation in,
|
||||||
|
rich text out. Needs: nothing but a go.
|
||||||
|
- **O8. Locale in `Format`.** Recommendation: a host-supplied `Locale` input,
|
||||||
|
default `en`, formats stay a closed set.
|
||||||
|
- **O9. Diagnostics added by later cuts** (`AssetMissing`, `NoAccessibleName`)
|
||||||
|
extend D3's list; confirm the list stays closed and versioned with core.
|
||||||
|
- **O10. Performance budgets** (frame time at 1k and 10k nodes, wasm size). No
|
||||||
|
number was measured anywhere in the sources read; Clay's "microsecond layout"
|
||||||
|
claim has no table (R-F §2.1) and the Taffy README numbers are layout-only
|
||||||
|
(R-F §2.2). Recommendation: cut 6 records the first measured numbers in a
|
||||||
|
bench fixture, and budgets are set from them rather than guessed.
|
||||||
|
- **O11. Thing Script text syntax: closed by ruling, no open question.**
|
||||||
|
`thing2:ruling:thing-script-form` (InForce, ruled 2026-10-10, choice
|
||||||
|
`document-authority-text-later`; operator: "all recommended") says the typed
|
||||||
|
document is Thing Script's one authority, and a `.thingscript` text syntax comes
|
||||||
|
later, parsed only by core. Consequences held in this spec: nothing but the typed
|
||||||
|
document is authoritative; no language package or host parses a text form (packages
|
||||||
|
build the typed document and nothing else; tooling reaches the one parser through core, which
|
||||||
|
the question's recommendation expects to expose via wasm); when the text form is cut
|
||||||
|
it lands in `thing-core` next to `resolve` and produces a `SurfaceDocument`, so
|
||||||
|
`resolve` still sees only the typed document, and its tests are round-trip
|
||||||
|
(text to document to text) plus the existing `resolve` suite on the parsed
|
||||||
|
output. It has no cut yet and sits after cut 3; the map's cut order is not
|
||||||
|
changed here.
|
||||||
|
- **O12. Prior-art standard.** This spec reads "prior-art sources cited" as: every
|
||||||
|
decision names the sources and a confidence, with [read]/[background] honesty.
|
||||||
|
Open: whether the standard also fixes a format, a minimum source count or a
|
||||||
|
required venue; the tags above are where to retrofit it.
|
||||||
|
- **O13. Where the repo's unmerged contract-ts / contract-cs branches stand.**
|
||||||
|
Ruled as far as the mind records: the live specs are `cut-contract-ts.r3` and
|
||||||
|
`-cs.r3`, which wait on contract-cs h2 being admitted, held by Soul and merged
|
||||||
|
by Self. This spec did not re-verify branch heads; that check belongs to the
|
||||||
|
merging Self.
|
||||||
|
|
||||||
|
## 7. Substrate gaps felt writing this spec
|
||||||
|
|
||||||
|
- Resolved: the `thing2` and `thing` campaigns are now readable
|
||||||
|
(`huginn rulings thing2 thing`, `huginn view <id>`), and this spec was checked
|
||||||
|
against them on 2026-11-10. Rulings are now cited by id. Remaining gap: the
|
||||||
|
`huginn rulings` header line truncates the choice, so each ruling needs a `view`.
|
||||||
|
- No published golden-command suite exists to copy (D4); the harness is ours to
|
||||||
|
build and is itself a cut-1 testkit deliverable.
|
||||||
|
|
@ -473,6 +473,10 @@ code fails it.
|
||||||
|
|
||||||
## Cut order (deliverables)
|
## Cut order (deliverables)
|
||||||
|
|
||||||
|
Per-piece contracts, the build graph with sizes, a test per piece and the prior-art
|
||||||
|
citations for each choice are in `docs/thing2-build-spec.md`; this list is the
|
||||||
|
summary.
|
||||||
|
|
||||||
0. **`stale-voidbot-cleanup`** (Eve main, any time): spec
|
0. **`stale-voidbot-cleanup`** (Eve main, any time): spec
|
||||||
`thing2:cut_spec:cut-stale-voidbot-cleanup.r1`. Owned here by
|
`thing2:cut_spec:cut-stale-voidbot-cleanup.r1`. Owned here by
|
||||||
`thing:follow_up:stale-voidbot-cleanup-on-main`.
|
`thing:follow_up:stale-voidbot-cleanup-on-main`.
|
||||||
|
|
@ -494,7 +498,7 @@ code fails it.
|
||||||
(`thing2:verdict:cut-contract-rs.s1`, `.s2`). The siblings are mapped
|
(`thing2:verdict:cut-contract-rs.s1`, `.s2`). The siblings are mapped
|
||||||
against `6ae2da0` and run side by side, touching only their own package and
|
against `6ae2da0` and run side by side, touching only their own package and
|
||||||
one `.gitignore` insertion each, at separate anchors:
|
one `.gitignore` insertion each, at separate anchors:
|
||||||
`thing2:cut_spec:cut-contract-cs.r2` and `thing2:cut_spec:cut-contract-ts.r2`.
|
`thing2:cut_spec:cut-contract-cs.r3` and `thing2:cut_spec:cut-contract-ts.r3`.
|
||||||
Waiting, one line each:
|
Waiting, one line each:
|
||||||
- Cross-runtime store check, after both siblings land: the C# package opens
|
- Cross-runtime store check, after both siblings land: the C# package opens
|
||||||
a store the TS package wrote and the reverse, which proves the catalog
|
a store the TS package wrote and the reverse, which proves the catalog
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue