Skip to content

Commit bff640d

Browse files
committed
fix(create-objectstack): stamp the template's engines.protocol from PROTOCOL_VERSION's major
sync-template-versions.mjs stamped engines.protocol from create-objectstack's own package major, though its header says the key carries the PROTOCOL major. In Changesets pre mode the protocol moves ahead of the version pass (packages 17.7.0 beside PROTOCOL_VERSION 18.0.0), so the blank template kept '^17' and a project scaffolded from main was refused at the protocol handshake on boot. - The stamp now reads the protocol major from PROTOCOL_VERSION's declaration (source text, the expression check-changeset-no-major.mjs reads it with); specVersion and the @objectstack/* ranges keep the package range. An unreadable declaration is a hard failure, never a fall-back. - The blank template is re-stamped by the script: '^18'. - template-consistency.test.ts judges engines.protocol against PROTOCOL_MAJOR (spec source, via the existing vitest alias), not the package major. - The self-test's fixtures carry a package major (42) and a protocol major (43) that differ; Control I and Control J pin the two sources and the hard failure. template-version-stamps.test.ts's fixture gains the same split. Claude-Session: https://claude.ai/code/session_01BmsuLyUeuG5CNpZFMH1jzS Co-authored-by: Claude <noreply@anthropic.com>
1 parent 04b8218 commit bff640d

5 files changed

Lines changed: 279 additions & 36 deletions

File tree

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
---
2+
"create-objectstack": patch
3+
---
4+
5+
fix(create-objectstack): the blank template's `engines.protocol` is stamped from the protocol major, not the package major
6+
7+
Clause-②: no
8+
9+
`engines.protocol` in the scaffolded `objectstack.config.ts` is the range the runtime's protocol handshake checks before it loads the project.
10+
11+
- The release stamp (`scripts/sync-template-versions.mjs`) used to write it from create-objectstack's own major. It now writes the major of `PROTOCOL_VERSION`, the value the handshake compares against. The two differ while the protocol moves ahead of a release: in that window the unreleased template declared `'^17'` beside a protocol-18 runtime, which refused to load the project it scaffolded.
12+
- The template now declares `'^18'`, written by that stamp rather than by hand.
13+
- Unchanged: `specVersion` and the `@objectstack/*` dependency ranges still carry the package range.
14+
- No released version shipped the mismatch. A project you scaffolded earlier keeps its own range; to move it to protocol 18, run `objectstack migrate meta --from 17 --write`, which rewrites `engines.protocol` with the rest of the step.

‎packages/create-objectstack/src/template-consistency.test.ts‎

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import { copyDir, TEMPLATE_FILE_ALIASES } from './template-copy.js';
1616
import { TEMPLATES } from './template-registry.js';
1717
import { gitFreeEnv } from '../../../scripts/git-env.mjs';
1818
import { SKILLS_CATALOG, SKILLS_INSTALL_COMMAND } from './skills-install.js';
19+
import { PROTOCOL_MAJOR, PROTOCOL_VERSION } from '@objectstack/spec/kernel';
1920

2021
const pkgRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
2122
const repoRoot = path.resolve(pkgRoot, '..', '..');
@@ -191,7 +192,17 @@ describe('bundled template declared version surfaces', () => {
191192
// `objectstack.manifest.json` — the two were conflated in this suite's own
192193
// naming and in the sync script's log strings, and that conflation is part
193194
// of how the sibling key below went unwatched for eleven majors.
194-
it("objectstack.config.ts stamps engines.protocol at the scaffolder's major (ADR-0087 D1)", () => {
195+
//
196+
// NOTE the value: the PROTOCOL major, judged against `PROTOCOL_VERSION` —
197+
// the constant the runtime's ADR-0087 D1 handshake compares a scaffolded
198+
// project's range to — and NOT against this package's own major. The two
199+
// differ for the whole pre-mode window in which the protocol moves ahead of
200+
// the version pass (packages at 17.x beside protocol 18): judged against the
201+
// package major, a template the runtime refuses at boot stayed green here.
202+
// `@objectstack/spec/kernel` resolves to the spec's SOURCE through this
203+
// package's vitest alias, so the verdict needs no build — it runs on the
204+
// release path's post-version tree, where nothing has been built.
205+
it('objectstack.config.ts stamps an engines.protocol that PROTOCOL_VERSION satisfies (ADR-0087 D1)', () => {
195206
const config = readTemplateFile('objectstack.config.ts');
196207
const match = /engines:\s*\{\s*protocol:\s*'\^(\d+)'\s*\}/.exec(config);
197208
expect(
@@ -200,9 +211,10 @@ describe('bundled template declared version surfaces', () => {
200211
).not.toBeNull();
201212
expect(
202213
Number(match![1]),
203-
`${template} stamps engines.protocol '^${match![1]}' but create-objectstack is v${ownMajor} — ` +
204-
'scripts/sync-template-versions.mjs re-stamps this at version time; keep them in lockstep',
205-
).toBe(ownMajor);
214+
`${template} stamps engines.protocol '^${match![1]}', which PROTOCOL_VERSION ${PROTOCOL_VERSION} does not ` +
215+
'satisfy: every project it scaffolds is refused at the protocol handshake by the runtime it ships ' +
216+
'with. Run `node scripts/sync-template-versions.mjs` — it stamps this key from PROTOCOL_VERSION\'s major.',
217+
).toBe(PROTOCOL_MAJOR);
206218
});
207219

208220
// The key #9264 is about. Required by TemplateManifestSchema, read by the

‎packages/create-objectstack/src/template-version-stamps.test.ts‎

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,10 @@
2525
// re-found the day a second template ships.
2626
//
2727
// The same fixture is deliberately built STALE (pinning ^17 while its
28-
// scaffolder reads 42.0.0). That makes the import-safety assertion non-vacuous
28+
// scaffolder reads 42.0.0 and its PROTOCOL_VERSION reads 43.0.0 — two majors,
29+
// because `engines.protocol` is stamped from the protocol constant and every
30+
// other surface from the package, and with the two equal a stamp read from the
31+
// wrong source would pass). That makes the import-safety assertion non-vacuous
2932
// in the one way that matters: an unguarded module imported against a stale
3033
// tree REWRITES it, so "the files are byte-identical after import" is evidence
3134
// only when there was something for a rewrite to do. Byte-identity over an
@@ -62,6 +65,7 @@ type SyncModule = {
6265
findTemplateDirs: (templateRoot?: string) => string[];
6366
stampedPaths: (options?: { root?: string }) => string[];
6467
loadScaffolderVersion: (file?: string) => { version: string; major: string; range: string };
68+
loadProtocolMajor: (file?: string) => { source: string; protocolMajor: string };
6569
};
6670

6771
const loadSync = async (file = SYNC_SCRIPT): Promise<SyncModule> =>
@@ -75,6 +79,11 @@ let fixtureScript: string;
7579

7680
/** Deliberately not the live version, so a stamp that ran is unmistakable. */
7781
const FIXTURE_VERSION = '42.0.0';
82+
/**
83+
* The fixture's PROTOCOL major — one ahead of its package major, the shape the
84+
* pre-mode window leaves in the real tree. `engines.protocol` must move to THIS.
85+
*/
86+
const FIXTURE_PROTOCOL_MAJOR = '43';
7887
/** Deliberately stale: every fixture surface pins this and must move to 42. */
7988
const STALE_MAJOR = '17';
8089

@@ -140,6 +149,11 @@ beforeAll(() => {
140149
path.join(fixture, 'packages', 'create-objectstack', 'package.json'),
141150
JSON.stringify({ name: 'create-objectstack', version: FIXTURE_VERSION }, null, 2) + '\n',
142151
);
152+
// The protocol major's one source, in the shape the real constant spells it.
153+
writeFixtureFile(
154+
path.join(fixture, 'packages', 'spec', 'src', 'kernel', 'protocol-version.ts'),
155+
`export const PROTOCOL_VERSION = '${FIXTURE_PROTOCOL_MAJOR}.0.0';\n`,
156+
);
143157

144158
for (const template of FIXTURE_TEMPLATES) {
145159
const dir = fixtureTemplateDir(template);
@@ -215,6 +229,7 @@ describe('sync-template-versions.mjs is import-safe', () => {
215229
expect(typeof sync.stampedPaths).toBe('function');
216230
expect(typeof sync.findTemplateDirs).toBe('function');
217231
expect(typeof sync.loadScaffolderVersion).toBe('function');
232+
expect(typeof sync.loadProtocolMajor).toBe('function');
218233
expect(Array.isArray(sync.TEXT_STAMPS)).toBe(true);
219234
expect(sync.TEXT_STAMPS.length).toBeGreaterThan(0);
220235
expect(sync.TEMPLATE_DIR).toBe('packages/create-objectstack/src/templates');
@@ -249,9 +264,10 @@ describe('the entry-point guard leaves the CLI path working', () => {
249264
'a non-@objectstack dependency is never touched',
250265
).toBe('^6.0.0');
251266

252-
expect(fs.readFileSync(path.join(dir, 'objectstack.config.ts'), 'utf8')).toContain(
253-
"engines: { protocol: '^42' }",
254-
);
267+
expect(
268+
fs.readFileSync(path.join(dir, 'objectstack.config.ts'), 'utf8'),
269+
`${template}/objectstack.config.ts engines.protocol moves to the PROTOCOL major, not the package major`,
270+
).toContain(`engines: { protocol: '^${FIXTURE_PROTOCOL_MAJOR}' }`);
255271

256272
const manifest = fs.readFileSync(path.join(dir, 'objectstack.manifest.json'), 'utf8');
257273
expect(manifest).toContain('"specVersion": "^42.0.0"');

‎packages/create-objectstack/src/templates/blank/objectstack.config.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ export default defineStack({
3636
// match the ObjectStack version you installed — change it when you
3737
// deliberately move to a new protocol major, not to silence a mismatch.
3838
// Guide: https://objectstack.ai/docs/upgrading
39-
engines: { protocol: '^17' },
39+
engines: { protocol: '^18' },
4040
},
4141

4242
// `automation` backs flow execution and materializes any declarative

0 commit comments

Comments
 (0)