Commit 4c33bd4
docs(skills): correct the
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>retiredFromLoadPath jurisdiction and worked example in the retirement checklist (#17947)1 parent e5272b7 commit 4c33bd4
1 file changed
Lines changed: 12 additions & 12 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
206 | 206 | | |
207 | 207 | | |
208 | 208 | | |
209 | | - | |
210 | | - | |
211 | | - | |
212 | | - | |
213 | | - | |
214 | | - | |
215 | | - | |
| 209 | + | |
| 210 | + | |
| 211 | + | |
| 212 | + | |
| 213 | + | |
| 214 | + | |
| 215 | + | |
| 216 | + | |
216 | 217 | | |
217 | 218 | | |
218 | 219 | | |
219 | 220 | | |
220 | 221 | | |
221 | 222 | | |
222 | 223 | | |
223 | | - | |
224 | | - | |
225 | | - | |
226 | | - | |
227 | | - | |
| 224 | + | |
| 225 | + | |
| 226 | + | |
| 227 | + | |
228 | 228 | | |
229 | 229 | | |
230 | 230 | | |
| |||
0 commit comments