Skip to content

ci(test-core): retire the CLI's file-level slicing by the partitioner's own slice-count derivation - #21487

Merged
objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/issue-20388-cli-slice-pins
Oct 3, 2026
Merged

objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/issue-20388-cli-slice-pins

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Oct 2, 2026 •

Copy link
Copy Markdown
Contributor

Unblocks #20388 (the Test Core shard-timings refresh), which #16465 and #16468 wait on.

This PR binds no card: #20388, #16465 and #16468 all remain open. #20388 lands by itself after this merges, on its own green, through the auto-merge the maintainer armed.

What this changes

scripts/partition-test-shards.mjs --self-test now passes on BOTH datasets: the scripts/test-shard-timings.json on main, and #20388's (blob 84342b45, byte-identical to its head 8d60ae86, overlaid unstaged in a second worktree and never committed).

  • FILE_SHARDED_PACKAGES is emptied (the CLI's 2 is retired). The mechanism stays: the item grammar, expandSlices, the vitest file-count floor, the OS_TEST_SHARD wiring judge, the generator's slice reassembly, and the dormant CLI wiring in turbo.json and packages/cli/vitest.config.ts.
  • Pin 3c is rewritten. It no longer substitutes the hard-coded CLI_MEASURED = 1231.52 bridge. It asks the committed dataset, for every map entry, whether n - 1 slices would also meet the bound (sliceCountProblems), and refuses an n that n - 1 could replace. Five fixtures hold each refusal in both directions: needed, retire, lower, unmeasured entry, and an entry below 2.
  • Pin 3b is rewritten. It grades the real split as before. It also grades a cut of the dataset's heaviest package in two, so the spread check is not vacuous while the map slices nothing.
  • The mechanism's pins read a fixture map through new optional sliced parameters (default: the live map) on sliceCountFor, weighItems and driftReport. The re-pointed cases are the vitest floor, the slice-share prediction, drift on a sliced package, observed-run-wins, weighItems slicing, and the wiring reader pointed at the CLI's real tree.
  • Pin 6's inversion pair is replaced. example-todo over core flipped in the refresh (35.30s vs 57.09s). The new pair is plugin-pinyin-search over sdui-parser: 2 vs 13 test files, 14.40s vs 1.62s on main's dataset, 38.77s vs 4.54s on chore(ci): refresh the Test Core shard-timings dataset #20388's. A new guard reads the dataset's own order, so a flipped pair asks for a new pair instead of reporting a weighing defect.
  • PREVIOUS_FILE_SHARDED_PACKAGES (new, beside the map) holds the outgoing map, { '@objectstack/cli': 2 }. The generator's slice-digest matcher decodes a digest against both declared maps, exactly by hash, so a run summary written before this change still reads. Without it, the refresh lane refused every run on main; see the next section. A decoded slice is still summed within its run (an incomplete set contributes nothing), and a digest neither map names is still refused. The map has no reader once a day has passed, because run summaries are kept for one day (retention-days: 1).
  • scripts/measure-test-shard-timings.mjs (outside the claimed surface, and why). This is the matcher change above. Its live-map case also read FILE_SHARDED_PACKAGES['@objectstack/cli'], which reds under an empty map (measured: the unedited file exits 1). That case now covers both live maps, and a new case reads a pre-change run's two halves into their 733.33s whole while still refusing a count neither map names. The env-carried battery floor goes from 10 to 11. A stale "(2 for @objectstack/cli today)" parenthetical is dropped.

Battery floors are raised to the measured counts: balancing pins 21 to 25, OS_TEST_SHARD wiring 9 to 11. File-level slice items stays at 20. The counts are the same on both datasets.

The three bin readings, and the keep-or-retire choice

Predicted bins at 6 shards against the 1.3x bound, using the partitioner's own partition / balanceOf / expandSlices:

dataset case bins (s) max/mean heaviest item verdict
#20388 (72 pkgs, 7430.00s) K CLI sliced at 2 1391/1208/1208/1207/1208/1208 1.124x spec 1391s meets
#20388 R CLI whole (733.33s) 1391/1208/1206/1208/1208/1208 1.124x spec 1391s meets
#20388 R-worst CLI whole at 1231.52s 1391/1308/1307/1307/1308/1307 1.053x spec 1391s meets
main (70 pkgs, 3997.47s) K 666/666/666/667/666/666 1.000x spec 404s meets
main R 666/666/667/667/667/666 1.001x cli 458s meets
main R-worst (the old bridge) 1232/708/707/709/708/707 1.549x cli 1232s breach

On #20388's dataset, both R and R-worst meet the bound, and slicing changes no bin's maximum (spec is the heaviest item in all three cases). Solving C ≤ (1.3/6)(6696.67 + C), the CLI fits whole until about 1852s, which is 1.5x its worst reading. The file's own rule makes n the smallest count that meets the bound. That count is 1, so the entry is retired.

Four axes:

  • Real need: measured. Slicing buys nothing in the predicted maximum on the refreshed dataset. It costs a duplicated closure build and a sequential slice leg on two shards. In chore(ci): refresh the Test Core shard-timings dataset #20388's run 36380128221, "Build the sliced package's dependency closure" took 6m15s on Test Core (5/6) and 6m43s on (6/6), and 5/6 was the longest job at 19m23s.
  • Long-term: keeping 2 would leave a count no pin can justify. The old counterfactual cannot be rewritten on any measured basis, because R-worst meets. Retirement keeps the mechanism, and pin 3 (the floor) is the live trigger to slice again.
  • AI-error resistance: the new 3c reds on a slice count that is larger than needed. A future author cannot leave slicing configured without a measurement behind it.
  • Startup focus: fewer live moving parts and no new gate. 3c replaces a case inside the existing self-test, and the retirement is immediate.

The refresh lane across the change: red, then green

  • Red at c614a094. This PR's pull_request rehearsal of shard-timings-refresh.yml (run 37074888579, job 111062437870) refused all ten eligible hourly runs on main, 37010481060 through 37070188866. It then ended with "The 0 eligible run(s) ... NOTHING was regenerated".
  • Cause, reproduced locally (the run's own log and artifacts are behind the egress policy here). A synthetic run was built from main's dataset in the exact turbo 2.10.10 shape: six whole-leg summaries, plus two slice legs whose OS_TEST_SHARD digest is the sha256 of 1/2 or 2/2 (d939926f…, the measured format). It was fed to the lane's own two commands: the generator with --run / --merge-into, then select-shard-timings-run.mjs --check-coverage.
    • aa463223 (the map {cli: 2}): exit 0, coverage OK.
    • c614a094 (the empty map): exit 1, "matches no slice the partitioner can emit for it (none: FILE_SHARDED_PACKAGES does not slice it)".
    • d049d353: exit 0, coverage OK, and a packages map identical to aa463223's.
  • So the hypothesis holds, and its rival is falsified. The 2026-09-30 green rehearsals predate the env carrier, so they cannot separate "the empty map refuses these slices" from "the env carrier never decoded on real summaries". The rehearsal at d049d353 does: run 37075730446 (job 111065090886) is green. It accepted its first candidate, with no "dropping that run" warning, so main's real env-carried summaries decode once the matcher knows the outgoing map.
  • Why decode the outgoing map, not keep the 2 slices. Read along the four axes:
    • Need: every run summary that exists today carries the CLI's two slices. That is the lane's only input until a day after the merge.
    • Long-term: the matcher stays a whitelist. It reads two declared maps by hash equality, never a guessed count, and any future map change in either direction gets the same one-day bridge by setting one constant in the same PR.
    • AI error: the refusal is unchanged for any digest neither map names (N4 below), and completeness is still judged within a run.
    • Startup: no new gate, and no grace window beyond the artifacts' own one-day life.
    • Keeping 2 would bring back a slice count that pin 3c refuses on both datasets, with no measured counterfactual to replace it.
  • The next scheduled weekly refresh (Mon 2026-10-05 05:30Z), if this has merged by then: expected to succeed. With one-day retention, its candidates are runs made under the new map: whole CLI, no slice digest. Measured on synthetic summaries at d049d353: a post-change run alone gives exit 0 (cli whole, 740s). A post-change run accumulated with a pre-change sliced run gives exit 0 (cli 599.08s, the median of 740 and 458.15). So a refresh that straddles the merge also reads. It is still a prediction: it depends on an eligible full-battery run existing, as every refresh does.

Self-test on both datasets (head d049d353)

BEFORE  main dataset    (aa463223)  exit 0  partition-test-shards: self-test OK (70 measured packages -> 71 shard items, 6 shards, max/mean 1.00x <= 1.3x, floor 404s, bins 666/666/666/667/666/666s)
BEFORE  #20388 dataset  (aa463223)  exit 1  Error: slice derivation: UNSLICED, @objectstack/cli at 1231.52s is 1391s against a 1321s mean and now fits under 1.3x on its own ... Re-derive the slice count (or retire it) ...
AFTER   main dataset    (d049d353)  exit 0  partition-test-shards: self-test OK (70 measured packages -> 70 shard items, 6 shards, max/mean 1.00x <= 1.3x, floor 458s, bins 666/666/667/667/667/666s, file-level slices: none)
AFTER   #20388 dataset  (d049d353)  exit 0  partition-test-shards: self-test OK (72 measured packages -> 72 shard items, 6 shards, max/mean 1.12x <= 1.3x, floor 1391s, bins 1391/1208/1206/1208/1208/1208s, file-level slices: none)

The BEFORE red reproduces #20388's AFTER block on today's main (line 1075 now, 967 in that body). It also mislabels spec's 1391s as the CLI's, because the old counterfactual read the overall heaviest item. A second red was masked behind it. With the 3c throw muted on aa463223, pin 6 reds on #20388's dataset with "The run is weighing test-file count again". That is a misdiagnosis: the pair had flipped.

Mutation proof, one per rewritten or added pin

Every leg was taken from committed c614a094 through scripts/ablation-replace.mjs. The whole table plus N1 to N4 was then re-run from committed d049d353 on both datasets: 48 legs, every one landed and restored. In each leg the anchor hit 1 and the replacement went 0 to 1. Each leg was restored to the HEAD blob with git diff HEAD empty, under a shell trap that re-proved the hashes in both worktrees at the end. The red line below is the self-test's first Error:, the same on both datasets unless shown.

# mutation first red
M1 re-add '@objectstack/cli': 2 to the live map slice derivation, committed dataset (1 of 1 ...), "sliced 2 ways at 458.15s (main) / 733.33s (#20388), but at 1 the split already meets 1.3x ... Retire the entry"
M2 meetsBound always meets a count of 2 that 1 cannot replace was refused
M3 n-1 = 1 never judged a package that fits whole kept its slicing with no refusal
M4 only n-1 = 1 judged a count of 3 where 2 meets the bound was accepted
M5 unmeasured entry not refused an entry the dataset never measured was accepted
M6 the "at least 2" floor dropped an entry of fewer than 2 slices was accepted
M7 expandSlices stops slicing slice spread: cutting @objectstack/cli (main) / @objectstack/spec (#20388) in two produced no ... pair to grade
M8 pin 6 back to the old pair green on main (the pair still holds there); on #20388: the dataset no longer measures @objectstack/example-todo slower than @objectstack/core (35.3s vs 57.09s) ... Pick a new inversion pair
M9 weighItems weighs test-file count weight: @objectstack/plugin-pinyin-search weighed 2 and @objectstack/sdui-parser weighed 13 ...
M10 every package takes the map's first count slice count: a package outside the slice map was sliced
M11 sliceCountFor ignores sliced slice count: the configured package did not read its configured count
M12 vitest floor refusal dropped slice floor: slicing below the test-file count was not refused (no throw)
M13 floor refuses below 1000 files the floor's own refusal for 500 files, thrown from the "plenty of test files" case
M14 prediction stops dividing prediction: a sliced package was charged its WHOLE dataset entry
M15 driftReport ignores sliced drift: a sliced overshoot read 0.83x and was not reported as drift
M16 observed slices ignored drift: an observed WHOLE run was charged a slice-sized prediction (1.67x)
M17 weighItems ignores sliced weighItems: 1 package(s) produced 1 item(s), expected 2
M18 wiring reader's default is not the live map slice wiring: judged 1 of 0 sliced package(s) ...
M19 packages/cli/vitest.config.ts reads OS_TEST_SHARD_UNREAD slice wiring, the tree read through a fixture map ... never reads OS_TEST_SHARD into vitest's shard
M20 measure: samplesFromSummary's default sliced is not the live map env slice: a @objectstack/cli digest of 1/3, a count neither live map names, was not refused listing (1/2, 2/2) (got "no throw")
N1 the matcher stops reading the previous map the live-maps case: @objectstack/cli ran with OS_TEST_SHARD set ... matches no slice the partitioner can emit for it, or emitted under the map it replaced (none: ...)
N2 samplesFromSummary's default previous is not the live one the same refusal, from the live-maps case
N3 samplesFromSummary drops its previous argument the across-change case: a: cli ran with OS_TEST_SHARD set (digest d939926f…) ... matches no slice ...
N4 the matcher decodes a count neither map names env slice: a @objectstack/cli digest of 1/3, a count neither live map names, was not refused listing (1/2, 2/2)

Premise checks (the dispatch's A1 to A4)

  • A1 holds on aa463223: the map was { '@objectstack/cli': 2 }, pin 3b carried the 1231.52s/800.7s prose, and pin 3c carried CLI_MEASURED = 1231.52.
  • A2 reproduced on current main; see BEFORE above.
  • A3 recomputed. On main: 70 packages, 3997.47s, cli 458.15s, spec 403.65s. On chore(ci): refresh the Test Core shard-timings dataset #20388: 72 packages, 7430.00s, measuredAt 2026-09-28, runs [36380128221], spec 1391.38s (heaviest), cli 733.33s. The two new packages are organizations and vitest-filter-preflight. The package set equals today's workspace minus the CI-excluded dogfood.
  • A4: well-formed. Today's generator writes the identical shape: the same top-level and provenance keys, and byte-identical note, mergeRule and refresh strings. It accepts the file as a --merge-into target (carried spec on a HIT witness). skippedAsCached, skippedIncompleteSlices and carriedOver are all empty, and the CLI's 733.33s is a two-slice sum from one run. The 09-28 run carried its slices as a --shard passthrough, which today's generator still reads (sliceOfCliArguments). Its run-summary artifacts are no longer retained (the run lists only test-core-timing-table and build-output), so a byte-level re-generation is NOT MEASURED.

Consequences outside this diff (known, deliberate)

  • The refresh lane. See its own section above: red at c614a094, green at d049d353.
  • Nightly tiers. test-nightly-tiers.yml partitions the one tier-owning package at 2 shards. Measured locally: shard 1/2 now carries @objectstack/cli whole, and 2/2 carries nothing ("No packages on this shard", exit 0). The run stays inside its 45-minute timeout by its own header's estimate. Its header prose and its 2-shard matrix are now stale; no carrier.
  • The window between this merge and chore(ci): refresh the Test Core shard-timings dataset #20388's. main then splits on the stale weights with the CLI whole. Modeled with chore(ci): refresh the Test Core shard-timings dataset #20388's weights standing in for real cost: the heaviest actual bin stays spec's (1775s sliced, 1773s whole). The CLI's bin is 1116s at 733.33s, or 1614s at 1231.52s. Merging main into chore(ci): refresh the Test Core shard-timings dataset #20388 right after this lands keeps the window short.

Gates (head d049d353)

node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack (change set from git, 2 paths) derived 32 families. The list is the same on c614a094 and d049d353. On both heads, all 32 ran with exit 0, recorded and reconciled by --ran: "32 derived, 32 run, 0 NOT-MEASURED, 0 UNRUN". node scripts/report-test-timings.mjs --self-test also ran, exit 0. The list includes the three self-tests (partition / measure / select-shard-timings-run), check:cross-package-test-inputs, check:nul-bytes, check:pm-dispatch-gates (1976 cases, 977s), check-scripts-symbol-anchors and check-self-test-wired.

Lint, narrowed and proven. eslint --no-inline-config --format json on the 2 changed files reports 2 files, 0 errors and 0 warnings on both heads, and neither file is ignored. --print-config shows 2 per-file rules (no-restricted-imports, comment-swallow/no-code-inside-block-comment) and no parserOptions.project or projectService. Type-aware linting is off, so this diff cannot move any untouched file's verdict. The full pnpm lint is CI's.

skip-changeset: root scripts/ ships in no package's files[] (the root package is private).

Acceptance notes


Generated by Claude Code

…'s own derivation, and re-point the slicing pins at fixtures

The refreshed shard-timings dataset (run 36380128221) moves the mean, not
only the CLI: @objectstack/spec at 1391.38s is the heaviest item, and with
the CLI sliced at 2, whole as measured (733.33s), or whole at its worst
reading (1231.52s), the split is 1.124x / 1.124x / 1.053x of the mean,
all inside 1.3x. The smallest slice count that meets the bound is 1, so
FILE_SHARDED_PACKAGES is emptied; the mechanism stays.

- Pin 3c no longer substitutes the 1231.52s bridge reading for the CLI. It
  asks the committed dataset, for every map entry, whether n - 1 slices
  would also meet the bound (sliceCountProblems), with five fixtures that
  hold each refusal in both directions.
- Pin 3b grades the real split and a cut of the dataset's heaviest package,
  so it is not vacuous while the map slices nothing.
- The mechanism's pins (vitest floor, slice-share prediction, drift on a
  sliced package, weighItems slicing, the OS_TEST_SHARD wiring reader) read
  a fixture map through new optional `sliced` parameters.
- Pin 6's inversion pair flipped in the refresh (example-todo 35.30s vs
  core 57.09s); the new pair is plugin-pinyin-search over sdui-parser, and
  a guard now reads the dataset's order so a flipped pair asks for a new
  pair instead of reporting a weighing defect.
- measure-test-shard-timings.mjs: its live-map case holds for an empty map.

Claude-Session: https://claude.ai/code/session_01HRYqpqGcWpJuJkDmbRF75w
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added the size/m label Oct 2, 2026
@objectstack-fleet objectstack-fleet Bot added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Oct 2, 2026
…ange replaced, so the refresh lane survives the retirement

The refresh lane's pull_request rehearsal on this branch (run 37074888579)
refused all ten eligible hourly runs on main and regenerated nothing. Every
retained run summary carries the CLI's slices as an OS_TEST_SHARD digest
of 1/2 or 2/2, and with FILE_SHARDED_PACKAGES emptied the generator's
digest matcher had no candidate for either, so it refused each summary
whole.

- partition-test-shards.mjs declares PREVIOUS_FILE_SHARDED_PACKAGES, the
  outgoing map ({ '@objectstack/cli': 2 }), with when it decodes nothing:
  run summaries are kept for one day.
- sliceOfEnvironment matches a digest against both closed maps, exactly by
  hash. A decoded slice is still summed within its run (an incomplete set
  contributes nothing), and a digest neither map names is still refused.
- measure self-test: the live-maps case covers both defaults, and a new
  case reads a pre-change run's two halves into its 733.33s whole while
  refusing a count neither map names. Battery floor 10 to 11.

Claude-Session: https://claude.ai/code/session_01HRYqpqGcWpJuJkDmbRF75w
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/l and removed size/m labels Oct 2, 2026
@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 2, 2026 23:40
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 2, 2026 23:40
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 2, 2026
Merged via the queue into main with commit 9ff7428 Oct 3, 2026
41 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-20388-cli-slice-pins branch October 3, 2026 00:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/l skip-changeset PR has no user-facing published change; bypasses the changeset gate

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants