|
1 | 1 | # ADR-0087: Metadata protocol upgrades for AI consumers — conversion over notification, executable migrations, machine-verifiable upgrades |
2 | 2 |
|
3 | | -**Status**: Accepted (2026-07-04, #2582) · trued up to as-built 2026-07-15 (see Addendum) · **Amended** (2026-09-30, #20390 ruling A — the artifact-ingestion window decides per entry by each retired conversion's `retiredAfter`; see the amendment note under the 2026-09-13 addendum's window bullet) · **Amended** (2026-10-09, #22449 ruling B′ — the two D4 projections, the per-major section of `spec-changes.json` and the protocol upgrade guide, are generated at publish rather than committed; see the amendment note under D4, which says what is generated where and where each part landed, and the struck clauses in the P2 phase-status bullet) |
| 3 | +**Status**: Accepted (2026-07-04, #2582) · trued up to as-built 2026-07-15 (see Addendum) · **Amended** (2026-09-30, #20390 ruling A — the artifact-ingestion window decides per entry by each retired conversion's `retiredAfter`; see the amendment note under the 2026-09-13 addendum's window bullet) · **Amended** (2026-10-09, #22449 ruling B′ — the two D4 projections, the per-major section of `spec-changes.json` and the protocol upgrade guide, are generated at publish rather than committed; see the amendment note under D4, which says what is generated where and where each part landed, and the struck clauses in the P2 phase-status bullet) · **Amended** (2026-10-13, #23038 — the artifact-ingestion window's third disjunct, landed as `58f5c90999`: a floor that is the first release of a major replays that major's conversion step, verdict `'converted-major-step'`; see the 2026-10-13 amendment note under the 2026-09-13 addendum's window bullet) |
4 | 4 | **Deciders**: ObjectStack Protocol Architects |
5 | 5 | **Builds on**: [ADR-0059](./0059-third-party-backward-compatibility-gates.md) (layered backward-compat gates — this ADR is its consumer-facing sequel), [ADR-0078](./0078-no-silently-inert-metadata.md) (no declarable-but-unenforced metadata — the un-checked `engines.protocol` is exactly this class), [ADR-0025](./0025-plugin-package-distribution.md) (§3.2 `engines.protocol` / `engines.platform` compatibility ranges, §3.10 #3 protocol-first check order), [ADR-0033](./0033-ai-assisted-metadata-authoring.md) (the authoring population this ADR designs for), [ADR-0049](./0049-no-unenforced-security-properties.md) (enforce-or-remove), [ADR-0054](./0054-runtime-proof-for-authorable-surface.md) (prove-it-runs), AGENTS.md Prime Directive #12 (contract-first, no consumer-side dialect fallbacks — §"Why the conversion layer does not violate PD #12" draws the line) |
6 | 6 | **Consumers**: `@objectstack/spec` (protocol version constant, conversion layer, deprecation/change registries), `@objectstack/cli` (`validate`, `doctor`, `migrate meta`), the runtime metadata loader (handshake + conversion), `@objectstack/mcp` (the AI-native change/migration surface), `@objectstack/create-objectstack`, the Release workflow, and every third-party consumer — whose maintainer is assumed to be an **AI agent** |
@@ -1167,6 +1167,39 @@ unconditional strip would start deleting legal metadata the day the keys return. |
1167 | 1167 | > refused it outright. The module docblock of |
1168 | 1168 | > `packages/metadata-core/src/artifact-forward-conversion.ts` states the rule |
1169 | 1169 | > and is its authority. |
| 1170 | +
|
| 1171 | + > **Amended (2026-10-13) — a first-release-of-a-major floor replays that |
| 1172 | + > major's step.** Provenance: #23001, landed as `58f5c90999` (PR #23023), |
| 1173 | + > recorded here by #23038. The window has a THIRD disjunct: the door replays |
| 1174 | + > an entry E when `floor < runtime` OR `floor <= E.retiredAfter` OR |
| 1175 | + > (`floor` is `M.0.0` AND `E.toMajor` is M). A floor that is the first |
| 1176 | + > release of major M, at or above the runtime label, replays every retired |
| 1177 | + > entry of the conversion step into M (`CONVERSIONS_BY_MAJOR[M]`, the step |
| 1178 | + > `objectstack migrate meta --from M-1` replays), with the default flips |
| 1179 | + > still refused by id first, under the verdict `'converted-major-step'` |
| 1180 | + > (`packages/metadata-core/src/artifact-forward-conversion.ts#idsTheFloorPostdates`). |
| 1181 | + > Why: `M.0.0` is what a bare `^M` range reads as and the floor every build |
| 1182 | + > from M's prerelease declares, while the package label still names the |
| 1183 | + > last release of M-1 and every retirement landing in the prerelease is |
| 1184 | + > stamped at or below that label; so the per-entry half read a `^18` |
| 1185 | + > artifact built before a prerelease retirement as postdating every stamp |
| 1186 | + > (`'authored-current'`), and the strict parse refused it. The replay is |
| 1187 | + > safe because a conversion rewrites only the shape it positively |
| 1188 | + > recognizes: an artifact built after a retirement comes back unchanged, |
| 1189 | + > and one built before it boots. ⚠️ The M2 caveat: an entry whose old shape |
| 1190 | + > becomes legal again cannot be protected for a bare-major floor by this |
| 1191 | + > half, nor by the label half, which strips it for the same artifact one |
| 1192 | + > release later; the change that re-legalizes a retired key decides that |
| 1193 | + > entry's fate at this door, where the default-flip refusal |
| 1194 | + > (`packages/metadata-core/src/artifact-forward-conversion.ts#DEFAULT_FLIPS_NOT_REPLAYED_HERE`) |
| 1195 | + > is the existing mechanism. The `retiredAfter` the note above defines is a |
| 1196 | + > stable `x.y.z`: under a prerelease label (Changesets pre mode) it is the |
| 1197 | + > newest published stable release, never a prerelease tarball (#23042, |
| 1198 | + > landed as `719183f50`, PR #23130). The addendum's closing sentence, |
| 1199 | + > "within-line retirements (17.1 → 17.2)", still names the case that |
| 1200 | + > created the window and is annotated there, not rewritten: the window now |
| 1201 | + > also serves a prerelease-major floor. The module docblock remains the |
| 1202 | + > rule's authority. |
1170 | 1203 | - **⚠️ The shipped key is the DECLARED FLOOR, not the authored version.** The |
1171 | 1204 | ruling says "authored `specVersion`"; what an artifact manifest actually |
1172 | 1205 | carries is a protocol *range*, so the implementation keys off that range's |
@@ -1216,3 +1249,11 @@ should CONSUME this policy rather than reimplement it. Nothing here changes the |
1216 | 1249 | protocol handshake's jurisdiction either — a cross-major gap refuses at |
1217 | 1250 | `checkProtocolCompat` before conversion could matter, and this window is for |
1218 | 1251 | within-line retirements (17.1 → 17.2), which are the case that created it. |
| 1252 | + |
| 1253 | +> **Annotated (2026-10-13).** Since `58f5c90999` the window also serves a |
| 1254 | +> prerelease-major floor: `^18` on a runtime labelled from 17.7.0 through |
| 1255 | +> 18.0.0, and from 18.0.1 on the label half replays the whole chain for it |
| 1256 | +> (the 2026-10-13 amendment note under the window bullet). The handshake |
| 1257 | +> still answers first: the protocol constant moves to M when M's prerelease |
| 1258 | +> opens, so a `^M` floor passes it while the package label still names the |
| 1259 | +> last release of M-1, and only then does the window decide. |
0 commit comments