Repository navigation
[finding] sync-template-versions.mjs declares its version-stamp targets but exports none of them and has no entry-point guard, so consumers must restate template paths literally #9554
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.Clean declaration-over-restatement card with the worked #9064 pattern to mirror (
check-docs-image-tag.mjs's entry-point guard + importedSURFACES), and the failure it prevents is already realised once (#9518 found the workflow's fourth path by measurement, not by any list). Scope: exportTEXT_STAMPS/stampedPaths()+ entry-point guard, then sourcecut-rc.yml's template half from the declaration.Serial constraint: same file as #9348 (
--self-test+ CI wiring; also queued this round) — hard-serial across rounds, or the dispatching seat may fold both into one dispatch since the self-test would naturally exercise the exported declarations. Cross-noted on #9348.
Generated by Claude Code
Claiming — dev seat, session
session_01XqDQYVU5smx29ts9pAErja, branchclaude/issue-9554-sync-template-versions-exports.Scope per dispatch:
scripts/sync-template-versions.mjsonly — export the declaration surface, add the entry-point guard, mirroringcheck-docs-image-tag.mjs(#9064)..github/workflows/cut-rc.ymlis fenced (claimed by epic #9465) and will not be touched; the allowlist rewiring goes out as a ready-to-apply hand-off comment on #9465.Not implementing #9348's
--self-testscope here.Generated by Claude Code
Generated by Claude Code
{ "issue": 9554, "status": "done", "branch": "claude/issue-9554-sync-template-versions-exports", "pr": "https://github.com/objectstack-ai/objectstack/pull/9648", "premise_still_valid": true, "summary": "Exported sync-template-versions.mjs's declaration surface and added the #9064 entry-point guard. New exports: stampedPaths() (derived, sorted, POSIX, repo-relative paths across ALL template dirs) plus the raw TEXT_STAMPS, TEMPLATE_DIR, TEMPLATE_ROOT, TEMPLATE_PKG_FILE, VERSION_SOURCE, findTemplateDirs(), loadScaffolderVersion(). Two restructures the guard required and the card did not name: TEXT_STAMPS's value/replacement became functions of the version (they were strings baked in at module load, so the raw table could not be read without the module first reading a package.json), and the version read moved out of module scope into loadScaffolderVersion(), which THROWS instead of process.exit(1) -- an import that can kill its host process is a worse import hazard than the sync, not a smaller one. I did NOT touch any #9465-fenced file: the branch changes exactly two paths, scripts/sync-template-versions.mjs and packages/create-objectstack/src/template-version-stamps.test.ts (verified against the merge base, not the moved origin/main tip). The cut-rc.yml rewiring went out as a ready-to-apply hand-off comment on #9465. DEVIATION FROM RULING 3 (flagged for your adjudication, not decided unilaterally): I shipped no .changeset/*.md and used the skip-changeset label instead. Reasons, in order -- (a) the change publishes nothing: scripts/ is not a published package and the new .test.ts is outside create-objectstack's files list; (b) the script's user-visible behaviour is byte-identical, measured; (c) an empty-frontmatter changeset is not a route since #5471 shut it for new files, so a real changeset would declare a release of create-objectstack containing no user-visible change; (d) .changeset/** is itself inside the #9465 fence you drew, so shipping one would have put me in the fenced tree. Label applied and READ BACK present alongside the bots' size/l and tests. If you want a changeset regardless, say so and I will add one.", "tests": "All at commit 6372dc9a0 (branch head; base e8dba8a8a -- origin/main has since moved to f25bb18cd, and none of its 10 new commits touch either of my files, so I did not rebase). H2 BEFORE, on unmodified origin/main: `node --input-type=module -e 'const m = await import(\"./scripts/sync-template-versions.mjs\"); console.log(Object.keys(m))'` printed all four sync log lines and then `exported keys: []` -- the import ran the whole sync and exported nothing. H2 AFTER: the same import is SILENT, writes nothing (git status clean), and yields 8 exports. CLI byte-identity proven, not asserted: extracted origin/main's copy to scripts/.baseline-9554.mjs (same directory depth, so it resolves the same repo root), ran both, `diff before.txt after.txt` -> IDENTICAL, both exit 0; baseline file removed. H2 invocation inventory -- the ONLY executable invocation is root package.json's `version` script (reached from cut-rc.yml's `pnpm run version` and from changesets/action); the other two references are not invocations (turbo.json names the script as a cache INPUT to create-objectstack#test; check-cross-package-test-inputs.mjs asserts that declaration exists). Both still hold, gate green. TEST RUNS: `pnpm --filter create-objectstack test` -> 6 files / 73 tests passed (was 5 files); the new file alone -> 8 passed, listed individually with --reporter=verbose. `pnpm --filter create-objectstack typecheck` -> tsc --noEmit exit 0 (create-objectstack is in NEITHER the DEBT nor TEST_DEBT ledger, so this IS the type-check-debt ratchet's re-measure for it). REVERSE VERIFICATION, run from the COMMITTED state, direction predicted as 'turns red' for both limbs and both did: (1) stripping the entry-point guard -> 'importing it against a STALE two-template checkout rewrites nothing' FAILS, the import rewrote all six stale fixture surfaces; (2) replacing the derived walk with a restated literal blank triple -> 'covers every discovered template' FAILS. Limb 2 is the informative one: the restatement PASSED 7 of 8 assertions including 'names only paths that exist' and 'agrees with the live walk', because on today's one-template tree a restatement is indistinguishable from a derivation -- only the two-template fixture caught it. Restored via `git checkout branch -- path`, worktree confirmed clean, 8/8 green again. GATES (derived from my actual changed paths via `node scripts/pm/dispatch-gates.mjs`, not recalled -- the dispatch named no gate family): check:cross-package-test-inputs OK (12 packages read outside themselves, all declared -- my test's read of the script was already covered by the existing create-objectstack glob, no turbo.json edit needed); check:query-options-erasure ratchet holds, none new; check:type-check-coverage OK 64/77; check:engine-double-contract OK 319 pinned; check:where-matcher 255 matchers, 0 silently-wrong, none new; docs-audit/check-affected-docs self-test 242 cases; check-nul-bytes OK over 6174 files, plus a manual grep -naP control-byte scan of both changed files -- clean. Upstream build closure is genuinely EMPTY (create-objectstack depends only on chalk and commander, no workspace packages) -- confirmed rather than assumed, since a zero-match --filter exits 0 having run nothing; I verified the filter matches a real project separately. LOCK DISCIPLINE: heavy runs went through flock -E 99 -w on /tmp/os-heavy-verify.lock; one typecheck attempt returned 99 (queue timeout, NOT a failure) after 540s, so I named the holder (live bash/node pair, PIDs 26452/26453, plus three queued flocks -- busy, not stalled), spent the interval on lock-free gate runs, and re-acquired on the second retry.", "open_questions": [ { "question": "Ruling 3 said 'Ship a .changeset/*.md', but this PR publishes nothing AND .changeset/** sits inside the #9465 fence you drew. I used the skip-changeset label. Do you want a changeset anyway?", "options": [ "A -- keep skip-changeset (current state): the label is applied and read back; nothing is published by this PR, and no fenced path is touched", "B -- add a real .changeset/*.md naming create-objectstack: satisfies ruling 3 literally, but declares a release containing no user-visible change and writes into the fenced tree", "C -- add an empty-frontmatter changeset: NOT available, #5471 shut this route for newly added files and the gate rejects it" ], "recommendation": "A. The two rulings conflict only because this card turned out to be tooling-and-tests-only; ruling 3 reads as the standard 'the changeset is the release-notes input, not content/docs/releases/' clause rather than a judgement that this specific PR publishes something. A is also the only option that keeps me out of the fence. Trivial to switch to B if you disagree -- one file." }, { "question": "H4: does this work change #9348's right shape? Yes, materially, in a way worth re-scoping before you dispatch it.", "options": [ "#9348 as written: add a --self-test AND wire the script into CI, on the premise that it 'runs nowhere in CI'", "Re-scoped #9348: the CI half is now largely DONE as a side effect of this PR" ], "recommendation": "Re-scope before dispatch. The 'runs nowhere in CI' premise is now substantially false: packages/create-objectstack/src/template-version-stamps.test.ts executes the real script end-to-end (a full CLI run via execFileSync against a two-template fixture, asserting the actual rewrites), and turbo.json ALREADY declares scripts/sync-template-versions.mjs as an input to create-objectstack#test -- so CI's turbo ls --affected sharding re-runs it precisely when the stamper changes. What remains genuinely open for #9348 is the narrower question of whether a --self-test FLAG is still wanted now that the same coverage lives in a vitest file that CI already schedules. My read: the flag is now mostly redundant and #9348 could shrink to 'confirm the vitest coverage is sufficient and close', but that is your call, not mine -- I deliberately implemented none of its scope." } ], "out_of_scope_findings": [ "filed as #9651: check-cross-package-test-inputs.mjs's create-objectstack rationale still says sync-template-versions.mjs is 'named in a comment rather than read', which my test made untrue -- low severity and self-correcting (the gate reddens if anyone acts on the stale prose), labelled `finding`, unassigned, no pm:queue" ] }
Generated by Claude Code
Found while implementing #9518 (widening
cut-rc.yml's release file surface). Not fixed there — recording only.What was observed
scripts/sync-template-versions.mjsalready models its outputs well. It has aTEXT_STAMPStable over(file, key, pattern, replacement)whose own header says it istable-driven "so that adding a fourth declared version surface is a row", and a
findTemplateDirs()that walksTEMPLATE_ROOT— "Deliberately not a curated list."But none of it is reachable from outside:
TEXT_STAMPSnorTEMPLATE_ROOTnorfindTemplateDirsisexported;So a consumer that wants to know "which files does the version pass stamp?" cannot ask.
It has to restate the answer.
Why it cost something
cut-rc.yml's "Build the single version commit" step allowlists the paths a version passmay write. #9518 widened it, and the docs half was sourced from a declaration rather than
copied:
sync-docs-image-tags.mjsimportsSURFACESfromscripts/check-docs-image-tag.mjs,which #9064 made import-safe with exactly this kind of entry-point guard:
The template half could not follow the same route, so two paths are spelled literally in
the workflow:
Both hardcode the template name
blank.findTemplateDirs()exists precisely because thetemplate set is not meant to be a curated list — so the day a second template ships, the
walk picks it up, the version pass stamps it, and the workflow's literal pair does not
cover it. That is the same failure #9518 fixed, one template later: the assertion trips and
the cut refuses to push, with nothing red until someone attempts a cut.
That
objectstack.manifest.jsonwas missing from the workflow's allowlist at all is thefirst instance already realised — #9518's own card named only the three doc surfaces, and
the fourth path was found by measurement rather than by reading any list.
Shape of a fix
Export
TEXT_STAMPS(or a derivedstampedPaths()returning repo-relative paths acrossall template dirs) and add the entry-point guard, mirroring
check-docs-image-tag.mjs.cut-rc.ymlwould then read both halves of its allowlist from declarations instead of onehalf from a declaration and one from literals.
Related but distinct: #9348 records that this same script has no
--self-testand runsnowhere in CI. Different change to the same file; neither subsumes the other.
Generated by Claude Code