Skip to content

build-schemas.ts 检查 (b) 用叶名匹配 conversion surface —— 无关簇的 .type 就能让一个 tombstone 冒充「已登记迁移」 #4659

Closed

Description

@os-zhuang

从 #4658(双源清账 C6)的路线定价实验里掉出来的范围外发现,不在 #4658 的 PR 里修,单独立案。与 #4650 同族(都是 authorable-surface 门禁的自我校验漏洞)。

事实

packages/spec/scripts/build-schemas.ts 检查 (b)(「live → retired 必须有登记的迁移」)这样匹配:

const surfaces = registeredRetirementSurfaces().flatMap((s) => s.split(' / '));
const unregistered = newlyRetired.filter(
  (k) => !surfaces.some((s) => s.endsWith('.' + k.split(':')[1])),
);

k 形如 automation/Event:type,k.split(':')[1] 只取叶名 type,再拿它跟全部 major 的所有 conversion surface 做 endsWith。匹配完全不看 key 属于哪个 def —— 任何一条无关的 conversion 只要 surface 以同名叶子结尾,就把这个 tombstone 判为「已登记」。

实测证据

在 #4658 的定价实验里,我把 src/automation/state-machine.zod.ts 的 EventSchema 两个 key 同时改成 retiredKey(...),没有新增任何 conversion。pnpm check:authorable-surface 的输出:

❌ 1 key(s) were tombstoned with no registered migration:
     - automation/Event:schema

只报了 schema。automation/Event:type 静默通过 —— 因为 CONVERSIONS_BY_MAJOR[11] 里 flowNodeHttpRename 的 surface: 'flow.node.type' 以 .type 结尾。两者毫无关系:一个是 flow 节点的 type,一个是 automation 状态机事件的 type。

为什么要紧

检查 (b) 的整个目的是保证退休进入 spec-changes.json / upgrade guide / os migrate meta(ADR-0087 D4)。叶名匹配让这个保证对常见叶名整体失效:type、name、config、filter、schema 之外的 objectName、flowName 等 surface 已在册,任何以它们结尾的 key 都能零迁移文档退休,门禁全绿,消费者只能靠失败才知道。

这与 #4650 是同一类问题:门禁的证据链自己不设防。#4650 是「删基线行没人校验」,本单是「登记检查可被无关条目满足」。

建议处置(需维护者裁决,故不自行动手)

把匹配从叶名收紧到定位到 def 的路径。两个方向:

  1. surface 里带上 def。让 conversion 的 surface 与 ${defKey}:${name} 可判定地对应(或加一个显式的 retires: ['automation/Event:type'] 字段),检查 (b) 改成精确集合匹配。最严格,但要回填现有 conversion。
  2. 保留 surface 的散文形态,另加一个显式登记表。新增 RETIRED_KEYS_BY_MAJOR: Record<number, string[]>,值就是 ${defKey}:${name},检查 (b) 只认这张表。改动小,且「declared = enforced」。

倾向 2:它让登记这件事从「碰巧字符串对上」变成作者必须写下确切的 key,正是 AI 生成的元数据最容易蒙混的那一环。

关联:#4650(基线手编漏洞)、#4658(发现现场)、#4535、ADR-0087、ADR-0104

Activity

  1. added
    domain:specDomain lane — seat card indexed by label:pm:seat
    pm:queueReady for the PM dispatch loop
    toolingFix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface
    and removed
    domain:specDomain lane — seat card indexed by label:pm:seat
    on Aug 3, 2026
  2. os-zhuang commented on Aug 5, 2026

    @os-zhuang
    ContributorAuthor

    补一条现场记录:叶名词表刚刚又宽了一格,新增 description。

    PR #5509(#4827,ADR-0087 D2 条目 page-header-subtitle-alias)登记的 surface 是:

    page.component.page-header.description
    

    按本单描述的匹配逻辑,registeredRetirementSurfaces() 把它拆进候选集之后,检查 (b) 里任何叶名为 description 的 key(形如 ui/ElementTextInputProps:description、ui/PageProps:description 等)一旦被 tombstone,都会 endsWith('.description') 命中这条完全无关的 page-header conversion,从而被判为「已登记迁移」、零迁移文档静默通过。

    和本单已记录的 .type 是同一个机制,只是 description 在 authorable surface 上的分布比 type 更广 —— 它是各类组件/字段的通用描述键,所以这一格拉宽的实际暴露面不小。

    PR #5509 没有绕过这个问题:它就是要登记 page-header 上的这个键,surface 写成别的形状才是不诚实。记在这里只是让本单的处置有一个更新的定价输入 —— 按本单建议 2(显式 RETIRED_KEYS_BY_MAJOR,值为确切的 ${defKey}:${name})收口后,这类偶然命中会一并消失。


    Generated by Claude Code

  3. self-assigned this
    on Aug 6, 2026
  4. baozhoutao commented on Aug 6, 2026

    @baozhoutao
    Contributor

    认领:PM 循环第 4 轮(spec-tooling 车道,C 包 #5163)
    会话:session_01559M8FVm6W6vDLABL3jvdW
    分支:claude/issue-4659-retired-keys-registry
    Worktree:objectstack-issue-4659
    域:domain:spec-tooling
    文件面:packages/spec/scripts/build-schemas.ts(检查 (b) 的匹配逻辑)、packages/spec/src/migrations/registry.ts(新增显式登记表,非 zod 语义面,#5337 已有本车道先例)、相应测试、api-surface 等生成物整体重生成(新增导出)、.changeset/*.md。⛔ 不碰 packages/spec/src/**/*.zod.ts 与 strictness 台账。(越界即停,报告说明)

    PM 裁决:走正文方向 2(显式 RETIRED_KEYS_BY_MAJOR,值为确切 ${defKey}:${name},检查 (b) 只认此表)—— 三轴中「防 AI 蒙混」与「declared = enforced」都指向让作者写下确切 key 而非碰巧字符串对上;评论区新证据(description 叶名把暴露面又拉宽一格)加重同一方向。串行前置:#5807/#5851 已 MERGED,build-schemas.ts 让空;#5744(spec 座位,blocked 未认领)文件面为 build-openapi.ts,无交集。


    Generated by Claude Code

  5. baozhoutao commented on Aug 6, 2026

    @baozhoutao
    Contributor

    验收:ACCEPT → PR #5902(转 ready 入队)

    裁决(方向 2)按样落地,且暴露面测量把本单从「一个坑」升格为「整面墙」:现有 97 个 [RETIRED] 键全部存在叶名巧合命中 —— 检查 (b) 对账面上每一个墓碑都是可满足的。复核确认:

    1. RETIRED_KEYS_BY_MAJOR 纯追加落位(1976-2043 行,零既有行触碰)、精确 ${defKey}:${name} 集合匹配、处方可整行复制粘贴(点名 major 与文件);
    2. (b2) 镜像检查补上「登记了却仍 LIVE」的反向撒谎面,同时刻意容忍 aged-out 稳态 —— 两侧语义都有 pin;
    3. 空表即绿的无回填证明是机械验证的(真门 + 沙箱双确认),不是推理;
    4. 反向验证 3 红 2 绿按预测的非均匀分布如实呈报 —— 绿路径 pin 在回退代码上也绿是构造使然,拒绝包装成全红是对的;
    5. 退役 playbook(.claude/skills/spec-property-retirement/SKILL.md)§3 把本缺陷当 caveat 记着 —— 一并更正,防下一次退役照旧文档踩回去;虽超出申报文件面,采纳。

    CI 24 项零红。必答项(#4725 定价):略变简单(登记证明结构有了在树模板 + 错误路径已在 TSDoc 封死),reachability 半边与 manifest 同删缺陷不受影响,原价保留;注意 per-KEY 表答不了 per-def 问题,#4725 需要姊妹表或纯 reachability。

    范围外发现 #5898 已核实(检查 (c) 同款叶名匹配 + Math.min 取最早 major → 基线行会被提前放行,data/Index:type 的时钟被 protocol 11 无关条目启动 —— 实测非推理;97 条回填是独立判断,不做 rider 的克制正确)。


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

pm:dispatchedDispatched to a dev agent by /pm-dispatchpm:queueReady for the PM dispatch looptoolingFix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions