Skip to content

[finding] check:turbo-task-graph reports turbo's legitimate //#<task> root-task spelling as an unknown package, and its failure text asserts something a dry run contradicts #12465

Closed

Description

@yinlianghui

Measured while implementing #12373 (the generic-key widening of check:turbo-task-graph), on c246e4fcf6, session session_01UjM2ia8Av1v5NqfqQEQmC6. Filed unassigned; recording the gap, not claiming it. Nothing is red today — this repo's turbo.json carries no //# key, so the gate's population never meets this shape.

This is in the pre-existing package-scoped arm, not the generic limb #12373 adds. #12373's change does not touch it in either direction.

What was measured

turbo.json was temporarily given a real root task and driven through turbo run (turbo 2.10.10, restored byte-identical afterwards):

"//#lint": { "cache": false }
$ pnpm exec turbo run lint --dry=json
EXIT 0
root task in graph: ['//#lint']
  command: node --stack-size=4000 node_modules/eslint/bin/eslint.js . --no-inline-config

So //#<task> is a working turbo spelling: it binds the root manifest's own script and lands in the graph with a real command.

node scripts/check-turbo-task-graph.mjs on that same tree:

EXIT 1
FAIL: turbo.json carries 1 inert task entry.

  - "//#lint" names the package "//", which is not in this pnpm workspace.
    Turbo accepts the key SILENTLY — measured on 2.10.10: exit 0, no diagnostic,
    and the override never reaches the task graph. Everything this entry was
    written to configure is unconfigured.

Why this is worth recording

Two separate defects, and the second is the expensive one:

  1. A false positive on a legitimate shape. splitTaskKey('//#lint') returns { pkg: '//', task: 'lint' }, and // is not a workspace member because it is not a package name at all — it is turbo's reserved token for the repo root. The gate would refuse a correct entry.
  2. The failure text states something the tool contradicts. "the override never reaches the task graph" is true for the misspelled-package case the sentence was written for, and false for this one — the dry run above shows the task in the graph with a real command. A gate whose diagnosis is wrong sends the reader to delete a working entry.

The root manifest's non-membership is load-bearing elsewhere in this gate — #12373's generic limb turns on exactly that fact, and says so in its failure text. // is the one place where that same fact points the other way: the root is not a member, but turbo does address it, under a name the enumerator can never produce.

Suggested disposition (not a decision)

  • Recognise // in splitTaskKey (or before it) as the root manifest, and judge //#<task> against the root package.json's scripts rather than against the member map. This keeps the invariant ("a task nothing can run is inert") for the root too — //#nope where the root declares no nope is still a real finding.
  • ⛔ Do not close it by exempting // from judgement: that trades a false positive for a hole, in the file whose whole point is that nothing else reads it.
  • Whichever lands wants a --self-test case in both directions, since a clean tree contains no //# key to notice a regression with.

Refs: #12046 (the coverage card the gate was built under) · #12373 (measured while implementing it) · scripts/check-turbo-task-graph.mjs


Generated by Claude Code

Activity

  1. self-assigned this
    on Aug 26, 2026
  2. yinlianghui commented on Aug 26, 2026

    @yinlianghui
    ContributorAuthor

    Claim: domain:devx @ objectstack execution seat (#6023), session session_01UjM2ia8Av1v5NqfqQEQmC6, round R12. Branch claude/issue-12465-root-task-spelling.

    Unblocked: held serial behind PR #12468, which merged as c8d795a99e. scripts/check-turbo-task-graph.mjs is free.

    Premise checked at dispatch on origin/main, with an instrument control, because my first read was a zero:

    scripts/check-turbo-task-graph.mjs   present, 569 lines
      positive control: holdersOf / ROOT_FILE_WATCH_HINTS → 7 hits (so #12468's work IS on main)
    turbo.json  keys are all "@package#task"; keys beginning "//" → 0
    

    ⭐ That zero splits this card in two, and the halves are not worth the same.

    • The //# misclassification is LATENT. This turbo.json contains no root-task key today, so nothing is red and nothing can be. It is a defect that fires only if someone writes turbo's legitimate root spelling.
    • The failure text asserting something a dry run contradicts is LIVE. An instrument whose self-description is false is a real defect in this lane whether or not it has fired — the whole reason we file these.

    ⚠️ Read this before deciding how much to build. Earlier this round, card #12436 was closed not planned because its measured rate was 0/1151 and the gate it proposed would have caught nothing while producing five false refusals. That card was mine, and the lesson applies here with the same force: a guard for a spelling nobody uses is a maintenance cost with no payer. Do not assume the //# half must be implemented because it is written down.

    The routing question I want answered by measurement, not taste: is the honest deliverable the failure-text correction alone? If so, say so and deliver that — a smaller, correct PR beats a larger speculative one, and this seat will not read it as an under-delivery.

    Serial-queue check: no open PR claims scripts/check-turbo-task-graph.mjs. ⛔ Avoid scripts/pm/dispatch-gates.mjs and scripts/pm/bare-root-worklist.mjs (PR #12476 still queued on the latter).


    Generated by Claude Code

  3. yinlianghui commented on Aug 26, 2026

    @yinlianghui
    ContributorAuthor
    {
      "issue": 12465,
      "status": "done",
      "branch": "claude/issue-12465-root-task-spelling",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/12503",
      "premise_still_valid": true,
      "summary": "The card's premise holds in full: `splitTaskKey('//#lint')` yields pkg '//', the membership lookup misses, and the gate calls turbo's own root spelling a package that is not in the workspace while telling the author to delete the entry. Assumption B confirmed against merged code — '//#' still lands in the pre-existing package-scoped arm (judged=1, genericJudged=0); PR #12468's verdict() restructure did not move the arm boundary. ASSUMPTION A IS FALSIFIED AS FRAMED, and this changed the deliverable: the dispatch split the card into a LATENT misclassification and a LIVE false failure text, but a control measurement shows the sentence 'the override never reaches the task graph' is TRUE for a genuinely unknown package (@fx/nope#build is absent from the graph entirely) and FALSE only for '//' — so both halves have the identical trigger condition, a '//#' key existing, and neither is live today. There is therefore no coherent 'failure-text-only' deliverable: making the sentence true for the '//' case REQUIRES recognising '//', and hedging the shared sentence would weaken an accurate diagnosis on all 21 member keys to describe a case that no longer reaches that arm. On the #12436 question the seat asked me to answer by measurement: the lesson does not transfer, because that card proposed ADDING a guard for an unused spelling (0/1151, five false refusals), whereas '//#' 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). Scope respected: turbo.json was never edited to manufacture a subject; all turbo measurements ran in a throwaway fixture workspace under the scratchpad.",
      "tests": "All at final commit 04df3ad5ce (git rev-parse --short HEAD), exit codes captured before any pipe. TURBO MEASUREMENT (fixture workspace, turbo 2.10.12 = what ^2.10.10 resolves to; repo turbo.json untouched): '//#lint' with root script present -> --dry=json EXIT 0, tasks in graph ['//#lint'], package='//', command=the root script, cache=false override resolved; real `turbo run lint` printed 'ROOT LINT RAN' and 'Tasks: 1 successful, 1 total'. '//#nope' with no root script -> in graph as '//#nope', real run 'WARNING No tasks were executed as part of this run', '0 successful, 0 total'. CONTROL '@fx/nope#build' -> graph is ['@fx/a#build'], the bad key absent entirely. GATE-vs-TURBO AGREEMENT: driving the fixed gate's own readers against that same fixture gives //#lint problems=0 rootJudged=1, //#nope problems=1 (root diagnosis), @fx/nope#build problems=1 (unknown package), @fx/a#typecheck problems=1 (missing script) — matching all four turbo rows. GATE UNION derived by `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack` with no paths passed (script took its own change set from merge-base; first run warned STALE TREE, so origin/main was fetched and merged and the union re-derived — family list identical). Green, each verdict line read from the gate's own output: check:turbo-task-graph EXIT=0 ('OK: 21 package-scoped and 8 generic turbo task(s) judged against 78 workspace package(s)'), check:agent-test-spelling 0, check:bash32-floor 0, check:cli-command-ids 0, check:cross-package-test-inputs 0, check:entry-guard 0, check:parse-guard 0, check:pnpm-filter-targets 0, check:nul-bytes 0 ('scanned 6901 text file(s) ... no raw ASCII control bytes'), check-cross-package-test-inputs.mjs 0. check-ci-filter-parity.mjs first returned EXIT=1 'PREREQUISITE NOT MET — nothing was measured' (yaml absent in a fresh worktree); per the standing rule that is a refusal to measure and not a finding, so `pnpm install` built the closure under the verify lock (VERDICT command-exit 0, held 6s, waited 28s) and it re-ran EXIT=0 'OK: all 109 declared cross-package glob(s) (84 unique) are covered'. GATE-SCRIPT LANDING OBLIGATIONS both run: scripts/pm/bare-root-worklist.mjs --self-test EXIT=0 and check:pm-dispatch-gates EXIT=0 (neither file was edited; 'package.json' is a root FILE, not a top-level directory, so it does not join the bare-root species). REPO-WIDE LINT RUN IN FULL, not narrowed — no narrowing to declare: `eslint . --no-inline-config -f json` over 5202 files, 0 errors, 0 warnings, EXIT=0, 59s under the verify lock. ABLATIONS — six legs, every pin added. NO REBUILD LEG APPLIES and this is stated rather than skipped: the subject is a dependency-free .mjs gate that --self-test imports directly, with no package exports and no dist/ between the edit and the run, so there is no stale-artifact path for a false green. Each leg ran from the COMMITTED state through a runner carrying `trap 'git checkout -- FILE' EXIT INT TERM`, and each mutation was CONFIRMED ON DISK before any reading was taken — grep -cF of the deleted needle (required 0) and of the injected marker (required >=1), with the runner aborting as 'MUTATION DID NOT LAND, reading is void' otherwise; no leg aborted. Results: (1) delete the root arm -> 8 cases red; (2) keep the arm's verdict but restore the FALSE text -> exactly 5 red, all text pins, counts still green — this is the leg that proves the text pins are not satisfied by the count, the self-satisfying-pin hazard; (3) fold root scripts into holdersOf -> 3 red including #12373's own 'ROOT package.json does NOT count' pin; (4) drop package.json/** from the declaration -> 2 red; (5) widen pkg === '//' to startsWith('//') -> 1 red ('///#build'); (6) delete the zero-scripts refusal -> 3 red. Every restore leg verified byte-identical with git hash-object (b63f7b58e7... for legs 1-4, f2d4b9a601... for legs 5-6, before and after each).",
      "open_questions": [],
      "out_of_scope_findings": []
    }

    Notes for the PM, outside the machine-read block:

    • Nothing was filed as an out-of-scope finding. The only defect-shaped material I met was the standing banner dispatch-gates prints on every derivation for every card (9 families whose declared population's layout moved, 2 unreachable by construction). That is a repo-standing fact the PM's own tool re-prints on each dispatch rather than something this card surfaced, so filing it would duplicate rather than record. Flagging it here for your triage instead.
    • Process deviation, declared: both commits carry a Part of #12465 trailer, which the squash convention says to keep out of commit messages. They are consistent with each other and carry no closing keyword, so a squash produces no contradiction and closes nothing unintended; correcting it now would need a force-push, which is prohibited. The single Fixes #12465 lives in the PR body only.
    • The GitHub sanitizer stripped the <task> fragments from the PR title and body on write (//#<task> became //#). Documented behaviour, cosmetic here, and the reason this comment spells the shape without angle brackets.
    • Worktree and its node_modules were torn down after the PR was opened.

    Generated by Claude Code


    Generated by Claude Code

  4. yinlianghui commented on Aug 26, 2026

    @yinlianghui
    ContributorAuthor

    os-dev-report

    Supersedes comment 5421279923 — identical payload. That one was written with the HTML-comment marker as its first line and the sanitizer ate the marker on write (read-back confirmed the body now begins with the code fence), which would have made it invisible to the PM's scan. This is the literal-text fallback. The prose notes stay on that comment; only the machine-read block is repeated here.

    {
      "issue": 12465,
      "status": "done",
      "branch": "claude/issue-12465-root-task-spelling",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/12503",
      "premise_still_valid": true,
      "summary": "The card's premise holds in full: splitTaskKey('//#lint') yields pkg '//', the membership lookup misses, and the gate calls turbo's own root spelling a package that is not in the workspace while telling the author to delete the entry. Assumption B confirmed against merged code — '//#' still lands in the pre-existing package-scoped arm (judged=1, genericJudged=0); PR #12468's verdict() restructure did not move the arm boundary. ASSUMPTION A IS FALSIFIED AS FRAMED, and this changed the deliverable: the dispatch split the card into a LATENT misclassification and a LIVE false failure text, but a control measurement shows the sentence 'the override never reaches the task graph' is TRUE for a genuinely unknown package (@fx/nope#build is absent from the graph entirely) and FALSE only for '//' — so both halves have the identical trigger condition, a '//#' key existing, and neither is live today. There is therefore no coherent 'failure-text-only' deliverable: making the sentence true for the '//' case REQUIRES recognising '//', and hedging the shared sentence would weaken an accurate diagnosis on all 21 member keys to describe a case that no longer reaches that arm. On the #12436 question the seat asked me to answer by measurement: the lesson does not transfer, because that card proposed ADDING a guard for an unused spelling (0/1151, five false refusals), whereas '//#' 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). Scope respected: turbo.json was never edited to manufacture a subject; all turbo measurements ran in a throwaway fixture workspace under the scratchpad.",
      "tests": "All at final commit 04df3ad5ce, exit codes captured before any pipe. TURBO MEASUREMENT (fixture workspace, turbo 2.10.12 = what ^2.10.10 resolves to; repo turbo.json untouched): '//#lint' with root script present -> --dry=json EXIT 0, tasks in graph ['//#lint'], package='//', command=the root script, cache=false override resolved; real `turbo run lint` printed 'ROOT LINT RAN' and 'Tasks: 1 successful, 1 total'. '//#nope' with no root script -> in graph as '//#nope', real run 'WARNING No tasks were executed as part of this run', '0 successful, 0 total'. CONTROL '@fx/nope#build' -> graph is ['@fx/a#build'], the bad key absent entirely. GATE-vs-TURBO AGREEMENT: driving the fixed gate's own readers against that same fixture gives //#lint problems=0 rootJudged=1, //#nope problems=1 (root diagnosis), @fx/nope#build problems=1 (unknown package), @fx/a#typecheck problems=1 (missing script) — matching all four turbo rows. GATE UNION derived by `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack` with no paths passed (script took its own change set from merge-base; first run warned STALE TREE, so origin/main was fetched and merged and the union re-derived — family list identical). Green, each verdict line read from the gate's own output: check:turbo-task-graph EXIT=0 ('OK: 21 package-scoped and 8 generic turbo task(s) judged against 78 workspace package(s)'), check:agent-test-spelling 0, check:bash32-floor 0, check:cli-command-ids 0, check:cross-package-test-inputs 0, check:entry-guard 0, check:parse-guard 0, check:pnpm-filter-targets 0, check:nul-bytes 0 ('scanned 6901 text file(s) ... no raw ASCII control bytes'), check-cross-package-test-inputs.mjs 0. check-ci-filter-parity.mjs first returned EXIT=1 'PREREQUISITE NOT MET — nothing was measured' (yaml absent in a fresh worktree); per the standing rule that is a refusal to measure and not a finding, so `pnpm install` built the closure under the verify lock (VERDICT command-exit 0, held 6s, waited 28s) and it re-ran EXIT=0 'OK: all 109 declared cross-package glob(s) (84 unique) are covered'. GATE-SCRIPT LANDING OBLIGATIONS both run: scripts/pm/bare-root-worklist.mjs --self-test EXIT=0 and check:pm-dispatch-gates EXIT=0 (neither file was edited; 'package.json' is a root FILE, not a top-level directory, so it does not join the bare-root species). REPO-WIDE LINT RUN IN FULL, not narrowed — no narrowing to declare: `eslint . --no-inline-config -f json` over 5202 files, 0 errors, 0 warnings, EXIT=0, 59s under the verify lock. ABLATIONS — six legs, every pin added. NO REBUILD LEG APPLIES and this is stated rather than skipped: the subject is a dependency-free .mjs gate that --self-test imports directly, with no package exports and no dist/ between the edit and the run, so there is no stale-artifact path for a false green. Each leg ran from the COMMITTED state through a runner carrying trap 'git checkout -- FILE' EXIT INT TERM, and each mutation was CONFIRMED ON DISK before any reading was taken — grep -cF of the deleted needle (required 0) and of the injected marker (required at least 1), with the runner aborting as 'MUTATION DID NOT LAND, reading is void' otherwise; no leg aborted. Results: (1) delete the root arm -> 8 cases red; (2) keep the arm's verdict but restore the FALSE text -> exactly 5 red, all text pins, counts still green — this is the leg that proves the text pins are not satisfied by the count, the self-satisfying-pin hazard; (3) fold root scripts into holdersOf -> 3 red including #12373's own 'ROOT package.json does NOT count' pin; (4) drop package.json/** from the declaration -> 2 red; (5) widen pkg === '//' to startsWith('//') -> 1 red ('///#build'); (6) delete the zero-scripts refusal -> 3 red. Every restore leg verified byte-identical with git hash-object (b63f7b58e7... for legs 1-4, f2d4b9a601... for legs 5-6, before and after each).",
      "open_questions": [],
      "out_of_scope_findings": []
    }

    Generated by Claude Code


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions