Aetheria/docs/mining-cut-refresh.md
2026-10-16 18:40:32 +02:00

60 KiB
Raw Permalink Blame History

Mining: refreshed cut map (v2, operator rulings of 2026-09-30)

Superseded in part (master df7c44f2). Chunk reach and picking no longer go through Zone.ChunksNear or an ActionGameManager candidate list. Zone.Targets (ServerShared/Targeting/) answers range and best-first queries, Entity owns the reticle, next, previous and nearest rules, and belt tasks, SettleBelts, BeltUpdates and MineAsteroid are gone. Read the as-built section "Mining index: as built" in docs/aetheria-release-map.md (branch eureka/aetheria-release-target) before any anchor below that names ChunksNear, VisibleChunksInReach's old body, ActionGameManager target code or belt threading.

v1 dated 2026-09-29; this is v2, 2026-09-30. Imagination output for Self. It re-anchors the Codex mining lane to the Body as it will stand after codex/fire-control-12 merges to master, and it incorporates the operator's rulings of 2026-09-30:

Question Ruling
Q4 A
Q5 A
Q6a A
Q6b A
Q7 A′
Q8 B
Q9 C
Q10 A
Q11 authored catalog records that yield simple commodities only
Q12 A
Q13 A, plus the detection correction

The Codex map (docs/mining-cut.md) is raw material, not authority. Where the two disagree, this file is the proposal.

What changed from v1:

  • Chunk detection now goes through the existing reflectance rule. It is no longer unconditional, and it is not deferred to EW (Cut 3).
  • Cut 6 is deleted. AI mining is deferred, and the same-path proof is a test agent in Cut 5.
  • Cut 7 authors Corrosive and Ionizing weapons.
  • The field-kind record arrives in Cut 3 (for cross-section), not Cut 5.
  • Two new forks: Q14 (how chunk sensor info integrates) and Q16 (how a belt gets its kind).
  • The CultMath-root claim is settled in the shared precondition.

The moddable-ships half of v1 lives in docs/moddable-ships-cut.md on codex/moddable-ships. Section 0's ship rows are kept for the combined test count.

Rulings added 2026-09-30 (Self, after v2; operator confirmed Q14 A and Q16 B the same day)

  • Q14 A. A ship's knowledge of a rock is the value the per-tick sensor rule would settle at, computed on demand through the shared Sensor.Gain. It is stateless and costs nothing per tick.
  • Q16 B. A belt's field kind is chosen at generation and saved. A belt from an older run with no kind gets one assigned once on first load, by the same function, and that assignment is saved.

How this was established

  • Branch tips read, 2026-09-29, after git fetch.

    • origin/master 9b85211f
    • origin/codex/fire-control-12 b66ba524
    • origin/codex/mining 283ce7dc
    • origin/codex/moddable-ships 375d6bd4
    • Nothing in F:\Projects\Aetheria's working tree was read or switched. Every read is git show or git grep against a SHA.
  • The post-fire-control Body is a synthetic merge. git merge-tree --write-tree origin/master origin/codex/fire-control-12 is clean, tree 91576243. I wrapped it in an unreferenced local commit, 8a2db490 (no ref, no push to origin), so it can be built. The fire-control checklist (docs/merge-to-master-checklist.md:7,107 on b66ba524) predicts the same clean merge.

  • Anchors. Each file:line names a real SHA, never the synthetic one:

    • b66ba524 for files only fire control touched;
    • 283ce7dc for files the mining branch touched;
    • 375d6bd4 for files the ships branch touched.

    The three file sets are disjoint. I checked with git diff --name-only over each branch's own range, and none of the three pairs shares a file. So each anchored line is byte-identical in the merged tree.

  • Tests ran on Yggdrasil, against synthetic merges (section 0). CultLib 45c2f40 and CultMath 6d5e209 are the pins in Directory.Build.props. The two LFS files the tests read (GameData/Aetheria.cc, Assets/Resources/Settings.asset) were staged beside the job, because the verify mirror carries no LFS.

  • Catalog facts come from a probe that opens the merged tree's GameData/Aetheria.cc through AetheriaStores.Open (section 0).

0. Probe results

Test suite on the synthetic merges, Yggdrasil, dotnet test tests/Aetheria.Shared.Tests, CultLib 45c2f40, CultMath 6d5e209, the dotnet image:

Tree Commit (local, unreferenced) Result
master + fire-control-12 + mining 396d19e5 379 passed, 0 failed
master + fire-control-12 + ships 0036204f 364 passed, 0 failed
master + fire-control-12 + mining + ships 474c35c3 381 passed, 0 failed

Fire control closed 12.4 at 362 tests. 379 = 362 + 17 mining tests, 364 = 362 + 2 ship tests, and 381 is both together. Nothing on either branch breaks a fire-control test, and nothing in fire control breaks a branch test. This proves the headless simulation only. No Unity compile ran.

Catalog probe (scratchpad/probe/Program.cs: it opens GameData/Aetheria.cc through AetheriaStores.Open in the merged tree; the catalog oid 2aa79c0a is the same in all three merges and at b66ba524):

  • 124 items: 32 GearData, 18 WeaponItemData, 13 SimpleCommodityData, 5 HullData, 51 compound commodities, 4 cargo bays, 1 docking bay.

  • Weapon damage types: Kinetic 9, Electric 3, Optical 3, Thermal 3. There is none of Corrosive or Ionizing, though the DamageType enum has all six. The mining map said Kinetic 10.

  • Penetration > 0: only Autocannon, LRMM72 and SRMM72, each at 0.25.

  • DamageSpread > 0: GT 3K 3.25, plight 3.3, pswarm 1.5, scorched void policy 1.5, and SRMM72 0.5. The map listed four and missed SRMM72.

  • No weapon has BlastRadius or Fuse. Blasts are dormant in shipped content until F12-2 authors them.

  • Simple commodities: the 13 the map named. Every one is priced 0 except Ammo, at 1000. The map's "every live simple commodity has Price 0" is off by one.

  • No live item carries a scanner or mining behaviour, on fire control or on mining merged.

  • Pre-existing, on b66ba524 as well as on the merges: four GearData records (Refinery, Deep Ore Extractor, Assembly Line, Shipyard) hold a null element in Behaviors (index 0 of 1). It does not come from mining's union-26 retirement: the same four are null on fire control alone. This matters to variants C4 (A.6).

  • Shipped hull mount ids are not unique:

    Hull Hardpoints Distinct Transforms Empty Transforms
    Djinni 22 20 3
    Longinus 13 12 2
    LonginusX 10 9 2
    Turret 7 3 5
    Zenith 5 1 5

    All five hulls use Prefab. This matters to ship migration (docs/moddable-ships-cut.md S5).

Shared precondition for both campaigns

Both campaigns wait on one event: codex/fire-control-12 merging into master with --no-ff, which Self does after the operator signs off (docs/merge-to-master-checklist.md:105-113 on b66ba524). Until then, neither campaign has a base to merge onto except the fire-control branch itself. After it:

git switch codex/mining          && git merge --no-ff master   # in a worktree, never the operator's tree
git switch codex/moddable-ships  && git merge --no-ff master

Merge, do not rebase. Both maps and their Soul receipts cite landed SHAs: mining cites 11 of its 13 commits, and the ships doc cites its commits' results. A rebase rewrites every one of those SHAs, which strands the maps' evidence. The merge has no conflicts (below), so it costs one merge commit per branch.

Building fire-control-12 headless needs two CultLib checkouts

Soul (ships branch) reported that fire-control-12 does not compile against its pinned CultLib 45c2f40, with erf and erfinv missing. v1's merges ran green on 45c2f40. Both claims are true. They describe two different build invocations.

  • Directory.Build.props @b66ba524 pins two revisions of the same repository: CultLibRevision 45c2f40 (CultCache) and CultMathRevision 6d5e209 (CultMath, tag cultmath-unity-v0.2.4).
  • CultMathRoot defaults to CultLibRoot when it is not passed.
  • erf and erfinv exist in CultMath at 6d5e209 (packages/cultmath/src/CultMath/math.cs:528). They do not exist at 45c2f40: git grep finds neither there.
  • FireControl.cs calls them at :1323 and :1348 @b66ba524 (Cut 12.0 added them to CultMath).

Probed on Yggdrasil at b66ba524, dotnet build Aetheria.Shared:

Invocation Result
A. -p:CultLibRoot=/CultLib only (at 45c2f40) fails: FireControl.cs(1323,54) CS0103 'erf', (1348,53) 'erfinv'
B. as A, plus -p:SkipCultLibRevisionCheck=true the same failure
C. -p:CultLibRoot=/CultLib -p:CultMathRoot=/CultMath (at 6d5e209) builds (v1's 379/381 runs used this)

The rule for every Hands and Soul job: pass both roots, each checked out clean at its own pin. Soul's run was invocation A: a missing CultMath root, not a broken branch.

A second defect this surfaced (not a mining cut). Case A should never have reached the compiler. The VerifyCultLibRevision target in Directory.Build.targets exists to refuse a CultMath root at the wrong revision, and here it stayed silent. Its Condition compares ProjectReference identities against a path spelled with backslashes ($(CultMathRoot)\packages\cultmath\...). I infer that on Linux the identity no longer matches that spelling, so the guard never runs. I have not verified this on Windows.

This is fire control's or the build owner's to fix. A pin guard that turns itself off on the verification host is a guard in name only. Hand it to Self as a spawn-worthy defect.


Part A. Mining

A.1 Status header (verified against git)

Branch codex/mining, tip 283ce7dc: 13 commits on 8bd25f6f. 8bd25f6f is an ancestor of fire control, which has since moved 76 commits to b66ba524.

What truly landed:

Cut Commits What
map e729e6d6, 670831c8, 007771fe, e4d38f16, a9538b20, 283ce7dc target, cut map, Q1-Q3 rulings, status
1 22a54ccb; fix d9c887c1 belts evaluated on demand; Task.Run belt threading deleted
2 3b9f0f1d, c924bae0; fixes daa64560, 34994732, 93bfdebd MiningTool, MineAsteroid and the survey Execute deleted; ChunkId, Zone wear store, ZonePack key 6

Production delta under Assets/Scripts (git diff --numstat 8bd25f6f 283ce7dc): +181 / −250. Tests add +681 (MiningCut1Tests.cs 240, MiningCut2Tests.cs 441).

Test counts. The map says 274 tests after Cut 2 (:493), counted on its own 8bd25f6f base. On the merged tree the suite is 379, of which 17 are mining's. Every one passes (section 0).

Corrections to docs/mining-cut.md at 283ce7dc:

  1. :5 "Nothing here has landed" is false. Cuts 1 and 2 landed at the SHAs above, and the map's own status block (:487-500) says so. Replace it with a status line naming both cuts and the base.
  2. :9 anchors at 8bd25f6f are stale for every fire-control file. At b66ba524, FireControl.cs is 1,614 lines. Splash is gone. Apply is a switch over three functions (ApplyDirectHit :680, ApplyBlastHit :740, Detonate :1074). Silhouette is at :1278 and PendingShot at :1464. Every Cut 3 and 4 anchor in the map (:396-412, :669-721) is re-anchored in A.3 below.
  3. :24-27 "Fire control is mid-rewrite" is stale. Cut 12.4 closed 2026-09-29 (c50029d5), and 362 tests passed at that close (docs/fire-control-cut.md 12.4 status on b66ba524).
  4. Chunk detection: both the Codex map and v1 of this map got the ruling wrong.
    • The Codex map's Cut 3 (:667-668, :693) specifies FireControl.ChunkDetectionRange from the scanner's Range, an UnaidedChunkRange fallback, and a test AScannerExtendsChunkDetection. Its Cut 7 (:834-836) restores "the Resource Scanner as the chunk sensor".
    • The Codex Q2 text (:176-185) makes a chunk visible unconditionally with PSensor 1, and defers detection to the EW campaign. v1 of this map followed that text.
    • The operator's correction, 2026-09-30: "We can just assign a reflectivity and cross section to each chunk just like we do with entities, then their visibility depends on how much light is shining on them, no new mechanism needed ... This is what I actually intended with my ruling about deferring detection."
    • So chunk detection is in this campaign, through the existing reflectance rule (Cut 3). All three earlier readings are superseded:
      • the scanner as chunk sensor;
      • unconditional visibility;
      • PSensor fixed at 1.
    • ResourceScanner stays parked.
  5. Q2's blast consequence (:221-223) was written against Splash. Cut 12.4 replaced it with Detonate(Zone, float2 worldPlanar, float radius, float damage, DamageType) (FireControl.cs:1074 @b66ba524).
    • Detonate takes no shooter.
    • It walks zone.Entities only (:1084).
    • So a proximity burst beside a rock does nothing to the rock today, and nothing could route its loot.
    • This was new question Q10, ruled A on 2026-09-30: a blast covers every chunk it overlaps (Cut 4).
  6. :135, and the SimpleCommodityData comment it copies, are wrong about the retired keys. Both claim keys 6, 7 and 8 are retired. They are live: ItemData declares them for every item, SpecificHeat Key(6), Conductivity Key(7) and Price Key(8) (ItemData.cs:291-298 @b66ba524), and SimpleCommodityData inherits them. Only key 11 is retired on SimpleCommodityData. Keys 12 and 13 are still free. The comment at ItemData.cs:304 should say "key 11".
  7. Q7's recommendation (:298-310) predates the Q2 ruling. Q2 (:171-174) says composition and loot tables are per field kind, with debris rolling salvage through the same roll. Q7 recommends per-ore Abundance on SimpleCommodityData, which has one table for every field kind. The two are unreconciled. Q7 is restated below. Resolved 2026-09-30: Q11 (kinds are authored records yielding simple commodities) and Q7 A′.
  8. The Target blast-radius count (:193-196, "about 60 reads in 12 files") has grown.
    • At b66ba524, git grep finds 28 target reads in ActionGameManager.cs and 22 in FireControl.cs, where the map counted 20 and 11.
    • It finds 11 in Entity.cs, plus sites in the Unity weapon managers (GuidedProjectileManager, LightningGunManager, ConstantParticleWeaponManager, GuidedProjectile) that the map does not list.
  9. The catalog changed after the mining probe.
    • 4f218e85 and dd437580 (locomotion Cut 1) landed Longinus, Djinni and five thrusters in Aetheria.cc after 8bd25f6f.
    • Catalog oid at 8bd25f6f: 1862ddc4. At b66ba524, and in all three merged trees: 2aa79c0a.
    • The census in section 0 replaces the map's weapon and commodity counts.
    • Kinetic is 9 weapons, not 10 (:313).
    • Ammo is priced 1000 (:117, :421).
    • SRMM72 also authors DamageSpread (:270).
    • The Q8 conclusion stands: no Corrosive or Ionizing weapon exists.
  10. Still owed from Cut 1 (:508-510): a Unity compile of the ZoneRenderer/AsteroidBeltUI change, an isolated Stryker rerun, and the operator's fly-through. Nothing after d9c887c1 records any of them.
  11. Still owed from Cut 2 (:493): Soul has not verified the two-commit fix (34994732, 93bfdebd). The map folds that check into Cut 3's Soul pass. Keep it named, so that it is not lost when Cut 3 is rewritten.

A.2 Merge path onto the fire-control-12 lineage

Probe Result
git merge-tree --write-tree origin/codex/fire-control-12 origin/codex/mining clean, tree b7feb776
git merge-tree --write-tree 8a2db490 origin/codex/mining (master+fc12) clean, tree 7280568c
mining merged, then ships merged on top clean, tree a7410174
files touched by both mining (8bd25f6f..283ce7dc) and fire control (8bd25f6f..b66ba524) none

The merged tree passes the full headless suite: 379/379, and 381/381 with ships (section 0).

No textual conflicts, and no shared file. The semantic seam is not textual: mining Cut 3 onward edits the code fire control rewrote after the mining map was written. That is handled by re-anchoring, not by merging.

A.3 Ordered cuts

The Codex numbering is kept so the map's history still reads, and Soul fix slots are added. Cut order:

Cut Nature Blocked by
M0 Placement: merge master, correct the map fire-control-12 on master
M-Soul Fix batch from the parallel Soul pass over Cuts 1-2 (content unknown here) Soul's report
3 A chunk is a target, detected by reflected light; the field-kind record appears M0 (Q14 A, Q16 B ruled)
4 A shot at a chunk goes through FireControl (re-anchored to 12.4); blasts cover chunks 3; the fire-control no-target fuse ruling
5 Composition, loot, the mined lot, and the same-path test agent 4
6 Deleted (Q9 = C). The test agent moved into Cut 5. —
7 Content: field kinds, ores, the Drill Bit, and new Corrosive and Ionizing weapons 5; variants C4 for families authored as variants

Every mining question is ruled.

M0. Placement and map correction

  • Repo/branch: Aetheria, codex/mining, in a worktree. Do not use the operator's tree.
  • Deletes first: none in code.
  • Keeps/moves/adds:
    • git merge --no-ff master once fire-control-12 is on master.
    • In docs/mining-cut.md: apply corrections 1-11 of A.1.
    • Replace Cut 3 and Cut 4 with the versions below.
    • Strike UnaidedChunkRange from Cut 5's and Cut 7's tunables, and "Resource Scanner as the chunk sensor" from Cut 7.
    • Replace Q2's "detection is deferred to the electronic-warfare campaign" paragraph (:176-185) with the operator's 2026-09-30 correction, verbatim.
    • Record the 2026-09-30 rulings in the map's status header.
    • Delete Cut 6 from the map; its history note points here.
    • Mark the :396-412 table as history, anchored at 8bd25f6f.
  • Verification:
    • Yggdrasil, the same job that section 0 ran: ygg-verify.sh <aetheria clone> <merge sha> dotnet '<clone CultLib 45c2f40 to /CultLib and 6d5e209 to /CultMath; restore the two LFS files; dotnet test tests/Aetheria.Shared.Tests -p:CultLibRoot=/CultLib -p:CultMathRoot=/CultMath>'. Both roots are mandatory (shared precondition).
      • The job mounts the Yggdrasil mirrors read-only with DOCKER_ARGS="-v /home/gamecultadmin/eureka-verify/repos:/repos:ro -v /home/gamecultadmin/eureka-verify/lfs-aetheria:/lfs:ro".
      • It runs git config --global --add safe.directory "*" first. The GIT_CONFIG_* safe-directory setting that ygg-verify.sh exports does not reach a local fetch from /repos. Rule pinned: fire control and mining compose without a behaviour change on either side. Every pre-existing test passes unchanged.
    • The verify mirror has no LFS (the push from F:\Projects\Aetheria fails in the LFS pre-push hook on an ssh: remote). Push from a --shared --no-checkout clone, and mount the two LFS blobs. This is recorded so the next Hands does not rediscover it.
    • Unity batchmode compile of the merged tree (Self, in a worktree, never with the operator's editor open on the main tree). This is the Cut 1 renderer compile the map still owes.
    • Operator: fly through a belt (the Cut 1 check the map still owes). Rocks orbit smoothly, and the minimap and the map screen track them.
  • Subtraction: docs only.

M-Soul. Fix slot for the parallel Soul pass

  • Repo/branch: codex/mining, after M0.
  • Content: whatever the parallel Soul pass finds in Cuts 1-2, including the unverified 34994732 and 93bfdebd. This map does not predict it. Cut 3 does not start until this slot is closed or empty, because Cut 3 builds on Zone.Wear, ChunkExists and ChunkRadius.

Cut 3. A chunk is a target, and is detected by reflected light

  • Repo/branch: Aetheria, codex/mining, after M-Soul.
  • Written for: Q13 = A with the detection correction, Q14 = A, Q16 = B (all ruled).

How detection works today (read at b66ba524)

  1. Emitters write into Entity.VisibilitySources (Entity.cs:63, a Dictionary<object, float>). Every entry is one source's contribution:

    • Reflector.cs:46: CrossSection × Zone.GetLight(Position.xz). This is the rule the operator named.
    • Black-body radiation: Entity.cs:1269.
    • Weapons: InstantWeapon.cs:257, ConstantWeapon.cs:166.
    • Thrusters: Thruster.cs:110-111.
    • Sensor pings: Sensor.cs:117.
    • Visibility.cs:44 and Radiator.cs:121.

    Sources decay each tick (Entity.cs:1083-1087, VisibilityDecay 0.5).

  2. Entity.Visibility (Entity.cs:136) is the sum of the sources.

  3. Each Sensor behaviour on an observer loops over every entity in the zone, every tick (Sensor.cs:157-189).

    • It adds a gain into EntityInfoGathered[target] (Entity.cs:64, a ReactiveDictionary).
    • Passive gain is target.Visibility × Sensitivity × SensitivityCurve(angle/π) × dt / dist (:179-184).
    • Ping gain is target.Visibility × Sensitivity × PingBoost × dist, once per ping (:170-175).
    • It then decays: next *= 1 − TargetInfoDecay × dt (:186; TargetInfoDecay 0.5 in shipped Settings.asset).
  4. Crossing TargetDetectionInfoThreshold (0.1) adds the target to or removes it from VisibleEntities, via the ObserveReplace subscriber at Entity.cs:237-253.

  5. Fire control reads both. PFire gates on VisibleEntities.Contains (FireControl.cs:173) and prices PSensor from the info value (:179). The same info also gates the armour and gear reveal thresholds (0.5 and 0.8) through IsRevealed.

  6. Light is Zone.GetLight(float2) (Zone.cs:369 @283ce7dc). It sums PowerPulse(d / LightRadius, 8) over every sun within its LightRadius. Beyond every sun's radius, light is 0.

How a chunk plugs in, without becoming an entity

Two things are needed:

  • A chunk's visibility, V(chunk, t): cross-section(chunk) × GetLight(pose(chunk, t)). This is Reflector.cs:46's rule, applied to a pose that is already a pure function of time (Cut 1). No stored source, and no decay: a chunk has only the reflected term, because rocks carry no thrusters, weapons or heat (black-body emission is out of scope, per the operator's correction).
  • An observer's info on a chunk. The gain rule is the existing one. Q14 decides how it integrates over time. Recommended: the closed-form equilibrium of the per-tick rule, evaluated on demand, with no per-tick state.

No adapter, and why that is the goal. The adapter would be a pseudo-entity per chunk, or an IDetectable wrapper, so that Sensor.Execute's loop over Zone.Entities sees rocks. That is rejected option C of the Codex Q2 in a costume:

  • It puts every chunk into every sensor's per-tick loop. The map measured 184 ns per observed body per tick, which is 149 µs per sensor-bearing ship for the 809-chunk probe zone.
  • It puts chunks into EntityInfoGathered, whose ObserveReplace subscriber builds VisibleEntities, VisibleEnemies and VisibleFriendlies. Every consumer of those collections would then have to learn that some "entities" are rocks.

The shared owner has to be the rule, not the container. The cut extracts the gain terms at Sensor.cs:170-184 into one static function, and both paths call it:

  • the entity loop, per tick, unchanged in behaviour;
  • the chunk query, on demand.

Nothing about a chunk is stored per observer.

Deletes first

  • The Codex scanner-as-sensor plan: no ChunkDetectionRange, no UnaidedChunkRange, and no scanner read anywhere in the target path.
  • The v1 placeholders: unconditional chunk visibility, and PSensor fixed at 1.
  • Optional, Self's call: the parked ResourceScanner behaviour still runs a per-tick Update over three stats no code reads (ResourceScanner.cs:35-58 @283ce7dc). Delete the Update and the properties, and keep ResourceScannerData authored.

Adds

  • Sensor.Gain(...), a static function extracted verbatim from Sensor.cs:170-175 (ping) and :179-184 (passive). Inputs: target visibility, sensitivity, the curve at the angle, distance, dt, and whether this is a ping. Sensor.Execute calls it. This is a pure extraction.

  • FieldKindData, a new catalog document type (Q11: authored catalog records). Cut 3 introduces it with only what detection needs:

    • Name ([CultName]);
    • CrossSection (float): reflectivity per schematic cell of chunk area;
    • GenerationWeight (float), for Q16.

    Cut 5 adds the yield entries. Only one kind exists at first, "Asteroid".

  • The belt names its kind. AsteroidBeltData gains a nullable CultRecordRef<FieldKindData> Kind, at the next free key after Asteroids Key(9) (ZoneData.cs:101-105 @283ce7dc).

    • ZoneGenerator writes it at generation, through one function FieldKinds.Assign(belt key, catalog): a StableHash pick weighted by GenerationWeight.
    • Under Q16 = B, a belt loaded without a kind is assigned by the same function on first load and written back. One derivation serves both the generation path and the load path.
  • Zone.ChunkVisibility(ChunkId) = kind.CrossSection × π(ChunkRadius / SchematicCellSize)² × GetLight(ChunkPose.xy).

    • Area in schematic cells keeps the numbers comparable to hull reflectors. The census has Longinus at 500-150 over 66 cells and Djinni at 2500-500 over 166, so about 2-15 per cell.
    • A worn chunk shrinks and dims. A broken chunk has radius 0, so it is invisible.
  • Entity.ChunkInfo(ChunkId): the info this observer has on the chunk, from its active Sensor behaviours through Sensor.Gain, integrated per Q14. Visible means ChunkInfo > TargetDetectionInfoThreshold.

  • TargetRef: a readonly struct holding Entity Entity and ChunkId? Chunk, with value equality and IsNone. It lives beside ChunkId (Zone.cs:527 @283ce7dc).

  • Zone.ChunksNear(float2 position, float range, List<ChunkId> into): the one chunk query.

    • Belts whose annulus cannot reach are skipped.
    • The rest are evaluated through ChunkPose (Zone.cs:285).
    • Broken chunks are skipped through ChunkExists (:231).
    • It knows nothing about detection. Callers filter by ChunkInfo.
  • Zone.ChunkVelocity(ChunkId): analytic velocity plus the parent Orbit.Velocity.

  • Entity.SetTarget(TargetRef): the one writer. The player's handlers and the Cut 5 test agent both call it (target invariant 3, "set by one writer path that both the player and the AI call"). Today every handler writes Target.Value directly (ActionGameManager.cs:377,385,395,403), so there is no one writer to share.

  • Reach (Q13 = A): cycling and any programmatic chunk pick use ChunksNear(position, longest active weapon Range.Max), then filter to chunks the observer can see.

Per-file changes (b66ba524 unless marked)

  • Sensor.cs:170-184: call Sensor.Gain. Behaviour must stay identical; the whole existing suite is the check.
  • Entity.cs:46: Target becomes ReactiveProperty<TargetRef>, written only through SetTarget.
  • Entity.cs:201, :227: entity removal clears the slot only when it holds that entity.
  • Entity.cs:229: TargetedBy fires for entities only.
  • Entity.cs:233: TargetedByCount counts entity targets only.
  • Entity.cs:298-299: today it clears a target missing from EntityInfoGathered, which would clear every chunk target on the next pass. For a chunk, it clears when ChunkInfo ≤ TargetDetectionInfoThreshold, the same "lost track" rule expressed through the chunk path. A broken chunk has 0 visibility, so it is lost the same way.
  • Entity.cs:317: TrySelectTargetItem is entity-only.
  • Entity.cs:1078: TargetRange reads the chunk position for a chunk.
  • ActionGameManager.cs:365-377 (reticle pick): pick the chunk under the reticle from ChunksNear at the reticle, if visible.
  • ActionGameManager.cs:381-388 (TargetNearest): stays enemies-only. It drives lock.
  • ActionGameManager.cs:390-404 (next and previous): include visible chunks within Q13's reach.
  • ActionGameManager.cs:412-414 (Cycle Target Item): entity-only.
  • ActionGameManager.cs:582, :745, :769, :1046, :1224, :1239, :1266-1267, :1355-1373: .Entity reads, or the chunk position for the indicator.
  • ActionGameManager.cs:1285: UpdateFireControlDebug takes TargetRef.
  • Weapon.cs:101 StanceAllowsFire: true for a chunk. Weapon.cs:110 ArcAllowsFire: position through the ref.
  • LockWeapon.cs:86: a chunk never locks (Q12 = A, ruled).
  • TurretController, Combat.cs, Minion.cs, Ship.cs, EntityInstance.cs, PropertiesPanel.cs, FieldDriver.cs, ActionBarSlot.cs: .Entity and null checks.

Authority map

  • Owners:
    • Entity.Target is the one slot, and SetTarget is its one writer.
    • Sensor.Gain is the one detection gain rule.
    • FieldKindData owns a chunk's reflectivity.
    • FieldKinds.Assign owns which kind a belt is.
  • Inputs: player handlers and the test agent; sensor stats; chunk pose and radius; sun light.
  • Outputs: the current TargetRef; ChunkVisibility; ChunkInfo.
  • Derived state:
    • ChunkVisibility and ChunkInfo are pure functions of time and are never stored.
    • TargetItem is entity-only.
    • TargetRange.
  • Forbidden writers:
    • any second target field;
    • any direct Target.Value = outside SetTarget;
    • any chunk in Zone.Entities or EntityInfoGathered;
    • any per-tick chunk detection loop;
    • any chunk-specific copy of the gain arithmetic;
    • any unconditional visibility for chunks.
  • Shared paths: reticle, cycling, the test agent, dock and undock re-activation, and belt generation and load (through FieldKinds.Assign).
  • Deletion line: no ReactiveProperty<Entity> Target remains; no Target.Value = outside SetTarget; no gain arithmetic outside Sensor.Gain.

Verification

Yggdrasil, the M0 command with --filter FullyQualifiedName~MiningCut3, then the full suite.

Test Rule it pins
SensorGainExtractionChangesNothing The existing detection tests, and a recorded EntityInfoGathered trace over N ticks for a fixed scene, are byte-equal before and after the extraction.
ChunkInfoMatchesAnEntityHeldStill Q14's parity. An entity with a Reflector of the same cross-section, at the chunk's position and held still, converges under the per-tick loop to the value ChunkInfo returns (tolerance from the discrete fixed point). One rule, two integrators.
AChunkInDarknessIsNotVisible A chunk outside every sun's LightRadius has ChunkVisibility 0 and cannot be targeted or fired on.
ABrighterOrBiggerChunkIsSeenFurther Monotone in cross-section, radius and light.
AWornChunkDims, ABrokenChunkIsInvisible Visibility follows ChunkRadius.
CyclingReachIsTheLongestActiveWeaponRange Q13. A visible chunk beyond it is not offered.
AChunkTargetSurvivesTheInfoPassWhileSeen The negative for Entity.cs:298-299. It fails on the naive retype.
AChunkTargetIsDroppedWhenItGoesDark The lost-track rule, through the chunk path.
TargetRefEquality Value equality; a chunk never equals an entity.
ChunkTargetIsNeverLocked Q12 = A.
FieldKindAssignmentIsOneFunction Generation and first-load assignment give the same kind for the same belt key; the kind is written back once.
every existing fire-control and sensor test Pass unchanged. This is the negative that ship detection and targeting did not move.

Also:

  • Negative greps: git grep -n "ReactiveProperty<Entity> Target\b" and git grep -n "Target.Value =" outside Entity.SetTarget are empty.
  • Stryker over Sensor.Gain, Zone.ChunkVisibility, Entity.ChunkInfo, ChunksNear and TargetRef.
  • Census before and after: AsteroidBeltData is a run-store type and is absent from the catalog, and FieldKindData shows one record once Cut 7's content lands. Until then, Cut 3 ships a test fixture kind and a one-record catalog write through AetherDb, flagged for operator review.
  • Unity compile: Self.
  • Operator:
    • target a lit rock with the reticle, and cycle through rocks and ships;
    • fly into a belt far from the sun and confirm its rocks cannot be picked;
    • dock and undock with a rock held.

Subtraction estimate

About 0 removed (optionally about 15 for the scanner Update). About 140 added:

  • Sensor.Gain extraction nets about 0;
  • FieldKindData plus assignment, about 40;
  • ChunkVisibility and ChunkInfo, about 30;
  • TargetRef, ChunksNear, SetTarget, about 70.

Plus about 95 mechanical site edits. One new catalog document type (FieldKindData), and one nullable run-store slot (AsteroidBeltData.Kind).

Cut 4. A shot at a chunk goes through FireControl, and blasts cover chunks (re-anchored to 12.4)

  • Repo/branch: codex/mining, after Cut 3.
  • Written for: Q10 = A, Q12 = A (both ruled).
  • Lands after: the fire-control no-target fuse ruling (docs/fire-control-cut.md, 12.4 status, "a fused weapon fired with no target bursts at the shooter's own position", FireControl.cs:389). That ruling's fix edits the same target != null branches this cut retypes (:367, :372-378, :389). If the ruling is still open, this cut preserves today's behaviour for the no-target case and says so in its commit.

Deletes first: none. PendingShot.Target (:1468) and ShotOutcome.Target (:1598) become TargetRef.

Adds:

  • Silhouette.Disc(float radiusCells, float precision). The shared integrator is extracted from Silhouette :1302-1308 (the Phi sum over merged intervals, and span). The hull path and the disc path call one function. No second Phi summation exists.
  • A chunk pass inside Detonate (Q10 = A). After the entity pass (:1084-1149):
    • ChunksNear(worldPlanar, radius + max chunk radius).
    • Each covered chunk takes damage × CircleCircleOverlap(blast, chunk) / (π r_blast²), the same normaliser the cell shares use (:1092).
    • CircleCircleOverlap is the closed-form lens area, about 12 lines, beside CircleSquareOverlap (:1153).
    • Each share goes to Zone.Wear.
    • One area rule: a blast's disc is divided over whatever it covers, whether cells or chunks. Conservation holds across both.
  • Detonate gains an optional loot sink, the detonating shot, so the shooter receives the ore (Q10 = A).
    • A sourceless blast passes null. None exists today, but a mine is one (F12-6). A null-sink blast wears chunks and yields nothing.
    • This is the only new parameter. Detonate still reads no host (12.4's rule).

Per-file changes (FireControl.cs @b66ba524):

  • PFire :161-181:
    • For a chunk, the visibility gate (:173) is ChunkInfo > TargetDetectionInfoThreshold, and info (:179) is ChunkInfo (Cut 3). This replaces v1's "no gate, info 1".
    • PSensor prices a chunk exactly as it prices a ship at the same info.
    • A LockWeapon is gated out (:175, Q12 = A). The arc stays (:176).
  • Forecast :206, HitProbability :228, Inspect :243-283: the disc silhouette. Inspect's Visible and Info come from the chunk path.
  • PredictedIntercept :133, TravelDirection :307: position and velocity through the ref. Bearing :316 is not called for a chunk, because a disc has no facing.
  • DeviationProbability :336-341: the live chunk position is Zone.ChunkPose at now.
  • Fire :355-418: freezes the ref.
  • Step :439: targetGone includes !ChunkExists.
  • CommitProbability :506-520: for a chunk, the disc (no GetData(Hull) at :516).
  • Commit :550-600: for a chunk hit, no Lane (:563-578) and no shield block (:585-592). Cell, Bearing and Lateral stay zero in MakeOutcome (:1028).
  • Apply :651-678:
    • Null fuse on a chunk: Zone.Wear(chunk, Damage) (Zone.cs:260 @283ce7dc).
    • Contact or delayed fuse on a chunk: no ApplyBlastHit, which needs a hull (:747-760). Detonate at the chunk's centre at arrival. The pose is a pure function of time, so the point is exact.
    • Proximity: Detonate as today, now with the chunk pass.
    • Contact or delayed on a ship: ApplyBlastHit as today, and its Detonate now also covers nearby chunks.
  • Unity presentation: EntityInstance.cs (4 target reads) and the four weapon managers aim at the chunk's rendered position. Presentation only.

Authority map:

  • Owner: FireControl, unchanged. Detonate stays the one area owner.
  • New inputs: chunk pose, velocity, radius and info, through Zone and Entity.ChunkInfo.
  • When blast damage to chunks is decided: at detonation. Each covered chunk is judged where it is at arrival, which is 12.4's own bystander rule (ApplyBlastHit's comment at :773-777: "every bystander is judged against where it actually is at arrival"). A direct hit on a chunk is still decided at Commit.
  • Forbidden writers:
    • any chunk hit test outside Commit, except Detonate's area pass;
    • any second area-share formula;
    • any second Phi integrator;
    • Detonate reading a host.
  • Shared paths: player trigger, the Cut 5 test agent, AI Activate (Combat.cs:121), and turrets (which never hold a chunk). Any blast from any source goes through Detonate.
  • Deletion line: none.

Verification (Yggdrasil, --filter FullyQualifiedName~MiningCut4, then the full suite):

Test Rule it pins
AChunkShotUsesTheSameFactors Accuracy, PSpread, Sigma and PSensor equal a ship shot's at equal range, precision and info. One fire path.
AnUnseenChunkCannotBeFiredOn The gate is the detection rule, not a constant.
DiscMassIsTheClosedForm POnHull = Φ(R/σ) − Φ(−R/σ): the shared integrator.
HullSilhouetteUnchangedByTheExtraction Every existing Silhouette test and the ProbeC equivalence still pass. The negative for the extraction.
AChunkThatBreaksMidFlightIsAMiss Step's gone rule.
ChunkHitsWearTheChunk, AHitOnABrokenChunkChangesNothing Wear; re-pins 34994732 through the real path.
ABlastConservesAcrossCellsAndChunks The shares delivered to cells plus chunks, plus the uncovered area, equal the damage. Same lattice style as 12.4's 1,944-disc conservation probe.
ABlastBesideAnUntargetedRockWearsIt Q10 = A.
CircleCircleOverlapIsTheLens Closed form, with the tangent, contained and disjoint cases (12.4's F1 tangent lesson).
LaunchersCannotFireOnAChunk Q12 = A.
ChunkDiceAreTheShotsOwn The same galaxy and shot id give the same outcome.

Also: Stryker over the new branches. Operator: shoot a lit rock with each damage type; it shrinks and breaks, and misses at range look plausible.

Subtraction estimate: about 5 removed (the integrator extraction) and about 115 added (the chunk pass and the lens area add about 35 over v1).

Cut 5. Composition, the loot roll, the mined lot, and the same-path test agent

  • Repo/branch: codex/mining, after Cut 4.
  • Written for: Q4 A, Q5 A, Q6a A, Q6b A, Q7 A′, Q9 C, Q10 A and Q11, all ruled on 2026-09-30.

Deletes first:

  • BodyData.Resources (ZoneData.cs:78-79 @283ce7dc, key 4) becomes a retired-key comment. Its only reader, MineAsteroid, died in Cut 2.
  • Agents/Tasks/Mining.cs and its .meta (@283ce7dc: 19 lines, no reader, not persisted per AgentTask.cs:11, and not registered in any union). AI mining is deferred (Q9 = C), and the campaign that picks it up writes its own task.
    • Leave the TaskType.Mine enum member (AgentTask.cs:27). Removing it would renumber the members after it for no gain.

Adds:

  • FieldKindData gains Entries, a List<FieldYield>. Each FieldYield is:

    • Commodity: a CultRecordRef<SimpleCommodityData> (simple commodities only, Q11);
    • Abundance (float);
    • Affinity: an object list of (DamageType, float) pairs, absent meaning 1.

    Every list here is an object list, not a dictionary. Variants address object-list elements by id and replace dictionaries whole (variants Q7). So a "rich belt" variant can override one entry's abundance.

  • Zone.ChunkComposition(ChunkId, Span<…>): derived. Abundance_o × exp(ChunkCompositionVariance × g(StableHash(field key, index, o))), normalized, from the belt's kind (Cut 3). Never stored.

  • The loot roll (Q4 A, Q5 A), for one hit with damage D on a chunk of size s:

    • units = min(D, remaining HP) / HP(s) × Yield(s), the floor plus one Bernoulli draw on the remainder;
    • the weight of ore o = Affinity_o(d) × Comp(o)^(1/Depth), with Depth = 1 + P × MiningDepthPerPenetration;
    • one draw per unit.
  • Where each roll happens:

    • Direct hit: inside Commit, after the hit draw, from the shot's own generator (FireControl.cs:540 @b66ba524). Frozen into ShotOutcome.Loot (commodity and count pairs).
    • Blast (Q10 = A): inside Detonate's chunk pass, per covered chunk, with that chunk's share as D. The draws come from a generator seeded by the same (zone, shot) pair as the shot's commit dice, plus a fixed blast salt, walked in ChunksNear order (belt key, then index). That keeps the dice the shot's own, and deterministic.
    • This amends target invariant 5. The old wording was "the loot is decided at Commit". The new wording: "loot is decided by the owner that decides the damage, from the shot's own dice". That is Commit for a direct hit, and detonation for a blast, following 12.4's bystander rule. I state it here, not as a new fork: it follows from Q10 = A plus 12.4's ruled model.
  • ItemManager.ExtractedLot(zone, field, commodity) (Q6b A): find-or-mint, with a source-to-lot index derived at construction and never persisted.

  • Extracted (Provenance.cs:116 @b66ba524) gains Body at key 2 (CultRecordRef<BodyData>, nil on older records).

  • Lot moves from CraftedItemInstance (ItemInstance.cs:38) to ItemInstance, keeping key 11. Coordinate with variants C4 (A.6).

  • The deposit (Q6a A): units go straight into the shooter's cargo at arrival (a direct hit) or at detonation (a blast), through TryStore (Entity.cs:1805). A full hold releases the overflow as floating items through Zone.Release, which the player and NPCs collect (ruling aetheria-release:ruling:mining-overflow-floats, 2026-10-16, superseding Q6a A's 'lost', whose premise was Unity-only pickups; cut line mining-yield in the release map).

  • Tunables: MiningDepthPerPenetration on GameplaySettings; AsteroidYield (ExponentialLerp) and ChunkCompositionVariance on PlanetSettings. None may be CultMath-typed while settings are Unity-serialized.

  • The same-path test agent (Q9 = C, replacing Cut 6). A headless test drives a ship through exactly the player's calls:

    1. Entity.SetTarget(TargetRef(chunk)) (Cut 3);
    2. weapon.Activate() (the call Combat.cs:121 and the player's trigger make);
    3. Zone.Update ticks.

    There is no Agent, no AgentTask and no Minion state. It proves the path an AI will later drive, without building the AI.

Per-file changes (b66ba524):

  • FireControl.Commit, ApplyDirectHit and Detonate: the roll and the deposit, as above.
  • Entity.cs:1856: the stack merge matches Data.Key only today; it must also match Lot.
  • Entity.cs:1934: the split constructs a SimpleCommodity without Lot; it must copy Lot.
  • SavedGame.cs:113: the ledger roots are .OfType<CraftedItemInstance>() today; they become every ItemInstance with Lot != 0.
  • ItemManager.cs:72: GetLot(CraftedItemInstance) takes ItemInstance.

Authority map:

  • Owners:
    • Commit decides direct-hit loot, and Detonate decides blast loot, each from the shot's dice.
    • ItemManager mints lots.
    • EquippedCargoBay places units.
    • FieldKindData owns what a field yields.
    • FieldKinds.Assign owns which kind a belt is.
  • Inputs: frozen damage, damage type and penetration; the chunk's composition and remaining hit points at the deciding moment.
  • Outputs: ShotOutcome.Loot, units in cargo, ledger lots.
  • Derived state: composition (never stored); the source-to-lot index.
  • Forbidden writers:
    • any loot draw outside Commit and Detonate's chunk pass;
    • any mined SimpleCommodity with Lot == 0;
    • any shared random stream;
    • per-ore tables on SimpleCommodityData;
    • any extraction call that bypasses FireControl.
  • Shared paths: the player's trigger and the test agent (identical calls); save GC; Continue, where the index is rebuilt.
  • Deletion line: BodyData.Resources is retired, and Mining.cs deleted, before the roll is added.

Verification (Yggdrasil, --filter FullyQualifiedName~MiningCut5, then the full suite):

Test Rule it pins
RightDamageTypeRaisesTheOresShare The Q4 table, as a fixture on a kind record.
PenetrationFlattensTowardRareOre, ZeroPenetrationReadsTheRawComposition Q4's depth term.
AChunkYieldsItsAuthoredTotalWhateverBreaksIt Invariant 7, summed over direct hits and blasts from different weapons.
DirectHitLootIsDecidedAtCommit Loot frozen in ShotOutcome, performed at arrival.
BlastLootIsDecidedAtDetonationFromTheShotsDice The amended invariant 5. The same galaxy and shot give the same ore.
ABlastPaysTheShooter Q10 = A.
ASourcelessBlastWearsButYieldsNothing The null loot sink.
MinedUnitsCarryAnExtractedLotNamingZoneAndField Provenance, including Extracted.Body.
UnitsFromOneSourceStack, UnitsFromTwoSourcesDoNot Q6b A, and the negative for Entity.cs:1856.
ASplitStackKeepsItsLot The negative for Entity.cs:1934.
AMinedStackSurvivesSaveGc The negative for SavedGame.cs:113. It fails on the old roots.
AFullHoldReleasesTheOverflow Ruling mining-overflow-floats (was Q6a A's AFullHoldLosesTheOverflow).
CompositionIsStableAcrossContinue Derived, never stored.
TwoKindsRollDifferentTables Per field kind (Q2, Q11).
ATestAgentMinesThroughThePlayersPath Q9 = C. After N seconds: ore in the hold, every unit's lot Extracted from that belt, and every unit traceable to a ShotOutcome or a detonation. The negative: grep for any call to Zone.Wear or ItemManager.ExtractedLot outside FireControl.cs.

Also:

  • Stryker over the formula, the blast pass and the mint.
  • Census: the new FieldKindData slots read as defaulted_missing_slot until Cut 7.
  • Operator: mine one lit rock with a Thermal gun and one with a Kinetic gun, and read the cargo and its brand rows.

Subtraction estimate: about 21 removed (Resources 2, Mining.cs 19). About 190 added: the roll, the blast loot, the mint, the kind entries and the test agent. The test agent replaces Cut 6's ~90 lines.

Cut 6. Deleted (Q9 = C)

AI mining is deferred. Its same-path proof is the test agent in Cut 5. The Codex Cut 6 (:800-822 @283ce7dc), its locomotion Cut 4 dependency and its inheritance of F12-5 all leave this campaign.

The campaign that builds AI mining starts from:

  • Zone.CreateAgent (Zone.cs:126-133 @283ce7dc);
  • the Minion transitions (Minion.cs:10-21 @b66ba524);
  • the one writer Entity.SetTarget;
  • Combat.cs:121's Activate.

Cut 7. Content (operator review sheet, one catalog commit)

This is a content cut, applied through an AetherDb mining-content [apply] command. It is a dry run by default, and the precedent is targeting-catalog. The operator reviews a sheet before apply. The command honours variants Q1a: no plain Upsert at a variant key.

1. Ores. The 14 legacy ores missing from live, restored with their legacy mass, specific heat, conductivity and category (values are in legacy-0414, per the Codex map's :830-832):

  • Minerals: Crystal, Silicon, Carbon, Nitrogen;
  • Metals: Iron, Titanium, Copper, Lead;
  • Compounds: Ethanol, Hydrocarbons, Acid, Oxygen, Carbon Dioxide;
  • Organics: Plant Matter.

Simple commodities only (Q11).

2. Field kinds. One FieldKindData "Asteroid" record:

  • CrossSection per cell: recommend about 5. That is dull rock, below Longinus's 7.6 and Djinni's 15 per cell at their maximum reflector values (section 0).
  • GenerationWeight 1.
  • Entries over the live and restored ores, each with an Abundance and Affinity over all six damage types, now that Q8 = B makes every type reachable.

Further kinds ("rich", "ice", debris) are the operator's to add. Each is a variant of "Asteroid" overriding entries by id, once variants C4 lands.

3. Drill Bit (Q1 A).

  • A ConstantWeaponData beam at Kinetic, with high penetration and range 5-20.
  • DPS, efficiency and penetration are from legacy MiningToolData (DPS 1-2.5, efficiency 2-5, penetration 1-5, range 5-20).
  • No live weapon uses ConstantWeaponData (census), so the Drill Bit is also the first live constant weapon. The operator's check covers the beam's presentation.

4. Corrosive and Ionizing weapons (Q8 = B, against v1's recommendation).

The balance fact this rests on (read at b66ba524): damage type does nothing in combat today.

  • WeaponData.DamageType (Weapon.cs:22-23) is frozen into the shot (FireControl.cs:1477) and passed to Shield.CanTakeHit and TakeHit (Shield.cs:166, :186). Both ignore their type argument.
  • Armour absorption (ArmorAbsorb, ItemAbsorb) and Detonate take no type.
  • The only other reader is AbsorbEvent, which is presentation (CapabilityEvents.cs:28).
  • Mining's affinity (Cut 5) becomes the first simulation reader of damage type.
  • Therefore a new-type weapon's combat power is fixed entirely by its non-type stats. Its identity shows only in mining and presentation.

Anchors (census, merged tree):

  • Kinetic 9, Electric 3, Optical 3, Thermal 3.
  • Sustained single-target DPS (Damage × Count / Cooldown, min to max quality) for the auto class:
    • Autocannon 6;
    • ClearPath 20-180;
    • Spectra 40-340;
    • ColdFire 16-160;
    • CShot RainbowLite 33-600.
  • Burst weapons (DeathCluster, FastBlast+-) are magazine-limited and are not comparable by DPS.

Records proposed. One per type. Each is a minimal override of an existing weapon, so its combat envelope equals its base's by construction, and balance risk is zero on day one.

New record Type Base (class, hardpoint) Overrides Why this base
(name: operator's) Corrosive ClearPath (AutoWeaponData, Ballistic) Name, DamageType Short-range stream: a sprayer. Range 250-600, DPS 20-180, cheap (price 50,000, mass 50).
(name: operator's) Ionizing Spectra (AutoWeaponData, Energy) Name, DamageType Beam-like optical: a particle beam. Range 800-1750, DPS 40-340.

Optional second step, flagged for review and not default: give the Ionizing record Penetration 0.25 (Autocannon's value) with damage × 0.8 to pay for it. That makes it the rare-ore tool through Q4's depth term. It changes combat, because penetration drives 12.3's lanes, so it needs the operator's eye and F12-2's retune.

Presentation: the effect prefab and icon stay the base's until art exists. The Corrosive and Ionizing presentation look is the operator's call, and so are the names.

Variants: both records are ideal variants: a base plus two overrides, one of them nested in Behaviors[weapon].DamageType. A nested override needs element ids (C2), and Aetheria adopting them needs C4.

  • If Cut 7 runs before C4: author them as plain records, and list them in C4's family re-author pass.
  • If it runs after: author them as variants.

Either way, nobody copies a record by hand twice.

5. Tunables: values for AsteroidYield, ChunkCompositionVariance and MiningDepthPerPenetration.

Verification:

  • census and dangling before and after.
  • Cut 5's loot fixture runs against the live catalog. It asserts that every ore in every kind is reachable, meaning some live weapon's damage type has an affinity above 1 for it.
  • EveryDamageTypeHasALiveWeapon pins Q8 = B.
  • Operator: review the sheet; mine with the new Corrosive and Ionizing weapons; feel the Drill Bit.

A.4 Subtraction ledger (estimate, whole campaign from here)

Cut Removed Added Formats and targets
1-2 (landed) 250 181 src, 681 test union 26 retired; ZonePack key 6
3 ~0 (optionally ~15, the scanner Update) ~140, plus ~95 mechanical site edits Target retyped; FieldKindData document type; AsteroidBeltData.Kind (nullable)
4 ~5 (integrator extraction) ~115 PendingShot and ShotOutcome target retyped; Detonate gains a chunk pass and a loot sink
5 ~21 (Resources, Mining.cs) ~190 FieldKindData.Entries; Extracted key 2; Lot moves to ItemInstance key 11; BodyData key 4 retired
6 deleted 0 none
7 0 content 14 ores, 1 field kind, the Drill Bit, 2 weapons

No new package, assembly or executable target. The net is additive from Cut 3 on. Each addition buys a named capability: detectable, targetable chunks, the loot roll, blast mining, and provenance. Deleting Cut 6 removed about 90 lines of AI code from the plan.

A.5 Operator questions (mining)

Ruled, 2026-09-30 (recorded, not reopened)

Q Ruling Where it lands
Q4 A, the formula as written Cut 5
Q5 A, per hit Cut 5
Q6a A, straight into cargo; overflow floats (ruling mining-overflow-floats, 2026-10-16, replaces 'lost') Cut 5
Q6b A, one lot per (zone, field, commodity) source Cut 5
Q7 A′, a per-kind entry list (commodity, abundance, affinity), derived, never stored per chunk Cuts 3 and 5
Q8 B, author Corrosive and Ionizing weapons (against v1's recommendation) Cut 7 §4
Q9 C, AI mining deferred; the path is proven by a test agent (against v1's recommendation) Cut 5; Cut 6 deleted
Q10 A, a blast covers every chunk it overlaps; the shooter gets the loot Cuts 4 and 5
Q11 field kinds are authored catalog records; tables yield simple commodities only Cuts 3, 5 and 7
Q12 A, launchers cannot mine Cuts 3 and 4
Q13 A, reach is the longest active weapon range. Detection correction: chunks are detected by the existing reflectance rule, not deferred to EW Cut 3

Earlier rulings: Q1 A, Q2 A (one slot, two kinds; field-kind generalization), Q3 A.

Open, two forks raised by the Q13 correction

Q14. How does a ship's knowledge of a rock build up over time? (Blocks Cut 3's ChunkInfo.)

Context. Your correction puts chunks on the same detection rule as ships:

  • visibility = cross-section × light (Reflector.cs:46);
  • each sensor adds info in proportion to visibility, sensitivity and angle over distance;
  • info decays.

For ships this is a per-tick integrator. Every sensor visits every entity every tick and keeps a running value per pair (Sensor.cs:157-189). Knowledge rises over a few seconds, then holds or fades. That per-tick loop is exactly what cannot run over rocks: the probe zone has 809, and the loop costs about 184 ns per body per tick per sensor, which is the kind of per-rock cost you asked to slim down.

The gain arithmetic itself is shared in every option below; Cut 3 extracts it into one function. The question is only how it is integrated over time for a rock.

Options.

  • A. Settled value, on demand. A rock's info is the value the per-tick rule would settle at, for this observer, now. It is computed when something asks (targeting, the fire gate), stored nowhere, and costs nothing per tick.
    • What you lose: the rise time. A lit rock is known at its settled level the moment it is in range. A rock never tries to hide, so that delay carries little play.
    • Pings: a ping's boost is visibility × … (Sensor.cs:170-175), so a dark rock gains nothing from a ping in any option.
    • A parity test pins that a ship with the same reflector, held still, settles at exactly A's value.
  • B. Per-tick accumulation within reach. Each sensor-bearing ship integrates rocks within its longest weapon range every tick.
    • It has the ships' exact dynamics, rise time included.
    • Cost per tick grows with ships × rocks in reach, and it keeps state per ship per rock, which needs pruning.
  • C. Lazy integration. Store (value, last-time) per ship per rock, and integrate the elapsed time when queried.
    • It is close to B's dynamics with no per-tick cost.
    • The state grows with every rock a ship ever looked at, and it is approximate when light changes between queries.

Recommend A. It is the same rule with no new state and no per-tick cost, and it matches the campaign's founding invariant: chunk quantities are pure functions of time. The one behaviour it drops, the rise time, belongs to things that move and hide, and rocks do neither.

Q16. How does a belt get its field kind, including belts in runs that started before this campaign? (Blocks Cut 3.)

Context.

  • Your Q11 ruling makes field kinds authored records. A chunk's reflectivity (Cut 3) and its yield table (Cut 5) both come from its belt's kind.
  • Today belts are generated with no kind (AsteroidBeltData, ZoneData.cs:101-105 @283ce7dc), and a saved run keeps its belts for the whole run.
  • Only one kind will exist at first ("Asteroid"), but you will add more.

Options.

  • A. Derived, never stored. A belt's kind is a pure function of the belt's identity and the catalog's kinds (weighted pick). Nothing is saved. Adding or reweighting a kind silently re-kinds belts the player has already mined.
  • B. Stored at generation, assigned once for old belts. New belts get a kind when generated. A belt loaded without one is assigned by the same function the first time it loads, and saved. Adding a kind later never changes an existing belt.
  • C. Stored at generation only. Belts from older runs stay kind-less: dark, unminable and unseen, until New Game.

Recommend B. One assignment function serves both paths (generation and first load), so there is no second rule. A belt keeps the identity the player learned. A is the only option where a catalog edit rewrites the world under a running save.

A.6 Dependencies (mining)

  • Fire-control-12 on master: hard, for every cut from M0 on.
  • Both CultLib roots in every build: CultCache 45c2f40 and CultMath 6d5e209 (shared precondition). The VerifyCultLibRevision guard does not fire on the Linux verify host. That is a separate defect, handed to Self.
  • The no-target fuse ruling: the same lines as Cut 4 (FireControl.cs:389). Land it first or fold it in.
  • F12-2, the penetration retune: Q4's MiningDepthPerPenetration absorbs it. It gates Cut 7 §4's optional Ionizing penetration step.
  • F12-6, mines: when mines ship, they are sourceless blasts. They wear chunks and yield nothing (Cut 4's null sink).
  • Settings-globals: Cut 5's tunables join GameplaySettings and PlanetSettings wherever those live. None may be CultMath-typed while settings are Unity-serialized.
  • No longer dependencies (Cut 6 deleted): locomotion Cut 4, and F12-5 (AI and proximity blasts).
  • Document variants:
    • C2 (element ids, in flight on hands/variants-c2a):
      • "Ids everywhere" (variants Q3) requires an element-id member on every object-list element type. For mining, that is FieldYield (new, Cut 5), the affinity pair type inside it (new), and ChunkWearPack (ZoneData.cs:51 @283ce7dc, a list element in ZonePack).
      • If C4 lands first, Cuts 3 and 5 add ids to their own new types. If they land first, C4's sweep lists them.
      • Slot coordination: Cut 5 claims key 11 on ItemInstance (the Lot move), and Cut 3 claims the next free key on AsteroidBeltData. C4 must not put an element id on either. Whichever lands second checks.
    • C3 and C4 (Studio, then Aetheria adoption at caching-unity-v1.5.0):
      • Field-kind families ("rich belt" as a variant of "Asteroid") and the two new weapons (variants of ClearPath and Spectra, with a nested DamageType override) are the natural variant content.
      • If Cut 7 lands before C4, it authors them as plain records and adds them to C4's re-author list.
    • Aetheria pins CultLib 45c2f40 and caching-unity-v1.4.0 today (Directory.Build.props). No mining cut needs a CultLib bump.
    • For the C4 owner, not mining: four catalog records hold a null Behaviors element: Refinery, Deep Ore Extractor, Assembly Line and Shipyard (section 0, present on b66ba524). C4's MintElementIds pass over Aetheria.cc has to decide what a null list element is: refuse it, drop it, or skip it. The variants cut map does not say.