Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion .claude/skills/spec-property-retirement/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,13 @@ ratchet(#2978)会先开火,
2. `was removed in @objectstack/spec <version> (#issue[, ADR-XXXX Dn])`。
3. 一个破折号从句讲**它为何惰性或错误** —— "it never had an effect"、"no renderer ever read it"。
4. 祈使句修复:改名写 "use `<replacement>`" + "Rename the key; the value (…) is unchanged.";删除写 "Delete the key." + **真正生效的机制是什么**。
5. ``Run `os migrate meta --from <N-1>` to rewrite it automatically.`` —— **仅当**有 conversion 重写 sources。消息不点名 conversion id;conversion 由 CLI 命令引用。
5. ``Run `os migrate meta --from <N-1>` 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 <N-1>`
to list the mechanical edits for the <X> case; <what the tool does with the rest>.``
(样板:`ui/dashboard.zod.ts` `compareTo.offset`)。守这两个形状的 pin 人群含本文件:
`packages/spec/src/shared/retired-key-migrate-sentence.test.ts`。

这个字符串*就是*撞上它的人的迁移文档 —— 包括一次跳好几个 major、load-path
conversion 已不再覆盖的那位。
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
77 changes: 77 additions & 0 deletions packages/spec/src/shared/retired-key-migrate-sentence.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,22 @@ 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 `<N-1>` (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 {
Expand Down Expand Up @@ -370,3 +386,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 (`<N-1>`, `<N>`) 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 <old>` 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([]);
});
});
18 changes: 14 additions & 4 deletions scripts/check-ci-filter-parity.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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 ──────────────
Expand All @@ -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;
}
Expand Down
5 changes: 5 additions & 0 deletions scripts/check-cross-package-test-inputs.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
5 changes: 4 additions & 1 deletion scripts/pm/check-skill-line-ratchet.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 2 additions & 1 deletion turbo.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
Loading