F8 (Soul's earlier fix batch, 2026-09-24) moved range computation ahead of the
visibility gate in FireControl.PFire so Fire's own flight time stays real even
when the shot prices at PFire 0, but no test observed that behaviour --
N4 (Soul's own mutation set, reverting F8) survived every existing test.
InvisibleTargetStillResolvesAtItsRealFlightTime fires at a target removed from
VisibleEntities and checks the shot does not resolve before its real 5s
arrival (N4's bug would resolve it instantly, at range 0) and resolves as a
miss once it does.
Manually verified against the exact N4 patch: it now fails (arrival at ~1s
instead of ~6s, i.e. range treated as 0).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
TwoProngHullSplitsTheScatterAcrossBothIntervals's own within-prong check
(atNearColumn > atFarColumn) held under M12/M12b (an entire second silhouette
built at the wrong sigma) and the sigma/sqrt(2) Stryker mutants, because every
wrong sigma tried still puts more mass on the nearer column -- direction alone
cannot separate a sigma of 1.5 from 1.5*sqrt(2). Their analytic near/far mass
ratios are well apart (~2.9 true vs ~1.73 at sigma*sqrt(2)), so the test now
computes the true ratio from the same per-cell Gaussian integral it already
uses and asserts the empirical ratio (8000 shots) tracks it within a tolerance
that rejects the wrong-sigma ratio.
Manually verified against the exact M12 and M12b patches from Soul's mutation
script: both now fail this test (empirical ratios 581 and 1.68, matching a
collapsed and a sqrt(2)-shifted sigma respectively, against the analytic ~2.9).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Replaces LateralDrawIsHalfOpenAtTheShadowsTopEdge (reflection into the private
LateralDraw) with:
- ShotAtBearingLoTouchingOnlyACellCornerHitsMetal and
ShotAtBearingLoOnTurningArmourHitsMetal, Soul's two named-seed crash repros
(Cut12-edge-68442709 and Cut12-edge-64078070 -- StableHash(zone name) seeds
Zone.CombatSeed, so the exact name reproduces the exact die roll) run
through the real public Fire/Zone.Update path, asserting the shot commits a
hit whose cell's own projected lateral interval contains the drawn Lateral,
instead of throwing.
- LaneNeverEmptiesAtAnIntervalEdge, the general sweep: every interval's Lo and
the point just below its own half-open Hi, across several bearings,
precisions and hulls (including a non-convex, holed one), must never return
an empty lane from FireControl.Lane.
Build gained a zoneName parameter (Cut 6.1's one-fixed-name convention doesn't
serve a repro that needs an exact CombatSeed) and a tracking parameter and an
equipLMarker fixture, both used by later findings in this batch.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
S1 (HIGH, crash in play): Lane's own shadow prefilter (a re-derivation of
Silhouette's centre/h) and the independent SlabAlongB 2-axis intersection test
could disagree at a float edge, rejecting a cell the shadow sum had already
counted metal for -- an empty lane after a hit passed its roll (R3 violation),
reproducible through Zone.Update. Both are replaced by one shared admission
test (Extent(c, axis), the exact support of a unit square along an axis) that
Silhouette and Lane now both call; SlabAlongB is deleted, and its geometry is
kept only as a non-rejecting AlongBearing() that computes entry/exit for
already-admitted cells (the doc's own equivalence proof -- "s in the shadow" is
exactly "the line intersects" -- makes rejection there unreachable by
construction, not merely unlikely).
S3 (allocation): Interval and LaneCell now implement IComparable<T> (same
order as the deleted IntervalByLoComparer/LaneCellComparer) so Silhouette's and
Lane's sorts use Array.Sort(T[], int, int), which resolves T's own comparison
at JIT time, instead of the (T[], int, int, IComparer<T>) overload, which
wraps even a cached singleton comparer in a fresh delegate per call.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
F1 (correctness): LateralDraw clamped the drawn lateral to the closed
[Lo, Hi], not the spec's half-open [Lo, Hi). Soul enumerated all 2^24
NextFloat outputs against TurningArmourIntoTheShotTakesItOnTheArmour's own
geometry and found 3 that draw exactly Hi, which Lane's own half-open filter
then admits no cell for -- a hit that passed its roll landing nowhere (R3).
Fixed the clamp; Lane's silent int2.zero fallback on an empty lane is now a
thrown InvalidOperationException, since an empty lane is provably impossible
after the fix. Committed Soul's edge probe as
LateralDrawIsHalfOpenAtTheShadowsTopEdge.
F6/F8: HitProbability, Inspect and Fire each assembled bearing -> Silhouette
-> PSpread (or recomputed range) in their own copy; a shooter-frame bearing
bug in HitProbability's copy survived while the identical bug in Inspect's
copy was caught. Collapsed into one Forecast function and one PFire(out range)
overload; both callers, and Fire, read the one computation.
F2: FireControlCut12Tests' zone names carried a Guid, breaking every other
cut's fixed-seed convention (FireControlCut6Tests.cs:62-63) and reseeding the
die every run -- the actual source of the flake in
TurningArmourIntoTheShotTakesItOnTheArmour. Fixed to a fixed zone name.
F3/F4/F5/F7: added a two-prong hull fixture (LateralDraw's walk past the first
interval, and a wrong-sigma placement, were unreachable when every fixture
projects to one interval), a real-roll empirical test for CommitProbability
(the old one called the pure function twice and compared it to itself),
enough Penetration in TurningAfterCommitChangesNothing for ApplyHit to
actually read the bearing, and a 2x2 item fixture for ResolveAimPoint's
average (every "Marker" elsewhere is 1x1, so sum/length and sum*length agree).
F9: Lane's own comment claimed no allocation while its sort used
Comparer<T>.Create over a closure; LaneCell now carries its own precomputed
projection and Lane sorts with a stateless singleton comparer. Corrected an
overclaiming comment in FireAuthorityTests (SigmaFloor makes the aimed-lane
draw ~68% per shot, not deterministic; the fixture's fixed zone name is what
actually makes it reproduce).
Cost: Silhouette takes an optional caller-rented Interval[] buffer; the
forecast path (HitProbability/Inspect, via Forecast) rents from
ArrayPool<Interval>.Shared and returns it before returning, since neither
reads sil.Intervals after the call. Commit/CommitProbability still get an
owned array, since LateralDraw needs it alive past the call. Measured on
LonginusX (66 cells, Release): unaimed 672 B -> 184 B/call (1512-1637ns before,
1482-1560ns after); aimed 1160 B -> 672 B/call (2123-2140ns before,
2094-2162ns after) -- the remaining ~488B/call gap between aimed and unaimed
is ResolveAimPoint's own CellsOf allocation (a List<int2>+ToArray per call),
not in this batch's scope.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The first Stryker pass (375 mutants, 69 survivors on this cut's own changed
lines) found one real split-authority bug and several untested edges:
- HitProbability recomputed target.Position - source.Position a second time
for PSpread's range, a second place the same subtraction could flip sign.
PFire now returns its own gate-computed range by out parameter; HitProbability
reads that one value instead of a second copy.
- TravelDirection's point-blank fallback and Fire's null-target guard around it
were only ever exercised at shooter/target pairs where the real direction
happened to equal the (0,1) fallback by coincidence.
- diagnostic.PFire (presentation) was only ever checked at PSensor == 1, where
Accuracy * PSensor and Accuracy / PSensor agree.
- CommitProbability's four-factor product was only ever checked with
pDeviation == 1 (no fixture moved a target between fire and commit), where
* and / by 1 agree.
- ResolveAimPoint's zero-cells fallback (an aimed item that no longer occupies
any cell) was never reached; every fixture's marker survives to be hit.
New tests: TravelDirectionMatchesThePredictedInterceptAndFallsBackAtPointBlank,
DiagnosticPFireIsAccuracyTimesPSensor, CommitProbabilityIsTheExactProductOfItsFourFactors,
AimPointFallsBackWhenTheAimedItemNoLongerOccupiesAnyCell.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
FireControl.Silhouette replaces HullKernel/WeightedPick/POnHull: the hull's
lateral shadow at a bearing, merged into intervals, integrated exactly with
CultMath.erf/erfinv instead of a discrete per-cell Gaussian sum. FireControl.Lane
does exact slab traversal to pick the impact cell from a lateral offset; only
its first element (the entry-nearest occupied cell) is used here, per the cut
map -- the rest is 12.3's armour-first absorption march.
Bearing timing: PendingShot.PBase is renamed PFire and now carries only the
shooter-side factor (Accuracy x PSensor) behind the gates; TravelDirection,
Spread and FireRange are frozen alongside it. The target's facing is read live
at Commit through FireControl.CommitProbability (steps 1-5 of the spec's
"Bearing timing"), not frozen at Fire, so a target that turns its armour into
an incoming shot takes the hit there. ShotOutcome gains Bearing/Lateral,
frozen at commit; Apply reads only those, never a live position or facing.
Entity.ApplyHit takes an already-schematic bearing instead of a world
direction, and the ToSchematic call 12.1 put inside it is gone.
Superseded tests: PlacementAndProbabilityShareOneKernel, EveryHitLandsOnMetal
and AimingAtTheSternHitsTheStern are replaced by
FireControlCut12Tests.PlacementAndProbabilityShareOneSilhouette,
HitsLandOnTheFacingEdge and AimingAtAnItemCentresTheScatterOnItsLane.
FireAuthorityTests.AimedHitLandsOnSelectedItem and PenetrationMarchIsPlanar are
updated for ApplyHit's new bearing parameter and Lane's entry-nearest-cell
placement (a solid hull's Tool item is never itself the entry cell; reaching
it now needs the same penetration march the new tests exercise).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The headless build pinned one CultLib revision for every package. CultMath
0.2.4 needs a newer revision than CultCache's, and bumping the single pin
would have compiled six commits of newer CultCache under the headless tests
while Unity stays on caching-unity-v1.4.0. CultMathRoot/CultMathRevision now
pin CultMath at the 0.2.4 tag; CultLibRoot/CultLibRevision keep CultCache at
45c2f40. Verified: 244 pass with both roots right, and each wrong root is
refused by name.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Lock hash is the tagged merge 6d5e2096; batchmode resolve and compile clean.
Nothing calls erf yet; Cut 12.2 will.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Entity.ToSchematic(float2) is the one schematic-frame transform;
Entity.ApplyHit's penetration march and FireControl.Splash's
directional half both call it instead of each carrying an inline
forward/right copy. ShotOutcome.Aimed and its plumbing (Q12-4 = A) are
deleted: the aimed local/flag in Commit, MakeOutcome's aimed
parameter, and the AimedCells half of ResolveAimPoint's tuple, which
now returns float2 directly.
No behaviour change. All 244 tests pass unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Records what Soul found (the wrong-signed erfinv edge that would have put hits
on the opposite side of a hull), corrects three stale verification lines, and
sets a cost measurement 12.2 owes rather than assumes.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The blast radius, not penetration, picks the model: no radius means the lane
walk with cell-by-cell absorption; a radius means the shot reaches fuse depth
and detonates as an area. AP round snipes a component, explosive penetrator
guts a section.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Operator: distribute the damage over the affected area. A blast covers the
cells within its radius and each takes the share of the disc inside it;
damage over open grid is lost, so an airburst wastes most of itself and an
internal detonation delivers nearly all of it. A shot has a direction and
keeps the lane model; a blast does not. Q12-6 ruled out, Q12-7 dissolved,
Q12-8 ruled.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Records the operator rulings of 2026-09-22 (Q12-1..4, penetration retune as a
follow-up, WeaponModifiers as labels) and adds sub-cut 12.4, which replaces
Splash with one detonation owner.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The Splash sign flip Cut 11 pinned was an undefended mutant, not a shipped
bug: the line is unchanged since 482c47bf. The operator's wrong-side bug
was legacy Unity code.
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>
Cut 6a authored the premium targeting system strictly worse than the cheap one,
because Resolution is consumed as a cost while its name promises a benefit.
Operator ruling: fix the formula, not the handle. HitProbability now derives its
ceiling reciprocally, so higher Resolution is a sharper instrument and the
unaided fallback of 1 becomes the floor by construction rather than by accident.
No rename.
Both designs also shipped from a single seller, and galaxy generation draws a
random subset of factions -- so ship generation still threw in any galaxy that
missed that draw, and Cut 6a's ten green seeds only sampled the die. Targeting
now comes from the roster's own sensor factions, Finch and Lucent, alongside
NiteLife; and FillInterior degrades to unaided fire instead of throwing when no
seller is present, which is only safe because Cut 5.1 gave unaided fire a floor.
Also records that the existing loadout command runs with a null galaxy, where
IsAvailable passes everything -- so availability had never been exercised by any
verification in this campaign until the exclude-sellers mode this cut adds.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>