Skip to content

fix(scripts): an injected tree hashes apart from an installed one, so a local Spec Main Shape Gate reproduction cannot leave verdicts a sibling worktree replays (objectui#12114) - #12125

Merged
objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/issue-12114-injected-tree-cache
Oct 11, 2026
Merged

objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/issue-12114-injected-tree-cache

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #12114

Dispatched implementation, PM seat domain:devx round R83; session https://claude.ai/code/session_01DwLS3LzXyNmyTunMGbpc8W.

What changed

  • turbo.json gains "globalDependencies": ["node_modules/@objectstack/spec/.spec-main-shape-gate.json"]: the marker inject already writes into every replaced spec copy, reached by a literal path through the repository root's own link to the spec (the root manifest declares @objectstack/spec).
  • scripts/spec-main-shape-gate.mjs exports TURBO_HASH_MARKER. inject now logs that turbo will hash the marker, or warns when there is no root link to hash. The header gains the section "...and an injected tree must not leave a verdict an INSTALLED tree replays", and the existing "The turbo cache is a FALSE GREEN here" section is reworded so it stays true.
  • scripts/__tests__/spec-main-shape-gate.test.ts: three new pins (below).
  • Four scripts/__tests__/turbo-*-inputs.test.ts headers said "globalDependencies is unset", which this change makes false. They now say it "holds no source file". These are comment-only edits outside the claim's declared file surface (see Acceptance notes).
  • .github/workflows/spec-main-shape-gate.yml is not touched. CI still runs TURBO_FORCE=true pnpm type-check, and its existing pin is unchanged.

No flag to remember. An installed tree has no marker and hashes exactly as before. An injected tree hashes apart from every installed tree, in both directions. Two injections never share an entry, because the marker carries the sha and the injection time. A reinstall replaces the copy, so the marker goes with it.

Mechanism readings (turbo 2.10.9, from the lockfile)

  • The cache is shared across worktrees (H1). The first turbo dry run in this linked worktree created the main checkout's .turbo/cache (the worktree got none of its own). Every run logs Remote caching disabled, using shared worktree cache.
  • --force still writes. turbo run --help defines --force as "Equivalent to --cache=local:w,remote:w". The forced CI-recipe run in the injected worktree wrote entry e02dab71f230fcc0 into that shared directory. So adding TURBO_FORCE=true to a local reproduction protects that run's reads and does nothing about its writes.
  • Injection does not move the hash on main (H2). Same worktree, before and after inject with main's script: the filtered @object-ui/types#type-check hashes e02dab71f230fcc0 both times. On the whole turbo run type-check graph (dry run), 83 of 83 task hashes are equal before and after injecting (46 of 46 type-check tasks).
  • What turbo 2.10 hashes from globalDependencies (H4, measured with dry runs).
    • A literal path into node_modules IS hashed, although git ignores it, including a path through the root symlink.
    • A wildcard inside node_modules (for example node_modules/.pnpm/@objectstack+spec@*/node_modules/@objectstack/spec/...) hashes nothing, with no warning. That is why the entry is a literal path, and the root link is what makes a literal path version-free.
    • An absent file hashes as nothing. Build task hashes are equal with and without the entry.
  • Undoing an injection.
    • A plain pnpm install --frozen-lockfile keeps both the injection and the marker.
    • pnpm install --frozen-lockfile --force replaces the copy and removes the marker with it. The hash went back to 95eb3fecb9111eb5, the same as the sibling worktree's installed hash.

Live two-worktree reproduction, before and after

Setup.

  • Worktree A is this branch. Worktree B is a second worktree at the same commit.
  • Both have an identical untracked probe in packages/types/src that imports a type named SpecGateProbe12114 from @objectstack/spec.
  • The tarball handed to inject is the installed published spec plus that one export. Deviation: it is not built from objectstack main. The thing under test is turbo's cache key, which never reads the spec's bytes, and the probe makes the false verdict visible.
  • Every step runs turbo run type-check --filter=@object-ui/types.

Before (29b99490, main's script and turbo.json):

step turbo output verdict
A: inject, then TURBO_FORCE=true (the CI recipe) cache bypass, force executing e02dab71f230fcc0 green, correct for the injected spec
B (installed spec), unforced cache hit, replaying logs e02dab71f230fcc0, Cached: 1 cached, 2 total; the replayed log even names worktree A's path exit 0: FALSE GREEN
B, forced src/__gate_probe_12114__.ts(2,15): error TS2305: Module '"@objectstack/spec"' has no exported member 'SpecGateProbe12114'. red, the true verdict

After (81dea3ff; the head a1b9bd68 differs only by a header comment):

step turbo output verdict
A and B installed, dry run both 95eb3fecb9111eb5; whole graph 83 of 83 equal installed trees still share
A: inject logs turbo hashes node_modules/@objectstack/spec/.spec-main-shape-gate.json, ...; hash now 67c390de3fefe78f; whole graph 0 of 83 equal to installed injected tree hashes apart
A, forced cache bypass, force executing 67c390de3fefe78f green
B (installed), unforced cache miss, executing 95eb3fecb9111eb5 then TS2305, Cached: 0 cached, 2 total red, the true verdict
A, unforced again cache hit, replaying logs 67c390de3fefe78f reuses only its own injected entry

The full 83-task graph was dry-run, not executed: executing every build would overrun the container's foreground limit. The dry-run counts cover the cache key of every task.

Pins, and proof that they can fail

The new describe block, titled "a local reproduction cannot leave a verdict an installed tree replays (objectui#12114)", holds three pins:

  1. Wiring. turbo.json names TURBO_HASH_MARKER, which is node_modules/ + spec + MARKER_FILE. The path has no glob characters, the root manifest declares the spec, and node_modules is git-ignored.
  2. Real turbo. It runs --dry=json over a fixture git repository that copies this repository's turbo.json, with pnpm-shaped relative links and the real inject CLI. An injection moves every task hash, a second injection moves them again, and a simulated reinstall puts the hashes back exactly.
  3. Firing control. With no root link, the same injection leaves the hashes unchanged and inject prints the warning. The marker inside the virtual store alone is invisible to turbo.

Red runs, each made from a committed state with the paths restored from HEAD afterwards (git diff HEAD empty, and each path's blob hash equal to HEAD's):

  • On main's code (script and turbo.json at 29b99490, new test file): Tests 3 failed | 28 passed (31).
  • Ablation of the turbo.json entry alone, script kept: Tests 2 failed | 29 passed (31). The wiring pin fails on the missing entry. The real-turbo pin fails with expected [ 'package.json' ] to include 'node_modules/@objectstack/spec/.spec-…'. The firing control stays green.

Gates (at a1b9bd68)

command exit reading
pnpm exec vitest run over the 13 test files that read turbo.json or the gate script (git grep -l -e turbo.json -e spec-main-shape-gate under scripts/__tests__) 0 Test Files 13 passed (13), Tests 625 passed (625)
pnpm lint:root 0 0 errors; none of the warnings is in a file this PR touches
pnpm check:new-line-citations 0 0 new citation(s)
pnpm check:control-bytes 0 OK
node scripts/check-changeset-presence.mjs 0 "no changeset is owed" (7 files, none a published source), so no changeset is added
node scripts/spec-main-shape-gate.mjs --self-test 0 26/26 passed
node scripts/check-governed-queue-guard.mjs --test on the 7 paths 0 NOT GOVERNED

Cost

The turbo.json edit moves the hash of the root task //#type-check:e2e, and every type-check task depends on it. Compared on an installed tree (whole-graph dry run, 29b99490 against this branch), 0 of 46 type-check hashes carry over and 36 of 36 build hashes carry over. That is a one-time cache miss, the same as any turbo.json edit.

Acceptance notes

  • File surface. The four turbo-*-inputs.test.ts comment lines are outside the claim's declared surface. They were edited because this change would otherwise leave their prose false. No open PR touches them.
  • Prose still true but not extended. content/docs/guide/ci-cd-pipeline.md (Spec Main Shape Gate section) and the fenced workflow header still say turbo's hash does not cover the content of node_modules. That remains true: the marker is the injection's identity, not the spec's content. Neither one describes the local reproduction recipe, which lives only in the script header. Carrier: none.
  • Leftover cache entries. The reproduction left two inert entries in the main checkout's .turbo/cache: e02dab71f230fcc0 and 67c390de3fefe78f. The first is keyed on a probe file that no longer exists; the second on a unique injection marker. My attempt to delete them was refused by the sandbox as a shared-resource write.
  • CI behaviour. Unchanged, except that the inject step prints one more log line.

Generated by Claude Code

… a local Spec Main Shape Gate reproduction cannot leave verdicts a sibling worktree replays

A linked git worktree shares the main checkout's `.turbo/cache`, and
`--force` (`TURBO_FORCE=true`) skips cache reads but still writes. A local
reproduction of the gate therefore recorded main-spec type-check verdicts
that a sibling worktree's ordinary installed-spec `pnpm type-check`
replayed as its own.

`turbo.json`'s `globalDependencies` now names the marker `inject` already
writes into every replaced spec copy, reached by a literal path through the
root link to the spec (turbo does not expand a wildcard inside
`node_modules`). An installed tree has no marker and hashes exactly as
before; an injected tree hashes apart in both directions, with no flag to
remember; a reinstall replaces the copy and the marker with it. CI's
`TURBO_FORCE=true` is unchanged.

Part of objectui#12114.

Claude-Session: https://claude.ai/code/session_01DwLS3LzXyNmyTunMGbpc8W
Co-authored-by: Claude <noreply@anthropic.com>
… the objectui#12114 wiring pin fails

Claude-Session: https://claude.ai/code/session_01DwLS3LzXyNmyTunMGbpc8W
Co-authored-by: Claude <noreply@anthropic.com>
… introduces the turbo hash marker

The header said "the marker above" before any line above it named the
marker.

Claude-Session: https://claude.ai/code/session_01DwLS3LzXyNmyTunMGbpc8W
Co-authored-by: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

2 participants