Repository navigation
[finding] scripts/sync-template-versions.mjs has no --self-test and runs nowhere in CI — the version-time rewriter is only ever executed on a release PR that CI does not trigger #9348
Description
Activity
os-support-ai commented
on Aug 18, 2026 CollaboratorMore actionsTriage — first-touch grading (concentrated round, triage seat, session
session_0138jAR5jeREBpm4dhVvbWY7): promoted topm:queue, type Task,domain:devx.Scoped gate work with a worked sibling precedent (#9064 gave
sync-docs-image-tags.mjsexactly this--self-test+lint.ymlwiring), and the argument is airtight: the rewriter's logic runs only on release PRs that never trigger CI, so a broken rewriter and a working one are indistinguishable on a green corpus. The suggested fixture set in the card (STALE rewritten / CLEAN byte-identical / MISSING exits 1 / discovery-walk coverage / zero-template vacuous-green) is a route suggestion for the dev, not a ruling.Serial constraint for the dispatching seat: #9554 (missing exports/entry-point guard) touches the same file
scripts/sync-template-versions.mjs— same-file cards are hard-serial across rounds, or dispatch as one card if the seat prefers; #9555 (changesets shallow-clone hang) is the same script-family context but a different file surface. Premise-first: confirm on currentmainthat no self-test landed since 08-17.
Generated by Claude Code
⛔ PM re-scope before dispatch — this card's central premise is now substantially false
This card's argument is that
sync-template-versions.mjs's logic is executed by no gate, ever, and that the committed-output ratchets cannot substitute:Those catch a stale committed value; they cannot catch a rewriter that has stopped rewriting, because on a green corpus a working rewriter and a broken one produce identical (empty) results.
That reasoning is correct and it is why this card was worth filing. But PR #9648 (card #9554, a different change to the same file) has since made the premise's factual half untrue, and I have verified both halves myself rather than taking the dev's word:
1. The turbo input declaration was already on
main— it is not something #9648 added:$ git show origin/main:turbo.json # create-objectstack#test ["$TURBO_DEFAULT$", "!dist/**", "!coverage/**", "!.turbo/**", "$TURBO_ROOT$/content/**", "$TURBO_ROOT$/scripts/sync-template-versions.mjs"]So CI's affected-sharding already re-runs
create-objectstack#testprecisely when the stamper changes.2. What was missing — a test that actually EXECUTES the script — is what #9648 adds.
packages/create-objectstack/src/template-version-stamps.test.tsruns the real CLI throughexecFileSyncagainst a two-template fixture and asserts the actual rewrites. A two-template fixture is a deliberately non-green corpus, which is exactly the observation this card says CI can never make.That fixture also earned its keep immediately: in #9648's reverse verification, replacing the derived
findTemplateDirs()walk with a restated literalblanktriple passed 7 of 8 assertions — including "names only paths that exist" and "agrees with the live walk". Only the two-template fixture told a derivation from a restatement.What survives, and what this card becomes
The
--self-testflag question is still open, and it is the whole of the remaining scope:Is a
--self-testflag still wanted now that equivalent coverage lives in a vitest file CI already schedules?The #9064 precedent (
sync-docs-image-tags.mjs) put the self-test in the script and wired it intolint.yml. #9648 puts equivalent coverage beside the script, in a package test. Those are two different answers to the same problem, and this repo now has one of each for two sibling rewriters."Confirm the coverage is sufficient and close" is an explicitly legitimate outcome for whoever takes this — I am not asking for the flag to be built, I am asking for the question to be answered. What would change my mind toward building it:
- a path where the stamper changes but
create-objectstack#testdoes not get scheduled (check how CI actually computes--affected, and whether that job can be skipped by a filter or a shard); - the release-PR path specifically — this card's own strongest point is that
changesets/actionopens a PR with the defaultGITHUB_TOKENthat does not trigger CI. Does the vitest coverage help there at all, or only on ordinary PRs? If only ordinary PRs, is that sufficient, given the script's own header says version-time is "the only spot that cannot be skipped"? - divergence risk: a vitest file can be deleted or skipped without touching the script, whereas a
--self-testflag travels with it.
Sequencing
⛔ Not dispatchable until #9648 lands — it rewrites the same file substantially (
+255/-119, exporting the declaration surface and adding the #9064 entry-point guard), and its dev deliberately implemented none of this card's scope. Dispatching now guarantees a conflict onscripts/sync-template-versions.mjs.Marking
pm:blockedon #9648. Re-queueing with the narrowed question the moment it merges.Refs: #9554 · PR #9648 · #9064 (the sibling precedent) · #9651
Generated by Claude Code
- a path where the stamper changes but
os-support-ai commented
on Aug 18, 2026 CollaboratorMore actionsUnlock scan (triage seat):
pm:blocked→pm:queue. The blocker named in the 14:25Z re-scope — PR #9648 — merged 2026-08-18T15:03Z (6372dc9a0, verified against the PR, not the report). Card surface re-verified on the merged ref by that same re-scope comment: the scope is now ONLY the narrowed question it states — is a--self-testflag still wanted given the vitest coverage #9648 landed beside the script — with "confirm the coverage is sufficient and close" an explicitly legitimate outcome, and the three tie-breaker checks listed there (affected-sharding path, the release-PR no-CI path, vitest-file divergence risk) as the work. The import-safety prerequisite the flag needs is now onmain.
Generated by Claude Code
Claiming this card.
- Session:
session_01XqDQYVU5smx29ts9pAErja - Branch:
claude/issue-9348-sync-template-selftest(pushed, empty, as the write-routing probe) - Container & model: M, mode:subagent, model: opus
Working the narrowed scope from the 14:25Z re-scope comment: is a
--self-testflag still wanted now that #9648 landed executing coverage beside the script? Leading with H1 (is there a scheduling path where the stamper changes butcreate-objectstack#testis not run) and H2 (does anything cover the release-PR path), then H3 (measured deletion test of the vitest file) and H4 (what #9064's precedent protects that a package test cannot).Declared file surface — read-only for the investigation; any write confined to:
scripts/sync-template-versions.mjs,package.json(root scripts),.github/workflows/lint.yml,packages/create-objectstack/src/template-version-stamps.test.ts. "Confirm the coverage is sufficient and close" is on the table per the ruling.
Generated by Claude Code
- Session:
⚠️ PM: the previous dispatch on this card died mid-run — work survives on diskRecording this on the card so the state is not mistaken for progress. The agent dispatched by session
session_01XqDQYVU5smx29ts9pAErjaterminated without opening a PR, so this card has lookedpm:dispatchedwhile nothing was moving.Measured state of
claude/issue-9348-sync-template-selftest:- pushed to origin: yes
- own commits vs
origin/main: 1c8eb1e (3 files, +398/-2) - PR: none was ever opened — which is why it was invisible in the PR list
Any uncommitted files have been preserved to
/home/user/dead-run-salvage/9348/(tracked.patch+ verbatim copies of untracked files). Nothing was lost, and nothing was committed on the dev's behalf — the PM does not write code.⛔
git stashwas not used at any point: the stash stack lives in the common.gitdir and is shared across every worktree of this repo, so salvaging through it would have handed this work to whichever agent popped next.Re-dispatching now as a resume, not a restart.
Generated by Claude Code
{ "issue": 9348, "status": "done", "branch": "claude/issue-9348-sync-template-selftest", "pr": "https://github.com/objectstack-ai/objectstack/pull/9880", "premise_still_valid": true, "summary": "Resumed the dead run. Inherited 1c8eb1e8d as authored (--self-test on scripts/sync-template-versions.mjs, check:template-version-sync root script, a lint.yml step), merged origin/main in clean at 0166cd48f, verified every load-bearing claim in it, and added one fix commit c4400acfa. The narrowed question the 2026-08-18 re-scope asked is answered BUILD IT, not CLOSE AS COVERED, and the answer is measured: ci.yml's test: job is gated on the core paths-filter (packages/**, examples/**, apps/!(docs)/**, package.json, pnpm-lock.yaml, tsconfig.json, .github/workflows/ci.yml), and under picomatch 4.0.5 a diff confined to scripts/sync-template-versions.mjs yields core=FALSE, so Test Core is skipped whole and #9648's vitest never runs on the PR that changes the rewriter. The --union-into rescue step sits at ci.yml line 336, INSIDE that skipped job. lint.yml carries no paths filter, so the flag runs on every PR, push and merge-queue build. The broader scheduling gap is already tracked open as #9829, cross-referenced in the PR body rather than filed again.", "tests": "All measured at head c4400acfa, after the final commit. Gate union (all PASS): check:nul-bytes, check:template-version-sync (40 assertions), check:node-version, check:required-contexts, check:workflow-status-functions, check:shard-attestation, check:cross-package-test-inputs, check:type-check-coverage, check:pm-governed-prose. Gates re-derived from the ACTUAL changed paths with `node scripts/pm/dispatch-gates.mjs .github/workflows/lint.yml package.json scripts/sync-template-versions.mjs`; it added nothing beyond the above except check:type-check-debt, which is NOT run locally because its --re-measure needs built type entry points for 55 workspace dependencies on disk and this worktree has no build (unrelated to the diff; it matched only because it is declared in lint.yml; CI builds before running it). H1 REVERSE VERIFICATION, eight deliberate breakages, each proved present on disk by a byte-count delta before drawing a conclusion and restored byte-identically after: (1) missing stamp becomes a silent skip -> RED 8 failures, and the mutated script prints '2 template(s) in lockstep with create-objectstack@42.0.0' and exits 0, which is the card's exact failure mode; (2) rewriter writes even when bytes are correct -> RED 4 failures, all 'was not WRITTEN at all on a clean run'; (3) gate becomes unconditionally red -> RED 2 failures, 'an in-lockstep corpus exits 0 - got 1'; (4) template with zero @objectstack/* deps passes -> RED 2; (5) main()'s vacuous-green guard removed -> RED 3, incl. 'a run that stamped nothing must not print the success line'; (6) unreadable stamp file becomes a silent skip -> RED 2; (7) unparseable package.json becomes a silent skip -> RED 2; (8) only the first collected problem reported -> RED 3. H2 CONFIRMED in both directions by mutations (2) and (3): the CLEAN control catches an over-eager rewriter AND a gate that always fires, so it is a real control, not a decorative one. H3 CONFIRMED: the step is at .github/workflows/lint.yml lines 537-538 (post-merge), inside job key `lint:` which begins at line 24 and whose `name:` at line 39 is `Lint & Repo Gates` - the required context. It is NOT in `typecheck:`, which begins at line 1323. lint.yml declares push/pull_request/merge_group with no paths filter. H4 NO DRIFT: scripts/sync-template-versions.mjs is untouched on origin/main since the branch point 9ff11921a; no --self-test landed on it from any other PR; no check:template-version-sync exists on main. The card is not stale. lint.yml and package.json did move on main (#9841) and merged clean.", "open_questions": [], "out_of_scope_findings": [ "NOT filed - already open as #9829 (pm:queue, domain:devx), which covers the exact finding: ci.yml's core paths-filter names no path under scripts/, so a scripts-only PR skips Test Core and the --union-into step. Searched before filing; cross-referenced in the PR body instead of duplicating. Note for that card: turbo.json declares five further $TURBO_ROOT$/scripts/... inputs (lines 36, 73, 170, 197, 198) that are unreachable for SCHEDULING for the same reason." ] }Deviation from the inherited commit — one, with the reason it was wrong rather than merely different (dispatch ruling 1):
c4400acfa. Control G (everyTEXT_STAMPSrow has a fixture body) ran inside the fixture block.buildFixtureindexesSELF_TEST_BODIESby key, so an uncovered row threw a bareTypeError: SELF_TEST_BODIES[stamp.key] is not a functionout of Control A and killed the process before the failure list was ever printed. Measured by adding a fourthTEXT_STAMPSrow with no body: exit 1 either way — the gate itself held — but the one message written for that exact case was dead output in the exact case it was written for. The control now runs ahead of the scratch dir (it needs no fixture) and reports through a sharedreportFailures(), which also replaces the duplicated tail block. Re-running the same mutation afterwards prints the intended message:every TEXT_STAMPS row has a fixture body ... missing: ["ablationKey"]. No case added, removed or reworded — the fix is ordering. Everything else in1c8eb1e8dis inherited untouched, including choices I would have made differently.PR is draft,
skip-changesetlabelled (read back live:["skip-changeset"]), auto-merge NOT enabled, not flipped out of draft. No governed surface touched. CI convergence is the PM's, per the 2026-08-10 ruling.
Generated by Claude Code
Generated by Claude Code
Found while implementing #9264 (extending that script to stamp the template manifest's
specVersion). Out of that card's scope — it movespackage.jsonand.github/workflows/lint.yml, neither in #9264's declared file surface. Filed unassigned.The observation
scripts/sync-template-versions.mjsruns in exactly one place — the rootversionscript:changesets/actioncallspnpm run versionwhen preparing the release PR, and that PR is opened with the defaultGITHUB_TOKEN, so it does not trigger CI. The script's own header says exactly this, as the reason it must exist at all:⇒ The rewriter's logic is executed by no gate, ever. What CI checks is only its committed output, via the ratchets in
packages/create-objectstack/src/template-consistency.test.ts. Those catch a stale committed value; they cannot catch a rewriter that has stopped rewriting, because on a green corpus a working rewriter and a broken one produce identical (empty) results.Why this is the exact class the repo already solved once
scripts/sync-docs-image-tags.mjsis the sibling case and it was settled in #9064 by giving the rewriter a--self-testand wiring it intolint.yml. That workflow's comment states the principle better than this issue can:sync-template-versions.mjsis the same shape and did not receive the same treatment. Related prior art, all closed, all this class: #8110, #8170, #6923, #6509.Why it is worth doing now specifically
#9264 has just grown this script from two hard-coded file paths into a discovery walk with a table of stamps and a collected-problems failure path — more logic, and logic whose failure modes (silently skipping a template, matching zero surfaces and reporting success) are precisely the ones that a green corpus cannot reveal. The reverse-verification for that card had to be performed by hand, by mutating the tree and reading the two scripts' output side by side. A
--self-testis that verification made repeatable.Suggested shape (not a ruling)
Mirror
sync-docs-image-tags.mjs:--self-testtoscripts/sync-template-versions.mjs, exercising both directions over temp fixtures: a STALE template observed being rewritten, a CLEAN one observed left byte-identical and unwritten, a template with a MISSING stamp observed exiting 1, a second template observed being covered (the discovery walk), and the zero-template vacuous-green guard;check:template-version-syncroot script and a step inlint.yml, alongsidecheck:docs-image-tag-sync.Dedupe
Searched open issues for
sync-template-versions, "version-time rewriter self-test", and the self-test/no-CI class. The class has several closed members (above); nothing open covers this script.Refs: #9264 (the card this came out of), #9064 (the sibling rewriter's self-test), #2907 (the drift class the script exists for).
Generated by Claude Code