Skip to content

Commit 4cc5bcd

Browse files
docs(governed): the retirement route is retiredKey() on any shape, with the retiredAfter stamp and the step-18 registry shape (#20797)
Fixes #20465 Fixes #20575 Clause-②: no Family PR, one commit per card. Tier H as a whole (`AGENTS.md`, `docs/adr/**`), so it stays a draft for the maintainer's review. Nothing here publishes: `skip-changeset`. ## What changed **#20465, commit `249b7836`** (the route, the `retiredAfter` stamp, and the ADR-0087 window sentence from the spec seat 4 fold `5873572274`): - `.claude/skills/spec-property-retirement/SKILL.md` §2 route table. Row 1 is now "any shape, strict or not" → `retiredKey()` tombstone. On a strict shape a bare deletion is loud, but it reports only an unrecognized key and loses both the prescription and the `tsc` channel. Row 2 keeps the guidance map only for a spelling the shape never declared (a retired key's old alias, a wrong-layer pointer). Such a spelling has no property for a tombstone to replace. The example is now `data/mapping.zod.ts`. The same section's ledger table, and two lines in §4, rename "strict 删除" to "无墓碑删键", because the route table no longer names a strict deletion. - The kit's `retiredFromLoadPath` checklist item now names the required `retiredAfter: 'x.y.z'`. `tsc` refuses a retirement without it. A new retirement carries the current `packages/spec/package.json` label, and `retired-after.census.test.ts` pins each value. The artifact door opens its window per entry from this stamp. - `AGENTS.md` Post-Task Checklist step 3 says the same as the kit, in one rule: `retiredKey()` whether or not the schema is `.strict()`, and a `*_RETIRED_KEY_GUIDANCE` entry only for a spelling the shape never declared. The closing clause now says what actually differs between the routes: a tombstone keeps its liveness-ledger row, and a key deleted without one loses it. - `docs/adr/0087-metadata-protocol-upgrade-contract.md`, in the 2026-09-13 addendum's window bullet: the sentence "`floor >= runtime` replays nothing" is struck through and amended in the dated style that ADR-0005 and ADR-0053 used on 2026-09-30. The amendment note gives the provenance (the #20390 ruling A, landed as `e956924e`), and the status line gains an Amended entry. **#20575, commit `2e3f8aa0`** (the step-18 D3-chain item, worded for the shape on `main` after #20535 and #20574): - For step 18, the conversion goes only into `MAJOR_18_CONVERSIONS` in `conversions/registry.ts`. It is inserted at its identifier's sorted position, as the list's header says. `CONVERSIONS_BY_MAJOR[18]` and the step's `conversionIds` both derive from that list. The `rationale` gains one `STEP18_RATIONALE` fragment at the sorted position of its D3 semantic id. The merge pin that each header names refuses an append at the tail. - Earlier steps keep the old wording, and the misspelled-id warning now covers them only. Step 18 cannot hit it, because its ids are derived. ## Measured first (premise holds), at `7a09eee1` - **The route.** The `retired-key.ts` header (`:22`–`:28`) gives the reason tombstones stay on closed shapes. The `acceptsNothing` doc in `strict-object.ts` calls a tombstone "strictly stronger than a `guidance` entry". The precedents are real: `action.aria` (`dcd3bceaa`, a `strictObject`, whose comment at `action.zod.ts:1678` says "`retiredKey()` rather than a bare deletion although this is a `strictObject`"), the list view's `tabs` (`6e3e5462c`, in the strict `ListViewShapeSchema`), and the cube members' inner `name` (`analytics.zod.ts:243`, `:298`). - **Where the guidance map still carries weight.** `data/mapping.zod.ts` lists the retired keys' old alias spellings (`query`, `onError`) in its guidance map so that "an author who learned the alias should land on the prescription". Those spellings were never declared keys, so a tombstone has nothing to replace. `app.zod.ts` (`showall`, `location`) has the same shape. - **`retiredAfter`.** `conversions/types.ts` types it as REQUIRED on the retired arm of the union. The census test pins an entry that no published tarball carries retired to the package label (`17.5.0` on this tree). - **ADR-0087's sentence is false on `main`.** `idsTheFloorPostdates` in `artifact-forward-conversion.ts` replays a retired entry when the floor is at or below its `retiredAfter`, even when the floor is at or above the runtime label (verdict `'converted-retired-after'`). On this tree three entries carry `retiredAfter: '17.5.0'`, which equals the label (`time-default-utc-suffix-dropped`, `cube-refresh-key-removed`, `connector-triggers-removed`). An artifact whose floor is `17.5.0` therefore replays three entries, where the ADR said it replays none. `artifact-forward-conversion.test.ts:564` pins the same case one release back. - **Step 18.** `step18.conversionIds` is `CONVERSIONS_BY_MAJOR[18]!.map((c) => c.id)`, and `CONVERSIONS_BY_MAJOR[18]` is `inApplicationOrder(MAJOR_18_CONVERSIONS)`. `rationale` is `joinRationale(STEP18_RATIONALE)`. The pins are `packages/spec/scripts/conversions-major18-merge.test.ts` and `step18-rationale-merge.test.ts`. `step17` still carries a hand-kept `conversionIds` list. ## For the reviewer: one point where the text departs from the triage wording The #20465 triage direction says to keep the guidance map "only for the case that needs it: a shape where a tombstone cannot sit, such as a `z.preprocess` stage ahead of the closed shape (`retired-key.ts:177`)". The route follows that direction, but the example did not hold up when measured: - `retired-key.ts:177` describes `acceptRetiredDefaultResidue`. That preprocess stage strips an emitted default ahead of a closed shape that still declares the key as a `retiredKey()` tombstone. The tombstone sits there. - The list view's `tabs` tombstone (`6e3e5462c`) sits in a strict member of `ViewMetadataSchema`, which is itself a `z.preprocess` ahead of its union. - The guidance map is consulted only on the `unrecognized_keys` path of a `strictObject`, so a preprocess stage cannot carry one either. So both texts name the case that was measured: a spelling the shape never declared. If the maintainer means a different case, the Tier H review is the place to say so, and the kit follows. ## Scope notes - The claim's surface for the kit named `:86`–`:87`, `:207` and `:215`–`:220`. Consequential edits in the same file: the §2 ledger table row and the ORPHAN bullet (`:102`, `:107`), and §4 `:242` and `:257` (the "strict 删除" term). The file is otherwise unchanged. - The new lines were paid for by deleting narrative that is kept elsewhere: - the orphan leg's history (the "report `aria`/`performance`" story), which `packages/spec/scripts/liveness/orphans.mts` carries verbatim in its header; - the "上一版样例栽在这" sentence in the `retiredFromLoadPath` item. The rule it taught is the item's own preceding sentence and the `conversions/types.ts` docblock. ## Line budget (`node scripts/pm/check-skill-line-ratchet.mjs`, exit 0) | file | before | after | |---|---|---| | `.claude/skills/spec-property-retirement/SKILL.md` lines | 337 / 337 | 337 / 337 | | same file, widest table row | 326 / 326 bytes | 324 / 326 bytes | | `AGENTS.md` lines | 1106 / 1116 | 1107 / 1116 | Every added line of the kit and of `AGENTS.md` is within 120 bytes, except table rows, which are structurally exempt. After commit 1 the kit read 335 / 337. The ratchet prints an informational hint to lower the row pin to 324; the pin lives in `scripts/pm/check-skill-line-ratchet.mjs`, which is outside this surface, so it is left for the owning seat. ## Gates, at head `2e3f8aa0` These were derived by `node scripts/pm/dispatch-gates.mjs --commands` from this change set (31 families, the same set the dispatch named). All exited 0. Reconciled with `--ran`: "31 derived, 31 run, 0 NOT-MEASURED, 0 UNRUN", a derived zero, with every exit code recorded. - `check-adr-0087-registration` (and its self-test), `check-adr-links` (and self-test; 691 links resolve), `check-adr-symbol-anchors` (and self-test; 2157 anchors across 140 records resolve), `check-ci-filter-parity`, `check-closing-keyword-parity` (and self-test), `check-comment-mask-corpus`, `check-harness-current --self-test`. - `pnpm check:` adr-anchors, agent-test-spelling, changeset-gate-self-tests, cross-package-test-inputs, doc-authoring, docs-audit-scope, driver-memory-census, future-spec-major, gitlink-declared, nul-bytes, pm-governed-merges, pm-governed-prose (2 instruction surfaces name all 6 governed surfaces), pm-prior-rulings, pm-skill-id-lint (34 files clean), pm-skill-ratchet, refd-timer-probe, required-contexts, skill-frame-sync, watch-hint-literal. - `pnpm --filter @objectstack/lint run check:doc-formula-expressions`. The first run exited 3 (PREREQUISITE NOT MET: `@objectstack/formula` and `@objectstack/lint` were unbuilt in the fresh worktree), which measured nothing. After `turbo run build` for those two packages under the verify lock, the re-run exited 0. - Beyond the derived set: `packages/spec/src/shared/retired-key-migrate-sentence.test.ts` reads this kit as part of its corpus. 1 file and 14 tests passed. ## Acceptance notes - Also out of this PR's surface: kit §3's `RETIRED_KEYS_BY_MAJOR` item still says to add the entry "in `packages/spec/src/migrations/registry.ts`". Since #7297, an entry is one file under `packages/spec/src/migrations/entries/retired-keys/` followed by `gen:migration-registry`, and the region in `registry.ts` is generated. The kit is echoing the `build-schemas.ts` gate (b) failure text, which still says to paste the key into `registry.ts` under the current major. That gate text is the producer, and it is outside this surface. It is reported to the PM in the dev report, not filed. ## 维护者速读(草稿) **改了什么** - 退役手册 §2 与 AGENTS.md 第 3 步统一为一条路线:任何 schema(strict 与否)都用 `retiredKey()` 墓碑。guidance map 只留给 shape 从未声明过的拼写(如退役键的旧别名),那里没有属性可供墓碑替换。 - 手册的 `retiredFromLoadPath` 条目补上必填的 `retiredAfter` 版本戳。 - 第 18 步 D3 链条目改写为 main 上的排序键形状:conversion 只进 `MAJOR_18_CONVERSIONS`,rationale 只加一个排序片段。 - ADR-0087 中「floor ≥ runtime 不重放任何条目」一句已按落地的逐条窗口规则做了日期化修订。 **为什么改** - 两份受管文本与代码头注、本周三次落地的先例相反,每次退役都要在报告和复核里重新争论路线。 - 第 18 步条目让作者去改一个已经由代码派生、不再手写的列表。 - ADR 那句话在 main 上已经为假:3 个 `retiredAfter` 等于当前版本标签的条目,在 floor 等于 runtime 时仍会被重放。 **风险与代价(含回滚)** - 纯文档与 agent 规则,不发布任何包,也没有运行时影响。回滚方式是 revert 本 PR 的两个提交。 - 一处措辞偏离:分诊方向举的例子是「z.preprocess 阶段」,实测不成立(那里墓碑照样能放)。两份文本改为点名实测成立的情形,详见正文「For the reviewer」一节。 **席位意见** **你要做的** - 审阅 AGENTS.md 与 ADR-0087 两处改动(Tier H),同意则给 APPROVED review。 - 如果希望 strict shape 保留「删键 + guidance map」路线,或坚持使用原示例的措辞,请在评审里写明,手册这一半会随之修改。 --- _Generated by [Claude Code](https://claude.ai/code/session_01KTZmMfzVzjNvyaLyQ8mHvg)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent d2b188f commit 4cc5bcd

3 files changed

Lines changed: 44 additions & 26 deletions

File tree

‎.claude/skills/spec-property-retirement/SKILL.md‎

Lines changed: 18 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -83,8 +83,8 @@ conversion,钉上 non-warn。十四个键里有一个是这样被证伪的 —
8383

8484
| Schema | 路线 | 机制 |
8585
|---|---|---|
86-
| **非 `.strict()`** | `retiredKey()` 墓碑 | `packages/spec/src/shared/retired-key.ts` 的 `retiredKey(guidance)` —— `z.never({ error: () => guidance }).optional()`。两个通道:`tsc`(输入类型 `never`)与 parse(处方本身,不是 "unrecognized key")。 |
87-
| **`.strict()`** | 删键 + guidance map | 从 shape 里删除;向该 schema 的 `*_RETIRED_KEY_GUIDANCE` 加条目,由 `strictObject()` 的 `guidance:` 槽消费(`shared/strict-object.ts`;整族一条走 `guidanceSets`)。样板 `ai/tool.zod.ts`,审计 `shared/alias-integrity.test.ts`。⛔ 别再手写 `$ZodErrorMap`。 |
86+
| **任何 shape**(strict 与否) | `retiredKey()` 墓碑 | `packages/spec/src/shared/retired-key.ts` 的 `retiredKey(guidance)` —— `z.never({ error: () => guidance }).optional()`。两个通道:`tsc`(输入类型 `never`)与 parse(处方本身)。strict 上裸删也响,但只报 "unrecognized key",两通道都丢。 |
87+
| **从未声明的拼写** | guidance map | 退役键的旧 alias、错层指针,墓碑无属性可换:向 `*_RETIRED_KEY_GUIDANCE` 加条目,由 `strictObject()` 的 `guidance:` 槽消费(整族走 `guidanceSets`)。样板 `data/mapping.zod.ts`,审计 `shared/alias-integrity.test.ts`。⛔ 别手写 `$ZodErrorMap`。 |
8888
| **没人 parse 它** | 都不用 | 没人能收到的处方是噪音。有意删掉 baseline 行并在 changeset 里写明 —— 先例 #3896 与 #4834(PR #4878),都在 kernel plugin-runtime 家族。家族删除后幸存的解释块在 `packages/spec/src/kernel/index.ts`(搜 `plugin-runtime.zod`)。 |
8989

9090
永不从非 strict schema 上裸删一个键:zod 会静默剥掉它,你只是用一个静默 no-op 换了
@@ -99,18 +99,15 @@ liveness 门禁走的是 **schema 的 shape**,逐个属性去
9999
| 路线 | 键还在被走的 shape 里? | 它的台账条目 |
100100
|---|---|---|
101101
| `retiredKey()` 墓碑 | **在**(`z.never()` 是属性) | **保留** —— `status: "dead"`、一个 `verifiedAt`、一条 `note` 写明 REMOVED + 条目为何还在 |
102-
| strict 删除 | 不在 | **删除**,连同 CLI advisory-lint 的预期 |
102+
| 删键(无墓碑) | 不在 | **删除**,连同 CLI advisory-lint 的预期 |
103103

104104
现在两个方向都会红 CI,搞反了两边都很响:
105105

106106
- 删掉**墓碑**键的行,报 **UNCLASSIFIED**(#3896 清扫一次 14 个 —— 本节就是防它);
107-
- 留着 **strict 删除**键的行,报 **ORPHAN** 行。
107+
- 留着**无墓碑删除**的键的行,报 **ORPHAN** 行。
108108

109-
orphan 这条腿是新的(`packages/spec/scripts/liveness/orphans.mts`)。它落地之前这个方向从不失败
110-
—— 门禁走 schema 再查行,键已离开 shape 的行根本不会被问到,原地腐烂。report 的
111-
`aria`/`performance` 行就这样比它们的键多活了一整个 release,靠有人恰好读到那个文件
112-
才手工删掉。你撞上 orphan 报错而属性确实还可编写时,要修的是 **walk**,不是行:
113-
walk 看不见的属性就是 ratchet 管不到的属性。
109+
orphan 这条腿住 `packages/spec/scripts/liveness/orphans.mts`,来历见其头注。你撞上 orphan 报错而属性
110+
确实还可编写时,要修的是 **walk**,不是行:walk 看不见的属性就是 ratchet 管不到的属性。
114111

115112
墓碑条目的 note 模板(house style 原文,如 `liveness/action.json`):
116113

@@ -204,18 +201,21 @@ conversion 是消费者跟的;D3 条目是升级方的 agent 读的。三个都
204201
(`flow.nodes[].outputSchema`),那也是 upgrade guide 打印的。多键 conversion
205202
仍用恰好 `' / '` 连接子句(tool 清扫以来的 house style)。下游不再有任何东西
206203
从它解析归属 —— 那个职责移给了上面的条目。
207-
- [ ] **`retiredFromLoadPath: true`** —— 退役恒真,但管辖权只有 authoring 漏斗
204+
- [ ] **`retiredFromLoadPath: true` 与 `retiredAfter: 'x.y.z'`** —— 后者必填(缺则 `tsc` 拒),新退役填
205+
`packages/spec/package.json` 的当前版本标签(`retired-after.census.test.ts` 逐值钉;artifact 门据它
206+
逐条开窗)。前者退役恒真,但管辖权只有 authoring 漏斗
208207
`normalizeStackInput`;三处 data-at-rest seam 以 `includeRetired: true` 故意重放退役
209208
条目,它**一处也拦不住**:`applyConversionsToStoredItem`(钉死)、automation
210209
engine 的 flow rehydration、`applyArtifactForwardConversions`。对*改名*它意味着
211210
「没有 alias 窗口,故意的」;对**默认值翻转**,只有确知输入早于翻转的 seam 才可重
212211
放,其余按 id 退订 `excludeConversionIds` —— `app-hidden-to-unpublished` 在 artifact
213-
门即如此。上一版样例栽在这:它教「只有 migrate meta 能应用翻转」,而 boot 时照样
214-
应用,该 conversion 已撤(`packages/spec/CHANGELOG.md`)。
215-
- [ ] **一步 D3 链**,在 `packages/spec/src/migrations/registry.ts` —— 把 id 加进
216-
`MIGRATIONS_BY_MAJOR[N].conversionIds`,扩写该步的 `rationale`。
217-
`conversion.toMajor` **必须等于**该步的 major。⚠ 没有东西直接断言「每个
218-
conversion 都接进了某一步」,拼错的 id 在 replay 时被**静默跳过**;
212+
门即如此。
213+
- [ ] **一步 D3 链**,在 `packages/spec/src/migrations/registry.ts`;`conversion.toMajor` **必须等于**该步的
214+
major。**18 步**:conversion 只进 `conversions/registry.ts` 的 `MAJOR_18_CONVERSIONS`,照其头注按
215+
标识符排序插入,本步 `conversionIds` 由它派生;`rationale` 只加一个 `STEP18_RATIONALE` 片段,
216+
按其头注插在你 D3 semantic id 的排序位;尾部追加被两处头注点名的 merge 测试拒收。
217+
**更早的步**:id 加进 `MIGRATIONS_BY_MAJOR[N].conversionIds`,扩写该步 `rationale`。⚠ 没有东西
218+
直接断言「每个 conversion 都接进了某一步」,拼错的 id 在 replay 时被**静默跳过**;
219219
chain-replay 测试抓得到它,只因为没接线的 fixture 永远到不了自己的 `after`。
220220
所以把那个测试的失败读作「没接线」,不是「transform 坏了」。
221221
- [ ] **fixture 必须不相交 —— 两重。** 每个 fixture 都被整张表 replay,必须恰好等于
@@ -239,7 +239,7 @@ conversion 是消费者跟的;D3 条目是升级方的 agent 读的。三个都
239239

240240
从上往下做;每一行背后都有一个门。
241241

242-
- [ ] **Schema** —— 墓碑或 strict 删除(§2),外加 schema 内注释:删了什么、真正生
242+
- [ ] **Schema** —— 墓碑或无墓碑删键(§2),外加 schema 内注释:删了什么、真正生
243243
效的机制是什么。
244244
- [ ] **孤儿值 schema** —— 一个键的 `XxxConfigSchema` 没有别的消费者就随它一起走
245245
(`PerformanceConfigSchema`、`AIKnowledgeSchema`、`ToolCategorySchema`)。没有
@@ -254,7 +254,7 @@ conversion 是消费者跟的;D3 条目是升级方的 agent 读的。三个都
254254
要手改)。
255255
- [ ] **生成 baseline** —— `pnpm --filter @objectstack/spec gen:schema` 会动
256256
`authorable-surface/<category>.json`(墓碑 → 一条新的 `… [RETIRED]` 行;
257-
strict 删除 → 该行**消失**,这是门 (a) 的绊线,所以同一个 PR 里有意删掉它)与
257+
无墓碑删键 → 该行**消失**,这是门 (a) 的绊线,所以同一个 PR 里有意删掉它)与
258258
`json-schema.manifest/<category>.json`。#5837 起两者都按 category 分片 —— 门
259259
禁把整个目录读成一个集合,退役流程不变;变的只是那一行住在哪个文件。然后
260260
`gen:spec-changes`、`gen:upgrade-guide`、`gen:api-surface`、`gen:docs`。

‎AGENTS.md‎

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1077,11 +1077,12 @@ Both non-handshake shapes, and how to classify and probe your own:
10771077
spec key, an export, a config field), the changeset body must state the FROM → TO mapping and the one-line fix —
10781078
this text ships to consumers as `CHANGELOG.md` inside the npm package and is what an upgrading agent greps after the
10791079
tombstone error. Removing an authorable spec key also requires a tombstone so the rejection itself carries the
1080-
prescription — `retiredKey()` (`packages/spec/src/shared/retired-key.ts`) on a non-strict schema, or an entry in
1081-
the relevant `UNKNOWN_KEY_GUIDANCE` / `*_RETIRED_KEY_GUIDANCE` map (see `object.zod.ts`, `ai/tool.zod.ts`) when the
1082-
schema is `.strict()`. The changeset is one of fourteen surfaces a retirement touches — follow the
1083-
`spec-property-retirement` skill (`.claude/skills/`) rather than reconstructing the kit, and note the two routes
1084-
imply **opposite** liveness-ledger dispositions.
1080+
prescription — `retiredKey()` (`packages/spec/src/shared/retired-key.ts`) on the schema whether or not it is
1081+
`.strict()`, and an entry in the shape's `*_RETIRED_KEY_GUIDANCE` map (see `data/mapping.zod.ts`) only for a
1082+
spelling the shape never declared, such as the retired key's old alias, where a tombstone has no property to
1083+
replace. The changeset is one of fourteen surfaces a retirement touches — follow the
1084+
`spec-property-retirement` skill (`.claude/skills/`) rather than reconstructing the kit, and note that a tombstone
1085+
keeps its liveness-ledger row while a key deleted without one loses it.
10851086
**A breaking changeset must also state its ADR-0087 disposition, in writing** — exactly one marker in the changeset
10861087
body, which also carries the PR's `Clause-②` line: `pnpm check:adr-0087-registration` reads the arm there. ⛔ The
10871088
categories are NOT copied here — the gate prints the full set when it fails.

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

Lines changed: 20 additions & 3 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)
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)
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**
@@ -903,14 +903,31 @@ unconditional strip would start deleting legal metadata the day the keys return.
903903
manifest declares (`engines.protocol`, ADR-0025) and the `@objectstack/spec`
904904
version the process actually runs. `floor < runtime` replays the FULL chain,
905905
retired entries included, before the strict parse — the artifact is the
906-
"consumer arriving late" D3 keeps every conversion forever for. `floor >=
906+
"consumer arriving late" D3 keeps every conversion forever for. ~~`floor >=
907907
runtime` replays nothing: the artifact claims the current or a newer surface,
908-
and the strict parse, tombstones included, stays the authority. That branch is
908+
and the strict parse, tombstones included, stays the authority.~~ — **amended
909+
2026-09-30** — `floor >= runtime` replays only the retired entries whose
910+
`retiredAfter` the floor does not exceed (verdict `'converted-retired-after'`),
911+
and nothing when no entry is that recent; every other entry meets the strict
912+
parse, tombstones included, as its authority. That branch is
909913
what makes the window *versioned rather than a blanket amnesty*, and it is the
910914
branch the M2 return needs. No declared range replays (an artifact of unknown
911915
age is old data at rest, and conversions only rewrite shapes they positively
912916
recognize); an unresolvable runtime version replays nothing, because amnesty
913917
rests on positive version evidence.
918+
919+
> **Amended (2026-09-30) — the window decides per entry.** Provenance: the
920+
> #20390 ruling, comment `5865890672` (director batch #235 item 1, letter A;
921+
> maintainer 「同意 A」), landed as `e956924e` (PR #20435). Every retired
922+
> conversion carries a required `retiredAfter`, the last published
923+
> `@objectstack/spec` whose authoring surface still accepted the old shape,
924+
> and the door replays an entry E when `floor < runtime` OR
925+
> `floor <= E.retiredAfter`. Why: `main` refuses keys the next release retires
926+
> while it still carries the last release's label, so the label-only
927+
> comparison read an artifact built by that last release as current and
928+
> refused it outright. The module docblock of
929+
> `packages/metadata-core/src/artifact-forward-conversion.ts` states the rule
930+
> and is its authority.
914931
- **⚠️ The shipped key is the DECLARED FLOOR, not the authored version.** The
915932
ruling says "authored `specVersion`"; what an artifact manifest actually
916933
carries is a protocol *range*, so the implementation keys off that range's

0 commit comments

Comments
 (0)