fix(spec): 退休登记按确切 key 判定 —— 无关簇的同名叶子不再替 tombstone 背书 (#4659) - #5902
Merged
Conversation
`build-schemas.ts` 检查 (b)(`check:authorable-surface`)此前判定「这次退休
已登记」的方式是:取 key 的叶名,和全部 major 的所有 conversion / migration
`surface` 子句做 `endsWith('.' + name)`,完全不看 key 属于哪个 def。任何无关
登记只要 surface 以同名叶子结尾,就替这个墓碑背书。#4658 实测:
`automation/Event:type` 零 conversion 静默通过,命中的是 protocol 11 的
`flow.node.type`;#5509 之后 `.description` 也进了这个免检名单。
- 新增导出 `RETIRED_KEYS_BY_MAJOR`(`src/migrations/registry.ts` 末尾追加,
未改动该文件任何既有行),值是确切的 `${defKey}:${name}`。
- 检查 (b) 改为对该表精确集合判定;失败信息直接打印要粘贴的那一行和 major。
- 新增检查 (b2):登记了一个仍然 live 的 key 直接失败;登记了一个本次构建已不
再产出的 key 不是错误(墓碑老化后的预期稳态)。
- conversion 的 `surface` 散文一字未动;检查 (c) 的叶名匹配保留,原因与后续
处置记在 #5898。
- 退休 playbook(`.claude/skills/spec-property-retirement/SKILL.md`)同步:
原先那条「surface 必须以裸 key 结尾」正是本单的缺陷,已改写。
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01559M8FVm6W6vDLABL3jvdW
…ired-keys-registry
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
Contributor
📓 Docs Drift CheckThis PR changes 1 package(s): 110 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
|
baozhoutao
marked this pull request as ready for review
August 6, 2026 11:36
This was referenced Aug 6, 2026
This was referenced Aug 6, 2026
Closed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #4659
前提复核(先证再改)
issue 正文引的是 08-02 的代码,而
build-schemas.ts当天被重写过三次(#5807 / #5851)。所以先按机制复核,不按行号:origin/main上检查 (b) 的判定逻辑一字未变 —— 取 key 的叶名,和全部 major 的所有 conversion / migrationsurface子句做endsWith:实测复现(改动前,把两个 key 在基线里去掉
[RETIRED]标记,让门禁看到一次 live → retired 跃迁,不新增任何 conversion):检查 (b) 一个字都没说,运行一路掉到末尾那个「基线没重新生成」的报告器上。前提成立。
顺带量出了暴露面的真实大小:当前 97 条
[RETIRED]key,97 条的叶名都能在已登记子句里找到匹配 —— 也就是说这道门禁对今天在册的每一个墓碑都是可满足的。改动
PM 裁决的方向 2,原样落地:
新增导出
RETIRED_KEYS_BY_MAJOR(packages/spec/src/migrations/registry.ts),值是确切的`${defKey}:${name}`字符串,与authorable-surface.json的写法一致(去掉[RETIRED]标记)。检查 (b) 改为对这张表做精确集合判定 —— 没有
endsWith,没有取叶名,不从相邻 key 辐射。失败信息打印要粘贴的那一行和它该进哪个 major(处方即契约):新增检查 (b2)(dispatch 要求的第 (c) 项裁定):表里登记了一个当前仍然 live 的 key → 失败。理由是它替一次尚未发生的退休提前放行 —— 墓碑真落地那天,检查 (b) 已经被满足,没人再写下任何东西。反过来,登记了一个本次构建已不再产出的 key 不是错误:墓碑满约两个 major 后由检查 (c) 放行其基线行,登记条目留下并从此指向空,这是预期稳态。相反的裁定(「条目必须永远解析得到」)会逼着每次老化都去删掉登记 —— 记录自己把自己删掉。
conversion 的
surface散文一字未动。它面向作者、按作者书写元数据的形状表达(flow.nodes[].outputSchema),本来就无法可判定地映射回 def key;搬走的是机器事实,不是散文。一次退休仍然两样都要写,失败信息两条都点名。退休 playbook 同步(
.claude/skills/spec-property-retirement/SKILL.md§3/§4):原先那条 checklist 写的正是本单的缺陷本身 —— 「surface必须以裸 key 结尾。匹配是surfaces.some((s) => s.endsWith('.' + key))…… Caveat: 只比最后一段,所以 schema 名从不被检查 ——dashboard.aria就能满足ui/FormView:aria。别指望这道门禁做归属判定。」这条留着就是把下一个退休作者送进已经删掉的路径,所以一并改写。回填语义(dispatch 要求想清楚并机械验证)
不需要回填,已实测。 检查 (b) 只在新的 live → retired 跃迁上触发,而跃迁是相对已提交的
authorable-surface.json基线算的 —— 更早的墓碑在基线里已经是[RETIRED],prev.get(k) === false永远不成立,不会再触发。机械验证:未改动的树 + 空表,跑真实门禁
check:authorable-surface,以及沙盒里的完整--check:所以这张表读作「在确切-key 门禁下登记的退休」,不是「历史上的全部退休」。这一点写进了
RETIRED_KEYS_BY_MAJOR的 TSDoc(「Not a backfill of history」),连同一条明确的禁令:不要用叶名匹配 conversion registry 去反推缺失的历史 —— 那正是本单删掉的推断。registry.ts 的改动行(同日 churn 声明)
packages/spec/src/migrations/registry.ts是 ADR-0087 活跃工作面。本 PR 对它是纯追加:在文件末尾MIGRATION_MAJORS之后新增 1976–2043 行(TSDoc +RETIRED_KEYS_BY_MAJOR),既有行一行未改(git diff里该文件只有+,没有-)。src/migrations/index.ts只在既有 export 块里多一行名字。多行拼接字符串会躲开按行 grep,所以反向核对过:
registeredRetirementSurfaces全仓 0 命中(已随本 PR 删除),旧文案tombstoned with no registered migration在代码里 0 命中(仅存于 CHANGELOG 与一条历史 changeset 的散文中)。已
git merge origin/main(合入 4 个新 commit,含 #5854 / #5861 对packages/spec的改动),合并后重建 + 全量复验。测试
新增 5 个用例,放在既有沙盒文件
build-schemas-check-mode.test.ts,单独一个 describe。为什么第二个沙盒:这些用例需要门禁读到逐例不同的登记表,而既有沙盒把
src/软链到仓库真身,在那里写就是写进被跟踪的 registry。新沙盒改为cpSync复制src/(9.8 MB,约 40 ms),它的src/migrations/registry.ts因此是 fixture。门禁本身没有加任何 test-only 接缝 —— 被替换的表是 fixture 数据,和既有沙盒那条伪造的refs/remotes/origin/main同性质。为什么绿色用例仍然 exit 1:live → retired 跃迁的存在,等价于「已提交基线与本次产出不一致」,所以每个 fixture 按构造都会被脚本末尾的「基线未重新生成」报告器判为陈旧。那是另一个失败、另一条处方,断言一律按消息区分,从不只看退出码 —— 这点在测试注释里写明,没有粉饰。
data/Index:type的叶名今天仍被 protocol 11 的flow.node.type命中,api/HttpFindQueryParams:distinct仍被data.query.distinct(另一个 def 的键)命中 —— 哪天不再撞车,这两个 fixture 就不再模拟缺陷,测试当场说出来。全包:
check:dual-source-exports/check:exported-any/check:nul-bytes亦绿。反向验证 —— 方向先定,再跑
把检查 (b) 还原成叶名匹配、并摘掉 (b2),重跑这 5 个用例。预判 3 红 2 绿,实测一致:
2 key(s) were tombstoned…从未出现)、用例 2、用例 4(旧代码根本没有 (b2),整轮 exit 0)。范围外发现
Math.min(最早的 major),一律偏向提前放行。实测data/Index:type的老化时钟被 protocol 11 的flow.node.type起算,而它自己那条诚实登记object.indexes[].type在 major 17 —— 当前 major 17 下,这条基线行今天就可以被删掉。没有随本 PR 一起修,是因为检查 (c) 裁决的是历史墓碑:当前 97 条[RETIRED]一条都不在新表里,要改读确切 key 必须先回填这 97 条及其真实退休 major,而唯一能机械推导 major 的来源要么是这条坏掉的匹配器(用坏数据喂新表),要么是authorable-surface.json的 git 考据(可做,但是一次独立判断)。build-schemas.ts里registeredClauseMajors()的 TSDoc 已点名 build-schemas.ts 检查 (c) 的「墓碑已满 2 个 major」证明仍用叶名匹配 —— 无关簇的登记可以替一次退休提前起算 #5898,不留给下一个读者去踩。Generated by Claude Code