|
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) · **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) |
| 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) · **Amended** (2026-10-27, #26220, the maintainer's decision A: D8's predicate 4 gains an index-signature limb, and D9's predicate 1 maps a `src/index.tsx` source entry; see the 2026-10-27 amendment notes under D8's predicate 4 and under D9's unmappable-entry paragraph) |
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** |
@@ -927,6 +927,45 @@ checks all four by name (`published`, `no-spec-diff`, `no-metadata-surface-diff` |
927 | 927 | never guessed at: a reference that silently landed on the wrong same-named member |
928 | 928 | would be writable but wrong, which is worse than the refusal it replaces. |
929 | 929 |
|
| 930 | + > **Amended 2026-10-27 (#26220) — predicate 4 reads an `any` INDEX SIGNATURE.** |
| 931 | + > Provenance: the maintainer's decision A on the combined D8 + D9 question of |
| 932 | + > #26220, recorded on the card as claim amendment `6433464805` (2026-10-27T04:03Z). |
| 933 | + > The class it admits: a published interface whose declared members stay as they |
| 934 | + > were while its index signature moves off `any`. Measured on #26293: |
| 935 | + > `ui/packages/types/src/base.ts#ComponentRendererProps` is |
| 936 | + > `{ schema: TSchema; [key: string]: any; }` at the merge base and |
| 937 | + > `{ schema: TSchema; [key: string]: unknown; }` at HEAD. Every forwarded key was |
| 938 | + > readable as anything and now has to be narrowed first, so a consumer's code can stop |
| 939 | + > compiling, and nothing reaches `objectstack migrate meta`. Read as written above, |
| 940 | + > predicate 4 refuses it twice over: the interface is a named structural declaration, |
| 941 | + > concrete at base by construction, and `unknown` at HEAD counts as erased. |
| 942 | + > |
| 943 | + > For a bare `<path>#<Symbol>` naming an exported `interface`, or an exported `type` |
| 944 | + > alias whose right-hand side is an object type literal, predicate 4 **also** holds |
| 945 | + > when all three of these hold: |
| 946 | + > |
| 947 | + > 1. at the merge base the declared member surface carries at least one index |
| 948 | + > signature whose value type is `any`; |
| 949 | + > 2. at HEAD at least one of those index signatures carries a value type other than |
| 950 | + > `any`. Here `unknown` **counts as narrowed**: through an index signature `any` |
| 951 | + > admits every read and `unknown` admits none, which is exactly the break this |
| 952 | + > category exists for. The whole-symbol reading above is unchanged, and there |
| 953 | + > `unknown` stays erased; |
| 954 | + > 3. everything else is identical at both revs: the declaration's heritage (its |
| 955 | + > `extends` clause, its type parameters) and every member, compared member by |
| 956 | + > member on the comment-masked, whitespace-normalised surface, with only the value |
| 957 | + > types of the index signatures set aside. A member added, removed, renamed, |
| 958 | + > re-typed or made optional fails this limb. |
| 959 | + > |
| 960 | + > **Why this is not a weakening.** The #6048 shape, a concretely typed interface that |
| 961 | + > LOST a member, fails condition 3 and is refused by predicate 4 exactly as before, |
| 962 | + > with the same founding-case refusal. The regression pin below keeps asserting it. |
| 963 | + > The limb admits only a surface whose sole change is that an index signature moved |
| 964 | + > off `any`. That is the same base-side reading predicate 4 always made, applied to the |
| 965 | + > one member that can be erased while the declaration around it is concrete. A dotted |
| 966 | + > member path keeps reading a member's return annotation alone; the limb does not |
| 967 | + > apply to it. |
| 968 | +
|
930 | 969 | ### ⭐ Predicate 4 is what makes this a narrowing rather than a hole |
931 | 970 |
|
932 | 971 | This is the **only** category exempt from the `no-migration-prescription` refusal. |
@@ -1062,6 +1101,29 @@ claim at HEAD (a name could sit behind it), and so does a type whose member set |
1062 | 1101 | HEAD depends on something the reader cannot resolve. A re-export of another workspace |
1063 | 1102 | package's root entry is followed through the same mapping. |
1064 | 1103 |
|
| 1104 | +> **Amended 2026-10-27 (#26220) — the source entry may be `src/index.tsx`.** |
| 1105 | +> Provenance: the maintainer's decision A on the combined D8 + D9 question of #26220, |
| 1106 | +> recorded on the card as claim amendment `6433464805` (2026-10-27T04:03Z). The mapping |
| 1107 | +> pinned above reads only `src/index.ts`. The `ui/` packages build their declaration |
| 1108 | +> entry with vite-plugin-dts (`insertTypesEntry`, `include: ['src']`) from |
| 1109 | +> `src/index.tsx`. Measured at `83e9d718`, 21 of the 38 published `ui/` packages have that |
| 1110 | +> entry, and all 21 name `./dist/index.d.ts` as their types entry, so a |
| 1111 | +> `runtime-export-retired` operand on any of them was refused at predicate 1 as |
| 1112 | +> unmappable. The plugin-ai retirement of #25921 (PR #26365) is the first such case. |
| 1113 | +> `@objectstack/client-react`, named above as unmappable, is the same shape under tsup |
| 1114 | +> (`tsup src/index.tsx`). |
| 1115 | +> |
| 1116 | +> The mapping is now: the declaration entry the `exports` map names (`dist/index.d.ts`, or |
| 1117 | +> its `.d.mts` / `.d.cts` twin) is generated from the package's **one** source entry, |
| 1118 | +> `src/index.ts` **or** `src/index.tsx`. A package carrying both is refused as ambiguous |
| 1119 | +> (none does at `83e9d718`), and a package carrying neither is refused as unmappable, |
| 1120 | +> loudly and by name as before. Nothing else about predicate 1 changes. The compiler |
| 1121 | +> reads the `.tsx` entry and every `.tsx` module behind it with JSX enabled. #26319 made |
| 1122 | +> the reader resolve `.tsx` modules for predicate 2, and an entry that is itself `.tsx` is |
| 1123 | +> read the same way. The build tool is never asked: the source file's name is the |
| 1124 | +> fact the gate reads, and a build that generates `dist/index.d.ts` from some other file |
| 1125 | +> stays unmappable. |
| 1126 | +
|
1065 | 1127 | **The deviation accepted with the ruling.** A merge base has no build output (`dist/` |
1066 | 1128 | is gitignored), so predicate 1 compiles the **source** entry that generates the |
1067 | 1129 | public `.d.ts`, not the `.d.ts` itself. For the question "which names does the |
|
0 commit comments