Skip to content

fix(spec): authorable-surface 被删基线行必须自证合法 — merge-base 锚定 + 可达性窄例外 (#4650) - #4726

Merged
os-zhuang merged 1 commit into
mainfrom
claude/issue-4650-baseline-delete-proof
Aug 3, 2026
Merged

fix(spec): authorable-surface 被删基线行必须自证合法 — merge-base 锚定 + 可达性窄例外 (#4650)#4726
os-zhuang merged 1 commit into
mainfrom
claude/issue-4650-baseline-delete-proof

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Fixes #4650

改动仅三个文件:packages/spec/scripts/build-schemas.ts(门禁本体)、packages/spec/scripts/build-schemas-check-mode.test.ts(单测)、一个 changeset。零生成物改动、零 src/ 改动(只读 import kernel/metadata-type-schemas 的两个公开访问器),与 A 道 #4691 文件互斥。写模式与 --check 在本分支上跑完后 git status --porcelain 只剩这两个脚本文件——生成器输出与已提交工件逐字节一致。

一、检查语义边界表

检查 比较对象 防什么 本 PR
(a0) rename 未携带 本 commit 基线文件(经 RENAMED_DEFS 携带)vs 本次构建 rename 伪装的删除 不变,单测钉住
(a) key 消失 本 commit 基线文件 vs 本次构建 形状变了基线没动(静默剥离,#3733/ADR-0104) 不变,单测钉住
(b) live→retired 未登记 同上 tombstone 未进 ADR-0087 登记 不变
(c) 删行自证(新增) merge-base 基线 vs 本次构建 基线动了没有正当理由(#4650 主洞) 新增
规范形(新增) 磁盘字节 vs 生成器输出字节 手编痕迹(#4662 的 description 破折号类) --check 红 / 写模式重写

(c) 的判定集合是「merge-base 基线有、本次构建不再发出」的 key(先经 RENAMED_DEFS 携带,rename 不会被误读为删除)。每个这样的 key 必须命中三条证明之一,全部门禁内实算:

  1. aged-out tombstone:基准版本条目带 [RETIRED],且其 surface 在 CONVERSIONS_BY_MAJOR / MIGRATIONS_BY_MAJOR 的登记 major 距当前 ≥ 2(取最早登记;子句匹配沿用检查 (b) 的 / 拆分 + 叶名后缀规则,build-schemas.ts 检查 (b) 用叶名匹配 conversion surface —— 无关簇的 .type 就能让一个 tombstone 冒充「已登记迁移」 #4659 记录的共同弱点不在本单扩大);
  2. 从元数据根不可达(2026-08-02 裁决的窄例外):错误文案明确「只豁免本文件的 tombstone 要求,不是该 schema 可随便改的许可」;
  3. 整 def 不再发出:归 json-schema.manifest.json ratchet(gen:schema silently drops PageTabsProps since #2967 — references regen would delete real docs #2978)与 check:api-surface 管辖,本门禁放行并逐 def 打印提示。

二、CI 基准选择(为何不空转)

issue 草案的 git show HEAD: 在 CI 里恒等自比(HEAD 就是 PR commit)。本实现:

  • 锚 = merge-base(HEAD, origin/main)origin/main 缺失时脚本自行 git fetch --quiet --depth=1 origin +refs/heads/main:refs/remotes/origin/main——lint.yml 的 typecheck job 是默认浅检出(无分支 ref),不必改 workflow;浅历史走不到公共祖先时回退 origin/main tip(PR 合成 merge commit 的 parent1 即该 tip,语义等价);
  • 本地滞后分支比较的是 fork 点,main 后续新增的 key 不会被误报为删除;老 commit 的 detached 检出同理不误报;
  • 基准完全不可解析时响亮失败(exit 1 + 修复指引),绝不静默跳过——静默跳过就是换了皮的 authorable-surface 的 tombstone 门禁可被手编基线绕过 —— 删掉基线行就删掉了证据(#4638 / #4643 已两次这样过绿) #4650 绕过。此分支有单测钉住;
  • 「已提交后再跑(模拟 CI)同样红」:worktree 实测 + 单测双重证明(单测里 HEAD 越过 sabotage commit、origin/main 停在 base,worktree 干净——正是 CI 检出形状)。

三、Sabotage 证据(均为真实命令输出)

1. 修复前复现(worktree:临时删掉 src/data/object.zod.tsicon prop + 手删基线行 data/Object:icon,均已还原):

PRE-FIX EXIT AFTER HAND-DELETE: 0        ← 现门禁绿,#4638/#4643 的捷径复现
✅ Successfully generated 1705 schemas.

2. 修复后同一 sabotage 红,错误指向 #4650;已提交后再跑同样红:

❌ 1 authorable baseline line(s) were deleted without proof (#4650):
     - data/Object:icon — def reachable from the metadata-type roots; the entry at 23338c3823a6 was LIVE (never tombstoned).
   ...compared against the baseline at merge base 23338c3823a6 with origin/main,
   which this commit cannot rewrite.
POST-FIX EXIT: 1
POST-FIX EXIT (SABOTAGE COMMITTED, CI SIMULATION): 1

3/5. 不可达 def 删行绿(打印判定来源)、aged-out tombstone 删行绿、整 def 消失绿(沙箱:把待删行注入 base commit、PR commit 删除——与真实攻击集合等价;data/Object:compactLayout 登记于 major 11,历史真实的 aged-out 案例):

GREEN-PATH EXIT (deletions COMMITTED): 0
ℹ️  3 baseline deletion(s) since 17123e92dce5 carry their own proof (#4650):
     - api/SessionResponse:zzOverCollected4650 — def not reachable from the 24 metadata-type roots
       (BUILTIN_METADATA_TYPE_SCHEMAS + EXTRA_METADATA_TYPE_SCHEMAS overlay; BFS over this
       build's in-memory Zod graph): an over-collected entry, never parsed against a
       metadata document. This waives ONLY the tombstone requirement of this file — it is
       not a license to change the schema (#4650).
     - data/Object:compactLayout — [RETIRED] at 17123e92dce5 and registered at major 11 (current 17): tombstone aged out.
     - identity/Session:* (2 line(s)) — def no longer emitted by this build; whole-schema
       removals are adjudicated by json-schema.manifest.json (#2978) and check:api-surface.

4. 可达且无 tombstone 的行 → 红:见上 data/Object:icon;单测另覆盖「retired 但未登记」「retired 已登记但未满 2 majors(动态取 major 17 的 skill.triggerPhrases,fixture 老化会响亮失败提示换选)」两个红分支。

6. 检查 (a) 原语义仍在:单测「a key the BUILD stops emitting while still recorded is fatal before (c) ever runs」——文件仍记录、构建不再发出 → (a) 红,(c) 不吞。

7. --check 零写入:

--- git status --porcelain after WRITE mode:
 M packages/spec/scripts/build-schemas-check-mode.test.ts
 M packages/spec/scripts/build-schemas.ts
CHECK EXIT: 0
(--check 后 porcelain 相同——除本 PR 两个脚本外无任何写入,#4724 的纪律未退步)

8. 单测:12 passed(5 存量 + 7 新增,新增覆盖上述全部分支);全量 spec suite:

Test Files  294 passed (294)
     Tests  7394 passed (7394)
TYPECHECK-OK(tsc --noEmit)
eslint 两个改动文件:0 问题

沙箱本身就是真 git 仓库 + 伪造的 refs/remotes/origin/main——不加任何 test-only env 缝,门禁跑的就是 CI 跑的 git 解析路径。

四、可达性 BFS:根集合与图来源

  • :listMetadataTypeSchemaTypes() + getMetadataTypeSchema() = BUILTIN_METADATA_TYPE_SCHEMAS(当前 24 个)∪ EXTRA_METADATA_TYPE_SCHEMAS overlay,门禁内每次实算,禁 PR 描述人工论证;
  • :本次构建内存中的真 Zod 实例图(gen:schema 发出每个 def 时同步记录其 Zod 实例)。遍历对 def 做泛型深走(shape/options/in/out/element/innerType/keyType/valueType/数组/Map),另加两条 def 走不到的边:z.lazygetter()、check-clone 的 _zod.parent;
  • derived-clone 桥接:.refine()/.extend()/.strip() 克隆与原 def 无 identity(extend/strip 连 parent 链都没有),但共享逐属性 schema 实例。任一「(属性名, 实例) 对」出现在已访问对象节点的 shape 里 ⇒ 该 def 记为可达。ui/View 正是实证:ViewMetadataSchema 内嵌的是 ViewSchema.refine(…) 克隆,纯 identity BFS 会把 ui/View 误判不可达并放走它的删行——桥接后受保护。误差方向 fail closed(顶多多要一个 tombstone,绝不静默放行);
  • 数字:本构建 1705 个 def;tier1(identity/parent)145 + tier2(clone 桥接)55 = 200 个受保护,其余为过度收集(与评论 1 静态复算的量级一致);
  • 与静态复算的一处差异(如实报告):真 Zod 图下 shared/RateLimitConfigshared/FieldMappingautomation/DataSyncConfigintegration/DataSyncConfig不可达(评论 1 的静态近似认为 RateLimitConfig / FieldMapping 可达)。原因:import 边不等于 Zod 引用边(24 根里没有 endpoint/connector/manifest 类型,api/endpoint.rateLimit 不在根图上)。门禁按裁决用真图并逐条打印判定;若维护者认为某簇应受保护,加根或加显式 Zod 引用即可,门禁自动跟进。

五、残余风险(诚实声明)

六、changeset 定级:patch

纯构建期门禁收紧:无运行时 export、无 schema 形状、无生成物变化,npm 消费者唯一可见的是 CHANGELOG 条目。issue 的 protocol:breaking 标签是问题域标注(本门禁保护的是 protocol-breaking 类删除),不使本 PR 本身成为 breaking。

七、评论 2 扩展项(非新增的 description/格式改动):已做

--check 现在比对整个文件字节与生成器规范输出:keys 相同但字节不同(description 手改、缩进、重排)→ 红,信息指向 #4662/#4650;写模式自动重写为规范形。已提交工件与规范形逐字节一致(实测),因此本 PR 落地时该检查零噪声。范围仅 authorable-surface.json(manifest 的同类问题归 #4725 评估)。


🤖 Generated with Claude Code

https://claude.ai/code/session_0176qgxgCXTJCUv4YFLtusP9


Generated by Claude Code

…elves (#4650)

Check (a) reads authorable-surface.json from the commit under check, so
hand-deleting a baseline line deleted the evidence it runs on (#4638,
#4643 landed exactly that way; #4662 proved the file was hand-edited).
gen:schema / check:authorable-surface now add check (c): every key
present at the merge base with origin/main but absent from this build
must carry one of three in-gate proofs —

  1. aged-out tombstone: base entry [RETIRED] + an ADR-0087
     conversion/migration registered >= 2 majors ago;
  2. def not reachable from the metadata-type roots (2026-08-02 ruling):
     BFS over the build's in-memory Zod graph from
     BUILTIN_METADATA_TYPE_SCHEMAS + EXTRA_METADATA_TYPE_SCHEMAS, with
     derived-clone bridging so .refine()/.extend() copies keep their
     originals protected; waives ONLY this file's tombstone requirement;
  3. whole def no longer emitted (manifest ratchet / api-surface
     jurisdiction).

Anchoring on the merge base (not HEAD) keeps the check alive in CI,
where HEAD is the PR's own commit and a HEAD-relative diff is always
empty. --check further rejects any byte of the file that is not the
generator's output (#4662 description drift class); write mode
regenerates it. Checks (a0)/(a)/(b) unchanged and pinned by tests.

Fixes #4650

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

vercel Bot commented Aug 2, 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 2, 2026 11:50pm

Request Review

@github-actions github-actions Bot added documentation Improvements or additions to documentation tests tooling size/l labels Aug 2, 2026
@github-actions

github-actions Bot commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

No hand-written docs reference the 0 changed package(s). ✅

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

Labels

documentation Improvements or additions to documentation size/l tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

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

2 participants