Skip to content

Commit 135843d

Browse files
os-litantclaude
andauthored
fix(cli): os migrate meta names the protocol, not a package version (#16058)
The chain line ended `(runtime 17.0.0)`. That value is `PROTOCOL_VERSION` -- the protocol major padded to a semver -- and it never tracks the installed package version. Printed as a bare semver under the word "runtime", beside the real package versions of the same upgrade session, it read as "your runtime is 17.0.0": an apparent downgrade on a 17.3.0 install. The value was never wrong; the label and the semver form were. The line now states the fact in the protocol's own units -- `(this runtime implements protocol 17)` -- relabelled rather than dropped, because with `--to` stopping below this build's major it is the only place the operator learns where the runtime actually stands. The `--json` `runtime` key is deliberately left as published: a machine-readable key on a shipped payload owes a reader census and a deprecation window before it moves. A new e2e pin drives the real CLI over both halves and asserts that key's current value, so the contract move cannot happen silently. Claude-Session: https://claude.ai/code/session_01D47qPfEWVPmhguWgBZCi5N Co-authored-by: Claude <noreply@anthropic.com>
1 parent 9c3fda5 commit 135843d

3 files changed

Lines changed: 100 additions & 1 deletion

File tree

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
---
2+
"@objectstack/cli": patch
3+
---
4+
5+
`os migrate meta` no longer prints the protocol version under the word "runtime", where it read as the installed package version.
6+
7+
The chain line used to end `(runtime 17.0.0)`. That number is `PROTOCOL_VERSION` — the protocol major padded to a semver — and it is not, and never tracks, the version of the installed `@objectstack/cli` or `@objectstack/spec`. On a 17.3.0 install the line appeared beside the real package versions of the same upgrade session (`npm view`, the changelog), so it read as "your runtime is 17.0.0": an apparent downgrade or a stale install, neither of which was true.
8+
9+
The value was never wrong — the label and the semver form were. The line now states the fact in the protocol's own units:
10+
11+
```
12+
Chain: protocol 17 → 17 (this runtime implements protocol 17)
13+
```
14+
15+
The parenthetical is relabelled rather than dropped, because it carries a fact nothing else on screen does: when `--to` stops below this build's major, it is the only place the operator is told where the runtime actually stands (`Chain: protocol 16 → 16 (this runtime implements protocol 17)`).
16+
17+
The `--json` payload is deliberately untouched: its `runtime` key still carries the same padded protocol semver. Renaming a machine-readable key is a contract change owing a reader census and a deprecation window of its own, and it is tracked separately — an e2e pin now asserts the key's current value so that move cannot happen silently.

packages/cli/src/commands/migrate/meta.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -327,6 +327,11 @@ export default class MigrateMeta extends Command {
327327
await emitJson({
328328
from: result.fromMajor,
329329
to: result.toMajor,
330+
// Deliberately NOT relabelled alongside the human line below:
331+
// this is a machine-readable key on a published payload, so
332+
// moving it is a contract change owing a reader census and a
333+
// deprecation window of its own (#15585, option C). The value is
334+
// the protocol major padded to a semver, not a package version.
330335
runtime: PROTOCOL_VERSION,
331336
applied: result.applied,
332337
todos: result.todos,
@@ -350,7 +355,15 @@ export default class MigrateMeta extends Command {
350355
}
351356

352357
printInfo(`Config: ${chalk.white(absolutePath)}`);
353-
printInfo(`Chain: protocol ${fromMajor}${toMajor} (runtime ${PROTOCOL_VERSION})`);
358+
// State this build's protocol major in the protocol's own units.
359+
// `PROTOCOL_VERSION` is that major padded to a semver ('17.0.0'), never
360+
// the installed package version -- printed as a bare semver under the
361+
// word "runtime" it read as one, so on a 17.3.0 install the operator saw
362+
// an apparent downgrade next to the real package versions of the same
363+
// upgrade session. The fact itself is worth keeping: with `--to` below
364+
// this build's major it is the only line saying where the runtime
365+
// actually stands. So it is relabelled and de-padded, not dropped.
366+
printInfo(`Chain: protocol ${fromMajor}${toMajor} (this runtime implements protocol ${PROTOCOL_MAJOR})`);
354367
console.log('');
355368

356369
if (result.applied.length === 0 && result.todos.length === 0) {

packages/cli/test/migrate-meta.e2e.test.ts

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import { dirname, join, resolve } from 'node:path';
2323
import { createRequire } from 'node:module';
2424
import { fileURLToPath } from 'node:url';
2525
import { ObjectStackDefinitionSchema } from '@objectstack/spec';
26+
import { PROTOCOL_MAJOR, PROTOCOL_VERSION } from '@objectstack/spec/kernel';
2627
import { childEnv } from './helpers/serve-process.js';
2728

2829
const execFileP = promisify(execFile);
@@ -443,3 +444,71 @@ export default defineStack({
443444
expect(refused, '`os validate` must still reject a retired key').toBe(true);
444445
}, 180_000);
445446
});
447+
448+
/**
449+
* The chain line states this build's protocol in the protocol's own units.
450+
*
451+
* `PROTOCOL_VERSION` is the protocol major padded to a semver ('17.0.0') and is
452+
* never the installed package version. Printed here as a bare semver under the
453+
* word "runtime", it read as one: on a 17.3.0 install the operator saw
454+
* "runtime 17.0.0" beside the real package versions of the same upgrade
455+
* session, which reads as an apparent downgrade or a stale install. Nothing
456+
* about the VALUE was wrong; the label and the semver FORM were.
457+
*
458+
* Both halves are asserted, because either one alone is satisfiable the wrong
459+
* way. A line that merely stopped saying "runtime" could still print the padded
460+
* semver in a version position; and a line that dropped the parenthetical
461+
* altogether would lose the one fact it carries -- with `--to` stopping below
462+
* this build's major it is the only place the operator is told where the
463+
* runtime actually stands, which is why the third case drives exactly that.
464+
*
465+
* The `--json` `runtime` key is pinned UNCHANGED here on purpose. It is a
466+
* machine-readable key on a published payload, so moving it is a contract
467+
* change owing a reader census and a deprecation window of its own. This pin is
468+
* what makes that move loud instead of silent.
469+
*/
470+
describe('os migrate meta — the chain line names the protocol, not a package version', () => {
471+
const LABEL_CONFIG = `
472+
export default {
473+
manifest: { id: 'chain_label_e2e', name: 'Chain Label E2E', version: '1.0.0', type: 'app' },
474+
objects: [{ name: 'label_ticket', label: 'Ticket', fields: { title: { type: 'text', label: 'Title' } } }],
475+
};
476+
`;
477+
let labelDir: string;
478+
479+
beforeAll(() => {
480+
labelDir = mkdtempSync(join(tmpdir(), 'os-migrate-meta-label-'));
481+
writeFileSync(join(labelDir, 'objectstack.config.ts'), LABEL_CONFIG);
482+
});
483+
484+
afterAll(() => {
485+
try { rmSync(labelDir, { recursive: true, force: true }); } catch { /* ignore */ }
486+
});
487+
488+
it("names this build's protocol major, in majors", async () => {
489+
const stdout = await runMeta(['--from', String(PROTOCOL_MAJOR)], labelDir);
490+
expect(stdout).toContain(
491+
`Chain: protocol ${PROTOCOL_MAJOR}${PROTOCOL_MAJOR} (this runtime implements protocol ${PROTOCOL_MAJOR})`,
492+
);
493+
}, 120_000);
494+
495+
it('prints no padded protocol semver in the human output, under any label', async () => {
496+
const stdout = await runMeta(['--from', String(PROTOCOL_MAJOR)], labelDir);
497+
expect(stdout).not.toContain(PROTOCOL_VERSION);
498+
expect(stdout).not.toMatch(/runtime \d+\.\d+\.\d+/);
499+
}, 120_000);
500+
501+
it('still says where the runtime stands when --to stops below this build\'s major', async () => {
502+
const below = PROTOCOL_MAJOR - 1;
503+
const stdout = await runMeta(['--from', String(below), '--to', String(below)], labelDir);
504+
expect(stdout).toContain(
505+
`Chain: protocol ${below}${below} (this runtime implements protocol ${PROTOCOL_MAJOR})`,
506+
);
507+
expect(stdout).not.toContain(PROTOCOL_VERSION);
508+
}, 120_000);
509+
510+
it('leaves the --json `runtime` key exactly as published', async () => {
511+
const parsed = JSON.parse(await runMeta(['--from', String(PROTOCOL_MAJOR), '--json'], labelDir));
512+
expect(parsed.runtime).toBe(PROTOCOL_VERSION);
513+
}, 120_000);
514+
});

0 commit comments

Comments
 (0)