Uh oh!
There was an error while loading. Please reload this page.
fix(scripts): export sync-template-versions' declaration surface and guard its entry point - #9648
Conversation
…guard its entry point The script models its targets well — a TEXT_STAMPS table and a findTemplateDirs() walk that is deliberately not a curated list — but exported none of it and ran the sync at module scope, so a consumer that imported it to ask which paths the version pass writes rewrote every template instead of getting an answer. Export stampedPaths() plus the raw declarations, and add the entry-point guard #9064 added to check-docs-image-tag.mjs. The version read moved out of module scope into loadScaffolderVersion(), which throws instead of exiting: an import that can kill its host process is a worse hazard than the sync, not a smaller one. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XqDQYVU5smx29ts9pAErja
📓 Docs Drift CheckNothing in this diff resolved to a documentable surface (no symbol, route or SDK anchor derived from 0 changed package(s)), so this run has no opinion about the docs. |
✅ PM ACCEPT — #9554 / PR #9648 · both open questions ruledVerified independently: 2 files ⭐ Ruling on Q1: A — keep |
Uh oh!
There was an error while loading. Please reload this page.
… read it now is `CROSS_PACKAGE_TEST_INPUTS`' `create-objectstack` entry justified its `scripts/sync-template-versions.mjs` glob by the file being "named in a comment rather than read". PR #9648 landed `template-version-stamps.test.ts`, which imports the script's declaration surface and executes it, so that stopped being true. The glob is unchanged and still correct -- only its stated reason moved. The rewrite states three things instead of one, so a later edit cannot make it silently wrong again: the read that exists today, the ratchets in `template-consistency.test.ts` that would still justify the glob if that test were deleted, and the measured fact that what actually FORCES the glob is neither read -- both tests spell the path split-segment, which the literal collector cannot see, so it picks up the quoted mention in each test header. Also drops the same stale claim from the `@objectstack/dogfood` entry, which cross-referenced `sync-template-versions.mjs` as a fellow mention-only declaration. The `check-nul-bytes.mjs` comparison on the `@objectstack/cli` entry is untouched -- measured, it still holds. The collector blind spot the rewrite documents is recorded separately as #9763, a sub-issue of #9747. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XqDQYVU5smx29ts9pAErja
Fixes#9554
scripts/sync-template-versions.mjsalready modelled its outputs well — aTEXT_STAMPStable whose header says adding a fourth version surface should be "a row", and a
findTemplateDirs()walk that is "deliberately not a curated list". None of it wasreachable: nothing was exported, and the sync ran at module scope. A consumer that
imported the file to ask which paths does the version pass write? rewrote every
bundled template instead of getting an answer.
Measured on
origin/mainbefore the change:The whole sync ran, and the module exported nothing.
What changed
stampedPaths()— the derived answer: sorted, POSIX, repo-relative paths acrossall template dirs. Deriving it here is what stops each consumer re-implementing the
walk and the join.
TEXT_STAMPS,TEMPLATE_DIR,TEMPLATE_ROOT,TEMPLATE_PKG_FILE,VERSION_SOURCE,findTemplateDirs(),loadScaffolderVersion()— the raw declarations, mirroring howcheck-docs-image-tag.mjsexportsSURFACES/PATTERNS/VERSION_SOURCEalongside itsderived functions (Docs image tags have no version-time rewriter, so the new check:docs-image-tag gate cannot fire on the release PR that breaks it #9064).
TEXT_STAMPS'svalue/replacementbecame functions of the version rather thanstrings baked in at module load. This is what lets the table be a static declaration an
importer can read without the module first going and reading a package.json — the same
split Docs image tags have no version-time rewriter, so the new check:docs-image-tag gate cannot fire on the release PR that breaks it #9064 makes between static
PATTERNSand a threaded-throughexpected.loadScaffolderVersion(), whichthrows instead of calling
process.exit(1). An import that can kill its hostprocess is a worse import hazard than the sync, not a smaller one.
Why
stampedPaths()reports three files per template, not twoIt names
package.jsonalongside the two text-stamp files. A consumer asking which filesthe version pass writes is asking about
package.jsontoo — it carries the@objectstack/*ranges this script rewrites. An export that answered the narrowerquestion while being named for the wider one would seed the next restatement.
This is a superset of the two literals in the release allowlist by exactly one path per
template, and that path is already allowlisted there by the
'*package.json'pathspecand the
(^|/)package\.json$re-check alternation. So consuming the full list permitsnothing new; it only moves the answer from a literal to a declaration.
⛔ The fenced half is NOT in this PR
.github/workflows/cut-rc.ymlis claimed by epic #9465 and is not touched here, noris any adjacent file routed around it. This branch changes exactly two files:
That workflow's own comment already asks for this change, which is why the split is clean:
It now has those two things. The hand-off below is posted on #9465 for that lane to apply;
following the precedent of PR #9620, which hit the same fence and handed its piece over
rather than reaching across it.
Verification
H2 — every invocation site, before and after. The only executable invocation is the
root
versionscript (changeset version && ... && node scripts/sync-template-versions.mjs && ...), reached fromcut-rc.yml'spnpm run versionstep and fromchangesets/actionon the release PR. The other two references are not invocations:
turbo.jsonnames thescript as a cache input to
create-objectstack#test, andcheck-cross-package-test-inputs.mjsasserts that declaration exists. Both still hold.CLI output is byte-identical to
origin/main, proven by running the pre-change file atthe same directory depth:
After the change, importing is silent and writes nothing, and the declarations are
readable:
H3 — the test that keeps this fixed rather than re-found. The repo ships exactly one
template today, so on the live tree the walk and a literal
blankpair agree and nothingis red — which is why this could only be found by reading. The load-bearing cases therefore
run against a temp checkout carrying two templates.
That fixture is deliberately built stale (pinning
^17while its scaffolder reads42.0.0), which is what makes the import-safety assertion non-vacuous: an unguarded moduleimported against a stale tree rewrites it, so "byte-identical after import" is evidence
only when there was something for a rewrite to do.
Both limbs were reverse-verified from the committed state. Predicted direction for each was
turns red, and both did:
importing ... rewrites nothingfails — the import rewrote all six stale fixture surfacesblanktriplecovers every discovered templatefailsThe second ablation is the more informative one: the restated triple passed 7 of the 8
assertions, including
names only paths that existandagrees with the live walk,because on a one-template tree a restatement is indistinguishable from a derivation. Only
the two-template fixture caught it. That is the finding, reproduced as a test.
Commands run, all at
6372dc9a0:The upstream build closure is genuinely empty —
create-objectstackdepends only onchalkandcommander, no workspace packages — confirmed rather than assumed, since azero-match
--filterexits 0 having run nothing.No changeset:
skip-changesetThis PR publishes nothing.
scripts/is not a published package, and the new.test.tsisnot shipped (
create-objectstack'sfilesisdist/README.md/CHANGELOG.md). Thescript's user-visible behaviour is byte-identical, as measured above. An empty-frontmatter
changeset is not a route — #5471 shut it for new files — so a real changeset here would
declare a release of
create-objectstackcontaining no user-visible change. Theskip-changesetlabel is the correct mechanism, and it also keeps this PR out of thefenced
.changeset/tree entirely.Scope
#9348 (same file: no
--self-test, runs nowhere in CI) is not implemented here andremains open; it is a separate change to the same file. The import-safety added here is its
prerequisite, since a self-test needs to call the module without running the sync.
Generated by Claude Code
Generated by Claude Code