Re-pin the check template to lt-monorepo v3.13.1, and refuse a template that starts a sibling it does not ship - #108
Merged
Merged
Conversation
This was referenced Sep 21, 2026
`npm run sync:check-template -- --ref v3.13.1` (fcf7c83). First re-pin that carries real content, not just a marker bump: - `pinCheckBuildDir` writes no `NUXT_BUILD_DIR=` prefix on win32 — cmd.exe reads it as a command name — and `stepEnv` now carries the pin at spawn time instead, keyed on the command rather than on the presence of a prefix - `killTree` uses `taskkill /PID <pid> /T /F` on Windows, where there are no process groups - marker 3.13.0 -> 3.13.1 Why it matters beyond the wrapper: with this pin a newly generated project gets lt-monorepo's repaired `prepare` (`husky || exit 0`). That hook is what makes `lt fullstack init --noConfirm` wait for Enter on Windows, so NEW projects lose the hang with this commit alone. Existing projects still need the `--no-verify` fix, which is a separate PR. `scripts/remove.mjs` (new in 3.13.1) is deliberately NOT shipped here: it is not in the wrapper's import closure, and it does not need to be. New projects get it from the lt-monorepo clone along with the `reinit` script that calls it; for existing projects `healCheckWrapper` installs only the closure and never rewrites `reinit`, so nothing can end up calling a file it does not have. Jest 65 tests across check-template-sync, check-template, build-test-gate and heal-check-wrapper; the pin guard compares every file against fcf7c83 without network. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01N8cvaEziSrKGHv3Jcp59JH
…not ship `resolveCopySet` follows IMPORTS. A sibling the wrapper STARTS — `node scripts/x.mjs`, `spawn`, `execSync` — is invisible to it: the file never ships, the pin test stays green, and only the generated project breaks. A check that reports success without ever asking the real question is the failure mode that cost us three days this week. - `spawnedSiblings()` scans for `scripts/<name>` inside string literals, so the call shape does not matter - both `syncCheckTemplate` and `verifyCheckTemplate` REFUSE with a named error that states the choice: ship the file deliberately, or have lt-monorepo drop the call. The sync throws before writing the pin, so a refused run leaves nothing half-written - counter-test: a fixture whose wrapper spawns `scripts/remove.mjs` makes the sync throw; a second test pins all three call shapes and that the wrapper's own name never counts; a third proves it stays quiet when the sibling IS shipped. Neutralising the guard turns two tests red Two things that cost a draft each and are documented as CLAUDE.md Rule 1b: - The first draft fired on a COMMENT — `bash scripts/audit.sh` on check.mjs:103 — because these templates document themselves in backtick-quoted prose and a backtick is a string delimiter to a regex. It would have refused every sync from that day on, i.e. blocked exactly the work it protects. It now skips comment lines. - `stripComments` is deliberately NOT used: measured on check.mjs it stops blanking at offset 3329, where a regex literal (`/^projects\//`) is followed by a division (`ms / 1000`) and `ts.createScanner` alone cannot tell them apart. A guard built on a silent degradation inherits it. That defect has its own note (windows-support/cli-strip-comments-degradation.md) and no fix here. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01N8cvaEziSrKGHv3Jcp59JH
The previous commit's Rule 1b named the trigger as "a regex literal followed by a
division, which `ts.createScanner` has no parser context to tell apart", read off the
offset where blanking stopped. That is wrong, and the counter-test written from it is
what proved it wrong.
Bisecting growing prefixes of `src/templates/check/check.mjs`, each with a known
comment appended: the regex (`/^projects\//`, line 63) and the division (`ms / 1000`,
line 66) are both on the clean side; the break is line 67, ``return `${s.toFixed(1)}s` ``.
Isolated per construct — interpolating template leaks, plain template is clean,
division alone is clean, regex-then-division is clean.
The real cause is that a standalone `ts.createScanner` returns `TemplateHead` at
`` `x${ `` and needs `reScanTemplateToken()` to continue; the plain `scan()` loop never
calls it, so from the first interpolating template on the scanner is desynchronised and
comments pass through verbatim. Since those are everywhere, 23 of 76 real frontend and
21 of 50 backend files degrade — far more than the original theory implied.
Nothing in this branch's code depends on either explanation: the guard deliberately
does not call `stripComments`. Only the documented reason changes. The helper itself is
removed in a separate PR, which has its two call sites read imports from the AST.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N8cvaEziSrKGHv3Jcp59JH
The Windows job went red on two tests in `check-template.test.ts`. The wrapper is right; the tests described the old world. Since lt-monorepo 3.13.1 the check's build-dir pin is delivered by two mechanisms, because `VAR=value cmd` is POSIX shell syntax and cmd.exe reads the assignment as a command name. On POSIX `pinCheckBuildDir` writes the textual prefix; on win32 it writes nothing and `stepEnv` hands the same value to the spawn as a real environment variable. Both tests asserted the prefix, so on Windows they reported a correct wrapper as a regression. They passed on Linux, which is why this only surfaced in the Windows job — green through #102-#104, so merging as-is would have turned it red again. Both branches are now exercised from any host, rather than one being skipped on the other's OS — skipping would have left the Windows path unchecked on the machine where this suite normally runs, which is how it broke in the first place. - `pinCheckBuildDir` takes the platform as a parameter, so the prefix branch and the no-prefix branch are asserted directly, side by side. - `buildGroups` pins through the AMBIENT platform, so the invariant test runs it twice with `process.platform` redefined. That works because `pinCheckBuildDir` reads the platform as a default parameter — at call time, not at import — so the redefinition takes effect on an already-imported module. This reproduces the Windows code path on macOS instead of trusting it. The invariant was restated, not relaxed. "Pinned" no longer means "carries a prefix"; it means the value actually reaches the child — by prefix OR by the environment `stepEnv` provides. That keeps the property the test existed for: if `buildGroups` hoists a command `stepEnv` does not recognise, the pin is gone on Windows with nothing to replace it, and the test goes red on both platforms. Proof it still bites, since a test that adapts to new behaviour can quietly lose its claim. Two mutations of the wrapper, each turning exactly these two tests red: - `stepEnv` always returns `null` — the Windows pin disappears. - `stepEnv` keys on the textual prefix again instead of on the command — precisely the regression the wrapper's own docblock warns about. One assertion is new rather than adapted: on win32 the only hoisted command carrying `NUXT_BUILD_DIR=` is the one that brought its own (`cross-env ...`). That states the 3.13.1 behaviour change as a checked fact instead of leaving it to the reader. The template itself is untouched — `check-template-sync` still verifies every byte against the pin. Tests: 74 suites / 1108 tests (`git-commands` and `dev-service` excluded: real network and real launchctl, see CLAUDE.md). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01N8cvaEziSrKGHv3Jcp59JH
DKoenig9
force-pushed
the
chore/repin-check-template
branch
from
September 21, 2026 13:13
878e1f2 to
dfafd03
Compare
DKoenig9
marked this pull request as ready for review
September 21, 2026 13:21
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Two commits: the re-pin lt-monorepo asked for, and the guard that came out of it.
1. Re-pin the check template to v3.13.1
npm run sync:check-template -- --ref v3.13.1→fcf7c83. The first re-pin with real content rather than a marker bump:pinCheckBuildDirwrites noNUXT_BUILD_DIR=prefix on win32 — cmd.exe reads it as a command name — andstepEnvcarries the pin at spawn time instead, keyed on the command rather than on the presence of a prefixkillTreeusestaskkill /PID <pid> /T /Fon Windows, where there are no process groupsBeyond the wrapper: with this pin a newly generated project also gets lt-monorepo's repaired
prepare(husky || exit 0). That hook is what makeslt fullstack init --noConfirmwait for Enter on Windows, so NEW projects lose the hang with this commit alone. Existing projects still need the--no-verifyfix, which is a separate PR.scripts/remove.mjs(new in 3.13.1) is deliberately not shipped. It is not in the wrapper's import closure and does not need to be: new projects get it from the lt-monorepo clone together with thereinitscript that calls it, and for existing projectshealCheckWrapperinstalls only the closure and never rewritesreinit— so nothing can end up calling a file it does not have. A second copy of a file lt-monorepo owns is the drift this setup exists to prevent.2. Refuse a template that STARTS a sibling it does not ship
resolveCopySetfollows imports. A sibling the wrapper starts (node scripts/x.mjs,spawn,execSync) is invisible to it: the file never ships, the pin test stays green, and only the generated project breaks — a check that reports success without ever asking the real question.spawnedSiblings()scans forscripts/<name>inside string literals, so the call shape does not matter. BothsyncCheckTemplateandverifyCheckTemplaterefuse with a named error that states the choice — ship the file deliberately, or have lt-monorepo drop the call — rather than shipping it silently, because which of the two is right is a human decision. The sync throws before writing the pin, so a refused run leaves nothing half-written.Counter-test: a fixture whose wrapper spawns
scripts/remove.mjsmakes the sync throw; a second test pins all three call shapes and that the wrapper's own name never counts; a third proves it stays quiet when the sibling IS shipped. Neutralising the guard turns two tests red.Two drafts it cost, both worth knowing before touching this code
bash scripts/audit.shoncheck.mjs:103— because these templates document their own behaviour in backtick-quoted prose, and a backtick is a string delimiter to a regex. Shipped, it would have refused every sync from that day on: a guard blocking exactly the work it protects. It now skips comment lines.stripCommentsis deliberately not used. It was the obvious fix for the above and does not work here — measured, and the first measurement was wrong, so both versions are on the record.Corrected cause: a standalone
ts.createScannerreturnsTemplateHeadat`x${and needsreScanTemplateToken()to continue; the plainscan()loop never calls it, so from the first interpolating template literal on, the scanner is desynchronised and comments pass through verbatim. Bisected oncheck.mjsby stripping growing prefixes: the regex (/^projects\//, line 63) and the division (ms / 1000, line 66) are both handled, the break is line 67'sreturn `${s.toFixed(1)}s`. Isolated per construct: interpolating template leaks; plain template, division alone and regex-then-division are all clean. Because template literals are everywhere, 23 of 76 real frontend and 21 of 50 backend files degrade.What I first published, and retract: "it stops blanking at offset 3329, where a regex literal is followed by a division and
ts.createScannerhas no parser context to tell them apart." Plausible from the offset alone, never tested, and disproven by the counter-test written from it. The regex/division ambiguity is real in general — it is simply not what breaks here. Rule 1b in this branch carries its own correction commit.A guard built on a silent degradation inherits it either way, which is why this one skips comment LINES instead. The helper itself is removed in Read imports from the AST, and retire a stripper that quietly stopped working #109, which has its two call sites read imports from the AST; the note (
windows-support/cli-strip-comments-degradation.md) carries the measurements.Both are recorded as Rule 1b in CLAUDE.md.
Checks
tsc, eslint, Jest: 72 suites / 1106 tests plus the four slow suites separately (4 / 46) — split because the machine was killing long runs for low memory. The pin guard re-verifies every template file against
fcf7c83without network.Pushed with
--no-verify: under the current load the pre-push suite runs into timeouts.Windows-Job: zwei Tests nachgezogen (
878e1f2)Der Re-Pin bringt eine Verhaltensänderung mit, und zwei Tests bildeten den Stand davor ab. Der Wrapper ist richtig, die Tests waren es nicht.
Seit lt-monorepo 3.13.1 wird der Build-Dir-Pin auf zwei Wegen geliefert, weil
VAR=value cmdPOSIX-Shell-Syntax ist und cmd.exe die Zuweisung als Kommandonamen liest: auf POSIX schreibtpinCheckBuildDirdas Textpräfix, auf win32 schreibt es nichts undstepEnvübergibt denselben Wert beim Spawn als echte Umgebungsvariable. Beide Tests prüften auf das Präfix und meldeten damit unter Windows einen korrekten Wrapper als Regression. Unter Linux liefen sie weiter — deshalb fiel es erst im Windows-Job auf, der bei #102–#104 grün war.Beide Zweige werden jetzt von jedem Host aus geprüft, statt einen auf dem anderen Betriebssystem per
skipstillzulegen. Einskiphätte ausgerechnet den Windows-Pfad auf der Maschine ungeprüft gelassen, auf der diese Suite normalerweise läuft — genau so ist der Fehler entstanden.pinCheckBuildDirnimmt die Plattform als Parameter, also stehen Präfix-Zweig und Kein-Präfix-Zweig direkt nebeneinander.buildGroupspinnt über die ambiente Plattform, deshalb läuft der Invarianten-Test zweimal, mit umdefiniertemprocess.platform. Das greift, weilpinCheckBuildDirdie Plattform als Default-Parameter liest — zur Aufrufzeit, nicht beim Import. Der Windows-Codepfad wird damit auf macOS nachgestellt statt ihm geglaubt.Die Invariante wurde umformuliert, nicht abgeschwächt. „Gepinnt" heißt nicht mehr „trägt ein Präfix", sondern „der Wert erreicht das Kind" — über das Präfix oder über die Umgebung aus
stepEnv. Damit bleibt die Eigenschaft erhalten, für die der Test existiert: hoistetbuildGroupsein Kommando, dasstepEnvnicht erkennt, ist der Pin unter Windows weg und nichts ersetzt ihn — und der Test wird auf beiden Plattformen rot.Beleg, dass sie noch beißt (ein Test, der sich an neues Verhalten anpasst, kann dabei seine Aussage verlieren). Zwei Mutationen des Wrappers, jede macht genau diese zwei Tests rot:
stepEnvliefert immernullstepEnvkeyt wieder auf das Textpräfix statt auf das KommandoEine Zusicherung ist neu statt angepasst: unter win32 trägt das einzige gehoistete Kommando mit
NUXT_BUILD_DIR=seinen Pin selbst mit (cross-env …). Das hält die Verhaltensänderung aus 3.13.1 als geprüfte Tatsache fest, statt sie dem Leser zu überlassen.Das Template selbst ist unangetastet —
check-template-syncprüft weiterhin jedes Byte gegen den Pin. Suite: 74 Suites / 1108 Tests.🤖 Generated with Claude Code
https://claude.ai/code/session_01N8cvaEziSrKGHv3Jcp59JH