2026-09-29 01:34:42 +02:00
|
|
|
# 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;
|
2026-09-29 01:55:09 +02:00
|
|
|
- ~~an override naming a member that does not exist on the type~~ **superseded 2026-09-29
|
|
|
|
|
(operator, variants Q4 c): a dangling override soft-drifts like any dropped member, ignored
|
|
|
|
|
with a warning; see `document-variants-cut.md` section 4.**
|
2026-09-29 01:34:42 +02:00
|
|
|
|
|
|
|
|
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.
|
2026-09-29 13:10:36 +02:00
|
|
|
*Future direction (operator, 2026-09-30, from Aetheria moddable ships):* "we'll want cross-store variant
|
|
|
|
|
support eventually; modders should be able to add variants of existing items without modifying the
|
|
|
|
|
original". Ruling 3 holds for this campaign. A mod store layering variants over a shipped catalog is the
|
|
|
|
|
named future consumer that would reopen it. Until then, no consumer builds its own cross-store resolver:
|
|
|
|
|
Aetheria's compose step stays out of resolution.
|
2026-09-29 01:34:42 +02:00
|
|
|
- **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.
|