Uh oh!
There was an error while loading. Please reload this page.
fix(create-objectstack): stamp the template manifest's specVersion in the version-time sync, on every template - #9349
Merged
Conversation
…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>
Contributor
📓 Docs Drift CheckNothing 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
Coarse fallback — 1 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): |
os-zhuang
marked this pull request as ready for review
August 17, 2026 16:09
This was referenced Aug 17, 2026
Uh oh!
There was an error while loading. Please reload this page.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for freeto join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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.
objectstack.manifest.jsonspecVersion^6.0.0^17.0.0objectstack.config.tsengines.protocol^17^17(unchanged)scripts/sync-template-versions.mjsre-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 greensync-template-versionsrun was never evidence aboutspecVersion.Measured before editing
Re-verified on
origin/main@4906c900f, not inherited from the card.The blast radius is wider than the registry contract.
create-objectstackcopies the manifest into every scaffolded project — it rewritesname,displayNameandnamespaceand dropsdescription(src/index.ts), and has never touchedspecVersion. So every project scaffolded since v7 was stamped^6.0.0while installing@objectstack/spec@^17.0.0.What reads
specVersion— both halves checked.checkSpecVersionGap(packages/cli/src/utils/spec-version.ts) is reached fromcompile.ts:424,validate.ts:237anddoctor.ts:1989, but every call site passesconfig.manifest— thedefineStack({ manifest: … })object out ofobjectstack.config.ts, which carries nospecVersionkey. It never opensobjectstack.manifest.json, so the stale value did not feed that advisory. The live consumers are the template-manifest contract (check:template-manifestsparses every shipped manifest againstTemplateManifestSchema) and the scaffold copy above.scripts/check-release-page-status.mjsalso contains the identifier, but only as a local variable forpackages/spec/package.json's version — unrelated.Deletion was not available, as the dispatch ruled:
specVersionis required byTemplateManifestSchema(packages/spec/src/cloud/template-manifest.zod.ts:84,z.string(), no.optional()).packages/specis untouched.The two keys are two facts
Worth stating because the card frames them as two protocol declarations. They are not:
engines.protocolis the ADR-0087 D1 runtime handshake range and carries the protocol major (^17).specVersionis documented by its own schema as the "Compatible@objectstack/specsemver range" and carries the package range (^17.0.0) — the same value the script already writes into the template's own@objectstack/specdependency.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.0was 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:
src/templates/, the same waycheck-template-manifests.tsfinds the manifests it parses, plus its vacuous-green guard (zero templates fails rather than passing);package.jsonwith 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.variablescompact on one line, andJSON.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.protocolstays green (it reads a different file):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:
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:
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:
tscgenuinely covers this test file (an injectedTS2322was caught at the expected line), so the green typecheck is not vacuous.Gates at
451011b4e— all run after the final commit:check: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 lintcheck-adr-0087-registration,check-changeset-no-major,check-empty-changesetdocs-audit/check-affected-docscheck-cross-package-test-inputscheck:template-manifests— the gate that parses this manifestcreate-objectstack test65 passed,typecheckcleanScope 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 identicalcheck-nul-bytes.mjsentry, 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.protocolinobjectstack.config.tsonly. This change neither helps nor blocks it —specVersionis 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 coverspecVersionalongsideengines.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
versionscript, 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.mjssolved exactly this with a--self-testwired intolint.ymlascheck:docs-image-tag-sync. Filed as #9348 rather than ridden in, since it movespackage.jsonandlint.yml.Generated by Claude Code