From 68fbce4a8a59a8835f208ddb60b2185d5a159f94 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Sat, 12 Sep 2026 23:12:42 +0200 Subject: [PATCH] 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 --- docs/aetheria-perfect-machine-map.md | 1 + docs/cultcache-migration-target.md | 164 +++++++++++++++++++++++++++ 2 files changed, 165 insertions(+) create mode 100644 docs/cultcache-migration-target.md diff --git a/docs/aetheria-perfect-machine-map.md b/docs/aetheria-perfect-machine-map.md index 54371d340..7e3b8ab60 100644 --- a/docs/aetheria-perfect-machine-map.md +++ b/docs/aetheria-perfect-machine-map.md @@ -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 diff --git a/docs/cultcache-migration-target.md b/docs/cultcache-migration-target.md new file mode 100644 index 000000000..0668c74f2 --- /dev/null +++ b/docs/cultcache-migration-target.md @@ -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`**, resolved through an explicit cache. + `DatabaseLink`, 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`/`UpsertAsync` + type parameter stops choosing the schema. +- **Typed lookups honor inheritance.** `Watch`, `GetByName`, + `GetGlobal`, `GetByIndex` match assignable types, as `GetAll` and + `Get` 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`, 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()`, `Get(key)` | Pass | +| `UpsertAsync(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` 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` 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`, `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.