Skip to content

Commit 147f9cd

Browse files
os-zhuangclaude
andauthored
fix(skills): teach the house migrate-sentence in spec-property-retirement and widen the pin's population by that one file (#11092)
* fix(skills): teach the house migrate-sentence in spec-property-retirement and widen the pin's population by that one file (#10848) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MsbKEG4LtERSLaDrbehM3e * fix(gates): declare the pin's new cross-package input and pay the forced gate tolls (#10848) - single-line the resolve() so the cross-package scanner can name the read - ci.yml crosspkg parity entry for the declared glob (check:ci-filter-parity) - skill-line ratchet ceiling 328 -> 334, maintainer-ruled 2026-08-22 Option A Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MsbKEG4LtERSLaDrbehM3e * fix(gates): move check-ci-filter-parity's rollback pin to the post-#10848 measurement (10 -> 11, new member pinned by name) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MsbKEG4LtERSLaDrbehM3e --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent d29e271 commit 147f9cd

7 files changed

Lines changed: 110 additions & 7 deletions

File tree

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

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,13 @@ ratchet(#2978)会先开火,
147147
2. `was removed in @objectstack/spec <version> (#issue[, ADR-XXXX Dn])`
148148
3. 一个破折号从句讲**它为何惰性或错误** —— "it never had an effect"、"no renderer ever read it"。
149149
4. 祈使句修复:改名写 "use `<replacement>`" + "Rename the key; the value (…) is unchanged.";删除写 "Delete the key." + **真正生效的机制是什么**
150-
5. ``Run `os migrate meta --from <N-1>` to rewrite it automatically.`` —— **仅当**有 conversion 重写 sources。消息不点名 conversion id;conversion 由 CLI 命令引用。
150+
5. ``Run `os migrate meta --from <N-1>` to list the mechanical edits for existing sources; apply them by hand.``
151+
—— 命令重放链、打印机械修改清单,从不写 source 文件(#9591 的 in-place codemod 落地前恒真)。
152+
消息不点名 conversion id;conversion 由 CLI 命令引用。唯一允许的变体(按形状、不按站点):
153+
conversion 只覆盖值的一部分时,两从句形点名覆盖的部分 —— ``Run `os migrate meta --from <N-1>`
154+
to list the mechanical edits for the <X> case; <what the tool does with the rest>.``
155+
(样板:`ui/dashboard.zod.ts` `compareTo.offset`)。守这两个形状的 pin 人群含本文件:
156+
`packages/spec/src/shared/retired-key-migrate-sentence.test.ts`
151157

152158
这个字符串*就是*撞上它的人的迁移文档 —— 包括一次跳好几个 major、load-path
153159
conversion 已不再覆盖的那位。

.github/workflows/ci.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,7 @@ jobs:
170170
- 'docs/audits/2026-07-unknown-key-strictness-ledger.md'
171171
- 'skills/objectstack-formula/**'
172172
- '.github/workflows/scaffold-e2e.yml'
173+
- '.claude/skills/spec-property-retirement/SKILL.md'
173174
174175
test:
175176
# Sharded 6-way BY PACKAGE: a core-touching PR ran the affected suite

packages/spec/src/shared/retired-key-migrate-sentence.test.ts

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,22 @@ const HERE = path.dirname(url.fileURLToPath(import.meta.url));
6969
const SPEC_SRC_ROOT = path.resolve(HERE, '..');
7070
/** #7030: `packages/lint/src`, the one other corpus carrying this sentence. */
7171
const LINT_SRC_ROOT = path.resolve(HERE, '../../../lint/src');
72+
/**
73+
* [#10848] The population widens by EXACTLY ONE governed file (maintainer
74+
* ruling 2026-08-22, deliberately not all of `.claude/**`): the retirement
75+
* playbook every new tombstone's guidance string is authored from. It sat
76+
* outside both corpora and prescribed the withdrawn sentence, so the skill
77+
* taught authors to red this very pin — and a red pin over a skill-taught
78+
* sentence invites weakening the PIN rather than the skill. It cannot ride
79+
* the corpus walk: it is markdown (the walk yields `.ts` only), its `--from`
80+
* operand is a placeholder like `<N-1>` (never `\d+`), its sentences end at a
81+
* code-span close (never at a string-literal quote), and `reconstruct()`
82+
* would drop every markdown line that opens with `*`/`**`. So it is judged
83+
* below as its own corpus: raw text, whitespace-normalised, with
84+
* placeholder-aware anchors — the withdrawn-claim direction reuses
85+
* `WITHDRAWN_CLAIM` verbatim.
86+
*/
87+
const RETIREMENT_SKILL_MD = path.resolve(HERE, '../../../../.claude/skills/spec-property-retirement/SKILL.md');
7288

7389
/** One scanned corpus: a root directory, plus its own out-of-scope exemptions. */
7490
interface Corpus {
@@ -370,3 +386,64 @@ describe('`os migrate meta` sentences are the house sentence, across corpora (#6
370386
expect(judgeMigrateSentences(commented, 'comments.zod.ts')).toHaveLength(0);
371387
});
372388
});
389+
390+
/**
391+
* [#10848] Markdown-corpus anchors for the retirement skill (see the
392+
* `RETIREMENT_SKILL_MD` docblock). Same two legal shapes as
393+
* `HOUSE_AT_MARKER`/`MIXED_AT_MARKER`, adapted on exactly three axes: the
394+
* `--from` operand may be a placeholder (`<N-1>`, `<N>`) as well as a literal
395+
* major; the judged text is the whole file with runs of whitespace collapsed
396+
* (markdown wraps sentences mid-clause); and "last sentence of its literal"
397+
* becomes "last sentence of its double-backtick code span" (`.``), so prose
398+
* cannot bury the command mid-span either. The marker requires the leading
399+
* `Run` on purpose: the skill legitimately NAMES the command mid-prose
400+
* (`migrate meta --from <old>` in §3's `retiredFromLoadPath` bullet) without
401+
* prescribing a sentence — only taught sentence templates are judged.
402+
*/
403+
const SKILL_FROM_OPERAND = /(?:\d+|<[^>`]+>)/.source;
404+
const SKILL_MARKER = new RegExp(
405+
`Run \`os migrate meta --from ${SKILL_FROM_OPERAND}\``,
406+
'g',
407+
);
408+
const SKILL_HOUSE_AT_MARKER = new RegExp(
409+
`^Run \`os migrate meta --from ${SKILL_FROM_OPERAND}\` to list the mechanical edits for existing sources; apply them by hand\\.\`\``,
410+
);
411+
const SKILL_MIXED_AT_MARKER = new RegExp(
412+
`^Run \`os migrate meta --from ${SKILL_FROM_OPERAND}\` to list the mechanical edits for the [^;]+ case[^;]*; [^;]+\\.\`\``,
413+
);
414+
415+
describe('the spec-property-retirement skill agrees with this pin (#10848 — the one-file population widening)', () => {
416+
const raw = fs.readFileSync(RETIREMENT_SKILL_MD, 'utf8');
417+
const flat = raw.replace(/\s+/g, ' ');
418+
419+
it('every prescription sentence the skill teaches is house-form or MIXED two-clause', () => {
420+
const bad = [...flat.matchAll(SKILL_MARKER)]
421+
.filter((m) => {
422+
const rest = flat.slice(m.index ?? 0);
423+
return !(SKILL_HOUSE_AT_MARKER.test(rest) || SKILL_MIXED_AT_MARKER.test(rest));
424+
})
425+
.map((m) => flat.slice(m.index ?? 0, (m.index ?? 0) + 120));
426+
expect(bad, bad.join('\n')).toEqual([]);
427+
});
428+
429+
it('anti-vacuity: the skill teaches BOTH shapes, so the scan judges at least two sites', () => {
430+
// Convention 5 carries the house template and its one allowed variant
431+
// (the partial-conversion two-clause shape). Zero or one marker means the
432+
// skill stopped teaching a shape — or this scan went blind on the file.
433+
const rests = [...flat.matchAll(SKILL_MARKER)].map((m) => flat.slice(m.index ?? 0));
434+
expect(rests.length).toBeGreaterThanOrEqual(2);
435+
expect(rests.some((r) => SKILL_HOUSE_AT_MARKER.test(r))).toBe(true);
436+
expect(rests.some((r) => SKILL_MIXED_AT_MARKER.test(r))).toBe(true);
437+
});
438+
439+
it('[#9529] the withdrawn automatic-rewrite claim is absent from the skill, in every spelling', () => {
440+
// Judged over the raw text rather than reconstruct(): a markdown line
441+
// opening with `*`/`**` would be dropped as a comment line, hiding a
442+
// claim. WITHDRAWN_CLAIM is English-only, so the skill's Chinese prose
443+
// cannot fabricate a match; a hit is a real regression of the ruling.
444+
const claims = [...flat.matchAll(WITHDRAWN_CLAIM)].map(
445+
(m) => flat.slice(Math.max(0, (m.index ?? 0) - 60), (m.index ?? 0) + 60),
446+
);
447+
expect(claims, claims.join('\n')).toEqual([]);
448+
});
449+
});

scripts/check-ci-filter-parity.mjs

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -563,11 +563,21 @@ export async function selfTest() {
563563
'the checked-in `crosspkg` still names the same-root-different-file entry #10015 added',
564564
);
565565
// The pre-#10015 list, as the measurement that motivated this gate: with the
566-
// four roots removed, the ten declarations #10015 fixed go uncovered here.
566+
// four roots removed, the ten declarations #10015 fixed go uncovered here —
567+
// plus, since #10848, the one post-#10015 declaration none of those roots
568+
// ever covered (the retirement skill's SKILL.md, a `.claude/` literal), so
569+
// the rollback now uncovers eleven. This pin is judged over the LIVE
570+
// declaration table on purpose: a declaration added under a root the
571+
// rollback keeps leaves the count alone, one under a new root moves it and
572+
// is recorded here by name.
567573
const preFix = judge(fixtureWorkflow({ core: real.filters?.core, crosspkg: ['scripts/**'] }), CROSS_PACKAGE_TEST_INPUTS);
568574
assert(
569-
new Set(uncoveredGlobs(preFix)).size === 10,
570-
`rolling \`crosspkg\` back to its pre-#10015 list uncovers exactly the ten -- got ${new Set(uncoveredGlobs(preFix)).size}`,
575+
new Set(uncoveredGlobs(preFix)).size === 11,
576+
`rolling \`crosspkg\` back to its pre-#10015 list uncovers the ten it fixed plus #10848's one -- got ${new Set(uncoveredGlobs(preFix)).size}`,
577+
);
578+
assert(
579+
uncoveredGlobs(preFix).includes('.claude/skills/spec-property-retirement/SKILL.md'),
580+
`-- and the post-#10015 member is #10848's declaration, by name`,
571581
);
572582

573583
// ── (7) WIRING: the gate and its self-test really run in CI ──────────────
@@ -593,7 +603,7 @@ export async function selfTest() {
593603
`same-root-different-file case observed failing and then covered by naming the file, a glob covered by ` +
594604
`\`core\`, one covered only by \`crosspkg\` and one covered by neither judged separately in one table, the ` +
595605
`stale-entry direction, seven refusals over subjects that could not be read, the checked-in ci.yml, the ` +
596-
`pre-#10015 rollback uncovering exactly ten, and the CI wiring read out of lint.yml.`,
606+
`pre-#10015 rollback uncovering the ten it fixed plus #10848's one, and the CI wiring read out of lint.yml.`,
597607
);
598608
return 0;
599609
}

scripts/check-cross-package-test-inputs.mjs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -264,6 +264,11 @@ export const CROSS_PACKAGE_TEST_INPUTS = {
264264
// schema files it inventories, so the ledger IS an input to the ratchet.
265265
'content/docs/api/error-catalog.mdx',
266266
'docs/audits/2026-07-unknown-key-strictness-ledger.md',
267+
// src/shared/retired-key-migrate-sentence.test.ts judges the ONE
268+
// governed markdown file its population was widened by (#10848,
269+
// maintainer-ruled): the retirement playbook that teaches authors the
270+
// prescription sentence the pin holds. One file, not `.claude/**`.
271+
'.claude/skills/spec-property-retirement/SKILL.md',
267272
],
268273
heldBy: {
269274
// The two repo-wide `*.object.ts` walkers. Each seeds a recognised

scripts/pm/check-skill-line-ratchet.mjs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,10 @@ export const CEILINGS = new Map([
120120
['.claude/skills/checklist-test/SKILL.md', 232],
121121
['.claude/skills/checklist-author/SKILL.md', 61],
122122
['.claude/skills/dogfood-verification/SKILL.md', 155],
123-
['.claude/skills/spec-property-retirement/SKILL.md', 328],
123+
// 328 → 334 (#10848): maintainer-ruled (2026-08-22, Option A) — convention 5
124+
// replaced with the pin's house sentence AND the pin docblock's one allowed
125+
// variant shape carried into the skill, +6 lines within the card's budget.
126+
['.claude/skills/spec-property-retirement/SKILL.md', 334],
124127
// #9792: root AGENTS.md is the largest, most-read, most binding instruction
125128
// file in the repo and had no ceiling — the hole the oversized 39-line
126129
// read-layer clause (compacted by #9715) entered through. Set at its line

turbo.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,8 @@
4040
"$TURBO_ROOT$/packages/metadata-protocol/src/**",
4141
"$TURBO_ROOT$/packages/plugins/plugin-audit/src/**",
4242
"$TURBO_ROOT$/content/docs/api/error-catalog.mdx",
43-
"$TURBO_ROOT$/docs/audits/2026-07-unknown-key-strictness-ledger.md"
43+
"$TURBO_ROOT$/docs/audits/2026-07-unknown-key-strictness-ledger.md",
44+
"$TURBO_ROOT$/.claude/skills/spec-property-retirement/SKILL.md"
4445
]
4546
},
4647
"@objectstack/core#test": {

0 commit comments

Comments
 (0)