You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Commit a194b4e
Browse filesBrowse the repository at this point in the historyBrowse files
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)
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](https://claude.ai/code/session_01DwLS3LzXyNmyTunMGbpc8W)_
---------
Co-authored-by: Claude <noreply@anthropic.com>
0 commit comments