fix(ci): close the cache saturation that makes the matrix non-deterministic - #80
Merged
Conversation
The frozen section as produced by Claude.ai, with the eight specs of the reading list ticked off in the living section. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`Mat4(T)` is declared in `src/foundation/math/mat4.zig` and `rtti` names it
by alias, which preserves the type IDENTITY `classifyField` matches on. Its
storage is `extern struct { m: [16]T }` and not `Mat3`'s `cols: [N]Vec`,
because it carries a frozen C twin that a `@Vector` layout cannot serve.
The twin had no assertion at all — `WeldMat4` has zero use sites and was tied
to `rtti.Mat4` by a doc comment — so a field-by-field comparison lands with a
witness that it can return false.
`affineInverse` dotted the column where the formula wants the row; a diagonal
fixture cannot tell the two apart, the rotated non-uniform one did.
The pose layout is decided on the bench, not on intuition: SoA-3 wins blend
AND forward kinematics at 32/64/128 bones over three runs. The per-channel
form, the only one that vectorises, loses the blend by 75% — which refutes
the stated argument for SoA while confirming SoA. An instrument defect was
corrected first: the AoS body was written scalar against a vector SoA body,
and equalising it moved the 32-bone ratio from 0.936x to 1.006x at identical
checksums.
Test floor 2290 -> 2300 / 2298, re-derived from the suite.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The three layouts differed in two things at once — memory order AND element representation — so the gap could belong to either. `AoS-vec` holds both fixed but the order: same 48 bytes, same alignment, `@Vector` members. It reverses the verdict. Interleaved beats split on blend 12 times out of 12 across four runs and ties it on FK, so SoA-3 is dominated. What the earlier reading had credited to the split was the `[3]f32` element: the corpus's own AoS candidate, one Tier 0 `Transform` per bone, is the slowest viable form by about 10% on both operations, an axis neither document named. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`src/interfaces/AnimationModule.zig` declares the types its own signatures name and attests its non-freeze with a test that fails the day a protocol version appears. The assert block covers what an implementation owes today and grows with it: asserting an entry nothing can satisfy forces a body that returns a plausible answer, which hides worse than an absence. `init` reaches `world` and `persistent_allocator` and nothing else, registers the `Skeleton` component there, and registers no system — a system with no body to run is a mechanism nothing executes. Poses live outside the ECS and the component carries a handle. Instance slots are never recycled: an id is a position, so a reused slot would make a stale handle address a different skeleton and read as working. Counter-factual run: recycling reddens exactly that test and no other. Test floor 2300 -> 2315 / 2313. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Loading refuses a malformed hierarchy rather than repairing one: a loader that reroots an orphan or clamps an index makes every downstream invariant untestable. The topological order is checked here, which makes a cycle unrepresentable rather than detected — a cycle must contain a forward reference and a forward reference is what this refuses, so the two are one check and one code. `error.NoRoot` was written and removed: the test for it failed, and reading why showed no input reaches it. Bone zero has no index below it, so it is the root or the hierarchy was already refused. An error nobody can provoke is an assertion, and it is now one, with the argument at the site. The inverse bind pose is stored, not derived. Deriving would force the bind pose and the rest pose to coincide; they legitimately differ, which is why glTF ships both. Test floor 2315 -> 2329 / 2327. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The pass produces MODEL space — each bone relative to the skeleton root — and never world: the entity transform enters at the consumers, and baking it here would make the skin matrices and the socket reads both wrong in a way that looks right at the origin. Rig and instance are two stores, so one hierarchy backs many poses. A fresh instance stands in its bind pose: a skeleton at the identity is a heap of bones at the origin and reads as a sampling bug. The single pass is asserted twice and neither is timing — the topological invariant read off the loaded rig, and the fixed point, since a second pass would move something if a parent were evaluated after its child. A coverage hole found by counter-factual: reversing the rotation operands left the suite green, because the acceptance chain rotates about one axis and its leaf carries the identity, and quaternions commute in both cases. Closed with two non-parallel rotations, a sequential-application oracle, and a discrimination guard. Test floor 2329 -> 2339 / 2337. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The attack was written first and it caught the plausible implementation: a profile mapping twenty-two of twenty-three roles, the gap in the middle. A resolver indexing the mapping array by ordinal answers foot_l's bone for the unmapped calf_l — a neighbour, which is the failure the invariant names — and the positive test stays green under it, because a resolver right for twenty-two roles passes every ordinary test. Two bones of one name are refused at load. Otherwise a name addresses two bones and the resolver has to pick, which is acting on possibly the wrong one. It caught a real duplicate on its first run. The counter-proof corpus pins the addressing type: a bare string, a bare index and a bare ordinal must not compile in a reference position, while both legitimate variants must. Both halves were shown to redden — and the first attempt to show it was a bad counter-factual that moved the guard one level in instead of removing it. Test floor 2339 -> 2348 / 2346. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`bindModelPose` had no caller and no test, duplicated what `instantiate` does inline, and carried an unstated aliasing contract — it passed one buffer as both input and output of the kinematics pass. `resolveRole` and `resolveName` were public beside a header claiming `resolveBone` is the one path. Two public entries that map a role or a name are two more paths, in the module's own surface, at the place the invariant is written down. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Deviations, blockers, the closing self-audit and the final measurements. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Three defects in shipped behaviour. `Mat4.approxEql` compared NaN EQUAL — `@abs(x-y) > tol` is false for NaN — so four assertions could not fail on a total failure of the inverse. `rig()` returned a pointer the next `loadRig` frees, which the shipped scenario holds across its whole `main`. Neither new process entry installed the float environment, and the bench is the one whose published numbers decided the layout. Eight guards that could not fail, each verified by mutation. The two "single linear pass" tests passed against a kinematics reduced to a copy; the `id == len` boundary was never probed; three refusals of `parse` were unreachable from `parse`; the C-twin's negative witness differed in width, so the field loop it exists for never ran. `fromTrs`'s third column was pinned by nothing, and the first repair was blind to two columns for a different reason — a quarter turn makes their diagonal terms exactly zero. What ships is a half turn plus a generic rotation checked against `rotateVec3` plus a guard that refuses a fixture with a near-zero basis term. All nine terms now detected. And the class this milestone had already closed twice, committed while closing it: `resolveName` stayed public under a header saying it was not. Scope: the module now publishes a resource and registers one `fixed_update` system, driven through a real `dispatchFrame`; the seven unimplemented interface entries are present and refuse; `parse` validates its float payload. Test floor 2348 -> 2357 / 2355. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Thirteen cells green. The windows ReleaseSafe f64 cell cleared its 55-minute budget by 45 seconds, which is the cost this brief predicted for a gate touching a depended-upon Tier 0 header. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The same two cells took 49m40 and 54m15 cold, then 4m28 and 4m26 warm — a factor of 11 to 12 on a branch whose only intervening change is one line of journal. The 54 minutes were compilation, not test execution. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Found by external review after this milestone's own adversarial pass had closed. `compose` built a model-space `BoneTransform` from two others, and that composition is exact for the position and WRONG for the scale: it multiplies the two scales componentwise, which is the right answer only when no rotation between the two bones reorients the scale axes. MEASURED before the fix, on a root scaled `(3, 1, 1)` with a quarter-turned child: the pass put the grandchild at `(0.0000, 3.0000, 0.0000)` where the matrix product puts it at `(0.0000, 1.0000, 0.0000)`. Wrong BY THE SCALE FACTOR, finite and plausible, on a rig no artist would call unusual. What let it through is written at `skeleton.zig`'s header: a comment on that composition documented an approximation about SHEAR — genuinely inexpressible in TRS — and a documented approximation on one quantity does not cover an error on another. It reads as an argument, which is this repository's costliest defect class. `ModelPose` is declared on the interface, `compose` is deleted, `forwardKinematics` composes `Mat4` and the local poses stay TRS — a clip samples translation, rotation and scale, and nothing is gained by widening what it writes. `pose.zig` gains `allocModel`/`freeModel`; `root.zig`, the tests, the demo and the layout bench follow. The pin is a test named for the defect: an accumulated scale must not survive a rotated child. Its oracle is the matrix product, computed by walking each bone up to the root — not the pass's own arithmetic, which would agree with itself whatever it did. AND THE BENCH'S FK ARM STOPPED DISCRIMINATING, which is reported rather than quietly re-run: it measured a TRS output, and against `[]Mat4` the ratios straddle 1.0 (0.870-1.153) with the sign flipping between bone counts inside one run, matrix composition dominating the read cost. The layout verdict stands on the BLEND arm, unchanged and stable across three runs. Recorded in the bench header and in `bench/results/pose_layout.md`, where the FK row is marked superseded rather than left to read as current. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Found by external review after this milestone's own adversarial pass had closed. `validatePayload` refused a stored rotation whose length missed one by more than `unit_rotation_tolerance` and did nothing to the ones it admitted — so a quaternion legitimately inside the band stayed off-unit, the matrix built from it stretched, and the stretch COMPOUNDED down the hierarchy onto a consumer that had been told the payload was validated. The tolerance is NOT tightened. It exists to refuse the absurd — a null quaternion, an arbitrary one — and it keeps that job; what it never was is a guarantee of unit length. The two run in order, and the order is load-bearing: a null norm NORMALISED is a NaN, so the refusal must come first. Both halves are pinned, and the counter-factual that inverts them reddens two tests with `expected error.NonUnitRotation, found error.NonFiniteTransform` — the NaN arriving exactly where the argument says it would. THE FIRST ATTACK WRITTEN FOR THIS MEASURED ALMOST NOTHING AND IS THE REASON THE FIX IS WHERE IT IS. A 128-deep chain at half the tolerance passed at 1.000321, so rather than escalate the fixture blindly the exposure was PROBED: drift is governed by the rotation ANGLE and not by the norm error alone. At the band's edge a 1.2 rad per-bone rotation reaches 8.5 % stretch over 128 bones (5.7 % about (1,1,1)); the same error at 0.1 rad reaches 0.06 %. The first attack used 0.1 rad, which is why it proved the opposite of what it claimed. Tightening the band would not have closed this. The attack ships at the measured worst case, and it asserts BOTH halves: the stored rotation is unit AFTER LOAD — the direct property, which does not depend on a chain length — and the FK tip is unit-length to 1e-3 after 128 compositions. Its non-vacuity is established in the same execution: the same chain rebuilt from the unnormalised quaternion is required to stretch past 1.05, so the assertions above it are about the normalisation and not about a fixture that could never have drifted. Suite floor re-derived FROM THE SUITE: 2357 -> 2359 on macOS, 2355 -> 2357 on windows, the closure agreeing independently at 2359. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Found by external review after this milestone's own adversarial pass had closed. `rig()` returned a `Rig` BY VALUE, and that copy carries the store's own slice handles: `borrowed.parents[1] = 127` wrote into the hierarchy the loader had validated, and `borrowed.deinit(gpa)` freed the module's memory while the store went on holding the same slices. Both compiled. Both were silent. THE PREVIOUS REMEDY IS WHAT PRODUCED THIS ONE, and it is the finding's real content. `rig()` first returned a `*const Rig`, which dangled the moment another rig was loaded, and the repair changed the SHAPE of the return — pointer to value — which closed the dangle and left ownership exactly where it was. A remedy that changes the form of a return moves the defect instead of closing it; what was needed was a second TYPE. So: two types, not one. `Rig` stays the owner — mutable slices, `deinit`, internal to the store, and its doc now says that is what it is. `RigView` is what leaves: every slice `[]const`, no destructor, and no `profile_mappings` at all, that field being the backing storage `deinit` frees and having no meaning to a reader. `resolveBone` takes the view too — resolution is a READ and has no business receiving a destructor. The counter-proof is a NON-COMPILATION, in the corpus that already drives one: `case-view-write` fails with `cannot assign to constant` and `case-view-deinit` with `no field or member function named 'deinit' in 'skeleton.RigView'`. The first mark is generic in isolation, so it is paired with its own file name — a mark that any constant assignment anywhere would satisfy discriminates nothing. The control compiles in the same execution and now READS through the view: bone count, bone name, parents, bind pose, inverse bind and the profile. Without that half, two refusals would prove the type useless rather than tight. AND THE HARNESS IS SHOWN TO FIRE. With `rig()` restored to returning a `Rig`, both new cases COMPILE and the corpus reddens: `process exited with code 0 (expected exited with code 1)`, twice. The two lines this commit makes impossible were legal code on the branch that was about to be tagged. The corpus keeps its addressing name for a second family rather than growing a rename: the path is prescribed by the brief's frozen section, and a cosmetic divergence from it is still a divergence. What is NOT claimed is that no code can name the owning record — Zig has no module-private declaration. What is checkable, and checked, is that the owning type never appears in an entry's return type. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The hotfix was reviewed GO MERGE with one objection: quaternion norm is multiplicative, so |q1*...*qn| = (1+e)^n independent of angle, therefore "drift is governed by the angle" is false and the two fixtures must have had norm errors 100x apart. MEASURED, both objects in one execution, delta measured after construction rather than declared, over 128 bones. The quaternion-product norm is FLAT to five figures across angles 0.1 to 3.0 — 1.066054 to 1.066059 — while the matrix chain at the same delta runs 1.000641 to 1.289770. The reviewer's theorem is exact (Euler's four-square identity, verified over 200000 random pairs against this repo's own Quat.mul at 7.6e-16) and their arithmetic is right at every step. It governs Quat.mul, which src/modules/kinesis/ calls ZERO times. forwardKinematics composes Mat4, and Mat4.fromTrs writes an unscaled 1 on each diagonal term while every off-diagonal term carries the full |q|^2 — so M(k*u) = k^2*R(u) + (1-k^2)*I, an affine mix with the identity, verified elementwise to 1 ULP. The decisive cell is theta = 0: delta fully present, stretch exactly 1.000000, where a law independent of angle demands 1.066. BUT THE OBJECTION WAS RIGHT ABOUT THE SENTENCE IT ATTACKED. "Tightening the band would not have closed it" is FALSE — a non-sequitur, since the drift is the product n*delta*(1-cos theta) and a product closes by shrinking any factor; 100x tighter takes the 128-bone stretch from 8.5% to 0.08%. What is true and stronger is that the band is defeated by DEPTH: max_bones is 4096, and at the band's own edge such a chain stretches 13.6x at 1.2 rad and 3583x at pi. Both false sentences are deleted here, not annotated. AND THE FIXTURE WAS ADMISSIBLE BY ONE ULP AND BY LUCK — a second defect the objection's wrong number nevertheless landed on. k = sqrt(1 + tolerance) names a length the band REFUSES: 1 + 1e-3 is unrepresentable in f32 and rounds up to 8389 ULP where the predicate admits 8388. It loaded only because five roundings happened to land one ULP low at that angle; the identical construction is refused at 15.2% of angles in (0,3], non-monotonically — 1.80 rad refused between an accepted 1.50 and an accepted 2.00. Repaired, and the attack is STRONGER: 7550 ULP under the 8388 ceiling against a 2 ULP construction spread, 0% of angles refused, 3.0 rad instead of 1.2, unnormalised stretch 1.257397 instead of 1.085067, floor 1.05 -> 1.2. The 0.9 factor is written as a rule (factor <= 1 - N*2^-23/tol) and not as a number, because it stops working at a tighter tolerance. rotationIsUnitEnough is extracted from validatePayload as the ONE declarant of the band arithmetic, whose summation order decides the last ULP. The in-test assertion is TWO-SIDED, since "inside the band" alone is satisfied by a unit quaternion under which the test is vacuous — counter-factual: forcing k = 1.0 aborts it. Mat4.fromTrs now declares the unit precondition its twins Mat3.fromQuat and Quat.rotateVec3 both declare and it did not, with the consequence rather than the bare word: the mix is anisotropic, so no scale factor describes it. Reported and NOT fixed here, verified on the source in both directions: body_manager.setRotation stores a rotation raw, reachable through the FROZEN setBodyTransform and through sync_in.zig's per-tick ECS seam, while addBody normalises and integration.zig skips exactly the .gameplay bodies syncIn writes to. Forge, frozen surface, needs an M1.D number and an owner. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Dedicated CI hotfix branch, before M1.2.1: it touches only ci.yml and is validated by its own runs. The diagnosis, the four-point arbitration and the two refuted hypotheses are recorded before any edit lands. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
THE CAUSE IS IN THE KEY. All three cache keys carry `${{ github.sha }}`, so no
save is ever overwritten: each commit creates two fresh archives per cell, and
restoration is necessarily a prefix match on the previous lineage. Growth is
monotone in the number of commits pushed — two entries x fifteen cells per run,
eight of them windows at 1-2 GB — against GitHub's repository-wide 10 GB
ceiling. The ceiling is not brushed by accident, it is reached BY CONSTRUCTION
in a handful of commits, after which LRU eviction takes over.
MEASURED on run 34777902366 at `5d79846`. Both f64 windows cells were CANCELLED
with every step green, `Complete job` included — 57 min against a 55 budget and
21 min against a 20 — so neither is M1.D.19 (which loses tests and prints `test
runner failed to respond`) nor M1.D.29 (an assertion genuinely falling). A
re-run at the SAME sha returned 9.6 min and 5.6 min, the only difference being
that the first attempt's save had run; the restore keys attribute it exactly,
`e60dda8...` then `4ad6487...`. And `ReleaseSafe / f32` took 52.1 min TWICE, its
own attempt-1 save having been evicted meanwhile by the other cells of the same
run. The cells evict each other INSIDE one run. Repository cache read live:
11.55 GB / 13 entries, then 9.44 GB / 11 a few minutes later, with the four
windows cells holding 8.44 GB of 9.44 — 89 % of the budget for 4 of 15 cells.
So this is not a slow CI, it is a NON-DETERMINISTIC one: the same sha passes or
fails on what eviction did meanwhile, and the re-run cleared it by luck.
POINT 1 — the `-build` save is deleted, with its now-dead measurement step
(exactly one consumer). Its unique window was a job killed between it and the
final save, i.e. the timeout, which saturation causes: it existed to survive an
ill it fed. This file already recorded it killing a cell outright —
`Save Zig cache (post-build)` measured going 39s -> 5m23s -> 7m39s on
windows-2025 / Debug while the useful work stayed flat, and that leg died at its
ceiling twice. The all-or-nothing doctrine was MOVED rather than deleted: it
lived in the removed block and the surviving block referenced it.
POINT 2 — 55 -> 75 and 20 -> 35, and the reason is not comfort. A budget's job
is to separate SLOW from HUNG, and at 55 against a cold cell measured at 52-57
it separated nothing: it turned a known slowness into a random failure and made
M1.D.19 indistinguishable from M1.D.27. The Debug budget was the tighter of the
two in proportion — 21 measured against 20 allocated — and no debt entry had
ever named it. The superseded "a cold Debug leg is ~11 min of work, ~45 % inside
the 20-min budget" is removed rather than left beside its correction.
TWO HYPOTHESES REFUTED, one per party. That the milestone had outgrown its
budget: the PREVIOUS run carried all three hotfix commits and both new tests and
did the same cell in 10.6 min — two tests do not cost 46 minutes. And mine, that
M1.2.0/G1 widened the blast radius by making `type_info.zig` import
`foundation`: measured, `command_buffer.zig` and `scheduler.zig` already did
before the milestone and `core_module.addImport("foundation")` dates from
M1.1.14, so breaking that one dependency would change nothing.
NOT IN THIS PUSH: saving only from `main`, whose cost is not known in advance
and which owes a before/after measurement on the same branch. And splitting the
cell by test domain is REFUSED for now — it is the only lever that aggravates
the root cause, since splitting multiplies cache entries.
Detail: `briefs/ci-hf-cache-saturation.md`.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
CLAUDE.md's M1.1.1-HF3 row says the class was instrumented and that the first recurrence would self-name via the dump. It did, on this branch's own CI, run 34795835386 attempt 1, windows-2025 / ReleaseSafe / f64 — and did not reproduce at the same sha. Written here because a scratchpad does not survive the session and this is the only open PR; a repository file is the only durable place available. It is NOT this hotfix's subject. The dump establishes that the work was entirely drained (pending_count 0, the 13 chunks all consumed, so 2882 failed steals for 0 successes is correct), that no worker entered a park (parks_entered 0 on all four) when all of them should have, and that the test could not exit: its phase (a) loops until sum(parks_entered) > sum(parks_completed), false forever at 0 > 0. It does NOT establish why, and that is deliberately not guessed at. The question it makes answerable by reading code: what does a worker do after an unsuccessful steal, and under what condition does it enter a park. Tier 0, and its owner's. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Dedicated CI hotfix, before M1.2.1. Touches
.github/workflows/ci.ymlandnothing else, and is validated by its own runs — which is why it is not folded
into an animation milestone's review.
The cause is in the key
All three cache keys carry
${{ github.sha }}, so no save is everoverwritten: each commit creates two fresh archives per cell and restoration is
necessarily a prefix match on the previous lineage. Growth is monotone in the
number of commits pushed — two entries × fifteen cells per run, eight of them
windows at 1–2 GB — against GitHub's repository-wide 10 GB ceiling. It is not
brushed by accident; it is reached by construction in a handful of commits, and
LRU eviction takes over.
Measured, run 34777902366 at
5d79846:windows-2025, ReleaseSafe, f64windows-2025, Debug, f64windows-2025, ReleaseSafe, f32Every step of both cancelled cells succeeded,
Complete jobincluded — soneither is
M1.D.19(tests LOST,test runner failed to respond) norM1.D.29(an assertion falling). Restore keys attribute it exactly:e60dda8…then
4ad6487…. Thef32cell passed on attempt 1 and restored the stalelineage on attempt 2, its own save having been evicted meanwhile — the cells
evict each other inside one run. Repository cache read live: 11.55 GB / 13
entries, then 9.44 GB / 11 a few minutes later; the four windows cells held
8.44 GB of 9.44, 89 % of the budget for 4 of 15 cells.
So this is not a slow CI, it is a non-deterministic one. The re-run cleared
it by luck.
What lands here
Point 1 — the
-buildsave is deleted, with its now-dead measurement step(exactly one consumer). Measured: the inherited
-buildentries hold 3.69 GBtoday. Its unique window was a job killed between it and the final save — the
timeout, which saturation causes: it existed to survive an ill it fed. And
ci.ymlalready recorded it killing a cell outright,Save Zig cache (post-build)going 39s → 5m23s → 7m39s onwindows-2025 / Debugwhile theuseful work stayed flat. The all-or-nothing doctrine was moved, not deleted.
Point 2 — 55 → 75 and 20 → 35. Not comfort: a budget's job is to separate
SLOW from HUNG, and at 55 against a cold cell measured at 52–57 it separated
nothing. The Debug budget was the tighter of the two in proportion — 21 measured
against 20 allocated — and no debt entry had ever named it.
Two hypotheses refuted, one per party
That the milestone had outgrown its budget: the previous run carried all
three hotfix commits and both new tests and did the same cell in 10.6 min —
two tests do not cost 46 minutes. And that
M1.2.0/G1widened the blast radiusvia
type_info.zigimportingfoundation: measured,command_buffer.zigandscheduler.zigalready did before the milestone andcore_module.addImport("foundation")dates from M1.1.14 — breaking that onedependency would change nothing.
Not in this push
Saving only from
main(point 3) — the gesture that closes the cause ratherthan the symptom, but the only one whose cost is not known in advance; it owes a
before/after measurement on the same branch.
Splitting the cell by test domain is refused for now — the tidiest-looking
lever and the only one that aggravates the root cause, since splitting
multiplies cache entries.
How to read this run
The volume effect is partly masked: the inherited
-buildarchives survive untilLRU retires them. What is immediately readable is that this run writes one
entry per cell instead of two, and that no cell is cancelled with every step
green.
Detail:
briefs/ci-hf-cache-saturation.md.M1.D.27is mis-diagnosed by its ownentry — cause, not number — and Guy rewrites it under the same number.
🤖 Generated with Claude Code