Skip to content

feat(root-abi): 接入锁定上游 compiler 并只读发布 Root ABI 制品(R-00018 / LCE-P0-005) - #8

Merged
Go1c merged 4 commits into
mainfrom
feat/r-00018-root-abi-generator
Aug 29, 2026
Merged

feat(root-abi): 接入锁定上游 compiler 并只读发布 Root ABI 制品(R-00018 / LCE-P0-005)#8
Go1c merged 4 commits into
mainfrom
feat/r-00018-root-abi-generator

Conversation

@Go1c

@Go1c Go1c commented Aug 29, 2026

Copy link
Copy Markdown
Contributor

做了什么

从只读镜像与锁定上游 compiler 产出 Header / C# Binding / Rust ContractTypes / layout report,逐份与上游 bundle 声明摘要对账后只读发布(规格 §8、§3.6)。

本 crate 是薄适配器:不含模板、slot 表、type map、布局常量——那些全部属于上游 compiler。在本仓自己实现模板会制造第二个 ABI 定义处,两处定义迟早分叉(规格 §4「私有模板会制造第二 ABI」)。tests/no_private_schema.rs 用源码级断言盯着这条边界。

开工前的可行性实测改变了实现路径

上游 packages/index.json 的 12 个语言生成包 consumers 均不含本仓;Root ABI 三份声明输出里,只有 abi/lumio_core.h 在镜像内——另两份(Rust / C# binding)所属 package 的 consumers 不含本仓,被 R-00263 的投影规则正确排除。

按「缺上游制品 → BLOCKED」我本该在这里停。但镜像里的 root-abi-bundle.json 声明了全部三份的摘要。实测:

复算 声明
compilerHash 217437fd…f290 ✅ = sha256(lumio_contract.py ‖ lumio_generate.py)
inputHash 696a58d0…5bb3 ✅ 由镜像内两份输入复算
CHeader / RustBinding / CSharpBinding ✅ 三份摘要全等

那两份不在镜像里的文件不是缺口:它们由锁定 compiler 重新生成、对着上游声明的摘要验证,本仓不发明任何东西。

「镜像里没有这个文件」和「本仓无法验证这个文件」是两件事。 前者成立、后者不成立,我差一步就把前者当后者报了 BLOCKED。

几处实现取舍

compiler 身份先于一切。 先跑再验等于已经执行了未经核对的代码。身份摘要口径由上游 compiler_hash() 固定,顺序与拼接方式都是摘要的一部分。

Input Hash 重算而不照抄 bundle。 抄下来只能证明「我读到了这个数」,重算才能证明「镜像里的输入确实就是产生这个数的那份」。

descriptor 的构造抽成 generate 与 verify 共用的唯一一处。 首版让 verify 跳过 descriptor 自身,注释写「完整性由它记的每一条都对得上间接证明」——那句是错的,往末尾加一个空格完全不被发现。hand_editing_any_generated_byte_makes_verification_fail 当场抓到,改为按同一规则重建后逐字节比对。

--plan 不再被静默忽略。 计划经 composition::verify_frozen_plan 读取(ADR 0006 第 8 条:消费者不得自建第二套解析器),其 architecture 基线与提交必须与本仓 lock 一致——否则「按 A 计划构建、按 B 基线生成 ABI」会一路无声走到运行时。

验证证据

验收项 证据
① Compiler/Input/Output Hash 完整 just generate-abi p0-linux 打印三者;verify 回读复算全部匹配
② 手改稳定失败 六份产物逐份各改一个字节,每次都被 OutputHashMismatch / UnregisteredFile 拦下(含 descriptor 自身)
③ 同输入重建零差异 两次生成到不同空目录,六份产物逐字节相同,outputHash 相等
④ 生成目录没有未登记文件 文件集合与登记表完全一致才通过:多一个是未登记,少一个是缺失

cargo test -p lumio-core-root-abi-generator 10/10;just check exit 0;just nextest 75/75;just check-contracts exit 0;spec-lint OK。

一处越界,理由在这里

justfilecheck-generated 并入了本卡的 verify。该 recipe 的原注释写着:「root-abi generator 的 verify-generated(LCE-P0-005)落地后按规格再并入本 recipe」——这是 LCE-P0-001 预留给本卡的接入点。不并入的话,生成物没有任何门禁守着,手改不会被任何检查发现(这正是上一张卡 P1-1 的形态)。对照组实测:改一个字节 → exit 3 → 重建 → 绿。

已知缺口

  • python3 是未登记工具依赖:锁定 compiler 是 Python,generator 执行路径上出现 python3,而 tools/tools.lock.toml 没有它(该文件只有 R-00265 能写)。已上报总调度;倾向按「同 git / node 的宿主基础设施」处理——两者也都在用且都没登记,单独给 python3 立规矩反而不一致。
  • §8.3 两处接口偏离compiler_path + compiler_digest 合并为 compiler_directory(compiler 身份由两个文件共同决定,单路径表达不了;期望摘要必须来自上游 bundle 而非调用方传入,否则调用方能绕过对账);build_plan: FrozenBuildPlan 改为 frozen_plan_path: Option<PathBuf>(Root ABI 输入集合全部来自上游 inputSet,计划的作用是交叉核对基线/提交)。两处都写进了类型文档。
  • worktree 布局下 just fetch-architecture-tools 需要 LUMIO_ARCHITECTURE_REPO:sync 脚本按 ../ / ../../ 找同级源仓,worktree 在 <repo>/.claude/worktrees/<name> 下找不到。属既有脚本行为,未在本卡修改。

🤖 Generated with Claude Code

Cui and others added 4 commits August 29, 2026 16:23
从只读镜像与锁定上游 compiler 产出 Header / C# Binding / Rust ContractTypes /
layout report,逐份与上游 bundle 声明摘要对账后只读发布(规格 §8、§3.6)。

本 crate 是**薄适配器**,不含模板、slot 表、type map、布局常量——那些全部属于上游
compiler。在本仓自己实现模板会制造第二个 ABI 定义处,两处定义迟早分叉(规格 §4)。
tests/no_private_schema.rs 用源码级断言盯着这条边界:代码里出现 C/C# 类型拼写、
生成文本片段、写死的布局常量,或引用架构源工作区,都会红。

开工前先做了可行性实测,结论改变了实现路径:上游 packages/index.json 的 12 个语言
生成包 consumers 均不含本仓,Root ABI 三份声明输出里只有 abi/lumio_core.h 在镜像内。
按「缺上游制品」本该报 BLOCKED,但镜像里的 root-abi-bundle.json **声明了全部三份的
摘要**;以镜像输入重跑锁定 compiler 的 emitter 后,三份摘要与声明值全等。所以那两份
不在镜像里的文件不是缺口:它们由锁定 compiler 重新生成、对着上游声明摘要验证,本仓
不发明任何东西。「镜像里没有这个文件」与「本仓无法验证这个文件」是两件事。

几处实现取舍:
- compiler 身份先于一切:先跑再验等于已经执行了未经核对的代码。身份摘要口径由上游
  compiler_hash() 固定(sha256(lumio_contract.py || lumio_generate.py)),顺序与拼接
  方式都是摘要的一部分。
- Input Hash 重算而不照抄 bundle:抄下来只能证明「我读到了这个数」,重算才能证明
  「镜像里的输入确实就是产生这个数的那份」。
- descriptor 的构造抽成 generate 与 verify 共用的**唯一**一处。首版让 verify 跳过
  descriptor 自身、注释写「完整性由它记的每一条都对得上间接证明」——那句是错的,
  往末尾加一个空格完全不被发现。hand_editing 测试当场抓到,改为按同一规则重建后
  逐字节比对。
- --plan 不再被静默忽略:计划经 composition::verify_frozen_plan 读取(ADR 0006 第 8 条:
  消费者不得自建第二套解析器),其 architecture 基线与提交必须与本仓 lock 一致,
  否则「按 A 计划构建、按 B 基线生成 ABI」会一路无声走到运行时。

justfile 的 check-generated 并入本卡的 verify —— 该 recipe 的原注释就写着「root-abi
generator 的 verify-generated(LCE-P0-005)落地后按规格再并入本 recipe」。不并入的话
生成物没有任何门禁守着,手改不会被发现。对照组实测:改一个字节 → exit 3 → 重建 → 绿。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
reviewer 四条 P1,共同的形态是「摘要链的上半段是真的,下半段是自证的」。他实测出两条
绕过路径都能让 just check-generated 绿灯,其中一条能把注入内容送进 C Header 与两份
binding。

P1-1 metadata/ 与 reports/ 两份产物没有任何独立锚点。它们唯一的约束是 descriptor 的
fileDigests,而 descriptor 正是从同一批盘上字节重建出来的——被背书者与背书者同源,
是恒等式不是校验。整份替换 ABI 文档再重建 descriptor,verify exit 0。
补锚点:ABI 文档锚回镜像里 inputSet 声明的那一份(逐字节相等,已被 inputHash 钉死);
layout report 锚回本次由上游 layoutProfile 现算的内容(那个值第 425 行本来就算出来了,
之前直接丢弃)。

P1-2 摘要链的根(上游 bundle)未与 lock 对账。read_bundle 无条件采信它声明的
compiler.digest / inputHash / outputFiles[].digest,而 lock 的 requiredPathSha256 里
正有这份文件的登记摘要——没读。改 bundle 里三个 outputFiles.digest 再重建 descriptor,
六份产物可被整体替换。改为读 lock 校验后才解析。

P1-3 require_compiler! 用 eprintln! + return 实现「跳过」——那等于测试通过,libtest 还
会把提示吞掉。在没取过工具链的机器上,6 个测试恒为绿且恒为空,而本卡四条通过条件的
全部证据都挂在它们身上。宏自己的注释还写着「静默跳过与通过长得一样,那正是 B-00002
要消灭的形态」,随后做的就是这件事。改为缺工具链即失败。

P1-4 卡面要求的上游 semantic validator 从未被调用,报告却无条件声称已通过。上游
emit_root_abi 的第一件事是 validate_abi_document(schema + ADR-040 语义),docstring 明写
「在写出任何一个输出字节之前拒绝非法 ABI 文档」;驱动脚本重写它的主体时把这次调用
跳掉了,同时 AbiCompatibilityReport 的 schema_valid / semantic_rules_valid / symbols_valid
写死 true——9 个字段里 7 个是常量或恒真式,compiler_lock.rs 那条 assert!(report.schema_valid)
是在断言一个字面量。
改为:驱动脚本先跑 validate_abi_document;schema/semantic 依据 descriptor 记录的
validatorRan(descriptor 已被逐字节重建校验,改不动);symbols 真查一次——ABI 文档声明的
entrySymbol 必须出现在发布的 C Header 里。

跑上游 validator 需要 lumio_contract 能按仓库根布局读 fixture(它在导入时就读),
所以用只读镜像的 schemas/ fixtures/ ids/ packages/ 加锁定 tools/ 在临时目录拼一个
一次性 contract root,用完即删;镜像本身不动,绝不使用架构源仓工作区。

新增负向测试覆盖 reviewer 实测的两条绕过、bundle 篡改、compiler 字节漂移分支。
13 个测试全绿(原 10 + 3)。

同批处理的 P2:
- no_private_schema.rs 的能力边界写进文档(前三条是关键词黑名单,reviewer 给出五种
  现成绕法),并补一条真正结构性的断言:src/ 下不得有非 .rs 文件——挡住「模板外置成
  templates/*.h.in 再 include_str!」这条最省事的路。
- descriptor 补输入逐文件摘要(§3.6):只有聚合 inputHash 时,镜像漂移只能说「不一致」,
  指不到是哪个文件。
- CLI 子命令改成规格 §8.4 的 verify-generated --generated。
- frozen_plan_path 的 Option 意味着核对可跳过,这条弱化写进注释与 ADR,不再只说「为什么
  是路径」。

沉淀 ADR 0009:摘要链每一环都要有外部锚点、descriptor 不得自证、报告不得声称没做过的
检查、上游输出集合以上游为准。判据写成可复用的一句:被背书者与背书者必须不同源。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
修 P1-4 时引入了 ADR 0009 第 1 节判据的直接违例:重建 descriptor 时,validatorRan 与
entrySymbol 是从**被校验对象自己**取回去的,于是逐字节比对对这两个字段恒真。reviewer
实测:只改 descriptor 一个字段、不动任何产物,verify-generated 直接 exit 0。

这条盲区正好落在 hand_editing 测试的形状之外——那个测试往每份文件**追加**一个空格,
追加会被重建比对抓到,**替换**自证字段的值不会。

两处分别修,理由不同:
- entrySymbol 有外部真值:镜像里的 ABI 文档(已被 inputHash → bundle → lock 钉死)。
  重建与 symbols 检查都改用它,descriptor 里那份只作记录不作判据。
- validatorRan 记录的是生成期事件,回读期**不可能**外部重建。所以不假装它受保护:
  重建时传字面量 true,任何非 true 的取值都造成字节不符而硬失败,语义上等于
  「自称 validator 没跑过的制品一律拒收」。这同时补上了「validatorRan=false 仍 exit 0」
  这个门禁洞。

原则写进 ADR 0009 第 3 节:**回读期无法外部重建的字段,不要假装它受保护**——要么以
字面量参与重建,要么换一个有外部真值的来源。同时订正三处做了错误安全声称的文档
(lib.rs、compiler.rs、ADR 第 3 节与后果段)——ADR 第 1 节的判据与第 3 节的机制此前是
互相矛盾的,那是本卡新写的规范,不能带着自相矛盾进主干。

新增 replacing_a_self_reported_descriptor_field_is_caught 覆盖三组替换。

同批 P2:
- 删掉未经 lock 校验的 read_bundle(它紧挨着 read_bundle_verified,下一个人有一半概率
  拿错那个)。
- ABI 文档路径统一由 input_set::abi_document_path 给出,并断言它**在 inputSet 内**——
  「在 inputSet 内」正是这个锚点成立的前提,此前只写在注释里;同时与 compiler 自报的
  路径交叉核对,上游改 ABI_DOCUMENT 时响亮失败。
- 临时 contract root 改排他 create_dir,与 publish.rs 同一口径。
- error.rs 补明 CompilerInvocationFailed 这一桶同时覆盖宿主环境缺失与 ABI 内容非法两类,
  不要按码值反推语义(reviewer 复审后不坚持拆码,理由是加进 validator 后这一桶里确实
  装着真实的内容失败)。

测试 13 → 15(compiler_lock 10 + no_private_schema 5);上轮交回物写的 13 是错的。
三摘要不变:217437fd… / 696a58d0… / bdbab5d3…。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
reviewer 复审放行时点名这条够格进 lessons:同一失效模式在本批出现两次同型
(ADR 0007 明写用 toml crate 却手写扫描;ADR 0009 写下「被背书者与背书者必须不同源」
却在同提交里让 descriptor 两个字段自证),相邻同族形态还有三次。

根因不是粗心:写规范与执行规范之间没有机械检查,而刚写完规范时最容易觉得自己已经
遵守了。五次里没有一次是通读代码发现的,全部来自实跑构造的反例——已有的 grep 自验
规则只能证明「被声称的 X 存在」,证明不了「X 覆盖的范围等于声称的范围」。

规避写成可验证的行为:判据与它的反例测试同一提交内同时出现;反例的构造方式要覆盖
多种形态(追加一个字节与替换某字段的值覆盖面完全不同);声称「由 X 覆盖」时另问一句
「X 挡不住的是什么」并写进文档。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@Go1c
Go1c merged commit 25808f3 into main Aug 29, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant