Skip to content

Commit 4bce21d

Browse files
feat(adr-0087): retire a deleted published package whole (PACKAGE#*, D9 amendment) (#26766)
<!-- claude-projects-attribution: {"github_login":"os-warren"} --> _Requested by **Warren** · [project thread](https://claude.ai/code/project/chan_01U5RdnAAmkSQmZbqh6FtqZ8?thread=cmsg_01U5RdnAAmkSQmZbqh6FtqZ8AiiSUKX6yjeRe7S4FPtjpW)_ Fixes #26220 Clause-②: no Metadata-protocol: none ## 维护者速读 - 改了什么:ADR-0087 D9 加整包删除条款:已发布的包在 HEAD 被删除时,`runtime-export-retired` 的操作数写成 `包名#*`;门禁 `check-adr-0087-registration` 加对应谓词,自测新增 WP 电池 55 条。 - 为什么改:执行 PR #26365 评论 6439038287 的裁决 A(维护者「同意」)。被删的包在 HEAD 没有入口可读,`@object-ui/plugin-ai` 的下线(#26365)在这个门禁上会一直红。 - 风险与回滚:只放行整包删除这一种新形态,而且条件要全部成立:基点上是已发布的工作区包,HEAD 上没有 `package.json`、`pnpm-workspace.yaml` 不再列它的目录、不在 changesets 的 fixed 组。其他判定不变,原有 627 条自测照样全绿。回滚即 revert 本 PR。 - 先看:`docs/adr/0087-metadata-protocol-upgrade-contract.md` 里 D9 下面的第二条 2026-10-27 修订说明;`scripts/check-adr-0087-registration.mjs` 的 `verifyWholePackageRetired` 和谓词 2 的整包分支(`packageReference`)。 - 两分钟验证:`node scripts/check-adr-0087-registration.mjs --self-test`,预期以 0 退出并打印 `682 assertions`。 - 需要你做的:无。本 PR 等主任席位的契约复核(`CONTRACT_REVIEW_TIER`)PASS 和席位 ACCEPT,在那之前不需要你操作。 ## What changes **ADR-0087 D9, amended (Tier H).** A second dated note under D9's unmappable-entry paragraph, plus a Status-line entry, records the ruling on PR #26365 (comment `6439038287`, letter A, the maintainer's 「同意」 2026-10-27T10:07Z). A published package deleted at HEAD is retired whole with a third operand kind, `PACKAGE#*`: - **Predicate 1 (`retired-from-entry`)** is proven about the package. At the merge base it must be a workspace member, not `private`, and its published entry must map as D9 already requires. At HEAD it must be absent from the workspace in all of the ruling's three senses: no `package.json` (the base manifest is gone, and no workspace manifest carries the name, so a package that moved or was renamed is not deleted), no `pnpm-workspace.yaml` glob naming its directory literally, and no `fixed` group in `.changeset/config.json` listing it. A missing or unreadable config is refused, never read as an empty group. - **Predicate 2 (`no-metadata-surface`)** asks the package what it asks an export, at the merge base: none of the package's files is a metadata surface, and no metadata surface outside it names it in code (comment spans masked): an import specifier, by its root or a subpath, or any other occurrence of the package name, a string value included. - **Predicate 3 (`from-operands-named`)**: the FROM → TO block names the package on a single line, not one per export, by its package name. Its exports take no line of their own. An export or member operand on a deleted package is still refused at predicate 1, and the refusal now names the `PACKAGE#*` spelling. The closed-vocabulary blocks of the 2026-08-13 and 2026-10-12 addenda list the third marker line, and the 2026-10-12 addendum now reads "three operand kinds". **`scripts/check-adr-0087-registration.mjs`.** - `parseExportOperand` accepts `PACKAGE#*`. - `verifyWholePackageRetired` sits beside `sourceEntryAt` and holds predicate 1's package proof. It names every HEAD condition that fails, not only the first. - `packageReference` and a whole-package branch hold predicate 2. It refuses on the first comment-masked occurrence of the package name outside the package's own files, and reports an import specifier as IMPORTS. - Predicate 3 reads a FROM operand equal to a `#*` package's name as that operand. - The header comment carries the new operand. ## Why The ruling closes the last open question on #26220. Without the clause, a whole-package retirement could only land red. Its alternatives were `registered`, which would name a conversion that never happens, and `not-required (unpublished)`, which reads `private: true` at HEAD, a field a deleted package cannot carry. ## Tests `--self-test` at `994c3279`: exit 0, `682 assertions over real temp git repos` (627 at base `dc9d1ff8`). The new battery `WP (#26220)` has 55 assertions, and the roster floor goes from 57 to 58 batteries. Each red differs from WP-G1 in one respect. <details><summary>WP cases</summary> - G1: the #26365 shape. G1b: the verdict names `#*` and the manifest. G2: the one line written as an arrow removal. G3: a metadata surface that names the package only in comments. - R1: still in the fixed group. R2: `private: true` at base. R3: not a workspace member at base. R4: not deleted. R5: moved to another directory under the same name. R6: `pnpm-workspace.yaml` still names the directory. R7: no changesets config at HEAD. R8: no mappable published entry at base. - R9: the package carries a `*.zod.ts`. R10: a metadata surface outside it imports it. - R11: no FROM line. R12: per-export FROM lines instead of the package line. R13: an export operand on the deleted package points at `#*`. - R14: a metadata surface outside the package names it as a string value, with no import. - U1–U5: operand grammar; `packageReference` reads a subpath import, skips the package's own files, does not match a name that only shares the prefix (`plugin-xy`), and reports a string-value mention with its line while skipping a comment. </details> **First consumer, measured with this branch's gate against `origin/main` `dc9d1ff8`:** - PR #26365's head `bde1e722`, unchanged: exit 1. Each of its three export operands is refused at HEAD, and each refusal now names `@object-ui/plugin-ai#*`. - A local probe commit on that head that changes only its changeset (the marker becomes `runtime-export-retired @object-ui/plugin-ai#*`, and its three per-export FROM lines become one ``- FROM `@object-ui/plugin-ai` TO …`` line): exit 0, `verified: @object-ui/plugin-ai#* (package, ui/packages/plugin-ai/package.json)`. So #26365 needs exactly those two changeset edits. They belong to that PR and are not made here. ## Acceptance notes - **Wording beyond the ruling's text.** The ruling fixes predicate 1's proof and predicate 3's one line. It says nothing about predicate 2. D9 requires all three predicates for every operand, so the note restates D9's own predicate 2 for the package: no metadata-surface file in the package, and no metadata surface outside it naming the package in code (contract review 6457699551, must-fix 1). "Its exports take no line of their own" follows from predicate 3's existing "exactly the named operands". - **The fixed group is read literally.** Only `fixed` is read, as the ruling names it. A glob entry names no package, and `linked` / `ignore` are not read. - **No ablation.** os-dev excludes scripts from ablation. Each red asserts the specific refusal text it expects, and #26365's before/after pair above is the end-to-end witness. - **No changeset.** Nothing publishes: the root package is `private`, and `docs/adr/**` is not shipped. The PR carries `skip-changeset`. ## Verification `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` at `994c3279` (merge base `dc9d1ff8`, 2 paths): 44 commands derived, 44 run, all exit 0. Each ran with stdin from `/dev/null`, and each exit was captured before any pipe. `check:doc-formula-expressions` ran after `turbo run build` of `@objectstack/formula` and `@objectstack/lint` under the verify lock. `--ran` reconciliation: 44 derived, 44 run, 0 NOT-MEASURED, 0 UNRUN. No test file names the edited script (`git grep` over `*.test.*` / `*.spec.*`: 0 hits). Tier H (`docs/adr/**`). The director seat's contract review at `CONTRACT_REVIEW_TIER` is PASS on `994c3279` (comment 6458009682), after a FAIL on `75285daf` (6457699551). Landing waits for the seat's ACCEPT. ## Human Minimum - [x] 1. Schemas start as Zod; `packages/spec` holds no runtime logic. (No `packages/spec` change.) - [x] 2. Every name I added uses the casing for its kind. - [x] 3. No formatter ran; `pnpm lint` passes. (No formatter ran; lint is CI's run.) - [x] 4. One issue, one branch off an up-to-date `main`, one pull request. - [x] 5. A change that publishes carries a changeset. (This one publishes nothing: `skip-changeset`.) - [x] 6. `packages/spec` touched: n/a. - [x] 7. No hand edits to `content/docs/references/`, `packages/*/CHANGELOG.md` or `content/docs/releases/`. - [x] 8. Title, body and comments are in English. (维护者速读 is Chinese by house rule.) - [x] The *Before Submitting* list in CONTRIBUTING.md is done. --- _Generated by [Claude Code](https://claude.ai/code/session_01JQwxbX6swE1AuYRHTZsPqo)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 4daad78 commit 4bce21d

2 files changed

Lines changed: 335 additions & 14 deletions

File tree

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

Lines changed: 49 additions & 5 deletions
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**
@@ -753,6 +753,7 @@ gate's log and its `--list` output.
753753
<!-- adr-0087: not-required (type-surface-only <path>#<a>.<b>.<member>[, ...]) <why> -->
754754
<!-- adr-0087: not-required (runtime-export-retired <package>#<Export>[, ...]) <why> -->
755755
<!-- adr-0087: not-required (runtime-export-retired <package>#<Type>.<member>[, ...]) <why> -->
756+
<!-- adr-0087: not-required (runtime-export-retired <package>#*) <why> -->
756757
```
757758

758759
**The vocabulary is closed, and every exemption is re-verified on every run** — an
@@ -763,8 +764,9 @@ avoid. `registered` must name ids that resolve *and* are new in the diff;
763764
`no-migration-prescription` is refused by a body that carries a migration
764765
prescription; `runtime-interface-only` is the subject of the rest of this
765766
addendum; `type-surface-only` — the sixth, added 2026-08-30 — is the subject of
766-
the addendum after it; and `runtime-export-retired` — the seventh, added 2026-10-12 —
767-
is the subject of the addendum after that. The checks live in
767+
the addendum after it; and `runtime-export-retired` — the seventh, added 2026-10-12,
768+
whose whole-package operand `<package>#*` was added 2026-10-27 (#26220) — is the
769+
subject of the addendum after that. The checks live in
768770
[`scripts/check-adr-0087-registration.mjs`](../../scripts/check-adr-0087-registration.mjs),
769771
which is also where each one's measured history is written down.
770772

@@ -1035,12 +1037,14 @@ fails predicates 2 and 3 outright — it bumps `@objectstack/spec` and adds
10351037

10361038
## Addendum (2026-10-12) — the seventh category: a published runtime EXPORT retirement (#22918)
10371039

1038-
The vocabulary above gained a seventh answer, with two operand kinds that may be
1039-
mixed in one marker:
1040+
The vocabulary above gained a seventh answer, with three operand kinds that may be
1041+
mixed in one marker (the third, `<package>#*`, amended 2026-10-27, #26220 — see the
1042+
second 2026-10-27 amendment note under D9):
10401043

10411044
```text
10421045
<!-- adr-0087: not-required (runtime-export-retired <package>#<Export>[, ...]) <why> -->
10431046
<!-- adr-0087: not-required (runtime-export-retired <package>#<Type>.<member>[, ...]) <why> -->
1047+
<!-- adr-0087: not-required (runtime-export-retired <package>#*) <why> -->
10441048
```
10451049

10461050
### The dead end it closes
@@ -1139,6 +1143,46 @@ package's root entry is followed through the same mapping.
11391143
> fact the gate reads, and a build that generates `dist/index.d.ts` from some other file
11401144
> stays unmappable.
11411145
1146+
> **Amended 2026-10-27 (#26220) — a published package deleted at HEAD is retired whole:
1147+
> `<package>#*`.** Provenance: the maintainer's 「同意」 to letter A, recorded by the
1148+
> director seat as the ruling on PR #26365, comment `6439038287` (2026-10-27T10:07Z).
1149+
> Predicate 1 reads an export's absence from the package's entry at HEAD, and a deleted
1150+
> package has no entry there, so every operand on it was refused as "no workspace manifest
1151+
> at HEAD". The deletion of `@object-ui/plugin-ai` (PR #26365) is the first such case.
1152+
> Neither `registered` (it names a conversion `objectstack migrate meta` would apply, and
1153+
> nothing here is converted) nor `not-required (unpublished)` (it reads `private: true` at
1154+
> HEAD, which a deleted package cannot carry) is the true answer.
1155+
>
1156+
> A third operand kind names the package itself:
1157+
>
1158+
> ```text
1159+
> <!-- adr-0087: not-required (runtime-export-retired <package>#*) <why> -->
1160+
> ```
1161+
>
1162+
> For that operand the three predicates read:
1163+
>
1164+
> 1. **retired-from-entry** is proven about the package. At the merge base it is in the
1165+
> workspace with a published entry: a workspace member, not `private`, whose entry maps
1166+
> as above (an unmappable entry is refused, as for any operand). At HEAD it is absent
1167+
> from the workspace: no `package.json` (its base manifest is gone, and no workspace
1168+
> manifest carries its name, so a package that moved, its directory renamed, is not
1169+
> deleted), not in `pnpm-workspace.yaml` (no glob names its directory literally), and
1170+
> not in the changesets fixed group (no `fixed` group in `.changeset/config.json` lists
1171+
> it; a missing or unreadable config is refused, never read as an empty group).
1172+
> 2. **no-metadata-surface** asks the package what it asks an export, at the merge base:
1173+
> none of the package's files is a metadata surface, and no metadata surface outside it
1174+
> names it in code (comment spans masked, as `metadataSurfaceReference` reads): an
1175+
> import specifier, by its root or a subpath, or any other occurrence of the package
1176+
> name, a string value included.
1177+
> 3. **from-operands-named**: the FROM → TO block names the package on a single line, not
1178+
> one per export, by its package name (`` - FROM `<package>` TO … ``). Its exports take
1179+
> no line of their own, and a FROM line naming one is out of range, as any operand the
1180+
> marker does not name is.
1181+
>
1182+
> An export or member operand on a package deleted at HEAD stays refused at predicate 1,
1183+
> and the refusal names the `<package>#*` spelling. Nothing else about the category
1184+
> changes: the operands named are judged, and the claim's completeness is not.
1185+
11421186
**The deviation accepted with the ruling.** A merge base has no build output (`dist/`
11431187
is gitignored), so predicate 1 compiles the **source** entry that generates the
11441188
public `.d.ts`, not the `.d.ts` itself. For the question "which names does the

0 commit comments

Comments
 (0)