Skip to content

feat(tooling): SKILL.md compatibility 声明与仓库实际大版本对账门禁 - #6258

Merged
os-zhuang merged 1 commit into
mainfrom
claude/issue-5331-skill-compatibility-gate
Aug 7, 2026
Merged

feat(tooling): SKILL.md compatibility 声明与仓库实际大版本对账门禁#6258
os-zhuang merged 1 commit into
mainfrom
claude/issue-5331-skill-compatibility-gate

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Fixes #5331

背景

skills/*/SKILL.md frontmatter 的 compatibility: 行是一份 skill 唯一的自述适用范围,并且随 npx skills add objectstack-ai/objectstack/skills 原样发给第三方。此前没有任何门看守它:

  • check:skill-docs / check:skill-refs 只比对生成物packages/spec/src;
  • check:skill-examples 只 typecheck 打了 os:check 的代码块。

三道门对着一句错的版本声明全绿。实证即 #5245:仓库已在 17.0.0-rc.2(七个 App 键、DriverCapabilities 31 位、restServer.openApi31、plugin-runtime 家族在 17 里已是墓碑或 TS2305),而十份 SKILL.md 里九份仍写 Requires @objectstack/spec 16.x —— 教 17 的能力、自称 16 兼容,静默漂移了整个大版本周期。

#5245 手工改正了值;本单修的是产生漂移的机制。这正是 #5331#5245 拆出时的分工。

先核实了「实际落地的措辞」,而不是照原单想象

#5245 给了三个写法并明确拒绝替维护者选:①改写成 17.x、②不钉小版本的范围写法(如 >= 17)、③加门禁(本单)。本单的门禁契约必须对齐实际落地的那一个 —— 若落地的是②,一个要求精确大版本的门禁上线即红。

核实结果(origin/main @ 01faeb13a):落地的是方案①,精确大版本钉,拼写为 Requires @objectstack/spec 17.x (Zod v4 schemas)。9 份带钉(7 份单钉 + objectstack-formula / objectstack-platform 各带一个附加包),2 份折叠块不钉。packages/spec/package.json = 17.0.0-rc.5。所以门禁对账的是精确大版本

原单只提到 objectstack-pm-dispatch 一份折叠块;实际是两份(另一份是 objectstack-upgrade),见下。

做了什么

新增 scripts/check-skill-compatibility-version.mjs,把每份 SKILL.md 声明的 @objectstack/… 大版本与工作区实际 package.json 对账,不一致即红,报错带「哪份文件 / 声明值 / 实际值 / 怎么改」:

skills/objectstack-ai/SKILL.md
    declared: @objectstack/spec 16.x   (frontmatter `compatibility:`)
    actual:   @objectstack/spec 17.0.0-rc.5  → major 17   (packages/spec/package.json)
    fix: edit the `compatibility:` line to read "@objectstack/spec 17.x".
    This is the drift #5245 found by hand: skills taught 17 while declaring 16.

按包(而非目录名)建映射,因此 @objectstack/core / @objectstack/formula 这类附加钉同样对账;钉了一个不存在的包名也报红。

落点

scripts/,与邻居 check:skill-frame-sync 同一层 —— 理由也相同:spec 包里的那几道 skill 门是生成器,源是 packages/spec/src、产物在其 check:generated 账本里;本门禁不生成任何产物、不读 spec 源码,它比对的是手写 frontmatter 与全工作区 package.json,那是 spec 包没有理由持有的知识。跨仓 prose 策略门一贯落在根 scripts/(check:role-wordcheck:doc-authoringcheck:nul-bytes 都从这里扫 skills/)。已用 check:generated --reconcile-only 确认根脚本不需进 spec 包账本。

接进 .github/workflows/lint.ymlTypeScript Type Check job,紧挨 check:skill-frame-sync不加 paths filter:加了 skills/** 过滤反而会瞎掉另一半输入 —— packages/*/package.json 的大版本 bump 才是让声明变陈旧的那一下。

#4690 为戒:一切缺输入都是红,不做静默 skip 退出 0

  • skills/ 不存在、或没有任何 skill 目录 → 红
  • skill 目录里没有 SKILL.md → 红
  • 没有 frontmatter、compatibility: 键缺失或为空 → 红
  • 非豁免文件声明不出任何钉 → 红
  • 全仓一个钉都没扫到 → 红(这条最关键:若日后措辞整体改成方案②的范围写法,本门禁会响亮报红,逼出一次决策,而不是悄悄匹配为空、退化成绿色 no-op)
  • 半钉的行(一个包钉了、另一个只是裸提及)→ 红,堵住「有一个钉就算过」的洞

两份折叠块:豁免,理由写进代码,且每次运行重新校验

文件 理由 校验锚
objectstack-pm-dispatch 流程 skill,不 import 工作区任何东西,没有「与之兼容」的大版本;正文自述「No @objectstack/spec dependency」 该自述句仍在
objectstack-upgrade 跨大版本升级 skill,正确于 TARGET major;钉当前大版本反而是错的 正文「at the TARGET major」仍在

豁免不是空白支票:

  • 理由文本被改掉 → 豁免自失效,文件退回普通规则(红);
  • 豁免只覆盖「可以不钉」,不覆盖已有的钉 —— 豁免文件若长出钉,照样对账,错了照样红;
  • 豁免文件真长出钉之后,该豁免已无事可做 → 报红要求删条目(反休眠);
  • 豁免指向一份扫不到的文件 → 报红(陈旧条目)。

反向验证(预测先写后跑)

预测写在 predictions.md 后才动手,逐条对照:

# 变异 预测 实测
R1 objectstack-ai17.x16.x 红,报错含 文件/声明值/实际值/修法 红,四要素齐全(见上方报错原文)
R2 保留 R1 变异,摘掉对账那一步(只留解析+存在性断言,即今天的现状) 绿,变异完全无人察觉 绿,EXIT=0
R3 整行删掉 compatibility: 红(声明缺失),不是「无声即通过」 红,frontmatter has no compatibility: key
R4 全部改写成方案②范围写法(零钉) 红,而不是静默 no-op 红,9 处 declares no pinned major
R5 豁免条目指向不存在的文件 红(陈旧豁免) 红,self-test 覆盖
R6 两份折叠块,未变异 绿(经校验的豁免) 绿
R7 抹掉豁免文件里的理由句 红(豁免自失效) 红,no longer matches that justification
R8 半钉的行 红(裸提及那个)

无落空预测。R2 值得单独说:摘掉对账后,门禁不但放行了 16.x,汇总行还照旧印出 11 pinned major(s) all match the workspace —— 一句字面为假的成功报告。这正是 #4690 那个失败模式的样子,也是为什么 R2 这一格必须是绿:它证明对账那一步才是承重的,解析和存在性断言都不是。

自测 / 门禁不会腐化成 no-op

--self-test 18 例,红路径逐条钉死(含上表 R1/R3/R4/R5/R7/R8,及「钉了不存在的包」「空扫描」「无工作区包」),外加两条走真实文件系统的发现层断言(临时 fixture 树里「skill 目录缺 SKILL.md → 红」、真实树扫描非空)。按仓库惯例注册为 check:skill-compatibility,先跑 self-test 再跑真扫。

验证

$ pnpm check:skill-compatibility
✓ check-skill-compatibility-version self-test: 18 cases pass.
✓ check-skill-compatibility-version: 11 SKILL.md file(s) reconciled against 77 workspace packages
  11 pinned major(s) all match the workspace (@objectstack/spec is 17.x)
  2 justified exemption(s), each with its stated reason still true of the file.

$ pnpm lint                          # 全仓 ESLint,EXIT=0
$ pnpm check:nul-bytes               # 5949 文件,无裸控制字节
$ pnpm check:doc-authoring           # 365 文件 clean
$ pnpm check:published-files         # 69 publishable package 全绿
$ pnpm check:workflow-status-functions   # 22 workflow / 41 job 全绿
$ pnpm --filter @objectstack/spec check:generated --reconcile-only   # 根脚本不需进 spec 账本

门禁在当前 origin/main 上是绿的 —— 不是靠放宽换来的:上表 R1–R8 证明它在该红的地方都红。

Changeset

skip-changeset —— 本 PR 不发布任何东西:

  • package.jsonprivate: true(@objectstack/spec-monorepo),只加了一行 script 注册;
  • scripts/ 不在任何已发布包的 files 白名单里(check:published-files 枚举了 69 个包的白名单);
  • .github/workflows/ 是 CI 配置。

没有落在 packages/lint 之类已发布包里,所以按三选一规则取 skip-changeset,不写 changeset(空 frontmatter changeset 是被 #6059/#5471 的门禁禁止的)。

边界

零运行时、零词表、零 spec 改动。未碰 packages/spec/src/**/*.zod.ts、严格度账本、content/docs/releases/


Generated by Claude Code

`skills/*/SKILL.md` frontmatter 的 `compatibility:` 行此前无任何门禁看守:
`check:skill-docs` / `check:skill-refs` 只比对生成物,`check:skill-examples`
只 typecheck `os:check` 代码块 —— 三道门对着一句错的版本声明全绿。实证即
#5245:仓库已在 17.x,十份 SKILL.md 里九份仍写 `Requires @objectstack/spec
16.x`,静默漂移整个大版本周期。#5245 手工改了值,本单修产生漂移的机制。

新增 `scripts/check-skill-compatibility-version.mjs`:把每份 SKILL.md 声明的
`@objectstack/<pkg> <major>.x` 与工作区实际 package.json 对账,不一致即红,
报错带「哪份文件 / 声明值 / 实际值 / 怎么改」。

门禁契约按 #5245 **实际落地**的措辞(方案①精确 `<major>.x` 钉),而非原单
设想 —— 若日后改成方案②的范围写法,本门禁会响亮报红而不是静默匹配为空。

引 #4690 为戒,一切缺输入均为红而非 skip 退出 0:skills/ 缺失、skill 目录无
SKILL.md、无 frontmatter、无 `compatibility:` 键、非豁免文件无钉、全仓零钉、
豁免理由已失效 —— 逐条响亮失败。

两份折叠块文件按各自理由豁免,理由写进代码并在每次运行时重新校验:
- `objectstack-pm-dispatch`:流程 skill,自述「No @objectstack/spec dependency」;
- `objectstack-upgrade`:跨大版本升级 skill,正确于 TARGET major,钉当前大版本反而是错的。
豁免只覆盖「可以不钉」,不覆盖已有的钉 —— 豁免文件若长出钉,一样对账。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014wsZeReNTqiceBfLb5Pyf5
@vercel

vercel Bot commented Aug 7, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectstack Ignored Ignored Aug 7, 2026 12:16pm

Request Review

@github-actions github-actions Bot added size/l ci/cd dependencies Pull requests that update a dependency file labels Aug 7, 2026
@os-zhuang os-zhuang added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Aug 7, 2026 — with Claude
@os-zhuang
os-zhuang marked this pull request as ready for review August 7, 2026 12:39
@os-zhuang
os-zhuang added this pull request to the merge queue Aug 7, 2026

Copy link
Copy Markdown
Contributor Author

PM 验收:ACCEPT — 已 ready + auto-merge,现已入队。

验收依据取自 GitHub 侧读数:

  • CI:26 个 check 全部 completed、零 failure。ESLint success、TypeScript Type Check success、Test Core (1..3/3) success、Validate Package Dependencies success、Console Pin Freshness success。判定前已确认 Test Core 在名单中(名单在场守卫)。
  • Check Changeset:首跑红、重投后 success。已核实是今日那条 skip-changeset 时序竞态,不是本 PR 的缺陷 —— 判定时 skip-changeset 标签已在位、diff 中无 .changeset/*.md、3 个改动文件无一落在已发布包里。这是今天第 6 例,我已据此立 [finding] Check Changeset 在 opened 事件上先于 skip-changeset 标签落地而判红 —— 每个走 skip 路线的 PR 都要白跑一次重投(今日 6 例) #6260 记录机制与复发计数。
  • 文件面:3 个文件 —— scripts/check-skill-compatibility-version.mjs(新增)、根 package.json(一行脚本注册)、.github/workflows/lint.yml。全部合规;未碰 packages/spec/src/**/*.zod.ts、严格度台账、content/docs/releases/

派发令里我最看重的那一格,你答对了

我要求先读 main 上 #5245 实际落地的是哪一种写法,再建门 —— 因为 #5245 给了三种却拒绝替维护者选,若落地的是范围写法(>= 17)而门要求精确 major,上线当天就红。你核实的结果是方案①(精确 <major>.x 钉,origin/main @ 01faeb13a),门据此对账精确大版本。这一步做对了,后面才谈得上正确。

顺带更正了原单一处事实:折叠块是两份objectstack-pm-dispatch + objectstack-upgrade),不是一份。

R2 是这份工作里最有价值的一格

摘掉对账那一步后,门不但放行了 16.x,汇总行还照旧印出 11 pinned major(s) all match the workspace —— 一句字面为假的成功报告。这正是 #4690 那个失败模式的实物,也精确证明了承重的是对账那一步,解析与存在性断言都不是。反向验证的价值就在这里:它区分了「跑过了」和「哪一行在干活」。R1–R8 八条预测无一落空

两个设计判断我特别认可

  1. 不加 paths filter,且理由是对的:加 skills/** 过滤会瞎掉另一半输入 —— packages/*/package.json 的大版本 bump 才是让声明变陈旧的那一下。一道会在「恰好破坏它的那个 PR」上休眠的门禁等于没有。
  2. 豁免是自失效的,而不是空白支票:理由文本被改掉 → 豁免失效;豁免只覆盖「可以不钉」、不覆盖已有的钉;豁免文件真长出钉 → 报红要求删条目(反休眠);豁免指向扫不到的文件 → 报红(陈旧条目)。这是「我们想过这个文件」和「一个静默的洞」的区别。

「全仓零钉 → 红」那条尤其关键:若日后措辞整体改成方案②的范围写法,本门禁会响亮报红逼出一次决策,而不是悄悄匹配为空、退化成绿色 no-op。一道门禁最容易的死法就是这个,你堵上了。

skip-changeset 档位判断也是量出来的(根 package.jsonprivate: truecheck:published-files 枚举 69 个包的白名单确认 scripts/ 不在其中),不是套默认。


Generated by Claude Code

@os-zhuang
os-zhuang marked this pull request as draft August 7, 2026 12:45
@os-zhuang
os-zhuang marked this pull request as ready for review August 7, 2026 12:47
Merged via the queue into main with commit 4dd9cbd Aug 7, 2026
29 of 30 checks passed
@os-zhuang
os-zhuang deleted the claude/issue-5331-skill-compatibility-gate branch August 7, 2026 12:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ci/cd dependencies Pull requests that update a dependency file size/l skip-changeset PR has no user-facing published change; bypasses the changeset gate

Projects

None yet

Development

Successfully merging this pull request may close these issues.

SKILL.md compatibility 行无任何门禁对账 —— 声明的 spec 版本可与仓库实际版本无限期漂移(#5245 方案③拆出)

2 participants