40 KiB
Item Provenance: Schema Cut Map
Date: 2026-09-17
Status: Imagination pass, cut map. The ends are in docs/item-provenance-target.md; the
Body and the fork history are in docs/item-provenance-substrate.md. This map owns the
means of the schema cut only. Generation, station roster, recipes, routes, piracy and
factory tooling are the next scope and get their own map.
Progress (Self updates this in each cut's landing commit):
-
Work lands on branch
codex/item-provenance, cut from7425ff1b. -
Cut A landed at
69801bbband passed Soul with no findings. Evidence:Aetheria.SharedandAetherDbbuild; 29 tests pass; the four negative greps are empty.- The open editor's recompile after the edit logged 0
error CS, which stands in for the batchmode compile. - Temperature and Armor now restore unconditionally. That is safe, because
Entity.MapEntityinitializes both beforePack.
-
Cut B landed at
415f8e11,76c331e9and1e647953. Soul's findings, awaiting a fix batch:- F1, critical and pre-existing (also on
codex/cultcache-cutover): no run store holding an entity can be reopened.- Cause:
EntityPack.PersistedBehaviors(EntitySerializer.cs:200) is keyed byint2, andCultMessagePackSecurityhas no hash-resistant comparer for it. - Effect: after the first save,
AetheriaStores.Openthrows before the main menu. - It is the only failing key type among the persisted types.
- The fix shape is an operator fork.
- Cause:
- F2: the reachability test reads through
EntitySerializer.Items, so dropping the Equipment, CargoBays, DockingBays or CargoContents roots survives. - F3: the single-tier fixture cannot fail
GetTieror a re-roll on mint. - F4:
GetPrice, the durability and thermal exponents, role fills, aProducedbrand, and first-zone-only roots have no tests. - F5:
tests/mutation_tests.pyanchors assume LF, and it counts compile errors as kills. - F6: a stale
SavedGame.cs:124comment. - With F1 bypassed in scratch, all 34 tests pass.
- F1 ruling (operator, 2026-09-17: "ruling A is fine"):
PersistedBehaviorsbecomes an array of(int2 position, PersistentBehaviorData[] data), matchingEquipmentandCargoBays. It lands on this branch;codex/cultcache-cutoverkeeps the defect until this branch merges.- Recorded CultLib follow-up: CultCache should refuse, at type registration, a persisted dictionary key with no hash-resistant comparer, so a type that can be written but never read is loud on day one.
docs/headless-playground-cut.mdCut 0 is this same fix.
- Fix batch landed at
e4df73e4,4fc9070b,cb2febec,561c5b85,080d3b49andc2ff5431. Evidence:- 42 of 42 tests pass.
- The mutation script ran on CRLF: the control stayed green and 16 of 16 mutants were killed.
- Soul passed the batch.
- A real-catalog probe reopened a committed run holding 46 generated instances.
- Every persisted dictionary key is now
intorCultRecordRef. - All seven GC-root mutants are killed, and 42 of 42 tests pass in a clean worktree.
- Follow-up (low): one run of
mutation_tests.pyreported a transientERRORon its last mutant, and a rerun was clean. Treat anERRORas inconclusive and rerun. Record it again if it recurs.
- Persisted behaviors deleted at
8fe09a23, a pure deletion: −101/+4 lines, with key 5 retired and commented. Evidence:- 41 of 41 tests pass;
ReopenSurvivesAGeneratedShipWithEntitieswas kept. - The negative grep is empty, and the Unity code has no references.
- The mutation run: control green, 17 of 17 killed.
- Self read the diff; because it is pure deletion, no separate Soul pass.
- 41 of 41 tests pass;
- F1, critical and pre-existing (also on
-
Cut C landed at
333a7370(code) andb2036fb8(catalog rewrite).- Soul passed it.
- A reflection dump of all 180 records before and after shows zero content changes.
- The game opens the catalog read-only;
danglinglists 0. - No reader of a design's manufacturer remains.
- An old
run.ccfails loudly on lot 0. - The batchmode log compiled the Cut C HEAD, which covers Cut B's Unity edits.
- Two low findings, fixed in a follow-up:
- Census's per-design maker is a second rule: the first product from any maker.
It diverges from
ItemManager.Brandwhen two factions sell one design. Brand's tie-break by record key has no test (OrderBy→OrderByDescendingsurvives).
- Census's per-design maker is a second rule: the first product from any maker.
It diverges from
- Fixed at
fc1c0526and5e9634ef:- Census lists every maker selling a design; its output is byte-identical today.
- A same-maker tie-break test was added.
- 42 of 42 tests pass.
tests/mutation_tests.pyholds 19 entries: the control stays green and all 18 mutants are killed.
- Count correction: two earlier reports counted the control as a kill. At
8fe09a23the script held control + 16 mutants, not 17, and atb2036fb8control + 17, not 18. No mutant verdict changes.
- Soul passed it.
-
The schema cut is complete. Remaining: the operator's play smoke, and merging
codex/item-provenancetogether withcodex/cultcache-cutover.- 115 records rewritten: 12,686,267 → 12,683,061 bytes, 0 ignored slots.
- The
AetherDboutputs, including census, are byte-identical: no design's maker disagreed with its products' maker. - 41 of 41 tests pass; mutations: control green, 18 of 18 killed.
- Unity batchmode reports 0
error CS(log in the session scratchpad). - Open, found while fixing F1: nothing implements
IPersistentBehavior, soPersistedBehaviorshas no production writer or reader. To exercise the wire shape, Hands added aMarkerPersistentBehaviorDataunion case. That is a production type that exists only for tests.- Ruled: delete the feature (operator, 2026-09-17). "eh, fine, get rid of it, I'm sure there's a better way to store it than the setup I had, anyway".
- Deleted:
PersistedBehaviors(key 5 retired),IPersistentBehavior,PersistentBehaviorData,MarkerPersistentBehaviorData, and the pack/restore code with its tests and mutants. - Behavior state that must persist gets designed later, with a real behavior as its first consumer.
- The pairs-array ruling still stands as the shape for position-keyed persisted maps.
-
Repo:
F:\Projects\Aetheria, branchcodex/cultcache-cutover, anchors against HEADb0df689d. Commits since the substrate map (dbe1dd83) touch only the three docs, and every anchor used below was re-read at HEAD. -
CultLib pin:
a0813c6eed24d30bf88073ef615b633c77ebfcd6(Directory.Build.props:5). Every headless command below takes-p:CultLibRoot=<clean detached worktree at a0813c6>. -
Baseline at HEAD, measured:
dotnet test tests/Aetheria.Shared.Tests29 passed;AetherDbcensus,factions,station-fit,loadout 1exit 0;hardpoint-fitexits 4 (already failing at HEAD, not this cut's to fix).
0. Rulings applied, and how they were read
Settled (operator, 2026-09-17), applied as written: F0 (a) placeholder origin; F1 (c)
one run-store ledger document with GC in RunSave.Commit; F4 (a) quality per lot per
role, stored; ItemData.Manufacturer leaves the design; branding derived, never stored;
factories are built in place (next scope; the Produced shape must be able to say it).
Readings this map makes, each checkable against the target's quotes:
FactionProductData.RolesandProductRolestay. The substrate's §11 cut them. The F0 ruling reverses that: generation "starts from branded items with specific role spreads" and synthesizes provenance "to hit those properties". A product's per-role mean/SD is that spread. It stays as the brand's authored generation input, read by the one lot-minting function. Not a fork.- The derived brand is shown where the design's manufacturer was
(
PropertiesPanel.cs:371-372). It is the brand projection's only game reader; without it the projection has no reader. - Existing saves are discarded still holds for the run store.
GameData/run.ccdoes not exist in the working tree today. An old run store would fail loudly on Continue (ProvenanceLedgerlookup of lot 0 throws); no gate or converter is added. The catalog is rewritten in Cut C and checked byAetherDbcaptures. - Deleted per the substrate's defaults: the
instantiatere-mint path andTradeMenuDebug.
1. Body re-verified; where it disagrees with the substrate map
All substrate anchors used here hold at b0df689d (ItemInstance.cs:33-53,
ItemManager.cs:70-90,108-197, LoadoutGenerator.cs:35-110,189-269,
Loadout.cs:95-158, EntitySerializer.cs:59-137,179-196, Entity.cs:1085,1212-1225,1261,1290,
SavedGame.cs:96-124, AetheriaStores.cs:10, ActionGameManager.cs:264,499,540,735-739,
PropertiesPanel.cs:371-372, ItemData.cs:272,280-281). MainMenu.cs clears the run at
:112, not :111.
Contradictions and corrections found in the Body:
TradeMenu.csandZoneRenderer.csneed no edit. The substrate's file list names them. Their readers areGetPriceandGetTier, whose signatures stay (CraftedItemInstancein); only the bodies move to the lot.SimpleCommoditygets no lot, and stacks need no lot rule. The substrate putLoton theItemInstancebase with a merge-within-lot rule atEntity.cs:1498,1576. Nothing mints aSimpleCommodityonceInstantiategoes: the only caller ofCreateInstance(SimpleCommodityData, int)isItemManager.cs:119, and mining's mint is commented out (Zone.cs:298-304). A base field would have no writer.Lotgoes onCraftedItemInstanceat key 11, whichSimpleCommoditydoes not use (it uses 2). Moving it to the base later with the same key is byte-identical (probe Q4), so mining yield adds without reshaping.TradeMenuDebugis dead: its script GUID6eed32eb39c04a31b81e8a55685b222cappears in no.unity,.prefabor.asset, and no.csnames the type.- Every one of 115
ItemDatarecords carries slot 3 in its persisted schema, not only the 37 with a manufacturer set. After removal, all 115 load withignored_extra_slotuntil rewritten (probe Q1). - The catalog has exactly one product per design (37 products, 37 distinct designs, 0
designs with more than one maker, 0
(maker, design)duplicates, 0 products with an unset maker, 0 authored role spreads, 0 designs with roles; probe Q1). The brand projection keyed by(maker, design)is unambiguous on today's data. TheLoadoutTestsfixture is not:FirstAvailableProductInKeyOrderBuildsTheSlot(LoadoutTests.cs:289-312) adds two same-maker Lamp products and reads.Product. ItemManagerlives one scene load, and every run starts with a scene load. It is constructed inActionGameManager.Start(:264). There is one scene,ARPG. New Game and Continue both end inSceneManager.LoadScene("ARPG")(MainMenu.cs:106,139,167), and noDontDestroyOnLoadexists. So a ledger held byItemManagerspans exactly one run.- Aside, not this cut:
CreateInstance(CraftedItemData, float)mints aCompoundCommodityfor aConsumableItemDatadesign (ItemManager.cs:136-141), whileTryActivateConsumablecasts toConsumableItem(Entity.cs:336). This map keeps that behaviour byte for byte in the new mint primitive and does not fix it silently.
2. Probes
Source: C:\Users\Meta\AppData\Local\Temp\claude\F--Projects-Aetheria\2305971e-ad74-41e1-a7cb-2181a612c355\scratchpad\provenance-probe\.
It was run 2026-09-17 against a detached scratch worktree of Aetheria at b0df689d with
ItemData.cs:280-281 deleted, and CultLib a0813c6. The catalog was a copy of
GameData/Aetheria.cc (sha1 4d08859552dbd864ad13363179fd4bd97b567e4a, 12,686,267 bytes).
Both worktrees were removed afterwards; re-running needs that worktree recreated with the
two-line deletion.
| Id | Question | Result |
|---|---|---|
| Q1 | Does the real catalog open read-only with ItemData key 3 removed? |
Opens. LastSchemaMigrationReports: 180 reports, 115 with IgnoredExtraSlots = {3}. 115 items, 102 crafted, 37 products; duplicate/maker/role counts as in §1.5. |
| Q2 | Does a [CultGlobal] ledger holding Dictionary<int, Lot> with a nested [Union] origin and CultRecordRef fields round-trip in the run store, committed in one batch with a SavedZone? |
GetGlobal before any write: null. After commit and reopen: next 4; lots 3; key global:sha256:8660…; zones 1, all three cases intact (Attributed faction, Produced station 12 facility 1 inputs 3, Extracted zone 4). batch.Remove(key) then reopen: null. |
| Q2' | Can Produced.Station be a CultRecordKey? |
No: FormatterNotRegisteredException: GameCult.Caching.CultRecordKey is not registered in resolver at commit. int works (Q2). See fork N1. |
| Q3 | Does one writable commit re-upserting every ItemData clear the drift? |
rewrote 115; bytes 12,686,267 -> 12,683,061; reopen: 0 reports with ignored slots, 115 items. |
| Q4 | Does a key-11 field on a subclass serialize identically to the same key on the base? | equal True len 14. |
Not probed: CultCache Studio's rendering of the ledger (operator check); Unity compile (batchmode in each cut).
3. Authority map
Lot properties and provenance.
- Owner:
ProvenanceLedger, one run-scoped[CultGlobal]document. In play, the live instance isItemManager.Lots. - Inputs: the lot-minting functions
ItemManager.CreateLot(...), which read the product (maker, design,Rolesspreads) or the console's design, plusItemManager.RandomandGameplaySettings.Tiers. - Outputs:
Lot { Design, Origin, Quality, Roles }byLotId(anint, first id 1, never reused within a run). - Derived state:
CraftedItemInstance.Datais no longer an independent value. It is copied fromLot.Designby the one instance mint,ItemManager.CreateInstance(int lot), and stays on the instance becauseItemsOfType, stacking and everyGetDatakey off it.- Stats, wear, price and tier are derived from the lot through
ItemManager.GetLot. EquippedItemandConsumableItemEffectcache theLotreference at construction. That is safe because lots are immutable and the live ledger is never pruned.- Brand is derived by
ItemManager.Brandand never stored.
- Forbidden writers:
CraftedItemInstance.Quality,.Ingredientsand.Product: deleted.LoadoutGenerator.CopyBuild: deleted; a matching hardpoint mints from the same lot.ItemManager.InstantiateandUnpack(instantiate): deleted, because they re-rolled quality and dropped provenance.ItemData.Manufacturer: deleted.- Nothing outside
ItemManager.CreateLotconstructs aLotor assigns its fields. Tests may construct lots directly.
- Shared paths: NPC and station generation (
LoadoutGenerator), preset materialization (Loadouts.Materialize), starting ship andspawnturret(the generator, no re-mint) and consolegiveall mint throughCreateLot->CreateInstance(lot). Loot, buying and pickup move instance values; their lot rides along. - Deletion line: in Cut B, the three instance fields and
CopyBuildgo before the ledger gains a reader.
Ledger persistence and GC.
- Owner:
RunSave. - Inputs: the live ledger and the
SavedZones being committed. - Outputs:
RunSave.Lots(cache)is the only reader of the stored ledger. It returns the stored global, or a new empty ledger when there is none.RunSave.Commit(cache, game, zones, lots)writeslots.Reachable(roots)in the same single-store batch as the zones and the game.RunSave.Clearremoves the ledger throughRunTypes.
- GC roots: every
CraftedItemInstancein every committedSavedZone.Contents.Entities. An entity yields its hull, equipment, cargo bays, docking bays, cargo and docking-bay contents, and itsChildren, recursively (EntitySerializer.Items(pack)). Closure followsProduced.FacilityandProduced.Inputs. Zones never generated have no contents and contribute nothing. - Deliberately not roots: floor
ItemPickups.PackZonenever captures them (Zone.cs:108-119), so they vanish on reload anyway, and their lots go with them. - Invariants:
- GC prunes only the written copy. The live ledger keeps unreachable lots, because a floor pickup can still be collected after a save.
- The written copy keeps
NextLot, so ids are never reused within a run. - A root naming an absent lot throws. That is corruption, and it must be loud.
- Forbidden writers: nothing else calls
GetGlobal<ProvenanceLedger>or upserts one.ZoneGenerator's staged-upsert pattern (ZoneGenerator.cs:99-108) is not used for lots: the ledger is live simulation state, captured at commit like entities. - Lifecycle: the run begins with a scene load (§1.6), whose
StartreadsRunSave.Lots(CultCache). New Game has already cleared the run store (MainMenu.cs:112), so it gets an empty ledger. Continue gets the stored one. Death clears the run store (ActionGameManager.cs:1090); the next run reloads the scene.
Branding.
- Owner:
ItemManager.Brand(CraftedItemInstance)->(Faction Maker, FactionProductData Product). - Inputs:
- The lot origin's faction:
Attributed.FactionorProduced.Faction;Extractedhas none. instance.Data.- The catalog's
FactionProductData.
- The lot origin's faction:
- Rule:
Productis the first product in record-key order (StringComparer.Ordinal, the orderLoadout.cs:99already uses) whoseManufactureris the maker and whoseDesignkey is the instance's design. It is null when the maker is unset or makes no such product. - Stays authored in the catalog:
FactionProductData(Name,Description,Design,Manufacturer,Roles). It is the brand catalog. - Rule until segment bands exist (next scope): at most one product per
(maker, design).censuslists violations (§1.5 shows none today).
4. Adds, by rule
New file Assets/Scripts/ServerShared/Provenance.cs. It needs no csproj edit: it falls
under Aetheria.Shared.csproj's ServerShared\** glob and the Unity asmdef folder. Unity
generates its .meta, which is committed.
ProvenanceLedger:[CultDocument("aetheria.provenanceledger", "1"), CultGlobal, MessagePackObject].- Key 0
int NextLot = 1; key 1Dictionary<int, Lot> Lots. int Add(Lot)assigns and returnsNextLot++.- The indexer
Lot this[int]throwsInvalidOperationExceptionnaming the id when it is absent. It has no try-variant. ProvenanceLedger Reachable(IEnumerable<int> roots)returns a new ledger: sameNextLot, the closure overProduced.Facility/Inputs, and the sameLotobjects. It never mutatesthis.
- Key 0
Lot:[MessagePackObject].- Key 0
CultRecordRef<ItemData> Design.ItemData, notCraftedItemData, so anExtractedcommodity lot needs no reshape. - Key 1
Provenance Origin; key 2float Quality; key 3List<RoleFill> Roles. QualityForRole(string)moves here fromItemInstance.cs:47-53, with the same fallback toQuality.
- Key 0
RoleFillmoves fromItemInstance.cs:56-62unchanged (keys 0, 1).Provenance: abstract, with[Union]only and no[MessagePackObject](MsgPack005; precedentItemInstance.cs:18-23).Union(0) Attributed { Key 0 CultRecordRef<Faction> Faction }: the F0 placeholder. Faction only; the generator deletes it.Union(1) Produced { Key 0 CultRecordRef<Faction> Faction; Key 1 int Station; Key 2 int Facility; Key 3 int[] Inputs }.Union(2) Extracted { Key 0 int Zone; Key 1 CultRecordRef<SimpleCommodityData> Commodity }.Zoneis the run's zone index, the identitysavedzone-{i}already uses.
CraftedItemInstance.Lot:[JsonProperty("lot"), Key(11)] int. Keys 2, 9 and 10 are retired, not reused; the comment follows theItemData.cs:303precedent.ItemManager:- Constructor
(CultCache, ProvenanceLedger lots, GameplaySettings, Action<string>)with a required ledger, andProvenanceLedger Lots { get; }. Lot GetLot(CraftedItemInstance).float RollQuality(): the tier roll body of:151-159, unchanged.int CreateLot(FactionProductData): the maker asAttributed,RollQuality(), and a role spread per design role exactly as:175-185.int CreateLot(CraftedItemData, CultRecordRef<Faction> maker, float quality): no roles.CraftedItemInstance CreateInstance(int lot): the type branch of:128-141, withData = lot.Design,Lot = lot, and durability from the design.CraftedItemInstance CreateInstance(FactionProductData)stays as the convenience: null plus a log when the design is missing (as:166-171), otherwiseCreateInstance(CreateLot(product)).Brand(...)as in §3.
- Constructor
RunSave.Lots(CultCache);RunSave.CommitgainsProvenanceLedger lots.EntitySerializer.Items(EntityPack): the GC root walk (§3), a static enumerator next toPack/Unpack.
Fields with no reader or writer in this cut
Named by rulings, so kept, and flagged as instructed:
Producedin full (Faction,Station,Facility,Inputs) has no writer.- Its readers in this cut:
BrandreadsFaction, and GC closure readsFacility/Inputs. Both are exercised by tests that buildProducedlots directly. Stationhas no reader at all.- It can say "built in place at its station, from inputs": a facility lot is a
Producedlot whoseStationis its own station and whoseInputsareExtractedlots there. Retooling cost is a factory-tooling concern that adds a document or union case and changes no field here.
- Its readers in this cut:
Extractedin full has no writer. Its only reader is GC closure, which treats it as a leaf.Lot.Designon lots with no instance: none exist until generation, so no reader.
Not in the cut, because no ruling names them and nothing reads them: segment quality
bands, Founded, Seized, station roster, source, route and shipment records, recipes
or role input specs, a station id field on OrbitalEntityPack, and tooling state.
5. Cuts
Three cuts, in order. A and C are subtractions Soul can falsify alone; B is the behaviour change.
Cut A: delete the re-mint path and the dead debug menu
Subtraction. The one behaviour change is the scar fix: the starting ship and spawnturret
keep what the generator built.
- Deletes:
Assets/Scripts/UI/Menu/TradeMenuDebug.cs(472 lines) and its.meta.ItemManager.cs:92-106CreateInstance(SimpleCommodityData, int)(15; its only caller is:119).ItemManager.cs:108-124Instantiate(17).
EntitySerializer.cs:- Remove the
instantiateparameter from:59,65-66,72,76,83,86,93,99. :75,85,103-105,116,121pass the pack's own items.- Delete the guards at
:133and:136, soTemperatureandArmorare restored unconditionally. That is what every non-instantiate caller already gets.
- Remove the
ActionGameManager.cs:540and:735-739: drop thetrueargument. The comment at:740names aLoadouts.First(... StarterShipTemplate), truecall that no longer exists; delete it.- Verification:
dotnet build Aetheria.Shared/Aetheria.Shared.csproj -p:CultLibRoot=<wt>anddotnet build tools/AetherDb -p:CultLibRoot=<wt>are green.dotnet test tests/Aetheria.Shared.Tests -p:CultLibRoot=<wt>: 29 passed.- Unity batchmode compile (§6) has no
error CS. - Negative:
rg -n "instantiate|\.Instantiate\(" Assets/Scripts/ServerShared/EntitySerializer.cs Assets/Scripts/ServerShared/ItemManager.csis empty.rg -n "EntitySerializer\.Unpack\([^)]*, true\)" Assetsis empty.rg -n "TradeMenuDebug|6eed32eb39c04a31b81e8a55685b222c" Assetsis empty.rg -n "SimpleCommodityData item, int count" Assetsis empty.
- Soul reads, because no test covers it:
GenerateShipLoadoutandGenerateTurretLoadoutpack immediately after construction and outfitting (LoadoutGenerator.cs:46-49,61-64). RestoringTemperatureandArmorfrom those packs reinstates the generator's own arrays. If anyTryEquipinRestorerecomputes armor differently, the starting ship differs; the play smoke covers it.
- Ledger: about -516 +0 (plus
.meta).
Cut B: lots, ledger, GC, stats through the lot, derived brand
- Deletes:
ItemInstance.cs:35-53:Quality,Ingredients,Product,QualityForRole(19).ItemInstance.cs:56-62:RoleFill, moved.ItemManager.cs:126-187: the two design overloads and the product overload body, replaced by the §4 set.LoadoutGenerator.cs:262-269:CopyBuild(8).
- Per-file changes:
ItemInstance.cs:33-54:CraftedItemInstanceholds onlyLot(key 11) and the retired-keys comment.ItemManager.cs::27-32: the constructor takes the ledger.:70-84Evaluate:var lot = GetLot(item);lot.QualityForRole(stat.FromRole)at:73,lot.Qualityat:77.:86-90GetPrice:GetLot(item).Quality.:189-197GetTier: the same.- Add
GetLot,RollQuality, bothCreateLots,CreateInstance(int)andBrand.
Entity.cs:ConsumableItemEffectconstructor:1056-1066cachesLot;:1085readsLot.QualityForRole.EquippedItemconstructor:1210-1225cachesLot = ItemManager.GetLot(item)and readsLot.Qualityat:1221,1225.:1261readsLot.QualityForRole;:1290readsLot.Quality.EquippableItemis assigned only at:1215, so the cache cannot go stale.
LoadoutGenerator.cs:210-226: whenpreviousItem != null, mintItemManager.CreateInstance(previousItem.EquippableItem.Lot); otherwiseCreateInstance(entry.product). Rewrite the comment at:219as "same lot". TheRandomdraw sequence of the lot rolls shifts, because the second unit no longer rolls; generator selection usesLoadoutGenerator.Randomand is unaffected.Loadout.cs:89-94comment: a failed materialization leaves minted lots in the live ledger, and the next commit drops them.:129,135are unchanged.SavedGame.cs:- Add
RunSave.Lots. :96-111Commit(cache, game, zones, lots): compute roots fromzones[i].Contents?.EntitiesthroughEntitySerializer.Items, thenbatch.Upsert(lots.Reachable(roots))in the existing batch.- Update the comments at
:52,93-95,113(the ledger is the fifth run record).
- Add
AetheriaStores.cs:10:RunTypesaddstypeof(ProvenanceLedger).EntitySerializer.cs: addItems(EntityPack).ActionGameManager.cs::264:new ItemManager(CultCache, RunSave.Lots(CultCache), Settings.GameplaySettings, Debug.Log).:247:RunSave.Commit(CultCache, game, zones, ItemManager.Lots).:499give:ItemManager.CreateInstance(ItemManager.CreateLot(item, default, .95f)). An unset maker meansBrandreturns no maker and no product.
PropertiesPanel.cs:371-372: for aCraftedItemInstance, a "Manufacturer" stat readsBrand(...).Maker.Name, and is omitted when there is no maker; the "GameCult" fallback goes. WhenProductis non-null, add the productNameandDescriptionas properties. This is the first flavour text the player sees. ASimpleCommodityshows no brand.ItemData.Manufactureris still read here until this edit; Cut C deletes the field.tools/AetherDb/Program.cs::395passesnew ProvenanceLedger().censusadds a "(maker, design)with more than one product" section, the Brand rule's check.save(:353-378) readsRunSave.Lots(db.Cache)and prints, per run, the lot count, crafted instances, instances whose lot is absent, and instances whoseDatadiffers fromLot.Design. This is the only headless check of a real played save.
- Tests. Each is paired with the mutation that must kill it; Soul applies each mutation
and sees red.
- Every
new ItemManager(in tests passesnew ProvenanceLedger()(10 inLoadoutTests, 1 inRunSaveTests).RunSaveTests.cs:43,60,61,76,86,94,112pass a ledger toCommit. The record counts inRepeatedSavesKeepRecordCountConstant(:48-49) andClearRemovesEveryRunRecordrise by one for the ledger global.
- Every
| Test | File | Asserts | Mutations that must kill it |
|---|---|---|---|
MaterializedLotsSurviveSaveAndReload |
LoadoutTests |
Materialize a HandBuilt ship; pack it into a zone; Commit via Open(); reopen. Every crafted instance resolves in RunSave.Lots, Data == Lot.Design, the origin is Attributed with the product's maker, and Quality equals the pre-save lot's. |
Commit omits the ledger upsert; CreateInstance(int) leaves Lot 0; CreateLot(product) writes default faction; ProvenanceLedger absent from RunTypes (the commit throws unrouted) |
CommitKeepsOnlyReachableLots |
RunSaveTests |
Hand-built ledger. Lot 1 is on a hull. Lot 3 is Produced { Facility 4, Inputs [5] } in a docked child ship's docking-bay contents. Lots 2 and 6 are unreferenced. NextLot is 7. After commit and reopen, the stored lots are {1, 3, 4, 5} with NextLot 7, and the live ledger still holds 2 and 6. |
Reachable returns this; closure skips Inputs or Facility; Reachable prunes in place; the written copy resets NextLot; Items skips Children or DockingBayContents |
MissingLotIsLoud |
RunSaveTests |
new ProvenanceLedger()[0] throws; Reachable(new[] { 9 }) throws |
the indexer returns null or default; Reachable skips missing roots |
MatchingHardpointsShareALot |
LoadoutTests |
A fixture hull, 4x4 with two identical Sensors hardpoints, plus a capacitor gear and product (FillInterior requires one, LoadoutGenerator.cs:250-253). GenerateShipLoadout with galaxy null: both hardpoint units carry the same Lot. |
the matching-hardpoint branch calls CreateInstance(entry.product) |
FirstAvailableProductInKeyOrderBuildsTheSlot (rewritten, :289-312) |
LoadoutTests |
lamp-z and lamp-a get two new makers; each case asserts items.Brand(unit).Product.Name |
Brand ignores the maker; Materialize drops key order |
StatsReadTheLot |
LoadoutTests |
A Lamp lot with Quality .2 and Roles [lens .9]: Evaluate with FromRole "lens", Min 0, Max 1, QualityExponent 1 gives .9; with no role it gives .2; GetTier reads .2 |
Evaluate reads Quality for a role stat; QualityForRole falls back wrongly |
- Verification:
- The three headless commands of Cut A are green, with 34 tests (29 + 5 new, 1 rewritten).
- Unity batchmode has no
error CS. AetherDb loadout 1output equals the HEAD capture (0 hulls failed); only the quality roll sequence changed.AetherDb censusequals HEAD, plus the empty duplicate section.- Negative greps:
rg -n "\bIngredients\b|\.Product\b|CopyBuild|item\.Quality\b|EquippableItem\.Quality\b|Item\.QualityForRole|EquippableItem\.QualityForRole" Assets/Scripts tools testsis empty.rg -n "new Attributed" Assets/Scripts toolsmatches onlyItemManager.cs, the site the generator cut replaces.rg -n "GetGlobal<ProvenanceLedger>" Assets tools testsmatches onlySavedGame.cs.rg -n "new Lot\b|\.Origin =|\.Roles\.Add" Assets/Scriptsmatches onlyItemManager.cs.rg -n "new (EquippableItem|CompoundCommodity|ConsumableItem)\b" Assets/Scripts toolsmatches onlyItemManager.CreateInstance(int), the one writer of a crafted instance'sData. Tests may construct instances by hand.
Cut C: remove the design's manufacturer and rewrite the catalog
Subtraction, plus a data rewrite.
- Deletes:
ItemData.cs:280-281(Manufacturer, key 3), with a one-line retired-key comment. - Readers switch:
tools/AetherDb/Program.cs:54: census makers come from the product selling each design, and "(none)" for unsold designs.AetheriaStoresTests.cs:32writes aFactionProductData { Design = Lance, Manufacturer = faction }.:109assertsAssert.Same(faction, cache.Get(cache.GetAll<FactionProductData>().Single().Manufacturer)).
- Catalog rewrite:
- Unity and CultCache Studio must be closed; the operator's editor holds no catalog handle.
- Hands runs a scratch console, not committed. It is the body of probe Q3: open
GameData/Aetheria.ccwritable throughAetheriaStores.Open(..., catalogWritable: true)and re-upsert every storedItemDatawith its own key in oneCommit. - Expected: 115 rewritten, 12,686,267 -> 12,683,061 bytes, and 0 schema reports with ignored slots on reopen.
- Commit the binary. The CultLib worktree is clean before and after.
- Verification:
- The headless commands are green (34 tests); Unity batchmode has no
error CS. - Negative:
rg -n "Manufacturer" Assets/Scripts/ServerShared/ItemData.csis empty. - Negative:
rg -n "(data|item|design)\.Manufacturer" Assets/Scripts tools testsis empty; product.Manufacturerreaders remain by design. - AetherDb captures before and after the rewrite:
census,factions,dangling,station-fit,loadout 1andhardpoint-fit.- Equal:
factions,dangling(0 dangling refs),station-fit,loadout 1, andhardpoint-fit(still exit 4, identical text). - Differs by design:
censusper-kind maker lists, now from products, so the 14 unsold designs show "(none)". - Any other diff is a defect.
- Equal:
- The headless commands are green (34 tests); Unity batchmode has no
- Ledger: -3 +2 C#; catalog -3,206 bytes.
6. Build budget and Unity
- Packages touched:
Aetheria.Shared(netstandard2.1),tools/AetherDb(net10.0), andtests/Aetheria.Shared.Tests(net10.0), Debug only. - CultLib
src/GameCult.Caching,GameCult.Caching.MessagePackandcultmathare referenced ata0813c6; none is modified. - No new project, package or target; one new source file.
- Build host: this Windows workstation. Target: the same .NET SDK for the headless check,
and Unity 6000.3.24f1 Windows editor for
Assembly-CSharp. - Unity batchmode, per cut, detached:
- First check
Get-Process Unity -ErrorAction SilentlyContinueandTest-Path F:\Projects\Aetheria\Temp\UnityLockfile. If the editor is open, stop and ask the operator to close it; do not kill it. - Then
Start-Process "C:\Program Files\Unity\Hub\Editor\6000.3.24f1\Editor\Unity.exe" -ArgumentList '-batchmode -nographics -quit -projectPath F:\Projects\Aetheria -logFile <scratch>\unity-cut-X.log' -PassThru. - Record the PID, and poll
Get-Process -Id <pid>andSelect-String -Path <log> -Pattern "error CS|Exiting batchmode". - Pass: exit, no
error CS. Commit the generatedProvenance.cs.metain Cut B.
- First check
- Temporary CultLib worktree:
git -C F:\Projects\CultLib worktree add --detach <scratch>\cultlib-a0813c6 a0813c6eed24d30bf88073ef615b633c77ebfcd6. Remove it withgit worktree removeafter the pass. Do not touch the other five CultLib worktrees.
Operator-only checks after Cut C:
- New Game and fly through one zone. The starting ship's items show tier colour, and the properties panel shows Manufacturer, product name and flavour text for branded gear.
- Dock and buy an item: its brand still shows in your cargo.
- Kill an NPC and pick up loot: brand shown.
- Warp through a wormhole (a save), quit, Continue: the same items with the same tier and brand.
- Then the agent runs
AetherDb save: 0 absent lots, 0 design mismatches. give Lampor any design name: the item shows no Manufacturer row.- Open
GameData/run.ccin CultCache Studio. Theaetheria.provenanceledgerglobal renders itsLotsdictionary withAttributedorigins and per-role lists. If the Studio cannot render anint-keyed dictionary of unions, that is a Studio defect to raise, not a schema change. - Die: Continue is disabled and
run.ccholds no ledger (AetherDb save: "run store holds no SavedGame", and no ledger).
7. Forks
N1. Produced.Station identity type. Low-blocking. The field has no writer until the
generator, so any answer can be retyped then with nothing on disk to migrate.
- (a)
int, a run-scoped station id. This mirrors the ruled ledger shape (F1 (c): run-scoped integer ids in one global document) and F3's recommended skeleton document, and it round-trips today (Q2). - (b) The station roster's record key as a string. It is wire-identical to a future
CultRecordRef<StationRecord>(CultRecordRefFormatterwrites the key string), but it pre-commits F3 to one run record per station.CultRecordKeyitself has no formatter (Q2'), so it would be an untypedstringnow. - (c) Leave
Producedout until the generator. A union case is additive and reshapes nothing, but the schema then no longer shows "who, where, from what", which the operator asked the cut to show. - Recommend (a). The cut is written under (a); (b) changes one field type and (c) deletes one class.
Recorded defaults, not asked:
- The live ledger's owner is
ItemManager, notGalaxy.ItemManagerspans exactly one run (§1.6). The ruling routes stat readers through it. Fixtures andAetherDbbuild anItemManagerwith no galaxy. Lotgoes onCraftedItemInstanceat key 11, not on the base (§1.2, Q4).LotIdis a bareint, not a typed struct. A struct dictionary key needs a hash-resistant comparer underUntrustedData:CultMessagePackSecurityhandles onlyCultRecordRef<T>(CultMessagePackSecurity.cs:14-21), so a struct key would need a CultLib change.- The catalog is rewritten by a throwaway console rather than left drifting, or saved through Studio, which is last-writer-wins over the whole file.
- Console
givewritesAttributedwith an unset faction. - Duplicate
(maker, design)products resolve by record-key order, andcensusflags them.
8. Subtraction ledger
Measured where a range is named; bounded estimates otherwise.
| Cut | Removed | Added | Targets, schemas, stores |
|---|---|---|---|
| A | TradeMenuDebug.cs 472 + .meta; ItemManager 32; EntitySerializer ~12 (param threading, two guards); ActionGameManager 2 args + 1 comment |
0 | -1 MonoBehaviour; 0 targets |
| B | ItemInstance 26 (19 fields and method, 7 moved RoleFill); ItemManager ~62 (:126-187); CopyBuild 8; PropertiesPanel 2 |
Provenance.cs ~90; ItemManager ~45; RunSave ~12; Items ~15; Entity ~6; LoadoutGenerator ~3; PropertiesPanel ~8; AetherDb ~20; tests ~170 |
+1 run schema aetheria.provenanceledger (global); CraftedItemInstance keys 2, 9, 10 retired, key 11 added; the run store gains 1 record |
| C | ItemData 2; census 1 |
census ~4; stores test fixture ±2; 1 retire comment | ItemData key 3 retired in the 7 concrete schemas that hold records; the catalog loses slot 3 on 115 records (-3,206 bytes) |
Net source excluding tests: about -620 / +200. Tests: about +170. TradeMenuDebug is
most of the deletion; without it the provenance change is close to net +100. That buys a
typed provenance graph, derived brand display and per-lot stats, and it removes three
per-unit fields and two re-mint paths.
9. Risks and rejected paths
Risks:
- Continue with an old run store throws on the first lot lookup. That is accepted
under "saves are discarded". No
run.ccexists in the tree today; the operator deletes any stray one before the smoke. - The live ledger grows by unreachable lots within a session: failed
materializations, consumed consumables, destroyed ships' unlooted gear. That is bounded
by what generation mints: the substrate probe counts about 16k instances for a whole
galaxy, and
CopyBuildsharing lowers the lot count. It is pruned on every write. - The ledger is one document. The single-file store already rewrites its whole view per commit (substrate §4), so the ledger adds bytes, not a new rewrite pattern.
- Studio rendering of
Dictionary<int, Lot>with union values is unprobed; it is an operator check. - Cut A's unconditional
Temperature/Armorrestore is argued from source, not tested.
Rejected paths:
- Staging lots into the cache as zone generation stages orbits. That would make the cache
the live owner and let
Clearrace a liveItemManager; lots are simulation state, captured at commit. - Pruning the live ledger at commit, which would break pickups collected after a save.
- A nullable lot or a "no provenance" branch in readers (F0 (b)).
- Keeping
Producton the lot "for display": that is branding stored. - A save-format version gate for old run stores.
- A typed
LotIdstruct. - A committed
AetherDbcatalog-rewrite command for a one-time rewrite. - Moving
Dataoff the instance so the lot is its only design authority. EveryItemsOfType, stacking andGetDatapath keys offItemInstance.Data, andSimpleCommodityhas no lot. Instead,Datais written only fromLot.Designat mint, andAetherDb savechecks the equality.