Self's review found that commit 4e6495dd's fix for F1 replaced the per-lane
forward-arithmetic defect with a second, still-independent membership
computation: Lane's own half-open admission (Lo <= s < Hi, from a cell's own
Extent) versus Lanes' ceil((cell.Lo - centreLateral) / laneSpacing) lane
index. The two agree only when a cell's own (Hi - Lo) equals the externally
supplied laneSpacing exactly in float -- the identical two-derivations trap
Cut 12.2's own fix batch already found and removed from Lane itself,
reintroduced one layer up. Confirmed reachable: on commit 4e6495dd, solid5
b=(0,1) s=0.49999997 n=1 leaves the ceil-based centre lane EMPTY while Lane(s)
itself finds all 5 cells of the row -- breaking R3 ("the centre lane always
meets metal", Commit's own guard) and the 12.2 placement contract.
FireControl.Lanes is now built entirely out of FireControl.Lane: it calls
Lane(hull, b, centreLateral + k * laneSpacing, ...) once per lane index, so
Lane(s) is literally this function's n = 0 case rather than a parallel
re-implementation of its rule. Commit's placement and Apply's damage split
now read one function. The narrower question left -- can two individually
correct Lane() calls, at two different lines, still walk the same cell? --
is resolved by ownership (the lane closest to the centre keeps a contested
cell; the centre lane, k = 0, never loses one), not by a competing formula,
so the centre lane's final cell set is always exactly Lane(centreLateral)'s
own output.
Lane() itself is unchanged (still called directly by Commit and by
FireControlCut12Tests' edge-seed and interval-edge tests, all still green:
HitsLandOnTheFacingEdge, LaneSeamIsHalfOpen, LaneNeverEmptiesAtAnIntervalEdge,
Cut12-edge-68442709, Cut12-edge-64078070).
Adds LanesQueryIsDisjointMatchesLaneAtCentreAndNeverEmptyOnHull: now that
Lanes is a public, pure geometric query, the boundary sweep is a behavioural
test of its own contract, no reflection -- over every cell's own shadow edge
and float neighbours, at exact-axis, 45-degree, |bx| > |by| bearings and
Soul's own two reproduction cases, spread 1-2: (a) no cell is walked by two
lanes, (b) the centre lane equals a plain Lane(s) call, (c) the centre lane
is never empty when the centre lateral is inside the hull's own shadow. Hand-
confirmed to fail on 4e6495dd's ceil-based Lanes (property (b), the same
input above) and to pass on this commit (fc123fix2-mut3.log).
Adds CommittedCellIsTheFirstCellTheCentreLaneDamages: an end-to-end check
through Fire -> Commit -> Apply (damageSpread 0, a single lane) that the
committed outcome.Cell is the first cell Apply damages.
Full suite: 286 tests, identical across 3 runs.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
F3 (medium, confirmed): NoCellIsStruckByTwoLanesAtAnAngledBearing used
penetration 0 (impact cells only) and one fixed-seed shot per bearing, at
only two bearings, so it survived R1e (one-cell spacing when |bx| > |by|),
R1h (spacing 1/max(|bx|,|by|)), R1c (spacing * 0.9), and R1b (spacing *
0.99). Its own comment also falsely claimed a plain revert "fails this at
both bearings" without ever checking a real revert.
Reworked as NoCellIsStruckByTwoLanesInOneShotAtAnyBearing: seven bearings
(exact axis, both 45-degree diagonals, |bx| > |by| and |bx| < |by| on both
sides, all within the fixture's own FiringArc), 300 hits sampled per bearing
at a wide targeting spread (so the committed Lateral's draw visits many
points along the hull's shadow), and ample penetration.
Hand-verified against fc123fix2-mut.log: reverting to one-cell spacing (R0)
and R1c/R1e/R1h are all caught by the full suite -- but not by this test.
F1's structural fix (this branch's earlier commit, FireControl.Lanes) assigns
each cell to exactly one lane index by a single computation, so a wrong
laneSpacing can misassign or drop a cell but can no longer make two lanes
claim the same one; R0/R1c/R1e/R1h are instead caught by the pre-existing
damage-shape tests (DirectHitAtAnAngledBearingMatchesAnIndependentModel,
FootprintWidensAtAnAngledBearingComparedToAxisAligned, and others). R1b (a
1% narrowing) survives the whole 97-test FireControl suite under any of
those tests, including this one, even at 5000 hits per bearing -- recorded
here as an open survivor for Soul's own mutation pass, not claimed dead.
This test's own job is now a standing regression guard on the disjointness
invariant through the real Fire/Commit/Apply pipeline, not a killer of
spacing-magnitude mutants (that job now belongs to the damage-shape tests).
F4: deletes MirrorImageShotsProduceStatisticallyMirroredHullDamage. Soul
showed it cannot fail on spacing -- it survives the full revert and every
spacing mutant, because no per-shot mirror pair is reachable through Fire
(LateralDraw's distribution reverses under mirroring, so a shot and its
"mirror" are independently drawn samples, not a matched pair). It was meant
to pin lane processing order under a shared-cell race; that question is
folded into the F2 disjointness rule (a shared cell can no longer arise) and
the pending F2 ruling on multi-cell item lane order, which remains on hold
per this fix batch's brief.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
MirrorImageShotsProduceStatisticallyMirroredDamage compared per-cell armour
damage SUMS between a bearing and its mirror, using heavy (1000) uniform
armour. That armour never depleted, so no cell's absorption ever competed
with another lane's, and the test could not see the one thing the operator's
ruling is about: two lanes racing for one cell's finite armour under the old
one-cell spacing. Renamed to MirrorImageShotsProduceStatisticallyMirroredHullDamage,
it now resets a small, finite armour value before every individual shot (so
the race is live on every trial) and compares aggregate HULL damage, where a
lost race's leftover actually lands.
Finding, reported rather than hidden: even with this fix, the test does not
fail under the reverted one-cell spacing. Reasoning checked against measured
behaviour: FireControl.Apply always processes lanes in a fixed index order
(li ascending, i.e. lateral increasing), never based on the bearing's sign.
Over many INDEPENDENTLY random lateral draws, a bearing and its mirror sample
statistically equivalent distributions of "which side's lane goes first" --
the fixed-index tie-break rule respects the mirror symmetry in aggregate, so
independently-drawn trials wash out the order-dependent bias that Soul's own
LaneOrderDemo probe found (that probe compares ascending vs descending
processing of the SAME committed shot, not independently-drawn mirrored
bearings). A production-level test that reliably fails under the old spacing
for this specific property would need to force an exact, deterministic
lateral for two geometrically mirrored shots, which is not reachable through
Fire/Commit without reflection (Commit's lateral draw is randomised, and
Apply is private). NoCellIsStruckByTwoLanesAtAnAngledBearing and
FootprintWidensAtAnAngledBearingComparedToAxisAligned remain the two tests
that actually discriminate the ruling behaviourally (confirmed failing under
a hand-reverted one-cell spacing). The mirror test stays in the suite as a
true, currently-passing invariant and useful regression coverage, not as a
defense of this specific ruling.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
F1: no existing test through Fire commits at a non-axis bearing.
DirectHitAtAnAngledBearingMatchesAnIndependentModel checks every cell's
armour, every item's durability and the hull's own total against an
independent double-precision ray/box model (ported from the Soul probe that
verified this cut's arithmetic to 14,984/15,000 shots), at 45 degrees, a
shallow angle and a bearing with |bx| > |by|.
DiagonalDirectHitReachesEveryCornerCellInOrderThroughFireCommitApply pins the
spec's own diagonal case (Probe1's corner-crossing lane) through the full
Fire -> Commit -> Apply path rather than a direct Lane call: the lane reaches
(0,0), then (1,1), then (2,2), in that order (a bearing this close to 45
degrees also admits the diagonal's row/column neighbours, so the walked lane
is usually five cells, not three -- the spec only requires the three corner
cells present and in order, the same standard the existing direct-Lane test
already pins).
F2 (operator ruling, 2026-09-25: spread lanes should diffuse sideways rather
than double up): three behavioural tests, deliberately not asserting the
spacing value itself (operator: "test behavior, not shape").
NoCellIsStruckByTwoLanesAtAnAngledBearing fires at 45 degrees and at a bearing
with |bx| > |by|, spread 2, heavy uniform armour, and checks no cell fires
ArmorDamage twice. MirrorImageShotsProduceStatisticallyMirroredDamage compares
300 hits at a bearing against 300 at its mirror image, and asserts the
per-cell damage totals are mirrored within tolerance.
FootprintWidensAtAnAngledBearingComparedToAxisAligned compares the lateral
span of hit cells for the same spread at 45 degrees against axis-aligned, and
asserts the angled footprint is wider.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
F4: AnAPRoundStopsAtTheFirstGapAndLeavesTheFarSideOfTheNotchUntouched, a notched
U-shaped hull, pins Lane's own contiguity break (an AP round with ample
penetration must stop at the first gap, not reach the far side).
F5: EverySideLanesRemainderReachesTheHullNotOnlyTheCentreLanes, an
axis-aligned spread-1 fixture (lanes share no cells, so order cannot matter),
pins that every lane's own leftover reaches the hull, not only the centre
lane's.
F6: AbsorbSpendsArmourBeforeTheItemOnTheSameCell, direct Absorb calls (this
file's existing style for the function's own arithmetic) with armour and an
item on the SAME cell for the first time -- no prior fixture combined them.
F7: ADirectHitFromStarboardDamagesStarboardCellsWhenFacingIsMostlyLateral, a
direct-hit test at a facing with |fx| > |fy|, computing "starboard" as the
independent right-perpendicular of the facing rather than reading it from
Entity.ToSchematic. Cut 11's Splash tests are the only thing guarding this
handedness today, and 12.4 deletes Splash.
Both F4 and F5 track hull damage per-attempt (reset on each retry), not as a
before/after total across every retry: this fixture's off-target lanes carry
no armour, so a stray hit on the wrong column during a retry spends real
damage on the hull and would otherwise contaminate a cross-attempt total.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Operator ruling (2026-09-25): overlapping spread lanes should diffuse sideways
rather than double up. FireControl.Apply now spaces lanes by the shadow width
a cell casts at the committed bearing (Extent's own h, doubled) instead of by
one cell -- 1 at axis-aligned bearings (unchanged), up to sqrt(2) at 45
degrees. Reuses Extent, the one function that already owns h for Silhouette
and Lane, rather than re-deriving it a second way.
F3: PenetrationExcludesACellExactlyAtItsOwnBoundary's 1e6-durability marker
absorbed the whole shot before the walk could reach the boundary cell, so the
test could not fail under the `>=`-to-`>` mutant. The marker now carries 10
durability, leaving a remainder that would visibly damage the boundary cell if
the walk incorrectly included it. Adds
PenetrationExcludesCellsBeyondANonIntegerDepth, pinning the ordinary
"< penetration" comparison at a depth (1.5) that isn't an exact-integer
boundary.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
First Stryker pass over the changed lines (Entity.cs: Absorb/DamageHull;
FireControl.cs: Apply, Splash) killed 584/844 candidate mutants; these
six survivors on the cut's own diff get real behavioural tests instead:
- Entity.Absorb's `d > 0f` and `d > 0.1f` guards, and DamageHull's
`damage > .1f` guard, each had their boundary flipped to `>=` and
survived -- at the exact boundary (0 incoming, or exactly the .1f
carry-forward threshold), real code does nothing while the mutant
fires an extra event or spends an extra fraction. Not a rare float
coincidence: a spent lane routinely reaches a cell with exactly 0
remaining, and an authored penetration/armour value can land exactly
on .1f.
- FireControl.Apply's `target.IncomingHit.OnNext(shot.Source)` (moved
here from the deleted Entity.ApplyHit) had no test that it still fires.
- FireControl.Apply's two `walked[li] > 0` checks (metalLanes counting,
and the absorb-loop entry) both flipped to `>= 0` and survived, since a
lane count can never be negative -- a real bug class (a lane entirely
off the hull would dilute the split and leak a damage share to the
hull) that a hull no wider than the spread now catches.
- The penetration cutoff's `>= shot.Penetration` flipped to `>` survived
at the exact boundary -- an axis-aligned lane's entry values are exact
integers, so an authored integer penetration routinely lands exactly on
a cell's own entry difference.
Recorded, not chased: `shot.Outcome.Lateral - (li - n)` (the lane-offset
sign) is an equivalent mutant -- 2n+1 lanes built symmetrically around n
cover the same SET of offsets either way, just enumerated in reverse, and
the loop's own per-lane work is order-independent. The missing
`ArrayPool<LaneCell>.Return` survivor is a perf-hygiene gap (GC pressure,
not a spec'd invariant -- only the forecast path is zero-allocation), not
yet reached by a dedicated allocation test.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
DamageIsAbsorbedInOrderAlongTheRay, ArmourFacesTheShot,
BuriedCockpitNeedsPenetration, SpreadWidensTheImpactAcrossLanes,
ARayThroughACornerCrossesTheCornerCell and LaneRemainderReachesTheHull,
per docs/fire-control-cut.md's Cut 12.3 verification list. Builds its own
fixtures (the file's own Build), matching every other cut's own test file.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>