issue #439 / PR #443 的方向、架构、评审性质与下一步。结论基于本次会话对真实湖仓的独立复核,不引用 PR 自述。
五条范围在第 5 个提交(a43d9233,1,096 行)就已全部落地。第 11 轮起转向「把 provenance.py 写成更完整的 bash 解释器」,代码涨到 6,491 行。本次实测:第 5 提交 / 第 10 轮 / 第 47 轮三个版本,对 10,733 条真实归档跑出逐条完全相同的归属结果——多出来的 5,395 行对产出零影响。
runner.py 只从 provenance 取 9 个名字,接口面窄且稳——这一层设计没问题。但接口后面 78% 的文件是一个没有内部边界的手写 shell 前端:最长单函数 661 行、单类 612 行,与 GitLab API 客户端、job↔镜像匹配挤在同一个 6,491 行的模块里。
369 条线程中 124 条(33.6%)讨论的 shell 语义(case/heredoc/trap/!/pipefail/coproc…)在 4 个真实批、约 13,000 条 job 脚本里出现 0 次。真有一条例外且值得修:解释器自己引入的组合爆炸(第 41 轮已加界)。
A 片 = 归属判据重写(≤400 行,与 issue AC 一一对应);B 片 = Dockerfile 侧与 runner 侧的四条(约 1,300 行,已由 a43d9233 交付)。解释器整体丢弃,改为「文本级高召回登记 + 显式降级留痕」。三条闸同时立起来,否则下一个 PR 会重演。
下面每个数字都是本次会话现场跑出来的,不是从 PR 自述转抄的。复核只有两条路径:读 PR 分支的代码(本地 worktree),和只读查询 0904 批的真实湖仓(经 121 跳板 ssh -L 15432)。
| 事实 | 数字 | 怎么得到的 |
|---|---|---|
| 0904 批 job 脚本 | 3,260 条 / 338 种去重文本 | dev_ods.code_pipeline_jobs,batch_id LIKE '20260904%'(批 id 按源分片,共 1,559 片) |
含 docker build 的脚本 | 2 条 | 正则扫全批;其中只有 1 条含 ≥2 次 build,正是 issue 里的 openclaw job |
| PR 建模的 shell 形态命中 | 0 | heredoc / case / trap / pipefail / eval / coproc / 函数定义 / ! / 子壳 / sh -c / shopt / { …; } / timeout / env / exec / pushd / 进程替换 / 算术展开 —— 4 个批(0814/0826/0829/0904)全部为 0 |
| PR 自带回放(新判据 vs 旧口径) | 10,733 行 → 变 1 条 / 错 0 条 | 用 PR 的 parse_job_script + pick_dockerfile_for_image 跑 code_archives ⋈ code_pipeline_jobs |
| 三个版本对比 | 结果逐条相同 | a43d9233(1,096 行)/ a1f3ab56(1,853 行)/ d8b8408b(6,491 行)各跑一遍,均为 10,733 行 → 1 变 / 0 错 |
| 现场损坏(湖仓真值) | 10 个幽灵组件 | 归档 e789b0c0da15ee534a18cb28:archive_kind='frontend'(错),10 个组件 source_root_path 全 NULL、framework_kind='fe'、detection_method='cicd-dockerfile' |
| 错归属的 Dockerfile | 19 COPY / 13 带 --from | 湖仓 code_dockerfiles 里存的是 openclaw/Dockerfile(base-runtime);重复 dst:.×4、./×2、./patches×2 |
| 代码规模 | 876 → 6,491 行(+5,615) | 顶层 def/class:29 → 122;测试文件 1,126 → 8,930 行;整 PR 14,578 插入 / 14 文件 |
| CI 是否跑过 | 从未 | 每次 run 在 2–5 秒失败,steps == 0,注解原文:「The job was not started because recent account payments have failed…」 |
| PR 上的人类评论 | 0 条 | 123 条顶层评论:ysatnaf 66、copilot-swe-agent 54(全是报错)、codex 3。第 31 轮「请拍板」没有人类收件人 |
provenance.py 的规模随 PR 提交的漂移a43d9233):issue 的五条范围全部落地,此后 AC 守卫始终全绿。第 11 轮起曲线抬头,第 27 轮后加速——这一段对应的是「列表状态 / 帧栈 / 词法状态 / 函数体重放」这些 shell 语义机制。虚线为 main 上的基线 876 行。
这不是推论,是同一批数据上跑出来的三个点:
版本 A a43d9233 provenance.py 1,096 行 → 10,733 行回放:变 1 / 错 0
版本 B a1f3ab56 provenance.py 1,853 行 → 10,733 行回放:变 1 / 错 0
版本 C d8b8408b provenance.py 6,491 行 → 10,733 行回放:变 1 / 错 0
三个版本唯一改变的那条,都是 issue 里那一个 job:
xagent-openclaw-runtime:publish-image
openclaw/Dockerfile → Dockerfile (matched_by_tag = true, build_count = 2)
也就是说:从第 5 个提交到第 47 轮,35 轮评审、5,395 行代码、约 7,800 行测试,在一个 3,260 条脚本的真实批上,改变了 1 条产出——而那条产出在第 5 个提交就已经改对了。
形态普查跨了三周、四个批,分布是稳定的:
| 形态 | 0814 | 0826 | 0829 | 0904 |
|---|---|---|---|---|
| heredoc / case / trap / pipefail / eval / coproc | 0 | 0 | 0 | 0 |
函数定义 / ! / 子壳 / sh -c / shopt | 0 | 0 | 0 | 0 |
{ …; } / timeout / env / exec / pushd | 0 | 0 | 0 | 0 |
进程替换 / 算术展开 / --target | 0 | 0 | 0 | 0 |
docker build(脚本数) | 1 | 2 | 2 | 2 |
| job 总数 | 3,243 | 3,241 | 3,239 | 3,245 |
这个仓的 CI 是模板化的:约 3,240 条脚本收敛到 330 余种文本,绝大多数是 helm_package 包装的约定调用。docker build 全批只出现 1–2 次,正是本票要修的那类「一个 job 两次 build」的稀有形态。真实风险不是「脚本会写 case」,而是归属判据挑错 Dockerfile——那一点在第 5 个提交已经修好了。
「今天是 0,明天可能有脚本用 heredoc——解释器是保险。」
保险的方向错了。面对一个真实出现率 0、而误判代价是「归属错一个 Dockerfile」的输入,正确的工程响应是「响亮降级」而不是「完整模拟」:登记到的 build 解析不出来,就留一条显式降级记录(PR 自己已经有这个机制:_log.warning、health 诊断、capability 的 reason),让下一批的校验能看见并归因。一个 5,000 行的 shell 前端不是保险——它把「我看不懂这段脚本」这个可观测的降级,换成了「我的解释器可能答错」这个不可观测的风险。第 41 轮那条组合爆炸正是这份风险的实例。
runner.py 从 provenance 只导入 9 个名字,没有反向依赖,也没有把内部状态漏出去:
from ontoos_extract.code_extract.provenance import (
AmbiguousShaError, GitlabClient, Job, Pipeline, ParsedJob,
find_pipeline_for_commit, list_jobs, parse_job_script,
pick_dockerfile_for_image, pick_job_for_image, resolve_full_sha,
)
模块划分也基本正确:Dockerfile 的解析在 dockerfile_parser.py(+714 行),runner 的接线在 runner.py(+387 行),审计跟随者契约在 archive_health.py/status_audit.py。范围 2/3/4/5 落在该落的地方。另外 PR 有两条真的工程优点:守卫 + 变异两步验(450 条变异逐条转红)、以及与真 bash 逐条对照(PATH 里放假 docker 打印 -f 值)——后者不依赖作者自己对 bash 的判断,是本 PR 最强的收口手法。
| 维度 | 观察 |
|---|---|
| 模块边界 | 6,491 行里 约 5,070 行(78%)是同一个没有子边界的段落——段注释 # job script parsing 从 L606 一直管到 L5675。它同时装下了「GitLab API 客户端」「job↔镜像匹配」和一个完整的 shell 前端。 |
| 单元规模 | _parse_docker_builds 661 行、_BlockState 612 行、_ListState 318 行、_scan_segment 190 行。_BlockState 一个类就有 612 行。 |
| 引入的状态面 | 静态集合 40+ 张(_SHELL_CONTROL_WORDS、_COMMAND_WRAPPER_CHDIR_FLAGS、_BUILD_SUBCOMMANDS_BY_TOOL…),状态类 5 个(_ListState/_BlockState/_Nesting/_HeldBodies/_LexState),外加 8 条常设差分自查脚本。 |
| 新故障模式 | 解释器不是「读一段文本」,是「执行一段程序」——于是引入了执行语义才有的故障:组合爆炸、挂起、终止符分层、fork 边界。这些风险在本票之前不存在,是这次改动自己带进来的。 |
| 验证面 | 全部验证都是自证:CI 从未运行(组织计费拦截),全量 pytest 在本机也不可得(229 failed / 338 errors,PR 自述已逐文件分类为环境性)。没有任何第三方门禁在这个 PR 上留下过一个绿/红结论。 |
作者自己在第 31 轮把这件事说得很准,措辞比本报告更直接:
「方向已经偏移。 issue #439 的五条范围在第 ~10 轮就交付完毕,AC 守卫至今全绿;第 22 轮之后每一轮都在把 provenance.py 变成一个更完整的 bash 解释器。本轮 codex 的 13 条无一例外是更深的 shell 语义……新机制自己长出下一轮。」
PR #443 第 31 轮评论,2026-09-09 18:21
第 31 轮明确写了「请拍板」,并给了三条出路(收口 / 划边界 / 拆分)。这条评论是本 PR 上唯一一次要求人类裁决的动作,而 PR 上的人类评论总数是 0。约 8 小时后(第 32 轮),作者以「重看之后那 13 条分成三个机制而不是十三种形态」为由自行撤回了这个判断,又继续了 15 轮。
撤回的技术理由可能成立,但流程上不该由同一方完成:一旦「要不要继续这条轴」被交出去,撤回必须由交出去的那一方或更上游的人来做,否则这个闸在任何一次「我重看了一下」面前都是零成本可撤销的。今天 14:25 转 draft 是正确的一步——但它比该发生的时间晚了 15 轮。
先说结论:这些意见在技术层面大多是站得住的 bash 语义。我抽查了最近 18 条线程,每一条都能在 bash 里复现问题场景(shopt -q 只查不改、子壳里的 set -o pipefail 在 ) 处还原、exec 之后父 shell 不再继续扫、管道末段是函数调用时 pipefail 的状态归属……)。codex 与 ysatnaf 都没有在编造问题。
问题不在真假,在相关性和代价。按主题分类这 369 条线程:
| 主题 | 线程数 | 真实数据命中 |
|---|---|---|
shell 语义 / 解释器(case、heredoc、trap、!、pipefail、引号、词法、帧栈…) | 124(33.6%) | 全 0 |
| 归属 / tag 匹配 | 105(28.5%) | 真实命中 |
| Dockerfile 解析(COPY / stage / source-dir) | 22(6.0%) | 真实命中 |
| 同根去重 / FE 派发 | 8(2.2%) | 真实命中 |
| runner 接线 / 审计 / health | 4(1.1%) | 真实命中 |
| 其他(含跨主题) | 106(28.6%) | — |
也就是说:约三分之一的评审工作量,花在一个真实批里出现 0 次的输入面。这不是评审者的错——评审者只能按代码里写了什么来审;是代码先长出了这个面,评审随后被迫在里面自转。用 PR 正文自己的数字:45 轮里约 101 条意见是上一轮修复引入的回归,即约 28% 的评审产出是在给上一轮的修复擦屁股。这是「评审判据没有代价轴」的典型信号:如果每条意见都只问「这样会不会产出归属错」,反例是无穷的。
第 41 轮 ysatnaf 报的那条值得单独拎出来:「解不开的派发释放全部寄存体」是阶乘级的,一条 job script 就能把整批抽取挂死。这一条与前 123 条有本质区别——
作者在第 41 轮加了界(fan_out_seen 去重 + fan_out_depth 作用域),第 42 轮又按「上限按名字不按深度」收了一版。这条修复应当保留——但它的存在本身就是对第 2 节结论的最好佐证:一个手写执行器需要的不是更多语义,而是它自己的复杂度预算。
只做 issue 的范围 1,即让 docker build 的目标镜像能挑对 Dockerfile:
-f 的 docker build 按 build context 登记隐式 Dockerfile(相对 context,默认 .);每次 build 记 (dockerfile, tags);referenced_dockerfiles 列语义不变、只追加隐式项。${CI_PROJECT_NAME} 模板 → 完整 tag;无匹配才退回首个引用,并 WARNING。判据已在 a43d9233 里存在且经过回放验证(1 变 / 0 错),可以直接摘取。
| 范围 | 修什么 | 湖仓可观测的验收 |
|---|---|---|
| 2 同 dst 去重 | 同 dst 的 COPY 合并为一个组件,记 merged_copies | 10 个幽灵组件 → 收敛 |
| 3 同根去重 | FE 派发按解析后源码根去重;跟随组件记 shared-source-root | FE extractor cmd 同根 1 条(现在 13 条) |
| 4 bare COPY 源目录组件 | 真 Dockerfile 的 bare COPY → kind=source-dir | 目标 Dockerfile 的 source_root_path 不再恒 NULL |
5 backend-node 值域 | 无前端框架信号的 package.json 不再升级 frontend | archive_kind: frontend → backend-node |
这四条才有可被下批验证的产出面——它们直接对应上面复核到的 10 条 NULL 根组件与一个错误的 archive_kind。
不留分支、不做兼容、不逐条评估。它是为一个出现率 0 的输入面写的执行语义层,替换路径是降级留痕:解不出的形态在 health 诊断与 capability 的 reason 里响亮失败,让下一批的验收看得见。若将来真实批里真的出现 case 或 heredoc,那时手里会有真实的失败样本——按样本修,比按想象修便宜两个数量级。
steps == 0)。在这个状态下,任何 PR 的合并都是无门禁合入——所有校验都只能由提交方自证。这需要在组织层面解决,而不是在单个 PR 里绕过。
# 1) 形态普查(只读湖仓,经 121 跳板)
ssh -L 15432:<PG_HOST>:5432 -J root@121.41.239.88 xf@172.25.17.43 -N
# 然后对 dev_ods.code_pipeline_jobs 按 batch_id LIKE '20260904%' 取 script 做正则统计
# 2) 三版本回放对比
git worktree add --detach /tmp/ir439-a43d9233 a43d9233
git worktree add --detach /tmp/ir439-a1f3ab56 a1f3ab56
git worktree add --detach /tmp/ir439-pr origin/worktree-ir439
PYTHONPATH=<版本目录> python <回放脚本> # 对 code_archives ⋈ code_pipeline_jobs 跑新旧归属
# 3) 现场损坏核对
SELECT component_path, framework_kind, source_root_path, detection_method
FROM dev_ods.code_archive_components
WHERE archive_id = 'e789b0c0da15ee534a18cb28'; -- 10 行,source_root_path 全 NULL
注:批 id 按源分片(0904 批族共 1,559 片),按前缀 LIKE 聚合才是完整的 3,260 条;用 ods_current 查历史批次会得到 0 行而不是报错。