From 7a515e6f45c3c2e4f2270a0247e3be688eecc06f Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Tue, 22 Sep 2026 20:27:02 +0200 Subject: [PATCH] Locomotion map revised: rulings recorded, content restore becomes Cut 1 Q1 (divergence accepted), Q2 (differential main thrust allowed, no thruster exclusion) and Q3 (fixtures follow the data) are ruled. A content-restore cut recovers the legacy thruster hulls, which the live catalog lost, and the rotation design no longer excludes any actuator. Co-Authored-By: Claude Opus 5 --- docs/locomotion-cut.md | 357 ++++++++++++++++++++++++++++++----------- 1 file changed, 265 insertions(+), 92 deletions(-) diff --git a/docs/locomotion-cut.md b/docs/locomotion-cut.md index 47d47d872..b015c6a7b 100644 --- a/docs/locomotion-cut.md +++ b/docs/locomotion-cut.md @@ -2,7 +2,10 @@ Date: 2026-09-22 -Status: target and cut map, from an Imagination pass. None of it has landed. Every anchor is against +Status: target and cut map, from an Imagination pass, revised 2026-09-22 after the operator ruled Q1, Q2 and +Q3 and asked for the legacy thruster hulls back. None of it has landed. **The cuts are renumbered by that +revision**: the content restore is the new Cut 1, and what the first committed version called Cuts 1 to 4 are now +Cuts 2 to 5. Every anchor is against `origin/codex/item-provenance` at `cd846916`; the working tree on `codex/fire-control-10` (`e8fbb6a6`) has an identical tree for every file named below. The first half of this document is the **target** (the ends). The second half is the **cut map** (the means). When the campaign closes, split the target out as @@ -52,6 +55,22 @@ Self, relaying an operator answer, 2026-09-22. This is the layer split, stated p - The long-way test therefore belongs to the shared-controller cut and drives `LookDirection` directly, as the player would. The crab-walk test belongs to the planner cut. +Operator rulings on this map, 2026-09-22, relayed by Self: + +- **Q1 = A, the divergence is accepted.** "The divergence is fine." A small divergence from base behaviour on + intact thruster ships is accepted: the Longinus's forward creep of about 10.7 m/s² while turning in place, and + the Djinn's roughly 9% faster strafe. Base is pinned exactly only where the old mixer and the allocator agree, + and the accepted divergences are named with their measured magnitudes in Cut 3's verification, so Soul can tell + an accepted divergence from a regression. **No per-thruster role rules are reintroduced to match base.** +- **Q2 = B, differential main thrust is allowed.** "That's absolutely viable and can be a better decision than + turning around the long way, I wouldn't introduce a thruster exclusion for this." Any live actuator's torque may + be spent on rotation, mains included. A damaged ship turning toward its dead side lurches forward, and the + planner may prefer that to the long way round when it is faster. +- **Q3, fixtures follow the data.** "Good catch on the Longinus main thrusters, I was misremembering." Two aft 2×2 + mains, as both legacy catalogs author them. +- **Restore the legacy thruster hulls.** Asked whether the lost layouts could come back: "can we restore these + layouts?" They become Cut 1, the campaign's first cut, ahead of the solver. + --- # Part I: Target @@ -87,9 +106,8 @@ creates a real handicap and not an exploit. Today's snap-to-look lerp (`Ship.cs:280-284`) is a free, thrusterless rotation, and it goes. 6. **Kinematic rotation is retained.** Rotation stays kinematic: yaw rate is proportional to live throttle (`Thruster.cs:106-107`, `AetherDrive.cs:154-155`), with no angular momentum. Nothing found here argues for - raising it. It keeps turn time exact (angle divided by rate), which the planner relies on. It also matches the - operator's model ("losing an attitude thruster means you can't rotate that way anymore"): with no momentum, - there is no coast. + raising it. It keeps turn time exact (angle divided by rate), which is what lets the planner compare a long + way round against a slow lurching turn without simulating either. 7. **A dead actuator never receives throttle.** A destroyed, disabled or unpowered actuator gets zero throttle, not a saturated request it cannot honour. 8. **Capability is one model.** Turn time, turn direction, rotation capacity and translation capacity are computed @@ -99,7 +117,7 @@ creates a real handicap and not an exploit. ## Canonical implementations - **Solver:** CultMath (`F:\Projects\CultLib\packages\cultmath`), a box-constrained linear least-squares solver, - added by Cut 1. It is the only numerical authority. Aetheria composes the problem and calls it. + added by Cut 2. It is the only numerical authority. Aetheria composes the problem and calls it. - **Allocation reference:** T. A. Johansen and T. I. Fossen, "Control allocation: A survey," *Automatica* 49(5), 2013. Its weighted-least-squares form with priorities is what `ShipControl` states. The active-set method follows O. Härkegård, "Efficient active set algorithms for solving constrained least squares problems in aircraft control @@ -108,10 +126,12 @@ creates a real handicap and not an exploit. ## Out of scope - **Fire-control Cut 12** (edge-on hit placement in `FireControl.cs`, `Entity.ApplyHit`, `DamageSchematic`) is - being mapped concurrently by another Imagination pass. Cuts 1 to 3 here touch none of those files. The allocator - is a read-only downstream consumer of item destruction (`ItemDamage`, `Entity.cs:488`). **Overlap:** Cut 4 (combat + being mapped concurrently by another Imagination pass. Cuts 2 to 4 here touch none of those files. The allocator + is a read-only downstream consumer of item destruction (`ItemDamage`, `Entity.cs:488`). **Overlap:** Cut 5 (combat facing) will rewrite `Agents/States/Combat.cs:88-127`, and presenting armour edge-on is exactly what Cut 12's hit - placement rewards. Cut 4 must be mapped after Cut 12 lands. + placement rewards. Cut 5 must be mapped after Cut 12 lands. Cut 1 restores catalog content, and the only file it + and Cut 12 could both touch is `GameData/Aetheria.cc`; Cut 1 must not be in flight while another cut writes the + catalog. - **The old AetheriaEve/CultMesh/daemon rebuild** is taxidermy. It is not precedent and not a consumer. - Thruster geometry (the normalized moment-arm `Thruster.Torque`, `Thruster.cs:64-69`, which ignores lever length and uses unweighted shape cells for centre of mass) is the game's model and stays as authored. Arrival braking in @@ -135,10 +155,11 @@ runtime-derived per tick, or per planner cadence, and are never saved. | `MovementDirection`, `LookDirection` (runtime) | fields on `Ship`/`Entity` | overwritten each frame by whoever flies the ship | player (`ActionGameManager`) or AI (`Agent` layer), never both on one ship (`Zone.cs:111` gives agents only to `!IsPlayerShip`) | | Committed turn direction, planner plan (runtime) | fields on `ShipControl` / `Agent` | reset on wormhole, dock, spawn | `ShipControl` / `Agent` | | `ThrusterData`, `AetherDriveData`, `HullData.Hardpoints/Shape` (persisted, catalog) | catalog record key | authored | unchanged; read only | -| `GameplaySettings.TorqueFloor`, `TorqueMultiplier`, `AetherTorqueMultiplier` (authored, `Settings.cs:205-207`; values in `Assets/Resources/Settings.asset`: 0.5, 0.1, 0.1) | field name | authored | unchanged values. `TorqueFloor` changes role; see Cut 2 | +| `GameplaySettings.TorqueFloor`, `TorqueMultiplier`, `AetherTorqueMultiplier` (authored, `Settings.cs:205-207`; values in `Assets/Resources/Settings.asset`: 0.5, 0.1, 0.1) | field name | authored | unchanged values. `TorqueFloor` changes role; see Cut 3 | -This campaign adds no authored field. If Hands finds it needs a tunable, that is an operator question, and any new -field on a persisted CultCache document must be nullable (MessagePack nil rule). +This campaign adds no authored **field**. Cut 1 adds authored **records** (hulls, thruster designs and their +products) to `GameData/Aetheria.cc` using the schema as it stands. If Hands finds it needs a tunable, that is an +operator question, and any new field on a persisted CultCache document must be nullable (MessagePack nil rule). --- @@ -178,7 +199,7 @@ clockwise positive. never used for rotation), attitude thrusters ±0.998. Djinni stern 0 / ±0.161, laterals ±0.727 (aft) and ±0.867 (fore), bow 0. -**Base flight table** (`cd846916`, after warm-up). These are the Cut 2 pins: +**Base flight table** (`cd846916`, after warm-up). These are the Cut 3 pins: | Input | Longinus (r, f, °/s) | Djinni (r, f, °/s) | |---|---|---| @@ -218,11 +239,12 @@ later thrusters push along an already-rotated nose. The allocator cannot see thi "Traction" (rotor mass (30,20,5), λ (0.5,1,1), MaxRpm 40000..80000). Two thruster designs exist ("deep space burnout", "Large Drive"), but no live ship can mount them (`docs/content-batch-one.md` §1 says the same). **AetherDrive is therefore the load-bearing actuator for the player today**, and the allocator must reproduce its - base behaviour exactly. + base behaviour exactly. Cut 1 restores the thruster hulls, which is what makes any play check on a thruster ship + possible at all. - **Existing tests sit on an off-centre thruster.** `BrownoutTests`/`ConditionRatioTests` equip a one-cell thruster by `TryFindSpace` onto a 5×5 hull. It lands at (1,1) with `Torque` −0.707, and `MovementDirection (0,−1)` fires it while it spins the ship. Under an allocator that holds heading, a lone off-centre thruster cannot fire without - rotating, so those fixtures must be re-seated (Cut 2). + rotating, so those fixtures must be re-seated (Cut 3). ## Legacy hull layouts (probe) @@ -241,8 +263,8 @@ live LonginusX's. Rows from y = 0: `.####.` ×4, `######` ×5, `.####.` ×2, `.. | Th.CW | (2,14) | 1×2 | CounterClockwise (pushes starboard, nose right) | | Th.CCW | (3,14) | 1×2 | Clockwise (pushes port, nose left) | -**The data has two aft 2×2 thrusters, not one.** The operator remembers "one big thruster". Fixtures follow the -data (see Q3). +**The data has two aft 2×2 thrusters, not one.** The operator remembered "one big thruster" and ruled on it, +2026-09-22: "Good catch on the Longinus main thrusters, I was misremembering." Fixtures follow the data. **Djinni** (mass 10000, drag 0.2, 14×17). Rows from y = 0: `....######....`, `...########...`, `..##########..`, `.############.` ×2, `##############` ×5, `.############.`, `...########...` ×3, @@ -284,23 +306,23 @@ matrices. A production float implementation without allocations will be faster t | Surface | Readers / writers | Fate | |---|---|---| | `Ship.MovementDirection` (`Ship.cs:27`) | W: `ActionGameManager.cs:1263` (player), `Agent.cs:67,73,77` (AI); tests `BrownoutTests.cs:143,402`, `ConditionRatioTests.cs:184,193,243,258`. R: `Ship.cs:261-291` | **Survives** as the translation input. Its meaning is defined by `ShipControl` (fraction of capacity per half-axis) | -| `Entity.LookDirection` (`Entity.cs:55`) | W: `ActionGameManager.cs:1254` (player), `Agent.cs:66`, `MoveTo.cs:26`, `Combat.cs:126`, `TurretController.cs:72,75` (turret entities; no `EntityTypeRestriction`, `TurretController.cs:14-15`). R (as heading): `Ship.cs:278`. R (as aim): `LockWeapon.cs:94`, `GuidedProjectileManager.cs:56`, `EntityInstance.cs:404`, `ShipInstance.cs:117` (tractor), `ZoneRenderer.cs:432`, `ActionGameManager.cs:376` (reticle target pick) | **Survives** as heading command *and* aim. `MoveTo.cs:26` dies in Cut 3 (a double writer: `Accelerate` overwrites it when Δv > 20). The heading/aim conflation is Cut 4's question (Q4) | +| `Entity.LookDirection` (`Entity.cs:55`) | W: `ActionGameManager.cs:1254` (player), `Agent.cs:66`, `MoveTo.cs:26`, `Combat.cs:126`, `TurretController.cs:72,75` (turret entities; no `EntityTypeRestriction`, `TurretController.cs:14-15`). R (as heading): `Ship.cs:278`. R (as aim): `LockWeapon.cs:94`, `GuidedProjectileManager.cs:56`, `EntityInstance.cs:404`, `ShipInstance.cs:117` (tractor), `ZoneRenderer.cs:432`, `ActionGameManager.cs:376` (reticle target pick) | **Survives** as heading command *and* aim. `MoveTo.cs:26` dies in Cut 4 (a double writer: `Accelerate` overwrites it when Δv > 20). The heading/aim conflation is Cut 5's question (Q4) | | `Thruster.Axis` (`Thruster.cs:45-49`) | W: `Ship.cs:256-288` only. R: `ShipInstance.cs:139` (particles), `Thruster.cs:88` (power request), `:96` (audio), `:102-111` | Getter **survives**. The public setter **dies**; only `ShipControl` commands throttle | | `AetherDrive.Axis` (`AetherDrive.cs:81-85`) | W: `Ship.cs:291` only. R: drive internals, audio `:170` | Same as `Thruster.Axis` | -| `IAnalogBehavior` (`Behaviors.cs:96-99`) | implemented by `Thruster.cs:40`; consumer only a comment, `Entity.cs:2007` | **Dies** (Cut 2 deletes first) | +| `IAnalogBehavior` (`Behaviors.cs:96-99`) | implemented by `Thruster.cs:40`; consumer only a comment, `Entity.cs:2007` | **Dies** (Cut 3 deletes first) | | Bucket sets `_forward/_reverse/_right/_left/_clockwise/_counterClockwiseThrusters`, `_thrusterItems`, `_aetherDrives`, `_aetherDriveItems` (`Ship.cs:30-39`) | `Ship.cs` only | **Die** | | `ForwardThrust`, `ReverseThrust`, `LeftStrafeThrust`, `RightStrafeThrust`, `ClockwiseTorque`, `CounterClockwiseTorque`, `Left/RightStrafeTotalTorque`, `Left/RightStrafeTorqueThrusters` (`Ship.cs:50-59`) | written by `RecalculateThrust`, read by the mixer; **no reader outside `Ship.cs`** (grep, including Unity side and tests) | **Die** | -| `Ship.TurnTime` (`Ship.cs:61-66`) | no consumer | **Dies**; replaced by `ShipControl.TurnTime` (Cut 2), consumed in Cut 3 | +| `Ship.TurnTime` (`Ship.cs:61-66`) | no consumer | **Dies**; replaced by `ShipControl.TurnTime` (Cut 3), consumed in Cut 4 | | `RemoveThruster`, `RemoveAetherDrive`, their `ItemDestroyed` subscriptions (`Ship.cs:123-143`) | internal | **Die**; liveness is read live (invariant 3) | -| `Agent.Accelerate` (`Agent.cs:58-79`), `FORWARD_DELTA_THRESHOLD`/`THRUST_DELTA_THRESHOLD` (`:16-17`) | called by `MoveTo.cs:27`, `Combat.cs:127` | **Rewritten** in Cut 3 (planner). Signature kept | +| `Agent.Accelerate` (`Agent.cs:58-79`), `FORWARD_DELTA_THRESHOLD`/`THRUST_DELTA_THRESHOLD` (`:16-17`) | called by `MoveTo.cs:27`, `Combat.cs:127` | **Rewritten** in Cut 4 (planner). Signature kept | | `Agent.TopSpeed` (`Agent.cs:26`) | `MoveTo.cs:25`, `Combat.cs:127`. No live catalog item carries `VelocityLimit` or `VelocityConversion` (probe), so it is 100 everywhere | Survives | -| `GameplaySettings.TorqueFloor` (`Settings.cs:205`) | `Ship.cs:115,118` | **Survives with a new role**: it sizes rotation authority (Cut 2) and no longer classifies which thrusters fire | +| `GameplaySettings.TorqueFloor` (`Settings.cs:205`) | `Ship.cs:115,118` | **Survives with a new role**: it sizes the rotation demand (Cut 3, Q5) and no longer classifies which thrusters fire | | `TorqueMultiplier`, `AetherTorqueMultiplier` | `Thruster.cs:107`, `AetherDrive.cs:155` | Survive; also enter the yaw-rate row of each column | | `Entity.Direction` writers | `Thruster.cs:106`, `AetherDrive.cs:154`, `Ship.cs:79` (wormhole), `Ship.cs:283` (snap), `ActionGameManager.cs:848` (spawn), `EntitySerializer.cs:66` | `Ship.cs:283` **dies** (invariant 5); the others survive | | `Entity.Velocity` external writers | `HullCollider.cs:15` (collision impulse), `ShieldManager.cs:79` (bounce), `VelocityConversion.cs:41`, `VelocityLimit.cs:48` | Not control; these are external forces and constraints. Untouched | | Undock capability check (`ActionGameManager.cs:913`) | `GetBehavior() == null` | Survives. It could later read the capability model; not this campaign | -| Mining, Survey, HaulingTask, StationTowing (`Agents/Tasks/*.cs`) | data-only task classes; no movement code (grep) | Not consumers. `PatrolOrbitsState` drives `MoveToOrbitState`, which Cut 3 covers | -| Player input path | `Input.Player.Look` → `_entityYawPitch` → `LookDirection` (`:1250-1254`); `Input.Player.Move` → `MovementDirection` (`:1261-1264`). No other action writes motion (grep of `Input.Player.*`: targeting, heat, shield, stance, tractor, ping, UI) | **Already on the shared surface**. The only bypasses are the ones `Ship.cs` itself holds (the snap, direct `Axis` writes), and both die in Cut 2 | +| Mining, Survey, HaulingTask, StationTowing (`Agents/Tasks/*.cs`) | data-only task classes; no movement code (grep) | Not consumers. `PatrolOrbitsState` drives `MoveToOrbitState`, which Cut 4 covers | +| Player input path | `Input.Player.Look` → `_entityYawPitch` → `LookDirection` (`:1250-1254`); `Input.Player.Move` → `MovementDirection` (`:1261-1264`). No other action writes motion (grep of `Input.Player.*`: targeting, heat, shield, stance, tractor, ping, UI) | **Already on the shared surface**. The only bypasses are the ones `Ship.cs` itself holds (the snap, direct `Axis` writes), and both die in Cut 3 | Out-of-scope note: the working tree has an uncommitted change to `Assets/Scripts/AetheriaInput.cs` (a "Cycle Target Item" action). That is fire-control work, and it does not touch motion. @@ -315,7 +337,13 @@ Status: cut map. Ends are owned by Part I above; this section owns the means. Nothing has landed. -Open: Q1 and Q2 block Cut 2's pins. Q3 blocks only fixture naming. Q4 blocks only Cut 4. +Rulings (operator, 2026-09-22): **Q1 A** ("The divergence is fine"), **Q2 B** ("that's absolutely viable and can +be a better decision than turning around the long way, I wouldn't introduce a thruster exclusion for this"), **Q3** +fixtures follow the data ("Good catch on the Longinus main thrusters, I was misremembering"), and Cut 1 exists +because of "can we restore these layouts?". The Q2 text below is marked as history; the live design is in Cut 3. + +Open: **Q4** (heading versus aim) blocks only Cut 5. **Q5** (how the rotation demand is sized under Q2 = B) blocks +Cut 3. Cut 1's recoverable-content list is an operator review item before Hands writes any record. Follow-ups outside this campaign: - `TurretController` has no `EntityTypeRestriction`, so equipping it on a ship would create a third @@ -325,7 +353,113 @@ Follow-ups outside this campaign: thrusters' rotation. It is a small artefact, and it is `Thruster.cs`'s to fix, not the allocator's. - `AetherDrive.Execute` logs "FUCK FUCK FUCK FUCK" on NaN velocity (`AetherDrive.cs:157-158`), a leftover probe. -## Cut 1. CultMath: bounded least squares (CultLib) +## Cut 1. Restore the legacy thruster ship hulls to the live catalog + +A pure content cut, separable from every code cut, so Soul can falsify it on its own. Operator, 2026-09-22: "can +we restore these layouts?" + +- **Repo/branch:** Aetheria, a branch from `codex/locomotion`. Depends on nothing. It blocks no code, but nothing + in this campaign can be flown on a thruster ship until it lands, and Cuts 3 and 4 get their real fixtures from + it. It writes `GameData/Aetheria.cc`, so it must not be in flight while another cut writes the catalog. +- **Standing rule** (memory: `aetheria-content-audits-pre-breach`): `GameData/Aetheria.cc` is the post-breach + bare-minimum rebuild, so absence there is not evidence of absence, and **the operator's word is the authority** + on what the pre-breach content was. The list below is therefore an **operator review item before Hands writes + anything into the catalog**. + +### What the legacy record actually holds (probe) + +Sources, all decoded in the scratchpad: the git-LFS objects behind `GameData/Legacy/AetherDB.2021-04-14.msgpack` +(`21ce7bd2…`, 76 KB) and `AetherDB.2021-03-05.msgpack` (`142998fd…`, 59 KB), and the last +`GameData/AetherDB.msgpack` before the Cut 9 import (`8963a686…`, 46 KB, the commit's own parent at `70fbaca1^`). +All three are present in `.git/lfs/objects`; nothing had to be fetched. + +The comparison explains the loss. The 2021-04-14 record carries 5 ship-class hulls and 45 gear designs. The import +source carries 3 hulls (LonginusX, Zenith, Turret) and 25 gear. **Longinus, Djinni and every thruster below the +2×2 class were already gone from the file Cut 9 imported**, which is why the live catalog has no thruster ship. + +**Recoverable in full from 2021-04-14** (name, shape, mass, price, hardpoint list with position, shape, rotation +and armour, behaviours, temperature band, manufacturer, prefab and schematic paths): + +| Record | Kind | Legacy detail | Notes | +|---|---|---|---| +| **Longinus** | ship hull | 6×17, mass 2500, drag 0.1, price 7,500,000, maker Alakrita | The shape is byte-identical to the live LonginusX's. 4 thruster hardpoints (`Th.L`/`Th.R` 2×2 aft, `Th.CW`/`Th.CCW` 1×2 in the nose) plus control module, 2 energy, 2 launcher, 2 radiator, reactor, sensors | +| **Djinni** | ship hull | 14×17, mass 10000, drag 0.2, price 10,000,000, maker Rossum & Douglas | 8 thruster hardpoints (3 stern, 4 lateral, 1 bow) plus 4 launcher, 2 ballistic, reactor, control module, 4 radiator, sensors, and a shield hardpoint | +| **Medium Drive** | thruster gear | 2×1, mass 100, thrust 100000..300000, price 50,000, no maker | **The one item both restored hulls need.** Fits the 2×1 stern/bow hardpoints and, rotated, the 1×2 lateral and nose ones | +| **Small Drive** | thruster gear | 1×1, mass 50, thrust 75000, price 25,000, no maker | Optional; the cheap end of the line | +| **Victoire** | thruster gear | 2×1, mass 25, thrust 200000..1500000, maker Alakrita | Optional; the racing thruster, and a natural Longinus fit | +| **Talaria** | thruster gear | 2×1, mass 150, thrust 75000..250000, maker Aeronautics Unlimited | Optional | +| **RevvITup 2.0** | thruster gear | 2×2, mass 150, thrust 250000..1000000, maker NiteLife Energy | Optional; a second 2×2 beside the two the catalog already ships | + +Everything those records depend on is **already in the live catalog**: all 12 factions (Alakrita and Rossum & +Douglas included), both existing 2×2 thrusters, and the Unity presentation. The prefabs still carry the hardpoint +transforms by their legacy names — `Th.L`, `Th.R`, `Th.CW`, `Th.CCW` in `Assets/Content/Prefabs/Ships/ +Longinus.prefab` (GUID `4a3db609…`, already the hull the live LonginusX points at) and `Thruster Stern 1..3`, +`Thruster Port/Starboard Fore/Aft` and `Thruster Bow` in `Djinni.prefab` (GUID `79024f63…`). The schematics exist +too (`schema_Longinus.png` `51702555…`, already referenced; `djinni_schematic_transparent.png` `66f3acdd…`, unused +today). So no art, no prefab work and no faction work is needed. + +**Not recoverable, because it post-dates the breach and has to be authored:** + +- `HardpointData.FiringArc` (Key 6) for the Djinni's weapon hardpoints. The legacy hardpoint record stops at + armour. 0 means "use `GameplaySettings.FiringArc`"; the `firing-arc-migrate` command in `tools/AetherDb` is the + precedent for deriving one from mount rotation. +- `ItemRole` lists on the restored designs, and `ProductRole` qualities on their products. + `RoleAuthoringTests` refuses a stat naming a role its design lacks, and `docs/content-batch-one.md` fixes the + nomenclature (`injector`/`nozzle` for thrusters, `plating` for hulls). The live "deep space burnout" carries + `injector, nozzle`; "Large Drive" carries none. +- A `FactionProductData` per restored design. `LoadoutGenerator` picks products, not designs, so a design without + one exists but is never generated or sold. The catalog holds 47 products today. +- Any `PowerSupply` term on the restored thrusters' stats (the stats-and-power cut's brownout curve). The shipped + thrusters request zero energy, so matching them is the safe default and is a content decision. +- Provenance is runtime, not authored: `ItemManager` mints the `Lot` (`docs/item-provenance-target.md`). + +**One open identification.** Both legacy hulls carry a second hull behaviour that LonginusX does not (behaviour +tag 11 in the legacy union, a single `PerformanceStat`; Longinus `50..50`, Djinni the same). LonginusX carries only +the one LonginusX still has today, `ReflectorData`. Hands must identify tag 11 against the current `BehaviorData` +union before authoring. **If it is `VelocityConversion`, it matters to this whole campaign**: that behaviour damps +velocity toward the nose (`VelocityConversion.cs:41`), which is a direct tax on crab walking, and no item in the +live catalog carries it. Put the identification in front of the operator with the rest of the list. + +### The cut + +- **Deletes first:** nothing. This is content restoration. +- **Adds:** the two hull records, `Medium Drive`, and whichever optional thrusters the operator's review keeps; + a `FactionProductData` for each; roles for each; firing arcs on the Djinni's weapon hardpoints. +- **Mechanism:** a one-shot command in `tools/AetherDb` (`restore-hulls [apply]`), in the shape of the existing + `*-migrate [apply]` commands: dry run by default, opens the catalog through `AetheriaStores.Open(catalogWritable: + true)` only when applying, writes every record in one `Commit`, and refuses to run twice. The Cut 9 importer + (`tools/AetherDb/Import.cs`, added in `70fbaca1` and deleted by Cut 10) is the precedent for reading the legacy + `[tag, payload]` records and rewriting them by the target type's `[Key]` shape; read it out of git history rather + than re-deriving the mapping. **Delete the command in the same campaign, as Cut 10 deleted the importer**, so the + carrying cost of the restore is zero once it has run. +- **Authority map:** + - Owner: the catalog (`GameData/Aetheria.cc`) owns authored content; the operator owns what the content *is*. + - Inputs: the legacy LFS records, the existing factions and assets, the operator's review of the list above. + - Outputs: new `HullData`, `GearData` and `FactionProductData` records. + - Derived state: none. Runtime `Lot`s and provenance are minted as for any other design. + - Forbidden writers: nothing else may write the catalog while this cut is in flight; the one-shot command must + refuse a second apply rather than upsert twice. + - Deletion line: the command goes once the records are in. +- **Verification:** + - `FireControlCut7Tests.ShippedCatalogOpensAndGeneratesAnArmedHull` still passes unchanged: the real catalog + opens, every `EquippableItemData` deserializes, and LonginusX still generates an armed loadout. + - New `RestoredHullsTests` against the real catalog, read-only: each restored hull exists, has the expected + thruster hardpoint count (Longinus 4, Djinni 8), every thruster hardpoint accepts a catalog thruster design + through `TryEquip` at that hardpoint's own position, and the ship then moves and turns under + `MovementDirection`/`LookDirection` for a few ticks. Before Cut 3 this runs on the old mixer, which is the + point: it proves the content, not the controller. + - `LoadoutGenerator` generates each restored hull when filtered to it, which is the check that the products and + roles are authored correctly. + - `dotnet run --project tools/AetherDb -- census` names the two hulls with their makers, and `dangling` reports + no new dangling reference. + - Operator: the review of the list above before anything is written, then the Studio click-through, then a play + check flying a restored hull. That play check is what unblocks the rest of this campaign. +- **Operator questions:** the recoverable list itself, and the behaviour-tag-11 identification, both above. +- **Ledger estimate:** no source removed. About 3 to 7 catalog records added, plus their products, plus a one-shot + command of roughly 150 lines that is deleted again in this campaign. Net carrying cost: the content, and nothing + else. + +## Cut 2. CultMath: bounded least squares (CultLib) - **Repo/branch:** `F:\Projects\CultLib`, a branch from `45c2f40` (Aetheria's pinned `CultLibRevision`, `Directory.Build.props`). Depends on nothing. @@ -362,15 +496,16 @@ Follow-ups outside this campaign: path. In Aetheria, bump `Packages/manifest.json`'s `org.gamecult.cultmath` tag and `Directory.Build.props` `CultLibRevision` in one commit. - **Authority map:** Owner: CultMath. Inputs: A, b, bounds, warm start. Outputs: x and status. Forbidden writers: - no least-squares or linear-solve code in `Aetheria/Assets/Scripts/**` (negative grep in Cut 2). + no least-squares or linear-solve code in `Aetheria/Assets/Scripts/**` (negative grep in Cut 3). - **Verification:** `dotnet build packages/cultmath/src/CultMath/CultMath.csproj`; `dotnet test` of CultMath.Tests. Aetheria's `dotnet test tests/Aetheria.Shared.Tests` still passes 244 after the bump. Unity compiles the new package, and only the operator can check that. - **Ledger estimate:** +150..220 lines of source and +150 lines of test in CultLib; Aetheria gets a 2-line bump. -## Cut 2. `ShipControl`: shared controller and allocator replace the mixer +## Cut 3. `ShipControl`: shared controller and allocator replace the mixer -- **Repo/branch:** Aetheria, a branch from the Cut 1 bump commit. Depends on Cut 1. +- **Repo/branch:** Aetheria, a branch from the Cut 2 bump commit. Depends on Cut 2. Cut 1 is not a + prerequisite for the code, but its restored hulls are what the operator's play check needs. - **First:** before editing, commit the base flight numbers above as test constants, reproduced by the fixture spec in "Findings". They are computed at `cd846916`, never captured from new code. Also run a LonginusX-with-Traction pin at base: `AetherDrive.Axis` for each probe input is `(my, mx, sqrt(|sin e|)·sign)`, including e = 30°, which @@ -411,22 +546,36 @@ Follow-ups outside this campaign: cancels the scale, so throttle equals input. - **Heading controller** (below the inputs, shared): - `look = LookDirection.xz`. A zero look means hold heading: no NaN, no command. - - Per rotation direction, rotation authority `ω_cap(dir)` is the sum of that direction's yaw-rate entries over - columns whose `|Thruster.Torque| > TorqueFloor`. Drive torque columns always count. Under Q2 (A) that is the - whole of rotation authority. **`TorqueFloor` no longer decides which thrusters fire.** It only sizes how - much rotation may be demanded. + - **No actuator is excluded from rotation** (Q2 = B). The allocator may spend any live column's torque to meet + the rotation demand, mains included, and a ship with no attitude thruster left on one side turns that way by + differential main thrust, lurching forward while it does. + - What `TorqueFloor` still does is **size the demand**, not gate an actuator. Per rotation direction, + `ω_cap(dir)` is the sum of that direction's yaw-rate entries over columns whose + `|Thruster.Torque| > TorqueFloor` (drive torque columns always count); **if that sum is zero, `ω_cap(dir)` + is the sum over every column instead.** So a ship with attitude authority does not burn its mains to turn + a few percent faster, and a ship without it still turns. This piecewise sizing is Q5; it is the only part of + the Q2 = B design that is a choice rather than a consequence. + - Probe, at the fixture values: a Longinus with `Th.CCW` destroyed has zero counter-clockwise attitude + authority, so the fall-back sizes the demand from its mains, and the allocator turns it counter-clockwise at + 78.5°/s with `Th.R` at full throttle and 86.20 m/s² of forward lurch. Clockwise is untouched at 394.4°/s. + A hull whose single main sits on the centre line (torque 0) has no counter-clockwise authority at all, even + with the fall-back, so for that ship the long way round is the only way round. - **Direction choice:** pick the direction that minimizes `angle_in_that_direction / ω_cap(dir)`, with - infinity where `ω_cap = 0`. This yields the long way round. Keep the committed direction unless the other - direction is faster by a margin, so symmetric ships do not dither near 180°. + infinity where `ω_cap = 0`. With the fall-back in play this is a real comparison rather than a formality: + the damaged Longinus above takes the long way round for a 90° turn to port (270°/394.4 = 0.685 s against + 90°/78.5 = 1.15 s) and the lurching short way for a 20° turn to port (0.255 s against 0.862 s), which is + exactly the operator's "can be a better decision than turning around the long way". Keep the committed + direction unless the other is faster by a margin, so symmetric ships do not dither near 180°. - **Rate demand:** `ω_d = min(profile(e)·ω_cap, e/dt)`, where `profile(e) = sqrt(sin e)` for e ≤ 90° (base's curve, which preserves the pins) and 1 beyond. The `e/dt` cap lands the heading without overshoot and replaces the snap. - - It exposes `TurnTime(heading)` and `RotationCapacity(dir)` for Cut 3. + - It exposes `TurnTime(heading)` and `RotationCapacity(dir)` for Cut 4. Both account for the fall-back, so + the planner sees the same slow lurching turn the controller would actually fly. - **Translation demand:** - `F_d = (mx·cap(±x), my·cap(±y))`, where `cap(d)` is the along-d component of the allocator's own solution to "a huge demand along d, rotation held at zero". Capacity is self-consistent: one objective, and no second rule for what full stick means. - - It exposes `TranslationCapacity(bodyDir)` along any body direction, same definition, for Cut 3. + - It exposes `TranslationCapacity(bodyDir)` along any body direction, same definition, for Cut 4. - Probe values: Longinus cap(+y) 172.41, cap(+x) 13.74 (with side effects, so the demanded strafe comes out about 0.09 m/s²). Djinni cap(+x) 37.04, cap(+y) 55.56, cap(−y) 18.52. - **Allocation objective:** one bounded-LS call, priorities by weight separation (Johansen & Fossen § @@ -477,14 +626,33 @@ Follow-ups outside this campaign: - Tests (new file `LocomotionControlTests.cs`, fixtures exactly as in "Findings": off-origin, off-axis nose with `|fx| > |fy|`, nonzero dt, asymmetric damage): - `IntactLonginusMatchesBase` and `IntactDjinniMatchesBase`: every row of the base table within tolerance - (2% of that ship's full-scale acceleration or turn rate), **except the divergences Q1 names**, which are - pinned at their probed new values once ruled. Pins the allocator against base. + (2% of that ship's full-scale acceleration or turn rate), except the four accepted divergences below, which + are pinned at their own probed values. Pins the allocator against base. + - **Accepted divergences** (operator, 2026-09-22, "The divergence is fine"). Soul reads these as pinned + behaviour, not as regressions, and anything outside them is a regression: + 1. Longinus turning in place fires a main at 0.12 throttle: forward creep 10.7 m/s² where base had 0, and + lateral slip 67.3 m/s² where base had 69.0. + 2. Longinus thrusting forward while turning 30° keeps 164.8 m/s² of the 172.4 base forward (−4%). + 3. Djinni strafe reaches 37.0 m/s² against base's 34.0 (+9%), by balancing torque with Stern 3 and Bow. + 4. Djinni turning in place runs Stern 2 and Bow at about 0.10 each; net force stays zero, but their exhaust + is visible. + Plus two intended fixes, which are not divergences to tolerate but behaviour this cut is buying: turns past + 90° run at full rate instead of decaying to zero at 180°, and `|Direction|` no longer shrinks when the look + is directly aft. - `LonginusXDriveAxisEqualsBaseFormula`: a drive-only hull; for every probe input, `AetherDrive.Axis` equals `(my, mx, sqrt(|sin e|)·sign)` to 1e-3. Pins the live player ship's feel exactly. - - `LonginusMissingCcwReachesHeadingOnDeadSideTheLongWay`: `Th.CCW` destroyed through the real `ItemDamage` - path. Write `LookDirection` directly, as the player would, to 90° left. Heading changes are clockwise every - tick (never CCW), and the ship reaches within 2° in under 1.0 s; the base-rate bound is 270°/394.33°/s = - 0.685 s. Base comparison: error still 90.0° after 30 s. Pins the shared feasible-direction choice. + - `CentreMainHullTakesTheLongWayRound`: the long-way fixture, built so the long way is the *only* way. One + centred main (torque 0, so no differential authority) and one clockwise attitude thruster, the + counter-clockwise one destroyed through the real `ItemDamage` path. Write `LookDirection` directly, as the + player would, to 90° left. Heading changes clockwise every tick (never counter-clockwise), and the ship + arrives within 2° in under 1.0 s; the base-rate bound is 270°/394.33°/s = 0.685 s. Base comparison: a ship + of that shape never turns at all. Pins the shared feasible-direction choice. + - `LonginusMissingCcwPrefersTheLongWayAtNinetyDegrees`: the two-main Longinus with `Th.CCW` destroyed, looking + 90° left. It turns clockwise, because 0.685 s beats the 1.15 s its mains would take. Pins that the + comparison is by time, not by a rule about which actuators are "for" turning. + - `LonginusMissingCcwLurchesRoundTheShortWayAtTwentyDegrees`: the same ship looking 20° left. It turns + counter-clockwise on differential main thrust (`Th.R` at full), arrives within 2°, and its forward velocity + rises while it does. Pins Q2 = B: the lurching turn is available and is chosen when it is faster. - `LonginusMissingCcwTurnsRightAsBefore`: looking 90° right reaches within 2° in 0.32 s ± one tick (base value). Pins that damage on one side leaves the other side alone. - `LookDirectlyBehindTurns`: intact, look exactly aft. The ship turns, `|Direction|` stays 1 ± 1e-4, and there @@ -512,20 +680,20 @@ Follow-ups outside this campaign: - `lerp\(Direction` in `Ship.cs` should match nothing. - No least-squares code outside CultMath: `Cholesky|pseudo.?inverse|ActiveSet` in `Assets/Scripts` should match nothing. - - Stryker: `dotnet stryker --since:` from `tests/Aetheria.Shared.Tests`. Every non-equivalent + - Stryker: `dotnet stryker --since:` from `tests/Aetheria.Shared.Tests`. Every non-equivalent survivor in `ShipControl.cs`, `Thruster.cs` and `AetherDrive.cs` is killed or triaged by name. Float boundary flips are equivalent, per the fire-control Cut 11 ruling. - Operator: fly LonginusX and confirm it feels unchanged (strafe, turn, look behind now turns). Then fly a - thruster ship, which needs a content hull with `Thruster` hardpoints; until one exists this check is - fixture-only, so say so. -- **Operator questions:** Q1 and Q2, below. + restored Longinus or Djinni from Cut 1, shoot a thruster off it, and confirm the damaged handling and the + lurching turn. Without Cut 1 this half of the check cannot be done at all. +- **Operator questions:** Q5, below. Q1, Q2 and Q3 are ruled. - **Ledger estimate:** −200 lines in `Ship.cs`, −10 elsewhere. `ShipControl.cs` adds about 250–350 lines, tests about 400. Net source roughly +100. That buys actuator-loss capability, which is the explicit ask, and removes seven parallel role sets, the snap, and permanent pruning. -## Cut 3. Heading planner in the AI +## Cut 4. Heading planner in the AI -- **Repo/branch:** Aetheria, after Cut 2. +- **Repo/branch:** Aetheria, after Cut 3. - **First:** pin base AI results as constants. On base, a Djinni with all stern thrusters destroyed never reaches the target (speed 0 at 120 s). Intact, it reaches it in 4.34 s. Both come from the probe (Agent plus MoveTo, dt 0.02, target 300 m to port from (37,−12), nose `normalize(0.8,0.6)`). @@ -536,6 +704,10 @@ Follow-ups outside this campaign: - Candidates: 16 evenly spaced headings, plus the Δv direction, plus the current heading. - Score for each: `TurnTime(θ) + |Δv| / TranslationCapacity(Δv expressed in θ's body frame)`, infinite where capacity is 0. Both terms come from `ShipControl`'s queries (invariant 8). + - **No direction is assumed impossible.** Under Q2 = B a damaged ship usually can still turn both ways, one of + them slowly and with a lurch, so `TurnTime` returns a finite number either way and the scoring compares them. + Nothing in the planner may branch on "this ship is a one-way turner"; the only asymmetry it sees is in the + numbers `ShipControl` hands it. - Pick the argmin with hysteresis: replan when a candidate beats the current plan by a margin, and on a cadence (for example every 0.25 s) or when the actuator set changes. The cadence exists for cost: 18 capacity solves per replan. @@ -559,80 +731,81 @@ Follow-ups outside this campaign: the stuck-forever cheese. - `CrabBeatsPointAndBurnWhenMainIsWeak`: Djinni with Stern 1 and Stern 2 destroyed (asymmetric; Stern 3's torque is balanced by the laterals), target 300 m at 60° left. The planner arrives strictly sooner than a naive - point-and-burn agent run in the same harness through the same Cut 2 allocator. Pins that crab walking wins when + point-and-burn agent run in the same harness through the same Cut 3 allocator. Pins that crab walking wins when it should, as a relation and not a captured number. - `IntactShipsStillPointAndBurn`: an intact Djinni picks a heading within 10° of Δv and arrives no later than base's 4.34 s + 10%. Pins no regression. - `LonginusWithoutReverseFlipsToBrake`: target velocity opposite the current velocity; the planner turns toward it (a Longinus has no reverse thrust). + - `PlannerPrefersTheLurchingTurnWhenItIsFaster`: the Longinus with `Th.CCW` destroyed, given a Δv 20° to port. + The heading it writes is reached counter-clockwise, and the forward lurch shows in the trajectory. Pins that + the planner inherits the controller's Q2 = B comparison instead of a one-way-turner assumption. - `PlannerUsesShipControlCapacities`: after destroying a thruster, the planner's turn time for a dead-side heading equals `ShipControl.TurnTime` for that heading. Pins invariant 8 and the long-way-aware estimate. - Negative grep: `LookDirection` in `Agents/States/MoveTo.cs` should match nothing. - - Stryker `--since:` over `Agent.cs`, `MoveTo.cs` and `ShipControl.cs` queries. + - Stryker `--since:` over `Agent.cs`, `MoveTo.cs` and `ShipControl.cs` queries. - Operator: watch a patrol Djinni with a stern thruster shot out crab to its next orbit. - **Ledger estimate:** −22 lines, +120 lines of source, +250 lines of test. -## Cut 4. Combat facing and tactics (target shape only) +## Cut 5. Combat facing and tactics (target shape only) This cut gets mapped after fire-control Cut 12 lands, because that cut decides what presenting a side is worth. - **Owner:** `CombatState` (`Combat.cs:88-127`), above the inputs. - **Shape:** choose the orbit side and facing from `ShipControl` capabilities (per-direction rotation authority, - translation capacity along candidate directions) and from weapon arcs. A one-way turner keeps its target drifting - toward its live turn direction. A ship with good turret arcs and poor rotation holds a heading and lets the - turrets work. Weapons with fixed arcs pull the heading onto the target. It uses the Cut 3 planner for thrust. + translation capacity along candidate directions) and from weapon arcs. Under Q2 = B there are few true one-way + turners, so the tactic is about asymmetry rather than impossibility: a ship that turns one way fast and the other + way slowly, with a lurch, keeps its target drifting toward its fast side. A ship with good turret arcs and poor rotation holds a heading and lets the + turrets work. Weapons with fixed arcs pull the heading onto the target. It uses the Cut 4 planner for thrust. - **Fork:** Q4 (heading versus aim), which must be ruled before mapping. ## Operator questions -Each question gives options and a recommendation. Self paces these one at a time. +Each question gives options and a recommendation. Self paces these one at a time. Q1, Q2 and Q3 were ruled on +2026-09-22; their text is kept as history, and the live design is in the cuts above. -- **Q1: intact-ship divergence from base (blocks Cut 2 pins).** The probe of the stated objective found these - differences on intact ships (new versus base): - - Longinus turning in place also fires a main at 12%, which adds 10.7 m/s² of forward creep (about 6% of full - forward) and cuts lateral slip from 69.0 to 67.3. The L2 objective trades a little slip for a little creep. - - Longinus forward+turn at 30° keeps 164.8 of 172.4 forward (−4%). - - Djinni strafe reaches 37.0 m/s² instead of 34.0 (+9%), by balancing torque with Stern 3 plus Bow. - - Djinni turning bleeds Stern 2 plus Bow at 10% each (zero net force, visible exhaust). - - Turns past 90° now run at full rate instead of slowing to zero at 180°. +- **Q5: how the rotation demand is sized under Q2 = B (blocks Cut 3).** No actuator is excluded from rotation, so + the only remaining choice is how much rotation the controller asks for. The demand cannot simply be "everything + every actuator could produce": a Longinus turning 90° in place would then run a main at 0.97 throttle and lurch + forward at 84 m/s², about half its full thrust, on every hard turn (probe). + - **A.** Size the demand from attitude authority (columns above `TorqueFloor`, plus drive torque), and fall back + to every column when that is zero for a direction. An intact ship does not burn its mains to turn a few + percent faster; a ship with no attitude thruster left on one side still turns that way, at 78.5°/s with a + lurch on the fixture Longinus. No actuator is ever excluded from *meeting* the demand. + - **B.** Always size from every column. Simpler to state, and it makes every hard turn on a ship with off-centre + mains a forward lurch, intact or not. - Everything else in the base table matches within 2%, including LonginusX's drive, which matches exactly. - - **A.** Accept these as the allocator's honest optimum and pin base exactly where they coincide. - - **B.** Require exact base behaviour. That means per-thruster role rules in the objective, which is the bucket - machine this campaign deletes. - - **Recommended: A.** The only live ship (LonginusX) is exact under A. The divergences are small, physical, and - arise only on thruster hulls, which the live catalog does not yet carry. -- **Q2: rotation from translation thrusters (blocks Cut 2).** When no thruster above `TorqueFloor` can turn a ship - one way, may the controller turn it that way by differential translation thrust? For a Longinus that is its two - mains: about 78°/s with a forward lurch. - - **A.** No. Rotation authority comes only from thrusters above `TorqueFloor` and drive torque, so a lost attitude - thruster means that direction is gone. - - **B.** Yes, as a slower fallback. - - **Recommended: A**, because it matches the operator's words ("losing an attitude thruster means you can't rotate - that way anymore") and keeps turning from making the ship lurch. Consequence of A: a Longinus with both attitude - thrusters gone cannot turn at all. -- **Q3: Longinus fixture layout (fixture naming only).** Both legacy catalogs author two aft 2×2 thrusters (`Th.L`, - `Th.R`), not the one big thruster the operator remembers. - - **A.** Fixtures follow the data: two mains. - - **B.** Model one centred main. - - **Recommended: A**, with a note that a lost main is itself a test case (`LonginusMissingMainHoldsHeadingUnderThrust`). -- **Q4: heading versus aim (blocks Cut 4 only).** `LookDirection` is both the heading command and the aim, read by + **Recommended: A.** It is what makes Q2 = B's lurching turn an option the ship takes when it is worth it, + instead of a permanent tax on turning. The cost of A is that `ω_cap` is piecewise, which is stated in Cut 3 and + pinned by two tests. +- **Q4: heading versus aim (blocks Cut 5 only).** `LookDirection` is both the heading command and the aim, read by `LockWeapon.cs:94`'s lock cone, guided projectiles, reticle targeting and the tractor beam. Capability-aware facing wants the AI to hold a heading that is not its aim, and under R2 any split applies to the player too. - **A.** Add a heading input beside the aim on the shared surface. The player's heading follows the view unless a player control is added later. - **B.** Keep one input, and the AI accepts degraded lock cones and missile aim while it manoeuvres. - **Recommended: A**, decided when Cut 4 is mapped. + **Recommended: A**, decided when Cut 5 is mapped. + +### Ruled (history) + +- **Q1: intact-ship divergence from base.** **Ruled A, 2026-09-22: "The divergence is fine."** The four accepted + divergences and their magnitudes are listed in Cut 3's verification. The rejected option was to require exact + base behaviour, which would have meant reintroducing per-thruster role rules into the objective. +- **Q2: rotation from translation thrusters.** **Ruled B, 2026-09-22: "that's absolutely viable and can be a + better decision than turning around the long way, I wouldn't introduce a thruster exclusion for this."** The + option this map recommended (A: rotation authority only from thrusters above `TorqueFloor`, so a lost attitude + thruster means that direction is gone) was rejected. Cut 3 and Cut 4 are written to B, and Q5 is what is left of + the question. +- **Q3: Longinus fixture layout.** **Ruled, 2026-09-22: "Good catch on the Longinus main thrusters, I was + misremembering."** Fixtures follow the data: two aft 2×2 mains. ## Subtraction ledger (estimates) | Cut | Removed | Added | Dependencies / targets | |---|---|---|---| -| 1 | 0 | ~200 src + ~150 test (CultLib) | CultMath gains one file; cultmath-unity 0.2.4; Aetheria pin bump | -| 2 | ~210 (`Ship.cs` ~200, `Behaviors.cs` 4, `Entity.cs` 1, setters) | ~300 src + ~400 test | `IAnalogBehavior` removed; no new package, target or authored field | -| 3 | ~22 | ~120 src + ~250 test | none | -| 4 | not mapped | not mapped | not mapped | +| 1 | 0 source; the one-shot command is deleted again in this campaign | 3–7 catalog records + products; ~150 lines of throwaway tool + ~120 test | No new package, target, asset or authored field. Existing prefabs, schematics and factions are reused | +| 2 | 0 | ~200 src + ~150 test (CultLib) | CultMath gains one file; cultmath-unity 0.2.4; Aetheria pin bump | +| 3 | ~210 (`Ship.cs` ~200, `Behaviors.cs` 4, `Entity.cs` 1, setters) | ~300 src + ~450 test | `IAnalogBehavior` removed; no new package, target or authored field | +| 4 | ~22 | ~130 src + ~280 test | none | +| 5 | not mapped | not mapped | not mapped |