Skip to content

Commit 75285da

Browse files
committed
feat(adr-0087): retire a deleted published package whole with <package>#*
ADR-0087 D9 gains a whole-package operand for `runtime-export-retired`, per the ruling on PR #26365 (comment 6439038287, letter A). A published package deleted at HEAD has no entry to read an absence from, so the operand is the package itself: predicate 1 proves it was a published workspace member at the merge base and is absent at HEAD (no package.json, no workspace glob naming its directory, not in the changesets fixed group); predicate 2 asks the package for a metadata surface or a metadata-surface importer; predicate 3 reads its one FROM line by package name. The gate gains that predicate and a 50-assertion WP battery. Claude-Session: https://claude.ai/code/session_01JQwxbX6swE1AuYRHTZsPqo Co-authored-by: Claude <noreply@anthropic.com>
1 parent dc9d1ff commit 75285da

2 files changed

Lines changed: 302 additions & 10 deletions

File tree

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

Lines changed: 38 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) · **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) · **Amended** (2026-10-26, #25767 ruling D2 = A, scoped by ruling 6434038284 of 2026-10-27 — a retirement kit (the `retiredKey()` tombstone and its D2 conversion with fixtures) lives one protocol major after the one it retired in and is then removed; a removed key is refused as a strict unknown key whose refusal names the protocol upgrade guide; the kit ages out, the chain's step stays, and the support floor stays 16; see the 2026-10-26 addendum, which also amends the "never deleted" wording in D2 and the conversion "forever" in the 2026-07-31 and 2026-09-13 addenda)
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) · **Amended** (2026-10-27, #26220, ruling A on PR #26365, comment `6439038287` — D9 gains a whole-package operand, `<package>#*`, for a published package deleted at HEAD; see the second 2026-10-27 amendment note under D9's unmappable-entry paragraph) · **Amended** (2026-10-26, #25767 ruling D2 = A, scoped by ruling 6434038284 of 2026-10-27 — a retirement kit (the `retiredKey()` tombstone and its D2 conversion with fixtures) lives one protocol major after the one it retired in and is then removed; a removed key is refused as a strict unknown key whose refusal names the protocol upgrade guide; the kit ages out, the chain's step stays, and the support floor stays 16; see the 2026-10-26 addendum, which also amends the "never deleted" wording in D2 and the conversion "forever" in the 2026-07-31 and 2026-09-13 addenda)
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**
@@ -1139,6 +1139,43 @@ package's root entry is followed through the same mapping.
11391139
> fact the gate reads, and a build that generates `dist/index.d.ts` from some other file
11401140
> stays unmappable.
11411141
1142+
> **Amended 2026-10-27 (#26220) — a published package deleted at HEAD is retired whole:
1143+
> `<package>#*`.** Provenance: the maintainer's 「同意」 to letter A, recorded by the
1144+
> director seat as the ruling on PR #26365, comment `6439038287` (2026-10-27T10:07Z).
1145+
> Predicate 1 reads an export's absence from the package's entry at HEAD, and a deleted
1146+
> package has no entry there, so every operand on it was refused as "no workspace manifest
1147+
> at HEAD". The deletion of `@object-ui/plugin-ai` (PR #26365) is the first such case.
1148+
> Neither `registered` (it names a conversion `objectstack migrate meta` would apply, and
1149+
> nothing here is converted) nor `not-required (unpublished)` (it reads `private: true` at
1150+
> HEAD, which a deleted package cannot carry) is the true answer.
1151+
>
1152+
> A third operand kind names the package itself:
1153+
>
1154+
> ```text
1155+
> <!-- adr-0087: not-required (runtime-export-retired <package>#*) <why> -->
1156+
> ```
1157+
>
1158+
> For that operand the three predicates read:
1159+
>
1160+
> 1. **retired-from-entry** is proven about the package. At the merge base it is in the
1161+
> workspace with a published entry: a workspace member, not `private`, whose entry maps
1162+
> as above (an unmappable entry is refused, as for any operand). At HEAD it is absent
1163+
> from the workspace: no `package.json` (its base manifest is gone, and no workspace
1164+
> manifest carries its name, so a package that moved or was renamed is not deleted),
1165+
> not in `pnpm-workspace.yaml` (no glob names its directory literally), and not in the
1166+
> changesets fixed group (no `fixed` group in `.changeset/config.json` lists it; a
1167+
> missing or unreadable config is refused, never read as an empty group).
1168+
> 2. **no-metadata-surface** asks the package what it asks an export, at the merge base:
1169+
> none of the package's files is a metadata surface, and no metadata surface outside it
1170+
> imports it in code, by its root or a subpath.
1171+
> 3. **from-operands-named**: the FROM → TO block names the package on **one** line, by its
1172+
> package name (`` - FROM `<package>` TO … ``). Its exports take no line of their own,
1173+
> and a FROM line naming one is out of range, as any operand the marker does not name is.
1174+
>
1175+
> An export or member operand on a package deleted at HEAD stays refused at predicate 1,
1176+
> and the refusal names the `<package>#*` spelling. Nothing else about the category
1177+
> changes: the operands named are judged, and the claim's completeness is not.
1178+
11421179
**The deviation accepted with the ruling.** A merge base has no build output (`dist/`
11431180
is gitignored), so predicate 1 compiles the **source** entry that generates the
11441181
public `.d.ts`, not the `.d.ts` itself. For the question "which names does the

0 commit comments

Comments
 (0)