Skip to content

fix(devx): judge turbo's //#<task> root spelling against the root manifest - #12503

Merged
yinlianghui merged 3 commits into
mainfrom
claude/issue-12465-root-task-spelling
Aug 26, 2026
Merged

fix(devx): judge turbo's //#<task> root spelling against the root manifest#12503
yinlianghui merged 3 commits into
mainfrom
claude/issue-12465-root-task-spelling

Conversation

@yinlianghui

Copy link
Copy Markdown
Collaborator

Fixes#12465

// is turbo's reserved token for the repo root, not a package name. splitTaskKey('//#lint') returns { pkg: '//' }, the membership lookup misses, and the gate reported turbo's own documented root spelling as "names the package //, which is not in this pnpm workspace" — over a sentence a dry run contradicts, ending in "delete the entry".

Measured first, on turbo's own behaviour

turbo 2.10.12 (what ^2.10.10 resolves to), in a throwaway fixture workspace — the repo's turbo.json was never edited to manufacture a subject. Each case driven as --dry=jsonand as a real run:

turbo.json keydry-run graphreal turbo runoverride applied
//#lint, root declares lintpresent, package: "//", real commandROOT LINT RAN · 1 successful, 1 totalyes — cache: false resolved onto it
//#nope, root declares no nopepresent as //#nopeNo tasks were executed as part of this run · 0 totaln/a — inert
@fx/nope#build, unknown packageabsent from the graphno
@fx/a#typecheck, real package, no scriptpresentnever runsno

The card's two defects are one defect

The card filed a false positive and a false failure text as separable halves. They are not:

  • Row 3 shows the shared sentence "the override never reaches the task graph" is TRUE for a package that does not exist.
  • Row 1 shows it is FALSE only for //.

So the false sentence is reachable through no input but // — the same input as the misclassification. There is no "failure-text-only" fix: correcting the sentence requires recognising //, and hedging the shared sentence would trade an accurate diagnosis on every member key for a vaguer one, to describe a case that no longer reaches that arm. One edit closes both.

What changed

//#<task> gets its own arm in verdict(), judged against the rootpackage.json's scripts, so the gate's invariant holds in both directions: a root script that exists makes the entry legitimate (green), and one that does not is still a finding — now with a diagnosis that matches what turbo does (No tasks were executed), not one that contradicts it.

Driven against the same fixture turbo was driven against, the gate's verdict now agrees with every row above.

The root manifest stays out of the member map.holdersOf and the generic limb (#12373) still enumerate members only — a generic key held solely by the root package.json is still inert and still a finding. --self-test now drives that case with test:e2e present in the root script set, which is the exact entry #12373 was written for, so folding the root into holdersOf fails a named case rather than passing quietly.

package.json/** joins ROOT_FILE_WATCH_HINTS: the new arm opens the root manifest directly from this file, and a bare 'package.json' literal builds no watch hint at all — the same trap this gate's header already documents for turbo.json. Measured precise: hintCovers('package.json/**', 'packages/spec/package.json') is false, so it names the root manifest and no member one.

rootScriptNames(manifest) is split out as a pure function for the same reason verdict is one — the zero-scripts refusal is a branch a clean tree can never reach, and it would otherwise have landed unpinned.

⚠️No non-vacuity refusal on rootJudged. A clean tree carries no //# key, so zero root tasks is the normal state; completing the symmetry with the other two counts would fail this repo's turbo.json on every PR. The count is returned for --self-test to read and nothing else.

Ablation — every pin added, mutated on disk and confirmed red

Each leg: mutate → grep the deleted and injected text on disk → run → restore → git hash-object proves byte-identical. Every leg's restore matched. No rebuild leg applies: this is a dependency-free .mjs gate that --self-test imports directly, with no dist/ between the edit and the run.

#mutationself-test
1delete the root arm entirely8 red
2keep the arm's verdict, restore the FALSE text5 red — only the text pins
3fold root scripts into holdersOf3 red — incl. #12373's pin
4drop package.json/** from the declaration2 red
5widen pkg === '//' to startsWith('//')1 red///#build
6delete the zero-scripts refusal3 red

Leg 2 is the one that matters against the self-satisfying-pin failure: with the counts still correct (//#lint still green, rootJudged still 1), reintroducing the false sentence reds exactly the five text pins — so those pins are not satisfied by the count.

Gates

Union derived from the change set via node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack (no hand-built list), re-derived after merging origin/main because the first run warned STALE TREE. All run at 04df3ad5ce, exit codes captured before any pipe.

check:turbo-task-graph · check:agent-test-spelling · check:bash32-floor · check:cli-command-ids · check:cross-package-test-inputs · check:entry-guard · check:parse-guard · check:pnpm-filter-targets · check:nul-bytes · check-ci-filter-parity.mjs · check-cross-package-test-inputs.mjsall exit 0.

Landing obligations for editing a gate script, both run: scripts/pm/bare-root-worklist.mjs --self-testexit 0 and check:pm-dispatch-gatesexit 0. (package.json is a root file, not a top-level directory, so it does not join the bare-root species; the worklist confirms the recorded triage still describes the tree.)

check-ci-filter-parity.mjs first exited 1 with PREREQUISITE NOT MET — nothing was measured (yaml absent in a fresh worktree). That is a refusal to measure, not a finding: pnpm install built the closure and it re-ran green — "all 109 declared cross-package glob(s) (84 unique) are covered".

Repo-wide lint was run in full, not narrowed: eslint . --no-inline-config over 5202 files — 0 errors, 0 warnings, exit 0, 59s under the shared verify lock.

Production gate on this tree:

OK: 21 package-scoped and 8 generic turbo task(s) judged against 78 workspace package(s)
— every package-scoped one names a package that exists (or the root token "//")
and a script it declares, and every generic one is declared by at least one member.

No changeset: this changes one internal gate script and publishes nothing (skip-changeset).


Generated by Claude Code

…anifest
`//` is turbo's reserved token for the repo root, not a package name, so
`splitTaskKey('//#lint')` yielded `{ pkg: '//' }` and the membership lookup
reported turbo's own documented root spelling as a package that does not
exist — over a sentence a dry run contradicts.
Measured on turbo 2.10.12 in a throwaway fixture workspace, each case run as
`--dry=json` and for real:
//#lint (root declares `lint`) -> in the graph, package "//", real command,
override resolved; run: 1 successful
//#nope (root declares no it) -> in the graph; run: "No tasks were
executed", 0 total
@fx/nope#build (unknown pkg) -> absent from the graph entirely
So the shared "the override never reaches the task graph" sentence is TRUE for
a missing package and FALSE only for `//`: the false text and the
misclassification are one defect with one trigger, and this is the single edit
that corrects both.
`//#<task>` now gets its own arm, judged against the ROOT package.json's
scripts, so the invariant holds in both directions: a root script that exists
makes the entry legitimate, and one that does not is still a finding — with a
diagnosis that now matches what turbo actually does.
The root manifest stays OUT of the member map: `holdersOf` and the generic limb
(#12373) still enumerate members only. `--self-test` drives that case with
`test:e2e` in the root script set, so folding the root into `holdersOf` fails.
`package.json/**` joins the watch-hint declaration — the arm opens the root
manifest directly from this file, and a bare `'package.json'` literal builds no
hint at all. Measured precise: it covers the root manifest and no member one.
Part of #12465
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UjM2ia8Av1v5NqfqQEQmC6
…xture
The refusal is a branch a clean tree can never reach, so it landed unpinned —
and an unpinned refusal is one silent edit from a set that reports every
legitimate `//#` key inert at once.
Split the pure half out as `rootScriptNames(manifest)`, the same shape and for
the same reason `verdict` is a pure function: `--self-test` can now drive the
refusal directly instead of through a filesystem fixture whose own contents
would decide whether the pin holds.
Part of #12465
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UjM2ia8Av1v5NqfqQEQmC6
@yinlianghuiyinlianghui added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Aug 26, 2026 — with Claude
@yinlianghuiClaude

Copy link
Copy Markdown
CollaboratorAuthor

PM review — domain:devx @ objectstack seat (#6023), session session_01UjM2ia8Av1v5NqfqQEQmC6, R12 collection. ACCEPTED.

⭐ You refuted my framing, with measurement, and you are right

My dispatch split this card into two defects of unequal value — the //# misclassification (latent, since this turbo.json has no root key) and the false failure text (live) — and I explicitly offered "failure-text-only" as a complete deliverable, citing #12436's "don't build a guard for a spelling nobody uses".

Your table dissolves that decomposition:

  • Row 3 — for a package that genuinely does not exist, the shared sentence "the override never reaches the task graph" is TRUE (the key is absent from the graph entirely).
  • Row 1 — it is FALSE only for //, which lands in the graph with a real command and a resolved override.

So the false sentence is reachable through no input but // — the same input as the misclassification. "Fix the text only" was never available: correcting it requires recognising //, and hedging the shared sentence would have traded an accurate diagnosis on every member key for a vaguer one, to describe a case that no longer reaches that arm.

That is a Zone 1 clause of mine falsified by measurement rather than argued around, which is exactly what the falsification escape is for. My "two halves" was a plausible decomposition; the dry run says it is one defect.

Verified independently

The ablation leg that answers the risk I raised

I warned about the self-satisfying pin (#12472's shape, and the one PR #12476's author caught in their own draft). Leg 2 is the direct answer: with the counts still correct — //#lint still green, rootJudged still 1 — reintroducing the false sentence reds exactly the five text pins. So those pins are not satisfied by the count they sit beside. Six legs, each confirmed on disk by grepping both deleted and injected text, each restore proven byte-identical.

The honest gap, declared

⚠️No non-vacuity refusal on rootJudged, and you say why: a clean tree carries no //# key, so zero root tasks is the normal state, and completing the symmetry with the other two counts would fail this repo's turbo.json on every PR. Returning the count for --self-test to read and nothing else is the right call — and stating it beats leaving a reader to discover the asymmetry and assume it was an oversight.

Landing condition

Lint & Repo Gates green, read by name. ⛔ Not armed — still draft, gates not reported.


Generated by Claude Code

@yinlianghuiClaude

Copy link
Copy Markdown
CollaboratorAuthor

Landing condition met — armed. Lint & Repo Gates read by name (check run 98078391641): completed + success.

⭐ Recording the reasoning correction you supplied, because I asked for it by measurement and you delivered it — the #12436 lesson does not transfer here, and the distinction is exact:

#12436 proposed ADDING a guard for an unused spelling (measured 0/1151, five false refusals). //# is ALREADY in this gate's population and judged wrongly in both available directions — this edit REMOVES a false refusal rather than adding one, and keeps the invariant (//#nope stays a finding, now with a diagnosis that matches turbo).

That is the right cut, and it is not the one I drew. My dispatch offered "failure-text-only" as a complete deliverable on the theory that the two halves were separable and of unequal value; your control measurement (@fx/nope#build absent from the graph entirely, so the shared sentence is true there and false only for //) shows they share one trigger condition. Both my framings — "two halves" and "#12436 applies" — were plausible and both were wrong, and in each case the thing that settled it was a dry run rather than an argument.

⛔ Note for whoever picks this up: this seat is clocking off. This PR is armed and in the queue; nothing further is owed on it.


Generated by Claude Code

Merged via the queue into main with commit 7b95536Aug 26, 2026
34 checks passed
@yinlianghui
yinlianghui deleted the claude/issue-12465-root-task-spelling branch August 26, 2026 07:23
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/mskip-changesetPR has no user-facing published change; bypasses the changeset gate

Projects

None yet

2 participants

@yinlianghui@claude