Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 46 additions & 0 deletions .changeset/protocol-version-gap-key-rename.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
---
"@objectstack/cli": minor
---

<!-- 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). -->

feat(cli)!: the `--json` payload key `specVersionGap` is renamed to `protocolVersionGap` (#14261)

**BREAKING** — a published machine surface changes a key name. `os validate --json` and
`os build --json` emit **`protocolVersionGap`** where they emitted `specVersionGap`. A
consumer reading `specVersionGap` reads `undefined` after this release and must switch to
the new name. There is **no alias and no dual-key transition window**: one axis, one name.

The value shape is unchanged — `null` when the app's declared compatibility range admits
the installed `@objectstack/spec`, otherwise the same advisory record with the same
members. Nothing else on either payload moves: no other key is added, removed or
reshaped, and the text faces of both commands are byte-identical.

## Why the name had to move

The axis this advisory reports moved in **#13860**: it used to read the undeclared
`manifest.specVersion` and now reads `manifest.engines.protocol`, which is declared
(`PluginEnginesSchema`), stamped by every scaffold, and enforced at boot. The published
key name stayed behind for one release, deliberately — renaming a machine face with
pinned consumers is a break, and no ruling covered it at the time.

Leaving it is a correctness problem, not untidiness. A key spelled `specVersion*` invites
the reader — an AI agent above all — to infer that a writable `manifest.specVersion`
exists. `ManifestSchema` is not `.strict()` and **silently drops unknown keys** (#14192),
so acting on that inference does not produce an error: it produces a manifest that looks
entirely normal and whose `specVersion` line never took effect. That is the same
ghost-key breadcrumb mechanism that caused #13860 in the first place, left standing on
the output side.

## What a consumer should do

```diff
- if (payload.specVersionGap) { … }
+ if (payload.protocolVersionGap) { … }
```

The breaking surface was measured before the rename and is closed inside this repository:
the only consumers of the old key were three in-repo e2e suites, which move in this same
change; **zero external consumers were found**. Graded `minor` by the maintainer's
explicit grading of 2026-09-02; the banner above carries the breaking-ness the level
cannot.
8 changes: 5 additions & 3 deletions packages/cli/src/commands/compile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -908,9 +908,11 @@ export default class Compile extends Command {
// Same key `os validate --json` uses, so a CI consumer reads one shape
// from either command rather than learning two.
conversions: conversionNotices,
// Published key name kept; the axis behind it moved to
// `manifest.engines.protocol` (#13860). See validate.ts.
specVersionGap: protocolGap,
// [#14261] Renamed from the retired `specVersion*` spelling: one
// axis, one name. The axis itself moved to
// `manifest.engines.protocol` in #13860; the published key name
// follows it here. Value shape unchanged. See validate.ts.
protocolVersionGap: protocolGap,
stats,
duration: timer.elapsed(),
}, 0, { compact: true });
Expand Down
19 changes: 12 additions & 7 deletions packages/cli/src/commands/validate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -541,12 +541,17 @@ export default class Validate extends Command {
// this one.
warnings: warningsSoFar(),
conversions: conversionNotices,
// The payload key keeps its published name. The AXIS it reports
// [#14261] The key now spells the axis it reports. That axis
// moved from the undeclared `manifest.specVersion` to
// `manifest.engines.protocol` (#13860), but this is a machine face
// with pinned consumers, and renaming it is a break nobody asked
// for. Its value shape is unchanged.
specVersionGap: protocolGap,
// `manifest.engines.protocol` in #13860, and the published key
// name lagged one release behind it. A key spelled `specVersion*`
// invites the inference that `manifest.specVersion` is writable;
// `ManifestSchema` is not `.strict()` and drops unknown keys with
// nothing said (#14192), so acting on that inference produces a
// manifest that looks fine and whose line never took effect. The
// rename is one stroke, no alias, no dual-key window; its value
// shape is unchanged.
protocolVersionGap: protocolGap,
duration: timer.elapsed(),
},
// `--strict` means one thing — "treat warnings as errors" — and it now
Expand All @@ -556,8 +561,8 @@ export default class Validate extends Command {
// its `⚠` block while the payload carries them under `conversions`.
// Gating on the payload field would have left `--json --strict` at 0
// for a config whose only advisories are conversion notices — the
// same divergence one collection narrower. `specVersionGap` stays out
// on both faces; it is never gated by `--strict` (see below).
// same divergence one collection narrower. `protocolVersionGap` stays
// out on both faces; it is never gated by `--strict` (see below).
//
// `valid: true` beside a 1 is not a contradiction, it is the text
// face verbatim: that path prints "Validation passed" and THEN fails
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/test/build-json-advisory-parity.e2e.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -310,7 +310,7 @@ describe('#11727 — `os build --json` carries the capability-provider and packa
'runtimeModule',
'runtimeModuleSize',
'size',
'specVersionGap',
'protocolVersionGap',
'stats',
'success',
'warnings',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -256,7 +256,7 @@ describe('#11643 — `os build --json` carries the undeclared-authoring-key warn
'runtimeModule',
'runtimeModuleSize',
'size',
'specVersionGap',
'protocolVersionGap',
'stats',
'success',
'warnings',
Expand Down
6 changes: 3 additions & 3 deletions packages/cli/test/validate-json-warning-parity.e2e.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@
*
* The text face folds two more advisory streams into the same `⚠` block that
* the JSON payload carries as its own top-level fields instead — `conversions`
* (ADR-0087 D2 load-time conversion notices) and `specVersionGap`. Those are a
* (ADR-0087 D2 load-time conversion notices) and `protocolVersionGap`. Those are a
* declared difference in SHAPE, not a drop: the information is reachable on both
* faces. Rather than silently ignoring them, every fixture ASSERTS both are
* empty, so the exact set equality below is honest about its scope — and if a
Expand Down Expand Up @@ -242,12 +242,12 @@ describe('#10953 — text and --json carry the same warning set', () => {
const payload = JSON.parse(json.stdout) as {
warnings?: unknown;
conversions?: unknown;
specVersionGap?: unknown;
protocolVersionGap?: unknown;
};

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

const lines = textWarnings(text.stdout);
const messages = jsonWarnings(payload);
Expand Down
Loading