test(create-objectstack): converge the third GIT_* allowlist carrier onto gitFreeEnv() - #19391
Conversation
…onto gitFreeEnv() The hand-maintained ten-name `GIT_*` location allowlist survived in one more file after the two spec carriers were converged. It is retired here the same way, so the repo carries one spelling of the strip and no transitional state. `REPO_READ_ENV` is now `gitFreeEnv()`. The blanket strip is strictly wider than the list it replaces and stays inside the boundary this file's header draws: it DELETES keys rather than pinning any to `/dev/null`, so it never sets `GIT_CONFIG_GLOBAL` and global/system config -- `safe.directory` with it -- stays open. Both children here are local reads against `cwd` (`ls-files` and `git grep`), so the transport settings the strip also removes are nothing either one needs. The `:42-54` prose is kept: it records why this carrier stops short of the fixture harnesses' `/dev/null` config pins. The import is a relative ES-module specifier that escapes the package, which forces two companion declarations -- `scripts/cross-package-test-inputs.mjs` and `turbo.json` -- so a change to the strip re-runs this package's suite instead of replaying a cached green over it. Measured before they landed: the gate named `scripts/git-env.mjs` and exited 1. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QCdUBjM47SxioST9z5Zwdf
📓 Docs Drift CheckNothing in this diff resolved to a documentable surface (no symbol, route or SDK anchor derived from 0 changed package(s)), so this run has no opinion about the docs. What this run could not see
Coarse fallback — 0 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): |
Seat grading — ACCEPT. Round 2 turned every one of round 1's open questions into a reading.⭐ This round's distinguishing quality: nothing was left as reasoning. Each thing round 1 could only argue, round 2 measured. ⭐ The forced breach, proven by a firing gate rather than by argumentRound 1 derived it from the gate's header and two declaration files, and said so — ⛔ "NOT MEASURED — I could not run the gate to prove the red." Round 2 ran it:
⇒ the companions are forced, and that is now a measurement. ⛔ The seat's original one-file fence was wrong, round 1's dev flagged rather than breached it, and round 2 proved the flag correct. The claim record ⭐ The typecheck answer is a reading, and its green is proven realRound 1's open question was whether
⇒ ⭐ the declaration is effective, not decorative. A green that cannot be made red is not evidence, and this one was made red on demand. The ablation, with the control proven first⭐ The firing control comes before any verdict, and it is the right one: under a leaked
⭐ L4 is what makes L3 mean anything: it proves the red comes from the leak, ⛔ not from the mutation itself. Without it, L3 only shows that breaking the code breaks the test. ⇒ the dispatch's question — after the conversion, does this test still fail for the reason it was written to catch? — is answered yes, by measurement. The conversion preserved a boundary this carrier deliberately drewThe ⭐
|
Fixes #19171
Clause-②: no
What this does
The hand-maintained ten-name
GIT_*location allowlist survived in one more file after PR #19168 converged the twopackages/speccarriers. This retires it the same way, so the repo carries one spelling of the strip and no "either is fine" transitional state — the #16644 ruling's 「不要保留「两种都行」的过渡态」.In
packages/create-objectstack/src/template-consistency.test.ts,REPO_READ_ENVbecomesgitFreeEnv(). The ten names it replaced are byte-identical to the allowlist #19168 deleted from both spec carriers — the same shape, not a variant — and the constant is not re-spelled anywhere in the file, so a census of the retired shape does not match the paragraph that explains it.The header prose at
:42-54is kept, deliberatelyThat paragraph records why this carrier stops short of the fixture harnesses'
/dev/nullconfig pins: closing global config would also discardsafe.directory, which is what lets git read a checkout owned by another user inside a container, and it would trade a passing read fordetected dubious ownership.gitFreeEnv()does not reintroduce that risk, and the reason is mechanical rather than a judgement call: it deletes keys instead of pinning any of them to/dev/null, so it never setsGIT_CONFIG_GLOBAL. Global and system config stay open,safe.directorywith them. The concern is preserved, and more strictly — deletion moves git toward its own defaults rather than substituting an empty config file.The widening is free here for the boundary the module's own header draws:
gitFreeEnv()is for a local child and never for one that talks to a remote. Both children in this file are local reads againstcwd(ls-filesandgit grep), so the transport settings the blanket strip also removes are nothing either one needs. Measured on this container: the ambient environment carries 11GIT_*keys, none of them among the ten the allowlist named, and theGIT_CONFIG_*triple carries onlycredential.interactiveand twourl.insteadOfremote rewrites. There is also landed precedent for a local read of the real checkout taking this environment —scripts/check-type-check-coverage.mjsspells itenv: gitFreeEnv(), // LOCAL-ONLY (#16644).The two companion files are FORCED, not chosen
The import is a relative ES-module specifier that escapes the package, and
check-cross-package-test-inputstreats an undeclared escaping import as a red gate by design. This is not an argument — it was measured on the conversion commit before the declarations existed:So
scripts/cross-package-test-inputs.mjsandturbo.jsongainscripts/git-env.mjs. Same shape PR #19168's own addendum recorded for its two extra files — both forced rather than chosen. The alternative — re-spelling the blanket strip inline to stay inside one file — is refused: it would be a fourth spelling of the retired shape, which is what the ruling forbids.Only the
.mjsis declared, not its.d.mtssibling: vitest resolves and loads the module at run time, which is what makes it a live input to this package'stestverdict, while the declaration is type-only and the generictypecheckturbo task takes$TURBO_DEFAULT$with no repo-root additions at all. See the acceptance note below.Does the test still fail for the reason it was written to catch?
Yes — measured, not assumed. A conversion that compiles, runs green and no longer asserts the property is the failure mode this card is about, so the deliverable is the ablation rather than the diff.
The battery builds a decoy repository under
mktemp -d(never the real checkout) and proves a firing control before any verdict:Under a leaked
GIT_DIRthe exact command this test runs answers for the decoy and reports 0 SKILL.md files — at exit 0, silently. That is the #9068 exposure class the file exists to close. A second probe confirms the leak survives the pnpm/exec spawn chain, so legs 2 and 3 are not void.L3's red is the vacuous-green guard itself:
L4 is the leg that makes the red mean something: with the strip removed but no leak present, all 37 still pass — so the red in L3 comes from the leak, not from the mutation. The mutation was landed through
scripts/ablation-replace.mjs(anchor 1 to 0, blob36ab445bd902to6b125ae11ffb), and the restore is proven on disk rather than by an exit code: blob equals HEAD andgit diff HEADis empty.Verification
Run at
0e2e1065b0, the commit this PR lands.pnpm --filter create-objectstack typecheck— exit 0. This was the open question: the module sits outside the package'srootDir: "src", and TS6059 was the failure mode. It resolves to the declaration instead, and that is a reading rather than an inference —tsc --noEmit --listFilesputsscripts/git-env.d.mtsin the program at entry 178 of 308, with the test file itself at 179.any. Mutating the call togitFreeEnv(the missing parentheses that the.d.mtsheader names as the costliest mistake here) turns the same typecheck red withTS2322: Type '(base?: ProcessEnv | undefined) => ProcessEnv' is not assignable to type 'ProcessEnv', then restores to blob equal to HEAD.pnpm --filter create-objectstack test— 16 files, 203 tests, all passed.pnpm --filter create-objectstack build— green; the dependency closure is empty by construction (this package has no workspace dependencies), so that leg is an empty run.pnpm check:cross-package-test-inputs— 255 self-test cases pass, then OK.scripts/pm/dispatch-gates.mjs: 68 derived, 65 green, 0 unrun. Three are NOT MEASURED, each exiting 3 on its own prerequisite (check:dual-build-cjs-loads,check:lean-entry-closure,check:type-check-debtall need a full-repo build, which is CI's). Re-derived aftergit fetch origin main: still 68, no new families.pnpm lint(eslint . --no-inline-config, repo-wide, not narrowed) — exit 0.No changeset: measured rather than assumed. This package's
files[]isdist,README.md,CHANGELOG.md; after a build,gitFreeEnv,REPO_READ_ENVandLEAKED_GIT_ENVhave zero hits across those paths while the positive controlssyncObjectStackDeps,SKILLS_INSTALL_COMMANDandTEMPLATESall hit. The changed test file is not a build entry point, and the other two files are repo-root tooling and config.Acceptance notes
Out of scope, noted and deliberately not filed:
scripts/git-env.d.mts's header names only the twopackages/specfixtures as its TypeScript consumers. A third now exists, so that sentence is stale. It is prose, not a gate —check:declaration-mirrorsdiscovers its own corpus and is green — so nothing enforces it and nothing breaks. Flagged by the dispatching seat as explicitly outside this fence.@objectstack/vitest-filter-preflight#testdeclaresscripts/js-comment-mask.mjswithout the.d.mtssibling that thirteen other turbo entries pair with it, while itstest/config-wiring-sweep.test.tsdoes import the module from TypeScript. On inspection this is consistent rather than wrong — atesttask is vitest, which never type-checks, so the declaration is genuinely irrelevant to that task's hash — and it is the same reasoning applied here forgit-env. Recorded because it reads as an inconsistency until you work that out; no successor is blocked by it.Generated by Claude Code