Skip to content

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
DKoenig9 merged 4 commits into
mainfrom
chore/repin-check-template
Sep 21, 2026
Merged

DKoenig9 merged 4 commits into
mainfrom
chore/repin-check-template

Conversation

@DKoenig9

@DKoenig9 DKoenig9 commented Sep 21, 2026

Copy link
Copy Markdown
Contributor

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.1fcf7c83. The first re-pin with real content rather than a marker bump:

  • pinCheckBuildDir writes no NUXT_BUILD_DIR= prefix on win32 — cmd.exe reads it as a command name — and stepEnv 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

Beyond the wrapper: with this pin a newly generated project also 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. 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 the reinit script that calls it, and for existing projects healCheckWrapper installs only the closure and never rewrites reinit — 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

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.

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 — 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.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 drafts it cost, both worth knowing before touching this code

  • The first version fired on a COMMENTbash scripts/audit.sh on check.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.
  • stripComments is 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.createScanner returns TemplateHead at `x${ and needs reScanTemplateToken() to continue; the plain scan() loop never calls it, so from the first interpolating template literal on, the scanner is desynchronised and comments pass through verbatim. Bisected on check.mjs by stripping growing prefixes: the regex (/^projects\//, line 63) and the division (ms / 1000, line 66) are both handled, the break is line 67's return `${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.createScanner has 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 fcf7c83 without 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 cmd POSIX-Shell-Syntax ist und cmd.exe die Zuweisung als Kommandonamen liest: auf POSIX schreibt pinCheckBuildDir das Textpräfix, auf win32 schreibt es nichts und stepEnv ü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 skip stillzulegen. Ein skip hätte ausgerechnet den Windows-Pfad auf der Maschine ungeprüft gelassen, auf der diese Suite normalerweise läuft — genau so ist der Fehler entstanden.

  • pinCheckBuildDir nimmt die Plattform als Parameter, also stehen Präfix-Zweig und Kein-Präfix-Zweig direkt nebeneinander.
  • buildGroups pinnt über die ambiente Plattform, deshalb läuft der Invarianten-Test zweimal, mit umdefiniertem process.platform. Das greift, weil pinCheckBuildDir die 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: hoistet buildGroups ein Kommando, das stepEnv nicht 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:

Mutation Wirkung
stepEnv liefert immer null der Windows-Pin verschwindet ersatzlos
stepEnv keyt wieder auf das Textpräfix statt auf das Kommando genau die Regression, vor der der Docblock des Wrappers warnt

Eine 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-sync prüft weiterhin jedes Byte gegen den Pin. Suite: 74 Suites / 1108 Tests.

🤖 Generated with Claude Code

https://claude.ai/code/session_01N8cvaEziSrKGHv3Jcp59JH

DKoenig9 and others added 4 commits September 21, 2026 15:09
`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
DKoenig9 force-pushed the chore/repin-check-template branch from 878e1f2 to dfafd03 Compare September 21, 2026 13:13
@DKoenig9
DKoenig9 marked this pull request as ready for review September 21, 2026 13:21
@DKoenig9
DKoenig9 merged commit fcdb7f3 into main Sep 21, 2026
2 checks passed
@DKoenig9
DKoenig9 deleted the chore/repin-check-template branch September 21, 2026 13:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant