Sitelet https://github.com/objectstack-ai/objectstack/pull/12374
Skip to content

ci(lint): a turbo.json package task must name a real package and a real script - #12374

Draft
yinlianghui wants to merge 2 commits into
mainfrom
claude/issue-12046-turbo-task-graph-coverage
Draft

ci(lint): a turbo.json package task must name a real package and a real script#12374
yinlianghui wants to merge 2 commits into
mainfrom
claude/issue-12046-turbo-task-graph-coverage

Conversation

@yinlianghui

@yinlianghui yinlianghui commented Aug 25, 2026

Copy link
Copy Markdown
Collaborator

Part of #12046

Adds check:turbo-task-graph — the first gate in the repo whose declared population is turbo.json.

⚠️ Two code spans in the first version of this body were eaten by GitHub's body sanitizer (turbo run [task] --dry=json and one pkg#typecheck, both short angle-bracket fragments even inside backticks — the AGENTS.md "GitHub mutates body BYTES" clause, reproduced). They are spelled with square brackets below.

The measurement the card asked for

The card's central reading holds. check-cross-package-test-inputs.mjs' Layer B (lines 1345–1362) iterates CROSS_PACKAGE_TEST_INPUTS and looks up turbo.tasks[`${name}#test`] — nothing else. A #typecheck task is structurally invisible to it.

Option 1 (widen Layer B) was measured and rejected as dishonest. That table's own header states what it declares: "which packages' tests read outside their own directory", and the escaping detector that keeps it honest walks *.test.* files only. There is no comparable population for #typecheck or #build, so widening Layer B would mean demanding that a pkg#typecheck task carry test-derived globs — a population the gate does not police. That is the #11556 shape from the other side, which the card names as the thing not to do.

A finding the card did not have: option 1 would not have closed the derivation half either. extractWatchHints refuses a literal with no path separator, and check-cross-package-test-inputs.mjs reaches this file as join(REPO_ROOT, 'turbo.json'). Measured:

"turbo.json"                 -> hints []                | covers turbo.json: false
"./turbo.json"               -> hints []                | covers turbo.json: false
"$TURBO_ROOT$/turbo.json"    -> hints []                | covers turbo.json: false
"turbo.json/**"              -> hints ["turbo.json/**"] | covers turbo.json: true

So whichever gate ends up owning turbo.json has to declare the subtree spelling — the ROOT_FILE_WATCH_HINTS idiom eight gates already use for AGENTS.md/**. This one does.

Before, at 1f6b8bb193:

$ node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack turbo.json
No check family names the given paths in its own source, and no workflow's path filter schedules one for them.

After, on this branch:

Local gates for this card (paste into the dispatch prompt):
  - pnpm check:turbo-task-graph   [lint.yml]   matched via turbo.json ⇢ gate source 'turbo.json/**'

What the gate judges, and why that population and not a wider one

Turbo itself decides the split. Measured on turbo 2.10.10, on a fixture workspace, each case a real turbo run [task] --dry=json:

turbo.json shape turbo's answer
@fx/nope#build — package does not exist exit 0, no diagnostic, key never reaches the graph
@fx/a#typecheck — package has no such script exit 0, task shown in the dry-run graph, never runs
dependsOn: ["prebuildxyz"] — unresolvable exit 1, x Could not find "@fx/a#prebuildxyz" in root turbo.json or "prebuildxyz"

The gate takes the two silent rows and deliberately leaves dependsOn to turbo, which polices it better — re-checking it would be this gate claiming a population it does not own, which is the failure one level up.

What a silent entry costs is intent, not a red build: misspell the package or the script and the override is not applied, the task falls back to the generic definition, and every signal the author has says the edit landed.

Package membership comes from scripts/workspace-enumerator.mjs — the repo's one parse of pnpm-workspace.yaml. Importing it grants this gate no path population (that module declares none, deliberately), so turbo.json/** stays the whole of what this file names.

What it deliberately does NOT judge — stated, not discovered later

Generic keys (no #) are out of population, and the header says why with the number: test:e2e is defined in turbo.json with outputs: ["playwright-report/**", …] and held by zero of the 78 workspace packages. The real Playwright script in examples/app-showcase is spelled test:smoke, which turbo.json does not configure at all. Repairing that needs an edit to turbo.json, outside this card's declared file surface, and a gate that ships red is worse than no gate. Filed as #12373; widen the population in the same change that repairs the entry.

Ablation — both directions, restored under trap … EXIT INT TERM

No build is involved: this gate is a plain Node script that reads source at runtime, so there is no dist/ for a mutation to hide in and ablation-dist-preflight does not apply. Every mutation was confirmed on disk by grepping both the injected and the removed text before the reading was taken.

Subject mutations (turbo.json), gate unmodified:

leg on-disk confirm production
@objectstack/plugin-auth#typecheck@objectstack/plugins-auth#typecheck injected 1, removed 0 exit 1, names the key and suggests @objectstack/plugin-auth
@objectstack/spec#test@objectstack/spec#typechek injected 1, removed 0 exit 1, names the key and prints the 51 scripts spec does declare

Rule mutations on a clean tree — the #11150 property, that a matching rule's regression is invisible to production because the empty finding set is the fixed point of shrinking:

leg production --self-test
unknown-package arm disabled exit 0, GREEN exit 1an unknown package in a task key is a finding, the unknown-package finding suggests the near miss
missing-script arm disabled exit 0, GREEN exit 1a task the package has no script for is a finding, the missing-script finding prints what the package DOES declare
ROOT_FILE_WATCH_HINTS reworded to 'turbo.json' exit 0, GREEN exit 1the root file is declared in the SUBTREE spelling; and dispatch-gates turbo.json collapses back to "No check family names the given paths"

The first rule ablation initially killed the self-test with a TypeError instead of reporting named cases — the #12273 shape, where a mutation aborts the suite exactly when the instrument is being read. The two arms are now independent (scripts && !scripts.has(task)) so each ablation yields named failures; the table above is the re-run after that change.

Restore proof, both files, git hash-object before vs after: byte-identical (f79ad7085715… / a47b23984253…), and git diff --stat -- turbo.json empty.

Verification

Gate union re-derived on the real diff with node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack (no hand-written path list), at 86b3bcc195 — all 26 derived families run, exit codes captured before any pipe:

  • check:turbo-task-graphOK: 21 package-scoped turbo task(s) judged against 78 workspace package(s), self-test all cases passed
  • check:cross-package-test-inputs, check-ci-filter-parity, check:entry-guard, check:parse-guard, check:agent-test-spelling, check:bash32-floor, check:cli-command-ids, check:pnpm-filter-targets, check:pnpm-acquisition, check:node-version, check:nul-bytes, check:required-contexts, check:aggregator-roster, check:step-collectors, check:self-test-wired, check:self-test-workflow-commands, check:whole-set-label-write, check:workflow-status-functions, check:shard-attestation, check:pm-dispatch-gates (664 cases), scripts/pm/ci-failure.mjs --self-test, scripts/docs-audit/check-drift-comment.mjs — all exit 0
  • check:type-check-coverage and check:type-check-debt --re-measure on a built closure, under the shared verify lock: 32 ledger entr(ies) re-measured in 287.0s, 1843 raw tsc error(s) total, none above its recorded number
  • pnpm lint (eslint . --no-inline-config, the full repo, not a narrowing) — exit 0, under the lock

bare-root-worklist owes nothing here. --self-test: 46 live row(s), 39 unreachable as spelled, 39 recorded verdict(s) — none stale, none missing, unchanged with this gate present. The species is a population constant holding a bare single-segment top-level directory word; turbo.json/** carries a separator (so bareRootLiterals skips it as already visible), turbo.json is a file rather than a directory, and this gate names no bare root anywhere. So scripts/pm/bare-root-worklist.mjs is untouched.

No changeset: root package.json is private: true and nothing published changes — skip-changeset, matching #12346 / #12347 / #12338, which shipped the same way. ⚠️ The label itself could not be applied from this seat: api.github.com refuses direct REST here (GitHub access is not enabled for this session), and the MCP write path resolves a PR number as an Issue and fails. Needs a hand.

Generated by Claude Code

claude added 2 commits August 25, 2026 20:24
…al script

`turbo.json` is the task graph for the whole monorepo and no gate read it
structurally except one narrow limb — `check:cross-package-test-inputs`' Layer
B, which inspects only the `<pkg>#test` keys of the packages declared in
`scripts/cross-package-test-inputs.mjs`. Every other key was judged by nothing.

Turbo does not cover the difference. Measured on turbo 2.10.10 against a
fixture workspace, each case a real `turbo run <task> --dry=json`:

  `@fx/nope#build` (no such package)  exit 0, no diagnostic, key never reaches
                                      the graph
  `@fx/a#typecheck` (package has no
   such script)                       exit 0, task shown in the dry-run graph,
                                      never runs
  `dependsOn: ["prebuildxyz"]`        exit 1, `x Could not find
                                      "@fx/a#prebuildxyz"`

So the two silent shapes are the population and `dependsOn` is deliberately
left to turbo, which polices it better.

The gate also closes the derivation half of the same finding. It declares
`turbo.json/**` — the subtree spelling `AGENTS.md/**` already uses for a
repo-root file — so a card whose surface is `turbo.json` now derives a family
where it derived zero before.

Part of #12046

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UjM2ia8Av1v5NqfqQEQmC6
…te leaves out

The header stated the deliberate omission and its live violation
(`test:e2e`, held by zero of 78 packages) but had nothing to point at.

Part of #12046

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UjM2ia8Av1v5NqfqQEQmC6
@github-actions github-actions Bot added size/m ci/cd dependencies Pull requests that update a dependency file labels Aug 25, 2026
@yinlianghui yinlianghui added skip-changeset PR has no user-facing published change; bypasses the changeset gate and removed ci/cd dependencies Pull requests that update a dependency file size/m labels Aug 25, 2026 — with Claude
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

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