F6/N2: added WideningDoesNotLeakToANonEntranceNoLagrangeZoneThatRollsAStation.
Seed 3's "EAC-7089" (the same no-Lagrange fixture the widened-candidate test
uses) naturally has no genuine Lagrange candidate; boosting one faction's
presence there via the same public SavedGame round trip
BuildGalaxyWithForcedEntrance uses makes its station-count roll nonzero
without touching ZoneGenerator's private RNG. Verified directly: dropping
ZoneGenerator's isTutorialEntrance guard on the widening condition turns
this fixture's station count from 0 into 2, so the fixture genuinely
exercises the leak path this test pins closed.
F7: fixed the leak test's comment (the widened-candidate fixture is seed 3,
not seed 1, so it no longer claims to share a fixture with a test that
doesn't). ForcedTutorialEntranceStationDoesNotInflateEnemyCount asserted
shipCount == 2, which was pinning TutorialZoneSettings' own default
NeutralWandererCount (2), not the enemy roll baseStationCount gates -- it
now filters ShipPacks by the same nearest-faction ZoneGenerator computes
internally (EntityPack.Faction), asserting the real enemy count (0) instead.
Also (Soul's finding): IsRosetteMember's planet-orbit count
(pack.Orbits.Count minus the OrbitalEntityPack count) over-counts whenever
a station or turret loadout generator returns null, since
CreateLagrangeOrbit already appended that orbit before the null check
skips adding the entity. It now derives the set from pack.Planets' own
Orbit refs directly (Census.cs's own approach), which sidesteps the bug
entirely.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
RestoredHullsTests.Settings() hand-set thermal radiation/conduction at
their bare-class defaults (1/1/1) against the shipped 3 / 1e-8 / 0.01
(Assets/Resources/Settings.asset), so equipped parts froze toward 0 K on
real physics and every test built on that fixture ran on thermal behaviour
nobody ships. It now loads the real GameplaySettings through
AuthoredSettings (tools/AetherDb/AuthoredSettings.cs), the same headless
YAML reader tools/AetherDb already uses to inspect Settings.asset outside
Unity -- the tests project now references AetherDb.csproj to reuse it
instead of hand-copying the same fields a second time.
LonginusNeverExceedsItsRestoredTopSpeedUnderFullThrust no longer needs
OverrideShutdown to keep the ship flying (that was compensating for the
broken fixture shutting equipment down within ~1s); it now drives a
generated Longinus (seed-pinned, including ItemManager.Random, which
defaults to a DateTime.Now seed and was making item quality -- and so
the measured speed -- non-deterministic) for 5s, the window Soul measured
before this loadout's thrusters cross 386 K and shut down. The tolerance
is now asymmetric instead of the old +-10: +4 on the ceiling (the mixer's
own tick order settles real, unforced flight at the cap plus one tick's
acceleration, measured at ~103.3 for this loadout) and -3 on the floor
(tight enough to catch a nerfed TopSpeed catalog value, which +-10 or a
wider ceiling would not).
Every other test in the file now runs on real settings instead of the
frozen fixture; all 15 tests in RestoredHullsTests pass unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
F3: the throw fired on selectedStationOrbits.Length == 0, which is also true
whenever stationCount rolls 0 -- an unrelated variable. It now checks
lagrangeCandidates.Length == 0 directly, the actual "no candidate orbit"
case, and the message says so.
F4: CreateLagrangeOrbit's FixedPosition copy and its comment described a
fallback orbit that no longer exists (widening always returns a real,
parented planet orbit). Deleted; nothing reads FixedPosition off a
lagrange/turret orbit.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
F1 (speed cap): assert against Longinus's legacy TopSpeed literal (100)
instead of reading the bound back out of the same catalog value being
tested -- the old assertion passed under TopSpeed=1000 because the
bound moved with the mutation. Also fixes the false "drag/thrust
balance" comment: there is no such balance near 100. At full throttle
Longinus's own thrusters heat their hull past ThermalOnline's shutdown
threshold within ~1s, and once the hull's own cell goes offline the
same way, VelocityLimit (a hull behaviour, gated identically) shuts off
with it, so the cap was only ever exercised for the first ~15 ticks.
OverrideShutdown -- a real, first-class field (the cockpit's "Override
Shutdown" control) -- forces the hull and both firing thrusters to keep
running for the whole 12s scenario, so the cap is what actually holds
speed down throughout, not an unreached ceiling.
F2 (entrance station): EntranceZoneAlwaysGetsADockedStationAcrossSeeds
now also requires the normal case to land on a genuine (non-rosette)
Lagrange orbit. Adds TutorialEntranceWithNoLagrangeCandidateStillSeats...,
built via a public SavedGame/SavedZone round trip (no reflection) that
re-targets a real galaxy's Entrance to a real zone already known to
have no Lagrange candidate (seed 3's "EAC-7089": four planets, one
rosette, no captured satellites) -- pins a docked station whose turrets
land on a real, finite, nearby orbit. Adds the leak checks (override
must not reach other tutorial zones or a non-tutorial game's own
entrance) and the enemy-count check (the override must not also raise
the enemy roll, which stays keyed to the pre-override station count).
F3 (asset identity): EveryAssetReferenceResolvesToAMetaGuid only checks
that a referenced GUID exists somewhere under Assets/, so it passed
when Djinni's Prefab was swapped for Longinus's. The new test compares
each reference against the specific legacy file's own GUID (Djinni's
own .prefab.meta; Thruster 1.prefab.meta for every restored thruster's
ParticlesPrefab).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Two writers decided the tutorial entrance's station orbit: the floor
(stationCount < 1 -> 1) and a synthesized fallback orbit, each masking
bugs in the other (removing either alone still passed the entrance
test). Collapse to one rule: the floor still guarantees stationCount
>= 1, but its orbit always comes from the same Lagrange selection. When
that selection has no candidate (a zone that is entirely one rosette),
widen it to every parented planet orbit ordinary placement draws from
before the rosette filter, instead of inventing an unparented,
zero-distance orbit -- which gave PlaceTurret's phase a division by
zero and left an orphan orbit record. If even the widened set is empty,
throw loudly instead of silently omitting the entrance's station.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Four tests against the real shipped catalog and the tutorial construction path, each pinning one
of this campaign's rules:
- EveryAssetReferenceResolvesToAMetaGuid: every hull's Prefab, every ThrusterData.ParticlesPrefab,
and every non-empty Schematic names a GUID an Assets/**/*.meta actually carries. Adapted from
Soul's own probe (AssetGuidsResolve); it is what caught Djinni's fabricated Prefab GUID and the
five restored thrusters' null ParticlesPrefab.
- EntranceZoneAlwaysGetsADockedStationAcrossSeeds: replaces
TutorialZoneGenerationCanProduceADockedStation, which never used the tutorial construction path
and only claimed a docking bay shows up somewhere in 25 zone-name variants. This builds the real
tutorial Galaxy with Assets/Resources/Settings.asset's own values and generates the Entrance zone
the way ActionGameManager.StartGame/PopulateLevel does, requiring a docked station in every run
that gets that far. Galaxy zone-graph generation has its own pre-existing, seed-dependent
flakiness (a disconnected zone graph throws inside the Galaxy constructor on roughly 1 seed in
15) confirmed unrelated to this cut by reproducing identically against the pre-fix-batch catalog;
that failure belongs to Galaxy.cs and is skipped rather than asserted on, with a floor requiring
at least 15 of 25 seeds to actually produce a Galaxy.
- LonginusNeverExceedsItsRestoredTopSpeedUnderFullThrust: straight-line full-thrust run against the
restored VelocityLimitData cap (100).
- LoadoutGeneratorFillsEveryThrusterHardpointAcrossSeeds: LoadoutGenerator, filtered to each
restored hull, fills every thruster hardpoint across ten seeds.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Operator, 2026-09-25 (docs/locomotion-cut.md): the tutorial's entrance zone always gets a
generated faction station with a docking bay, deterministically. Structurally, three defects made
that false on every seed: Galaxy.cs halves influence and picks the lowest-influence unowned zone
for Entrance, so stationCount rolled 0; a Lagrange orbit is only available when a planet has an
orbiting sibling, which a boring or empty system never has; and a null station loadout silently
skipped the slot.
GenerateZone now identifies the entrance zone by reference against the galaxy parameter it already
takes (isTutorial && galaxyZone == galaxy.Entrance), never by name:
- stationCount is floored to 1 for the entrance zone, after the roll that produced it.
- When no Lagrange orbit exists, a fallback orbit fixed at half the zone radius from centre is used
instead of leaving selectedStationOrbits empty. CreateLagrangeOrbit now carries FixedPosition
through so a Parent-less base orbit (the fallback) still positions correctly.
- A null station loadout throws for the entrance zone instead of continuing past it.
- The enemy count is based on the pre-override station count, so forcing the entrance's station
doesn't also hand it a bonus enemy.
The story-station path (Locations/LocationStory) is not used: it isn't saved, and
EntitySerializer.cs:77 would break Continue, per the map's own ruling.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Same pattern as restore-hulls itself: the one-shot command's carrying cost is
zero once it has run.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Landed via `dotnet run --project tools/AetherDb -- fix-loco1 apply`, then deleted the command in
the same batch (Cut 9/Cut 1 precedent). Fixes, each against the decoded 2021-04-14 legacy record
or a named catalog precedent:
- Djinni.Prefab was a fabricated/truncated GUID; corrected to Djinni.prefab.meta's own GUID.
- All five restored thrusters' ParticlesPrefab (null) -> Thruster 1.prefab's GUID, the same one
Large Drive uses (legacy record names the same prefab path for all of them).
- All five thrusters' invented OptimalTemperature/PlateauWidth (300/100) -> Large Drive's own
200-400 precedent, 251/24 (docs/stats-power-cut1-migration.md).
- ThermalResilience: Medium Drive/Small Drive -> 10, Victoire/Talaria/RevvITup 2.0 -> 15 (legacy
record). Victoire's Durability 100 -> 30 (legacy). Victoire and Medium Drive's legacy
descriptions restored (Talaria/RevvITup 2.0's legacy descriptions are the literal placeholder
"[TODO]" and are not).
- Djinni.Schematic (null) -> djinni_schematic_transparent.png's GUID, already in the tree, unused.
- Djinni's OptimalTemperature 250 -> 251, correcting a rounding slip in restore-hulls's own
"apply LonginusX's optimal-fraction to Djinni's band" derivation ((275.15-173.15)/400 = .255 of
band applied to Djinni's 200-unit band rounds to 251, not 250).
- All seven restored records (Longinus, Djinni, five thrusters) re-keyed from freshly minted GUIDs
to their own legacy GUIDs, decoded directly from GameData/Legacy/AetherDB.2021-04-14.msgpack,
matching the Cut 9 import precedent (tools/AetherDb/Import.cs, git history). Each record's own
FactionProductData.Design ref repointed at the new key.
Verified: dry run then apply, second apply refused (Longinus already carries its legacy key),
`dangling` reports 0, `census` still names both hulls, `loadout` generates every hull cleanly
across seeds 1-5.
Re-keying changed GetAll<GearData> enumeration order, which
RestoredHullMovesAndTurnsUnderMovementAndLookDirection's design-selection (thrusterDesigns.First())
depended on implicitly: it started landing every Djinni thruster hardpoint on Victoire, the one
restored thruster with a real EnergyUsage draw, and the fixture ship carries no reactor. Fixed by
equipping the fixture ship's own Reactor hardpoint, which every restored hull actually has, instead
of relying on incidental unpowered-item ordering.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Cut 1 restore-hulls command deletion also dropped a blank line separating
FitCommand from the dangling-refs command block. Restore it so the file is
byte-identical to base (git diff against pre-Cut-1 now empty).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Same contract Cut 10 applied to the Cut 9 importer: a one-shot restore
carries zero ongoing cost once its records are in the catalog. The
authored-choices review list this command printed lives in the cut's
report, not in source.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Read-only against GameData/Aetheria.cc: each hull exists with the
expected thruster hardpoint count (Longinus 4, Djinni 8), every thruster
hardpoint accepts a catalog thruster design through TryEquip at its own
position, and each hull moves and turns for a few ticks under
MovementDirection/LookDirection on the pre-Cut-3 mixer -- content proof,
not controller proof.
Also covers the operator's tutorial-station addition (2026-09-25):
Zenith (the live Station hull) is confirmed working already -- station-fit,
hardpoint-fit and `loadout` all pass for it, and the legacy record's only
station design ("Zenith Station") is that same hull, so nothing needed
restoring. ZoneGenerator's story-station path is dead code today (Galaxy's
tutorial constructor has its StoryProcessor call commented out), so
TutorialZoneGenerationCanProduceADockedStation drives the background
station path directly (real ZoneGenerator.GenerateZone call, real
catalog, a minimal SavedGame/Galaxy fixture) across 25 zone/seed
attempts and asserts at least one produces a station with a docking bay.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Landed via `dotnet run --project tools/AetherDb -- restore-hulls apply`.
Longinus (13 hardpoints, Alakrita) and Djinni (22 hardpoints, Rossum &
Douglas), plus Medium Drive, Small Drive, Victoire, Talaria and RevvITup
2.0 (approved 2026-09-22), each with a FactionProductData. Verified:
census names both hulls with makers, dangling reports nothing new,
LoadoutGenerator/`loadout` generates every hull in the catalog cleanly
across seeds 1-5, second apply refused.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
One-shot restore of Longinus, Djinni and their five thruster designs from
the decoded 2021-04-14 legacy record, in the shape of the existing
*-migrate [apply] commands: dry run by default, writes every record in one
Commit, refuses a second apply. Longinus's scalar fields and ReflectorData
come from LonginusX (confirmed byte-identical against the decoded legacy
record); its hardpoint layout is authored directly from the legacy decode,
since LonginusX's own interior was rearranged to fit its AetherDrive.
Djinni is authored entirely from the decode (no live twin exists).
Every legacy PerformanceStat's [Min, Max, HeatExp, DurabilityExp,
QualityExp] is translated into today's Min/Max/Terms shape, never copied.
Authored post-breach fields (roles, FactionProductData, PowerSupply terms,
FiringArc) are covered in the command's own header comment for operator
review. Deleted in a later commit of this same cut once landed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Q5 is dissolved rather than answered: the controller asks for the force and
the torque it wants together and the solver minimises both, so the lurch is
spent only when the translation was wanted anyway. The planner rolls
candidates forward and scores where the ship ends up, which is what makes
strafing beat the big circle. The legacy hulls' second behaviour is
VelocityLimit, not VelocityConversion, as the operator said.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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 <noreply@anthropic.com>
Inspect.PBase is HitProbability's own answer instead of a second product of
the factors. New behavioural tests pin a beam shot carrying its override
damage, a fitted targeting system forgiving a jink, a shot orphaned before
commit committing as a plain miss, and the HUD's arrival countdown. The
tautological damage assertion in BeamRollsPerInterval is deleted.
Stryker on FireControl.cs: 51 survivors to 43, all triaged in the map.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Most of Stryker's surviving mutants in FireControl.cs traced to a handful of
fixture constants that make a mutant compute exactly the same number as the
original -- the same defect class as the zero-velocity target and the damage
that happened to equal an item's durability, found here as a pattern.
- The kernel fixture's shooter sat at float3.zero, where `target - source` and
`target + source` are the same vector. HitProbabilityMatchesInspect gains a
configuration with the shooter far from the origin: the correct separation is
500, in range; the mutated sum is 1300, out of range.
- The kernel fixture's Resolution was 1000, which saturates pSensor to exactly 1
above the detection threshold, so multiplying and dividing by it agreed. Now 2.
- EvasionCountsUntilCommitAndNotAfter fired at zone time 0, so `now - FireTime`
equalled `now + FireTime`. Cut 5 gave this test a moving target to defend its
own declared mutation and it still survived; the clock now advances first.
- Every fixture used zero spread and a cell size of 1, leaving the whole spread
term untested. PSpreadIsTheSilhouettesShareOfTheSpreadCone pins what pSpread
means -- a silhouette filling the cone scores 1, half the cone scores .5, and
a bigger one saturates -- rather than how it is computed.
Triaged as equivalent, not chased: the float-threshold boundary flips, and the
three MixSeed mutants that turn a right shift left -- measured, the die stays
fair under all three (worst deviation from p .017-.030 against .022 correct), so
killing them would mean pinning the mixer's bit arithmetic, not its behaviour.
Recorded, not fixed: FireControl.Apply's derivation of hitDirection from the two
positions has no geometry test through the FireControl path, and
StatResolverTests.AModifierThatTargetsItsOwnMagnitudeStatIsRefusedAtEquip fails
intermittently in the full suite -- likely cross-class pollution through
CultCache's process-wide document registry.
241 tests pass.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Nine tests, each stating as behaviour a rule a real Stryker mutant survived:
- A shield that absorbs must eventually run dry, on both the discrete path and
Splash's (whose absorb branch had no coverage at all). Deleting Shield.TakeHit
survived because earlier tests asserted only that the hull was untouched.
- Splash damages the side the blast came from, across five facings and both
sides. Flipping the sign of `right` survived every suite, and it survives at
the default facing (0, 1) too: the flip only changes the damaged side when a
ship faces more along world x than z. So the test spins the target through
facings where it bites. The current code is correct; it can no longer regress.
- Different zones roll differently. Replacing `CombatSeed *` with `/` dropped the
zone from the seed and survived, because the die test checks one zone only.
- A shot whose target left the zone resolves as a miss.
That last one is a real defect (Soul C4), fixed here. Step rewrote the outcome
only for uncommitted shots, so a hit committed inside the horizon and then
orphaned by its target dying was republished as a hit -- a hit marker on a
corpse. The resolution is now a fresh miss; the committed outcome is left
untouched, since R4 makes it immutable.
The fixture puts the shooter off the origin on purpose: at float3.zero,
`target - source` equals `target + source`, which is how four position mutants
survived.
240 tests pass.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Twenty-three harnesses, 5,816 lines, across the fire control, stats and power,
shield reserve and condition ratio campaigns. Every one targeted code under
Assets/Scripts/ServerShared, which Stryker's pinned scope now covers, and every
one carried the same liabilities: literal-text anchors coupled to code shape, a
restore that could leak a mutant into the tree, a tree-clean verdict that
restored before it measured, and a red result that could not tell a killed
mutant from a compile failure. The behavioural tests they measured all stay.
Kept and recorded, not yet answered: the addressables harness and the three
shield panel harnesses. They target Unity Editor probes that Unity compiles
outside Aetheria.Shared, where Stryker cannot reach.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Pinned in dotnet-tools.json (dotnet-stryker 5.0.0) so any checkout reproduces
it with `dotnet tool restore`. Reports land in StrykerOutput/, now ignored.
Spike against FireControl.cs alone: 276 mutants in 7m18s, 208 killed, 67
survived, 4 uncovered, score 74.6%. It killed every mutation the hand-written
anchor harnesses defended, compiles all mutants into one assembly and switches
between them at runtime -- so it never edits a source file, has nothing to
re-anchor after a refactor, and cannot leak a mutant into the tree.
Among the survivors: Splash's handedness is untested (flipping the sign of
`right` passes every test -- the operator's "damage on the wrong side of the
ship"), deleting the discrete shield's TakeHit survives, and most of the rest
trace to degenerate fixture values -- shooter at the origin, fire time zero,
spread zero -- which make whole families of mutants arithmetically identical.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The diagnostics refactor (72c0109c) kept the debug HUD and the simulation on one
fire-control model by routing HitProbability THROUGH Inspect. That was the right
invariant implemented at the wrong level: every AI and turret evaluation, per
weapon per tick, built the full diagnostic struct and ran IsRevealed's sort, the
aim-point cell walk and HullKernel's allocations and per-cell exp -- and only
then checked whether the target was even in range. A Soul pass measured 9.87 us
and 448 B per call on paths that used to bail in nanoseconds. Operator: it made
the hot path look like a diagnostic one.
One model is now shared at the level of its factors. HitProbability runs its
cheap gates first and bails, exactly as it did before the refactor, and computes
PSensor, PSpread and POnHull only for a shot that is actually possible. Inspect
calls the same four factor functions and fills the struct unconditionally,
because a HUD wants to see why a shot is impossible. The HUD panel and its one
test consumer are unchanged.
Two tests make the design enforced rather than conventional:
- HitProbabilityMatchesInspect: exact equality across 21 configurations covering
every gate the hot path bails on plus Precision and info sweeps. Dies when a
factor changes in only one caller.
- GatedOutHitProbabilityAllocatesNothing: 1000 out-of-range calls allocate zero
bytes. Dies when HitProbability is routed back through Inspect.
Both mutations verified RED and distinguished from a compile failure, with
restores checked against a hash taken before mutating.
231 tests pass.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The combat roll was not a roll. FireControl seeded CultMath's xorshift32 per
shot and drew once; NextFloat returns the top 24 bits of the first round, and
one round cannot carry ShotId's low-bit variation into the high bits. Every
zone's hit roll spanned about an eighth with its threshold fixed at zone
creation, so a shot was a step function of p -- below the band it never hit,
above it always did. An unaided shooter hit 0 of 18,926 shots, which is the
operator's "I can target and fire with no hits registering," and being stable
per zone is why it came and went.
Measured across six zone seeds: hit fraction at p=.3 went from 0.0% in five of
six zones to 29-31%, at p=.6 to 58-61%, spanning the full range. The seed stays
a pure function of (zone, shot id); only its diffusion changes.
Three cuts shipped over this because nothing in the campaign measured a
distribution -- every fixture uses accuracy 1 or 0, and a constant satisfies a
determinism test perfectly. The missing test, not the seeding line, was the
defect.
Also: pOnHull collapsed to zero above Precision ~4 because no shipped hull's
centre of mass is integral, so a better targeting system made a weapon unable to
hit anything; sigma is now floored at half a cell. And the LockWeapon crash Cut
8 did not actually fix -- that code only runs on active entities -- is closed at
its real route, dock/target dies/undock.
229 tests. All nine harnesses clean, run sequentially in an isolated worktree.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
9.1 changed the line those two harnesses pin, stranding five anchors including
cut6's own no-op control -- so cut6 was defending nothing until now. Same thing
the diagnostics refactor did to four harnesses earlier, and the second time in a
day that a change to Commit has stranded anchors elsewhere. The rules are
unchanged; only the text moved.
That maintenance cost is now a known property of pinning mutations to literal
source text. It buys a stale anchor failing loudly within one run, which is how
both instances were caught immediately, and it belongs in Cut 10's list beside
Soul's harness audit.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
9.1, blocking. FireControl seeded CultMath's xorshift32 per shot and drew once.
NextFloat returns the top 24 bits of the FIRST round, and one round cannot carry
ShotId's low-bit variation into the high bits -- so every zone's hit roll spanned
about an eighth, with the threshold fixed at zone creation. A shot was a step
function of p: below the band it never hit, above it always did. An unaided
shooter hit 0 of 18,926 shots. That is the operator's "no hits registering," and
being stable per zone is why it came and went.
The seed derivation is unchanged -- still a pure function of (zone, shot id),
Cut 6b's rule -- and only its diffusion into the first output changes, via an
fmix32 finalizer. Measured across six zone seeds: hit fraction at p=.3 went from
0.0% in five of six zones to 29-31%, and at p=.6 to 58-61%, with the roll
spanning the full range.
The defect survived three cuts because nothing in the campaign measured a
distribution -- every fixture uses accuracy 1 or 0, and a constant satisfies a
determinism test perfectly. TheDieIsUniform now asserts the observed hit
fraction is within .08 of p across several zone seeds, which is the test whose
absence was the real defect.
9.2. Sigma is floored at half a cell. HullKernel approximates a continuous
Gaussian with a discrete sum over occupied cells, and that sum collapses once
sigma falls well under one cell against an aim point that is not exactly on a
cell centre -- true of every shipped hull. Past Precision ~4, pOnHull went to
zero and a better targeting system made the weapon unable to hit anything.
9.3. An active entity's Target is now always a live entity it has info on.
Cut 8's _active guard could never have fixed the operator's LockWeapon crash --
that code only runs on active entities, and cut8's own harness header says so.
The live route was dock, target dies, undock; Deactivate disposed the
subscription that nulls a stale Target and Activate reseeded EntityInfoGathered
without reconciling it. Still not a TryGetValue in LockWeapon.
229 tests pass.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A Soul pass measured the thing three cuts assumed. FireControl's per-shot seed
feeds CultMath's xorshift32, whose NextFloat returns the top 24 bits of the
FIRST round -- and one round cannot carry ShotId's low-bit variation into the
high bits. Every zone's hit roll spans 0.1249. A shot is a step function whose
threshold is fixed when the zone is created: below the band it never hits, above
it always does. An unaided shooter hit 0 of 18,926 shots.
That is the operator's "no hits registering," and being stable per zone is why
it came and went.
It is this map's own prescription, written by Self in Cut 6b, and the reason it
survived is that no test in the campaign measures a distribution -- every
fixture uses accuracy 1 or 0, and a constant satisfies a determinism test
perfectly.
Also takes pOnHull collapsing to zero above Precision ~4 (no shipped hull's
centre of mass is integral, so a tight sigma underflows every cell weight), and
the LockWeapon crash Cut 8 did not actually fix -- that code only runs on an
active entity, so the _active guard was never what would prevent it. Docking
still reaches it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Fixes the crash from the operator's play smoke. Cut 5.6 moved death removal into
the simulation and did half the transition -- Zone.Entities.Remove with no
Deactivate, where TryDock has always paired the two -- so a dead ship kept live
subscriptions and stale references while every survivor dropped it, and
Zone.Update's snapshot iteration handed it one more Update after the world had
forgotten it. Entity.Update never consulted _active; the flag existed and
nothing asked it. LockWeapon's raw indexer into EntityInfoGathered was simply
where it surfaced.
Death now removes and deactivates as one transition, and a deactivated entity
does not update. Not a TryGetValue in LockWeapon: that would silence one call
site and leave every other behaviour running on a corpse.
Also re-anchors seven mutations stranded by the diagnostics refactor, two of
which were no-op controls -- four harnesses were defending nothing until now.
Still open and deliberately not patched: the heat spike, the motionless ships
and the shots that registered no hits. Unreproducible since the diagnostics
changed timing, so the map records them with the unclamped dt as the place to
probe.
226 tests; all eight mutation harnesses clean with tree-clean PASS, controls
included, run in an isolated worktree. Unity batchmode NOT run for this cut --
the operator's editor holds the project lock.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The operator's play smoke crashed in LockWeapon reaching EntityInfoGathered
through a raw indexer as a ship vanished. Cut 5.6 moved death removal into the
simulation and did half the transition: Zone.Entities.Remove with no Deactivate,
where TryDock has always paired the two. The dead ship kept live subscriptions
and stale references while every survivor dropped it, and because Zone.Update
iterates Entities.ToArray() it received one more Update after the world had
forgotten it. Entity.Update never consulted _active; the flag existed and
nothing asked it.
Death now removes and deactivates in one transition, and a deactivated entity
does not update. Deliberately NOT a TryGetValue in LockWeapon: that silences one
call site and leaves every other behaviour running on a corpse.
Also re-anchors the seven mutations stranded by the diagnostics refactor
(72c0109c), which routed HitProbability through Inspect and rewrote the lines
four harnesses pin -- two of them no-op controls, so those harnesses were
defending nothing. The rules are unchanged; only the text moved. Anchors stay
strict: loudly stale is what surfaced this within one run.
The heat spike, motionless ships and shots that registered no hits remain
UNEXPLAINED and open. See the map's 8.3 -- unreproducible since the diagnostics
changed timing, so recorded with the unclamped dt as the place to probe rather
than patched speculatively.
226 tests pass.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The operator's play smoke crashed in LockWeapon reaching EntityInfoGathered as a
ship vanished. Cause: Cut 5.6 added death removal from the zone but not the
matching Deactivate, so a dead entity kept living subscriptions and stale
references while every survivor dropped it -- and because Zone.Update iterates a
snapshot, it still got one more Update after the world had forgotten it. Nothing
in Entity.Update consults _active.
TryDock has always paired removal with Deactivate; the death path just did half
of it. The fix is that pairing plus an _active guard, not a TryGetValue in
LockWeapon, which would silence one call site and leave every other behaviour
running on a corpse.
Also records the symptoms this does NOT explain -- the heat spike, motionless
ships and shots landing nothing -- as open, with the unclamped dt and the
unguarded division by it as the place to probe rather than patch.
And re-anchors the seven mutations stranded by the diagnostics refactor, which
rewrote the lines four harnesses pin. Two of the seven are no-op controls, so
those harnesses currently defend nothing.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Precision was a coin flip between landing exactly on the aimed item and landing
uniformly anywhere on the hull, so aiming at a stern thruster scattered its
misses across the bow -- no nearer the thing aimed at than never aiming at all.
One Gaussian kernel over the hull schematic now answers both halves: its
per-cell weights place the hit, and its on-hull mass prices the stray into
HitProbability, so a thin limb costs visible hit chance before the trigger and
centre mass is the safe play. One function, so the placement and the probability
cannot drift apart -- Cut 3's named risk was exactly two functions answering
where a shot goes.
There is still only one roll. A shot that passes it always lands on metal,
because the chance of the dart straying off the hull was charged at the roll.
Operator-facing consequences, measured rather than assumed:
- pOnHull and the existing pSpread term do NOT double-charge. pSpread prices the
weapon's own dispersion cone against the silhouette and is 1 for any zero-
spread weapon; pOnHull prices the targeting system's aim scatter against real
schematic metal around the actual aim point. Both keep their jobs.
- AI willingness shifts only where it should. Unaided fire is unaffected --
UnaidedAccuracy already caps it below AgentMinHitProbability. Aided fire at
centre mass is barely touched. Aimed shots at thin subsystems can now fall
below the threshold, so NPCs get pickier about sniping small targets. That is
the tradeoff working, and AgentMinHitProbability is the operator's knob.
Also fixes two pre-existing defects the kernel exposed. FireAuthorityTests.
ShieldTakesHit fired damage 10 at a fixture whose equipped gear had durability
10, so the item absorbed the hit whole and the hull never moved -- the shield
branch could have been deleted outright and the test would have passed. And the
Cut 3 harness still named a test Cut 7 had renamed.
222 tests, all seven mutation harnesses clean with tree-clean PASS, catalog
reads 53 designs / 47 products, 0 dangling refs, Unity batchmode exit 0 with 0
error CS.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
ShieldTakesHit: damage raised from 10 to 50, past the Reactor/Shield gear's own Durability of 10. Under the dart-throw kernel, Build's default Precision groups unaimed shots tightly enough around this small hull's centre of mass that this fixture's deterministic single shot lands on one of the two equipped Tool items. At damage 10 that happened to exactly consume the item's own durability with zero remainder, so hull durability stayed unchanged with or without the shield-absorb branch -- the mutation this test is supposed to kill was surviving by coincidence, not because the rule held.
mutation_tests_fire_control_cut3.sh: its closing section still referenced FireAuthorityTests.OutOfArcConsumesNoDraw, a test Cut 7 renamed to CombatNeverDrawsFromTheSharedStream before this branch started (verified against the merge-base commit). Replaced the stale not-yet-reached note with a real mutation against the current test: Commit reads shot.Source.ItemManager.Random once, restoring the shared-stream touch Cut 6b removed.
Modelled on tests/mutation_tests_fire_control_cut5.sh: a no-op control, byte-exact I/O, sha256-verified restore after every mutation, and an explicit tree-clean verdict at exit. Runs the four mutations named in the spec's Verification block plus the fifth this cut's own test file declares for EveryHitLandsOnMetal. Verified clean: all five checks behaved as declared, tree-clean PASS.
Adds AimingAtTheSternHitsTheStern, ThinLimbCostsHitChance, PlacementAndProbabilityShareOneKernel and EveryHitLandsOnMetal per the spec's Verification block. Each test builds its own synthetic hull/catalog fixture. The aim marker in every fixture is authored with very high durability so it survives the whole sample of shots without dropping out of the reveal ranking partway through.
FireControl.Commit used to place a hit with a coin flip between exactly
the aimed item and a uniform-random hull cell -- no spatial term, so a
near-miss on a stern item was as likely to land in the bow as the stern.
One Gaussian kernel, in hull-schematic cell units, now decides both
halves of "where will this shot go": w(cell) = exp(-d^2/2*sigma^2) over
every occupied cell, sigma = 1/Precision. HitProbability folds the
kernel's on-hull mass share (pOnHull) into PBase at fire time (R10/Q6
unaffected); Commit draws the landing cell from the same weights. A shot
that passes the roll always lands on metal -- the off-hull chance is
priced into that one roll, never resolved as a second miss stage. The
uniform-random branch is deleted outright, not demoted.
Precision now feeds every shot's pOnHull, aimed or not, so its unaided
fallback (FireControl.Precision) reads GameplaySettings.UnaidedPrecision
instead of a bare 0 -- the same shape as the existing UnaidedAccuracy/
UnaidedTracking floors. pOnHull is saturated: the discrete sum over-
estimates the continuous kernel's integral once sigma drops below about
one cell, and a mass share cannot exceed the whole kernel.
Existing fixtures across FireAuthorityTests, FireControlCut4/5/6Tests and
TargetingSystemTests authored Precision as 0 (or relied on the old 0
fallback) purely as a don't-care value under the coin-flip rule, where it
only gated the aimed-item branch. Under the kernel it drives every shot's
sigma, so those fixtures are re-authored to keep pOnHull near its
ceiling and stay pinned on what they actually test.
mutation_tests_fire_control_cut3.sh's two anchors into HitProbability's
return line and the deleted coin-flip branch are updated to the
equivalent lines/mutations on the new code.
Cuts 1-3 declared mutation tables in this document and nobody ever wrote them,
which is why a Soul pass could write five mutations against rules the campaign
claims to pin and watch all five survive a 197-green suite. Those tables now
exist as committed harnesses -- six in the repo where there was one this
morning, each with a no-op control and a sha256-verified tree-clean verdict.
Three findings came out of writing them, all the same shape: a test whose
fixture could not tell the mutant from the original.
- ShotResolvesOnArrival stepped the zone from t=4 to t=6 in one tick, crossing
both the commit horizon and arrival, so it could not tell an outcome applied
on arrival from one applied at commit -- R4's central distinction, undefended.
- OutOfArcConsumesNoDraw could not be killed at all, because Cut 6b had made it
vacuous: the shared RNG stream it guarded no longer exists, so it passed
identically with the target in arc. Rewritten to pin the rule that is live.
- 7.1 pins the reveal re-check at the shot's freeze, the fifth mutation Soul
left standing.
And 7.4: the shipped catalog is now opened by a test that enumerates every
document type. Verified by reintroducing the exact defect that broke the 6b/6c
merge -- it fails with the same MessagePack nil error, so the one hole that let
a branch brick the catalog while passing every gate is closed.
218 tests, all six harnesses clean, Unity batchmode exit 0 with 0 error CS.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Cut 3's harness reported OutOfArcConsumesNoDraw as "not yet reached" -- no
mutation could turn it red. Chasing that rather than accepting it found the
reason, and it is not a fixture defect.
The test asserted that a shot gated to zero draws nothing from ItemManager.
Random, so the shared stream advanced exactly as if the shot had never queued.
Cut 6b moved every roll onto a per-shot local generator seeded from (zone, shot
id), so Commit stopped touching that stream on EVERY path. The test then passed
for a reason unrelated to arcs -- it passed identically with the target dead
ahead -- and the rule it named stopped existing along with the stream it was
about. The short-circuit it guarded is now a cheap guard, not an invariant, and
Commit's comment was still asserting otherwise.
Rewritten as CombatNeverDrawsFromTheSharedStream, a Theory over both an
out-of-arc and an in-arc target, pinning the rule that IS live after 6b: no
combat path draws from the shared stream at all. Verified by hand -- restoring
the shared-stream read in Commit turns the in-arc case red while the out-of-arc
case stays green, which is exactly why the old test could not have caught it.
218 tests pass. All six mutation harnesses run clean with tree-clean PASS.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
ShotResolvesOnArrival stepped the zone from t=4 straight to t=6, crossing both
the commit horizon (4.5s) and arrival (5s) inside one Zone.Update. A mutation
collapsing Step's arrival gate to `if (shot.Committed)` therefore survived it:
one wide tick crosses both thresholds either way, so the test could not tell
"applied on arrival" from "applied as soon as committed". R4's central
distinction -- the commit decides the outcome, arrival performs it -- was
declared in Cut 3's prose, pinned by nothing, and this is the third rule in this
campaign found undefended by a fixture too coarse to distinguish the mutant.
Adds a tick at t=4.55, past commit and short of arrival, where the durability
must still be untouched.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
tests/mutation_tests_fire_control_cut1.sh and cut2.sh, modelled on
cut5.sh: byte-exact I/O, sha256-verified restore per mutation, explicit
tree-clean verdict. Each runs the mutation table its own cut already
declares in docs/fire-control-cut.md.
Two adaptations forced by the Body having moved since those cuts were
written (both cut2.sh's own table entries, and its header comment):
Cut 1's CombatStateStepsHeadless mutation targets FireControl.MountDirection
instead of the now-deleted HardpointTransforms readback, since Cut 3 moved
the AI's aim-direction path there; Cut 2's EveryArmedLoadoutGetsATargetingSystem
mutation targets the surviving acquisition predicate instead of a
`required: true` flag Cut 6c.2 already turned into a deliberate
degrade-and-log path.
TargetingSystemTests.cs's RevealOrder fixture equipped its three items in
the same order FireControl.IsRevealed's ranking would put them in, so
dropping that ranking entirely was unobservable -- Entity.Equipment's raw
enumeration order already matched by coincidence. Reordered the fixture's
equip calls to the opposite order (size-ascending, hardpoint-last) so the
mutation is actually distinguishable; assertions are unchanged.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
FireControlCut7Tests.cs adds the fifth mutation Soul found survived 197
green tests -- FireControl.Fire freezing source.TargetItem.Value instead
of the reveal-re-checked ResolvedTargetItem (7.1) -- and the campaign's
first assertion that the shipped GameData/Aetheria.cc catalog is actually
readable by the shipped code and can generate an armed LonginusX loadout
(7.3/7.4). The live-catalog test builds its own scoped CultDocumentRegistry
rather than AetheriaStores.Open's default shared one, because the shared
registry auto-discovers every [CultDocument] type loaded anywhere in the
test process -- including AetheriaStoresTests.cs's own TestCatalogGlobal,
a fixture type the real catalog has never heard of and was never meant to
satisfy.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Merging 6b and 6c bricked the catalog -- a new non-nullable float at a new
MessagePack key cannot read records written before it existed, so
AetheriaStores.Open threw and the game could not load its own data. Both
branches were green: one never opened the real catalog, the other never had the
field.
Cut 7's live-catalog test therefore has to enumerate every document type rather
than just look for a design, because a lazy read that never touches
WeaponItemData would have passed the broken merge too.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Cut 6b added WeaponItemData.AirburstRange as a non-nullable float at Key(29).
Every WeaponItemData record in GameData/Aetheria.cc was serialized before that
key existed, so the reader is handed nil for the absent slot and a float cannot
take nil -- AetheriaStores.Open threw MessagePackSerializationException and the
game could not read its own catalog. AirburstRange becomes float?, null meaning
no authored burst radius, which is also the more honest reading of a value no
product sets yet.
Neither cut could have caught this alone. 6b verified with unit tests over
synthetic fixtures and never opened the shipped catalog; 6c opened the catalog
constantly but on a branch without the new field. It appears only in the merge,
and only when something actually reads GameData.
The general rule, which will recur: a new non-nullable value-type field on a
persisted CultCache document cannot read records written before it existed.
Nullable, or a migration that rewrites every affected record -- and a migration
cannot run if the read already throws, so tolerant deserialization comes first.
Verified on the merged tree: catalog reads (53 designs, 47 products), 0 dangling
refs, 215 tests pass, and loadout with all three targeting sellers excluded
generates every hull with 1 armed hull falling back to unaided fire.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>