Repository navigation
authorable-surface 的 tombstone 门禁可被手编基线绕过 —— 删掉基线行就删掉了证据(#4638 / #4643 已两次这样过绿) #4650
Description
症状
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) 是可以立刻落的窄修。
影响范围
- 门禁失效期间合并的:feat(spec)!: 双源 C3 收敛 — 通知语汇归 ./api,./ui 与 ./system 侧死删 (#4610) #4638、feat(spec)!: 双源 C4 收敛 — Session 归 ./api,./identity 侧死删 (#4641) #4643(实质无害,见上)。
- ✅ spec 双源清账主单:基线 52 → 0(2026-08-03 收官)—— #4446 gate 落地后的偿还 worklist #4535 剩余 16 条落在真作者面上(
DataSyncConfigautomation 19 keys vs integration 9、RateLimitConfig进api/endpoint.rateLimit、HttpRequest进ui/view.read/write等),若沿用这条捷径就会真正触发 ComputedFieldCacheSchema 是 2026-06 字段剪除留下的第二个孤儿(#3726 表格误记为「已清理」) #3733 / ADR-0104 描述的静默剥离。已在 ✅ spec 双源清账主单:基线 52 → 0(2026-08-03 收官)—— #4446 gate 落地后的偿还 worklist #4535 的 worklist 里写明禁止手编基线,但那只是纪律,不是门禁。
关联
#4535(双源清账主单,v17 重切一节记录了同一发现)、#3733、#3855、ADR-0059 §5、ADR-0087、ADR-0104
Activity
os-zhuang commentedon Aug 2, 2026
补充:第 (2) 点由推测升为已证,且维护者已裁决需要一个可机检的窄例外
立单时我把「
authorable-surface.json过度收集」写成推测。#4658(C6EventSchema)给出了实证:两侧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 留在碰撞名上,而那会让双源行永远存活)。实现要点:
gen:schema已经在内存里持有generatedSchemas,可从 24 个根 BFS),不要用我上面那种正则近似 —— 别名 import、registerMetadataTypeSchema运行时注册、as unknown as z.ZodType铸型都会让正则漏边。EXTRA_METADATA_TYPE_SCHEMAS(运行时注册的 overlay)也是根的一部分。authorable-surface.json的 tombstone 要求,不应被读成「这个 schema 可以随便改」。原立单的第 (1) 点(校验被删基线行确系 aged-out tombstone)仍然需要 —— 两者是互补的:(1) 管有 tombstone 历史的行,窄例外管从来就不该被收进来的行。
同族问题另见 #4659(检查 (b) 按 leaf name 匹配 conversion surface)。本单仍未认领。
Generated by Claude Code
os-zhuang commentedon Aug 2, 2026
实锤:
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
Merge origin/main into claude/issue-4661-retry-policy-dual-source
os-zhuang commentedon Aug 2, 2026
🔒 认领(B 道 · 门禁,#4711/PR #4724 已合并后顺位派发)
session_0176qgxgCXTJCUv4YFLtusP9claude/issue-4650-baseline-delete-proofpackages/spec/scripts/build-schemas.ts+ 单测 + changeset;零生成物、零src/改动(与 A 道在跑的 spec 双源清账 C14:HttpMethod(./api, ./shared ≠ ./ui)—— 1 条,且两侧是「7 值 vs 5 值」的真分歧,不是同形别名 #4691 文件互斥)。Generated by Claude Code
os-zhuang commentedon Aug 2, 2026
✅ 验收通过(ACCEPT) — PR #4726 已转正并开 auto-merge,由 CI 门禁把关合并。
对实际 diff 逐项核对的结论:
build-schemas.ts+build-schemas-check-mode.test.ts+ 一个 patch changeset。零生成物、零src/改动,与 A 道 spec 双源清账 C14:HttpMethod(./api, ./shared ≠ ./ui)—— 1 条,且两侧是「7 值 vs 5 值」的真分歧,不是同形别名 #4691 文件互斥成立。git show HEAD:的恒等自比问题被正面处理。ViewSchema.refine类误判,fail closed)/ 整 def 消失移交 manifest ratchet 并响亮打印。检查 (a) 原语义有独立单测保活。--check对整文件做规范形字节比对,feat(spec)!: 双源 C5 收敛 — ActivationEventSchema 归 ./kernel 结构化形状,./studio re-export (#4653) #4662 那类 description 手编即红;写模式重写规范形。--check全程零写入(porcelain 干净,fix(spec):build-schemas.ts --check不再写json-schema.manifest.json(#4711) #4724 纪律未退步)。全量 spec suite 294 文件 / 7394 测试绿。两点留给维护者的事实记录(不阻塞本单):
shared/RateLimitConfig、shared/FieldMapping、两个DataSyncConfig在真图上从元数据根不可达(import 边 ≠ Zod 引用边;24 根中无 endpoint/connector/manifest 类型)。门禁按 2026-08-02 裁决以真图为准并逐条打印判定;若认为这些簇应受 tombstone 保护,加根或加显式 Zod 引用即可,门禁自动跟进(详见 PR §4)。.type就能让一个 tombstone 冒充「已登记迁移」 #4659 同族。Generated by Claude Code
feat(spec)!: 退役 RestServerConfig.openApi31 整块 —— declared ≠ enforced,…
feat(spec)!: EnvironmentArtifact 信封收敛为单一声明 —— C10 双源清账,./system 持活 wi…
feat(spec)!: EnvironmentArtifact 信封收敛为单一声明 —— C10 双源清账,./system 持活 wi…
chore(spec): rebase 到含 #4764 的 main 后全量重生成 —— 蓄意删键重放 + 生成物从新基重算(#4535 …
refactor(spec)!: rename connector-side RateLimitConfig (objectstack-a…
fix(spec): deleted authorable-surface baseline lines must prove thems…
feat(spec)!: 移除 `./automation` 的孤儿 `EventSchema` —— C6 双源清账,信封唯一真源在 `…
23 remaining items
spec: retire PackageRollbackResponseSchema and the rollbackPackage co…
spec: regenerate the merged tree's artifacts (regen-merge sync after #…
fix(spec): gate (a)'s remedy names every retirement route instead of …
Bind published response contracts for the 17 unbound client-SDK metho…
docs(spec): lead the deletion-gate anchor description with what it mu…
docs(spec): lead the deletion-gate anchor description with what it mu…
refactor(spec)!: delete the seven cron-typed positions outright — no …
chore(spec): advance the committed authorable-surface anchor to b9598e9
refactor(spec)!: delete the seven cron-typed positions outright — no …
feat(spec,core)!: the startup contract describes what the kernel prod…
chore(spec): advance the authorable-surface deletion-gate anchor to m…
feat(spec)!: retire `CubeJoin.sql` and `CubeJoin.relationship` — the …
feat(spec)!: split the translation bundle type — `settings` is a plat…
feat(spec)!: the translation item door refuses `settings` — platform-…
feat(spec)!: retire the export-job API family, IExportService and Sch…