Skip to content

Commit 2494b54

Browse files
committed
docs(adr-0087): amend D8 predicate 4 and D9 predicate 1 for ui packages
D8: predicate 4 also holds when an exported interface's only change is an `any` index signature moving to a non-`any` value type (`unknown` counts as narrowed there); heritage and every other member must be identical, so the #6048 member-removal shape stays refused. D9: the declaration entry maps to the package's one source entry, `src/index.ts` or `src/index.tsx` (vite-plugin-dts in ui/, tsup in client-react); both present is refused as ambiguous. Maintainer decision A on #26220 (claim amendment 6433464805). Claude-Session: https://claude.ai/code/session_01Am8eYNReiNP66D2JihPS1q Co-authored-by: Claude <noreply@anthropic.com>
1 parent 83e9d71 commit 2494b54

1 file changed

Lines changed: 63 additions & 1 deletion

File tree

‎docs/adr/0087-metadata-protocol-upgrade-contract.md‎

Lines changed: 63 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# ADR-0087: Metadata protocol upgrades for AI consumers — conversion over notification, executable migrations, machine-verifiable upgrades
22

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)
44
**Deciders**: ObjectStack Protocol Architects
55
**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)
66
**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`
927927
never guessed at: a reference that silently landed on the wrong same-named member
928928
would be writable but wrong, which is worse than the refusal it replaces.
929929

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+
930969
### ⭐ Predicate 4 is what makes this a narrowing rather than a hole
931970

932971
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
10621101
HEAD depends on something the reader cannot resolve. A re-export of another workspace
10631102
package's root entry is followed through the same mapping.
10641103

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+
10651127
**The deviation accepted with the ruling.** A merge base has no build output (`dist/`
10661128
is gitignored), so predicate 1 compiles the **source** entry that generates the
10671129
public `.d.ts`, not the `.d.ts` itself. For the question "which names does the

0 commit comments

Comments
 (0)