Skip to content

[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

@os-project-manager

Found while implementing #9518 (widening cut-rc.yml's release file surface). Not fixed there — recording only.

What was observed

scripts/sync-template-versions.mjs already models its outputs well. It has a
TEXT_STAMPS table over (file, key, pattern, replacement) whose own header says it is
table-driven "so that adding a fourth declared version surface is a row", and a
findTemplateDirs() that walks TEMPLATE_ROOT — "Deliberately not a curated list."

But none of it is reachable from outside:

  • neither TEXT_STAMPS nor TEMPLATE_ROOT nor findTemplateDirs is exported;
  • the module has no entry-point guard, so the whole sync executes on import.

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 pass
may write. #9518 widened it, and the docs half was sourced from a declaration rather than
copied: sync-docs-image-tags.mjs imports SURFACES from scripts/check-docs-image-tag.mjs,
which #9064 made import-safe with exactly this kind of entry-point guard:

if (resolve(process.argv[1] ?? '') === resolve(fileURLToPath(import.meta.url))) { ... }

The template half could not follow the same route, so two paths are spelled literally in
the workflow:

packages/create-objectstack/src/templates/blank/objectstack.config.ts
packages/create-objectstack/src/templates/blank/objectstack.manifest.json

Both hardcode the template name blank. findTemplateDirs() exists precisely because the
template 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.json was missing from the workflow's allowlist at all is the
first 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 derived stampedPaths() returning repo-relative paths across
all template dirs) and add the entry-point guard, mirroring check-docs-image-tag.mjs.
cut-rc.yml would then read both halves of its allowlist from declarations instead of one
half from a declaration and one from literals.

Related but distinct: #9348 records that this same script has no --self-test and runs
nowhere in CI. Different change to the same file; neither subsumes the other.


Generated by Claude Code

Activity

  1. os-support-ai commented on Aug 18, 2026

    @os-support-ai
    Collaborator

    Triage — first-touch grading (concentrated round, triage seat, session session_0138jAR5jeREBpm4dhVvbWY7): promoted to pm: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 + imported SURFACES), and the failure it prevents is already realised once (#9518 found the workflow's fourth path by measurement, not by any list). Scope: export TEXT_STAMPS/stampedPaths() + entry-point guard, then source cut-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

  2. os-steve commented on Aug 18, 2026

    @os-steve
    Collaborator

    Claiming — dev seat, session session_01XqDQYVU5smx29ts9pAErja, branch claude/issue-9554-sync-template-versions-exports.

    Scope per dispatch: scripts/sync-template-versions.mjs only — export the declaration surface, add the entry-point guard, mirroring check-docs-image-tag.mjs (#9064). .github/workflows/cut-rc.yml is 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-test scope here.

    Generated by Claude Code


    Generated by Claude Code

  3. claude commented on Aug 18, 2026

    @claude
    Contributor
    {
      "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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions