Skip to content

authorable-surface 的 tombstone 门禁可被手编基线绕过 —— 删掉基线行就删掉了证据(#4638 / #4643 已两次这样过绿) #4650

Closed

Description

@os-zhuang

症状

packages/spec/scripts/build-schemas.ts 的可作者化面 ratchet 检查 (a)(约 L408–428)本意是:可作者化 key 不许无声消失,因为这些 schema 不是 .strict(),Zod 会静默 STRIP 未知键 —— 作者继续写就得到一次干净的 parse 和一个永不生效的设置(#3733、ADR-0104)。错误文案要求走 tombstone:retiredKey() + D2 conversion + D3 chain step + major changeset。

但检查是这样做的:

const prev = new Map(surfaceDoc.keys.map(...));         // ← 读**磁盘上**的 authorable-surface.json
const vanished = [...prev.keys()].filter((k) => !currentKeys.has(k));
if (vanished.length > 0) { /* 报错 + process.exit(1) */ }

surfaceDoc 读的是同一个 commit 里可以被随手改掉的基线文件。把要删的 key 从 authorable-surface.json 里手工删掉,prev 里就没有它,vanished 为空,门禁静默通过 —— 删掉基线行就是删掉证据。

文件自己的 description 与错误文案都写着:

A tombstone that has aged out (~two majors) is the ONE legitimate reason to delete a line here — do it in the same PR, deliberately.

但没有任何东西校验这一条。 基线行的删除既不要求对应 [RETIRED] 标记存在过,也不要求它已 aged out,更不要求有 conversion 登记。

已发生两次

PR 删除的 key tombstone conversion / migration 结果
#4638(C3) ui/Notification:*、ui/NotificationConfig:*、system/NotificationConfig:* 无 无 绿
#4643(C4) identity/Session:* 全部 10 个 无 无 绿

复核 #4643 的 commit 21676eb5d:authorable-surface.json 里 10 行 identity/Session:* 被直接删除,src/conversions/ 与 src/migrations/ 零改动。

这两次的实质是否有害?

倾向于无害,但不该被当先例:

  • identity/Session 是 DB 行 / 运行时形状,ui/Notification 是 toast 实例形状 —— 都不是作者在元数据文件里手写的类型,三仓 import 级扫描也已证实零消费方。
  • 更根本的问题是 authorable-surface.json 过度收集:它记录每个被发出的 schema 的全部 properties,包括 api/SessionResponse:success、api/SessionResponse:meta 这类 REST 信封字段 —— 这些无论如何都不是「metadata author 可写」的东西。文件的名字比它的内容强。

所以真正的缺口有两个,建议分开处置。

建议

(1) 堵住捷径。 让基线行的删除必须自证合法。最小实现:检查 (a) 之外再加一条 —— 对比 git show HEAD:packages/spec/authorable-surface.json 与工作树版本,任何被删除的行必须满足「上一版本带 [RETIRED] 标记」且「其 surface 已在 CONVERSIONS_BY_MAJOR / MIGRATIONS_BY_MAJOR 登记过、且登记的 major 距当前 ≥ 2」。不满足就红,错误信息指向本单。

注意实现时别把 (a) 的现有语义弄反:(a) 防的是形状变了而基线没动,新检查防的是基线动了而形状没有正当理由 —— 两者都需要。

(2) 收窄收集面。 ratchet 应只记录从真作者面可达的 schema(object / field / flow / view / connector / plugin 等作者手写的元数据类型,及其传递引用),而不是每个被 gen:schema 发出的 schema。否则「可作者化」这个判定在评审时不可用 —— 每次都要人肉判断某个 key 是不是真的作者面,而这正是 #4638 / #4643 两次都得靠 reviewer 直觉的原因。

(2) 比 (1) 影响面大,可能需要先确定「真作者面根集合」的定义,建议单独排期;(1) 是可以立刻落的窄修。

影响范围

关联

#4535(双源清账主单,v17 重切一节记录了同一发现)、#3733、#3855、ADR-0059 §5、ADR-0087、ADR-0104

Activity

  1. os-zhuang commented on Aug 2, 2026

    @os-zhuang
    ContributorAuthor

    补充:第 (2) 点由推测升为已证,且维护者已裁决需要一个可机检的窄例外

    立单时我把「authorable-surface.json 过度收集」写成推测。#4658(C6 EventSchema)给出了实证:两侧 Event 均不从 BUILTIN_METADATA_TYPE_SCHEMAS 可达,其 authorable-surface.json 条目纯属过度收集 —— 没有任何元数据文档会被这两个 schema 解析,门禁在保护一个空集。

    我按 kernel/metadata-type-schemas.ts:77 的 24 个根做了静态引用图复算(1563 schema,247 可达),#4535 双源 worklist 剩余 12 簇里只有 4 簇真正可达(RetryPolicy / RateLimitConfig / HttpRequest / FieldMapping),其余 8 簇的条目都是过度收集产物。

    2026-08-02 维护者裁决:本单需要加一个可机检的窄例外 —— 证明不从 BUILTIN_METADATA_TYPE_SCHEMAS 传递可达的 def,允许删除其基线行而不要求 tombstone。这是 #4535 的 C6 / C7 / C10 / C13 能诚实收口的前置条件(否则唯一合法补救是把 tombstone 留在碰撞名上,而那会让双源行永远存活)。

    实现要点:

    • 可达性判定必须在门禁内计算,不能靠 PR 描述里的人工论证 —— 否则只是把手编基线换成手写理由。
    • 判定应走真 Zod 图(gen:schema 已经在内存里持有 generatedSchemas,可从 24 个根 BFS),不要用我上面那种正则近似 —— 别名 import、registerMetadataTypeSchema 运行时注册、as unknown as z.ZodType 铸型都会让正则漏边。
    • 注意 EXTRA_METADATA_TYPE_SCHEMAS(运行时注册的 overlay)也是根的一部分。
    • 「不从 BUILTIN 可达」≠「没人手写」:插件清单、connector 配置走另一扇授权门。例外只应豁免 authorable-surface.json 的 tombstone 要求,不应被读成「这个 schema 可以随便改」。

    原立单的第 (1) 点(校验被删基线行确系 aged-out tombstone)仍然需要 —— 两者是互补的:(1) 管有 tombstone 历史的行,窄例外管从来就不该被收进来的行。

    同族问题另见 #4659(检查 (b) 按 leaf name 匹配 conversion surface)。本单仍未认领。


    Generated by Claude Code

  2. os-zhuang commented on Aug 2, 2026

    @os-zhuang
    ContributorAuthor

    实锤:authorable-surface.json 确曾被手工编辑(来自 #4662 / C5)

    本单立单时说的是「基线可以被手编,而门禁不校验」。C5 的实施(PR #4662)给出了它确实被手编过的直接物证:

    该 PR 跑 gen:schema 后,authorable-surface.json 除预期的新增 key 外,还出现一处非新增改动 —— description 行里的破折号被规范化。也就是说文件此前的内容不是生成器的输出:若一直由 gen:schema 写,该字符早就是生成器写的形态了。

    结合本单已记录的两次(#4638 删 ui/Notification:* 等、#4643 删 identity/Session:* 全 10 行,均零 conversion),现在证据链是完整的:基线不仅可以被手改,而且已经被手改过,门禁一次都没有察觉。

    这加强了本单第 (1) 点的必要性 —— 校验被删基线行确系 aged-out tombstone —— 并且提示加固时应一并考虑:除「新增 key」外的任何 authorable-surface.json 内容差异都应当要求显式理由,包括 description / 格式类改动,否则手编痕迹会继续被当成噪声忽略。

    (顺带更正:PR #4662 的描述 §9 把这条发现标成了 #4658,那是 C5 的姊妹簇 C6 的簇单号,与本发现无关 —— 本发现归属本单。)


    Generated by Claude Code

  3. self-assigned this
    on Aug 2, 2026
  4. os-zhuang commented on Aug 2, 2026

    @os-zhuang
    ContributorAuthor

    🔒 认领(B 道 · 门禁,#4711/PR #4724 已合并后顺位派发)


    Generated by Claude Code

  5. os-zhuang commented on Aug 2, 2026

    @os-zhuang
    ContributorAuthor

    ✅ 验收通过(ACCEPT) — PR #4726 已转正并开 auto-merge,由 CI 门禁把关合并。

    对实际 diff 逐项核对的结论:

    两点留给维护者的事实记录(不阻塞本单):

    1. 真 Zod 图与评论 1 静态复算的差异:shared/RateLimitConfig、shared/FieldMapping、两个 DataSyncConfig 在真图上从元数据根不可达(import 边 ≠ Zod 引用边;24 根中无 endpoint/connector/manifest 类型)。门禁按 2026-08-02 裁决以真图为准并逐条打印判定;若认为这些簇应受 tombstone 保护,加根或加显式 Zod 引用即可,门禁自动跟进(详见 PR §4)。
    2. check (c) 路径 3 的残余风险(整 def 连 export 一起删除走 manifest ratchet,而 manifest 删行同样只是纪律)已另立 json-schema.manifest.json 的「deliberate removal」删行仍是纪律而非门禁 —— #4650 的同类洞,上移一层(整 schema 级) #4725,未认领,与 gen:schema silently drops PageTabsProps since #2967 — references regen would delete real docs #2978/build-schemas.ts 检查 (b) 用叶名匹配 conversion surface —— 无关簇的 .type 就能让一个 tombstone 冒充「已登记迁移」 #4659 同族。

    Generated by Claude Code

  6. 23 remaining items

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

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions