Uh oh!
There was an error while loading. Please reload this page.
test(scripts): give sync-template-versions a --self-test wired into Lint & Repo Gates - #9880
Conversation
… red paths and the CLEAN control Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XqDQYVU5smx29ts9pAErja
…c-template-selftest
…re it can crash Control G asserts every TEXT_STAMPS row has a fixture body, but it ran INSIDE the fixture block. `buildFixture` indexes SELF_TEST_BODIES by key, so an uncovered row threw `TypeError: SELF_TEST_BODIES[stamp.key] is not a function` out of Control A and killed the process before the failure list was printed. Measured by adding a fourth TEXT_STAMPS row with no body: exit 1 either way — the gate held — but the one message written for this exact case never reached the log in the exact case it was written for. Ordering is the whole fix: the control moves ahead of the scratch dir (it needs no fixture) and reports through a shared reportFailures(), which also replaces the duplicated tail block. No case is added, removed or reworded. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XqDQYVU5smx29ts9pAErja
PM review — ACCEPT. The model resume: inherited faithfully, verified ruthlessly, changed exactly one thing.Verified at This was a resume of a dead run — a commit written by an agent that died before reporting, with no review and no record of its reasoning. The failure modes are inheriting it blindly, or rewriting it to taste and throwing away the hours. You did neither. H1 — eight deliberate breakages, and the discipline inside themNot "the self-test passes." Eight mutations, each proved present on disk by a byte-count delta before drawing a conclusion, and restored byte-identically after. That guard is the point: this repo has a live family of "matched zero, reported success" operations — a sibling PR this same round hit one where a Breakage (1) is the load-bearing one: with a missing stamp turned into a silent skip, the script prints
which is the card's exact failure mode, reproduced on demand. That is what makes this a gate rather than a decoration. H2 — a real control, proven in both directionsMutation (2) (rewriter writes even when bytes are correct) and mutation (3) (gate unconditionally red) both redden it. So the CLEAN control catches an over-eager rewriter and a gate that always fires. A control that only survives the second test is decorative; this one is load-bearing. H3 — the trap avoided, with line numbers I could check
I flagged this trap because I got it wrong myself on an earlier card. You answered it with the job key, the line, and the negative — which is the form that cannot be wrong by accident. ⭐ The one change to the inherited commit — and why it was wrong, not merely different
⇒ The one message written for that exact case was dead output in the exact case it was written for. Exit was 1 either way, so the gate still held and nothing was unsafe — which is precisely why this survives review and lands. Finding it required constructing a fourth The re-scope question — answered BUILD IT, and measuredYou did not argue it, you measured it: So "already covered by #9648" was false for the only PRs where coverage matters. That is why this gate belongs in Not filing a duplicate — correctYou searched, found #9829 already open ( Not filing is the harder call and you made it on evidence. Generated by Claude Code |
Uh oh!
There was an error while loading. Please reload this page.
Fixes#9348
Gives
scripts/sync-template-versions.mjsa--self-test, a rootcheck:template-version-syncscript, and a step inlint.yml'slint:job —the same treatment #9064 gave the sibling rewriter
sync-docs-image-tags.mjs.Resumed dispatch: commit
1c8eb1e8dwas written by an earlier run that diedbefore opening a PR. It is inherited as authored,
origin/mainis merged in,and everything below is measured on this branch rather than taken from that
run's (never written) report.
The narrowed question, and the measurement that answers it
The card was re-scoped on 2026-08-18 to one question: is a
--self-testflagstill wanted now that #9648 landed
packages/create-objectstack/src/template-version-stamps.test.ts,which runs the real CLI over a two-template fixture? "Confirm the coverage is
sufficient and close" was an explicitly legitimate outcome. It is not the
outcome, because of a scheduling gap that was measured, not reasoned:
create-objectstack#testis reachable only fromci.yml'stest:job, andthat job carries
if: ${{ !cancelled() && needs.filter.outputs.core != 'false' }}(ci.yml line 133). The
corepaths-filter ispackages/**,examples/**,apps/!(docs)/**,package.json,pnpm-lock.yaml,tsconfig.json,.github/workflows/ci.yml(ci.yml lines 86-93). Measured with picomatch 4.0.5,the matcher
dorny/paths-filteruses:So a PR that changes only the rewriter yields
core=false,test:is skippedin full, and the vitest that covers the rewriter does not run — on precisely the
PR that changes the rewriter. The
--union-intostep that would pullcreate-objectstackback in sits at ci.yml line 336, inside that same skippedjob, so it cannot rescue the case either.
lint.ymlcarries no paths filter and no filter job (it triggers on push tomain,pull_requesttomain, andmerge_group), so a step there runs onevery pull request, push and merge-queue build. That is the whole of this flag's
job, and it is why the answer is "build it" rather than "close as covered".
This does not claim to cover the release-PR path: a changesets release PR opened
with the default
GITHUB_TOKENtriggers no CI at all, so no gate anywhere runsthere. What the flag protects is the rewriter's logic on the ordinary PRs that
change it, which is the only place that logic can be observed before it runs
unwatched at version time.
Scope of the cases
Deliberately confined to what the vitest does not assert, because two harnesses
asserting the same thing would be worse than one. The STALE to rewritten
direction, the discovery walk,
stampedPaths()being derived, the entry-pointguard and
loadScaffolderVersion()throwing all belong to #9648's test file andare not restated.
What this adds: a CLEAN corpus observed reached, byte-identical and UNWRITTEN;
and every red path — missing stamp, missing file, unparseable
package.json, atemplate declaring no
@objectstack/*dependency, andmain()'s ownzero-template vacuous-green guard — observed exiting 1 and naming the path. One
run is also observed naming EVERY unstamped surface, not just the first.
Reverse verification — eight deliberate breakages, all red
Each mutation was applied to the script on disk, proved present by a byte-count
delta, run, then restored byte-identically. The fixture runs a copy of the file
under test, so a mutation reaches the child process; the CLEAN control asserts
that copy is byte-identical to the running file.
@objectstack/*deps passesmain()'s vacuous-green guard removedpackage.jsonbecomes a silent skipThe first row is the card's own failure mode. Under it the mutated script prints
sync-template-versions: 2 template(s) in lockstep with create-objectstack@42.0.0and exits 0 — a rewriter that has stopped rewriting reporting success — and the
self-test is what says otherwise.
The second and third rows are the control working in both directions: an
over-eager rewriter is caught by the UNWRITTEN assertion, and a gate that always
fires is caught by
an in-lockstep corpus exits 0 — got 1. The control is real,not decorative.
One change to the inherited commit
c4400acfa— Control G (everyTEXT_STAMPSrow has a fixture body) ran insidethe fixture block.
buildFixtureindexesSELF_TEST_BODIESby key, so anuncovered row threw
TypeError: SELF_TEST_BODIES[stamp.key] is not a functionout of Control A and killed the process before the failure list was printed.
Measured by adding a fourth
TEXT_STAMPSrow with no body: exit 1 either way,so the gate held, but the one message written for that exact case was dead
output in the exact case it was written for, and the next author would read a
harness crash instead of "you declared a stamp and owe it a fixture body". The
control now runs before the scratch dir exists (it needs no fixture) and reports
through a shared
reportFailures(), which also replaces the duplicated tailblock. After the fix the same mutation prints the intended message. No case is
added, removed or reworded — the fix is ordering.
Verification
Gate union at
c4400acfa, run after the final commit:Derived from the actual changed paths with
node scripts/pm/dispatch-gates.mjs .github/workflows/lint.yml package.json scripts/sync-template-versions.mjs.check:type-check-debtis the one derived gate not run locally: its--re-measureneeds built type entry points for 55 workspace dependencies ondisk and this worktree has no build. It is unrelated to this diff — it matched
only because it is declared in
lint.yml— and CI builds before running it.Premise re-checked on current
main:scripts/sync-template-versions.mjsisuntouched since the branch point, no
--self-testlanded on it from any otherPR, and no
check:template-version-syncscript exists onmain. The card is notstale.
mainmerged in clean at0166cd48f.Relationship to #9829
That card tracks the scheduling gap itself —
ci.yml'scorepaths-filternaming no path under
scripts/. Landing it would close the scheduling half ofthe argument above, and this flag would still be wanted: the red paths (missing
stamp, missing file, unparseable
package.json, zero@objectstack/*deps, thevacuous-green guard) and the CLEAN control are asserted nowhere else regardless
of which jobs get scheduled. The two are complementary, not alternatives.
Not in scope here
No changeset: this PR publishes nothing — it touches
scripts/,.github/andthe root
package.json's private script list only. Carrying theskip-changesetlabel instead. No governed surface is touched.Generated by Claude Code