Skip to content

Commit 3cdb162

Browse files
ci(test-shards): the density cap carries the declaration closure's own displacement when a split carries the reader slice (#29838)
<!-- claude-projects-attribution: {"github_login":"marchtian"} --> _Requested by **田騏** · [project thread](https://claude.ai/code/project/chan_01VmuUhXU9SSUzqJYyXSmYSi?thread=cmsg_01VmuUhXU9SSUzqJYyXSmYSi9paTx1Lu8ENYwwssthfK3h)_ Fixes #29777 Clause-②: no Metadata-protocol: none ## 维护者速读 - 改了什么:Test Core 分片器判断「这次要不要把 CLI 切片」时,若切出的方案带着声明构建那一片,密度上限加上闭包计费分摊到其余分片的那一份(783 秒 ÷ 7 ≈ 112 秒);不带那一片时上限不变。 - 为什么改:每周分片计时刷新被分片器自检拒收。新数据里 CLI 降到 1432 秒、低于平均,原上限等于平均值,而闭包计费必然把其余分片推高约 7%,于是任何切法都过不了,全量运行只能把 CLI 整包串行跑(约 24 分钟,其余分片约 8 分钟)。 - 风险与回滚:只动一个仓根脚本,不发布任何包;今天的已提交数据切分结果逐字节不变。回滚即 revert 本 PR,影响仅是刷新 PR 再次被拒。 - 先看:`scripts/partition-test-shards.mjs` 的 `closureAllowance()` 与 `planShards()` 里 WITHIN CAP 那几行;再看自检新增的四个「declaration-closure allowance」用例。 - 两分钟验证:`node scripts/partition-test-shards.mjs --self-test`,应见 `self-test OK`,且 bins 仍为 1600.11/1597.48/1597.14/1599.09/1598.01/1597.78/1599.48/1597.04s。 - 需要你做的:无,席位落地。 ## What changes `scripts/partition-test-shards.mjs` only, implementing maintainer ruling record 6596963071 (option B, the first of its two slices): - **`closureAllowance(items, shardCount)`** (new): `DECLARATION_CLOSURE_CHARGE_SECONDS / (shardCount - 1)` when the items carry the declaration-reader slice's closure charge, `0` otherwise and on a one-shard run. Its docblock carries the measured reason. The charge (783 s of packing weight) moves that much whole-package work onto the other shards. So when the whole-package split is balanced, the density cap (its densest bin) is the mean, and no split carrying the charge can come back within it at any slice count. On the 2026-11-02 refresh the cap read 1455.81 s against 1552.68 / 1552.77 / 1552.63 s for the CLI cut 2 / 3 / 4 ways, so the full list ran the CLI whole. - **`planShards()`**: the WITHIN CAP check, and the repair placement behind it (`placeItems` / `refineToCap`), judge a candidate split against `cap + closureAllowance(candidate)`. The final placement uses the same. Each decision records its `allowance`. - **`renderSliceDecision()`**: names the allowance only when it was part of the verdict (a refusal past it, or a slicing only it admitted), so the shard log never claims a cap the split did not meet. Today's lines are unchanged. - **Self-test**: the hard constraint (the committed full list stays within the cap) is scored against the same adjusted cap. The success line names the allowance only when the bins exceed the bare cap. Unchanged, as ruled: the pin "cap = the whole split's densest bin" and the `capFixture` refusal/admission controls. - **New pins (4)**, in the `density cap and the per-run slice decision` battery, whose floor is raised 14 to 18. They are built on the refresh's package weights **frozen in the test** (79 packages from PR 28978 head `b5b4b790`, not read from the dataset file), at 8 shards: - the allowance's arithmetic: carried at 8, plain at 8, carried at 1; - carried: the CLI x2 is taken, cap 1455.81 s + allowance 111.86 s = **1567.67 s**, densest shard 1553.36 s (past the bare cap, within the adjusted one), slices spread, and the log names the allowance; - not carried: the same shape with no reader slice is allowed 0 s; - still binding: a cap one second short of admitting the carried split refuses it, naming the allowance. Not changed, as ruled: `scripts/test-shard-timings.json`, `FILE_SHARDED_PACKAGES` / `PREVIOUS_FILE_SHARDED_PACKAGES`, `ci.yml`'s `DTS_READER_SLICE` and the CLI vitest config's `DECLARATION_READER_SLICE`. The dataset and map swap (CLI 4 to 2, previous = 4) is the ruling's second slice, a separate lane PR that replaces PR 28978. ## Tests Measured on `objectstack-ai/objectstack` at head `e9569e920e` (merge base `b0ef25f524`). - `node scripts/partition-test-shards.mjs --self-test`, committed dataset: exit 0, and the verdict line is byte-identical to the base's. - **Byte-identity on today's committed dataset (base vs head).** I ran main() for every shard `1/8`..`8/8` on a full-list document with `--exclude @objectstack/dogfood`: stdout and stderr are byte-identical on all 8. In the plan JSON (`planShards` on the full list, as the self-test builds it), `items`, `bins` (1600.11/1597.48/1597.14/1599.09/1598.01/1597.78/1599.48/1597.04) and `cap` (2697.45) are identical. The one difference is the added decision key `"allowance": 111.85714285714286`. - **PR 28978's refreshed dataset (`b5b4b790`), scratch tree, not committed.** At head the self-test is red on one check only: pin 3c, "sliced 4 ways ... Lower it to 3". That is the map swap the second slice carries. At base it was red on 7, the first being `slice spread`. With the second slice's map applied in scratch (CLI 2, previous 4): exit 0, `bins 1551.96/1551.44/1553.29/1552.46/1552.43/1553.22/1551.01/1553.36s within the 1455.81s density cap plus its 111.86s declaration-closure allowance`, whole legs within 1868 s, CLI x2. - **Ablation** (scratch tree, `closureAllowance` mutated to `return 0`, marker grep-confirmed): exit 1. With failures collected, the three cases that depend on the allowance go red and the "not carried" case stays green. Restored by byte comparison; the restored self-test exits 0. - Importers' self-tests: `nightly-tiers`, `check-test-suite-ceilings`, `measure-test-shard-timings`, `ci/select-shard-timings-run`, `ci-shard-matrix`, `check-test-completeness` all exit 0. ## Acceptance notes - The adjusted cap leaves a 14 s margin on the refreshed shape (1553.36 s against 1567.67 s). - Pin 3c has no hysteresis: x2 holds on the refreshed dataset by 20 s (716 s against 736.45 s), so a later refresh can flip the derived count again. The ruling places that outside its scope, as its own finding. - No changeset: a root script publishes nothing, so `skip-changeset`. ## Verification <details><summary>Derived gates: 31 of 31 exit 0, reconciled with --ran</summary> `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` at `e9569e920e` derived 31 commands (17 pnpm, 14 node). All 31 were run with exit codes captured before any pipe, after `pnpm install --frozen-lockfile`. A first pass without `node_modules` gave PREREQUISITE NOT MET (exit 3) on 6 commands and 1 environment finding; all reran green after the install. `pnpm check:pm-dispatch-gates` exit 0 (fast tier, 1945 cases). `--ran`: "31 derived famil(ies) accounted for, 31 run, 0 NOT-MEASURED (a DERIVED zero, all 31 recorded an exit code and none of them is 3)". The derivation names one path-scheduled CI step (`Plan Test Core shards`) and the type-check lanes as NOT MEASURED locally; CI runs those. </details> Session: https://claude.ai/code/session_017ihL2gLghEqeDKTbDTC7Zd 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 6fc7872 commit 3cdb162

1 file changed

Lines changed: 168 additions & 15 deletions

File tree

‎scripts/partition-test-shards.mjs‎

Lines changed: 168 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1186,7 +1186,9 @@ export function balanceOf(bins, items = null) {
11861186
//
11871187
// ⛔ Density is never traded for balance. planShards() takes a slicing only
11881188
// when the sliced split keeps every shard within this cap, and the self-test
1189-
// pins the committed dataset's full list, split as CI splits it, within it.
1189+
// pins the committed dataset's full list, split as CI splits it, within it --
1190+
// both judged with the declaration-closure allowance below (closureAllowance())
1191+
// when the split carries the reader slice, and with no allowance otherwise.
11901192
//
11911193
// `packages` is whole-package `{name, weight}`, CI's exclusions already applied.
11921194
export function densityCap(packages, shardCount = SHARD_COUNT) {
@@ -1269,6 +1271,36 @@ export function refineToCap(bins, items, cap) {
12691271
return out;
12701272
}
12711273

1274+
// ── THE CAP'S DECLARATION-CLOSURE ALLOWANCE (ruling record 6596963071) ─────
1275+
//
1276+
// The reader slice's closure charge (DECLARATION_CLOSURE_CHARGE_SECONDS) is
1277+
// packing weight, and LPT spends it by moving that much whole-package work off
1278+
// the reader slice's shard onto the others. The density cap above is the densest
1279+
// bin of a split that carries no such charge, so when that split is balanced --
1280+
// the sliced package sits below the mean, and the cap IS the mean -- no split
1281+
// carrying the charge can come back within it, at any slice count: the charge
1282+
// alone lifts the other shards by about charge / (shards - 1). Measured on the
1283+
// 2026-11-02 refresh at 8 shards (the CLI 1432.04s, the mean 1455.81s): the
1284+
// cap read 1455.81s, and the CLI cut 2, 3 and 4 ways put its densest shard at
1285+
// 1552.68s / 1552.77s / 1552.63s, so the full list ran the CLI whole, a 1432s
1286+
// serial task, against ~500-570s on every other shard. On every dataset before
1287+
// it the cap was the CLI's own whole bin, which no slicing approaches, so the
1288+
// charge and the cap had never met.
1289+
//
1290+
// So a candidate split that CARRIES the reader slice is judged against the cap
1291+
// plus that displacement: DECLARATION_CLOSURE_CHARGE_SECONDS / (shardCount - 1),
1292+
// 111.86s at 8 shards. It is the charge's own arithmetic -- the density the
1293+
// closure pin already chose to move -- and nothing else: a split with no reader
1294+
// slice (the package whole, a run without it, a document that states its own
1295+
// slicing) is judged against the bare cap, and a split denser than the cap plus
1296+
// the charge's share is still refused, by name (renderSliceDecision()). A
1297+
// one-shard run displaces nothing and is allowed nothing.
1298+
export function closureAllowance(items, shardCount) {
1299+
return shardCount > 1 && items.some((i) => i.closure !== undefined)
1300+
? DECLARATION_CLOSURE_CHARGE_SECONDS / (shardCount - 1)
1301+
: 0;
1302+
}
1303+
12721304
// The split a run actually runs: partition(), and -- only when the split
12731305
// carries a slice -- refineToCap() against the density cap. A split with no
12741306
// slice is the plain LPT split it always was, byte for byte.
@@ -1291,7 +1323,9 @@ export function placeItems(items, shardCount, cap = Infinity) {
12911323
// SPREADS its slices land on distinct shards at this run's shard count,
12921324
// which is what makes them run in parallel at all;
12931325
// WITHIN CAP the sliced split keeps every shard within the density cap
1294-
// (densityCap), repaired by refineToCap() where LPT overshoots.
1326+
// (densityCap), plus closureAllowance() when the split carries
1327+
// the reader slice, repaired by refineToCap() where LPT
1328+
// overshoots; the final placement is repaired against the same.
12951329
//
12961330
// All three => sliced at the configured count. Any one fails => whole, and the
12971331
// decision says which. Packages are decided in name order, each against the
@@ -1321,8 +1355,11 @@ export function planShards(
13211355
const needed = own > target;
13221356
let spreads = false;
13231357
let densest = null;
1358+
let allowance = 0;
13241359
if (needed) {
1325-
const bins = placeItems(expandWith(pkg.name, pkg.sliceCount), shardCount, cap);
1360+
const candidate = expandWith(pkg.name, pkg.sliceCount);
1361+
allowance = closureAllowance(candidate, shardCount);
1362+
const bins = placeItems(candidate, shardCount, cap + allowance);
13261363
try {
13271364
assertSlicesSpread(bins);
13281365
spreads = true;
@@ -1331,24 +1368,29 @@ export function planShards(
13311368
}
13321369
densest = Math.max(...bins.map((b) => b.total));
13331370
}
1334-
const withinCap = densest !== null && densest <= cap;
1371+
const withinCap = densest !== null && densest <= cap + allowance;
13351372
const count = needed && spreads && withinCap ? pkg.sliceCount : 1;
13361373
chosen.set(pkg.name, count);
1337-
decisions.push({ name: pkg.name, configured: pkg.sliceCount, count, needed, spreads, withinCap, own, target, densest, cap });
1374+
decisions.push({ name: pkg.name, configured: pkg.sliceCount, count, needed, spreads, withinCap, own, target, densest, cap, allowance });
13381375
}
13391376
const items = expandSlices(weighed.map((p) => ({ ...p, sliceCount: chosen.get(p.name) })), readers);
1340-
return { items, bins: placeItems(items, shardCount, cap), decisions };
1377+
return { items, bins: placeItems(items, shardCount, cap + closureAllowance(items, shardCount)), decisions };
13411378
}
13421379

1343-
// One line per decision, for the shard log: what the run chose and why.
1380+
// One line per decision, for the shard log: what the run chose and why. The
1381+
// declaration-closure allowance is named whenever it was part of the verdict --
1382+
// a refusal past it, or a slicing only it admitted -- and left out when the bare
1383+
// cap already decided, so the line never claims a cap the split did not meet.
13441384
export function renderSliceDecision(d) {
13451385
const floor = `whole ${d.own.toFixed(0)}s against ${d.target.toFixed(0)}s, ${MAX_SHARD_OVER_MEAN}x the heaviest other serial task`;
13461386
const capText = Number.isFinite(d.cap) ? `${d.cap.toFixed(2)}s density cap` : 'no density cap';
1387+
const allowed =
1388+
Number.isFinite(d.cap) && (d.allowance ?? 0) > 0 ? ` plus its ${d.allowance.toFixed(2)}s declaration-closure allowance` : '';
13471389
let why;
13481390
if (!d.needed) why = `not the run's floor: ${floor}`;
13491391
else if (!d.spreads) why = `${floor}, but ${d.configured} slices cannot spread across this run's shards`;
1350-
else if (!d.withinCap) why = `${floor}, but sliced the densest shard would carry ${d.densest.toFixed(2)}s, past the ${capText}`;
1351-
else why = `${floor}; densest shard ${d.densest.toFixed(2)}s, within the ${capText}`;
1392+
else if (!d.withinCap) why = `${floor}, but sliced the densest shard would carry ${d.densest.toFixed(2)}s, past the ${capText}${allowed}`;
1393+
else why = `${floor}; densest shard ${d.densest.toFixed(2)}s, within the ${capText}${d.densest > d.cap ? allowed : ''}`;
13521394
return `${d.name}: ${d.count > 1 ? `sliced x${d.count}` : 'whole'} (${why})`;
13531395
}
13541396

@@ -1842,7 +1884,9 @@ const SELF_TEST_BATTERIES = Object.freeze({
18421884
'drift warning tier under the red (#16465)': 12,
18431885
'file-level slice items (#16173)': 20,
18441886
'file-level slices reach vitest through OS_TEST_SHARD (#19278)': 11,
1845-
'density cap and the per-run slice decision (#22075)': 14,
1887+
// 4 of these 18 are the declaration-closure allowance (ruling record
1888+
// 6596963071): 1 unit, 3 on the frozen 2026-11-02 shape.
1889+
'density cap and the per-run slice decision (#22075)': 18,
18461890
'the drift step reports on every event, `schedule` included, and refuses a missing event': 8,
18471891
// The nightly tiers state their own slicing (`sliceAcrossShards`). 6 of these
18481892
// 17 read the live tree: 3 read test-nightly-tiers.yml back (the matrix, the
@@ -3198,14 +3242,18 @@ function selfTest() {
31983242
// splits it, puts no shard past the cap -- and that split slices, so the cap
31993243
// is graded on the shape it exists to bound (pin 3b proves the slice is
32003244
// there). This is the case that fails when a slicing packs a shard denser
3201-
// than the split the drift step was proven green on.
3245+
// than the split the drift step was proven green on. Scored against the cap
3246+
// planShards() judged that split by: plus closureAllowance() when it carries
3247+
// the reader slice, which the full list does whenever it slices the CLI.
32023248
check(() => {
3203-
const over = real.map((b, i) => ({ shard: i + 1, total: b.total })).filter((b) => b.total > datasetCap);
3249+
const allowance = closureAllowance(datasetItems, SHARD_COUNT);
3250+
const over = real.map((b, i) => ({ shard: i + 1, total: b.total })).filter((b) => b.total > datasetCap + allowance);
32043251
if (over.length > 0 || !datasetItems.some((i) => i.slice)) {
32053252
throw new Error(
32063253
`density cap: the committed dataset's full list, split as CI splits it, puts ` +
32073254
`${over.map((b) => `shard ${b.shard} at ${b.total.toFixed(2)}s`).join(', ') || 'no slice on any shard'} ` +
3208-
`against the ${datasetCap.toFixed(2)}s cap (the whole-package split's densest bin). ` +
3255+
`against the ${datasetCap.toFixed(2)}s cap (the whole-package split's densest bin) ` +
3256+
`plus a ${allowance.toFixed(2)}s declaration-closure allowance. ` +
32093257
`Bins: ${real.map((b) => b.total.toFixed(2)).join('/')}s; ` +
32103258
(datasetPlan.decisions.map(renderSliceDecision).join('; ') || 'no slicing decision') + '.'
32113259
);
@@ -3234,6 +3282,107 @@ function selfTest() {
32343282
}
32353283
});
32363284

3285+
// THE DECLARATION-CLOSURE ALLOWANCE (closureAllowance(), ruling record
3286+
// 6596963071), on the shape that needed it: the 2026-11-02 refresh's package
3287+
// weights, frozen here rather than read from the dataset file so the pin does
3288+
// not move with a refresh, the CLI cut 2 ways, at 8 shards, with the reader
3289+
// slice and its rider fixed. The whole split is balanced, so the bare cap is
3290+
// the mean (1455.81s) and the charge lifts the other shards past it; carried,
3291+
// the reader slice earns 783 / 7 = 111.86s and the split is judged against
3292+
// 1567.67s.
3293+
const refreshedWeights = [
3294+
'cli 1432.04 spec 1008.78 runtime 987.16 objectql 693.87 rest 630.61 metadata-protocol 566.5',
3295+
'service-automation 526.88 plugin-security 515.26 plugin-auth 450.08 driver-sql 267.52',
3296+
'plugin-audit 261.68 plugin-approvals 243.57 cloud-connection 233.46 lint 207.88 verify 189.81',
3297+
'plugin-sharing 183.78 service-storage 183.37 client 164.18 service-messaging 145.67',
3298+
'service-analytics 144 driver-memory 141.87 service-settings 140.81 service-datasource 136.01',
3299+
'driver-turso 108.76 organizations 107.27 core 106.66 plugin-email 101.12 example-showcase 90.51',
3300+
'connector-app-github 82.27 driver-sqlite-wasm 78.42 platform-objects 77 trigger-record-change 75.65',
3301+
'plugin-hono-server 72.94 mcp 70.64 connector-app-stripe 69.8 plugin-dev 60.84 plugin-webhooks 53.92',
3302+
'service-mailbox 53.18 connector-mcp 50.82 http-conformance 49.59 plugin-pinyin-search 49.43',
3303+
'driver-mongodb 49.14 example-todo 47.71 service-package 45.89 metadata 43.72 connector-openapi 43.28',
3304+
'formula 43.23 service-queue 40.79 driver-odata 35.98 hono 35.4 vitest-filter-preflight 31.35',
3305+
'service-print 29.15 example-crm 27.79 trigger-schedule 26.54 types 25.69 metadata-core 25.24',
3306+
'create-objectstack 23.84 service-knowledge 22.56 connector-rest 22.09 service-sms 21.48',
3307+
'metadata-fs 19.4 connector-slack 18.54 downstream-contract 17.77 service-job 14.35',
3308+
'service-cluster 13.58 example-embed-objectql 13.14 service-i18n 13.13 client-react 12.26',
3309+
'trigger-api 10.48 service-cluster-redis 9.22 service-realtime 8.19 eslint-plugin 6.46',
3310+
'sdui-parser 5.34 embedder-openai 4.47 observability 3.47 knowledge-ragflow 3.23 connector-imap 3.2',
3311+
'service-cache 2.77 knowledge-memory 2.69',
3312+
].join(' ').split(' ');
3313+
// The packages that run `test` and `test:repo` as two tasks, as the workspace
3314+
// manifests read on that refresh; without them spec's 1008.78s would be the
3315+
// floor and the CLI would not be NEEDED at all.
3316+
const refreshedTwoTask = new Set(['core', 'objectql', 'rest', 'runtime', 'spec', 'types']);
3317+
const refreshedShape = (cliSlices) => {
3318+
const out = [];
3319+
for (let i = 0; i < refreshedWeights.length; i += 2) {
3320+
const short = refreshedWeights[i];
3321+
out.push({
3322+
name: short === 'create-objectstack' ? short : `@objectstack/${short}`,
3323+
weight: Number(refreshedWeights[i + 1]),
3324+
tasks: refreshedTwoTask.has(short) ? 2 : 1,
3325+
sliceCount: short === 'cli' ? cliSlices : 1,
3326+
});
3327+
}
3328+
return out;
3329+
};
3330+
const refreshedReaders = Object.freeze({
3331+
slices: Object.freeze({ '@objectstack/cli': 1 }),
3332+
packages: Object.freeze(['@objectstack/cli', '@objectstack/metadata']),
3333+
});
3334+
check(() => {
3335+
// The allowance itself: the charge's share of the other shards when a
3336+
// closure is carried, nothing when none is, nothing on one shard.
3337+
const carried = [{ name: 'a 1/2', weight: 1, slice: { index: 1, count: 2 }, closure: DECLARATION_CLOSURE_CHARGE_SECONDS }];
3338+
const plain = [{ name: 'a', weight: 1, slice: null }];
3339+
const got = [closureAllowance(carried, 8), closureAllowance(plain, 8), closureAllowance(carried, 1)];
3340+
if (got[0] !== DECLARATION_CLOSURE_CHARGE_SECONDS / 7 || got[1] !== 0 || got[2] !== 0) {
3341+
throw new Error(`closure allowance: carried at 8 / plain at 8 / carried at 1 read ${got.join(' / ')}s`);
3342+
}
3343+
});
3344+
check(() => {
3345+
// Carried: the refreshed shape slices x2, its densest shard past the bare
3346+
// cap and within the cap plus the allowance, and the log names the allowance.
3347+
const shape = refreshedShape(2);
3348+
const cap = densityCap(shape, 8);
3349+
const plan = planShards(shape, 8, { cap, readers: refreshedReaders });
3350+
const [d] = plan.decisions;
3351+
const judged = cap + d.allowance;
3352+
if (
3353+
shape.length !== 79 || cap.toFixed(2) !== '1455.81' || d.allowance.toFixed(2) !== '111.86' ||
3354+
judged.toFixed(2) !== '1567.67' || d.count !== 2 || !d.withinCap || !(d.densest > cap) ||
3355+
!(d.densest <= judged) || balanceOf(plan.bins).max > judged ||
3356+
!renderSliceDecision(d).includes('plus its 111.86s declaration-closure allowance')
3357+
) {
3358+
throw new Error(
3359+
`closure allowance: the refreshed shape (${shape.length} packages, cap ${cap.toFixed(2)}s, allowance ` +
3360+
`${d.allowance.toFixed(2)}s) was judged ${renderSliceDecision(d)}`
3361+
);
3362+
}
3363+
assertSlicesSpread(plan.bins);
3364+
});
3365+
check(() => {
3366+
// Not carried, not allowed: the same shape with no reader slice in the
3367+
// split is judged against the bare cap.
3368+
const shape = refreshedShape(2);
3369+
const [d] = planShards(shape, 8, { cap: densityCap(shape, 8), readers: NO_DECLARATION_READERS }).decisions;
3370+
if (d.allowance !== 0 || renderSliceDecision(d).includes('allowance')) {
3371+
throw new Error(`closure allowance: a split with no reader slice was allowed ${d.allowance}s: ${renderSliceDecision(d)}`);
3372+
}
3373+
});
3374+
check(() => {
3375+
// The allowance widens the cap and does not lift it: the carried split
3376+
// against a cap one second short of admitting it is refused, by name.
3377+
const shape = refreshedShape(2);
3378+
const [taken] = planShards(shape, 8, { cap: densityCap(shape, 8), readers: refreshedReaders }).decisions;
3379+
const short = taken.densest - taken.allowance - 1;
3380+
const [d] = planShards(shape, 8, { cap: short, readers: refreshedReaders }).decisions;
3381+
if (d.count !== 1 || d.withinCap || !renderSliceDecision(d).includes('declaration-closure allowance')) {
3382+
throw new Error(`closure allowance: a split past the cap plus its allowance was taken: ${renderSliceDecision(d)}`);
3383+
}
3384+
});
3385+
32373386
// The REPAIR: LPT overshoots a cap by where its last small items fall, and
32383387
// refineToCap() moves whole packages until the split is back within it. Two
32393388
// shards, `big` cut in two, and five packages: the whole split's densest bin
@@ -3729,8 +3878,12 @@ function selfTest() {
37293878
`partition-test-shards: self-test OK (${datasetPackages.length} measured packages ` +
37303879
`-> ${datasetItems.length} shard items, ${SHARD_COUNT} shards, ` +
37313880
`max/mean ${balance.ratio.toFixed(2)}x <= ${MAX_SHARD_OVER_MEAN}x, floor ${balance.floor.toFixed(0)}s, ` +
3732-
`bins ${balance.totals.map((t) => t.toFixed(2)).join('/')}s within the ${datasetCap.toFixed(2)}s density cap, ` +
3733-
`whole legs ${wholeLeg.sums.map((t) => t.toFixed(0)).join('/')}s within ${WHOLE_LEG_CAP_SECONDS}s, ` +
3881+
`bins ${balance.totals.map((t) => t.toFixed(2)).join('/')}s within the ${datasetCap.toFixed(2)}s density cap` +
3882+
// Named only when the bare cap did not decide, so the line never claims a cap the bins exceed.
3883+
(balance.max > datasetCap
3884+
? ` plus its ${closureAllowance(datasetItems, SHARD_COUNT).toFixed(2)}s declaration-closure allowance`
3885+
: '') +
3886+
`, whole legs ${wholeLeg.sums.map((t) => t.toFixed(0)).join('/')}s within ${WHOLE_LEG_CAP_SECONDS}s, ` +
37343887
`file-level slices: ` +
37353888
`${datasetPlan.decisions.filter((d) => d.count > 1).map((d) => `${d.name} x${d.count}`).join(', ') || 'none'})`
37363889
);

0 commit comments

Comments
 (0)