Skip to content

Commit 249b783

Browse files
committed
docs(governed): retirement route is retiredKey() on any shape; retiredAfter stamp; ADR-0087 window per entry
The retirement kit's route table and AGENTS.md Post-Task Checklist step 3 now prescribe a retiredKey() tombstone whether or not the schema is strict: on a closed shape a bare deletion is loud but reports only an unrecognized key, losing both the prescription and the tsc channel (retired-key.ts header). The strictObject guidance map is named only for a spelling the shape never declared, such as a retired key's old alias, where a tombstone has no property to replace (data/mapping.zod.ts). The retiredFromLoadPath checklist item gains the required retiredAfter stamp: tsc refuses its absence, a new retirement carries the current packages/spec label, and retired-after.census.test.ts pins each value. ADR-0087's artifact-window bullet said a floor at or above the runtime replays nothing; since e956924 the door replays every retired entry whose retiredAfter the floor does not exceed. Amended in the dated style. Lines paid by deleting narrative with another home: the orphan-leg history (orphans.mts header) and the withdrawn-example sentence. Claude-Session: https://claude.ai/code/session_01KTZmMfzVzjNvyaLyQ8mHvg Co-authored-by: Claude <noreply@anthropic.com>
1 parent 7a09eee commit 249b783

3 files changed

Lines changed: 38 additions & 22 deletions

File tree

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

Lines changed: 12 additions & 14 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,14 +201,15 @@ 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`)。
212+
门即如此。
215213
- [ ] **一步 D3 链**,在 `packages/spec/src/migrations/registry.ts` —— 把 id 加进
216214
`MIGRATIONS_BY_MAJOR[N].conversionIds`,扩写该步的 `rationale`。
217215
`conversion.toMajor` **必须等于**该步的 major。⚠ 没有东西直接断言「每个
@@ -239,7 +237,7 @@ conversion 是消费者跟的;D3 条目是升级方的 agent 读的。三个都
239237

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

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