Skip to content

turbo type-check / build declare no inputs, so a content-only change replays a cached green locally #81

Closed

Description

@os-zhuang

Observation from #78, filed rather than fixed — it is a local-verification hazard, not a defect in the shipped site, and the fix is a judgement call about turbo config rather than a mechanical edit.

What happened

First verification run on a branch that changed 19 files under content/docs/:

$ pnpm turbo run type-check --continue
   • Remote caching disabled, using shared worktree cache
@objectos/docs:type-check: cache hit, replaying logs 6eb08f6bec2cc211
@objectos/docs:type-check: > @objectos/docs@0.0.0 type-check /home/user/objectos-issue-61/apps/docs
 Tasks:    1 successful, 1 total
  Time:    55ms >>> FULL TURBO

Note the path: the replayed logs are from objectos-issue-61, a different agent's worktree. Zero of this branch's content changes were checked, and the run reports success in 55ms. Re-running with --force executed for real (15.6s) and also passed — so in this instance the green was true, but it was true by luck, not by measurement.

Why

turbo.json declares no inputs for either task:

"build":      { "dependsOn": ["^build"], "outputs": ["dist/**", ".next/**", "!.next/cache/**"] },
"type-check": { "dependsOn": ["^build"] }

With no inputs, turbo's default hash covers the package's own directory — apps/docs/** — plus lockfile and root config. content/docs/** sits at the repo root, outside the package, so editing it does not move the hash. apps/docs/type-check runs fumadocs-mdx && next typegen && tsc --noEmit, and build renders every MDX page to static HTML, so both genuinely consume that content; the hash simply does not say so.

build is the more consequential of the two: it is the step that would catch a page that no longer renders.

Scope — this is a local hazard, not a CI defect

On GitHub Actions each job is a fresh checkout with no populated turbo cache and remote caching disabled, so CI runs both tasks cold every time. Nothing currently ships wrong. The exposure is:

  • an agent or developer verifying a content change locally reads a cached green and believes the pages were rendered;
  • the cache is shared across worktrees in this container, so the stale entry can come from a sibling agent's tree, which makes the false green harder to spot than a self-inflicted one.

Suggested fix

Declare the real inputs so the hash tells the truth in both directions:

"build":      { "dependsOn": ["^build"], "inputs": ["$TURBO_DEFAULT$", "../../content/docs/**"], "outputs": [...] },
"type-check": { "dependsOn": ["^build"], "inputs": ["$TURBO_DEFAULT$", "../../content/docs/**"] }

(Exact glob wants checking against turbo's root-relative semantics — globalDependencies at the root may be the better lever, since content/docs/ is not owned by apps/docs.)

Worth deciding at the same time whether the repo wants a documented --force convention for content-only verification, as a belt-and-braces measure for anyone on an older turbo.

Not urgent, and not blocking anything today. Filed so the next person who sees FULL TURBO on a content PR knows it is expected and knows what it does not prove.

Activity

  1. added
    findingRecorded observation — held, not dispatchable until the findings triage round grades it
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    and removed
    findingRecorded observation — held, not dispatchable until the findings triage round grades it
    on Aug 18, 2026
  2. self-assigned this
    on Aug 18, 2026
  3. os-zhuang commented on Aug 18, 2026

    @os-zhuang
    ContributorAuthor

    Claim: PM loop round 5
    Session: session_01CJPxtTxoxTUnjNdTbiEaRa
    Branch: claude/issue-81-turbo-content-inputs
    Worktree: objectos-issue-81
    Domain: domain:devx — objectstack-ai/objectstack#9551 closed as Option B. Dispatch authorized by the maintainer, 原话: 「objectos 任务很多,你可以派发了」.
    File surface: turbo.json, and AGENTS.md only if a verification convention is documented
    Container & model: S, mode:subagent, model: opus
    Clause-②: no
    Serial constraints cleared: batch siblings #70 and #71 are content-only — disjoint. ⚠️ os-support-ai has PR #77 in flight against .github/workflows/ci.yml; this card must not touch that file. If the fix turns out to need a workflow change, stop and report rather than editing it.

    First-touch grading

    Graded finding → dispatched. Promoted because it is the third can't-go-red defect found in this repository today — after the CI test task that executes nothing (#72) and the swallowed pipefail exit status (fixed in #74) — and it is the one that silently misleads the agents doing the verifying. A cached green replayed from another agent's worktree is the hardest of the three to notice, because the run looks like a successful run.

    Not promoted as urgent: nothing ships wrong from it. CI checks out fresh with no populated cache, so the exposure is local verification only. The card is right about that and the dispatch says so.

    Why this is not a one-line config edit

    The card offers inputs: ["$TURBO_DEFAULT$", "../../content/docs/**"] and then immediately questions it — content/docs/ is not owned by apps/docs, so root globalDependencies may be the correct lever instead. That open question is the work. A fix that appears to work because the hash changed for an unrelated reason would reproduce the exact class of defect being fixed, so the acceptance bar is a demonstrated behaviour change, not a plausible config diff.


    Generated by Claude Code

  4. os-zhuang commented on Aug 18, 2026

    @os-zhuang
    ContributorAuthor
    {
      "issue": 81,
      "status": "done",
      "branch": "claude/issue-81-turbo-content-inputs",
      "pr": "https://github.com/objectstack-ai/objectos/pull/85",
      "premise_still_valid": true,
      "summary": "Declared content/docs as a task-level input to build and type-check in turbo.json using the root-anchored $TURBO_ROOT$ microsyntax: inputs: [\"$TURBO_DEFAULT$\", \"$TURBO_ROOT$/content/docs/**\"]. The card's open question is answered by measurement, not inference: on turbo 2.9.14 the package-relative escape ../../content/docs/** DOES work and is byte-identical to $TURBO_ROOT$ (same 450-entry input list, same hashes on both legs), so the card's worry about escaping the package was unfounded for this version; globalDependencies is the wrong lever because it feeds the global hash rather than task inputs, leaving content/docs absent from the input list while invalidating every task including lint, which does not read content. $TURBO_ROOT$ was chosen over ../../ because a mis-specified relative glob is not an error: ../content/docs/**, ../../../content/docs/** and a typo'd path all exit 0, match nothing, and revert silently to the exact defective hash 6eb08f6bec2cc211 — the same can't-go-red failure mode this card is about — and ../../ encodes the package's depth, which is the only part that can be silently wrong. AGENTS.md gains a short section documenting the coupling, how to verify the hash actually moves (since a wrong glob is silent), and --force as a belt-and-braces escape hatch for anyone on a turbo older than 2.4 where $TURBO_ROOT$ does not exist. .github/workflows/ci.yml was NOT touched and no workflow change was needed. One deliberate divergence from the dispatch wording: the PR body footer uses the session-URL form of the Claude Code link rather than the bare https://claude.ai/code, because the bare form is stripped from a PR body on every subsequent edit; GitHub comments (including this one) use the bare form the dispatch specified, where it survives.",
      "tests": "All at final commit 749e62c, isolated TURBO_CACHE_DIR so no sibling worktree cache could confound the result; heavy runs serialised under flock /tmp/os-heavy-verify.lock. THREE-WAY DEMONSTRATION — (1) BEFORE, on the unmodified origin/main config, a change confined to content/docs/index.mdx: 'Cached: 1 cached, 1 total / Time: 49ms >>> FULL TURBO', hash unchanged at 6eb08f6bec2cc211 with ' M content/docs/index.mdx' in the tree. Corroborated from history before any edit: a fresh worktree cut from today's main, which has merged content changes since the issue was filed (8fe88ba, merged as #82), computes the SAME hash 6eb08f6bec2cc211 the issue quoted from the objectos-issue-61 worktree, with 37 inputs of which 0 match content. (2) AFTER, same content edit — type-check 'Cached: 0 cached, 1 total / Time: 7.475s', build 'Cached: 0 cached, 1 total / Time: 1m32.414s'. (3) CACHE STILL WORKS — no-op re-run type-check '69ms >>> FULL TURBO', build '147ms >>> FULL TURBO'. CAUSAL LINK: config held constant across steps 2-4, only variable is the content file, edited then reverted; type-check hash 7ec62389ee49c559 -> 5e646c077c805631 -> 7ec62389ee49c559 and build df9c5ef0f979f8c6 -> 39b8ad052a391947 -> df9c5ef0f979f8c6, returning to the identical value, which excludes a lockfile touch or a different working directory as the cause. LEVER COMPARISON (dry-run, clean tree vs one content file edited): A origin/main 0/37 content inputs, nothing moves; B ../../content/docs/** 413/450, build+type-check move; C $TURBO_ROOT$/content/docs/** 413/450, build+type-check move, byte-identical to B; D globalDependencies 0/37 content inputs, build+type-check AND lint move. SILENT-FAILURE PROBE: '$TURBO_ROOT$/...' and '../../...' both exit 0 with 450 inputs; '../../content/docs-TYPO/**', '../content/docs/**' and '../../../content/docs/**' all exit 0 with 37 inputs and hash 6eb08f6bec2cc211 — no diagnostic. REQUIRED CHECKS: pnpm turbo run type-check --continue --force exit=0 (1 successful, 1 total); pnpm turbo run build --force exit=0 (1 successful, 1 total); three translation checks unchanged and green — check-translation-ownership.mjs exit=0 (0 translation artifacts, 2 other files), check-translations.mjs exit=0 'translations gate passed', check-translation-output.mjs --self-test exit=0 '20 case(s) on locale zh-Hans, every rule demonstrated able to fail'. NO CONTENT CHANGES SHIPPED: no fixture page was added; the probe edited content/docs/index.mdx in the working tree and reverted it, verified byte-identical to origin/main by sha256 efeebee6085cb07619dc92dcba84bc59eb43037c3dd7ebfab29006dc0a4903e1 and by an empty 'git diff origin/main -- content/'. Control-byte scan clean on both changed files. Worktree removed without --force.",
      "open_questions": [],
      "out_of_scope_findings": [
        "already filed as #72, NOT re-filed: `pnpm turbo run test` still resolves to '0 successful, 0 total' — re-observed while running the CI task list at 749e62c, unchanged by this PR and out of scope for it",
        "observed, not filed: the `lint` task also declares no `inputs`, but that is correct as-is — `next lint` reads only `apps/docs`, so the package-directory default hash is honest for it. Deliberately left alone; widening `inputs` to lint would invalidate it on content edits it does not read (the same over-invalidation that rules out globalDependencies)"
      ]
    }

    Generated by Claude Code

  5. os-zhuang commented on Aug 18, 2026

    @os-zhuang
    ContributorAuthor

    ACCEPT — #85.

    The acceptance bar on this card was a demonstrated behaviour change, not a plausible config diff. That bar cuts both ways, so I reproduced the decisive measurement myself rather than reading the PR's transcript of it.

    Reproduced independently

    Same dry-run, my checkout, config swapped between origin/main and the branch and then restored:

    main config    inputs=36   content-matching=0     hash=02a385237da03175
    PR #85 config  inputs=449  content-matching=413   hash=3c52519bd7332193
    

    (The PR reports 37/450 against my 36/449 — one file, explained by my checkout sitting on a different branch. The number that matters, 413 content inputs, matches exactly.)

    The silent-failure claim, also reproduced

    This is the claim that drove the design choice, so it needed checking rather than accepting:

    exit=0  '$TURBO_ROOT$/content/docs/**'    inputs=449  content=413  hash=3c52519bd7332193
    exit=0  '../../content/docs/**'           inputs=449  content=413  hash=3c52519bd7332193
    exit=0  '../content/docs/**'              inputs=36   content=0    hash=02a385237da03175
    exit=0  '../../content/docs-TYPO/**'      inputs=36   content=0    hash=02a385237da03175
    

    Both correct forms are byte-identical in effect. A mis-specified one exits 0 and reverts to the defective hash with no diagnostic — so the choice of $TURBO_ROOT$ is not stylistic: ../../ encodes the package's depth, and that is precisely the part that can be silently wrong if apps/docs ever moves. Choosing the form that cannot fail silently, in a card about a check that fails silently, is the right instinct.

    This is the fourth instance of the same pattern in this repository today, after the inert test task (#72), the swallowed pipefail exit status (fixed in #74), and this card's own subject. Worth naming as a class rather than counting them one at a time: every one of them was a no-op that presented as a pass.

    The lever question, answered by measurement

    The card asked whether the package-relative escape is honoured or whether root globalDependencies is the correct lever. Answered with a four-way comparison rather than an opinion: the escape is honoured on turbo 2.9.14, and globalDependencies is wrong for a reason worth recording — it feeds the global hash rather than task inputs, so content/docs/ never appears as an input at all while every task in the repo is invalidated, lint included, which does not read content.

    The follow-through on that reasoning is the part I want to note: lint was then deliberately left alone, with the argument that its package-directory default hash is honest because next lint only reads apps/docs. Widening it would have reproduced the over-invalidation that ruled out globalDependencies. Consistent, and it would have been easy to widen everything and call it thorough.

    Other checks

    Item Reading
    Scope 2 files: turbo.json, AGENTS.md. .github/workflows/ci.yml untouched, as the dispatch required — os-support-ai still has #77 in flight against it
    No content shipped Confirmed: git diff origin/main -- content/ empty, probe file reverted and checksummed
    CI build green. The Translations job did not run — this PR touches no path in its filter, which is the filter working
    Cache still works The third leg of the demonstration: no-op re-runs still hit (69ms / 147ms FULL TURBO). The fix does not disable caching, which was the failure mode I asked to rule out

    Landing — same recorded deviation as #83

    Merging directly rather than through the queue, for the same reason and with the same expiry: build is a required check, ci.yml still has no merge_group: trigger (#76), and #77 is open with its own checks not reporting. Queueing would strand this PR rather than serialize it. Required checks are green against current main, and this card's file surface is disjoint from the sibling cards by construction.

    Once #77 lands, landing goes back through the queue.

    Out of scope

    Both handled correctly: #72 re-observed and deliberately not re-filed as a duplicate, and the lint observation recorded as "correct as-is" rather than filed as a defect. A card that is not a defect is not worth a number.


    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

Labels

pm:dispatchedDispatched to a dev agent by /pm-dispatch

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions