Commit graph

2,264 commits

Author SHA1 Message Date
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
Meta Vens
b851b0d1c3 Cut 12.1: one frame owner, and the dead outcome flag
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>
2026-09-24 18:55:53 +02:00
Meta Vens
212121eb88 Cut 12.0 status: the code closes, the release is owed
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>
2026-09-22 21:44:27 +02:00
Meta Vens
0984438c6c Cut 12: operator confirms bow is +y on the schematic
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-22 20:20:10 +02:00
Meta Vens
6ee30e1996 Cut 12: scope Q12-8 to warheads; a dumb AP round does damage in a line
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>
2026-09-22 20:18:57 +02:00
Meta Vens
8342e1c528 Cut 12.4: blasts are areas, not rays
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>
2026-09-22 20:16:35 +02:00
Meta Vens
30f280a61f Cut 12 map revised: bearing at commit, spread as width, 12.4 detonation primitive, modifiers are labels
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>
2026-09-22 19:06:09 +02:00
Meta Vens
50781eafcf Correct the Splash handedness claim: 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>
2026-09-22 18:37:12 +02:00
Meta Vens
122baad04b Cut 12 map: shots arrive edge-on; and correct the Splash handedness claim
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>
2026-09-22 18:36:39 +02:00
Meta Vens
cd846916bc Merge Cuts 10 and 11: the hot path gates first, and mutation testing moves to Stryker
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-22 16:41:11 +02:00
Meta Vens
e8fbb6a6af Cut 11 status: Stryker progression and the triage of the 43 survivors
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-22 16:40:19 +02:00
Meta Vens
b0fd3cd06c Cut 11 second round: one owner for PBase, and tests for what the 51 survivors showed
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>
2026-09-22 16:39:52 +02:00
Meta Vens
1b187db45b Cut 11.3: fix the degenerate fixtures that made mutants indistinguishable
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>
2026-09-22 16:20:45 +02:00
Meta Vens
a6543af741 Cut 11.4: behavioural tests for what Stryker found undefended, and Soul C4
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>
2026-09-22 16:06:33 +02:00
Meta Vens
724ec5e7d3 Cut 11.1: retire the hand-written mutation harnesses Stryker replaces
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>
2026-09-22 15:59:45 +02:00
Meta Vens
b305debd49 Cut 11 map: mutation testing moves to Stryker, and the survivors it found
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-22 15:59:06 +02:00
Meta Vens
8ef3b34c94 Adopt Stryker.NET as a repo-local tool, for the mutation-testing spike
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>
2026-09-22 15:50:34 +02:00
Meta Vens
b15cbbe5de Cut 10: the hot path stops looking like, and costing like, a diagnostic
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>
2026-09-22 15:18:12 +02:00
Meta Vens
9db64c112a Merge fire control Cut 9: the die is not a die
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>
2026-09-22 03:01:42 +02:00
Meta Vens
73e080d8d6 Cut 9: re-anchor cut3 and cut6 onto the mixed seed
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>
2026-09-22 03:01:04 +02:00
Meta Vens
6db6dc78f3 Cut 9: the die is not a die
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>
2026-09-22 02:33:22 +02:00
Meta Vens
16d8493e0c Cut 9 map: the die is not a die
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>
2026-09-22 01:53:41 +02:00
Meta Vens
dcb8c448ce Merge fire control Cut 8: a removed entity is a dead entity
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>
2026-09-22 01:14:19 +02:00
Meta Vens
8a87dec90f Cut 8: a removed entity is a dead entity, and re-anchor the stranded mutations
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>
2026-09-22 00:50:57 +02:00
Meta Vens
f446e5c2c2 Cut 8 map: a removed entity is a dead entity
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>
2026-09-22 00:17:25 +02:00
Meta Vens
72c0109cf1 Add live fire control diagnostics 2026-09-20 21:58:44 +02:00
Meta Vens
6056327935 Merge fire control Cut 6d: where a hit lands is a dart throw, not a coin flip
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>
2026-09-19 22:46:10 +02:00
Meta Vens
e7be36ddd7 Apply targeting-catalog-6d: re-author Precision for the two targeting designs 2026-09-19 22:20:44 +02:00
Meta Vens
bf4744448f Add targeting-catalog-6d migration command to AetherDb tool 2026-09-19 22:20:22 +02:00
Meta Vens
aa850a3cbe Fix ShieldTakesHit coincidental pass and cut3 harness stale test name
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.
2026-09-19 22:16:25 +02:00
Meta Vens
8212a0149d Cut 6d: mutation harness for the dart-throw kernel
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.
2026-09-19 21:56:14 +02:00
Meta Vens
b3d4771358 Cut 6d: tests for the dart-throw kernel invariants
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.
2026-09-19 21:52:56 +02:00
Meta Vens
d528d88883 Cut 6d: the dart-throw kernel replaces the coin-flip landing rule
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.
2026-09-19 21:37:39 +02:00
Meta Vens
077b08e662 Merge fire control Cut 7: the harnesses that were prose
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>
2026-09-19 20:16:12 +02:00
Meta Vens
0ae8226ccb Cut 7: retire a test Cut 6b made vacuous, and the invariant it claimed
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>
2026-09-19 20:14:16 +02:00
Meta Vens
76758dae5d Cut 7.2: the Cut 3 harness, and the fixture that let commit-is-not-arrival slip
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>
2026-09-19 19:56:39 +02:00
Meta Vens
d937364354 Cut 7.2: mutation harnesses for Cuts 1 and 2
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>
2026-09-19 19:43:57 +02:00
Meta Vens
fd7ef71c15 Cut 7.1/7.4: the reveal re-check pin and the live-catalog test
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>
2026-09-19 19:33:09 +02:00
Meta Vens
ef223c1d9b Cut 7 amendment: the live-catalog test is the campaign's only readability check
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>
2026-09-19 19:13:04 +02:00
Meta Vens
c896489b39 Fix the merge of Cuts 6b and 6c: a new float field made the catalog unreadable
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>
2026-09-19 18:56:53 +02:00
Meta Vens
6636e757c7 Merge fire control Cut 6c: the formula gets fixed, and the single point of failure
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>
2026-09-19 18:52:26 +02:00