### 0.1 The request, in the operator's words (2026-09-30)
> "I feel like this would be much more manageable as a list of things to verify over time rather than a
> monolithic smoke script that doesn't respect how the game works. Move gear onto Djinni? That's a lot of items
> to spawn, and am I expected to have the catalog memorized to spawn them? If you want me to test a bunch of
> behaviors under specific conditions, find a way to spawn me in an arena where those conditions are present. If
> you want me to kill a guy and loot him, spawn me next to a ship with no armor or weapons, that sort of thing. I
> wouldn't mind a scenario affordance in the main menu for this sort of job, that way the test fixtures can be
> authored, retained and available. But that sounds like a Eureka cut."
What those words settle, so no question below re-asks them:
- **A scenario is an arena whose conditions are present at spawn.** The player's ship and every other entity are
already fitted and placed. The operator never spawns gear by catalog name and never fits a ship by hand.
- **The fixtures are authored and retained,** and the main menu lists them.
- **Verification is a list of behaviours checked over time,** not a play order.
### 0.2 Ends
- The main menu lists scenarios. Picking one starts a run in its arena.
- A scenario is typed catalog data. It is authored once, kept in git, and replayed at any later revision.
- Every operator check is a typed **check** record: what to do, what to observe, where the rule came from, and
the scenario that sets it up. Results accumulate as typed records carrying the revision they were observed at.
- Checks A-F and checklist steps 7-20 are the first consumers (section 5).
### 0.3 Invariants
- **One owner decides what a new run starts with.** A plain New Game and a scenario launch share the menu path,
the galaxy build, `StartGame` and the staging owner. Scenario launch is New Game with a scenario as input; there
is no second boot path. A plain New Game is the absent-scenario case.
- **One primitive admits an entity into a zone.** Zone construction, staging, warp arrival and undock all admit
through it, and agents are created there and nowhere else.
- **A scenario sets conditions and nothing else.** Once staged, its entities are ordinary run state. The rules
under test run through the same code whether or not a scenario started the run. No scenario-only branch exists
in simulation code.
- **Fixtures are typed state.** Scenario, loadout presets and test designs are CultCache catalog records. Checks and
results are CultCache records. No load-bearing JSON, text scripts or prose checklists.
- **A scenario that cannot stage is refused before anything is cleared,** with its failures listed.
- **The catalog stays read-only at runtime** (`AetheriaStores.cs:13-21`).
### 0.4 Not in scope
- The full run lifecycle owner (`Run`) of `docs/headless-playground-cut.md` Cut 1 (warp, dock, save, death as one
ServerShared owner) and its `AetherDb play` interpreter (Cut 3). This map takes over only the new-run staging
half. The playground map is stale (anchors `1e647953`, fork S superseded by settings-globals) and must be
re-anchored before Hands. When it lands, its `new` command stages a `Scenario` record instead of reading a
`.play` text line.
- Brokkr. It is not needed for any cut here: agents author scenarios headless through `AetherDb`, and the operator
launches them from the menu. Brokkr's Unity actions (`setEditorPlayState`, `captureEditorView`, scene edits;
`F:\Projects\Brokkr``1357705`, read) have no way to pick a scenario in the menu. Agent-driven Unity play would
need an Aetheria install cut plus a "launch scenario" intent. Recorded as a follow-up, not a fork.
- The ship-purchase docking-bay gap (checklist section 8). It is a design gap in `TradeMenu.Buy` and
`CommissionShip`, not a verification affordance. `CommissionShip` stays; `TradeMenu.Buy` is its consumer.
- Tier colours (settings-globals Cut 1). The check is recorded as a known failure (section 5).
- Balance and feel tuning. A check can record a feel call; it does not tune.
---
## 1. Body findings
### 1.1 The new-run path today (read)
| Concern | Anchor | Notes |
|---|---|---|
| Menu | `MainMenu.cs:92-184` | Continue `:103-108`; New Game `:110-176`; Settings; Quit. The same component runs in-game (`InGame`, `:96`), so the menu reappears on death (`ActionGameManager.cs:1171`). |
| Clear the run | `MainMenu.cs:113` | `RunSave.Clear` at click time, before the galaxy is built. |
| Galaxy | `MainMenu.cs:124-175` | `TutorialPassed` is never set anywhere, so New Game always takes the prelude branch `:146-175`. The background noise position comes from `UnityEngine.Random` (`:128,153`). The seed defaults to the clock (`Galaxy.cs:106,158`). |
| Handoff | statics `ActionGameManager.CurrentGalaxy`, `IsTutorial` (`:96-97`) | read by `StartGame`, `SectorMap`, and nulled by `Die` (`:1173`). |
| Start | `StartGame``:796-834`, called from `Start``:491` | New run `:803-815`: `PopulateLevel(Entrance)`, `LoadoutGenerator.GenerateShipLoadout` filtered by `GameSettings.StartingHullName` (`GameSettings.cs:13`, authored `LonginusX`), unpack, `Zone.Entities.Add`, `Activate`, `BindToEntity`. Continue `:817-833`. |
| Zone entry | `PopulateLevel``:710-744` | generates the pack lazily (`:714-722`), moves the pilot (`:731-737`). |
### 1.2 What a scenario can reuse (read, one probe)
designs with rotation, and weapon groups. It is a catalog type (`AetheriaStores.cs:9`), authored in Studio or by
the editor console command `capturepreset` (`ActionGameManager.cs:624-662`, `Loadouts.Commit``Loadout.cs:70-87`).
`Loadouts.Materialize` (`:96-159`) builds a live `Ship` all-or-nothing, listing every failure.
- **No game path calls `Materialize`** (`LoadoutGenerator.cs:152-153` says so; only `LoadoutTests.cs:310,899`
call it).
- **(probe)** The live catalog holds no preset: the schema name `aetheria.loadout` does not occur in
`GameData/Aetheria.cc` (12,698,108 bytes at `dcd7bbc5`), while every populated type's name does (for example
`aetheria.hulldata`, `aetheria.weaponitemdata`). Method: `grep -a -o` over the file.
-`Materialize` always builds a `Ship` (`:131`). A turret hull cannot be materialized.
- It resolves every design through a product (`Resolve`, `:103-115`). A design with no product fails.
- **Unbranded lots exist.** `ItemManager.CreateLot(design, maker, quality)` (`ItemManager.cs:161`) with no maker is
how `give` made its items (`ActionGameManager.cs:538,542`). Provenance's "no Manufacturer row" check depends on
them.
- **Economy exclusion follows from products.** Generation iterates products and requires
`product.Manufacturer.IsSet()` and `design.Price > 0` (`LoadoutGenerator.cs:126-133`). A design with no product
is never generated, stocked or sold. Loot is what a killed entity carries (`EntityInstance.cs:302-321`), so it
cannot leak through loot either unless a scenario fits it.
- **Prelude galaxies offer every product.** `IsAvailable` returns true when `Galaxy.IsPrelude`
(`LoadoutGenerator.cs:154-156`). A preset materialized in a prelude arena never fails on faction availability.
- **Agents are made only at zone construction.** `Zone`'s constructor gives every packed non-player `Ship` a
`Minion` (`Zone.cs:120-131`, `CreateAgent``:135-142`). An entity added later gets none. A `Minion` targets the
first visible enemy and fights (`Minion.cs`).
- **An entity joins a zone in four other places,** each by hand: `spawnturret` (`ActionGameManager.cs:587`),
`PopulateLevel` (`:735`), `StartGame` (`:813`) and `Entity.TryUndock` (`Entity.cs:1006`).
- **IFF overrides are entity-local and unsaved.** `Entity.SetIff(other, bool?)` (`Entity.cs:529-533`) decides
`IsHostileTo` ahead of faction rules (`:535-552`). A `Materialize`d ship has no faction, so it is neutral unless
overridden.
- **A target that dies and a target that leaves are the same case in fire control.**
`targetGone = shot.Target != null && !zone.Entities.Contains(shot.Target)` (`FireControl.cs:610`), and death
removes the entity from `Zone.Entities` (`Zone.cs:93`). Check A.3 ("the target warps away") is therefore set up by
killing a fragile target while a fused round is in flight. No departure machinery is needed.
- **An orbital entity with no orbit stays where it is put** (`OrbitalEntity.cs``Update`: position follows the
orbit only when one is set). That is how `spawnturret` placed turrets (`ActionGameManager.cs:583-588`).
- **Headless tests already open the live catalog and the authored settings**
(`RestoredHullsTests.cs`: `AuthoredSettings.Load`, `OpenReadOnlyRealCatalog`). A staging test can generate a
prelude galaxy, build the arena zone and stage a scenario with no Unity.
### 1.3 Debug affordances a scenario replaces (read)
| Affordance | Anchor | Added for |
|---|---|---|
| `give <name>` | `ActionGameManager.cs:515-544` | ad hoc items; the hull route through `CommissionShip` was added tonight in `f935c29b` for step 7 |
| `spawnturret` | `:564-589` | a hostile turret next to the player |
| Neutral wanderers | `ZoneGenerator.cs:342-356`, `EligibleWandererFactions``:361-365`, `ZoneGenerationSettings.NeutralWandererCount` (`Settings.cs:156`), tests `IffAndCombatTests.cs:329-350` | commit `f732cff9` (2026-09-17): "Spawn neutral wanderers so combat has something non-hostile to target". The code comment calls it a "Testing affordance". Every generated zone carries two. |
Staying, because no scenario replaces them: `iff` (flip a live target's stance mid-play), `trackmissile` (camera),
`revealzones` (map), `tow` (a verb bound to the console), `capturepreset` (the in-play authoring tool for presets).
### 1.4 Collisions with open branches (read)
-`codex/mining` rewrites `Zone.cs` (+239/-). Cut 1 edits the constructor loop and adds the admission primitive.
Land Cut 1 after mining merges, or re-anchor.
-`codex/moddable-ships` edits `AetheriaStores.cs` (line 9, the catalog type list). Cuts 1 and 3 edit the same
lines.
-`codex/item-provenance` carries the narrative map, which edits `MainMenu.cs:132,160` and the `Galaxy`
constructors. Cut 2 edits the same New Game block.
---
## 0b. Identity, lifecycle, authority
No cut is mapped against an empty cell.
| Kind | What names it | Lifecycle | Who decides |
|---|---|---|---|
| **Scenario** (`aetheria.scenario`, catalog) | `CultName``Name`; key `scenario:<Name>`, derived like `Loadouts.KeyOf` | Authored, then edited in place. A rename is a new record; the old one is deleted, and the checks that pointed at it are repointed in the same commit. A result never points at a scenario (it points at a check), so history survives. A scenario whose presets stop materializing is caught by the staging test (Cut 4), not at play time. | Authored by agents through `AetherDb` or by the operator in Studio. The operator owns what an arena contains. |
| **Test design** (catalog `ItemData`, no product) | name; key minted by `Upsert` | Authored with the scenario that needs it. Lives as long as a preset references it. Per Q3. | Operator (Q3) on whether they exist at all. |
| **Check** (`aetheria.check`, ledger store) | `CultName``Slug`; key `check:<Slug>` | Authored. Its statement is edited in place when a ruling changes (git keeps the history). It is retired, never deleted, once any result references it: `Retired` plus a reason. | The rule's owner (a ruling or a map) decides what it says. Agents transcribe it with a source pointer. |
| **Check result** (`aetheria.checkresult`, ledger store) | key `checkresult:<Slug>:<At as UTC ticks>` | Append-only. Never edited or deleted. A correction is a new result whose note says what it corrects. The latest by `At` is the current status. Order is carried in the data (`At`), not in delivery. | The operator's observation. Who writes the record is Q2. |
| **Pending scenario** (runtime) | `ActionGameManager.PendingScenario`, a static | Written once by the menu's launch, read and nulled once by `StartGame`'s new-run branch. Null for Continue. Never saved. | Command-only transport. It decides nothing. |
| **Staged entities** (runtime, then run state) | ordinary entities | Born at staging. From then on they are run state and saved with the zone like any other entity. Two things are not saved: whether a ship was left unpiloted, and scenario IFF overrides. After Continue, an unpiloted scenario ship gets a `Minion` by the constructor rule and its stance reverts to faction rules (neutral, since presets carry no faction). A scenario run is meant to be played through in one sitting. | `RunStart` at birth; the zone afterwards. |
---
## 2. Target shape
### 2.1 Types (names and rules; bodies are Hands')
| Slug | Statement (abridged; the record carries the full text) | Source | Setup |
|---|---|---|---|
| `restored-hull-flies` | Thrusters move and turn the Djinni. | step 7; `locomotion-cut.md:501` | Djinni shakedown |
| `restored-hull-speed-cap` | Take the hull offline with thrusters firing; speed does not run away. | step 7; `:377` | Djinni shakedown |
| `weak-reactor-starves-legibly` | A reactor too small for the loadout shows starvation, not a cooked reactor. | step 8; `stats-and-power-cut.md:705-706` | Starved reactor |
| `brownout-degrades` | Brownout reads as degradation, not breakage. | step 9; `:774,990` | Starved reactor |
| `brownout-refill-stutters` | A weapon refilling under brownout stutters rather than firing at full rate. | step 9; `:729-730` | Starved reactor |
| `memory-flat-across-zones` | Several zones with kills; memory does not climb. | step 10; `:672-673,980-982` | Long haul |
| `side-mounts-fire-abeam` | Side-mounted weapons fire at the beam target. | step 11; `fire-control-cut.md:397-398` | Arcs |
| `turret-tracks-round` | The hostile turret tracks the player all the way round. | step 11 | Arcs |
| `no-fire-through-hull` | Nothing fires at the stern target through the hull. | step 11 | Arcs |
| `shots-roll` | Rolled impacts and misses; a miss reads as a near-miss; damage matches the HUD. | step 12; `:659-661` | Duel |
| `subsystem-aim` | Reveal and selection work; aiming at a revealed subsystem concentrates damage. | step 12 | Duel |
| `kill-drops-loot` | The kill drops its cargo; pickup stores it; the tractor beam pulls. | step 12; `headless-playground-cut.md:789-794` | Duel |
| `target-vanish-safe` | The target dies with shots in flight: no crash, no frozen ship. | step 12; `fire-control-cut.md:1464-1470` | Duel |
| `beam-reads-higher` | The HUD `hull` factor reads higher from the beam than from the bow. | step 13; `:2380-2383` | Duel |
| `armour-face-missile` | Turn the armoured face into the AI's slow missile; it lands there. | step 13 | Duel |
| `ai-engages` | The AI still engages normally. | D; `:2658-2660` | Duel |
| `launcher-edge-pulse` | Launcher into the bow, then a flank: the schematic pulses the facing edge only. | step 14; `:2519-2520` | Launcher angles |
| `guided-explodes-on-detonation` | A guided round's explosion shows only when the simulation detonated. | E | Launcher angles |
| `guided-target-left-fades` | Kill the target while a guided round flies: the round fades with no explosion. | E | Launcher angles |
| `guided-hit-at-target` | A guided hit plays its effect at the target. | E | Launcher angles |
| `fused-clear-line-max-range` | Nothing selected, clear line: the round bursts at max range along the arc-clamped aim. | A.1; `:2625,2636-2642` | Fused rounds |
| `fused-first-hull-stop` | Nothing selected, aim at the bare hull: the round stops and detonates on it. | A.2; `:2632-2634` | Fused rounds |
| `fused-target-left-no-burst` | Fire at the fragile target, kill it with the direct weapon before arrival: no burst. | A.3; `:2636-2642`, `FireControl.cs:610` | Fused rounds |
| `fused-out-of-arc` | Select the stern target: the round fires and bursts at its range along the arc-clamped aim. | A.4; `:2643-2645` | Fused rounds |
| `fused-arming-pushout` | Select the close hull: the burst is pushed out to `BlastRadius` from the ship's centre. | A.5; `:2646-2653` | Fused rounds |
| `airburst-at-model` | A proximity airburst beside the LonginusX lands damage where the model shows the blast. | step 15; `:3094-3097` | Fused rounds |
| `penetrator-reaches-interior` | A delayed penetrator into the LonginusX nose detonates inside. | step 15 | Fused rounds |
| `hud-forecast-fused` | The HUD shows Direct, Burst at N m or Refused for fused weapons, and a percentage for direct weapons. | C; `:2661-2663` | Fused rounds |
| `unbranded-no-manufacturer` | An unbranded smoke weapon shows no Manufacturer row. | step 6 | Fused rounds |
| `refused-shot-free` | A refused shot costs no ammo, energy, heat, sound, visibility or cooldown. | B; `:2664-2666` | Refused rounds |
| `refused-beam-no-flicker` | A refused beam never starts. | B | Refused rounds |
| `refused-charge-never-charges` | A refused charged weapon never starts charging. | B | Refused rounds |
| `burst-cadence` | Burst cadence is unchanged. | F | Weapon feel |
| `charge-release-press` | A fast release-and-press on a charged weapon starts no second charge. | F | Weapon feel |
| `existing-build-play` | Under "Use Existing Build", the Weapon feel checks hold (GUID keys in the player catalog). | step 20; `addressables-cut.md:367-370` | Weapon feel, Setup: play mode Use Existing Build |
| `ai-never-fires-refused` | The hostile AI whose only weapon is refused never fires it. | D | AI fused discipline |
| `ai-never-bomb-fishes` | The neutral AI carrying a fused weapon never fires it. | D | AI fused discipline |
| `continue-keeps-items` | Warp, quit, Continue: same items, same tier and brand. | step 16; `item-provenance-cut.md:536-537` | Long haul |
| `death-ends-run` | Die: Continue is disabled and `run.cc` holds no ledger. | step 17; `:540-541` | Long haul |
| `shield-panel-rig` | The numbered shield-panel look, including the Cut 5 dicing and break-through. | step 18; `shield-panel-cut.md:683-700,737-739` | Setup: FieldShieldTest scene |
| `studio-*` (5 checks) | The fire-control union renders; the schematic underlay; restored hull records; the fuse fields edit; the provenance ledger renders from a real `run.cc`. | step 19 | Setup: CultCache Studio |
| `addressables-duplicates` | Analyze the duplicate dependencies: 5 known leftovers. | step 20 | Setup: Addressables Analyze |
| `new-game-start`, `purchase-keeps-brand`, `tier-colour` | New Game; buying keeps the brand; tier colours show. | steps 3, 4, 6 | Setup: New Game |
-`tier-colour`: fail ("known defect, `settings-globals-cut.md:150`; fixed by settings-globals Cut 1").
-`unbranded-no-manufacturer`: blocked ("`give Lamp` named a test fixture").
-`loot-keeps-brand`: no result ("not yet reported").
### 5.2 Scenarios
All use `Ambient: false` except Long haul. Positions are authored in the seed and pinned by the condition tests.
| Scenario | Player | Entities | Checks |
|---|---|---|---|
| Djinni shakedown | Djinni, full fit | none | 2 |
| Starved reactor | a combat hull with an undersized reactor, energy weapons and a shield | 1 unpiloted bare hull ahead, neutral | 3 |
| Arcs | a hull with side mounts | 3 unpiloted bare hulls (bow, beam, stern), neutral; 1 turret preset, hostile | 3 |
| Duel | combat fit | 1 piloted LonginusX, hostile, carrying a slow missile weapon, cargo holding a branded item | 8 |
| Launcher angles | GT 3K or pswarm plus a direct weapon | 1 unpiloted LonginusX, hostile; 1 fragile bare hull | 4 |
| Fused rounds | Smoke Proximity, Smoke Delayed, a direct weapon | unpiloted: LonginusX mid range, hostile; bare hull on the clear bearing; fragile bare hull; close bare hull inside `BlastRadius`; stern target | 9 |
| Refused rounds | Smoke Refused (instant, beam, charged) plus one working weapon | 1 unpiloted bare hull | 3 |
| Weapon feel | a hull with a 2x2 energy hardpoint: ChargeBlast SG, Autocannon, a burst weapon | 1 unpiloted bare hull | 4 |
| AI fused discipline | a sturdy hull | 1 piloted neutral with a fused weapon; 1 piloted hostile whose only weapon is refused | 2 |
| Long haul | standard combat fit, `Ambient: true` | none | 3 |
### 5.3 Test designs (Q3)
None has a product. They are unbranded and outside the economy.
**Verdict: hold for one fix batch.** It is queued, not dispatched, because the operator ordered a drain.
No correctness bug was found. The suite passes 516/516 with the catalog `b698e224` and the settings `b2e346f5` that the commit pins. The shared `pins/data/Settings.asset` (`347752dd`) is scenarios' file, so any runner that mounts that path by default has been testing mining with the wrong settings.
- **F1 (medium, operator; fixed on master `df7c44f2` by the targeting index, no longer a scan; still O(rocks) per press, deferred to `mining-index-tree` and `mining-belt-cells`):** picking a rock in a big belt costs one scan of every rock the search circle reaches. `Zone.ChunksNear` (`Zone.cs:333`) and `VisibleChunksInReach` (`Entity.cs:378-385`) run on every reticle, next or previous key press. Measured cost: 63 ms at 30k rocks, 449 ms at 300k and 4.3 s at 3M. The per-tick cost is fine.
- **F2 (medium):** nothing tests that the shipped catalog has a rock kind. Taking either side of the catalog conflict unchanged ships dark belts silently. Add a test for "at least one rock kind with weight above 0".
- **F3:** the merge rule is corrected above.
- **F4 (medium):** nothing tests the ping half of `Sensor.Gain`, because the golden fixture saturates at the cap. Two mutants survive (`Sensor.cs:168`, `:189`). Soul's sweep fixture kills both and gives the same hash (`35DE0E26…`) before extraction, at extraction and at the tip.
- **F5:** `FieldKinds.Ensure`'s explicit save (`ZoneData.cs:162`) is needed on a clean store. Keep it and commit the probe.
- **F6/F7/F8 (low):** the `SetTarget` refusal is tested only on dark rocks (`Entity.cs:345`). The outer skip edge of `ChunksNear` is untested. Rock reach reads `Enabled`, and nothing tests it.
- **Deviation (operator):** launchers count toward rock reach (Q13), but under Q12 they can't mine. So after Cut 4 a player can pick a rock that no weapon can act on.
- **Plausible (operator visual check):** the target indicator uses `AsteroidVerticalOffset`, but the shader also subtracts the nebula surface height.
Promises that held:
- The `Gain` extraction is bit-identical.
-`SetTarget` is the only writer.
- A dark target is dropped on the tick and on `Activate`.
- Docking, broken rocks and wormholes are handled.
- The catalog has 206 identical records, plus one kind and 18 weapons now Exact.
- Old saves get a kind on first load.
The fix batch commits Soul's four probes (in the session scratchpad, `soul-mc3-run/probes/`) and the catalog-kind test.
**Mining fix batch, Hands (2026-10-01): `f88d1fc1`, `83d8371e` on `codex/mining`. 523/523; 9 mutants each killed by their own test.**
- Launchers: `Weapon.CanMine` (virtual, true by default; `LockWeapon` sets false) is the one predicate. Reach reads `CanMine && Active`. Cut 4's firing gate (`FireControl.cs:175`) must read the same predicate.
- Soul F2/F4/F5/F6/F7/F8 tests are committed.
- Belt freeze **not built; stopped at a fork.**
- Each rock has its own angular speed (`Distance` is a continuous float), so no band shares a phase.
- At 3M rocks, 633k are in reach and 2,240 are visible.
- Proposed: a derived per-belt index (bands of 256 by distance, sorted by turn, with the query arc widened by rate spread and at most 8 band re-sorts per query); a visibility upper bound that skips dark bands; best-first next/previous and reticle; the rules move from `ActionGameManager` to `Entity`.
- Work in progress (unbuilt) is in the session scratchpad: `hands-mc3fix-index-wip.patch`.
- Question: today's Previous with no target picks the second-farthest rock (off by one). Hands recommends fixing it.
- **Structure:** "Let's go for it. There's a voice in my head screaming that this is too much code and we should just reduce the whole belt to a single entity, but this stuff will be extra important for when we have EW and much more crowded levels. Just don't specialize the indexing too much towards asteroid belts, because we'll want a bunch of subsystems feeding targeting data. Think the spacebound microfauna from the slime mold experiment, pretty sure there's a campaign for that."
- The index is a general targeting index that many sources feed: belts, entities, later EW and microfauna. Belts are one provider, not the shape of the index.
- The slime-mold experiment is `Assets/Shaders/Compute/Slime/Slime.cs`. No campaign doc was found by grep on master docs or by voidbot; ask the operator before assuming one.
- **Previous off-by-one:** "Fix the off-by-one". With no current target, Previous picks the farthest rock.
- **Next step:** an Imagination pass (Opus) maps the general index. Inputs: the provider seam, motion models (orbiting rocks with per-rock rates, and moving entities), the brightness/visibility bound as a per-provider bound, and best-first queries. Hands' work in progress (`hands-mc3fix-index-wip.patch`) is input, not spec. Then Hands on Sonnet, then Soul.