docs(skills): correct the retiredFromLoadPath jurisdiction and worked example in the retirement checklist - #17947
Conversation
…irement checklist The `retiredFromLoadPath: true` checklist item taught that only `os migrate meta --from <old>` may apply a retired conversion, and used a worked example that no longer exists in the registry. Both halves are corrected in place, at zero net lines: - Jurisdiction: the flag governs the authoring funnel `normalizeStackInput` and nothing else, and it stops none of the three data-at-rest seams that replay retired entries with `includeRetired: true` — stored-row rehydration (pinned), automation-engine flow rehydration, the artifact-ingestion door. Wording follows the docblocks already landed in `packages/spec`. - Default flips: soundness belongs to the seam, not to the flag; a seam that cannot treat "this input predates the flip" as a fact opts the id out via `excludeConversionIds`. - Worked example replaced by `app-hidden-to-unpublished`, which exists on the tree and is exactly that case. - One sentence records why the previous example was withdrawn. - The fixture-disjointness item loses its stale illustration, which named the same withdrawn conversion as if it still fired. Claude-Session: https://claude.ai/code/session_01DAcomhvR9kKizeYgg89Vo8 Co-authored-by: Claude <noreply@anthropic.com>
Acceptance notesCarried as a comment rather than appended to the body: a bare REST Noted, not filed — one clause in the same section that points the same way, left alone on purpose: The idempotency item ends It is not false, so it does not meet the bar for a card (no reproducible defect, no declared-contract violation, no metadata-authoring trap), and the zero-line budget on this file left no room to restate it. Carrier: this PR's own review, and the next edit to this item — whoever next touches the Two facts recorded so the next author does not re-derive them:
Generated by Claude Code |
Contract reviewHead: ① derived judgments: the diff makes three falsifiable claims about Implemented-by: claude/issue-17895-retirement-checklist-jurisdiction (mode:subagent) Verdict: PASS. Governed surface ( Generated by Claude Code |
维护者速读改了什么 —— 内部退役 playbook 为什么改 —— 旧句已经造成过一次已发布事故(boot 时把 NOT NULL 盖到作者写为可空的字段上,以撤销那个 conversion 收场),而 playbook 还在教下一位退役作者同一句话。这是 #16864 更正的第三份(spec 侧 PR #17888 已合,ADR 侧 #17894 另卡)。 风险与代价(含回滚) —— 纯文档面,不动代码;回滚 = revert 单文件 12 行。dev 顺手删了相邻 fixture 项里一条也引用旧样例的过期插图(同区域、同类、有证据)。 席位意见 —— 收。本席在 head 你要做的(一个动作) —— 受管面( Generated by Claude Code |
Fixes #17895
Share 3 of 3 of #16864's correction. Share 1 (the
packages/speccarriers) 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 nopackages/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: trueitem asserted two things that are not true of this tree.1. Jurisdiction. It said that only
os migrate meta --from OLD_MAJORmay apply a retired conversion — that setting the flag keeps the rewrite off every load path. Measured onorigin/mainat84e6b05, three non-testincludeRetired: trueliterals replay retired entries deliberately:packages/spec/src/conversions/stored.tspackages/services/service-automation/src/engine.tspackages/metadata-core/src/artifact-forward-conversion.tsStoredConversionOptionsis anOmitoverincludeRetired, 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.tscarries 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(theretiredFromLoadPathdocblock onMetadataConversioninconversions/types.tsand theincludeRetireddocblock inconversions/apply.ts) — deliberately not a third wording. The checklist translates register, not meaning.normalizeStackInputand nothing else, and the item now says in as many words that it stops none of the three seams, naming each one.excludeConversionIds.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.tslists exactly that id inDEFAULT_FLIPS_NOT_REPLAYED_HERE.migrate metamay apply it" claim was false of this tree, the artifact door applied it at boot, and the conversion was withdrawn as a result. Cited topackages/spec/CHANGELOG.md, whose entry carries the measurement.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.mjspins this file at337and 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.Where the lines came from, per the rule that additions are paid by deleting content and re-wrap is not currency:
retiredFromLoadPath: truemigrate metaexclusivity sentence; the parenthetical on why a retired entry still exists (already carried by this section's own opening, which namesspec-changes.json, the upgrade guide and thespec_changesMCP tool as the main channel)(a new objects[].fields fixture may not carry a bare required: true, or the notNull conversion fires on it)— false today, see belowNo 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.tshas no conversion that readsrequired: trueand writesstorage.notNull; the onlynotNulloccurrences in that file are inside the tombstone comment. Therequiredkey is touched by exactly one live entry,field-conditionalRequired-to-requiredWhen, which fires onconditionalRequired. 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 (beforestays 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.mdunless stated:field-required-notnull-explicitin this fileretiredFromLoadPathin this fileapp-hidden-to-unpublishedin this fileapp-hidden-to-unpublishedinpackages/spec/src/conversions/registry.tsnormalizeStackInputin this fileapplyConversionsToStoredItemin this fileapplyArtifactForwardConversionsin this fileflow rehydrationin this fileincludeRetiredin this fileSeam 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 thetypes.tsdocblock.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--rancarrying each exit code:All 17 exited 0. One needed a second pass:
check:doc-formula-expressionsfirst exited 3 —PREREQUISITE NOT MET, not a finding (@objectstack/formulaand@objectstack/lintunbuilt); afterturbo run build --filter=@objectstack/formula --filter=@objectstack/lintthrough the shared verify lock it exited 0.Repo-wide
pnpm lintis CI's run, and the narrowing here is measured rather than assumed. ① Population, read from eslint's own config:eslint.config.mjsdeclares nofiles:glob matching.md— every block is{ts,tsx,mts,cts,js,jsx,mjs,cjs}. ② File count, from--format jsonon the one changed file: 1 file, 0 errors, messageFile 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-ratchetcovers the publishedskills/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'sfiles[], 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.tsnow passesexcludeConversionIds: DEFAULT_FLIPS_NOT_REPLAYED_HERE, a module-local list namingapp-hidden-to-unpublished— landed by PR #17899 at134b410. The card that reported it, #17885, went tocompletedon 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