# CultCache Migration Postmortem Written 2026-09-15. It covers Aetheria and CultLib from the decision to migrate (2026-09-12 18:03Z) to Aetheria `58b27690` and CultLib `a0813c6` (2026-09-14 23:33 local). Related documents: - Means: [`cultcache-migration-cut.md`](cultcache-migration-cut.md) - Ends: [`cultcache-migration-target.md`](cultcache-migration-target.md) - Evidence behind this document: the main session transcript `2a6aec4d-7cba-481c-8586-be54662389a5.jsonl` and 114 subagent records. - The process this migration produced is formalized as the Claude Code skill `~/.claude/skills/eureka` (Eureka, the Claude Code counterpart of Epiphany). ## Summary Aetheria's in-tree CultCache, the operator's original 437-line cache, has been replaced by CultLib's modern CultCache. Along the way it gained CultMath in place of Unity.Mathematics. Every piece of game state now lives in typed `.cc` stores with one home store per type: - **Catalog** (`GameData/Aetheria.cc`, read-only at runtime): authored data. - **Run:** the current playthrough. - **Player:** settings and bindings. Four private file formats are gone, along with the vendored MessagePack, the vendored JsonKnownTypes, and the IMGUI Database Tools. CultCache Studio edits the catalog. **Scale:** - Aetheria: +5,652 / −36,920 lines, net −31,268. - CultLib: about +15.7k / −13.35k attributable to the migration, most of the growth being CultMath's generated HLSL surface and the new tests. - 114 subagents ran: 50 Hands, 32 Soul, 3 Imagination, 23 Eyes/Explore, 6 Mind Steward. **Still outstanding with the operator:** - the play smoke. The Studio click-through passed on 2026-09-17. It also found that the cutover had dropped the Database Tools' record grouping and drag-and-drop without recording them, because the feature inventory counted attributes only. **CultLib missed its own size target.** The Caching core ended at 4,368 lines against a target of about 3,150, and `CultCache.cs` at 2,265 against about 1,000. The ledger was never reconciled. ## Scope and invariants The target was a **cache-only** migration. - An earlier attempt, rolled back by `7006a6b0`, had failed because it bundled CultCache, CultMesh, Eve and daemons into one change. - AetheriaEve was ruled out as a consumer early ("preserved for taxidermy"). - CultMath was the only scope added mid-migration, by operator ruling ("it's the best workout we can give it"). Invariants carried to the end, each pinned by tests (listed in the Eyes fact sheet): - Wire parity across runtimes, with C# as the reference. - One home store per type, in C#, TS, Rust and Python. - Loading never writes. - The cache never invents globals; lifecycle owns them. - Runtime type decides schema. - Atomic single-store commit plus compare-exchange (proven in-process only). - No observer runs under the cache gate; order is carried by a per-cache Sequence. - Catalog read-only at runtime. - ServerShared builds headless with no UnityEngine. ## Timeline | Cut | Where | When (local) | Soul passes | Notable | |---|---|---|---|---| | 0 target + map | Aetheria `68fbce4a`..`82c1e72e`, second pass `c3377f11`..`040e50b2` | 09-12 23:12 to 09-13 15:52 | none (Imagination) | Fable ran out mid second pass | | 1 contract + red tests | CultLib `37f73c4`..`0db1fe5` | 09-13 16:01 | 1 | SoA parked at `parked/cultcache-soa` | | 2 subtraction | `d904960`..`14d7c64` | 09-13 16:21 to 17:17 | 1 | −2,533 lines; 5 kept durability invariants had lost their tests | | 3 routing, atomic commit, Sequence | `a3cc394`..`825b4f7` | 09-13 17:57 to 21:02 | **6** | deadlocks, a lost write, a ticket system built and then deleted | | 4 serialization, generator deleted | `6fcc2a7`..`5bd4e4e` | 09-13 21:09 to 09-14 00:21 | 3 | keyed get-only data loss | | 5 TS/Rust/Python | `8dd5a45`..`47c7111` | 09-14 00:37 to 12:43 | 4 | "BROKEN against its own contract" on pass 1 | | 6 Studio on an engine-free model | `8be174b`..`741d212` | 09-14 13:02 to 14:55 | **4** | unloadable-store key, IMGUI split authority | | 6b CultMath HLSL parity | `d514efe`..`d4c43ed`, merged `1b95dd6` | 09-14 13:29 to 14:57 | **4** | `step` NaN taken from docs rather than dxc | | 7 release | `546c919`, `ae194d5` | 09-14 15:16 | **0** | the tag push silently skipped publish | | CultMath 0.2.1 to 0.2.3 | `f060536`, `111b918`, `0f2c1f0` | 09-14 16:16 to 18:19 | via 8a | gaps filled in the owner, not the consumer | | 8a CultMath swap | Aetheria `4f4beb0f`..`2b38d2dc` | 09-14 17:38 to 18:29 | 2 | SectorMap pivot, degenerate zone seeds | | 8b data model | `b4c06b50`..`9bdf6ef2` | 09-14 18:42 to 19:17 | 1 | Unity attached no run store | | CultLib 1.0.58 / 1.0.59 | `b85a828`, `a0813c6` | 09-14 21:01 to 21:35 | 1 + 1 | nil map key broke Python | | 9 importer | `70fbaca1` | 09-14 21:11 to 21:23 | 1 | dangling refs traced to 2021 deletions | | 10 runtime cutover + presets | `4a545469`..`58b27690` | 09-14 21:43 to 23:33 | 3 | untested operator rulings; catalog writable in editor | The whole migration took about 48 hours of wall clock, including one night. ## Structural delta **Aetheria.** - The ledger estimated −33,000; actual is −36,920 / +5,652 (net −34,388 excluding docs). - Cut 8b alone removed 35,989 lines, of which 31,293 were vendored MessagePack and `Unsafe.dll`. - Added: - three UPM git pins; - a CultLib pin guard; - `Aetheria.Shared.Tests` (29 tests); - `AetheriaStores`, `RunSave`, `Loadout`, `StableHash`; - `GameData/Aetheria.cc` under LFS. - 20 document schemas replace one root union. **CultLib.** - Removed: - the MessagePack generator and the empty Analyzers project; - SoA (parked, not deleted); - the managed document; - ambient transactions; - `initializeGlobals`; - the TS/Rust/Python mirrors; - the v2/v3 directory formats; - typed serialize helpers; - the Studio's by-name reflection bridge; - the composite constructor heuristic; - the `soft` flush flag in core. - Added: - `Commit(Action)` with `Expect`/`TryCommit`; - a per-cache Sequence; - `CultInspectorModel`; - CultMath's HLSL surface, Unity bridge, pinned dxc, and PCG hashes; - `Contracts/cultcache-store-composition.md`. **The core size miss has visible causes.** Soul-driven gate and publication rules, compare-exchange, and the registry absorbing the generator's rules all added lines, and the inspection model moved into core. None of these was weighed against the target. The ledger stopped being updated after Cut 4. ## What Soul caught 32 Soul passes ran; 29 found at least one real defect that Hands' tests and report had passed. The tally is roughly 75 defects. They fall into a few families. **Concurrency.** Five deadlocks, stalls or hangs: - Cut 3's pull/upsert deadlock on directory stores, which hung 3 of 3 runs. - A ticket-ordered delivery that deadlocked through `CultNetDatabaseSubscriptionServer._lifecycleGate` and could stall a cache forever. - TS's per-cache queue deadlocking when a store awaited its own cache. - TS's AsyncLocalStorage re-entry guard, which both over-refused and missed escaping waits. Each of these had passing tests. Cut 3's race tests were red only on Windows, by accident. **Lost writes and data loss.** - A re-pull erased a staged write the contract said was safe. - A shared unpublished-changes list let one throwing observer discard other threads' committed changes. - Keyed get-only members were silently skipped. - Studio Save deleted presets captured during play. **Wire parity.** - 1.0.58 wrote an unset `CultRecordRef` dictionary key as nil, which Python's msgpack rejects under `strict_map_key`. It was released, then corrected 74 minutes later in 1.0.59. - A Python global stored under a legacy key made the cache unloadable. **Semantics taken from the wrong authority.** - CultMath's `step` NaN rule came from documentation. dxc compiles it the other way. - The matrix row indexer silently wrote to defensive copies. - The HLSL mirror "compiled as C#" claim was false at first. - The mirror test later proved only compilation, not values. **Lifecycle and ownership.** - **8b.** Unity attached no run store, so every run write threw. Hands had described those paths as "upserting orphans". Separately, `PlayerSettings.SavedRun` was a second owner of the run root. - **10.** The catalog was read-only in the editor only by convention. It had been opened writable for preset capture. - **Split authority recurred at small scale.** Affordability stayed in the menu after the charge moved into `Materialize`. The IMGUI Studio kept key, element and narrowing decisions after the model was supposed to own them. **Untested operator rulings.** - **Cut 10.** Removing the availability rule, the price rule, or all-or-nothing on a failed fit left all 15 tests green. Hands' "each new test fails under its mutation" covered only the tests Hands chose to write. - **Cut 6.** 3 of 7 inspection-model tests survived removal of their own rules. - **Cut 2.** Five durability invariants that were kept had lost their tests along with the stage probes. **Behaviour the captures could not see (8a).** - `sign` returns `int`, so SectorMap's `sign(x)/2+.5f` became integer division. - CultMath's shader-style `hash` gave 1,625 distinct zone seeds in 10,000 positions. **What these share.** The defect was invisible at the layer where the author checked: unit tests in one runtime, captures that did not exercise the code path, or a rule read from a document instead of the compiler. Soul found them by checking at the layer where each would actually fail: another runtime's decoder, dxc's lowered IR, Unity's decompiled `EditorGUI`, an independent decode of the legacy file, or a scratch probe over every document type. **The pass that did not run.** Cut 7, the release, got no Soul pass. Its problems surfaced late: - stale committed DLLs; - a commit SHA baked into release byte checks; - a native DLL rebuilt nondeterministically; - a tag push that silently skipped publishing. Hands or later Soul passes found each of these, but after tags were already pushed. ## Operator corrections 35 rulings or corrections are recorded, with quotes, in the Eyes fact sheet. The ones that matter here are those where the agents' model was wrong. For each, the question is what context was missing. - **Defunct neighbour repos treated as parity runtimes.** No doctrine said where canonical runtimes live. Fixed at the source: `F:\Projects\CLAUDE.md` now names `CultLib\packages\` as the only home. - **Imagination stated cause and effect it never traced** ("the effects you listed don't follow from the causes", 09-12). The map made mechanism claims from names. From then on, the brief standard required probes for every mechanism claim. - **Mirrors and non-decisions.** Replication had no consumers, and "3-5 are not decisions". The consumer audit came after the questions instead of before them. - **"Soul pass on the cut map"** was a faculty confusion. It is now in doctrine: a pass over a plan is Imagination. - **`initializeGlobals`.** The cache invented globals because Aetheria had a lifecycle problem. The operator named the real owner: lifecycle. - **Style bar.** "Does the current code look *anything* like that". Four months of drift had nobody holding the original design up for comparison. The original 437-line cache became the explicit bar. - **Atomic commit and compare-exchange** were slated for deletion as unused in C#. The operator knew they had been needed across Ghostlight and Epiphany. A consumer count limited to C# undercounted a capability the operator uses across projects. - **Generator** ("do we even need a generator anymore?"). Once MessagePack became canonical, the generator only duplicated the registry's rules. The map had kept it because it existed. - **Tickets, then Sequence** ("an itch regarding authority here"). Order was first carried by scheduling, as tickets that serialized delivery. The operator preferred order as data. A whole ticket subsystem was built, found deadlocking by Soul, and deleted. - **CultMath "drop-in" and the Unity license.** Both claims were wrong until checked, by audit and by the operator. HLSL, not Unity.Mathematics, was the real parity target, and only the operator knew that. - **Composite heuristic** ("what a silly and fragile idea"). Hands invented a constructor-matching guess to fill a drawer gap. It had no owner and no consumer. - **Loadouts, three definitions in one evening.** 1. The spec made loadouts player-store builds with a product per slot. 2. "Blueprint means design": no manufacturer. 3. "Think variants in Mechwarrior": authored catalog presets, not player-facing. Each redefinition reworked shipped code. The missing context was **product meaning**. Nobody asked what a loadout is *for* before specifying its store, its UI and its price. Imagination inferred the purpose from a broken legacy menu. ## Incidents - **Probe fork bomb, 2026-09-13.** An Imagination probe (`cc-cas-probe`) relaunched `Environment.ProcessPath` with the dll as an argument, so every child re-ran the parent. Starfire needed two hard reboots and survived a third process storm only through a cancelled shutdown. It cost about two hours and woke the operator's household. PCIe ASPM was blamed first. The probe (P7, the two-process race) never produced a result, so cross-runtime lock exclusion is still unproven. The operator ruled out process-spawn policing. The kept lesson is narrow: read how a child chooses its role before running launch code. - **Fable exhausted before Cut 1** (HTTP 429 at 13:51Z on 09-13). The plan had Fable on Imagination and Soul. Only 1 of 114 agents ran on Fable. Opus carried every other pass, including all 32 Souls, and the Soul hit rate held. - **Permission classifier.** It blocked a Hands edit, then a Hands `git add -A && git commit -F - <`), Epiphany and CodexConnector (Rust `Result`, `push_all`), VoidBot (TS promises), Heimdall. - **Aetheria:** - No spawner uses presets yet (candidates: `ZoneGenerator.cs:309`, `ActionGameManager.StartGame`). - Hardpoint-configuration variants. - New Game clears the old run before generation succeeds. - `LookRotation` NaN is unpinned. - **Release:** npm `cultcache-ts` 0.14.0 is tagged and unpublished by choice; registry publication is a later pass. Unity 1.0.47 to 1.0.56 were never tagged.