Skip to content

ci: the core filter starts Build Core for a build-input-only diff, held to turbo.json by check-ci-filter-parity - #21338

Merged
objectstack-fleet[bot] merged 5 commits into
mainfrom
claude/issue-21202-core-filter-build-inputs
Oct 2, 2026
Merged

objectstack-fleet[bot] merged 5 commits into
mainfrom
claude/issue-21202-core-filter-build-inputs

Conversation

@objectstack-fleet

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

Copy link
Copy Markdown
Contributor

Fixes #21202
Clause-②: no

What changes

  1. .github/workflows/ci.yml, the core: filter gains 18 literal entries: turbo.json, the root tsup.config.ts, and the 16 scripts/*.mjs files that a build task Build Core runs declares as a $TURBO_ROOT$/... input. A PR that changes only one of them now starts Build Core at PR time. Before this change the merge queue was the first place that build ran (PR build(turbo): build task hashes cover the root scripts each build runs (#21193) #21199, a turbo.json-only diff, skipped Build Core).

  2. scripts/check-ci-filter-parity.mjs gets a third subject, judgeBuildInputs. It derives the required set and does not keep a copy:

    • from turbo.json: the file itself, its globalDependencies, and every $TURBO_ROOT$/... input of a build or PKG#build task;
    • minus the builds that Build Core's run: pnpm build skips. Those are read from the root build script's --filter=!PKG tokens (today @objectstack/docs#build and its content/** input). If the script is misread, the gate can only drop an exclusion, which makes it require more of core. So a misread is a loud red, never a silent gap.
    • It reds when a declared build input has no covering core entry.
    • It also checks the reverse direction where the declarations alone can decide it. A literal core entry sitting in a directory that holds declared build inputs, but covering none of them, reds as a leftover.
    • It refuses eight unread states: no build-core job; its if: no longer names core; no run: pnpm build step; turbo.json or the root manifest unreadable or not JSON; no tasks map; no build task; a build script that is not turbo run build ....
    • It uses the same pure-string coverage rule as the existing crosspkg subject, so no third matcher is added.
  3. Two forced follow-ons, both outside the claim's declared file surface. Each is the remedy text of a gate that this change turned red. Neither is a design choice.

    • Build Core gains node scripts/check-dts-emitted.mjs --self-test. Once core: named that file, check:self-test-wired reported that a workflow names a script whose --self-test no workflow runs. That is true: 67 package build commands end with that checker, and its self-test ran nowhere in CI. The gate's remedy is "wire it into the step that runs it". The step takes milliseconds, needs no prerequisites, and runs before pnpm build.
    • scripts/pm/dispatch-gates.mjs, one self-test specimen. The case "a name-shaped script no family runs is NOT a gate script" used scripts/check-dts-emitted.mjs, which item 3a turned into a family file. The specimen is now scripts/check-test-typecheck.mts, which only package manifests run. That file is already named in the same docblock's list of such files.
  4. Three comments this change made false, corrected in e6aa08f322 (patch round 2, comments only): the GAP 1 block in scripts/sync-template-versions.mjs, the same block in .github/workflows/lint.yml, and the extglob docblock sentence in scripts/pm/dispatch-gates.mjs ("one entry of six"). Each now reads true with core matching the root build inputs. The re-measure for a diff confined to scripts/sync-template-versions.mjs reads core=true, crosspkg=true.

No change to turbo.json or fleet-write.yml. lint.yml changes only its GAP 1 comment block (about lines 2660 to 2675), which is disjoint from #21295's pm roster block.

Granularity: literal files, not scripts/**

I measured this over the 3288 first-parent commits of main in the 30 days to 1371dc980c, with scripts/pm/git-history.mjs proving the window was present (floor 2026-06-27). I used picomatch { dot: true }, the options dorny/paths-filter uses, under both 2.3.1 and 4.0.5. The two versions agree on every commit.

core filter commits it matches added by this change
before 2247 —
+ the 18 literal build inputs (this PR) 2275 +28
+ scripts/**, turbo.json, tsup.config.ts 2840 +593

516 of the 593 that scripts/** would add touch a scripts/pm/ tool or a scripts/check-* gate. The gate holds the literal list to the declarations, so the narrow choice costs nothing in drift.

Widening core starts every job that core gates, not only Build Core: Test Core, Temporal Conformance and Dogfood also run on those 28. That matches turbo's own view, because every one of those jobs builds, and a moved build hash re-runs everything downstream of it.

Acceptance: the filter evaluated per path

The core list is parsed out of ci.yml at the base and at this head, in the action's order (YAML, then the filters string). Each path is matched with picomatch { dot: true }.

path before (2.3.1 / 4.0.5) after (2.3.1 / 4.0.5)
turbo.json false / false true / true
tsup.config.ts false / false true / true
each of the 16 scripts/*.mjs build inputs false / false true / true
tsconfig.json (globalDependencies) true / true true / true
packages/cli/src/commands/init.ts (create-objectstack#build) true / true true / true
control: scripts/check-ci-filter-parity.mjs false / false false / false
control: scripts/pm/dispatch-gates.mjs false / false false / false
control: content/docs/index.mdx false / false false / false
control: packages/spec/src/ui/view.zod.ts true / true true / true

The 16 scripts are each checked individually: build-input-hash, check-dev-prereqs, check-dts-emitted, check-dts-references, check-regen-pending, cli-build-prerequisite, git-env, import-prerequisite, invoked-as, js-comment-mask, regen-artifacts, sync-scaffold-emission-policy, sync-template-versions, ts-parse, tsup-drop-sources-content, workspace-enumerator.

Live run: none yet. This PR changes ci.yml, which core already matched, so its own run proves nothing. The first demonstration will be the first PR after this one lands whose diff is confined to listed paths. At the measured rate that is about one a day (28 in 30 days). The turbo.json card #20790 is the known candidate if its PR touches only turbo.json.

The new leg can go red

  • Self-test battery (9), 33 cases; the gate goes from 68 to 101 assertions.
    • A build input dropped from a fixture core: reds, naming the path and the task that declared it.
    • turbo.json itself, a globalDependencies entry, and an input under a new root are each required.
    • Test-task inputs, negated inputs, and the excluded docs build are left out. The docs build's input is required again once the build script stops excluding it.
    • A leftover entry reds.
    • All eight refusals are covered (ten cases).
    • The checked-in tree is green. With scripts/invoked-as.mjs dropped from it, the gate reds through main(), the report path.
  • Battery (8)'s report-path scratch root now carries the real turbo.json and root manifest, so a red there cannot come from the new subject refusing a missing file.
  • Before/after on the real tree. The extended gate run over the base ci.yml exits 1 and lists the 18 inputs. Over this head it exits 0.
  • Ablations, done with scripts/ablation-replace.mjs. Each mutation was confirmed on disk, and each restore was proven by blob equal to HEAD and an empty git diff HEAD:
    • Coverage forced true in judgeBuildInputs: production exit 0 (green), --self-test exit 1 with 9 named failures.
    • Leftover rule disabled: production exit 0, --self-test exit 1 with 1 named failure.
    • My first attempt at the leftover ablation was a no-op. Its replacement text contained the anchor, so the tool refused and wrote nothing. I re-ran it with a non-overlapping replacement.

Gates (local, head ec413ce)

  • I re-derived the list with node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands (no paths; merge base 1371dc980): 58 commands.
  • --ran reconciliation: "58 derived famil(ies) accounted for — 54 run, 4 NOT-MEASURED (0 DERIVED from a recorded exit 3, 4 claimed)". Every one of the 54 recorded exit 0.
  • That includes check-ci-filter-parity and its --self-test, check:self-test-wired, bare-root-worklist --self-test, check:required-contexts, check:workflow-step-name-quoting, check:step-collectors, check:stall-guard-budget and check:nul-bytes.
  • It also includes check:pm-dispatch-gates, whose self-test passed 1976 cases (778 s on this box). Its "fabrication direction" case passes with the new specimen.
  • The four dist/-reading Build Core gates (check:dts-closure, check:dual-build-cjs-loads, check:lean-entry-closure, check:sourcemap-no-sources-content): their --self-test legs exit 0. Their real legs are NOT MEASURED locally, because they read a full workspace dist/ and this diff moves no build hash. The changed files are a workflow and two root scripts, none of them a declared build input. Build Core runs them on this PR.

Changeset

None. The changed files are a workflow, a root script and a scripts/pm/ tool. None of them is in any published package's files[], and the root package is private. skip-changeset applies.

Acceptance notes (not changed here)


Generated by Claude Code

claude added 4 commits October 2, 2026 04:29
… by check-ci-filter-parity

The `core` paths-filter now lists turbo.json, the root tsup.config.ts and
each `$TURBO_ROOT$/scripts/...` input of a build task Build Core runs, so a
diff confined to a build input starts Build Core at PR time instead of
first building in the merge queue.

check-ci-filter-parity gains a third subject that derives the required set
from turbo.json (itself, globalDependencies, and every root input of a
`build` / `<pkg>#build` task) minus the packages the root `build` script
excludes, and reds while one is uncovered by `core:` or while a `scripts/`
entry in `core:` covers no declared build input.

Claude-Session: https://claude.ai/code/session_01UtnxvdiN376GF3sgXwAw4d
Co-authored-by: Claude <noreply@anthropic.com>
…put leg can go red

Battery (9), 33 cases: a build input dropped from a fixture `core:` reds by
path and declaring task; turbo.json, a globalDependencies entry and a
new-root input are each required; test-task and negated inputs and the
excluded docs build are left out until the build script stops excluding it;
a stale `scripts/` entry reds; ten refusals; and the checked-in tree is
green, with one input dropped red through the report path. Battery (8)'s
report-path scratch root now carries the real turbo.json and root manifest,
so its red cannot come from the new subject refusing a missing file.

Claude-Session: https://claude.ai/code/session_01UtnxvdiN376GF3sgXwAw4d
Co-authored-by: Claude <noreply@anthropic.com>
…is derived from the declarations

Naming scripts/check-dts-emitted.mjs in the `core:` filter made
check:self-test-wired see a workflow name a script whose `--self-test` no
workflow runs. That is true, not a mis-read: 67 package build commands end
with the checker, and its self-test ran nowhere in CI. Build Core now runs
it before `pnpm build`.

check-ci-filter-parity's reverse direction no longer keys on a bare
`scripts` root constant: a literal `core` entry is a leftover when it sits
in a directory holding a declared literal build input and covers none of
them, which the declarations alone decide.

Claude-Session: https://claude.ai/code/session_01UtnxvdiN376GF3sgXwAw4d
Co-authored-by: Claude <noreply@anthropic.com>
…no family runs again

The case pins that a check-named script no CI family runs is not a gate
script. Its specimen was scripts/check-dts-emitted.mjs, which became a
family file when Build Core began running its --self-test. The specimen is
now scripts/check-test-typecheck.mts, which only package manifests run.

Claude-Session: https://claude.ai/code/session_01UtnxvdiN376GF3sgXwAw4d
Co-authored-by: Claude <noreply@anthropic.com>
…s.mjs-only diff

scripts/sync-template-versions.mjs and lint.yml's GAP 1 comment both said
`core` is false for a diff confined to that script and "was never widened".
Since the core filter lists every root build input turbo.json declares,
and the script is one (create-objectstack#build), `core` is true for that
diff; re-measured with picomatch 2.3.1 and 4.0.5: core=true, crosspkg=true.
dispatch-gates.mjs's extglob docblock no longer counts `core` as six
entries.

Comments only.

Claude-Session: https://claude.ai/code/session_01UtnxvdiN376GF3sgXwAw4d
Co-authored-by: Claude <noreply@anthropic.com>
@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 2, 2026 06:08
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 2, 2026 06:08
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 2, 2026
Merged via the queue into main with commit 57c9fe3 Oct 2, 2026
42 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-21202-core-filter-build-inputs branch October 2, 2026 06:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

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

Projects

None yet

2 participants