ValidateAll (one owner, called by AetheriaStores.Open) refuses duplicate verb names; Validate refuses the reserved role
name actor. Tests pin the name-class edges (a z 0 9 _), a null role element, the starter verb replaced when only its
description or render path differs, the grammar global updated in place, and the fixture's second allegiance and zero-role
speak. The test's scoped registry is TestCatalog.Registry(). Builds; unverified until the full run.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Takes master's catalog pointer (GameData/Aetheria.cc) and its TestCatalog.Registry fixture path;
VerseGrammar is the global scratch catalogs author (TestCatalogGlobal stays deleted: the shipped
registry now has a real global, so a test-only second one would have to be authored beside it).
StatResolverTests, TestCatalogTests and AetheriaStoresTests author VerseGrammar.
Does not pass the suite: the catalog is master's and holds no grammar until the verse-grammar apply commit follows.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The test assembly registers a catalog [CultGlobal], so the global check already threw on a missing
file there and AStoreRefusesAMissingCatalog passed against the old Open (run at fe9da2e: 12 passed).
The refusal is now a FileNotFoundException naming the path, and the test pins that type, which the
global check never throws. Builds.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
AetheriaBuild (Assets/Editor) sets the version, builds Addressables, builds the
Win64 player, stages GameData through PlayerStaging, publishes AetherDb to
ModTools and removes the Burst debug folder; any failure exits 1 naming the step.
PlayerStaging is Unity-free and linked into Aetheria.Shared.Tests. A read-only
AetheriaStores.Open of a nonexistent catalog throws.
Builds on the headless side; the Unity .meta files and the batchmode run follow.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
AetheriaStores.Open, ShipModCatalog.Compose and ResolveCatalog take an optional registry (the game passes none and keeps the
process-wide one). TestCatalog.Registry() is the shipped assembly's registry, so the test assembly's TestCatalogGlobal is no
longer a catalog global the real catalog must hold (fixture-fails-on-real-package). TestCatalog.ForRepo(root) is the one
binding Repo uses, pinned by a test over a real catalog plus a package (repo-wiring-unpinned). A package directory without
ship.cc is excluded with a reason, in Compose and so in ResolveCatalog's pre-check (missing-ship-cc-skipped). The loader takes
only a lowercase .glb, matching the case-sensitive LFS rule (lfs-glb-case).
All commits build; not yet run (verification job follows).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Adds the aetheria.verse_grammar global and aetheria.verse_verb documents (one render path per verb, typed
participant roles), VerseGrammarValidation run by AetheriaStores.Open over every verb, the AetherDb
'verse-grammar [apply]' command (starter set: one ship-action verb per TaskType plus speak; question
verse-social-verbs default speak-only), and VerseGrammarTests with a deterministic fixture writer.
The shipped catalog does not carry the grammar yet: it lands in the next commit, so
The_shipped_catalog_holds_revision_one_and_the_seven_starter_verbs fails at this commit. Not yet built when committed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Mining owns belts, chunks and target picking; scenarios owns admission and run
staging; master owns ships and fire control (rule scenarios-cut.md S4).
- AetheriaStores.CatalogTypes: nine-type union, master's list plus FieldKindData, no Scenario.
- tools/AetherDb/Program.cs: both cases (ship-authoring, field-kinds), one usage string.
- Zone.cs: master's MineAsteroid dropped (mining deleted its inputs); scenarios' SettleBelts and its three calls deleted.
- RunStartTests: the two belt-task tests deleted (they read belt.Transforms and MineAsteroid).
- FireControlCut124Tests: master's strict Exact assertions; mining's CompatibleDrift allowance and its comment dropped.
- GameData/Aetheria.cc: master's pointer 70a7b0a9. This commit lacks the Asteroid field kind in the catalog, so
TheShippedCatalogHasAPickableFieldKind fails here by design; the next commit rebuilds it.
Builds: this commit builds (verified at the end with commit 2); the catalog pin test fails at this commit.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A Scenario is an abstract C# class compiled with the game: Generate picks the galaxy through GalaxyStage (Main or
Prelude, at the scenario's seed, the background noise drawn from the same seed), Stage places the run's first
entities through ScenarioStage (Fit, Bare, Generated, Preset, Player, Place, Cargo), which collects named failures
and admits nothing. RunStart generates the galaxy, the arena under Ambient and admits everything through Zone.Admit,
all or nothing. The aetheria.scenario record, ScenarioShip, ScenarioEntity, RunStart.Check and the null-scenario
branch are cut.
New Game opens a submenu: Tutorial Galaxy and Main Galaxy, then (editor and development builds) Djinni Shakedown,
Starved Reactor, Arcs, Duel, Launcher Angles and Long Haul, each with its brief. MainMenu.Launch is the one way into a
new run; ActionGameManager.PendingScenario carries the scenario to StartGame once.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The one conflict is AetheriaStores.CatalogTypes: keep ShipAuthoring from the
ships lane and Scenario from the scenarios lane.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- FieldKindData, a catalog document (Q11): a kind's reflectivity per schematic
cell and its generation weight. AetheriaStores routes it to the catalog.
- AsteroidBeltData.Kind, key 10 (Q16 B). FieldKinds.Assign is the one pick (a
weighted choice by a stable hash of the belt's key); FieldKinds.Ensure gives
a kindless belt its kind and saves it. ZoneGenerator calls it at generation
and Zone calls it on first load, so an old belt is assigned once by the same
rule and keeps its kind after later catalog edits.
- Zone.ChunkVisibility: Reflector's rule for a chunk -- kind cross-section x
area in schematic cells x the light on it. Stored nowhere; a worn chunk dims,
a broken one or a kindless belt's is dark.
- Zone.ChunksNear: the one chunk query, skipping belts whose annulus cannot
reach. AsteroidBelt gains InnerRadius for that skip.
- TargetRef: an entity, a chunk, or none, with value equality and no ==.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
RunStart (ServerShared, no Unity type) owns what a new run starts with
(docs/scenarios-cut.md, 2.2):
- GenerateArena: the galaxy's entrance pack, generated under the scenario's
Ambient rule. ZoneGenerator.GenerateZone gains `ambient`; false keeps
planets, orbits and stations and generates no ship, turret or wanderer.
- Check: every failure staging a scenario would hit, found with no zone,
touching no store.
- Stage: all-or-nothing. With no scenario, the generated starting-hull ship
at the origin, as StartGame built it. With one, the player preset and the
entities at their authored place and heading with cargo, stance set both
ways through SetIff, a Minion for each piloted one; all through Zone.Admit.
Scenario (aetheria.scenario v1) joins AetheriaStores.CatalogTypes.
RunStartTests stage into a real prelude arena from the shipped catalog and
authored settings.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Operator ruling 2026-09-30: a fused weapon with a selected but out-of-arc
target fires. Its round flies along the arc-clamped aim and bursts at the
target's range. Non-fused weapons still refuse.
- FireControl.ArcPermitsFire is the one trigger arc gate (fused weapons are
exempt); Fire reads the same InArc through PFire's new designated/inArc
outs, which replace `bears`. No valid targeting data still bursts at Range.
- F3: a fused weapon whose effective Range does not exceed its BlastRadius
stays safed at the trigger (FuseCanArm, read from the runtime Range). The
catalog check on Range.Min is deleted; it read the authored floor, which
Evaluate can scale below.
- F4: a targeted proximity burst is pulled back along its line to Range.
- F8: a proximity round that detonates commits as Burst (or Hit); Apply's
Proximity case collapses into the Burst path via Outcome.BurstPoint.
- Tests: arc-only and LockWeapon gates, out-of-arc burst, trigger gate,
runtime-Range refusal, Range clamp, Burst typing, commit horizon, and
Soul's adopted probes (aim clamp edges, contact point, target leaves,
shooter destroyed).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Soul: a no-lock round bursts at Range, so Range <= BlastRadius (or a zero
Range) lands the blast on the shooter. Made structurally impossible at the
catalog rather than policed at runtime: StatValidation.ValidateFuse joins
the checks CultRecordRefs.Validate runs on every Upsert, at the weakest value
the Range stat can take. Only a weapon that will detonate (a Fuse and a
BlastRadius above zero) is held to it.
AetheriaStores.Open now calls that same Validate for every equippable and
consumable document instead of carrying its own copy of the three checks, so
Upsert and Open can no longer disagree.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
A power request may not depend on power supply (Cut 6/F6), and continuous
consumers brown out through a power term on their own performance stats
(Cut 7). For Radiator (PumpedHeat) and AetherDrive (Torque) those were the
same stat, making six shipped catalog records illegal and leaving six
BrownoutTests cases skipped.
Resolved per the operator's ruling: a power request is evaluated
nominally -- what the item wants at full supply, not what it is currently
managing. EquippedItem.EvaluateNominalPower (mirroring ConditionRatio's
own NominalContext) pins only PowerSupplyFactor to 1; Heat/Durability
stay live, since they are not the term a request would be circular
through. Every PowerRequest/RefreshReserve/RefreshInputCapacitor
implementation now reads a StatValidation.PowerRequestFields stat through
it, so the same stat can carry a brownout curve and be a request field
without the request depending on its own answer.
StatValidation.ValidateNoPowerSupplyOnRequest (F6's static half of the
rule, forbidding the term outright) is deleted -- nominal evaluation
removes the circularity at the root instead. Only the dynamic
modifier-chain half (StatModifier.ValidateNoPowerSupplyChain) still
refuses anything, since EvaluateNominalPower forwards ScaleModifier/
ConstantModifier unchanged (real, not nominal).
Un-skips BrownoutTests' six Radiator/AetherDrive cases and adds two new
regression tests (a direct EvaluateNominalPower probe, and a two-tick
Radiator demand check) backed by a new mutation harness. Updates the four
PowerCurveTests/AetheriaStoresTests cases that pinned the now-superseded
static refusal to instead assert acceptance. GameData/Aetheria.cc is
unchanged and still validates.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
F1 power supply multiplies the resolved value (operator ruling), so zero
supply means zero and a flat stat still responds. F2 the bus agrees with
Active in both directions. F3 an empty tier reports no demand instead of
full supply, and sub-epsilon demand is rationed and subtracted. F4/F5 the
shield restores while inactive and CanTakeHit no longer mutates. F6/F7 the
validator names every stat a request reads and the tools validate before
writing. F8 re-anchors two dead mutation harnesses.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
CultRecordRefs.Upsert is the one function every catalog write is supposed to
go through, but tools/AetherDb/Program.cs's Dangling, ShieldMigrate and
BrownoutMigrate commands each repair an existing record in place and must
preserve its key -- Upsert's own cache.UpsertAsync(document) cannot take a
caller-supplied key, so all three wrote raw and unvalidated through
`db.Cache.Commit(batch => batch.Upsert(document.GetType(), document, key))`.
`brownout-migrate apply` is how the shipped catalog's PowerSupply terms were
actually authored, unvalidated, the whole time.
Extracted Upsert's validation body into CultRecordRefs.Validate(object), and
call it directly from all three tool commands immediately before their own
identity-preserving commit -- the same rules, invoked where Upsert itself
cannot reach. The comment above StatValidation naming the write-bypass holes
(landed with F6) now names all four instead of two: CultCache Studio's
generic editor (still open -- not this repository's to patch) plus the three
tool commands above (closed).
Pinned in tests/Aetheria.Shared.Tests/AetheriaStoresTests.cs:
ValidateRefusesAnEquippableItemWithAZeroSpanHeatRangeTheSameWayUpsertWould and
ValidateRefusesAPowerRequestCarryingAPowerSupplyTermTheSameWayUpsertWould pin
that the extracted method actually refuses what Upsert would (fail under a
reverted Validate that is a no-op); ValidateIsANoOpForADocumentTypeItDoesNotGovern
pins that a document type it has no opinion about doesn't crash it.
Verified tools/AetherDb/AetherDb.csproj still builds clean against the pinned
CultLib worktree.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
StatValidation.ValidateRoleUsage walks every PerformanceStat field on every
behaviour and refuses a StatSource.Quality term whose Role is not in the
design's own declared Roles -- wired into AetheriaStores.Open and
CultRecordRefs.Upsert alongside the existing heat/modifier/power-supply
checks. Lot.QualityForRole already falls back to workmanship for a role an
old lot never rolled (forward-compat for a role added after minting); this
is the other direction: a term naming a role the design never declared at
all is an authoring mistake, not a lot predating the role, and must fail
loudly.
RoleAuthoringTests.cs pins three rules with both a positive and negative
control: Upsert refuses an absent-role term but accepts a declared one, and
two products of one design with different ProductRole means mint lots that
resolve a role-termed stat to different, deterministic values.
tests/mutation_tests_stats_power_cut8.py (this repo's own numbering follows
cut7.py's brownout work) mutates ValidateRoleUsage in both directions
(never-refuse, always-refuse) and QualityForRole's fallback, plus
ItemManager.CreateLot's mean read -- all six behave as declared.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
docs/stats-and-power-cut.md Cut 6, purely additive per its own map: the enum member, the
resolver's generation-cache and the bus's PowerSupply write already existed (Cuts 2/3/5)
as an identity everywhere. This lands the two things Cut 6 actually adds.
The curve: EquippedItem.PowerSupplyFactor now returns pow(PowerSupply, exponent) instead
of the Cut-3 identity stub, so a stat that declares a PowerSupply term degrades as this
tick's grant falls and reads unchanged at full supply (pow(1, n) == 1 regardless of n).
PowerBus.AllocateTiers now invalidates StatSource.PowerSupply once per draw per tick -- the
missing half of the wiring named in the cut map -- so the resolver actually recomputes when
the grant moves, using the same per-source generation scheme Heat/Durability already use
(unconditional, at most once per tick, never touching a stat that did not declare the
source).
The request-independence rule: a stat that decides a power request may not depend on power
supply, directly or through a modifier chain. Direct dependency is checked statically
(StatValidation.ValidateNoPowerSupplyOnRequest, ItemData.cs) against a fixed registry of
(BehaviorData type, field) pairs -- the one PerformanceStat each IPowerConsumer's own
PowerRequest evaluates as its request, a compile-time fact rather than something authored
per instance -- and runs at catalog load (AetheriaStores.Open), at Upsert, and again at
equip (EquippedItem's constructor) as defense against the two named bypasses that write a
catalog document without going through Upsert. The modifier-chain half is dynamic
(StatModifier.ValidateNoPowerSupplyChain): a modifier's own magnitude stat is walked
through the same magnitude->target edges ValidateNoCycle already builds for the self-cycle
check, and refused at Initialize (equip time) if it is power-tainted and targets a known
request stat, naming both items.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
PerformanceStat kept two Dictionary<Entity, ...> fields for attached modifiers,
grown by one key per entity that ever evaluated the stat and never trimmed
(docs/stats-and-power-cut.md §0.3). StatResolver.cs is the new sole owner of
every resolved (item, stat) value and every attached modifier, one instance
per Entity and reachable only from it. EquippedItem and ConsumableItemEffect
now read and write through it instead of through the catalog object; the two
dictionaries and their four accessors are deleted outright, not hidden.
StatModifier no longer resolves its (type name, field name) reference by
runtime reflection search on every equip: StatValidation.ResolveStatField
does it once, memoized, and StatValidation.ValidateStatModifiers checks it at
catalog load and at Upsert, naming the record instead of failing with an
opaque NullReferenceException the first time something equips it. A modifier
whose own magnitude stat is (directly or transitively, through another
modifier on the same entity) one of the stats it writes is refused at equip:
resolving it would depend on a value computed from itself one tick late.
A resolved value now caches by the generation of the sources its declared
Terms actually read, invalidated per source by EquippedItem.UpdatePerformance
(Heat, Durability) and ConsumableItemEffect.Update (ConsumableProgress), so a
stat recomputes only when a term it declared moves.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
StatValidation.ValidateHeatResponse now refuses NaN in any of the four heat-response
fields, a zero-span range (Minimum == Maximum -- Tractor Beam's exact bug), and a
plateau whose low or high edge pokes past its bounds, matching the R-heat ruling and
the Performance() comment that already claimed this was enforced. The writable-path
hole is closed for every path this repo owns: CultRecordRefs.Upsert (AetheriaStores.cs),
the one function every tool, test and migration script writes catalog documents
through, validates before a write reaches disk instead of waiting for the next Open().
CultCache Studio's generic editor bypasses Upsert and still has no per-document
validation hook -- that hole is CultLib's, not this repo's, and stays named in the code
rather than silently assumed closed.
EquippableItemData's four heat fields get non-degenerate defaults (0/100/50/20, the
same shape HeatResponseTests already hand-authors) because dozens of existing test
fixtures across the suite build designs that never cared about heat and defaulted into
the same zero-span trap Tractor Beam shipped with; enforcing the zero-span refusal
without this would have required touching every one of those fixtures instead of the
root cause.
Four new mutations pin S7's survivors, each with its own dedicated test rather than a
coincidentally-sensitive existing one: out-of-bounds semantics (return 1f instead of 0f
outside bounds), the plateau-clamp validation's two sides, and a Quality term ignoring
its own exponent (StatsReadTheLot's Exponent = 1 everywhere could not tell "reads the
exponent" from "always uses 1"; the new test uses Exponent = 2). Performance()'s own
runtime plateau clamp is separately confirmed dead code -- the outer bounds guard makes
it unobservable at every input, verified by an exhaustive numeric sweep -- and is
recorded as such rather than forced into a fake kill.
80 tests (9 new), 17 mutation cases in tests/mutation_tests_stats_power_cut1.py, all
green.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
PerformanceStat's four fixed fields (HeatExponentMultiplier,
DurabilityExponentMultiplier, QualityExponent, FromRole) tombstone into a
declared List<StatTerm> (source + exponent + role), walked by the one
evaluation path Cut 0 unified. A stat with no terms resolves to Max (the
same identity a stat with all-zero exponents already produced), so the
migration is exact: a zero multiplier becomes no term, not a term with
exponent 0.
R-heat (operator, docs/stats-and-power-cut.md): EquippableItemData's
HeatPerformanceCurve (a BezierCurve, plus the 100-sample scan that inferred
OptimalTemperature and its cache) is retired in favor of an authored
minimum, maximum, optimum and plateau width, with linear falloff outside
the plateau. StatValidation refuses an optimum outside its bounds or a
negative plateau width, loudly, at catalog load. The plateau's consequence
rides along for free: Wear's thermal term is pow(ThermalPerformance, ...),
and Performance() now returns exactly 1 across the plateau, so a
well-managed item takes wear only from deltaTemp there -- the
operational-lifespan lever the operator asked for.
ConsumableItemEffect's progress-through-duration substitution for "heat"
becomes its own StatSource (ConsumableProgress) rather than an unconditional
override applied regardless of what a stat declares.
Tests: LoadoutTests' four PerformanceStat-shaped fixtures rebuilt against
Terms/authored heat fields; HeatResponseTests.cs (new) pins load
validation, the plateau shape, and the three wear-lever claims (held
inside takes no thermal wear, held outside does, a fast swing across still
wears) each under its own mutation in tests/mutation_tests_stats_power_cut1.py
(new, 10 rules including a no-op control; all pass).
64 pre-Cut-1 tests still pass unmodified; 71 total with the 7 new ones.
Unity batchmode: 0 error CS. ShieldPanelCut1Verify and Cut2Verify pass;
Cut3Verify fails with an unrelated pre-existing "Kernel 'KInit' not found"
compute-shader error, reproduced against Cut 0's own landing state with no
shader/compute file in this diff -- flagged for Soul, not fixed here.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Quality, Ingredients and Product leave CraftedItemInstance; a unit now
carries only a LotId into the run-scoped ProvenanceLedger. ItemManager
gains the one lot-minting path (CreateLot/CreateInstance(int)), GetLot,
and Brand(), which derives a maker and product from provenance instead
of a stored field. RunSave persists the ledger pruned to what the saved
zones' entities still reach; the live ledger is never pruned, so a
floor pickup collected after a save still resolves. CopyBuild is gone:
matching hardpoints now share the same lot instead of copying values.
PropertiesPanel shows the derived brand where the design's manufacturer
used to be. AetherDb's census flags duplicate (maker, design) products
and its save command checks every crafted instance's lot against a real
run store.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Globals are owned by lifecycle, so the cache must never run without one,
however the catalog is opened. Before this, opening the catalog writable
skipped the check entirely. The editor now opens the catalog writable, so a
missing global went silent in play mode.
AetheriaStores.Open now skips the check only for a writable catalog that
has no records: a seed store being authored from nothing, as the test
fixtures do. A populated catalog is always checked, and so is a read-only
empty one. Nothing else relied on the old skip: the game has no catalog
globals, AetherDb opens the populated Aetheria.cc, and every writable test
reopen already holds its fixture global.
WritablePopulatedCatalogMissingGlobalIsLoud removes the global from a
populated catalog and checks that a writable reopen throws. It fails when
the old writable-skip condition is restored.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Loadouts are ship presets (variants) for spawners, not a player feature.
- Route Loadout to the catalog; the player store no longer holds it.
- Materialize takes no credits: affordability, the charge and Loadouts.Price
are deleted, since only the player menu priced loadouts. Availability,
key-order product choice, all-or-nothing failures and the weapon-group
rules stay.
- Delete the InventoryPanel Save Loadout and Restore Loadout entries and
RestoreLoadout, which also removes its docking-bay overwrite.
- The editor opens the catalog writable in the one process cache, and the
editor-only console command capturepreset "<name>" [replace] captures the
piloted ship. Loadouts.Commit keys a preset by name, refuses a same-name
replace unless asked, and commits conditionally on that key. The single-file
store then lands only that record onto the file as it is on disk. An
unconditional commit would re-serialize every live catalog instance,
including any that play has mutated.
- Tests: capture -> commit -> fresh reopen round-trip, explicit replace with
no duplicate, and a preset commit that does not persist a mutated live
Faction. The money-only tests are removed.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Loadout.cs owns loadouts: the document, capture, save and materialization.
- Loadout ("aetheria.loadout", player store) holds a hull design ref, and
per slot a hull cell, a rotation and an EquippableItemData design ref,
plus weapon groups as slot indices. It names no product, manufacturer,
unit roll, cargo, faction, position or behaviour state.
- Loadouts.Capture records the hull design and one slot per equipped item
(gear, cargo bays, docking bays). Entity.Equipment also holds the hull's
own unit (MapEntity adds EquippedHull); that is Loadout.Hull, not a slot.
- Loadouts.Save is the only writer: one Commit to the player store, reusing
the existing key for the name.
- Loadouts.Materialize is the only builder of a ship from a loadout. For
the hull and each slot in order it resolves the design and takes the
first product in catalog order that LoadoutGenerator.IsAvailable (now
public) accepts, with no any-manufacturer fallback. Every failing slot is
listed; units are built only after everything resolved, and any resolve
or fit failure returns null with no ship, no charge and no write.
- Loadouts.Price is the hull design's price plus each slot design's price.
- InventoryPanel: Save Loadout asks for a name (default: the ship's name)
and saves the capture. Restore Loadout, shown when docked with saved
loadouts, lists each with its price, debits credits only after
Materialize succeeds, then docks the ship as before; a failure opens
"Loadout cannot be built here" with one line per failing slot.
Tests: new LoadoutTests (3): LoadoutRoundTripsThroughAFreshCache,
LoadoutHoldsNoRunOrGalaxyRefs, MissingDesignReports. Each fails with its
rule mutated out. The fixture hull is 3x3, not the spec's 2x2:
HullData.InteriorCells is Shape.Shrink(), so a 2x2 hull has no interior
cell a cargo bay can occupy.
Builds: headless, 14 tests pass, Unity batchmode compile reports no error CS.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
AetherDb gains legacy-census and a one-shot import (tools/AetherDb/Import.cs,
deleted by Cut 10). The import rewrites each legacy [tag, payload] record by
the target type's [Key] shape: slot 0 Guid becomes the record key and nil,
CultRecordRef members and DatabaseLink wrappers become D strings, the all-zero
Guid becomes nil, StatModifierData.RequireBehavior keeps the simple type name.
All 180 records land in one Commit; a second import refuses.
InputLayout moves to the catalog (Q9-1 A), with the test fixture seeding it
while the catalog is writable. GameData/*.cc is LFS, GameData/*.lock ignored.
The legacy msgpack and ansi104.json sources are removed.
Imported: SimpleCommodityData 13, CompoundCommodityData 51, GearData 25,
HullData 3, NameFile 12, Faction 12, PersonalityAttribute 3, CargoBayData 4,
DockingBayData 1, WeaponItemData 18, FactionProductData 37, InputLayout 1.
Six refs dangled already in the legacy data and are preserved as authored.
The Key(0) precondition checks only the Guid-slotted tagged types; InputLayout
is imported bare and keeps its Key(0).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Cut 8b. Aetheria.Shared builds headless against GameCult.Caching and
GameCult.Caching.MessagePack at the pinned CultLib; the pin guard also
checks the Caching project exists.
Documents: [CultDocument("aetheria.<type>", "1")] with [MessagePackObject]
and integer [Key]s on the Q7 concrete types (the eight ItemData leaves,
Faction, FactionProductData, PersonalityAttribute, NameFile, OrbitData, the
four BodyData leaves, SavedZone, SavedGame, PlayerSettings, InputLayout).
SavedGame and PlayerSettings are [CultGlobal]; Name members are [CultName].
ItemData, CraftedItemData, EquippableItemData, BodyData and AgentTask lose
[MessagePackObject]; BodyData and AgentTask lose their [Union]s. InputLayout
moves into ServerShared so AetheriaStores can route it.
References: Guid and DatabaseLink<T> members become CultRecordRef<T>;
Guid-keyed maps become ref-keyed with [CultReference(..., many: true)];
ZonePack holds refs to orbit and body records. Runtime maps (Zone orbits,
bodies and belts, cargo ItemsOfType, agent targets) key by CultRecordKey.
Identity is the record key: ID comparisons become reference or key
comparisons. StatModifierData.RequireBehavior is a simple type name.
AetheriaStores.Open composes the catalog (read-only unless writable), run
and player stores, one home per type. CultRecordRefs carries IsSet, a
null-safe Get(ref), RefOf (key from TryGetHandle) and Upsert.
ItemManager.GetData is the one item resolution path. MathResolver is
declared by [assembly: CultCacheFormatterResolver].
Inspector attributes: text, range and asset path map onto CultLib's
CultInspector attributes (asset path only on string members); every
replaced Aetheria attribute keeps [Inspectable] for the runtime panel.
Builds: Aetheria.Shared. Does not build: AetherDb and the Unity project,
which follow.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>