Skip to content

Commit 4c33bd4

Browse files
claude[bot]claude
andauthored
docs(skills): correct the retiredFromLoadPath jurisdiction and worked example in the retirement checklist (#17947)
Fixes #17895 Share 3 of 3 of #16864's correction. Share 1 (the `packages/spec` carriers) landed as PR #17888; share 2 is the ADR-0087 half, tracked as #17894 and left to its own PR — different file, different acceptance. **This PR touches no `packages/` path at all**: the whole diff is one file, `.claude/skills/spec-property-retirement/SKILL.md`, 12 insertions / 12 deletions. ## What the checklist taught, and why it was wrong The `retiredFromLoadPath: true` item asserted two things that are not true of this tree. **1. Jurisdiction.** It said that only `os migrate meta --from OLD_MAJOR` may apply a retired conversion — that setting the flag keeps the rewrite off every load path. Measured on `origin/main` at `84e6b05`, three non-test `includeRetired: true` literals replay retired entries deliberately: | seam | file | count | |:--|:--|:--| | stored-row rehydration (pinned, not offered) | `packages/spec/src/conversions/stored.ts` | 1 | | flow rehydration, automation engine | `packages/services/service-automation/src/engine.ts` | 1 | | artifact-ingestion door | `packages/metadata-core/src/artifact-forward-conversion.ts` | 1 | `StoredConversionOptions` is an `Omit` over `includeRetired`, so no caller of the first seam can turn it off. **2. Worked example.** It argued the default-flip rule with `field-required-notnull-explicit`, which the registry deliberately does not contain — `packages/spec/src/conversions/registry.ts` carries a tombstone at that spot reading `⛔ WITHDRAWN — there is deliberately NO field-required-notnull-explicit conversion in this registry`. A reader grepping the name met the checklist's worked example and the tombstone at the same time. ## What changed The corrected jurisdiction wording is **copied in substance from what PR #17888 already landed** in `packages/spec` (the `retiredFromLoadPath` docblock on `MetadataConversion` in `conversions/types.ts` and the `includeRetired` docblock in `conversions/apply.ts`) — deliberately not a third wording. The checklist translates register, not meaning. - The flag's jurisdiction is named as the authoring funnel `normalizeStackInput` **and nothing else**, and the item now says in as many words that it stops none of the three seams, naming each one. - The default-flip half is restated as the landed docblocks state it: soundness belongs to the **seam**, not to the entry or the flag — only a seam that can treat "this input predates the flip" as a fact may replay one, and a seam that cannot opts the id out through `excludeConversionIds`. - The worked example is now `app-hidden-to-unpublished` (`registry.ts` :6242), re-confirmed on the tree: `retiredFromLoadPath: true`, old and new shapes both legal and meaning different things, and its own docblock argues the rule in the same terms the removed entry did. It is also the live instance of the opt-out sentence above — `artifact-forward-conversion.ts` lists exactly that id in `DEFAULT_FLIPS_NOT_REPLAYED_HERE`. - One sentence records **why** the previous example was withdrawn: its docblock's "only `migrate meta` may apply it" claim was false of this tree, the artifact door applied it at boot, and the conversion was withdrawn as a result. Cited to `packages/spec/CHANGELOG.md`, whose entry carries the measurement. - The fixture-disjointness item lost its stale illustration, which named the same withdrawn conversion as if it still fired (see *Bounded in-place fix* below). ## Line budget — the dispatch's budget was wider than the ratchet's The dispatch allowed at most +2 net lines. **Measured, the real budget is 0**: `scripts/pm/check-skill-line-ratchet.mjs` pins this file at `337` and the file was at 337 lines, headroom 0. The item is therefore paid for entirely out of deletions, and the file lands at 337 again. ``` ✓ check-skill-line-ratchet: .claude/skills/spec-property-retirement/SKILL.md is 337 lines (ceiling 337; headroom 0). ✓ check-skill-line-ratchet: .claude/skills/spec-property-retirement/SKILL.md: widest table row is 326 bytes (pin 326; headroom 0). ``` Where the lines came from, per the rule that additions are paid by deleting content and re-wrap is not currency: | item | before | after | what was deleted | |:--|:--:|:--:|:--| | `retiredFromLoadPath: true` | 7 | 8 | the withdrawn worked example; the false `migrate meta` exclusivity sentence; the parenthetical on why a retired entry still exists (already carried by this section's own opening, which names `spec-changes.json`, the upgrade guide and the `spec_changes` MCP tool as the main channel) | | fixture disjointness | 6 | 5 | the illustration `(a new objects[].fields fixture may not carry a bare required: true, or the notNull conversion fires on it)` — false today, see below | | **file** | **337** | **337** | | No line was merged into another to free a line; every line of headroom above is a clause that was removed because it was false or because the same fact is stated elsewhere in the same section. Every line of the new item is under the gate's 120-byte cap (widest 113). ### Bounded in-place fix — the fixture item's illustration `packages/spec/src/conversions/registry.ts` has no conversion that reads `required: true` and writes `storage.notNull`; the only `notNull` occurrences in that file are inside the tombstone comment. The `required` key is touched by exactly one live entry, `field-conditionalRequired-to-requiredWhen`, which fires on `conditionalRequired`. So the illustration described the same withdrawn conversion as live. It sits inside the region this card declared (:209–:231), it is the same defect class, and the rule it illustrates (`before` stays minimal and avoids other entries' keys) survives intact — only the stale example is gone. ## Acceptance — both directions Run on the branch at `c4838a3`, against `.claude/skills/spec-property-retirement/SKILL.md` unless stated: | check | expected | measured | |:--|:--:|:--:| | `field-required-notnull-explicit` in this file | 0 | **0** | | literal control: `retiredFromLoadPath` in this file | > 0 | **3** | | `app-hidden-to-unpublished` in this file | > 0 | **1** | | `app-hidden-to-unpublished` in `packages/spec/src/conversions/registry.ts` | > 0 | **1** | | `normalizeStackInput` in this file | > 0 | **1** | | `applyConversionsToStoredItem` in this file | > 0 | **1** | | `applyArtifactForwardConversions` in this file | > 0 | **1** | | `flow rehydration` in this file | > 0 | **1** | | `includeRetired` in this file | > 0 | **1** | | negative control: a conversion id that does not exist | 0 | **0** | Seam re-measurement on `origin/main`, `git grep -c 'includeRetired: true'` over the three files: **1 / 1 / 1**. No fourth non-test seam appeared; the other hits in that grep are CHANGELOG prose and the `types.ts` docblock. ## Gates Derived with `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` (no paths — the tool takes its own change set from the merge base), all 17 run, then reconciled with `--ran` carrying each exit code: ``` ✓ dispatch-gates --ran: 17 derived famil(ies) accounted for — 17 run, 0 NOT-MEASURED (a DERIVED zero — all 17 recorded an exit code and none of them is 3). ``` All 17 exited 0. One needed a second pass: `check:doc-formula-expressions` first exited **3 — `PREREQUISITE NOT MET`, not a finding** (`@objectstack/formula` and `@objectstack/lint` unbuilt); after `turbo run build --filter=@objectstack/formula --filter=@objectstack/lint` through the shared verify lock it exited 0. **Repo-wide `pnpm lint` is CI's run, and the narrowing here is measured rather than assumed.** ① Population, read from eslint's own config: `eslint.config.mjs` declares no `files:` glob matching `.md` — every block is `{ts,tsx,mts,cts,js,jsx,mjs,cjs}`. ② File count, from `--format json` on the one changed file: 1 file, 0 errors, message `File ignored because no matching configuration was supplied`. ③ Invariance: the diff is one Markdown file and nothing in it enters any eslint program, so no verdict on any untouched file can move. `check:skills-token-ratchet` covers the **published** `skills/` bundle (34 authored files) and does not carry `.claude/skills/`, so there is no token reading to pair with the line reading here; it was run anyway and exited 0. ## Changeset `skip-changeset`, applied as a label. `.claude/**` publishes nothing: it is not in any package's `files[]`, ships in no tarball, and no released symbol moves. ## A measurement the dispatch did not carry While confirming the seams I found that the artifact-ingestion door has changed since the card was written. `artifact-forward-conversion.ts` now passes `excludeConversionIds: DEFAULT_FLIPS_NOT_REPLAYED_HERE`, a module-local list naming `app-hidden-to-unpublished` — landed by PR #17899 at `134b410`. The card that reported it, #17885, went to `completed` on 2026-09-12, before this card was graded. The door still opens the retired window; it refuses the default-flip class by id inside it. That does not weaken the card — the flag's jurisdiction is unchanged and the checklist was still teaching the false sentence — but it does change what the corrected item has to say, so the item now teaches the opt-out duty rather than implying the flag confines a default flip by itself. Leaving it out would have let the new worked example read as "a default flip the flag holds back", which is the same failure mode this card exists to end. Not touching #17885 itself, per the dispatch. ## 维护者速读(草稿) **改了什么** —— 只改一份内部 agent 手册 `.claude/skills/spec-property-retirement/SKILL.md` 的一条清单项(外加同一区域里一句同类的过期举例)。不碰任何 `packages/` 代码、不碰协议、不发布任何东西。 **为什么改** —— 这份 checklist 是每个做「属性退役」的 agent 照着执行的作业单。它教的一句话是假的:它说设了 `retiredFromLoadPath: true`,改写就只会发生在 `os migrate meta` 里。实际上有三处运行期入口故意重放退役条目。这条假话已经造成过一次已发布的事故 —— 一个 conversion 在 boot 时把 NOT NULL 写到作者声明为可空的字段上,最后以撤销那个 conversion 收场。而 checklist 至今仍在教同一句话,并且仍拿那个已被撤销的 conversion 当样例。这一改把它拉回到 `packages/spec` 里已经落地的说法上,并换成树上还活着的样例。 **风险与代价(含回滚)** —— 风险极低:纯文档面,不进 npm 包,不影响运行时,也不影响任何构建产物。行数棘轮 337/337 原地不动,没有抬任何上限。回滚就是 revert 这一个 commit,无任何后续清理。唯一的取舍是行数预算为 0,所以为了写下正确的说法,删掉了三处内容:已撤销的样例、那句假的管辖权断言,以及一句在本节开头已经说过的括注。 **席位意见** —— **你要做的** —— 这是 governed 面(`.claude/**`),按 Prime Directive #14 只能由维护者手合。PR 停在 draft,席位不会转 ready、不入队、不挂 auto-merge。请确认两件事:① 新写的管辖权句子与 `packages/spec` 里已落地的 docblock 说的是同一件事(而不是第三种说法);② 为了守住 0 行预算而删掉的三处内容,你认可它们的确可删。 --- _Generated by [Claude Code](https://claude.ai/code/session_01DAcomhvR9kKizeYgg89Vo8)_ Co-authored-by: Claude <noreply@anthropic.com>
1 parent e5272b7 commit 4c33bd4

1 file changed

Lines changed: 12 additions & 12 deletions

File tree

  • .claude/skills/spec-property-retirement

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

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -206,25 +206,25 @@ conversion 是消费者跟的。两个都要写。
206206
从它解析归属 —— 那个职责移给了上面的条目。#5898 起这对**每个**消费者都成立:门
207207
(c) 的 *aged-out tombstone* 证明曾是最后一个叶匹配者,现在也读同一张精确键
208208
表,再没有任何规则从 `surface` 解析归属。
209-
- [ ] **`retiredFromLoadPath: true`** —— 退役恒真。两种论证,不可互换:对*改名*它意
210-
味着「没有 alias 窗口,故意的」(拒绝由墓碑负责;条目存在是为了
211-
`spec-changes.json``os migrate meta` 仍携带它);对**默认值翻转**它承重正
212-
确性 —— 自动应用 `field-required-notnull-explicit` 的 loader 会把 NOT NULL 盖
213-
到 17 时代编写的 `required: true` 上,静默恢复 ADR-0113 删掉的三重绑定。只有
214-
`migrate meta --from <old>` 可以应用翻转 —— 在那里「这份 source 早于拆分」是
215-
事实而不是猜测。
209+
- [ ] **`retiredFromLoadPath: true`** —— 退役恒真,但管辖权只有 authoring 漏斗
210+
`normalizeStackInput`;三处 data-at-rest seam 以 `includeRetired: true` 故意重放退役
211+
条目,它**一处也拦不住**:`applyConversionsToStoredItem`(钉死)、automation
212+
engine 的 flow rehydration、`applyArtifactForwardConversions`。对*改名*它意味着
213+
「没有 alias 窗口,故意的」;对**默认值翻转**,只有确知输入早于翻转的 seam 才可重
214+
放,其余按 id 退订 `excludeConversionIds` —— `app-hidden-to-unpublished` 在 artifact
215+
门即如此。上一版样例栽在这:它教「只有 migrate meta 能应用翻转」,而 boot 时照样
216+
应用,该 conversion 已撤(`packages/spec/CHANGELOG.md`)。
216217
- [ ] **一步 D3 链**,在 `packages/spec/src/migrations/registry.ts` —— 把 id 加进
217218
`MIGRATIONS_BY_MAJOR[N].conversionIds`,扩写该步的 `rationale`
218219
`conversion.toMajor` **必须等于**该步的 major。⚠ 没有东西直接断言「每个
219220
conversion 都接进了某一步」,拼错的 id 在 replay 时被**静默跳过**;
220221
chain-replay 测试抓得到它,只因为没接线的 fixture 永远到不了自己的 `after`
221222
所以把那个测试的失败读作「没接线」,不是「transform 坏了」。
222223
- [ ] **fixture 必须不相交 —— 两重。** 每个 fixture 都被整张表 replay,必须恰好等于
223-
自己的 `after`,每条 notice 都归属自己的 id。`before` 保持最小、避开其它条目
224-
的键(新的 `objects[].fields` fixture 不许带裸 `required: true`,否则 notNull
225-
conversion 在它上面开火)。第二重容易漏:`retiredFromLoadPath` 的 fixture 还必
226-
须不被任何 *live-window* conversion 碰到,因为另有测试断言它以零 notice 走过
227-
默认加载路径。这条不相交契约正是逼出同 major 吸收(§0)的东西。
224+
自己的 `after`,每条 notice 都归属自己的 id。`before` 保持最小、避开其它条目的
225+
键。第二重容易漏:`retiredFromLoadPath` 的 fixture 还必须不被任何 *live-window*
226+
conversion 碰到,因为另有测试断言它以零 notice 走过默认加载路径。这条不相交契
227+
约正是逼出同 major 吸收(§0)的东西。
228228
- [ ] **幂等靠构造,不靠测试。** 没有测试把 conversion replay 两遍。`stripKeys`
229229
除天然幂等(`if (!(key in next)) continue`),`renameKey` 拒绝覆盖已存在的
230230
canonical 值;默认值翻转**不是**幂等安全的,靠它自己的守卫加

0 commit comments

Comments
 (0)