From 37ca91baedef1a50d433412afb2057a168c7860d Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 22 Aug 2026 15:46:13 +0000 Subject: [PATCH 1/3] 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 Claude-Session: https://claude.ai/code/session_01MsbKEG4LtERSLaDrbehM3e --- .../skills/spec-property-retirement/SKILL.md | 8 +- .../retired-key-migrate-sentence.test.ts | 80 +++++++++++++++++++ scripts/check-cross-package-test-inputs.mjs | 5 ++ turbo.json | 3 +- 4 files changed, 94 insertions(+), 2 deletions(-) diff --git a/.claude/skills/spec-property-retirement/SKILL.md b/.claude/skills/spec-property-retirement/SKILL.md index ad791fa582..c35d163665 100644 --- a/.claude/skills/spec-property-retirement/SKILL.md +++ b/.claude/skills/spec-property-retirement/SKILL.md @@ -147,7 +147,13 @@ ratchet(#2978)会先开火, 2. `was removed in @objectstack/spec (#issue[, ADR-XXXX Dn])`。 3. 一个破折号从句讲**它为何惰性或错误** —— "it never had an effect"、"no renderer ever read it"。 4. 祈使句修复:改名写 "use ``" + "Rename the key; the value (…) is unchanged.";删除写 "Delete the key." + **真正生效的机制是什么**。 -5. ``Run `os migrate meta --from ` to rewrite it automatically.`` —— **仅当**有 conversion 重写 sources。消息不点名 conversion id;conversion 由 CLI 命令引用。 +5. ``Run `os migrate meta --from ` to list the mechanical edits for existing sources; apply them by hand.`` + —— 命令重放链、打印机械修改清单,从不写 source 文件(#9591 的 in-place codemod 落地前恒真)。 + 消息不点名 conversion id;conversion 由 CLI 命令引用。唯一允许的变体(按形状、不按站点): + conversion 只覆盖值的一部分时,两从句形点名覆盖的部分 —— ``Run `os migrate meta --from ` + to list the mechanical edits for the case; .`` + (样板:`ui/dashboard.zod.ts` `compareTo.offset`)。守这两个形状的 pin 人群含本文件: + `packages/spec/src/shared/retired-key-migrate-sentence.test.ts`。 这个字符串*就是*撞上它的人的迁移文档 —— 包括一次跳好几个 major、load-path conversion 已不再覆盖的那位。 diff --git a/packages/spec/src/shared/retired-key-migrate-sentence.test.ts b/packages/spec/src/shared/retired-key-migrate-sentence.test.ts index f946acadd3..591924f472 100644 --- a/packages/spec/src/shared/retired-key-migrate-sentence.test.ts +++ b/packages/spec/src/shared/retired-key-migrate-sentence.test.ts @@ -69,6 +69,25 @@ const HERE = path.dirname(url.fileURLToPath(import.meta.url)); const SPEC_SRC_ROOT = path.resolve(HERE, '..'); /** #7030: `packages/lint/src`, the one other corpus carrying this sentence. */ const LINT_SRC_ROOT = path.resolve(HERE, '../../../lint/src'); +/** + * [#10848] The population widens by EXACTLY ONE governed file (maintainer + * ruling 2026-08-22, deliberately not all of `.claude/**`): the retirement + * playbook every new tombstone's guidance string is authored from. It sat + * outside both corpora and prescribed the withdrawn sentence, so the skill + * taught authors to red this very pin — and a red pin over a skill-taught + * sentence invites weakening the PIN rather than the skill. It cannot ride + * the corpus walk: it is markdown (the walk yields `.ts` only), its `--from` + * operand is a placeholder like `` (never `\d+`), its sentences end at a + * code-span close (never at a string-literal quote), and `reconstruct()` + * would drop every markdown line that opens with `*`/`**`. So it is judged + * below as its own corpus: raw text, whitespace-normalised, with + * placeholder-aware anchors — the withdrawn-claim direction reuses + * `WITHDRAWN_CLAIM` verbatim. + */ +const RETIREMENT_SKILL_MD = path.resolve( + HERE, + '../../../../.claude/skills/spec-property-retirement/SKILL.md', +); /** One scanned corpus: a root directory, plus its own out-of-scope exemptions. */ interface Corpus { @@ -370,3 +389,64 @@ describe('`os migrate meta` sentences are the house sentence, across corpora (#6 expect(judgeMigrateSentences(commented, 'comments.zod.ts')).toHaveLength(0); }); }); + +/** + * [#10848] Markdown-corpus anchors for the retirement skill (see the + * `RETIREMENT_SKILL_MD` docblock). Same two legal shapes as + * `HOUSE_AT_MARKER`/`MIXED_AT_MARKER`, adapted on exactly three axes: the + * `--from` operand may be a placeholder (``, ``) as well as a literal + * major; the judged text is the whole file with runs of whitespace collapsed + * (markdown wraps sentences mid-clause); and "last sentence of its literal" + * becomes "last sentence of its double-backtick code span" (`.``), so prose + * cannot bury the command mid-span either. The marker requires the leading + * `Run` on purpose: the skill legitimately NAMES the command mid-prose + * (`migrate meta --from ` in §3's `retiredFromLoadPath` bullet) without + * prescribing a sentence — only taught sentence templates are judged. + */ +const SKILL_FROM_OPERAND = /(?:\d+|<[^>`]+>)/.source; +const SKILL_MARKER = new RegExp( + `Run \`os migrate meta --from ${SKILL_FROM_OPERAND}\``, + 'g', +); +const SKILL_HOUSE_AT_MARKER = new RegExp( + `^Run \`os migrate meta --from ${SKILL_FROM_OPERAND}\` to list the mechanical edits for existing sources; apply them by hand\\.\`\``, +); +const SKILL_MIXED_AT_MARKER = new RegExp( + `^Run \`os migrate meta --from ${SKILL_FROM_OPERAND}\` to list the mechanical edits for the [^;]+ case[^;]*; [^;]+\\.\`\``, +); + +describe('the spec-property-retirement skill agrees with this pin (#10848 — the one-file population widening)', () => { + const raw = fs.readFileSync(RETIREMENT_SKILL_MD, 'utf8'); + const flat = raw.replace(/\s+/g, ' '); + + it('every prescription sentence the skill teaches is house-form or MIXED two-clause', () => { + const bad = [...flat.matchAll(SKILL_MARKER)] + .filter((m) => { + const rest = flat.slice(m.index ?? 0); + return !(SKILL_HOUSE_AT_MARKER.test(rest) || SKILL_MIXED_AT_MARKER.test(rest)); + }) + .map((m) => flat.slice(m.index ?? 0, (m.index ?? 0) + 120)); + expect(bad, bad.join('\n')).toEqual([]); + }); + + it('anti-vacuity: the skill teaches BOTH shapes, so the scan judges at least two sites', () => { + // Convention 5 carries the house template and its one allowed variant + // (the partial-conversion two-clause shape). Zero or one marker means the + // skill stopped teaching a shape — or this scan went blind on the file. + const rests = [...flat.matchAll(SKILL_MARKER)].map((m) => flat.slice(m.index ?? 0)); + expect(rests.length).toBeGreaterThanOrEqual(2); + expect(rests.some((r) => SKILL_HOUSE_AT_MARKER.test(r))).toBe(true); + expect(rests.some((r) => SKILL_MIXED_AT_MARKER.test(r))).toBe(true); + }); + + it('[#9529] the withdrawn automatic-rewrite claim is absent from the skill, in every spelling', () => { + // Judged over the raw text rather than reconstruct(): a markdown line + // opening with `*`/`**` would be dropped as a comment line, hiding a + // claim. WITHDRAWN_CLAIM is English-only, so the skill's Chinese prose + // cannot fabricate a match; a hit is a real regression of the ruling. + const claims = [...flat.matchAll(WITHDRAWN_CLAIM)].map( + (m) => flat.slice(Math.max(0, (m.index ?? 0) - 60), (m.index ?? 0) + 60), + ); + expect(claims, claims.join('\n')).toEqual([]); + }); +}); diff --git a/scripts/check-cross-package-test-inputs.mjs b/scripts/check-cross-package-test-inputs.mjs index 60fbf72624..c8106e3f71 100644 --- a/scripts/check-cross-package-test-inputs.mjs +++ b/scripts/check-cross-package-test-inputs.mjs @@ -264,6 +264,11 @@ export const CROSS_PACKAGE_TEST_INPUTS = { // schema files it inventories, so the ledger IS an input to the ratchet. 'content/docs/api/error-catalog.mdx', 'docs/audits/2026-07-unknown-key-strictness-ledger.md', + // src/shared/retired-key-migrate-sentence.test.ts judges the ONE + // governed markdown file its population was widened by (#10848, + // maintainer-ruled): the retirement playbook that teaches authors the + // prescription sentence the pin holds. One file, not `.claude/**`. + '.claude/skills/spec-property-retirement/SKILL.md', ], heldBy: { // The two repo-wide `*.object.ts` walkers. Each seeds a recognised diff --git a/turbo.json b/turbo.json index 667339bcae..a42874d791 100644 --- a/turbo.json +++ b/turbo.json @@ -40,7 +40,8 @@ "$TURBO_ROOT$/packages/metadata-protocol/src/**", "$TURBO_ROOT$/packages/plugins/plugin-audit/src/**", "$TURBO_ROOT$/content/docs/api/error-catalog.mdx", - "$TURBO_ROOT$/docs/audits/2026-07-unknown-key-strictness-ledger.md" + "$TURBO_ROOT$/docs/audits/2026-07-unknown-key-strictness-ledger.md", + "$TURBO_ROOT$/.claude/skills/spec-property-retirement/SKILL.md" ] }, "@objectstack/core#test": { From 61adbc2c193fc7c3b965834f4dc42f1cb015995f Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 22 Aug 2026 16:18:01 +0000 Subject: [PATCH 2/3] 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 Claude-Session: https://claude.ai/code/session_01MsbKEG4LtERSLaDrbehM3e --- .github/workflows/ci.yml | 1 + .../spec/src/shared/retired-key-migrate-sentence.test.ts | 5 +---- scripts/pm/check-skill-line-ratchet.mjs | 5 ++++- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index da3b144937..d951b7022d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -170,6 +170,7 @@ jobs: - 'docs/audits/2026-07-unknown-key-strictness-ledger.md' - 'skills/objectstack-formula/**' - '.github/workflows/scaffold-e2e.yml' + - '.claude/skills/spec-property-retirement/SKILL.md' test: # Sharded 6-way BY PACKAGE: a core-touching PR ran the affected suite diff --git a/packages/spec/src/shared/retired-key-migrate-sentence.test.ts b/packages/spec/src/shared/retired-key-migrate-sentence.test.ts index 591924f472..88613e62fe 100644 --- a/packages/spec/src/shared/retired-key-migrate-sentence.test.ts +++ b/packages/spec/src/shared/retired-key-migrate-sentence.test.ts @@ -84,10 +84,7 @@ const LINT_SRC_ROOT = path.resolve(HERE, '../../../lint/src'); * placeholder-aware anchors — the withdrawn-claim direction reuses * `WITHDRAWN_CLAIM` verbatim. */ -const RETIREMENT_SKILL_MD = path.resolve( - HERE, - '../../../../.claude/skills/spec-property-retirement/SKILL.md', -); +const RETIREMENT_SKILL_MD = path.resolve(HERE, '../../../../.claude/skills/spec-property-retirement/SKILL.md'); /** One scanned corpus: a root directory, plus its own out-of-scope exemptions. */ interface Corpus { diff --git a/scripts/pm/check-skill-line-ratchet.mjs b/scripts/pm/check-skill-line-ratchet.mjs index ac29e415e7..5e2d26f32b 100644 --- a/scripts/pm/check-skill-line-ratchet.mjs +++ b/scripts/pm/check-skill-line-ratchet.mjs @@ -120,7 +120,10 @@ export const CEILINGS = new Map([ ['.claude/skills/checklist-test/SKILL.md', 232], ['.claude/skills/checklist-author/SKILL.md', 61], ['.claude/skills/dogfood-verification/SKILL.md', 155], - ['.claude/skills/spec-property-retirement/SKILL.md', 328], + // 328 → 334 (#10848): maintainer-ruled (2026-08-22, Option A) — convention 5 + // replaced with the pin's house sentence AND the pin docblock's one allowed + // variant shape carried into the skill, +6 lines within the card's budget. + ['.claude/skills/spec-property-retirement/SKILL.md', 334], // #9792: root AGENTS.md is the largest, most-read, most binding instruction // file in the repo and had no ceiling — the hole the oversized 39-line // read-layer clause (compacted by #9715) entered through. Set at its line From 9219437d91decb233efce1122b6cfb47925184aa Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 22 Aug 2026 16:52:04 +0000 Subject: [PATCH 3/3] 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 Claude-Session: https://claude.ai/code/session_01MsbKEG4LtERSLaDrbehM3e --- scripts/check-ci-filter-parity.mjs | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/scripts/check-ci-filter-parity.mjs b/scripts/check-ci-filter-parity.mjs index d92bf3135f..fe5c4ac0ac 100644 --- a/scripts/check-ci-filter-parity.mjs +++ b/scripts/check-ci-filter-parity.mjs @@ -563,11 +563,21 @@ export async function selfTest() { 'the checked-in `crosspkg` still names the same-root-different-file entry #10015 added', ); // The pre-#10015 list, as the measurement that motivated this gate: with the - // four roots removed, the ten declarations #10015 fixed go uncovered here. + // four roots removed, the ten declarations #10015 fixed go uncovered here — + // plus, since #10848, the one post-#10015 declaration none of those roots + // ever covered (the retirement skill's SKILL.md, a `.claude/` literal), so + // the rollback now uncovers eleven. This pin is judged over the LIVE + // declaration table on purpose: a declaration added under a root the + // rollback keeps leaves the count alone, one under a new root moves it and + // is recorded here by name. const preFix = judge(fixtureWorkflow({ core: real.filters?.core, crosspkg: ['scripts/**'] }), CROSS_PACKAGE_TEST_INPUTS); assert( - new Set(uncoveredGlobs(preFix)).size === 10, - `rolling \`crosspkg\` back to its pre-#10015 list uncovers exactly the ten -- got ${new Set(uncoveredGlobs(preFix)).size}`, + new Set(uncoveredGlobs(preFix)).size === 11, + `rolling \`crosspkg\` back to its pre-#10015 list uncovers the ten it fixed plus #10848's one -- got ${new Set(uncoveredGlobs(preFix)).size}`, + ); + assert( + uncoveredGlobs(preFix).includes('.claude/skills/spec-property-retirement/SKILL.md'), + `-- and the post-#10015 member is #10848's declaration, by name`, ); // ── (7) WIRING: the gate and its self-test really run in CI ────────────── @@ -593,7 +603,7 @@ export async function selfTest() { `same-root-different-file case observed failing and then covered by naming the file, a glob covered by ` + `\`core\`, one covered only by \`crosspkg\` and one covered by neither judged separately in one table, the ` + `stale-entry direction, seven refusals over subjects that could not be read, the checked-in ci.yml, the ` + - `pre-#10015 rollback uncovering exactly ten, and the CI wiring read out of lint.yml.`, + `pre-#10015 rollback uncovering the ten it fixed plus #10848's one, and the CI wiring read out of lint.yml.`, ); return 0; }