Commit graph

2,304 commits

Author SHA1 Message Date
Meta Vens
12656b92d2 Retire stale Aetheria Persona priority cue 2026-09-26 13:06:52 +02:00
Meta Vens
7e63d7448a Cut 12.4(b): behavioural tests for Detonate and the fuse switch
Geometry, absorption order, symmetry/frame, shields and fuse tests from the
cut doc's own verification list, plus Soul's three fold-in probes (a
radius-without-a-fuse damage-outcome test, a moving-target proximity-burst
pin, and Detonate's nonpositive-radius guard) and the two catalog probes
(shipped-catalog neutrality on BlastRadius/Fuse, and a WeaponFuse/BlastRadius
round-trip through a written and reopened catalog copy).

Fixture: SchematicCellSize 2, target off the origin, hull centre of mass not
an integer cell, one facing with |fx| > |fy| -- the rules the cut doc names
for every blast test. Geometry expectations come from exact half-split
symmetry where possible (TheShareOffTheHullIsLost, ShieldPaysForWhatReachesIt)
and from an independent numeric area integral otherwise (EachCellTakesItsShare
OfTheDisc, TheFusePointStaysOnMetal), never from the SUT's own overlap code.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 01:37:53 +02:00
Meta Vens
7c99909d2b Cut 12.4(b): Detonate replaces Splash and Entity.Absorb
FireControl.Apply now switches on the frozen shot.Fuse: a null fuse keeps
12.3's lane path (ApplyDirectHit), a proximity fuse detonates at arrival with
no roll, and a contact or delayed fuse detonates only on a committed hit
(ApplyBlastHit), locating P through Lane and the new Reach owner (moved
verbatim from Apply's own inline penetration clip; the delayed fuse point
reads the identical rule). Step has no damage branch left.

Detonate (FireControl.cs) is the one owner of blast damage: candidates by
centre-plus-half-diagonal, exact circle/cell overlap shares computed once,
every blast's shield decided live per Q12-9 = A (including the host of a
contact/delayed blast -- Commit now gates its own shield decision on
shot.Fuse == null), armour absorbed per cell, and each item's covered cells
pooled into one ItemAbsorb call. Entity.Absorb and FireControl.Splash are
deleted; Entity gains ToSchematicPoint/ToWorldPoint for the point conversion
Detonate and the delayed fuse point need. Mine.Explode now calls Detonate.

Existing Splash/Absorb-era tests across Cut4/5/6/11/123 are rewritten or
retargeted to the new public owners; behavioural coverage for the blast
model itself lands separately.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 01:17:36 +02:00
Meta Vens
77be5cf433 Cut 12.4 map: slot 32 for Fuse, inert rule arrives with (b), measured drift
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 00:52:16 +02:00
Meta Vens
2bb245bc34 Cut 12 map: Q12-9 ruled A (the blast decides every shield)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 00:31:22 +02:00
Meta Vens
2625768f86 Cut 12.4(a): rename AirburstRange to BlastRadius, add WeaponFuse
WeaponItemData.AirburstRange (slot 29) becomes BlastRadius, same slot and
type so CultCache opens the catalog as compatible drift. A new nullable
slot 32 (30 and 31 belong to EquippableItemData) carries WeaponFuse?
Fuse. FireControl.Fire now decides "detonates" once, from BlastRadius
and Fuse directly, and freezes Fuse only when BlastRadius > 0 -- the
WeaponModifiers.Airburst label no longer decides anything (Q12-6).
PendingShot.BurstRadius is renamed BlastRadius and gains a Fuse field
that nothing reads yet; Step's Splash/Apply dispatch is unchanged.

No shipped weapon carries a blast radius or the Airburst label (Cut 12's
Probe4, reconfirmed against GameData/Aetheria.cc), so this is
behaviour-neutral on shipped content. Apply's fuse switch and deleting
Splash are Cut 12.4(b).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 19:15:26 +02:00
Meta Vens
27318cb39f Cut 12.4 map: re-anchor at d402283c, areas on the one-path absorption, Q12-9
Imagination pass: every anchor is re-probed; a disc runs armour per cell, then pools
each item through ItemAbsorb; Entity.Absorb dies with Splash; the penetrator's
fuse point reads the same Reach owner as the direct-hit walk; the ledger now
carries actuals for 12.1-12.3; Q12-9 (the host's shield for a blast) is opened.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 18:58:58 +02:00
Meta Vens
d402283c06 Cut 12.3 status: closed
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 18:38:21 +02:00
Meta Vens
cccf3a2ac4 Cut 12.3 fix batch (F5, F1, F2): model sweep never meets a cycle; commit P3-P5
FixedPointItemResolution now throws "the model never meets a cycle" if a pool
is still open after its fixed point settles, instead of silently
force-settling in the reverse of first-encountered order. The sweep already
gates its own draws (LanesReaching) so a genuine cross-item cycle is never
actually presented to the model; a thrown exception here means the gating
missed a real case, not a numbers question the model should answer with its
own competing tie-break. The stale "opposite tie-break" comment describing
the deleted behaviour is gone with it.

Three of Soul's five probes (soul125-FireControlCut123Tests.probe.cs) are
committed here, ported from reflection onto the public Fire/Commit/Apply path
this file's own tests already use -- FireCentre/FireUntilHit retries until
the committed hit lands on the expected column, the same idiom
ZeroDepositDoesNotStall and PooledPathHonoursTheItemThresholdLikeTheSoloPath
already use, never a direct call into the private Apply:

- ZeroDepositContributorFiresNoItemDamageEvent (P3): a zero-deposit
  contributor must not fire a phantom ItemDamage event. Kills Entity.cs's
  `incoming[i] > 0f` boundary flipped to `>=`, and a swap that reports the
  post-split share instead of the incoming one (F2).
- ClippedLaneDoesNotHoldThePool (P4): an item beyond every lane's own
  penetration clip must never be waited on.
- OwnRepeatVisitDoesNotHoldThePool (P5): a lane's own later visit to an item
  must not hold that item's pool open -- only other lanes are future
  reachers. Kills FireControl.cs's `alreadyContributed` guard being deleted
  (F1).

The other two probes (Soul5_AngledMirrorAndConservationWithSharedItems,
Soul5_AngledMirrorCycleShapes) are NOT committed. They drive Apply directly
through reflection with a hand-built PendingShot.Outcome so they can sweep
1400 exact, mirrored (bearing, lateral) pairs per test and compare against an
independent model -- Commit's own bearing is reachable through the public
path (shooter/target positioning), but its lateral is a random draw from the
per-shot RNG, not a value either test can set or predict without either
control of that RNG's seed and Silhouette's exact sigma, or a decoy Apply
entry point. Reaching them without reflection needs one of:
(a) `[InternalsVisibleTo]` for Aetheria.Shared.Tests on FireControl.Apply, or
(b) a public/internal test seam that takes a pre-built ShotOutcome (bearing,
lateral) and runs Apply's own walk against it.
Neither is authorized by this fix batch's map; this is reported rather than
silently reflected. The Soul5_Alloc benchmark (also reflection-based, and not
one of P1-P5) is likewise left uncommitted.

Mutation verification (docs/fire-control-cut.md, Cut 12.3, one-apply-path):
F1 and both F2 mutants confirmed killed by the new tests; the earlier
fix batch's own forced-sequential, resolve-one-pool-only, and cycle-throws
mutants (Soul's M3/M4/M6 family) still die against CycleTieBreakIsMirrorSymmetric
and friends.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 18:33:50 +02:00
Meta Vens
c07b4fee65 Cut 12.3 fix batch (F4, F6): delete unreachable guards, fix stale ArmorAbsorb comment
Entity.ItemAbsorb's `item == null` guard is unreachable: both callers (Absorb,
and FireControl.ApplyPooled's Resolve) already guarantee a non-null item
before calling in. Its `total > 0f ? ... : 0f` divide guard is unreachable
too -- the function already returned above whenever total <= 0.1f, so the
divide never sees zero. Both deleted; the surrounding comment now says why
instead of describing a guard that no longer exists.

ArmorAbsorb's own comment was stale from before the "one code path" ruling:
it described the item phase as deferred only for a cell shared with another
lane, when every occupied cell now defers to ItemAbsorb's pool, a lane that
turns out to be the sole contributor included (a pool of one, not a separate
solo path).

Soul's findings (docs/fire-control-cut.md, Cut 12.3): F4, F6.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 18:33:23 +02:00
Meta Vens
64d17a290d Cut 12.3 fix batch (F4): one writer for lane completion, delete dead reachability offset
ApplyPooled.Resolve no longer calls FinishLane directly -- AdvanceLane is now
the single place that checks pointer >= walked and finishes a lane, so lane
completion has one writer instead of two. StillReachableByAnotherLane's own
`blocked ? pointer+1 : pointer` collapses to plain `pointer[li2]`: the
alreadyContributed check just above it already excludes a lane that is
blocked on the very item being asked about, so `pointer[li2]` is never that
item's own cell by the time the +1 would have mattered.

Soul's findings (docs/fire-control-cut.md, Cut 12.3): F4.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 18:33:09 +02:00
Meta Vens
0dd16713b6 Cut 12.3 map: record the cycle rule as Self's default
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 18:01:38 +02:00
Meta Vens
166b6202b1 Cut 12.3 fix batch: the model sweep is now an independent fixed point
Rewrite the shared-item model sweep instead of leaving it a follow-up:
the earlier ModelWithSharedItems copied FireControl.Apply's own
survey-then-walk shape closely enough that reverting production to a
plain sequential per-lane absorb (no pooling at all) still passed it.

FixedPointItemResolution settles items in a different order from
production: it batches every lane's own armour-only walk into full
passes, then checks every currently open pool for readiness once per
whole pass (a dynamic, re-derived-each-time check, not a table built
once up front -- a precomputed-once version deadlocked on a non-convex
item crossed twice by the same lane). Lane geometry is read from the
real FireControl.Lanes instead of a second hand-rolled walk, removing
a class of disagreement that had nothing to do with absorption order.
Durabilities are now drawn below a lane's own share so every shared hit
overflows, hull damage is compared alongside armour and items, and the
tolerance is tightened now that lane geometry can no longer drift.
Also fixes: rejected attempts in the retry loop were mutating real
state without resetting it, corrupting the "before" snapshot; the
model was missing Entity.ItemAbsorb's own pooled .1f threshold.

Reran M0 (control, green) through M4: M1 (forced sequential absorb)
now kills the sweep as well as the mirror/proportional tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 17:38:08 +02:00
Meta Vens
ab14552a1e Cut 12.3 fix batch: tests for the one-apply-path pooling rules
Commit Soul's probes as behaviour tests against the pooled walk:
CycleTieBreakIsMirrorSymmetric (a genuine two-item cyclic dependency,
mirror-symmetric and damage-conserving by construction),
PooledPathHonoursTheItemThresholdLikeTheSoloPath (the .1f threshold is
decided once on the pool total, never per contribution, and pins the
pooled ItemDamage event count), and ZeroDepositDoesNotStall (a lane
whose armour ate its whole share still deposits and never blocks the
pool it belongs to). Adds the Custom fixture hook (an arbitrary named
item shape) the cycle test needs to build two crossing non-convex items.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 17:02:11 +02:00
Meta Vens
e9339a5673 Cut 12.3 fix batch: one apply path for every item (operator ruling)
Replace the survey-then-branch (ApplySequential vs. ApplyWithSharedItems)
with a single pooled walk. Lanes are clipped to penetration once, right
after Lanes returns; every item -- single-cell or multi-cell -- pools
whatever lanes reach it and resolves the moment no other unfinished lane
can still reach it down its own remaining clipped walk, with no separate
reachability survey. A cyclic dependency between two non-convex items
resolves every currently open pool at once, order-independent by
construction. Entity.ItemAbsorb becomes the one absorption function,
gated on the pooled total; Absorb (Splash's only remaining caller) now
funnels through it with a one-contribution pool.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 17:01:05 +02:00
Meta Vens
14ee2a52ca Cut 12.3: one apply path for all items (operator ruling)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 16:22:44 +02:00
Meta Vens
b55894a02a Cut 12.3 fix batch: tests for proportional multi-lane item absorption
F8 tests, through the real Fire/Commit/Apply pipeline (no reflection):
- ProportionalAbsorptionSplitsTheLeftoverBetweenTheLanes /
  ...LeavesNothingWhenTheItemAbsorbsEverything: the ruling's own literal case
  (two lanes bringing D1=25/D2=15 onto one 2x1 item), both the partial- and
  full-absorption branches.
- MirrorSymmetryHoldsWithASharedMultiCellItem: a symmetric hull and its own
  mirror-image fixture must produce the same sorted leftover pair. Fails on
  the pre-fix-batch code (commit d1cc047a) at itemDurability 20 ({0,20} vs
  {5,15}); a durability equal to one of the two D values turned out
  degenerate and was rejected first (see the test's own comment).
- EveryAxisAlignedSpreadShotStrikesExactlyTwoNPlusOneFacingCells: Soul's own
  owed regression guard on lane spacing, independent of item sharing. Killed
  by hand-mutating FireControl.cs's laneSpacing by *.99f (R1b): 28/400
  interior-column hits drop a facing column.
- ModelSweepAgreesWithProductionIncludingSharedMultiCellItems: an independent
  model (armour-then-item-then-hull per lane, extended with the same
  survey/sum-then-split shape production uses) checked against production
  over a sweep of two hull sizes, three bearings and both 2x1 bars and
  non-convex L-shaped items (Build gains lBars/LShape for this).

Tool-hardpoint items (Bar, Marker) can only occupy interior cells
(Entity.ItemFits), so every fixture above needed a hull at least 3-4 cells
deep -- shallower hulls (used in an earlier draft) fail marker/bar equip
outright.

Hand-verified against the M13 "lane_order_reversed" mutant Soul recorded as
surviving on the pre-fix code (FireControl.cs, Lanes' own `var k = li - n`):
MirrorSymmetryHoldsWithASharedMultiCellItem fails on the pre-fix code under
that mutant and passes on this cut's code under the same mutant, confirming
the new resolution is order-independent of which physical lane a buffer index
happens to hold, not only of Apply's own loop order.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 16:17:13 +02:00
Meta Vens
567579136a Cut 12.3 fix batch: proportional absorption for multi-cell items shared between lanes
Operator ruling (2026-09-25): "proportional absorption; go". A multi-cell item
struck by several lanes of one shot now absorbs their combined incoming damage
(up to its own remaining durability) and each lane's leftover is in proportion
to what it brought, replacing the old behaviour where whichever lane Apply
happened to visit first drained the item and the next lane inherited whatever
was left.

Entity.Absorb is split into ArmorAbsorb (per-cell, never shared) and ItemAbsorb
(the single-lane path), so FireControl.Apply can call armour immediately and
defer item resolution only for a cell whose item is reachable by more than one
lane this shot. FireControl.Apply gains a geometric survey (which items are
reachable by >1 lane, decided before any damage is applied) and takes the
untouched sequential walk (ApplySequential) whenever nothing is shared, or a
new coordinated walk (ApplyWithSharedItems) that deposits each lane's
post-armour remainder into a shared item's pool and resolves it once every
contributing lane has arrived, splitting the leftover proportionally
(sum-then-split, order-independent by construction). A deterministic tie-break
(smallest first-contact Entry) breaks the rare "opposite order" deadlock two
non-convex shared items can create between two lanes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 16:16:53 +02:00
Meta Vens
d1cc047a51 Cut 12.3: shared multi-cell items absorb proportionally (operator ruling)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 15:28:42 +02:00
Meta Vens
2d13541b9f Cut 12.3 status: closed apart from the multi-cell item ruling
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 15:17:25 +02:00
Meta Vens
cb71a3cfad Cut 12.3 fix batch: unify Lane and Lanes on one membership query
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>
2026-09-25 14:24:55 +02:00
Meta Vens
742354c67a Cut 12.3 fix batch: F3 rework, F4 delete unreachable mirror test
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>
2026-09-25 14:07:38 +02:00
Meta Vens
4e6495dd63 Cut 12.3 fix batch: F1, decide lane membership once per cell
Soul's second pass (CONFIRMED): even after the F2 shadow-width fix, Apply's
per-lane loop still called Lane() once per lane index, each call re-deriving
its own lateral s(li) = Lateral + (li - n) * laneSpacing by forward float
addition, then testing that value against a cell's Extent interval
independently of every other lane. Two lane lines are only exactly
laneSpacing apart in real arithmetic; accumulating the offset by repeated
addition can shift a side lane by an ulp or two, letting it land inside a
neighbouring cell's half-open interval that its own unrounded value would
never reach -- the same "two derivations of one quantity" defect Cut 12.2's
own fix batch already removed from Lane itself, recurring one layer up in
Apply.

FireControl.Apply now calls a new Lanes() that decides each candidate cell's
lane index with one computation: k = ceil((cell.Lo - centreLateral) /
laneSpacing) is the unique integer for which centreLateral + k * laneSpacing
falls in the cell's own [Lo, Hi) -- derived once, from values no other lane's
arithmetic has touched. A single Ceiling call returns exactly one integer, so
a cell can be assigned to at most one lane by construction, not by float
luck. When k == 0 this is the same condition Lane() tests for centreLateral
itself, so the centre lane's cell set is unchanged (the 12.2 placement
contract, HitsLandOnTheFacingEdge, still holds). laneSpacing is still used,
unchanged, to place each admitted lane's own line for AlongBearing's slab
solve -- geometry, not membership.

Lane() itself is untouched (still used directly by Commit and by
FireControlCut12Tests' edge-seed tests).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 14:07:36 +02:00
Meta Vens
d301c0fbbd 12.3 map: lanes are disjoint by construction, not by float spacing
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 13:22:09 +02:00
Meta Vens
be5382170d Cut 12.3 fix batch: rework the F2 mirror test to compare hull totals, not per-cell sums
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>
2026-09-25 12:17:59 +02:00
Meta Vens
c2e7a2166f Cut 12.3 fix batch: F1 angled-bearing parity, F2 spread-lane behaviour
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>
2026-09-25 12:08:29 +02:00
Meta Vens
078b1ff36d Cut 12.3 fix batch: F10, an independent AlongBearing expectation at several bearings
The previous AlongBearingReturnsTheExactSlabIntersection wrote out SlabAxis's
own threshold (1e-9f) and clamp a second time in its ExactSlab helper, so a
mutant that only moved that threshold (C7b, 1e-9 to .2) agreed with both
copies identically and survived.

AlongBearingReturnsTheExactSlabIntersectionAtSeveralBearings replaces it: the
expected entry/exit now come from an exact ray-box intersection that never
special-cases a near-zero bearing component -- IEEE division by a signed zero
already yields the correctly signed infinity, so an axis with no motion is
unconstrained without a hand-written epsilon to disagree with SlabAxis's own.
Checked at the original fixture's bearing plus two within about 11 degrees of
an axis, inside C7b's mutated .2f threshold but nowhere near the real 1e-9f
one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 11:53:47 +02:00
Meta Vens
435f905e8b Cut 12.3 fix batch: F4 first-gap, F5 side-lane remainders, F6 armour/item order, F7 handedness
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>
2026-09-25 11:39:53 +02:00
Meta Vens
b82b7eab21 Cut 12.3 fix batch: F2 lane spacing by shadow width, F3 penetration boundary fixture
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>
2026-09-25 11:32:02 +02:00
Meta Vens
f2c94c29be Q12-2 amended: spread lanes are spaced by a cell's shadow, so none doubles up
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 11:20:45 +02:00
Meta Vens
73b15d9f48 Cut 12.3 Stryker fix batch: exact-threshold and lane-count survivors
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>
2026-09-25 09:09:16 +02:00
Meta Vens
7f1fde8e71 Cut 12.3: verification for the direct-hit lane's own new rules
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>
2026-09-25 05:56:45 +02:00
Meta Vens
3fe66f555e Cut 12.2 carried items: pin Lane's seam contract and AlongBearing's values
Items carried into 12.3's brief from 12.2's third Soul pass, all on code
12.3's own lane walk builds on:

- LaneSeamIsHalfOpen pins the s >= Hi admission test (FireControl.cs) on
  the smallest fixture where the seam matters -- the s > Hi mutant that
  survived 12.2 would have interleaved two columns at a shared seam once
  12.3's walk started reading more than buffer[0].
- AlongBearingReturnsTheExactSlabIntersection gives AlongBearing/SlabAxis
  their first test of actual VALUES (every mutant there survived in 12.2,
  equivalent while only buffer[0].Cell was read) -- a diagonal fixture
  checks Lane's own Entry/Exit against an independent slab solve, cell for
  cell, in order.
- Corrected the allocation comments on Forecast and
  LaneAndSilhouetteAllocateNothingGivenAPooledBuffer: the ~120 B/call on a
  gated-in HitProbability is three GetBehavior<TargetingSystem> calls (not
  EquippedItem.Behaviors, which is a plain array), plus CellsOf's ~944 B
  when an item is aimed at; noted that the loose <500_000 bound doesn't
  catch a reintroduced comparer wrapper but the buffered zero-allocation
  asserts do.
- ZeroProbabilityShotCommitsAMissWithoutBuildingASilhouette is now
  behavioural: PFire 0 comes from closing VisibleEntities (the real gate),
  and "no Silhouette was built" is checked by asserting zero allocation
  rather than reading CommitProbability's own struct default.
- Dropped the unused System.Reflection import.

Also replaces ExpectedMarchCells, which reproduced Entity.ApplyHit's own
deleted 0.5-step march, with ExpectedLaneCells, built on the
already-verified FireControl.Lane plus an independent penetration cutoff --
used by TurningAfterCommitChangesNothing, whose fixture also gets real
per-cell armour (the flat 1000 armour every Build() authors would let the
new sequential Absorb model's first cell alone swallow the whole 5-damage
shot, leaving nothing to show on the rest of a several-cell lane).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 05:56:30 +02:00
Meta Vens
3b6b9e4a28 Cut 12.3: drop two Cut 12.3 tests that called the deleted Entity.ApplyHit
HardpointHitDamagesItemThenHull and PenetrationMarchIsPlanar both called
Entity.ApplyHit directly, which Cut 12.3 deletes. The rules they pinned:
"damage above armour plus item durability zeroes the item and passes the
remainder to the hull" is now FireControlCut123Tests.DamageIsAbsorbedInOrderAlongTheRay,
through the real Fire/Commit/Apply path. "The penetration march is planar,
in the target's own schematic frame" is subsumed by 12.2's own
HitsLandOnTheFacingEdge and TurningArmourIntoTheShotTakesItOnTheArmour
(which already exercise FireControl.Apply's committed, schematic-frame
bearing end to end) and by FireControlCut123Tests.ArmourFacesTheShot,
which pins planarity across several non-axis-aligned facings.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 05:56:11 +02:00
Meta Vens
70c3e0ccdb Cut 12.3, sub-cut A: armour absorbs first down the direct-hit lane
Deletes Entity.DamageSchematic (the even split over the whole hit shape)
and Entity.ApplyHit (the 0.5-step penetration march), replacing both with
Entity.Absorb (armour then item, per cell) and Entity.DamageHull, moved
verbatim from DamageSchematic's own body. FireControl.Apply now builds
2n+1 parallel lanes from the committed Bearing/Lateral (Q12-2 = A),
shares damage evenly over the lanes that meet metal, walks each lane in
order via the already-verified FireControl.Lane, absorbing cell by cell
up to the penetration depth, and sends every lane's own remainder to the
hull where it ends (Q12-3 = A). Splash is rewritten onto Absorb/DamageHull
(arithmetically identical to before) since DamageSchematic is gone.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 05:55:59 +02:00
Meta Vens
c7b6e80c70 Cut 12.2 closed: status, what three Soul passes found, and what 12.3 inherits
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 05:17:10 +02:00
Meta Vens
ca1670a216 Cut 12.2 Soul fix batch 2, S3: measure allocations, correct an overclaiming comment
Measured (Release, 7x9 solid hull -- the LonginusX shipped-hull fixture the
brief named is not available to this test file):

- Lane: 0 B / 1000 calls (was: 64 B/call from Array.Sort's comparer wrapper;
  now confirmed 0 after the earlier IComparable<T> change in this batch).
- Silhouette (caller-supplied buffer): 0 B / 1000 calls.
- HitProbability, gated in (a real, non-gated-out call): ~120 B/call. This is
  NOT from Lane, Silhouette or the comparer -- it comes from Accuracy/
  Resolution's own GetBehavior<T>, which walks Entity.Equipment and each
  item's Behaviors (both ReactiveCollection<T>); that type's enumerator is
  heap-allocated per foreach. This predates Cut 12.2 and is out of this cut's
  scope to fix.
- Inspect, gated in: ~280 B/call (same cause, more GetBehavior<T> calls).

The Forecast comment claiming "the forecast path (HitProbability, Inspect) no
longer allocates on a gated-in call" was not true and is corrected to say what
was actually measured: Silhouette itself is allocation-free given a pooled
buffer, and the residual cost belongs to Entity's own collection choice.
LaneAndSilhouetteAllocateNothingGivenAPooledBuffer asserts the true zero-alloc
claims exactly and records (with a loose regression bound, not a false zero)
the unrelated Entity-level cost on the full HitProbability path.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 03:30:30 +02:00
Meta Vens
73c53a9335 Cut 12.2 Soul fix batch 2, :~483: pin the zero-probability guard
CommitProbability's `if (p <= 0f) return 0f;` guards against building a
Silhouette for a shot that cannot hit. Stryker's `p < 0` boundary mutant
(excluding only p == 0 exactly) survived because nothing exercised the guard
at that exact boundary. ZeroProbabilityShotCommitsAMissWithoutBuildingA
Silhouette sets a real, fired shot's PFire to 0, calls CommitProbability
directly and asserts sil stays the zeroed default (Intervals null, Count 0 --
no Silhouette was built), then resolves the shot through Zone.Update and
checks it lands as a clean miss.

Manually verified against the exact `p < 0` patch: it now fails (sil.Intervals
comes back non-null, a real Silhouette array).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 03:24:13 +02:00
Meta Vens
0ece224b0b Cut 12.2 Soul fix batch 2, S8: an L-shaped aimed item, not just a square
Only a 2x2 (BigMarker) aimed item was ever tested -- a square's centroid and
its bounding-box centre are the same point, so N11 (aim point = bounding-box
centre instead of the true cell-average centroid) survived. AimingAtAnLShaped
ItemCentresOnItsCentroidNotItsBoundingBox equips a 3-cell L-tromino (Build's
new equipLMarker fixture), fires from a diagonal facing so the shape's true
asymmetry projects fully onto the lateral axis, and asserts the empirical mean
lateral offset tracks the item's own true centroid (computed by the test) and
not its bounding-box centre.

Manually verified against N11's own patch: it now fails (empirical -2.139,
tracking the wrong bounding-box prediction -2.121, against the true centroid
-1.886); the existing BigMarker test is unaffected since a square's two
candidate points coincide.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 03:21:40 +02:00
Meta Vens
4510a62c4f Cut 12.2 Soul fix batch 2, S7: author Tracking through Build, not by patching after Fire
TheHudEstimateIsTheCommitPrice used to fire each shot, then reach into
Zone.PendingShots and overwrite the frozen Tracking field after the fact. Build
now takes a `tracking` parameter (default 100000, matching the old fixed
value) alongside Precision and Accuracy, so this test authors Tracking = 6 up
front like every other frozen shooter stat, with no post-hoc patch loop.

Manually verified M15 (Commit applies PSpread twice) still fails this test
(empirical 0.070 vs forecast 0.126) after the rewrite.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 03:17:38 +02:00
Meta Vens
7eadea88ab Cut 12.2 Soul fix batch 2, S4: pin flight time for an invisible target
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>
2026-09-25 03:14:15 +02:00
Meta Vens
c2b27b10ee Cut 12.2 Soul fix batch 2, S2: pin the within-prong ratio, not just direction
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>
2026-09-25 03:11:27 +02:00
Meta Vens
d82b6bf9ed Cut 12.2 Soul fix batch 2, S1: behavioural tests, not reflection, for the shadow fix
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>
2026-09-25 03:06:44 +02:00
Meta Vens
d38efba3a3 Cut 12.2 Soul fix batch 2, S1/S3: one shadow authority in Lane, sort without a comparer
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>
2026-09-25 03:01:21 +02:00
Meta Vens
8bd25f6faa Cut 12.2 Soul fix batch: half-open clamp, split assembly, real rolls
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>
2026-09-24 23:08:19 +02:00
Meta Vens
efe4f7c954 Cut 12.2 Stryker fix batch: one range, five real gaps
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>
2026-09-24 21:25:15 +02:00
Meta Vens
7dbb3bcf7a Cut 12.2: where a hit lands is priced and drawn from one silhouette
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>
2026-09-24 20:40:50 +02:00
Meta Vens
dc81867998 Cut 12.1 status
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-24 19:57:53 +02:00
Meta Vens
e91ba36976 Pin CultLib per package, as Unity's manifest does
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>
2026-09-24 19:17:40 +02:00
Meta Vens
2a68b4c077 Repin CultMath to 0.2.4: erf and erfinv reach Unity
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>
2026-09-24 19:14:00 +02:00