Skip to content

fix(create-objectstack): stamp the template manifest's specVersion in the version-time sync, on every template - #9349

Merged
os-zhuang merged 2 commits into
mainfrom
claude/issue-9264-template-specversion-sync
Aug 17, 2026
Merged

fix(create-objectstack): stamp the template manifest's specVersion in the version-time sync, on every template#9349
os-zhuang merged 2 commits into
mainfrom
claude/issue-9264-template-specversion-sync

Conversation

@claude

@claudeclaudeBot commented Aug 17, 2026

Copy link
Copy Markdown
Contributor

Fixes#9264

The one bundled template declared the platform it targets in two places that disagreed by eleven majors, and only one of them was ever synced.

filekeywasnow
objectstack.manifest.jsonspecVersion^6.0.0^17.0.0
objectstack.config.tsengines.protocol^17^17 (unchanged)

scripts/sync-template-versions.mjs re-stamped the config key and the template's @objectstack/* dependency ranges, and never opened the manifest at all. Its failure mode was loud for the keys it covered and mute for the key it did not, so a green sync-template-versions run was never evidence about specVersion.

Measured before editing

Re-verified on origin/main @ 4906c900f, not inherited from the card.

The blast radius is wider than the registry contract.create-objectstack copies the manifest into every scaffolded project — it rewrites name, displayName and namespace and drops description (src/index.ts), and has never touched specVersion. So every project scaffolded since v7 was stamped ^6.0.0 while installing @objectstack/spec@^17.0.0.

What reads specVersion — both halves checked.checkSpecVersionGap (packages/cli/src/utils/spec-version.ts) is reached from compile.ts:424, validate.ts:237 and doctor.ts:1989, but every call site passes config.manifest — the defineStack({ manifest: … }) object out of objectstack.config.ts, which carries no specVersion key. It never opens objectstack.manifest.json, so the stale value did not feed that advisory. The live consumers are the template-manifest contract (check:template-manifests parses every shipped manifest against TemplateManifestSchema) and the scaffold copy above. scripts/check-release-page-status.mjs also contains the identifier, but only as a local variable for packages/spec/package.json's version — unrelated.

Deletion was not available, as the dispatch ruled: specVersion is required by TemplateManifestSchema (packages/spec/src/cloud/template-manifest.zod.ts:84, z.string(), no .optional()). packages/spec is untouched.

The two keys are two facts

Worth stating because the card frames them as two protocol declarations. They are not:

  • engines.protocol is the ADR-0087 D1 runtime handshake range and carries the protocol major (^17).
  • specVersion is documented by its own schema as the "Compatible @objectstack/spec semver range" and carries the package range (^17.0.0) — the same value the script already writes into the template's own @objectstack/spec dependency.

They agree on the major today only because the spec package's major and the protocol major are kept in lockstep. They are stamped from two different values, and the stale ^6.0.0 was already in package-range shape.

The fix

Table-driven over (file, key, pattern, replacement), with discovery instead of hard-coded paths — one-key-one-file coverage is what let this sit:

  • templates are found by walking src/templates/, the same way check-template-manifests.ts finds the manifests it parses, plus its vacuous-green guard (zero templates fails rather than passing);
  • every stamp is required. A missing file, an absent stamp, or a package.json with no @objectstack/* dependency is a hard exit 1 naming the path — never a skip. All problems are collected so one run names every unstamped surface.

The manifest is rewritten as text, not parsed and re-serialized: it keeps scaffold.variables compact on one line, and JSON.stringify(…, null, 2) expands the file from 549 to 591 bytes, reformatting unrelated structure on every release. Measured, not assumed.

The log strings that said "blank template manifest" while operating on the config are corrected — that wording is part of how this stayed invisible, and the same conflation in this suite's own describe names is fixed alongside.

Verification

Reverse-verified in three legs, each with its direction predicted first. All at 451011b4e.

Leg A — restore ^6.0.0. Two new ratchets red, engines.protocol stays green (it reads a different file):

× blank > objectstack.manifest.json declares specVersion at the current @objectstack/spec range
→ blank declares specVersion "^6.0.0" but create-objectstack is v17: expected 6 to be 17
× blank > specVersion agrees with the @objectstack/spec dependency the template installs
→ blank declares specVersion "^6.0.0" but installs @objectstack/spec "^17.0.0" — one fact
written twice, and they disagree

The script then rewrites it back, exit 0 — the coverage that did not exist before.

Leg B — delete the key. The loud-absence contract the ruling asked for:

✗ sync-template-versions: 1 unstamped surface(s).
• packages/create-objectstack/src/templates/blank/objectstack.manifest.json has no
specVersion stamp to sync. It should declare specVersion = ^17.0.0.
script exit=1

Absence is tested before the replace, deliberately: "key missing" and "value already correct" both leave the string unchanged, and only one of them is acceptable.

Leg C — a second template. The ruling that the fix must cover future templates, proven rather than asserted. A throwaway second template directory with all three surfaces at v6, against both scripts:

pre-fix script: ✓ blank … ✓ blank … exit=0 ← second template untouched, fully green log
fixed script: probe/package.json @objectstack/spec: ^6.0.0 → ^17.0.0
✓ probe/objectstack.config.ts: engines.protocol → '^17'
✓ probe/objectstack.manifest.json: specVersion → '^17.0.0'
✓ 2 template(s) in lockstep exit=0

The pre-fix script reports success while missing every surface of the new template. The test half discovers it too — bundled template declared version surfaces > probe > … reds on the stale value. Fixture removed; the tree is byte-identical to the commit.

I also probed my own instrument: tsc genuinely covers this test file (an injected TS2322 was caught at the expected line), so the green typecheck is not vacuous.

Gates at 451011b4e — all run after the final commit:

CI jobgatesresult
ESLintcheck:cross-package-test-inputs, check:engine-double-contract, check:where-matcher, check:query-options-erasure, check:type-check-coverage, check:nul-bytes, check:changeset-gate-self-tests, check:objectui-changeset, pnpm lintpass
pr-automation / cut-rccheck-adr-0087-registration, check-changeset-no-major, check-empty-changesetpass
docs-drift-checkdocs-audit/check-affected-docspass
ci.ymlcheck-cross-package-test-inputspass
(spec)check:template-manifests — the gate that parses this manifestpass
unitcreate-objectstack test 65 passed, typecheck cleanpass

Scope note — two files outside the declared surface

The dispatch bounded this to the manifest, the sync script and sibling tests. Two further files moved, both forced by a gate rather than chosen, and flagged here rather than absorbed silently:

  • scripts/check-cross-package-test-inputs.mjs — the new ratchet block's comment names the sync script in backticks, and that gate's literal collector takes any quoted repo path without parsing prose. Settled the way the gate's own ledger settles the identical check-nul-bytes.mjs entry, quoting it: "declaring one rarely-touched file is cheaper than teaching the scanner to tell prose from code, or than rewording a comment to dodge a scanner." Here the coupling is real as well as cheap — that script stamps the surfaces these ratchets assert.
  • turbo.json — the same gate requires the declaration and the turbo hash to agree; it fails if only one moves.

Verified this is a genuine regression from my change and not pre-existing: the gate is green against origin/main's version of the test file and red against mine.

Related card, deliberately untouched

#9149 is not addressed here and remains open. It concerns relocating the scaffold-e2e unpublished-version fallback into the scaffolder; that workflow step re-stamps engines.protocol in objectstack.config.ts only. This change neither helps nor blocks it — specVersion is inert on that path (nothing in validate/build reads the manifest file), so the fallback needs no new branch today. It does add one line to that card's third open question: a fallback relocated into the scaffolder would want to cover specVersion alongside engines.protocol, since both are now stamped from the same release-time pass.

Out of scope, filed separately

The version-time rewriter still gets no CI of its own. It runs only in the root version script, on a changesets/action release PR that CI never triggers, so the stamping logic itself is never executed by any gate — only its committed output is, by the ratchets here. scripts/sync-docs-image-tags.mjs solved exactly this with a --self-test wired into lint.yml as check:docs-image-tag-sync. Filed as #9348 rather than ridden in, since it moves package.json and lint.yml.


Generated by Claude Code

…very template (#9264)
The blank template declared its target platform in two places that disagreed by
eleven majors: `specVersion: "^6.0.0"` in objectstack.manifest.json against
`engines.protocol: '^17'` in objectstack.config.ts. sync-template-versions.mjs
re-stamped the config key and the template's @objectstack/* dependency ranges,
and never opened the manifest — so a green sync run was never evidence about
specVersion.
The scaffolder copies the manifest into every scaffolded project without
touching specVersion, so the stale range shipped to real users, not just to the
template registry.
specVersion is REQUIRED by TemplateManifestSchema, so deletion was not
available. It is also a different fact from engines.protocol: the schema
documents it as the compatible @objectstack/spec range (^17.0.0), while
engines.protocol is the ADR-0087 D1 protocol major (^17). They are stamped from
two different values.
Structural half, since one-key-one-file coverage is what let this sit:
- templates are DISCOVERED by walking src/templates/, matching
check-template-manifests.ts, so a second template is covered when it lands
- every stamp is required — a missing file, absent stamp or dep-less
package.json fails loudly naming the path, never skips
- the manifest is rewritten as text, not re-serialized, so the compact
scaffold.variables formatting survives every release
CI half: four per-template ratchets in template-consistency.test.ts, including
the cross-file invariant that specVersion must equal the @objectstack/spec range
the template installs.
Co-Authored-By: Claude <noreply@anthropic.com>
…t input (#9264)
check:cross-package-test-inputs flags the script path named in the new ratchet
block's comment. Settled the way this gate's own ledger settles the identical
check-nul-bytes.mjs case: declare the file rather than reword prose to dodge a
source scanner.
The coupling is real as well as cheap — that script stamps the three
per-template version surfaces the ratchets assert, so a change to the stamper is
exactly what those ratchets exist to catch.
turbo.json's create-objectstack#test inputs move in the same commit; the gate
requires the declaration and the turbo hash to agree.
Co-Authored-By: Claude <noreply@anthropic.com>
@github-actionsgithub-actionsBot added documentation Improvements or additions to documentation tests tooling labels Aug 17, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

Nothing in this diff resolved to a documentable surface (no symbol, route or SDK anchor derived from 1 changed package(s)), so this run has no opinion about the docs.

What this run could not see
  • 1 changed file(s) yielded no anchor (packages/create-objectstack/src/templates/blank/objectstack.manifest.json) — pages documenting those are invisible to this run

Coarse fallback — 1 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json origin/mainpackageMentionDocs.

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

Labels

documentationImprovements or additions to documentationteststooling

Projects

None yet

2 participants

@os-zhuang@claude