Skip to content

Commit 87ad73b

Browse files
feat(cli)!: rename the --json payload key specVersionGap to protocolVersionGap (#17240)
* feat(cli)!: rename the --json payload key specVersionGap to protocolVersionGap The advisory's axis moved to `manifest.engines.protocol` in #13860; the published key name lagged one release behind it. A key spelled `specVersion*` invites the inference that a writable `manifest.specVersion` exists, and because `ManifestSchema` is not `.strict()` and drops unknown keys with nothing said (#14192), acting on that inference yields a manifest that looks normal and whose line never took effect. One stroke, no alias, no dual-key window. Value shape unchanged. The three in-repo e2e suites that pinned the old key move with it; zero external consumers were measured. Claude-Session: https://claude.ai/code/session_015QE8qk46e5CHJxyQEUjbf8 Co-authored-by: Claude <noreply@anthropic.com> * chore(changeset): answer the ADR-0087 disposition question for the key rename `check-adr-0087-registration` requires a declared-breaking changeset to state its ledger disposition in writing. The renamed member is a CLI `--json` output key emitted from an inline object literal: no Zod schema, no `packages/spec` declaration, no stored representation, so `objectstack migrate meta` has nothing to reach. Claude-Session: https://claude.ai/code/session_015QE8qk46e5CHJxyQEUjbf8 Co-authored-by: Claude <noreply@anthropic.com> --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 5de9372 commit 87ad73b

6 files changed

Lines changed: 68 additions & 15 deletions

File tree

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
---
2+
"@objectstack/cli": minor
3+
---
4+
5+
<!-- adr-0087: not-required (no-migration-prescription) the renamed member is a CLI `--json` OUTPUT key emitted from an inline object literal — no Zod schema, no `packages/spec` declaration, no stored representation, so `objectstack migrate meta` has nothing to reach. The affected party is a script reading stdout (ADR-0087 D8). -->
6+
7+
feat(cli)!: the `--json` payload key `specVersionGap` is renamed to `protocolVersionGap` (#14261)
8+
9+
**BREAKING** — a published machine surface changes a key name. `os validate --json` and
10+
`os build --json` emit **`protocolVersionGap`** where they emitted `specVersionGap`. A
11+
consumer reading `specVersionGap` reads `undefined` after this release and must switch to
12+
the new name. There is **no alias and no dual-key transition window**: one axis, one name.
13+
14+
The value shape is unchanged — `null` when the app's declared compatibility range admits
15+
the installed `@objectstack/spec`, otherwise the same advisory record with the same
16+
members. Nothing else on either payload moves: no other key is added, removed or
17+
reshaped, and the text faces of both commands are byte-identical.
18+
19+
## Why the name had to move
20+
21+
The axis this advisory reports moved in **#13860**: it used to read the undeclared
22+
`manifest.specVersion` and now reads `manifest.engines.protocol`, which is declared
23+
(`PluginEnginesSchema`), stamped by every scaffold, and enforced at boot. The published
24+
key name stayed behind for one release, deliberately — renaming a machine face with
25+
pinned consumers is a break, and no ruling covered it at the time.
26+
27+
Leaving it is a correctness problem, not untidiness. A key spelled `specVersion*` invites
28+
the reader — an AI agent above all — to infer that a writable `manifest.specVersion`
29+
exists. `ManifestSchema` is not `.strict()` and **silently drops unknown keys** (#14192),
30+
so acting on that inference does not produce an error: it produces a manifest that looks
31+
entirely normal and whose `specVersion` line never took effect. That is the same
32+
ghost-key breadcrumb mechanism that caused #13860 in the first place, left standing on
33+
the output side.
34+
35+
## What a consumer should do
36+
37+
```diff
38+
- if (payload.specVersionGap) { … }
39+
+ if (payload.protocolVersionGap) { … }
40+
```
41+
42+
The breaking surface was measured before the rename and is closed inside this repository:
43+
the only consumers of the old key were three in-repo e2e suites, which move in this same
44+
change; **zero external consumers were found**. Graded `minor` by the maintainer's
45+
explicit grading of 2026-09-02; the banner above carries the breaking-ness the level
46+
cannot.

packages/cli/src/commands/compile.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -908,9 +908,11 @@ export default class Compile extends Command {
908908
// Same key `os validate --json` uses, so a CI consumer reads one shape
909909
// from either command rather than learning two.
910910
conversions: conversionNotices,
911-
// Published key name kept; the axis behind it moved to
912-
// `manifest.engines.protocol` (#13860). See validate.ts.
913-
specVersionGap: protocolGap,
911+
// [#14261] Renamed from the retired `specVersion*` spelling: one
912+
// axis, one name. The axis itself moved to
913+
// `manifest.engines.protocol` in #13860; the published key name
914+
// follows it here. Value shape unchanged. See validate.ts.
915+
protocolVersionGap: protocolGap,
914916
stats,
915917
duration: timer.elapsed(),
916918
}, 0, { compact: true });

packages/cli/src/commands/validate.ts

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -541,12 +541,17 @@ export default class Validate extends Command {
541541
// this one.
542542
warnings: warningsSoFar(),
543543
conversions: conversionNotices,
544-
// The payload key keeps its published name. The AXIS it reports
544+
// [#14261] The key now spells the axis it reports. That axis
545545
// moved from the undeclared `manifest.specVersion` to
546-
// `manifest.engines.protocol` (#13860), but this is a machine face
547-
// with pinned consumers, and renaming it is a break nobody asked
548-
// for. Its value shape is unchanged.
549-
specVersionGap: protocolGap,
546+
// `manifest.engines.protocol` in #13860, and the published key
547+
// name lagged one release behind it. A key spelled `specVersion*`
548+
// invites the inference that `manifest.specVersion` is writable;
549+
// `ManifestSchema` is not `.strict()` and drops unknown keys with
550+
// nothing said (#14192), so acting on that inference produces a
551+
// manifest that looks fine and whose line never took effect. The
552+
// rename is one stroke, no alias, no dual-key window; its value
553+
// shape is unchanged.
554+
protocolVersionGap: protocolGap,
550555
duration: timer.elapsed(),
551556
},
552557
// `--strict` means one thing — "treat warnings as errors" — and it now
@@ -556,8 +561,8 @@ export default class Validate extends Command {
556561
// its `⚠` block while the payload carries them under `conversions`.
557562
// Gating on the payload field would have left `--json --strict` at 0
558563
// for a config whose only advisories are conversion notices — the
559-
// same divergence one collection narrower. `specVersionGap` stays out
560-
// on both faces; it is never gated by `--strict` (see below).
564+
// same divergence one collection narrower. `protocolVersionGap` stays
565+
// out on both faces; it is never gated by `--strict` (see below).
561566
//
562567
// `valid: true` beside a 1 is not a contradiction, it is the text
563568
// face verbatim: that path prints "Validation passed" and THEN fails

packages/cli/test/build-json-advisory-parity.e2e.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -310,7 +310,7 @@ describe('#11727 — `os build --json` carries the capability-provider and packa
310310
'runtimeModule',
311311
'runtimeModuleSize',
312312
'size',
313-
'specVersionGap',
313+
'protocolVersionGap',
314314
'stats',
315315
'success',
316316
'warnings',

packages/cli/test/build-json-undeclared-key-parity.e2e.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -256,7 +256,7 @@ describe('#11643 — `os build --json` carries the undeclared-authoring-key warn
256256
'runtimeModule',
257257
'runtimeModuleSize',
258258
'size',
259-
'specVersionGap',
259+
'protocolVersionGap',
260260
'stats',
261261
'success',
262262
'warnings',

packages/cli/test/validate-json-warning-parity.e2e.test.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@
3535
*
3636
* The text face folds two more advisory streams into the same `⚠` block that
3737
* the JSON payload carries as its own top-level fields instead — `conversions`
38-
* (ADR-0087 D2 load-time conversion notices) and `specVersionGap`. Those are a
38+
* (ADR-0087 D2 load-time conversion notices) and `protocolVersionGap`. Those are a
3939
* declared difference in SHAPE, not a drop: the information is reachable on both
4040
* faces. Rather than silently ignoring them, every fixture ASSERTS both are
4141
* empty, so the exact set equality below is honest about its scope — and if a
@@ -242,12 +242,12 @@ describe('#10953 — text and --json carry the same warning set', () => {
242242
const payload = JSON.parse(json.stdout) as {
243243
warnings?: unknown;
244244
conversions?: unknown;
245-
specVersionGap?: unknown;
245+
protocolVersionGap?: unknown;
246246
};
247247

248248
// Scope declaration, asserted rather than assumed — see the header.
249249
expect(payload.conversions, 'fixture must raise no conversion notices').toEqual([]);
250-
expect(payload.specVersionGap, 'fixture must raise no spec-version gap').toBeNull();
250+
expect(payload.protocolVersionGap, 'fixture must raise no protocol-version gap').toBeNull();
251251

252252
const lines = textWarnings(text.stdout);
253253
const messages = jsonWarnings(payload);

0 commit comments

Comments
 (0)