# Thing 2.0 map Campaign `thing2` in the eureka mind. Imagination pass `imagination-thing2-contract`, session `self-2026-10-10c-eureka`, 2026-10-10. Target `thing2:target:r1` (`docs/thing2-target.md`). Rulings in force and cited by id: `thing:ruling:thing2-one-solver-taffy`, `thing:ruling:thing2-thing-owns-text`, `thing:ruling:wire-rename-freeze-fold`, `thing:ruling:operator-compat-rebuild`, `thing:ruling:operator-dsl-thing-script`, `thing:ruling:operator-member-names-keep`, `thing:ruling:operator-aetheria-eve-rename`, and the six `thing2:ruling:*` of 2026-10-10 (`code-home`, `thing-script-form`, `advertisement-owner`, `port-cutover`, `campaign-repo-limit`, `retired-repo-names`). This page holds body facts, the model page, the consumer contract at the level consumers need, the cut order at the level of deliverables, and rationale. The questions it raises are documents in the mind (`thing2:question:*`); it does not restate their options. Research for the forks: `docs/research/thing2-contract-forks-prior-art.md`. Evidence it builds on: `docs/research/thing2-consumer-census.md` (census, cited as C§n, BD1-BD9), `docs/research/thing2-foundation-prior-art.md`, `docs/research/thing2-text-input-a11y-prior-art.md`, and `docs/thing-campaign.md` (rename table T, edges E1-E7, body facts B1-B28). ## Body facts Probes P1-P16 ran 2026-10-10, P17-P22 2026-10-12, against `origin/` after `git fetch` unless a local path is named. Scripts are in the session scratchpad `imagination-thing2-contract/`. **P1. The Thing 1 contract is 38 schemas, most of them process machinery.** `p1_schemas.py` over Eve `schemas/` at `a758787`: 38 files. Eleven describe conformance, hand-off, split-target, release, capture and witness bookkeeping (`conformance_*`, `*_handoff`, `runtime_*`, `capability_matrix`, `provider_scenario`); five describe one renderer's output (`unity_*`, `electron_shell_projection`, `web_layout_probe`, `tui_grid`); seven describe the plugin system (`plugin*`). Every schema but `command_invocation`, `command_result`, `entity_soa_view`, `input_*` and `asset_catalog` sets `additionalProperties: true`. **P2. Commands travel in four overlapping envelopes, and the result is hand-built JSON.** `command.v1` (descriptor, open), `command_invocation.v1` (closed), `command_receipt.v1` (open, `state` enum `accepted|denied|pending|reconciled`) and `command_result.v1` (closed: exactly `schema, receipt, transientProjection, pluginPayload, draftDirective`). Ghostlight `crates/ghostlight-dungeon/src/eve.rs` at `327cbe3`: `validate_invocation` (`:665-700`) re-checks the envelope by string comparison; `command_result` (`:734-`) takes `state: &str` and builds the receipt with `json!`; its own log filter names a state `"completed"` that the receipt enum does not admit (`:746`). Today it passes only `accepted` and `denied` (tally of call sites). The browser rejects a flat result whole through `ajv` (`web/package.json` depends on `ajv`; `eve.rs:723-731` comment). Nothing but convention stands between a provider and a result the viewer refuses. **P3. One surface kind has two schemas and is stored as an untyped blob.** `surface.v1` declares `type: const "surface-state"`; `surface_state.v1` exists only as code: CultLib `packages/cultcache-ts/src/swarm-documents.ts:82-96` (members `providerId, title, version, updatedAt, surface: object`) and Odin `crates/odin-core/src/documents.rs` (`EveSurfaceStateRecord { surface: Value }`; advertisement and interface binding are `{ value: Value }`). The load-bearing shape is JSON inside CultCache. **P4. `interface_binding.v1` is not a binding.** weksa `scripts/weksa-daemon.mjs:321-341` writes routes (`endpoints`, `routes`, `cultMeshAddress`) and the whole operator surface into it; its document type parses anything (`schema: { parse: (value) => value }`, `:418`). It is a third copy of the advertisement's routes and the surface. **P5. The text DSL lets the renderer write state.** `web/eve-dsl.js:103-155`: `var`, `collection`, `derive` (`count`, `latest`) live in a renderer-local mesh, and `applyAction` appends or sets values there on a button press (`web/fixtures/reactive-composition.eve`: `button "Append Event" append swarm.events ...`). **P6. A domain-looking kind is layout plus a server-side command.** Muninn `crates/sleipnir-daemon/src/main.rs:1374-1415`: `binding.target` is a labelled target with `x, y, anchorX, anchorY` over a controller image; "listening" is the daemon's `pending_learn` state. Absolute positioning (Taffy `position: absolute` with insets), a button and a bound label compose it. **P7. Login needs a host effect, not a plugin payload.** Ghostlight `web/src/heimdall-access-adapter.ts:48-66`: a `pluginPayload` of schema `heimdall.auth_navigation_receipt.v1` makes the client call `window.location.assign(url)`; on return it reads the URL and resumes (`:82`, `:134`). **P8. Two live viewers render surfaces.** Ghostlight's web client vendors `@gamecult/eve-browser-lowering` and `eve-contracts` from `vendor/eve` (`web/package.json`). gamecult-ops `scripts/idunn/idunn-deployment-targets.ps1:262-272` at `9aed01f`: `nightwing-eve-browser-reference`, `Repo = "Eve"`, `Status = "runtime-enforced"`, `Deploy = $null`. Odin aggregates and renders nothing. **P9. CultCache-rs members are serde types.** CultLib `packages/cultcache-rs/src/lib.rs:5501-5520`: a `DatabaseEntry` whose member is a struct holding an enum round-trips through the MessagePack store. Whether the TS, C# and Python CultCache runtimes decode a recursive tagged union written by Rust with parity was not established in this pass. It is the first thing the contract cut proves (see the cut order). **P10. The stale `.voidbot` lines are still on Eve main.** `a758787`: `.gitignore:28-30` and `scripts/stage-to-eve.ps1:24` (`--exclude=.voidbot`). `git grep -n '\.voidbot' -- .gitignore scripts` prints exactly those four lines; `Sources/EVEViewController.mm` mentions `voidbot.swarm` (no dot) and is not matched. **P11. The campaign's repo list has room for the early cuts only.** Mind, `thing2:campaign:self`: repos `GameCult/Eve`, `GameCult/gamecult-ops`. `eureka-substrate:cut_spec:cut-campaign-repos.r1` (target-revision repo sets, up to 16) is in force with no report; both its questions are answered (`eureka-substrate:ruling:campaign-repo-set-home`, `:campaign-repo-removal`). **P12. Rust CultCache writes a union as serde's external tag; the C# reference writes `[key, fields]`. They do not read each other.** Probe pass `imagination-thing2-contract-cut`, 2026-10-10, local scratch `imagination-thing2-contract-cut/{rs,cs,ts}` against CultLib `6bdc23c9`. One record type in all three runtimes: `id`, `version`, `root: Node` with `Node` one of `Box { id, children: [Node] }`, `Text { id, content }`, `Use { id, composition, slots: [Node] }`, and `outcomes: [Outcome]` with a unit arm. Rust (`#[derive(DatabaseEntry)]`, serde enums) writes the payload `94 af..31 07 81 a3"Box" 92 ...`: each arm a one-entry map keyed by the variant name, fields positional, a unit arm the bare string `"Denied"`. C# (MessagePack `[Union(n, ...)]`, the reference) writes `94 af..31 07 92 00 92 ...`: each arm `[key, [fields]]`, the unit arm `[2, []]`. C# reading the Rust store fails: `MessagePackSerializationException: Failed to deserialize ProbeSurface value. <- Unexpected msgpack code 129 (fixmap) encountered.` Rust reading the C# store fails: `failed to decode CultCache entry "probe.surface" ... a value does not fit the store format`. Each runtime round-trips its own store. The `cultcache-rs` derive handles structs only (`cultcache-rs-derive/src/lib.rs:51`); enum members go through `rmp_serde`. Owner: CultLib (`thing2:follow_up:cultcache-union-parity`). **P13. TypeScript CultCache has no typed codec.** `cult-cache.ts:679-692`: with no `formatter`, a payload decodes to plain arrays and maps and `schema.parse` sees them raw; the interop test hand-writes a positional formatter per type (`test/cult-cache.test.ts:1263-1310`). In the probe, a hand-written TS codec for the union read the Rust store, wrote a store Rust read back equal, and re-encoded byte-identical to Rust's payload; against the C# store the same codec refused the `[key, fields]` arm (`node is not a tag map`). A TS reader matches whichever encoding its author hand-codes. **P14. Nothing live builds from the six retired family repos.** `git grep` at `origin/HEAD` of Ghostlight `327cbe3`, Odin `8e1de66`, VoidBot `46d891b`, Heimdall `7904e17`, Muninn `1d0ea0b`, weksa `d16cff5`, Stonks `3f6e91c`, Vili `8773c15`, Gjallar `6a92976`, StreamPixels `8d40541`, CultLib, Idunn `6667f0f`, gamecult-site `8169ee5` and Ymir `f8c0e39` for `EveUnity|EveFlutter|EveElectron| EveTui|EvePlugins|EveConformance` outside `*.md`: no match. gamecult-ops `9aed01f` matches only `docs/repo-census-2026-09/synthesis.html`; Eve matches a local smoke script (`scripts/run-aetheria-conformance-consumer-smoke.ps1:35`, a local path) and a spec; parked Mimir names them in one configuration file. GitHub, 2026-10-10: all six unarchived; active workflows only on EveFlutter and EveTui (`runtime.yml`) and EveConformance (`conformance.yml`); `GameCult/Thing` and `GameCult/AetheriaThing` do not exist; AetheriaEve is archived. **P15. Ghostlight's deploy compares its Eve clone's origin by exact string.** gamecult-ops `9aed01f` `scripts/deploy-ghostlight-yggdrasil.sh:20` (`eve_origin=https://github.com/GameCult/Eve.git`) and `:871-876` (`require_idunn_repo` refuses "a foreign origin" when `git remote get-url origin` differs), called at `:883`; the binding template carries the same URL (`idunn/yggdrasil/bindings/ghostlight.toml.in:14`). Editing either URL without re-pointing Yggdrasil's Idunn-owned clone stops the next Ghostlight deploy; leaving both on the old name keeps working through GitHub's rename redirect. The `Repo = "Eve"` targets (`idunn-deployment-targets.ps1:223`, `:236`, `:262`) are Thing 1 targets (iPad, Android, Nightwing browser reference) paired with `LocalPath = "$projectsRoot\Eve"`. **P16. gamecult-site carries a generated Thing 1 surface.** `scripts/generate-vn-eve-surface.mjs` writes `GameCult/static/interactive/gamecult-compound/gamecult-compound.eve-gui.json` (`gamecult.eve.*` ids), referenced by `GameCult/Pitch.md:15` (`data-eve-gui=`). The census (C) does not list the site as a consumer. **P17. The contract's integer leaves are signed because TS and Python have no unsigned leaf; the C# reference has one.** CultLib `b3bd8dd5`: `src/GameCult.Caching/CultSchemaTypeNames.cs` names any CLR type (so `UInt32`, `UInt64` and `Byte` slots exist in the reference); `cultcache-ts` `src/codec.ts` exports only `int32` and `int64` integer codecs and `cultcache-py` `leaves.py:147-148` only `int32` and `int64`. A `u64` above 2^63 has no TS or Python reading, so versions stay `Int64` and counts `Int32`, and the non-negative rule is the contract's, not the leaf's. `Rgba` keeps `u8` (the reference's `Byte`); the TS and Python packages mirror its 0-255 range from a refusal golden until CultLib gives them a byte leaf. **P18. A domain newtype's own `Deserialize` holds its rule at every decode door, and leaves the bytes unchanged.** Probe `imagination-contract-rs-r2/` (throwaway commit `fbbdaf79` on `649844aa`, Yggdrasil, 2026-10-12): `Seq(i32)` refusing below 1 and `Version(i64)` refusing below 0, each `Serialize` the inner integer and `Deserialize` through `new`. A result payload with seq 0 or -7 is refused by `rmp_serde::from_slice` ("Seq: at least 1") and by `cultcache_rs::decode_payload` (opaque: "a value does not fit the store format"); seq 0 through serde's in-memory `IntoDeserializer` is refused the same way, so the rule holds for any serde format. With `CommandResult.seq: Seq` and both composition versions `Version` (an `Int32` slot widened to `Int64`), `goldens_match` still compares byte-identical, `surface.cc` included. The derive reads each slot with `seq.next_element::()` (`cultcache-rs-derive/src/lib.rs`, `slot_codec`, at `b3bd8dd5`), so a field's type is the one owner of its domain on both paths. `DatabaseEntry` requires `DeserializeOwned` (`cultcache-rs/src/lib.rs:162`), so the public `Deserialize` of every kind cannot be removed: it is the decode door. **P19. Dotted provider ids are the Verse's norm; dotted local names are not needed.** `git grep` over Ghostlight, VoidBot, Odin, Heimdall, Muninn, Stonks and Eve (operator checkouts, 2026-10-12): provider ids `voidbot.swarm`, `odin.allseer`, `stonks.market`, `sleipnir.input-mirror.raven`, `heimdall`; Thing 1 surface ids repeat the provider as a prefix (`voidbot.swarm.surface`, `ghostlight.play` in `crates/ghostlight-dungeon/src/mesh.rs:37`). No `gamecult.thing.*` record exists outside the contract's tests, and Thing 1 records are rebuilt, not read (`thing:ruling:operator-compat-rebuild`), so the Thing 2.0 key grammar strands nothing. Probe evidence of the collision itself: Soul's `soul-thing2-contract-rs/rust1.log` KEY lines. **P20. In C#, the constructor MessagePack calls is the one door, and the decode recursion fits a default thread inside the bound.** Probe `imagination-contract-cs-ts/` (throwaway commit `e5030d3b` on CultLib `b3bd8dd5`, Yggdrasil, image dotnet sdk 10.0, MessagePack 3.1.7, Debug and Release, log `cs2.log`). A `[MessagePackObject]` class with get-only `[Key]` properties and one constructor taking the keys in slot order, throwing on `seq < 1`: - public constructor: `CultDocumentRegistry` accepts it; `result-pending.bin` decodes through `CultDocumentMessagePackSerialization` and re-encodes identical; `result-seq-zero.bin` is refused by the constructor through `DeserializeUntyped`, through `MessagePackSerializerOptions.Standard`, and through `System.Text.Json` (which calls the same constructor); no message holds the golden's canary. - private constructor with `[SerializationConstructor]` under `[MessagePackObject(AllowPrivate = true)]`: the same results, the registry accepts it, reflection finds no public constructor and no public setter, and `System.Text.Json` refuses the type (`NotSupportedException`). - internal constructor without `AllowPrivate`: the registry refuses the type ("no constructor MessagePack can call"), and `CultDocumentRegistry.Shared` then refuses every type in the assembly, so one bad type breaks the package. - `outcome-unknown-arm.bin` is refused through `CultDocumentMessagePackSerialization` (`CultUnionGuard`: "Union ... has no arm for key 4"), but decodes with a null outcome through `MessagePackSerializerOptions.Standard`: the guard lives only in CultLib's options, so a package decodes only through them. - Stack: a Box-shaped recursive union (3 containers per level) reaches the 256 bound at 84 levels. Decoding recurses per level: depth 10 decodes on a 64 KiB thread, depth 84 needs 256 KiB in Debug and 128 KiB in Release, and an overflow aborts the process (rc 134). .NET's 1 MiB default thread holds the deepest admitted payload with at least 4x margin, so C# carries no part of `cultlib-gaps:follow_up:decode-stack-safety`. **P21. In TypeScript, cultcache-ts runs a document's codec and schema on both doors, and its recursion fits the default stack.** Same probe commit, image kotlin (Node 24.21.0), log `ts2.log`. cultcache-ts at `b3bd8dd5` (0.14.0) has `slots`, `union`, `list`, `lazy`, `unit` and the 256 payload bound; `decodeDocumentPayload` runs the codec then `schema.parse`, and `encodeDocumentPayload` runs `schema.parse` then the codec (`src/document.ts`). A `seq` codec delegating to `int32` with a `>= 1` check, and a `CommandResult` class with a `#brand` private field, a private constructor, a codec wrapping its slot codec that mints an instance on decode, and a `schema.parse` that refuses anything without the brand: - `result-pending.bin` decodes to an instance and re-encodes identical; `result-seq-zero.bin` is refused ("seq (slot 1): Seq must be at least 1"); `outcome-unknown-arm.bin` is refused ("Union Outcome: unknown arm key 4"); `result-extra-slot.bin` reads as seq 1; no message holds the canary. - `structuredClone`, spread and `JSON.parse` of an instance lose the brand, and encoding any of them, or an object literal with seq 0, is refused; the factory with seq 0 throws. - `tsc` (strict, NodeNext, ESM importing the CommonJS cultcache-ts) passes with `@ts-expect-error` on an object literal and a spread typed `CommandResult`, on `new CommandResult(...)`, and on assigning `seq`; `structuredClone(result)` and `JSON.parse` still type as `CommandResult`, so the runtime brand check is the guard there. - Stack: the same chain reaches the bound at 84 levels. Depth 84 decodes with `--stack-size` 200 and the default 984; at 60 to 120 the codec throws (a `RangeError`, wrapped by the codec into its own refusal), never aborting. **P22. Packaging around the two packages.** Eve `6ae2da0`: `.gitignore` ignores `packages/*` with one `!` pair per package (lines 2-16), so a new package directory is invisible to git until it gets its pair, and global `obj/` (line 17) is where the Thing 1 test project keeps its output (`BaseOutputPath obj\bin\`). `GameCult.Eve.Surface.csproj` reads CultLib from `CultLibRoot`, defaulting to the sibling checkout `..\..\..\CultLib`. `npm view @gamecult/cultcache-ts` answers 404: cultcache-ts is on no registry, and the consumers that use it (Heimdall, StreamPixels) take it as `file:vendor/CultLib/packages/cultcache-ts` from a submodule. No file outside `crates/` and `docs/` names `thing-contract` or `GameCult.Thing.Contract` at `6ae2da0` except `Cargo.lock`. ## The model page Every persistent kind Thing 2.0 has. Ids follow rename table T: schema `gamecult.thing..v`, mesh key `thing::`. No row keeps a compatible eve id (`operator-compat-rebuild`). | Kind | What names it | What happens to it over time | Who decides (one writer) | | --- | --- | --- | --- | | Thing Script document (a published surface) | schema `gamecult.thing.surface.v1`; key `thing:surface:.