Found while working #9348 (reading how the version pass is validated). Out of that card's scope — it adds a --self-test to the rewriter and touches no workflow but lint.yml. Filed unassigned.
What was observed
cut-rc.yml's Build the single version commit step enforces a release-file allowlist. Its doc half is read from the declaration:
node --input-type=module \
-e 'import { SURFACES } from "./scripts/check-docs-image-tag.mjs"; for (const s of SURFACES) console.log(s.file);'Its template half is restated, two literals, both hard-coding the template name blank:
git add -A -- \
... \
packages/create-objectstack/src/templates/blank/objectstack.config.ts \
packages/create-objectstack/src/templates/blank/objectstack.manifest.json \
...
with the same two paths repeated in the grep -vE re-check below it.
The comment above that block explains why, and its stated precondition is no longer true:
The manifest is named literally, like its sibling config.ts, rather than read the way the doc surfaces below are: sync-template-versions.mjs does declare its targets (a TEXT_STAMPS table plus a walk of src/templates/), but exports none of them and has no entry-point guard, so importing it would RUN the sync instead of reading its list. Give it those two things and this pathspec should read from it too.
PR #9648 (card #9554) gave it exactly those two things. scripts/sync-template-versions.mjs now exports stampedPaths() — documented in its own header as "the export cut-rc.yml's release-file allowlist is for" — plus TEXT_STAMPS, TEMPLATE_DIR, TEMPLATE_PKG_FILE and findTemplateDirs(), and carries the #9064 entry-point guard, so importing it rewrites nothing. Verified on 9ff11921a: the export resolves and packages/create-objectstack/src/template-version-stamps.test.ts pins that an import against a stale two-template checkout leaves it byte-identical.
Why it matters, and the honest severity
Today it is inert. The repo ships one template, so the two literals equal what stampedPaths() derives, and every cut passes.
The day a second template ships it is a failed release cut, not a soft failure. The allowlist is enforced in two directions: paths outside it are refused, and — the half that bites here — anything the version pass modified but left unstaged fails the step:
::error::tracked files were modified but fall outside the release file surface, so the version commit would be incomplete. Refusing to push.
sync-template-versions.mjs discovers templates by walking src/templates/, deliberately ("a template added tomorrow is covered on the day it lands"), so it will stamp the new template's three surfaces; the pathspec will stage none of them; the cut refuses to push. Nothing is red until someone attempts a release.
#9648's test already states this as an assertion, against a two-template fixture:
The finding, stated as an assertion: the pair cut-rc.yml spells literally is a STRICT SUBSET of what the version pass actually writes as soon as a second template exists.
So the failure is predicted and pinned; only the workflow half is outstanding.
Shape of a fix (not a ruling)
Mirror the doc half, on the terms the comment already sets out:
node --input-type=module \
-e 'import { stampedPaths } from "./scripts/sync-template-versions.mjs"; for (const p of stampedPaths()) console.log(p);'then stage and re-check by whole-line exact match (grep -vxF -f), the way SURFACE_LIST already is. Two details worth carrying over from the doc half: an empty resolved list must be a hard failure rather than an empty allowlist (stampedPaths() already throws instead of returning [], so this is a resolution failure only), and stampedPaths() reports the per-template package.json too, which the existing *package.json pattern already covers — so the union is a superset of today's behaviour, never a narrowing. The two blank literals and their grep -vE alternatives come out in the same edit; leaving them in would re-seed the restatement.
Refs: #9554 / PR #9648 (landed the export and the guard) · #9348 (found here) · #9518 (the last time this allowlist refused every cut) · #9064 (the doc half's precedent).
Generated by Claude Code
Found while working #9348 (reading how the version pass is validated). Out of that card's scope — it adds a
--self-testto the rewriter and touches no workflow butlint.yml. Filed unassigned.What was observed
cut-rc.yml'sBuild the single version commitstep enforces a release-file allowlist. Its doc half is read from the declaration:node --input-type=module \ -e 'import { SURFACES } from "./scripts/check-docs-image-tag.mjs"; for (const s of SURFACES) console.log(s.file);'Its template half is restated, two literals, both hard-coding the template name
blank:with the same two paths repeated in the
grep -vEre-check below it.The comment above that block explains why, and its stated precondition is no longer true:
PR #9648 (card #9554) gave it exactly those two things.
scripts/sync-template-versions.mjsnow exportsstampedPaths()— documented in its own header as "the exportcut-rc.yml's release-file allowlist is for" — plusTEXT_STAMPS,TEMPLATE_DIR,TEMPLATE_PKG_FILEandfindTemplateDirs(), and carries the #9064 entry-point guard, so importing it rewrites nothing. Verified on9ff11921a: the export resolves andpackages/create-objectstack/src/template-version-stamps.test.tspins that an import against a stale two-template checkout leaves it byte-identical.Why it matters, and the honest severity
Today it is inert. The repo ships one template, so the two literals equal what
stampedPaths()derives, and every cut passes.The day a second template ships it is a failed release cut, not a soft failure. The allowlist is enforced in two directions: paths outside it are refused, and — the half that bites here — anything the version pass modified but left unstaged fails the step:
sync-template-versions.mjsdiscovers templates by walkingsrc/templates/, deliberately ("a template added tomorrow is covered on the day it lands"), so it will stamp the new template's three surfaces; the pathspec will stage none of them; the cut refuses to push. Nothing is red until someone attempts a release.#9648's test already states this as an assertion, against a two-template fixture:
So the failure is predicted and pinned; only the workflow half is outstanding.
Shape of a fix (not a ruling)
Mirror the doc half, on the terms the comment already sets out:
node --input-type=module \ -e 'import { stampedPaths } from "./scripts/sync-template-versions.mjs"; for (const p of stampedPaths()) console.log(p);'then stage and re-check by whole-line exact match (
grep -vxF -f), the waySURFACE_LISTalready is. Two details worth carrying over from the doc half: an empty resolved list must be a hard failure rather than an empty allowlist (stampedPaths()already throws instead of returning[], so this is a resolution failure only), andstampedPaths()reports the per-templatepackage.jsontoo, which the existing*package.jsonpattern already covers — so the union is a superset of today's behaviour, never a narrowing. The twoblankliterals and theirgrep -vEalternatives come out in the same edit; leaving them in would re-seed the restatement.Refs: #9554 / PR #9648 (landed the export and the guard) · #9348 (found here) · #9518 (the last time this allowlist refused every cut) · #9064 (the doc half's precedent).
Generated by Claude Code