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
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Oct 7, 2026
…red/predicted, warning past 1.3x (objectstack-ai#21998)

Fixes objectstack-ai#16465
Clause-②: no

Wires the Test Core shard timing-drift check the tree has carried
unwired since objectstack-ai#16173, adds the card's 1.3x as a warning tier under its
red, and restates the `ci.yml` comments that the wiring and the objectstack-ai#21487 /
objectstack-ai#21826 changes left stale. Root `scripts/` and one workflow, nothing
published: `skip-changeset`.

## The ruling this follows

Triage `5925054826`, as the unlock `6013064225` restates it, verbatim:

- "one red rule, the in-tree `partition-test-shards.mjs --check-drift`
at `MAX_MEASURED_OVER_PREDICTED = 1.5`;"
- "the card's 1.3× is a `::warning::` only;"
- "⛔ no second red rule, so no 70%-of-`timeout-minutes` red."

The maintainer's authority on the card: 「同意你的建议,你负责执行派发所有可行的优化」.

## What changed

**`.github/workflows/ci.yml`, the Test Core shard job only**

- New step `Check this shard's timing drift`: `--check-drift` over
`.turbo/runs/*.json` with `--label "Test Core (N/6)"` (the matrix
shard), with no `if:` and no `continue-on-error`. It sits between the
run-summary upload and the completeness guard, above the attestation
pair, so a drift red also withholds that shard's attestation. A shard
with no packages writes no summary; the step reports that as NOT
MEASURED and exits 0, because the script itself treats zero inputs as a
usage error.
- The "⛔ THE DRIFT STEP IS DELIBERATELY NOT WIRED HERE YET" block is
replaced by a description of what the step does. The stale CLI figures
("458.15s recorded, 1231.52s measured") and "the CLI is halved across
two runners" are gone. The comment now cites the dataset as objectstack-ai#21826
refreshed it (provenance run 37262126122, CLI 1702.69s) and the second
sample below.
- **WHY SIX** now names `@objectstack/cli` (1702.69s) as the heaviest
indivisible suite, ahead of `@objectstack/spec` (1134.86s). The
6/7/8/10-shard table is re-derived on the current dataset with the
partitioner's own `partition()` / `balanceOf()`: 1.04x / 1.22x / 1.39x /
1.74x, with the max at 1703s at every count. The old claim that the
self-test "pins this arithmetic" is corrected: what it pins is pin 3,
the heaviest package within 1.3x of the mean at `SHARD_COUNT`.
- **The 45-minute timeout comment.** ⛔ The value stays 45. The revert
condition is now stated against measured wall time instead of "once
objectstack-ai#16173 lands": back to 30 only when the slowest shard's job wall time
stays at or under 24 minutes (80% of 30) on every scheduled run for a
week. On the 16 main runs after the objectstack-ai#21826 refresh, shard 1/6 (the CLI
alone) read 6m14s to 35m43s of job wall time, and was over 30 minutes on
9 of them (for example 34m39s in run 37415122516 and 35m43s in run
37453598388). Two present-tense "30-minute wall" phrases in the same job
now say "`timeout-minutes` wall".
- **Slice-leg prose.** The build-closure step and the test step now say
that `FILE_SHARDED_PACKAGES` has been empty since objectstack-ai#21487, so no shard
runs a slice today.

**`scripts/partition-test-shards.mjs`**

- `WARN_MEASURED_OVER_PREDICTED = 1.3` sits beside
`MAX_MEASURED_OVER_PREDICTED = 1.5`, which is unchanged. It is the same
ratio over the same executed windows; it is not `MAX_SHARD_OVER_MEAN`,
and its docblock says why the two are kept separate.
- `driftReport()` gains `warned`, the band strictly between the two
bounds and exclusive of the red, so one shard gets one verdict.
- `renderDriftVerdict()` returns the four verdicts (OK, WARN, DRIFT, NOT
MEASURED) without printing them. WARN emits exactly one `::warning
title=Test Core shard timing drift::…` line on stdout and exits 0; DRIFT
exits 1, with the same remedy text as before.
`escapeWorkflowCommandMessage()` keeps the message on one line.
- New self-test battery `drift warning tier under the red (objectstack-ai#16465)`,
with 12 cases, registered in `SELF_TEST_BATTERIES` (floor 12). The
roster floor goes from 10 to 11. The cases cover:
- both edges of the band, the red's exclusivity and a healthy 1.18x
reading;
  - replay-only input;
- band non-empty (`WARN` below the red) and not below the balance
tolerance;
- the rendered annotation line, the red's exit code, and command
escaping.

Nothing in the self-test prints a workflow command
(`check:self-test-workflow-commands` green).

Not touched: the attestation schema, `scripts/test-shard-timings.json`,
`measure-test-shard-timings.mjs`, and the required-check names. `Test
Core` and `Test Core (N/6)` are unchanged, and `check:required-contexts`
is green.

## The second sample (carried item 2): executed windows only

The `test-core-run-summary-*` artifacts could not be read from this
container: the egress policy denies
`productionresultssa17.blob.core.windows.net`. Job logs reach only their
last 5000 lines through `get_job_logs`. So the sample comes from each
run's `Test Core` timing table, which the aggregator echoes into its
log. The table carries turbo's own execution windows through
`samplesFromSummary()`, which is the same reader and the same exclusions
as the gate. Replayed packages are listed there and excluded here.

| shard | reading | source runs |
|---|---|---|
| 1/6 (CLI alone) | **exact**, 0.61x, 1.05x, 0.65x, 0.78x, 1.02x, 1.03x,
1.01x, 0.79x | the 8 scheduled runs 37416453417, 37427862594,
37433381795, 37440129185, 37446949708, 37453598388, 37460325624,
37467795959 |
| 2/6 to 6/6 | **NOT MEASURED whole.** The table prints only the 10
slowest packages, so a shard's lighter members have no window here |
same runs |
| (small-set push runs) | exact on every shard that ran: 0.90x (spec
alone), 0.48x (rest alone), 0.67x (create-objectstack alone); 0.18x
(client alone) | 37422473456; 37413386379 |

On shards 2 to 6, the measured heavy members and the turbo
`--concurrency=4` capacity bound (the sum of a shard's task windows is
at most 4x its test step) give these results:

- **Proven under 1.5x:** 8 shard-run pairs (for example shard 3 in
37427862594, at most 0.82x).
- **Undecided** everywhere else. No measured package read 1.5x or more
in any of the 9 near-full runs.
- **Highest package reading:** `@objectstack/spec`, 1.45x / 1.39x /
1.43x on the three runs that executed it (37433381795, 37453598388,
37467882762).
- **Nearest the bound: shard 2/6.** spec carries 70% of its prediction,
so its 11 unmeasured members would have to average 1.61x to 1.77x before
the shard reached 1.5x.

**Stop condition:** not met. No executed-window reading put any shard at
or over 1.5x, so the red is wired as ruled. The per-shard numbers, the
bounds and this PR's own run are on objectstack-ai#16465 in the `os-dev-report`.

## Verification (at `430859c2f`)

- Derived gates: `node scripts/pm/dispatch-gates.mjs --repo
objectstack-ai/objectstack --ran …` printed "59 derived famil(ies)
accounted for — 59 run, 0 NOT-MEASURED (a DERIVED zero — all 59 recorded
an exit code and none of them is 3)". Every gate exited 0.
- Four of them (`check:dts-closure`, `check:dual-build-cjs-loads`,
`check:lean-entry-closure`, `check:sourcemap-no-sources-content`) first
answered exit 3 (PREREQUISITE NOT MET). They exited 0 once a workspace
build had run through the verify lock: 73 tasks, `VERDICT command-exit
0`.
- `node scripts/partition-test-shards.mjs --self-test` → `self-test OK
(72 measured packages -> 72 shard items, 6 shards, max/mean 1.04x …)`.
- `--check-drift` on synthetic summaries: 1.06x gives OK, exit 0; 1.34x
gives WARN plus one `::warning` line, exit 0; 1.54x gives DRIFT, exit 1.
A replay sits beside each, and they are excluded.
- The step's bash, run against an empty directory, gave NOT MEASURED and
exit 0. Against one summary it gave the verdict.
- `check-governed-merges --test` on the final two paths: "0 of 2 path(s)
hit the register … NOT governed", with 494 changed lines.

## Acceptance notes

- **Shards 2 to 6 still lack a whole-shard reading from main.** This
step is what produces one. The first scheduled runs after landing print
it per shard, and the WARN annotations show where.
- **This PR's own Test Core run is a thin sample.**
- Its affected set, simulated with `turbo ls --affected` and the
cross-package union at the merge base, is `@objectstack/spec`,
`@objectstack/client` and `@objectstack/driver-sql`.
- spec's `test` leg is expected to replay, which makes it NOT MEASURED.
client and driver-sql run alone.
  - So it exercises the wiring rather than the dataset.
- **Expect the warning on shard 2/6 of cold full runs.** spec read 1.39x
to 1.45x against its single-run 1134.86s entry. That is the warning
doing its job, and the remedy is the refresh lane, not this step.
- **Small-sample hazard, observed and not filed.** `driftReport()` has
no floor on predicted seconds. A PR whose shard executes only a
few-second package gets a ratio of that package alone. Every
lone-package reading found here was under 1.0x, so this is noted, not
filed.
- **The 24-minute (80%) revert criterion is this PR's choice**, stated
with its reason in the comment. The current 45 is already at 79% on
shard 1/6's slowest reading (35m43s).

---
_Generated by [Claude
Code](https://claude.ai/code/session_01VF48aw8RPG6wzDnMgp6rtw)_

---------

Co-authored-by: Claude <noreply@anthropic.com>
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Oct 9, 2026
…od and Temporal build steps (objectstack-ai#22086)

Fixes objectstack-ai#22077
Clause-②: no

The turbo remote cache that objectstack-ai#21186 wired into Build Core is now read by
every `turbo run build` step of the three Lint `Type Check` build lanes,
the Test Core shards, the dogfood shards and Temporal Conformance. Each
carrier gets Build Core's four env lines verbatim: the same write rule
(writes only on `merge_group` and on `push` / `workflow_dispatch`
against `main`, everything else reads), the same signing, and no new
secret. Two files: `.github/workflows/ci.yml` and
`.github/workflows/lint.yml`.

## Route that landed: the remote cache, not the artifact fallback

Nothing in the remote route failed, so the `dist/` upload-artifact
alternative was not needed:

- **Hashes.** Each carrier's plan is a subset of Build Core's `turbo run
build --filter=!@objectstack/docs` plan, with identical task hashes and
identical `globalCacheInputs`. Measured with `--dry=json` on
`3d9188502e`, turbo 2.11.5, `CI=true`:

  | carrier | build nodes (with a command) | same hash as Build Core |
  |:--|--:|--:|
  | Type Check `Build workspace packages` (3 lanes) | 70 (67) | 70/70 |
| `Build the ledgered packages' dependencies` / `Build the nested
packages ...` | 77 (71) | 77/77 |
| dogfood `Build the dogfood package's dependency closure` | 66 (63) |
66/66 |
| Temporal: driver-sql / non-SQL backends / metadata-protocol / runtime
| 8 / 18 / 14 / 31 | all |
| Test Core `Build this shard's dependency closure`, shards 1-6 | 60 /
62 / 63 / 62 / 32 / 62 | all |

Every plan is 100% `#build` tasks. Filters and `--concurrency` are not
part of a task hash.
- **Signing and permissions.** These are the same secrets and the same
expressions as Build Core. A same-repo PR receives them; a fork PR
receives none, so `TURBO_TOKEN` evaluates to `''` and turbo reports
`Remote caching disabled` (objectstack-ai#21186 measured this). `TURBO_CACHE` never
evaluates to `''`.
- **The cache already serves.** On the `main` push run at this PR's base
(CI `37627942232`), Build Core's build step took 6 s, because the queue
entry for the same tree wrote it. In the same run, the jobs that did not
read the remote rebuilt: dogfood took 200 / 201 / 336 s, and in Lint
`37627942309` the Type Check lanes took 297 / 330 / 335 s.

## Test Core: a new guarded build step

Test Core's existing build step, `Build the sliced package's dependency
closure`, runs zero iterations, because no package has been sliced since
objectstack-ai#21487. The closure was built inside `Run this shard's tests`. That run
cannot read the remote, because `test` / `test:repo` stay off it.

Merge-queue run `37623284168`, `Test Core (3/6)`, shows the cost: one
turbo run of the tests and their closure printed `Cached: 1 cached, 72
total` / `Time: 11m43.373s`.

Each shard now runs a new step, `Build this shard's dependency closure`,
before the slice step. It is one guarded `turbo run build`, with
`--filter=PKG^...` for every package on the shard (PKG's dependencies
without PKG), and it carries the env block. The test step then replays
that closure from the local cache.

Measured on all six shards of the local partition: this plan equals the
test plan's build tasks, with identical hashes, 0 extra and 0 missing.
One exception is `cli#build` on shard 1/6: `cli#test` has `dependsOn:
["build"]`, so the test step still builds that one, as before.

Other properties of the new step:
- An empty `FILTERS` exits before turbo runs, because a bare `turbo run
build` would build the whole workspace.
- It has no `--summarize`, so `.turbo/runs/` stays the test step's
alone, for the drift check and the timings capture.
- `check:stall-guard-budget` judges it at window 10m, cap 20m, budget
45m, slack 25m.

## The two pins

Both pins are written into Build Core's `Turbo remote cache (objectstack-ai#21186)`
comment as a PINS paragraph, and every carrier points back to it. They
are not a new gate.

**objectstack-ai#19086: a step that writes artifacts from a cache-served dist must not
be among those served.**
- The env goes on build-only steps. Checked mechanically on the final
tree: all 12 steps carrying `TURBO_TOKEN` hold Build Core's four lines
byte-identical and run only `turbo run build` / `pnpm build`, and no job
sets `TURBO_*` at job level. The dry-run plans above contain no `test`,
`test:repo`, `typecheck` or `gen:*` task.
- `gen:schema` and `gen:skill-refs` are cacheable turbo tasks, but no
workflow runs them through turbo (`turbo run gen`: 0 hits).
- Grepping the wired jobs for `gen:`, `--fix` and `--write` gives 0
hits, so no step there writes tracked artifacts.
- The typecheck step in `Type Check · workspace` stays off the remote.
Every build task its `^build` closure schedules is already in the wired
build step's plan (66 of 66, 0 extra), so it replays them locally.

**objectstack-ai#21193: the build hash covers every root input the builds read.** I
re-ran objectstack-ai#21193's probe on `3d9188502e`: append one comment line to a
file, re-derive the 72-task Build Core plan, restore from `HEAD`, and
prove the restore by blob hash.

| file | build hashes moved |
|:--|--:|
| `tsup-drop-sources-content.mjs`, `check-dts-emitted.mjs`,
`invoked-as.mjs` | 72/72 each |
| `check-dts-references`, `ts-parse`, `check-regen-pending`,
`regen-artifacts`, `git-env`, `import-prerequisite`,
`cli-build-prerequisite`, `check-dev-prereqs`, `build-input-hash`,
`workspace-enumerator`, `js-comment-mask` | 71/72 each |
| `sync-scaffold-emission-policy.mjs`, `sync-template-versions.mjs`,
`packages/cli/src/commands/init.ts` | 6/72 each |
| control: root `tsup.config.ts` | 72/72 |
| controls: `scripts/check-turbo-task-graph.mjs`, `ci.yml`, `lint.yml` |
0/72 each |

These are objectstack-ai#21193's own numbers. The last row also shows that this PR's
own diff moves no build hash. A static check agrees: the import closure
of every root script that a build command or a tsup config names lies
inside the declared `$TURBO_ROOT$` inputs. The spec generators' closure
(450 files) reaches 10 root scripts, all declared.

## Measurement (before / after)

**Before.** Build-step seconds, from the jobs API:

| step | `main` push at base `3d9188502e` (CI `37627942232` / Lint
`37627942309`) | merge-queue entry for the same tree (CI `37623284168` /
Lint `37623284080`) |
|:--|--:|--:|
| Build Core `Build packages (excluding docs)` (reads the remote
already) | 6 s | 193 s |
| Type Check · workspace `Build workspace packages` | 297 s | 323 s |
| Type Check · debt ledger: build, then ledgered build | 335 s, then 31
s | 340 s, then 32 s |
| Type Check · consumer gates: build, then nested build | 330 s, then 30
s | 342 s, then 31 s |
| dogfood 1/3, 2/3, 3/3 closure build | 200, 336, 201 s | 322, 340, 287
s |
| Temporal: driver-sql, non-SQL, metadata-protocol, runtime | 1, 35, 0,
79 s | 95, 47, 1, 91 s |
| Test Core `Run this shard's tests` (closure plus tests), shards 1-6 |
1181, 835, 616, 752, 663, 767 s | not split out; see the 3/6 reading
above |

**After** (seat-appended from the dev's report `6040381091` on objectstack-ai#22077;
this PR's `pull_request` run, CI `37635720375` / Lint `37635720510`,
head `cbb948b926`). Step seconds from the jobs API:

| step | before (`main` push, base `3d9188502e`) | after (this PR) |
|:--|--:|--:|
| Type Check · workspace `Build workspace packages` | 297 s | 1 s |
| Type Check · debt ledger: build, then ledgered build | 335 s, then 31
s | 1 s, then 1 s |
| Type Check · consumer gates: build, then nested build | 330 s, then 30
s | 1 s, then 1 s |
| dogfood 1/3, 2/3, 3/3 closure build | 200, 336, 201 s | 1, 2, 1 s |
| Temporal: driver-sql, non-SQL, metadata-protocol, runtime | 1, 35, 0,
79 s | 3, 3, 1, 3 s |
| Build Core (wiring unchanged) | 6 s | 7 s |
| Test Core new `Build this shard's dependency closure`, shards 1-6 | —
| 1, 0, 0, 0, 0, 0 s |

Most of that main-to-PR delta is the local `actions/cache` seed, which
this run restored from the base push. The **attributable** reading is
the same-base control, Lint `37632788013` (PR objectstack-ai#22084: no remote, same
seed), against Lint `37635720510`:

- debt `Build the ledgered packages' dependencies`: control `Remote
caching disabled` / `Cached: 66 cached, 71 total` / `Time: 29.447s`;
this PR `Remote caching enabled` / `Cached: 71 cached, 71 total` /
`Time: 527ms >>> FULL TURBO`;
- consumers nested build: control `Cached: 66 cached, 71 total` / `Time:
29.819s`; this PR `Cached: 71 cached, 71 total` / `Time: 558ms >>> FULL
TURBO`.

The 5 tasks the control executed lie outside the plan the typecheck seed
is saved from, so the remote served them here. Turbo's lines for the
dogfood, workspace, driver-sql and non-SQL steps are NOT MEASURED: they
fall outside the last 5000 log lines the read path returns. The
**merge-queue reading is pending**: the seat takes it from this PR's own
queue entry at landing and records it.

How to read the after numbers: a PR's restored `actions/cache` seed
comes from the latest `main` push. A cache hit does not say whether the
local seed or the remote served it. So the clean signal is the next
`main` push after landing: today Build Core takes 6 s there while the
Type Check lanes take about 300 s.

## Kept as ruled

- ⛔ No job removed, no required context changed, no new gate.
`check:required-contexts` is green.
- ⛔ The local `actions/cache` restore and save steps are untouched. They
stay until the remote hit rate is measured.
- ⛔ No new secret, and nothing written from a PR context.
- `turbo.json` and `scripts/**` are not touched.

## Local gates

The gates come from `node scripts/pm/dispatch-gates.mjs --commands
--repo objectstack-ai/objectstack` on this diff: 52 commands. Each exit
code was written to disk before it was read. The full table, with exit
codes and the `--ran` reconciliation, is in the `os-dev-report` on
objectstack-ai#22077. The workflow readers were green before this PR opened:
`check-ci-filter-parity`, `check-self-test-workflow-commands`,
`check-step-collectors`, `check-self-test-wired`,
`check-aggregator-roster`, `check:stall-guard-budget`,
`check:workflow-step-name-quoting`, `check:required-contexts`,
`check:pnpm-filter-targets` and `check:pm-expected-skips`.

## Acceptance notes

- `Dogfood Verify CLI` and `Console Pin Gate` also run turbo builds. The
card does not name them, so they are not wired here.
- The `Type Check · source gates` lane runs no turbo build, so it has
nothing to wire.
- This PR changes only `.github/workflows/**`, which publishes nothing.
`skip-changeset` applies.

---
_Generated by [Claude
Code](https://claude.ai/code/session_01VF48aw8RPG6wzDnMgp6rtw)_

Co-authored-by: Claude <noreply@anthropic.com>
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