Define the CultCache migration target

Aetheria moves onto modern CultCache from CultLib, cache only. States the end
state for Aetheria and CultLib, the wire-parity and foundation invariants, the
probe evidence, and the questions the Imagination pass must answer before any
cut. Points the parked perfect-machine map at it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Meta Vens 2026-09-12 23:12:42 +02:00
commit 68fbce4a8a
2 changed files with 165 additions and 0 deletions

View file

@ -6,6 +6,7 @@ Date: 2026-06-12
commit `7006a6b0`; the rebuild lives on in `F:\Projects\AetheriaEve`. Current
direction for this repo is [three-gates-scope.md](three-gates-scope.md). Read
this document as a record of the deferred plan, not as current direction.
The cache move alone is now [cultcache-migration-target.md](cultcache-migration-target.md).
This is the Proprioception and Imagination pass for turning Aetheria from a
Unity project with ancestral cache/network/UI machinery into a coherent

View file

@ -0,0 +1,164 @@
# CultCache Migration Target
Date: 2026-09-12
Status: target defined. The cut map is owed by the Imagination pass
(`docs/cultcache-migration-cut.md`); nothing is cut until it exists.
Aetheria moves off its in-tree CultCache, the ancestor of the one in CultLib,
onto modern CultCache in `F:\Projects\CultLib`. Adopting the infrastructure the
rest of GameCult maintains replaces a private copy with a shared one. This
document states the end state and the invariants that constrain getting there.
## This touches the foundation
CultNet is built on CultCache, and CultMesh on CultNet. Every CultLib change
below lands under all GameCult infrastructure, not just Aetheria.
- **Wire parity is an invariant.** CultCache maintains wire parity with the
other runtimes (`F:\Projects\cultcache-rs`, `cultcache-py`, `cultnet-rs`,
`cultnet-ts`, `CultLib\packages\cultcache-ts`). A change that alters bytes on
disk or on the wire must land in every runtime, or it does not land.
- **C# is the reference runtime.** The ergonomics arose there; the other
runtimes follow its contract. Contract changes are written down in
`CultLib\src\GameCult.Caching\Contracts\` first.
- **Existing consumers must survive.** AetheriaEve pins CultLib by commit,
Aquarium attaches a single store, Heimdall and Idunn carry copies.
## Why the previous attempt failed
The first migration (rolled back by `7006a6b0`, plan in
`aetheria-perfect-machine-map.md`) made the cache move phase 2 of seven,
bundled with an `Aetheria.State` service layer, CultMesh hosting, Eve UI, and
an Electron RTS client. The cache never finished moving. This migration moves
the cache and nothing else.
## Target state: Aetheria
- **All persistent state is CultCache state**, split by lifecycle into routed
stores inside one cache:
| Store | Holds | Written by | Lifetime |
|---|---|---|---|
| Catalog `GameData/Aetheria.cc` | Items, factions, products, name files, other authored records | CultCache Studio | Read-only at runtime |
| Run `.cc` | Galaxy, zones, bodies, orbits, entities, tasks | The run save path | Deleted on death |
| Player `.cc` | Settings, bindings, account | The game | Persists across runs |
- **Documents versus values.** A record with identity that something references
or that changes independently is a `[CultDocument]`. Values without identity
(item instances in cargo, behavior lists on a design, weapon groups) live
inside their owning document and keep their MessagePack `[Union]`s.
- **References are `CultRecordRef<T>`**, resolved through an explicit cache.
`DatabaseLink<T>`, the static `DatabaseLinkBase.Cache`, `DatabaseEntry`, its
union, and `DatabaseEntry.ID` are gone. A catalog record's identity is its
record key, which carries the legacy Guid string.
- **The live simulation owns in-play state.** `Entity` and its reactive fields
are not documents. Run documents are written at explicit save points through
one save path shared by quit, zone transition, and death.
- **Legacy data is imported once.** A one-shot importer in `tools/AetherDb`
reads `AetherDB.msgpack` and `GameData/NameFile/*.msgpack` by union tag, maps
each tag to its new document type, and writes records keyed by legacy Guid.
Slot numbers are preserved. Counts are checked against `census`, then the
legacy files are deleted. No legacy reader survives in the runtime.
- **Existing saves are discarded.** No save converter.
- **Deleted from Aetheria:** `Assets/Scripts/ServerShared/CultCache/`, the
vendored `Assets/Plugins/MessagePack`, vendored JsonKnownTypes, the legacy
Guid/Type resolvers, and the IMGUI Database Tools under
`Assets/Scripts/CultCache/Editor/`.
- **Consumption.** Unity takes the full `org.gamecult.cultlib` UPM package
pinned by git tag. `Aetheria.Shared/Aetheria.Shared.csproj` references the
same CultLib version, so the headless build still proves the simulation
compiles without Unity.
## Target state: CultLib
- **Store routing replaces replication.** Each document type has exactly one
home store, chosen by the record's runtime type (most specific assignable
route wins). A cache with one store and no routes behaves as today. Overlapping
routes, writes of an unrouted type, a record loaded from a store that is not
its home, and attaching a store after hydration are errors. Multi-store
replication is deleted: no consumer uses it, and transactions already refuse
more than one store.
- **Loading never writes.** Store dirtiness derives only from mutations.
- **Read-only stores.** Writes routed to a read-only store throw.
- **Transactions** require every mutation to route to one store, replacing the
rule that a transacting cache may attach only one store.
- **Runtime type decides schema.** The generic `AddAsync<T>`/`UpsertAsync<T>`
type parameter stops choosing the schema.
- **Typed lookups honor inheritance.** `Watch<T>`, `GetByName<T>`,
`GetGlobal<T>`, `GetByIndex<T>` match assignable types, as `GetAll<T>` and
`Get<T>` already do.
- **Consumers can serialize their value types** (Unity.Mathematics, and whatever
replaces `System.Type` fields) without breaking wire parity.
- **CultCache Studio absorbs Database Tools:** dictionaries, polymorphic
`[Union]` fields, Unity.Mathematics types, a record picker for
`CultRecordRef<T>`, and a custom drawer extension point.
## Evidence
Runtime probe, 2026-09-12, CultLib `main` at `c2a9a6e`, generator and
reflection builds, write then flush then reopen:
| Case | Result |
|---|---|
| Nested `[Union]` field inside a document | Pass |
| Document inheriting `[Key]` fields from an unattributed abstract base | Pass |
| `GetAll<AbstractBase>()`, `Get<AbstractBase>(key)` | Pass |
| `UpsertAsync<Gear>(new Weapon())` | Fail: stored and reloaded as `Gear`, `Weapon` fields lost silently |
Code-read, not probed: exact-type `Watch`/`GetByName`/`GetGlobal`/`GetByIndex`;
no consumer formatter resolver registration (fixed static options); the
generator never emits a codec for hierarchies and silently falls back;
CultCache Studio lacks dictionaries, unions, math types, and a ref picker.
MessagePack 3's analyzer (MsgPack005) rejects `[MessagePackObject]` on an
abstract type without `[Union]`; Aetheria's abstract `ItemData`,
`CraftedItemData`, and `ItemInstance` must change.
## Invariants
- Wire parity across runtimes; C# remains the reference.
- CultNet and CultMesh behavior is preserved or deliberately changed with every
runtime updated.
- Loading a store never writes it.
- Every document type has one home store; its runtime type decides schema and
route.
- `ServerShared` compiles headless with no UnityEngine reference.
- Legacy formats are read only by the one-shot importer.
- The migration does not bundle CultMesh hosting, Eve UI, daemons, or a state
service layer.
## Out of scope
CultMesh and Eve adoption in Aetheria, R3 for gameplay (UniRx stays), removing
Newtonsoft attributes from the 82 files that carry them, and a state service
layer.
## Open questions for the Imagination pass
1. Is routing a C# cache behavior only, or a semantic every runtime's cache must
match? What do the Rust, Python, and TypeScript caches do with multiple
stores today?
2. How do Unity.Mathematics values encode so other runtimes can read them? Does
CultMath already define portable shapes?
3. What replaces `System.Type` fields (`StatModifierData.RequireBehavior`),
which have no portable encoding?
4. Do CultNet or CultMesh depend on multi-store replication, on the generic
`UpsertAsync<T>` declared-type behavior, or on exact-type watches?
5. Can existing `.cc` stores in other projects hold records written under a
declared parent schema, and what does fixing `UpsertAsync<T>` do to them?
6. Which consumers pin which CultLib revision, and what is the release and
upgrade order?
7. Where exactly do Aetheria's world types fall on the document/value line, and
where are the save points?
8. How do catalog identity comparisons (`DatabaseEntry.Equals` on ID,
`Dictionary<Faction,…>`, `Hull.Data.LinkID == data.ID`) survive without
`DatabaseEntry.ID`?
## Process
A mini Epiphany loop. Imagination (a Fable agent) maps the cut into
`docs/cultcache-migration-cut.md`: authority maps, sequenced independently
executable cuts, deletion lines, negative checks, parity verification, and
build budgets. Hands (Opus agents) execute the cuts. Soul (a Fable agent)
verifies each against the map and the invariants above.