Skip to content

Commit 2381085

Browse files
fix(test-shards): judge pin 3c's slice count through a symmetric 5% band (#30138)
<!-- claude-projects-attribution: {"github_login":"marchtian"} --> _Requested by **田騏** · [project thread](https://claude.ai/code/project/chan_01VmuUhXU9SSUzqJYyXSmYSi?thread=cmsg_01VmuUhXU9SSUzqJYyXSmYSiNjyvCdXYygFqntMNR7S2dW)_ Fixes #29826 Clause-②: no Metadata-protocol: none ## 维护者速读 - 改了什么:分片切片数校验(pin 3c)由"只认恰好一个切片数"改为两侧对称 5% 滞回带,一个常量 `SLICE_COUNT_HYSTERESIS = 0.05`。 - 为什么改:CLI 测试时长的周间噪声(1339–1493 s)远大于 20 s 余量,每周自动刷新的计时数据 PR 会随机变红,需席位手工开车道 PR 才能落地。 - 风险与回滚:只改自测接受哪个手写切片数;任何一次 CI 运行的切法、密度上限、整包腿上限都不读它。回滚即 revert 本 PR。 - 先看:`scripts/partition-test-shards.mjs` 的 `sliceCountProblems()` 两个判定分支,其次是自测新增的 4 个带内/带外夹具。 - 两分钟验证:`node scripts/partition-test-shards.mjs --self-test`,预期打印 `self-test OK ... file-level slices: @objectstack/cli x2`。 - 需要你做的:无,席位落地。 ## What changes Option D as ruled in comment 6634084947 on the card. One file: `scripts/partition-test-shards.mjs`. - New `SLICE_COUNT_HYSTERESIS = 0.05`, commented as the observed noise width of the CLI median. - `sliceCountProblems()` (pin 3c) judges both halves through the band. RAISE refuses only when `weight / n > (1 + h) x limit`. LOWER (or Retire) refuses only when `weight / (n - 1)` is at or under `(1 - h) x limit`. A count inside the band passes. Each refusal names the slice, the limit and the band edge, to two decimals. The count a RAISE names is still the smallest one that meets the bare limit, so a raised count lands with margin. - `planShards()` (NEEDED / SPREADS / WITHIN CAP), the density cap and pin 3d are byte-for-byte unchanged. - Self-test: 4 new pin 3c fixtures on the existing 50 s-filler fixture (limit 65 s, band 61.75 to 68.25 s). Two are in-band passes: RAISE side at 134 s / 2, LOWER side at 125 s / 3. Each is also run with no band, and there it must be refused, so the pass is proven to come from the band. Two are out-of-band refusals beside the edges: 138 s / 2 must say `Raise it to 3` with `one slice is 69.00s`, and 122 s / 3 must say `Lower it to 2` with `one slice (61.00s)`. The balancing-pins battery floor goes from 25 to 29; it now registers 30 cases. - The `FILE_SHARDED_PACKAGES` "RE-DERIVED AT 2" paragraph gains one sentence saying why the band exists. ## Tests - `node scripts/partition-test-shards.mjs --self-test`: exit 0 on head `1c0a83b762`. - The four existing fixtures keep their verdicts at h = 0.05: PASS (120 / 2), Retire the entry (60 / 2), Lower it to 2 (120 / 3), Raise it to 5 (300 / 2). - Ablations, each through `scripts/ablation-replace.mjs`, restored to the HEAD blob with an empty `git diff HEAD`: <details><summary>Ablation readings (self-test exit code and first failing case)</summary> | Mutation | Exit | First failure | |---|---|---| | `SLICE_COUNT_HYSTERESIS = 0` | 1 | a count of 2 inside the RAISE band was not passed by the band alone | | LOWER branch judged against the bare `limit` | 1 | a count of 3 inside the LOWER band was not passed by the band alone | | RAISE branch judged against the bare `limit` | 1 | a count of 2 inside the RAISE band was not passed by the band alone | | `SLICE_COUNT_HYSTERESIS = 0.07` | 1 | a count of 2 past the RAISE band was accepted | | `SLICE_COUNT_HYSTERESIS = 0.1` | 1 | a package that is not the floor kept its slicing with no refusal (an existing fixture) | | battery floor 29 to 99 | 1 | battery "the balancing pins (#10472)" registered 30 case(s), below its pinned floor of 99 | </details> ## Acceptance notes - Thresholds re-measured on the committed dataset (CLI 1432.04 s; floor `@objectstack/metadata-protocol` 566.50 s; limit 736.45 s). I called the exported `sliceCountProblems()` on the dataset binned the way the self-test bins it, varied only the CLI weight on a 0.01 s grid, and they agree with the ruling's figures. Map 2 is accepted for CLI 699.63 to 1546.54 s (ruling: up to 1546.5). Map 3 is accepted for CLI 1399.26 to 2319.81 s (ruling: over 1399.3 to 2319.8). - Card bullet 1: the dataset with the map at 2 passes, and so does CLI at 1480 s with the map at 2 (also 1493 s, the measured high run). - Card bullet 2: a drift past the band still refuses with the numbers. At CLI 1546.6 s with map 2: `one slice is 773.30s -- past 1.3x ... (567s, so 736.45s) by more than its 5% band (773.27s) ... Raise it to 3.` At CLI 1399.2 s with map 3: `at 2 one slice (699.60s) ... with more than its 5% band to spare (699.63s). Lower it to 2`. - Floor-side reading by the same formula: with the CLI at 1432.04 s, map 2 holds while the floor stays at or above 524.56 s. - The nominal RAISE bound in the self-test moves from 1.3x to 1.365x. The maintainer ruled on that tradeoff (option D). No run's split reads it. ## Verification All 31 commands that `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` derives for this diff exited 0. The exit codes were captured before any pipe, and `--ran` reconciliation reports `31 derived, 31 run, 0 NOT-MEASURED, 0 UNRUN` (a derived zero: all 31 recorded an exit code). `pnpm check:pm-dispatch-gates` (fast tier, 1945 cases) also exited 0. The CI-only `Plan Test Core shards` step and the type-check lanes are not measured locally; CI runs them. `skip-changeset`: the diff touches only root `scripts/`, which no package publishes. `origin/main` was merged once before opening. Session: https://claude.ai/code/session_017ihL2ebbZkr2QAEG1x7UC8 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 2168447 commit 2381085

1 file changed

Lines changed: 89 additions & 13 deletions

File tree

‎scripts/partition-test-shards.mjs‎

Lines changed: 89 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,16 @@ export const SHARD_COUNT = liveShardTotal('test');
172172
// numerator up.
173173
export const MAX_SHARD_OVER_MEAN = 1.3;
174174

175+
// The band pin 3c judges a configured slice count against, as a fraction of
176+
// its limit, on both halves (ruling record 6634084947; see
177+
// sliceCountProblems()). It is the observed noise width of the CLI's median: a
178+
// refresh moves that median by a few percent with no change to the suite, and
179+
// a pin that accepts exactly one count flips on that noise -- every flip reds
180+
// the generated refresh PR until a hand-written lane PR changes this map. It
181+
// bounds only which hand-written count the self-test accepts; no run's split,
182+
// density cap or whole-leg cap reads it.
183+
export const SLICE_COUNT_HYSTERESIS = 0.05;
184+
175185
// The factor a shard's MEASURED test total may exceed its PREDICTED total by
176186
// before `--check-drift` names it DRIFT -- a `::warning::`, never a red (see
177187
// "TIMING IS A READING" below). The durable half of #16173.
@@ -388,7 +398,10 @@ export function timingModeNote(event) {
388398
// n = 1 1432.04s 2.53x the CLI IS the run's floor
389399
// n = 2 716.02s 1.26x meets -- metadata-protocol is the floor
390400
//
391-
// The margin is 20s (finding #29826). The whole split is now balanced, so the
401+
// The margin is 20s (finding #29826), inside the CLI's own run-to-run spread
402+
// (1339-1493s), so pin 3c judges both halves against a 5% band
403+
// (SLICE_COUNT_HYSTERESIS): this count holds up to 1546s, and a raised count of
404+
// 3 would hold down to 1399s. The whole split is now balanced, so the
392405
// density cap equals the 8-shard mean; the slice that reads built declarations
393406
// is admitted by closureAllowance() (#29777 ruling B).
394407
//
@@ -845,6 +858,9 @@ export function assertSlicesSpread(bins) {
845858
// SMALLEST at n - 1 it is not -- otherwise the slices past n - 1 cost a
846859
// turbo leg and a closure build each, and buy nothing.
847860
//
861+
// Each half is read through the SLICE_COUNT_HYSTERESIS band, so a count is
862+
// refused only once the dataset is past that band on the side it names.
863+
//
848864
// The FILE_SHARDED_PACKAGES docblock has the arithmetic on today's dataset.
849865
// Why the serial floor and not the bins: slicing redistributes weight and never
850866
// moves the mean, so a bin-sum bound reads the same at every n -- it is the
@@ -856,10 +872,21 @@ export function assertSlicesSpread(bins) {
856872
// a count below 2 -- 1 is no slicing at all, and 0 would make expandSlices emit
857873
// NO item for the package, a package no shard runs.
858874
//
875+
// Both halves judge against a band of SLICE_COUNT_HYSTERESIS around the limit
876+
// (ruling record 6634084947), not the limit itself: RAISE refuses only a slice
877+
// past (1 + h) x limit, LOWER only an n - 1 slice at or under (1 - h) x limit,
878+
// and a count inside the band passes. The count a RAISE names is still the
879+
// smallest that meets the bare limit, so a raised count lands with margin.
880+
//
859881
// `packages` is whole-package `{name, weight, tasks?}`. `judged` is returned
860882
// beside the problems so a caller can tell "every sliced package is derived"
861883
// apart from "no package was looked at".
862-
export function sliceCountProblems(packages, sliced = FILE_SHARDED_PACKAGES, bound = MAX_SHARD_OVER_MEAN) {
884+
export function sliceCountProblems(
885+
packages,
886+
sliced = FILE_SHARDED_PACKAGES,
887+
bound = MAX_SHARD_OVER_MEAN,
888+
hysteresis = SLICE_COUNT_HYSTERESIS
889+
) {
863890
const weights = new Map(packages.map((p) => [p.name, p.weight]));
864891
const problems = [];
865892
let judged = 0;
@@ -885,20 +912,25 @@ export function sliceCountProblems(packages, sliced = FILE_SHARDED_PACKAGES, bou
885912
);
886913
const floor = Math.max(0, ...others.map(serialFloorOf));
887914
const limit = bound * floor;
915+
const raiseAbove = (1 + hysteresis) * limit;
916+
const lowerAtOrBelow = (1 - hysteresis) * limit;
917+
const band = `${+(hysteresis * 100).toFixed(2)}%`;
888918
const meets = (count) => weight / count <= limit;
889-
if (!meets(n)) {
919+
if (weight / n > raiseAbove) {
890920
let enough = n + 1;
891921
while (!meets(enough)) enough++;
892922
problems.push(
893-
`${name}: sliced ${n} ways at ${weight}s, one slice is ${(weight / n).toFixed(0)}s -- past ${bound}x ` +
894-
`the heaviest other serial task (${floor.toFixed(0)}s, so ${limit.toFixed(0)}s), so the package is still ` +
895-
`the list's serial floor on its own. Raise it to ${enough}.`
923+
`${name}: sliced ${n} ways at ${weight}s, one slice is ${(weight / n).toFixed(2)}s -- past ${bound}x ` +
924+
`the heaviest other serial task (${floor.toFixed(0)}s, so ${limit.toFixed(2)}s) by more than its ${band} ` +
925+
`band (${raiseAbove.toFixed(2)}s), so the package is still the list's serial floor on its own. ` +
926+
`Raise it to ${enough}.`
896927
);
897-
} else if (meets(n - 1)) {
928+
} else if (weight / (n - 1) <= lowerAtOrBelow) {
898929
problems.push(
899-
`${name}: sliced ${n} ways at ${weight}s, but at ${n - 1} one slice (${(weight / (n - 1)).toFixed(0)}s) ` +
930+
`${name}: sliced ${n} ways at ${weight}s, but at ${n - 1} one slice (${(weight / (n - 1)).toFixed(2)}s) ` +
900931
`already sits within ${bound}x the heaviest other serial task (${floor.toFixed(0)}s, so ` +
901-
`${limit.toFixed(0)}s). ${n - 1 === 1 ? 'Retire the entry' : `Lower it to ${n - 1}`}: ` +
932+
`${limit.toFixed(2)}s) with more than its ${band} band to spare (${lowerAtOrBelow.toFixed(2)}s). ` +
933+
`${n - 1 === 1 ? 'Retire the entry' : `Lower it to ${n - 1}`}: ` +
902934
'a slice count a smaller one could replace is one no pin can hold.'
903935
);
904936
}
@@ -1884,10 +1916,11 @@ const SELF_TEST_BATTERIES = Object.freeze({
18841916
// testing anything. Flooring at the measured count makes that loud, and the
18851917
// remedy is the one the pin itself names -- pick a new inversion pair from
18861918
// the dataset -- never lowering this number.
1887-
// 8 of these 25 are pins 3b and 3c, the file-level slicing's (#16173), and
1919+
// 12 of these 29 are pins 3b and 3c, the file-level slicing's (#16173), and
18881920
// every one of them runs unconditionally: 3c's fixtures and 3b's cut of the
1889-
// heaviest package do not depend on what the live map slices.
1890-
'the balancing pins (#10472)': 25,
1921+
// heaviest package do not depend on what the live map slices. 4 of the 12 are
1922+
// 3c's band fixtures (SLICE_COUNT_HYSTERESIS): 2 in-band passes, 2 refusals.
1923+
'the balancing pins (#10472)': 29,
18911924
// 1 grades the committed dataset's split; 4 are fixtures, both directions.
18921925
'the whole-package leg cap and the load-sensitive pair (#25870)': 5,
18931926
// 4 grade the committed dataset and the live tree, 1 drives planRun() on
@@ -2205,7 +2238,8 @@ function selfTest() {
22052238
// and a count nothing can fail on is how a slicing outlives its reason.
22062239
// sliceCountProblems() asks both halves of the serial-floor derivation
22072240
// (see FILE_SHARDED_PACKAGES) of the committed dataset for every entry:
2208-
// the count meets it, and n - 1 does not.
2241+
// the count meets it, and n - 1 does not, each read through the
2242+
// SLICE_COUNT_HYSTERESIS band.
22092243
//
22102244
// The fixtures after it hold every refusal sliceCountProblems() makes, in
22112245
// both directions, on numbers that do not move with a refresh.
@@ -2253,6 +2287,48 @@ function selfTest() {
22532287
throw new Error(`slice derivation: a count whose slice is still the floor was accepted (${r.problems.join(' | ') || 'no problem named'})`);
22542288
}
22552289
});
2290+
// The SLICE_COUNT_HYSTERESIS band on the same fixture: 5% of 65s puts RAISE
2291+
// past 68.25s and LOWER at or under 61.75s. Each in-band pass is also run
2292+
// with no band, where it must be refused -- otherwise it sits inside the bare
2293+
// limit and proves nothing about the band. Each out-of-band refusal sits
2294+
// within a second of the band's edge and must carry its measured slice.
2295+
const withoutBand = (packages, sliced) => sliceCountProblems(packages, sliced, MAX_SHARD_OVER_MEAN, 0);
2296+
check(() => {
2297+
// RAISE side, in band: 134s in two is 67s, past 65s but within 68.25s.
2298+
const r = sliceCountProblems(fixture(134), { big: 2 });
2299+
const bare = withoutBand(fixture(134), { big: 2 });
2300+
if (r.problems.length > 0 || !bare.problems.some((m) => m.includes('Raise it to 3'))) {
2301+
throw new Error(
2302+
`slice derivation: a count of 2 inside the RAISE band was not passed by the band alone ` +
2303+
`(band: ${r.problems.join(' | ') || 'ok'}; bare: ${bare.problems.join(' | ') || 'ok'})`
2304+
);
2305+
}
2306+
});
2307+
check(() => {
2308+
// LOWER side, in band: 125s in two is 62.5s, within 65s but above 61.75s.
2309+
const r = sliceCountProblems(fixture(125), { big: 3 });
2310+
const bare = withoutBand(fixture(125), { big: 3 });
2311+
if (r.problems.length > 0 || !bare.problems.some((m) => m.includes('Lower it to 2'))) {
2312+
throw new Error(
2313+
`slice derivation: a count of 3 inside the LOWER band was not passed by the band alone ` +
2314+
`(band: ${r.problems.join(' | ') || 'ok'}; bare: ${bare.problems.join(' | ') || 'ok'})`
2315+
);
2316+
}
2317+
});
2318+
check(() => {
2319+
// RAISE side, past the band: 138s in two is 69s, past 68.25s.
2320+
const r = sliceCountProblems(fixture(138), { big: 2 });
2321+
if (!r.problems.some((m) => m.includes('Raise it to 3') && m.includes('one slice is 69.00s'))) {
2322+
throw new Error(`slice derivation: a count of 2 past the RAISE band was accepted (${r.problems.join(' | ') || 'no problem named'})`);
2323+
}
2324+
});
2325+
check(() => {
2326+
// LOWER side, past the band: 122s in two is 61s, at or under 61.75s.
2327+
const r = sliceCountProblems(fixture(122), { big: 3 });
2328+
if (!r.problems.some((m) => m.includes('Lower it to 2') && m.includes('one slice (61.00s)'))) {
2329+
throw new Error(`slice derivation: a count of 3 past the LOWER band was accepted (${r.problems.join(' | ') || 'no problem named'})`);
2330+
}
2331+
});
22562332
check(() => {
22572333
// The two-task half: a 100s package that runs as `test` + `test:repo` puts
22582334
// 50s on the path, not 100s -- so beside it a 120s package IS the floor and

0 commit comments

Comments
 (0)