Place the document variants target and cut map on main

The target (2026-09-17, with the operator's rulings) lived only on
codex/document-variants. The cut map is Imagination's pass of 2026-09-29.
Its operator questions Q1-Q7 are open.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Meta Vens 2026-09-29 01:34:42 +02:00
commit 1e86bf0dc3
2 changed files with 518 additions and 0 deletions

View file

@ -0,0 +1,372 @@
# CultCache document variants: cut map (draft)
Date 2026-09-29. Imagination output for `docs/document-variants-target.md` (branch
`codex/document-variants`). Body read at `origin/main` `268e0ef`. This map owns means only;
the target owns ends and rulings 1-7. Nothing here is decided that the batch in section 4
asks the operator to decide.
## 1. Probe facts (run, not inferred)
Probe: scratch clone `scratchpad/variant-probe` (commit `7b6de8e`, probe files only), run on
Yggdrasil via `ygg-verify.sh` with images `dotnet`, `rust`, `python:3.12-slim`, `node:22`.
Each runtime's current reader wrote a normal store, the bytes were altered four ways, and
the store was reopened. Output in `scratchpad/probe-out/*.txt`.
| Altered store | C# | Python | TypeScript | Rust |
| --- | --- | --- | --- | --- |
| A. v1 header, record gains slot 4, payload empty | refuses, "read past end of stream" | refuses, "incomplete input" | refuses, "Offset outside DataView" | refuses (strict tuple), misleading legacy-envelope message |
| B. v1, slot 4 plus a full payload | **loads the payload silently** | **loads silently** | **loads silently** | refuses |
| C. header `cultcache.store.v2` | **ignores the header**; refused only by the empty payload | refuses, misleading "legacy envelopes" message | refuses | refuses |
| D. payload holds the delta as a sparse array | **loads `Name=''`, `Max=99`** | **loads `name=None`** | refuses | loads; `get` of that key fails |
Consequences:
- The **empty payload is the only tripwire every shipped reader trips**, pinned builds
included (Aetheria pins `caching-unity-v1.4.0`). A variant record's payload must be empty,
and the delta must live outside it.
- A payload-as-delta encoding is ruled out: C# and Python misread it silently.
- C# never checks `formatVersion` (`CultDocumentMessagePackSerialization.DeserializeSnapshot`
defaults a missing one to v1). C#, Python and TypeScript skip extra record slots silently.
These are latent defects today and become misreads when the format grows.
- Every refusal is loud but none of them names the record. The target requires named refusals,
so current readers need a small fix (C0).
Other Body facts that shape the map:
- **CultNet has no member-path grammar.** `docs/cultnet-selection-cut.md` D2: "Declared
aliases, not a field-path language". It reaches top-level declared `index`/`role` aliases
only (`CultDocumentMemberView`). Nothing exists to reuse. See section 3.
- **The schema catalog describes top-level members only.** A nested type (`BehaviorData`,
`PerformanceStat`, `HardpointData`) appears only as a CLR type name. Soft drift and hard
rejection cannot see below slot level.
- **Resolution needs a codec, and the core has none.** `GameCult.Caching` holds objects. The
MessagePack codec lives in `GameCult.Caching.MessagePack`. `CultInspectorModel` already
takes the codec as two injected functions (`CultCacheMessagePack.CreateInspectorModel`).
- **Same concrete type implies same home store.** `Home(type)` routes by type, so ruling 3
("same store") falls out of ruling 6. No separate check is needed.
- **The name index shadows silently.** `Index()` does `MapOf(_names, type)[name] = key`, so a
second record with the same name overwrites the entry. A variant that inherits its base's
name would make `GetByName` return one of the two, depending on apply order.
- **Studio edits by whole-document upsert.** `CultInspectorEdit.Commit` calls
`UpsertAsync(type, copy, key)`, and `Duplicate` clones through the codec.
- **Raw readers bypass the cache.** In CultLib: `CultMesh.ReadSingleFileDocument`
(`CultMeshSingleFileDocuments.cs:112`), plus its legacy snapshot decoder. In consumers:
AetheriaEve `AetheriaRuntimeCatalogStore` (directory-store pages).
- **Kotlin reads no store files.** It sees CultNet raw snapshots only.
## 2. Model page (step 0b)
| Kind | Named by | Over time | Decided by |
| --- | --- | --- | --- |
| Variant record | its own `CultRecordKey`, minted like any record; same `schemaId` as its type | created, re-overridden, rebased, removed; never converted to or from a plain record implicitly (Q1) | author through Studio or the variant write API; the cache validates |
| Base reference | the base's key, in the record's variant slot | changed only by an explicit rebase; a base cannot be deleted while it has variants, unless the same batch rebases or removes them | author; the cache refuses a cycle, a missing base, a different concrete type, a global type |
| Override entry | `(op, path, id, value)`; op is Set, Insert or Remove | added by editing; removed by clearing; survives base edits while its path resolves | author; the cache refuses unresolvable entries (Q2, Q4) |
| Member path | a sequence of `(slot, elementId)` steps; `slot` is the MessagePack `[Key]`; `elementId` is `""` unless the step enters an object-list element | a type change can invalidate it | the type's owner (slot authority); ids by the element's creator |
| Element id | a `[CultElementId]` string member on an object-list element type; `""` means unset | minted by the cache on write when unset; copied unchanged into variants; a moved element is a new element | cache mints it; it is never retargeted |
| Resolved view | `CultStoredDocument.Document` for a variant key | recomputed inside the hold that admits the variant, or any base on its chain | the cache only; never persisted, never written back (Q1) |
| Index and name entries | as today, from the resolved view | a dependent's entries are dropped and re-added in the same `Apply` as its base change | the cache; a variant must override the `[CultName]` member (rule R6) |
| Watches | as today; each re-resolved dependent emits `Updated` with its own `Sequence` | published in the same hold as the base change | the cache |
| Compare-exchange | `(schemaId, storedAt)` of the stored delta (ruling 2) | a base change does not stamp its variants | the cache; the resolved view is not an identity |
| Persisted format version | store header `formatVersion` | `cultcache.store.v2` when the store holds at least one variant; `v1` otherwise (Q5) | the writing store, from content |
| Variant slot on the wire (CultNet) | **empty cell**: no CultNet message carries a delta today | C5 | ruling 4 decides deltas; the message shape is C5's design |
| Dictionary entries, scalar lists, arrays | **empty cell**: no stable identity exists | whole-member Set only (Q7) | Q7 |
| Template (non-item) bases | **empty cell**: parked by ruling 6 | none | not in these cuts |
## 3. Where the Body contradicts the target
1. **"A member path in CultNet's grammar"** (ruling 1 and fork 1 text) has nothing to reuse.
CultNet selection deliberately has no path language. This map specifies a minimal slot
path (section 2) owned by `GameCult.Caching`. If CultNet ever needs paths, it can adopt
this one. Nothing is shared today, and nothing is invented twice.
2. **"Overrides are subject to the same soft drift rules as full records"** conflicts with
the ends list: "Loading refuses an override naming a member that does not exist on the
type". Under soft drift, a persisted slot missing locally is ignored with a warning, so
the two rules disagree for a dropped member. Below top level, the catalog has no shape to
drift against at all. See Q4.
3. **"Deletes first"** has little to delete: the feature is additive. The honest deletions
are the silent extra-slot skips and C#'s ignored header. C0 puts them first.
## 4. Operator questions (one batch)
**Q1. A plain `Upsert` of a whole document at a variant's key.** It happens when Studio's
current edit commits, when AetherDb migrations do `batch.Upsert(type, doc, key)`, and on a
CultNet remote write.
- (a) Refuse it loudly. A variant changes only through the variant API. Flattening is an
explicit, separate operation.
- (b) Diff the document against the base and store the difference. Ids make the diff
deterministic, but a value set equal to the base stops being an override without anyone
saying so.
- (c) Flatten silently (today's behaviour by default).
- **Recommend (a).** Affects C1's write path, C3's edit commit, and every AetherDb migration
(each skips variants or uses the variant API).
**Q2. A base edit that removes an element some variant overrides.**
- (a) Refuse the commit unless the same batch clears or retargets those overrides. Studio
offers keep, retarget or clear at edit time, and "keep" means the base edit is not
committed. The store never holds a conflict, and loading refuses one.
- (b) Persist the conflict. The override goes inert, readers get the resolved document
without it, and Studio lists it.
- **Recommend (a).** Option (b) makes a reader's resolved document silently differ from what
the author wrote. Affects C2's validation, C3's rebase UI, and migrations that remove
behaviours.
**Q3. Scope of "ids everywhere".**
- (a) Every object-list element in every registered document type, in all 28 consumers. The
registry refuses a type whose element types lack an id.
- (b) Only document types that opt in with `[CultVariants]`. For those, the registry requires
`[CultElementId]` on every object element type reachable through their lists, including
union bases.
- **Recommend (b).** Option (a) breaks 28 registries for a feature that one consumer uses.
Affects C2 and Aetheria's adoption cut. A `BehaviorData` id needs a base slot above the
subclasses' highest key (32), which adds about 30 nil bytes per behaviour unless Aetheria
renumbers.
**Q4. Schema drift under overrides** (contradiction 2).
- (a) Refuse loading when an override names a member missing at any depth, or holds a value
that will not decode into the member's current type. Record this as a stated exception to
soft drift in `cultcache-schema-compatibility.md`.
- (b) Top-level overrides follow catalog soft drift (ignore with a warning); nested ones are
refused.
- **Recommend (a).** It follows the target's ends and is one rule. The cost: after a type
change drops an overridden member, the catalog must be rewritten before the new build opens
it.
**Q5. Format version.**
- (a) Write `cultcache.store.v2` only when the store holds a variant. Stores without variants
stay byte-identical `v1` for every consumer.
- (b) Always write v2 after the upgrade.
- **Recommend (a).** The empty payload remains the tripwire for pinned old C#, which ignores
the header. Affects C0 and C1.
**Q6. Slice scope.** Should the directory store and CultNet refuse variants loudly until
their own cuts? The directory store's only consumer is AetheriaEve's raw reader. CultNet
must carry deltas (ruling 4), and no message can yet.
- **Recommend yes.** It is two refusals. Otherwise C1 must design CultNet messages first.
Affects C1's size and the order of C5.
**Q7. What is addressable below a member.** The proposal: object-list elements by id only.
Dictionary entries, scalar lists and arrays are replaced whole.
- **Recommend yes.** Aetheria's cases (`Hardpoints` whole; `Behaviors[id]...Max`) fit. Keyed
dictionary addressing can be added later without a format change. Affects C2.
## 5. Cuts, in order
All cuts are in `F:\Projects\CultLib`. Rebase `codex/document-variants` (docs only) onto
`origin/main`. Each cut is one Hands pass on that branch and merges to main after its Soul
pass. Probes show that a C1 writer can merge safely before other runtimes change, because
every reader refuses it. Verification uses `ygg-verify.sh /f/Projects/CultLib <rev> <image> '<cmd>'`.
The command strings below are the `<cmd>`. Keep the job's exit status last.
Overlap: BP-1 (`cultmesh/rust-content-plane`) edits `docs/runtime-parity-scope.md`, which
C0 also edits. Rebase after BP-1 merges. `codex/fix-node24-ajv-esm` has no diff against main.
### C0. Tripwires: every reader refuses what C1 will write, and names the record
Deletes:
- The silent extra-slot skip in the C# record decoder
(`CultDocumentMessagePackSerialization.ReadPersistedRecord`'s trailing `Skip` loop) and
its copy in `CultMeshSingleFileDocuments.ReadLegacyPersistedRecord`.
- The `?? "cultcache.store.v1"` defaulting of the header.
- Python `raw_record[:4]` slicing.
- The TypeScript and Python fallthrough to the legacy envelope decoder when the header is a
`cultcache.store.*` string they do not know.
Adds, one rule in each runtime: a snapshot whose header is not `v1`, or whose record has more
than 4 slots, is refused with a message that names the version or the record key, and says
the store needs a runtime that resolves variants. C# accepts `v1` only, until C1 adds `v2`.
Keeps: every v1 byte and every v1 test.
Verification (rule: "unknown header or extra record slot is refused by name"). Each runtime
gets one test, and each test's negative is fixture B from section 1 (full payload plus slot
4) loading:
- `dotnet`: `dotnet test tests/GameCult.Caching.Tests --filter FullyQualifiedName~PreCut2StoreFormat`
- `node:22`: `npm install --no-audit --no-fund && cd packages/cultcache-ts && npm test`
- `python:3.12-slim`: `pip install -q msgpack -e packages/cultcache-py && python -m unittest discover packages/cultcache-py/tests`
- `rust`: `cd packages/cultcache-rs && cargo test` (message only; Rust already refuses)
Subtraction: about −20 lines of skip and fallthrough; +4 small refusals and 4 tests. Build:
the 4 cache packages only.
### C1. C# core: variant records, top-level Set overrides, resolution in the admitting hold
Adds:
- **`CultPersistedRecord.Variant`**, record slot 4: `[baseKey, overrides[]]`. Payload slot 3
is empty for a variant. An override is `[op, path[], id, value]`.
- Owner: `GameCult.Caching`.
- Consumer: both file stores and Studio.
- Invariant: an old reader refuses rather than misreads (section 1).
- Why no existing owner serves: the payload cannot hold the delta (probe D).
- **`CultStoredDocument.Variant`** (base key and overrides; null for plain records).
`Document` stays the resolved view.
- **A codec port on `CultCache`**: the `(serialize, deserialize)` pair `CultInspectorModel`
already takes, lifted.
- Owner: `GameCult.Caching.MessagePack`, supplied by `CultCacheMessagePack.Create`.
- Consumers: resolution and the inspector model.
- Invariant: one codec per cache; clones and override values use the store's encoding.
- `CultInspectorModel`'s constructor takes the port instead of its own pair, so there is one
pair, not two.
- A cache without a codec refuses variant writes.
- **Write API** on `CultCacheBatch`, with single-call equivalents: `UpsertVariant(key, baseKey,
overrides)`, `Flatten(key)` (Q1a), `Remove` unchanged. Rebase is `UpsertVariant` with a new
base.
- **`Validate` resolves** every admitted variant and every dependent of an admitted or
evicted base, in base-first order, against the post-batch set. It refuses, naming keys:
a cycle; a missing base; a different concrete type; a global type; an evicted base with
surviving variants; an override slot missing on the type (Q4); a value that will not
decode; a variant whose resolved `[CultName]` equals its base's (R6); a plain `Upsert` at a
variant key (Q1a).
- **`Apply` re-resolves dependents** in the same hold. Each emits `Updated` and is re-indexed.
A base change stamps no dependent `storedAt`.
- **Header**: the store writes `v2` iff it holds a variant (Q5a) and reads `v1` and `v2`.
- **Refusals** (Q6): `DirectoryMessagePackBackingStore` push and commit of a variant;
`CultNetDocumentRegistry` row building for a variant; `CultMesh.ReadSingleFileDocument` of
a variant key.
- **Contracts**: the variant record section in `cultcache-persistence-format.md`; the Q4
exception in `cultcache-schema-compatibility.md`; a "variants: C# resolves; others refuse"
row in `runtime-parity-scope.md`.
Authority map:
- Owner: `CultCache.Validate`/`Apply` owns the resolved view.
- Inputs: stored deltas, base records, the codec.
- Outputs: the resolved `Document`, index entries, changes.
- Demoted: the resolved `Document` is no longer a writable owner for a variant key (Q1a).
`storedAt` is the delta's identity only.
- Forbidden writers: store `Loaded` callbacks never resolve; Studio never writes a resolved
document to a variant key; CultNet never emits a variant row.
- Shared paths: a load (`Admit` with a source), a single write, a batch and a remove all pass
the same `Validate` and `Apply`. There is no second resolution path.
Verification. New `VariantTests.cs`; each rule gets a positive and a named negative:
- a load with a variant resolves;
- a base edit updates the variant's `Get`, `GetByName` and `GetByIndex`, and `Watch` sees both
changes inside one publication, with no gap in `Sequence` between them. Negative: a
subscriber observing the base change must already read the new variant.
- `Expect` on the variant survives a base change;
- each refusal case, and the refusal message names both keys;
- the header is `v1` without variants and `v2` with them;
- a read-only store resolves;
- a variant round-trips byte-identically through flush.
Cross-runtime vectors: the Caching interop peer gains `write-variant --file`. A test in
`packages/cultcache-ts/test/cult-cache.test.ts` asserts that TypeScript refuses that file by
name; Python and Rust tests read the same fixture from `contracts/cultcache/variant-store.cc`.
Command, `dotnet`:
`dotnet test tests/GameCult.Caching.Tests && dotnet test tests/GameCult.Networking.Tests --filter FullyQualifiedName~Registry`,
then the C0 commands for `node:22`, `python` and `rust` against the new fixture.
Subtraction: the inspector's own codec pair collapses into the port; otherwise net additive,
estimated +350 to 450 lines of core and +400 of tests. Build: the Caching and MessagePack
projects, Caching.Tests, the Networking test filter, the interop peer.
### C2. C# element identity and nested paths (Set, Insert, Remove)
Adds:
- **`[CultElementId]`** (a string member) and **`[CultVariants]`** (a document-type opt-in,
Q3b). The registry refuses an opted-in type with an id-less object element type reachable
through a list, union bases included.
- **Minting**: on write, the cache mints ids for unset ids in opted-in types, and refuses a
duplicate id within one list.
- **Path steps below top level.**
- **Insert** anchored after an id, where `""` means the head. An inserted run sits
immediately after its anchor and before base elements added there later.
- **Remove** by id.
- **Canonical order**: removes, then inserts in list order, then sets by path.
- **Q2a refusal**: a base commit that orphans an override is refused.
- **Q7**: whole-member Set for dictionaries, scalar lists and arrays.
- **`CultCache.MintElementIds()`**: the one-shot rewrite for existing stores (ruling 1). It
upserts every record of opted-in types through the normal path.
Keeps: the C1 format unchanged. C2 fills path shapes that C1 refused.
Verification:
- Two look-alike elements (two of one union subtype) get different overrides, and a base
insert before them retargets nothing.
- Rebase re-applies overrides by id. A missing id is refused, and the message names the
variant, path and id.
- A Set on a removed element is refused.
- Minting is idempotent.
- An opted-in type without ids is refused; a type that has not opted in is untouched. This is
the negative for Q3b's blast radius.
- A C#-written nested-override fixture joins `contracts/cultcache/`.
Command: `dotnet test tests/GameCult.Caching.Tests`.
Estimate +300 lines and +350 of tests. Build: as C1, without Networking.
### C3. Studio: edit the delta
Adds, in `CultInspectorModel` (engine-free; the lowering stays a renderer):
- For a variant edit, each member path is marked inherited or overridden.
- `Commit` writes `UpsertVariant` with overrides equal to the prior overrides plus a
structural diff by id. It is deterministic because ids exist.
- An explicit "override" pins a value equal to its base's.
- `ClearOverride(path)`.
- `CreateVariant(record)` next to `Duplicate`; it prompts for a name (R6).
- `Rebase(record, base)` returns a conflict list for Q2a before commit.
Lowering, in `CultCacheStudioWindow`/`Drawers`: an overridden marker and a revert control per
member, a base link, and variant creation and rebase controls.
Verification:
- `CultInspectorModelTests`: the diff produces exactly the touched paths; clear removes one
entry; the pin survives an equal value; rebase conflicts are listed.
- Negative: a Studio commit never calls a plain `Upsert` at a variant key.
Only the operator can check these in Unity against a copy of `Aetheria.cc`:
- the markers read correctly;
- clearing reverts the value live;
- a hull variant replaces `Hardpoints`;
- a laser variant changes one behaviour's `PerformanceStat.Max`;
- a base edit shows in the open variant.
Then tag `caching-unity-v1.5.0`. **This is the point where Aetheria is unblocked.**
Estimate +250 model, +150 lowering, +250 tests. Build: Caching.Tests. The Unity package is
checked by the operator in the editor.
### C4. Aetheria adoption (Aetheria repo, its own cut, after the release)
- Bump the package to `v1.5.0`.
- Add `[CultVariants]` to `HullData`, `EquippableItemData`'s concrete types and `ConsumableItemData`.
- Add `[CultElementId]` to `BehaviorData` (base) and `HardpointData`.
- Run `MintElementIds` once over `GameData/Aetheria.cc`.
- AetherDb migrations skip variants or use the variant API (Q1a).
- Re-author the families listed in section 6.
### C5. CultNet deltas (C# reference)
Adds a new raw-record message version that carries the variant slot, following selection D3:
a v1 message string, so peers not in this cut refuse through their own dispatch. The C1
refusal in `CultNetDocumentRegistry` is deleted. Scheduled when a CultNet consumer needs
variants; none does today.
### Parked: TypeScript, Python and Rust resolution
Each runtime gets its own cut when a consumer that reads a variant-bearing store exists
(ruling 5). None does. `Aetheria.cc` is read by C# only.
## 6. First payoff in `Aetheria.cc`
Scan of the live file (206 records, `v1`): records of one schema whose leaves differ by at
most 20 percent, name excluded. This is a heuristic; the operator confirms the real families.
| Type | Records | Families that collapse | Records becoming variants |
| --- | --- | --- | --- |
| geardata | 32 | Refinery / Assembly Line / Deep Ore / Surface Ore / Shipyard (1 to 3 leaves differ); Small / Medium / Large Drive; RevvITup 2.0 / deep space burnout / Talaria; Skiron / Iapyx; Arctica / cold like my heart | 10 |
| weaponitemdata | 18 | ChargeBlast SG / ChargeBlast+- (14 of 253 leaves differ); pswarm / scorched void policy; Earp / 6k Shooter; Spectra / ColdFire | 4 |
| hulldata | 5 | Longinus / LonginusX (only `Behaviors`, `Hardpoints` and two cosmetic members differ) | 1 |
That is 15 of 55 equipment records. In most of these families the difference lies inside
`Behaviors`, so element-level overrides (C2) are what pay. Top-level overrides alone would
still copy each behaviour list.
## 7. Consumers
There are 28 projects: C# 13, Rust 9, TypeScript 10, Python 1, Kotlin 0 (Explore survey;
dependency-level for most, read in detail for Aetheria and AetheriaEve).
- **See variants:** Aetheria through C# `Get`, `GetByName` and `CultRecordRef` (resolved
documents only), CultCache Studio (deltas), and AetherDb tools (must honour Q1).
- **Must stay oblivious or refuse:** every other consumer. No other project reads
`Aetheria.cc`. Raw readers refuse through the empty payload plus C0/C1: `CultMesh`
single-file reads, AetheriaEve's directory-page reader (the directory store refuses
variants, Q6), and Mimir (writer only).

View file

@ -0,0 +1,146 @@
# CultCache Document Variants: Target
Date: 2026-09-17
Status: target, before any substrate map or cut map. This document owns the ends.
A cut map, written after an Imagination pass over the Body, will own the means.
Branch: `codex/document-variants`.
## Why
A CultCache record today is always complete. To make a bigger laser from a
laser, an author copies the record and re-specifies every stat. For a ship hull
with several hardpoint layouts, every layout is a full copy of the hull. When
the base changes, each copy has to be found and edited by hand, and copies drift
apart silently.
The first consumer is Aetheria's catalog (`F:\Projects\Aetheria\GameData\Aetheria.cc`),
edited in CultCache Studio:
- **Hull variants:** one hull with several hardpoint layout options. A variant
replaces the whole `HullData.Hardpoints` member.
- **Item variants:** a bigger laser. A variant changes stats nested inside the
design's behavior list, for example one behavior's `PerformanceStat.Max`.
## Rulings (operator, 2026-09-17)
- **This is a CultCache feature, not an Aetheria mechanism.** Operator: "Yes, this
is absolutely a CultCache feature, write it up in CultLib".
- **A variant is stored as a delta from its base and resolved by the cache.** A
record stores a reference to its base plus a sparse set of overrides. The
cache resolves it; readers see complete documents. The operator chose this
over two alternatives. Authoring-only variants in Studio over flattened stored
copies leave a derived cache pretending to be data. Per-type variant records
in each consumer rebuild the feature bespoke for every type.
## Ends
- **Readers see complete documents.** `Get`, `GetAll`, lookups by name and by
index, watches and snapshots return resolved documents. No consumer outside
the cache and its authoring tools handles a delta.
- **A base change reaches its variants.** Editing a base changes every variant
that does not override the edited value. Watches, indexes and name lookups
observe that change on the variants too, in the same commit that changed the
base.
- **A variant is a record.** It has its own key and name, and references to it
behave like references to any record. In Aetheria a lot's design and a
product's design can name a variant directly.
- **Studio edits the delta.** It shows which values a variant inherits and which
it overrides, and it can clear an override so the base value applies again.
- **Loading refuses a broken variant loudly and names the records.** Cases:
- a cycle of bases;
- a base that does not exist;
- a base whose type the variant's type cannot inherit from;
- an override naming a member that does not exist on the type.
Deleting a base that still has variants is refused.
## Invariants that must survive
- **Wire parity** for the persisted format (`src/GameCult.Caching/Contracts/cultcache-persistence-format.md`,
`docs/persisted-record-encoding.md`). Every runtime that reads a store holding
variants resolves them identically, or refuses the store loudly. Which
runtimes resolve is decided in the map, against
`docs/runtime-parity-scope.md`; none may misread silently.
- **One home store per type**, and atomic single-store commit
(`cultcache-store-composition.md`). A base change and the resulting variant
changes are one commit, not a repair that runs afterwards.
- **Schema compatibility** (`cultcache-schema-compatibility.md`): overrides are
subject to the same semantic identity, soft drift and hard rejection rules as
full records.
- **Compare-exchange** (`Expect`, `ExpectUnchanged`) keeps a single meaning for
variants. The map settles whether it tests the stored delta or the resolved
document; it must not mean both.
## Fork rulings (operator, 2026-09-17)
- **1, granularity: stable element identity, ids everywhere** (operator, 2026-09-17: "ids
everywhere").
- Every object element in a list carries a stable id, minted when the element is created.
This is the approach of sequence CRDTs (RGA, YATA/Yjs, Automerge, Fugue).
- Matching algorithms (Myers, diff3, GumTree) are heuristics. They fail on look-alike
elements such as two `EnergyDraw` behaviors, which is why identity is minted, not
inferred.
- An override addresses an element id plus a member path in CultNet's grammar. An
insertion is a new element anchored after an existing id; a removal names the
removed id. Index addressing is out.
- Rebasing is deterministic. An override whose element id exists in the new base
re-applies. One whose element is missing is an explicit conflict that Studio shows,
to keep, retarget or clear; it is never silently retargeted.
- Existing stores get ids minted once, by a rewrite.
- **2, where resolution lives: at load, kept in memory** (operator: "agreed"). Resolution
computes a variant's complete document from its base chain and its overrides.
- The cache holds both the stored delta and the resolved document.
- A base edit re-resolves its variants inside the same commit, so watches and indexes
observe the change atomically.
- Compare-exchange (`Expect`) tests the stored delta. The resolved document is derived.
- **3, cross-store bases: no.** A variant and its base live in the same store.
- **4, CultNet: deltas.**
- **5, runtimes: C# first.** TypeScript, Python, Rust and Kotlin refuse a store
or snapshot containing variants, loudly, until each implements resolution.
- **6, type change: the same concrete type only** (operator: "agreed").
- An override can only name members the base type has, and rebasing requires the same
type.
- A variant whose type differs from its base's is refused loudly.
- Subtype variants are parked until real content needs a template base. A record
flagged as a template, not a real item, may be the smaller feature.
- **7, clearing and rebasing.**
- Clearing an override removes that member from the variant's stored delta. A member
that is not overridden is not stored.
- Rebasing (changing a variant's base) is supported. Operator: "rebasing is a nice
tool to have, things can get annoying otherwise".
## Open forks, for the Imagination pass to map and the operator to rule
1. **Override granularity.**
- Top-level members are enough for hardpoint layouts.
- A bigger laser needs overrides inside list elements. Addressing list
elements by index is fragile: an insertion into the base silently retargets
every variant's overrides.
- Candidates: stable element identity (for example a behavior's type within its
design); replacing whole list elements only; or addressing paths.
- CultNet's selection work already resolves member paths per leaf type,
inherited members included (`docs/cultnet-selection-cut.md`). A variant's
delta addressing should reuse that grammar, not invent a second one.
2. **Where resolution lives:** at load in the cache core, lazily on read, or as a
materialized view the cache maintains. This affects memory, watch cost, and
the compare-exchange meaning.
3. **Base and variant across stores.** Can a variant in one store name a base in
another (a run-store variant of a catalog record)? This interacts with
one-home-store routing and read-only stores.
4. **CultNet.** Do variants travel as deltas, or resolved? A subscriber that
receives only resolved documents needs no resolver; one that edits needs
deltas.
5. **Runtime scope.** Which of C#, TypeScript, Python, Rust and Kotlin resolve,
and which refuse a store containing variants.
6. **Type change.** May a variant's type be a subtype of its base's type,
adding members, or must the two types match?
7. **Clearing and reparenting.** What clearing an override writes, and whether
a variant can change its base.
## Not in scope
- Scaling or formula variants ("every stat times 1.5"). An override states a
value. Generated authoring can compute values.
- Any Aetheria catalog content. Aetheria adopts the feature after a release, as
its own cut.