首页
/ 解读 `verify-reapply-patches` 基线漂移防护:通过 `pristine_hashes` SHA-256 校验消除更新后的误报失败

解读 `verify-reapply-patches` 基线漂移防护:通过 `pristine_hashes` SHA-256 校验消除更新后的误报失败

2026-09-07 12:16:00作者:仰钰奇

.changeset/vivid-eagles-climb.md 记录了 get-shit-done(GSD)中的一次关键缺陷修复(Bug #3657 / PR #3767):当 gsd-pristine/ 快照在备份之后被刷新到更新版本时,补丁重放校验器 verify-reapply-patches 会因拿错误的文件作为 diff 基线而把上游删除的内容误判为「用户新增行丢失」,产生大量假失败。本文将结合仓库中的 校验器实现重放工作流回归测试,完整还原这一问题产生的根因、修复机制、结构化的报告格式以及工作流中的集成方式,帮助你理解 GSD 如何在「防误报」与「绝不放过真实丢失」之间取得平衡。

一、背景:GSD 更新、本地补丁备份与三路合并

get-shit-done 是一套面向 Claude Code 的轻量 meta-prompting / 规范驱动开发系统,通过 /gsd:update 更新自身时会整体重装文件。如果用户直接改动过 GSD 的文件,安装器(bin/install.js 中的 saveLocalPatches)会在重装前按「哈希比对发现文件被修改」为依据,把修改过的文件备份到配置目录下的 gsd-local-patches/,随后用 /gsd:update --reapply(见 更新工作流重放工作流)把用户改动重新合并进新版本。

这个合并过程依赖三路比较pristine baseline(更新前 GSD 原始未改动版本)、user-modified backup(用户改动后的备份)和 newly installed version(重装后的新版本),从而区分「用户自定义(要保留)」与「版本漂移(要接受)」。

要让三路比较成立,必须拿到与备份同代的 pristine 基线。为此仓库维护了两类配套数据:

  • gsd-local-patches/backup-meta.json:备份元数据,其中 pristine_hashes 字段记录了每个被备份文件在更新前版本的 SHA-256(以相对路径为 key,正斜杠分隔,便于跨平台查找)。
  • gsd-pristine/:安装器通过 populatePristineDir 把当前安装的原始文件(经 copyWithPathReplacement 安装变换管线)暂存后复制而来,供合并时作为基线快照(参见 install.js 中该 helper 的定义与 saveLocalPatches 的调用)。

问题恰恰出在「备份」与「重放」之间隔着多次 GSD 更新:一旦 backup-meta.json 记录的基线版本早于磁盘上当前 gsd-pristine/ 快照版本,合并与校验就会被错误基线误导。

二、Bug #3657 根因:旧备份 + 新 pristine 导致 diff 反转

verify-reapply-patches 的校验模型是:用「用户新增行 = diff(备份文件, pristine 基线)」反推必须存活的行,再去已合并的安装文件里逐个确认它们存在(源码 computeUserAddedLines)。

当磁盘上的 gsd-pristine/ 在备份之后被刷新到更新版本时,diff 的方向就被反转了:

  • 备份文件基于 v_old 版本捕获;
  • 磁盘 pristine 却是 v_new,v_old 中某些被上游删掉的行在新基线里不存在;
  • 于是这些「上游在新版本删掉的行」被 computeUserAddedLines 计算成「用户新增行必须存活」;
  • 而它们本来就不该出现在新版本合并结果里,校验因此对每一行被上游删除的内容都报告 fail_user_lines_missing,即使真实存在的用户自定义已经完好存活。

这就是 changeset 里说的 spurious failure:不是用户丢内容,而是基线版本错位引发的假报警。校验器源码的注释(见 verify-reapply-patches.cjsREASON 枚举与 verifyFile 内关于 Bug #3657 的长注释)把这条推理链写得非常明确。

三、修复机制:校验 pristine 的 SHA-256,失配则跳过而不是误判

本次修复(changeset 主题、PR #3767)没有引入新参数,而是让校验器在每次把 pristine 当作 diff 基线之前,先验证它确实是 backup-meta.json 里记录的那个版本:

  1. main() 一次性读取 pristine_hashes:通过 readPristineHashes 解析 patchesDir/backup-meta.json;文件缺失、不可读或没有 pristine_hashes 字段时返回空对象——空哈希表表示「没有任何文件可做哈希校验」,不是错误。
  2. verifyFile() 对每个待校验文件做哈希比对(核心逻辑见 verify-reapply-patches.cjs):
    • 哈希匹配:磁盘 pristine 与备份同代,作为正确基线参与 diff;
    • 哈希失配:说明磁盘 gsd-pristine/ 已被后续 GSD 更新刷新过。此时不使用该 stale pristine,直接把文件以 status: ok + reason ok_pristine_drift_detected 上报并返回——绝不基于错误基线生成假失败列表;
    • 无记录哈希(旧版安装器未写 backup-meta.json):维持修复前行为,直接用磁盘 pristine,保证向后兼容。

实现细节值得一提:

  • 哈希用 Node cryptosha256 对文件 UTF-8 内容求 SHA-256 hex;
  • 查找 key 前把 relPath 的反斜杠统一替换成正斜杠(relPath.replace(/\\/g, '/')),以兼容 Windows 下 path.join 产生反斜杠而 backup-meta.json 存正斜杠的差异;
  • 若 pristine 目录/文件缺失或不可读,则回退到 over-broad 模式:把备份文件里每个「有意义的行」都当作必须存活的行来检查。over-broad 模式可能偏严(偏好假阳性停机),但永远不会因为另一个原因静默放行丢失内容——这正是 #2969 所要求的错误倾向方向。

四、稳定 reason 码与 JSON 报告结构

校验器的诊断面是一个被 Object.freeze 锁定的枚举 REASON,共 7 个稳定 code:

code 含义
ok_no_user_lines_vs_pristine 备份与 pristine 完全一致,无用户行可校验(非失败)
ok_no_significant_backup_lines 无 pristine 时备份也没有有意义的行(非失败)
ok_pristine_drift_detected 磁盘 pristine 哈希与记录不符,跳过该文件(非失败,#3657 新增)
fail_installed_missing 安装文件不存在
fail_installed_not_regular_file 安装路径不是普通文件
fail_read_error 备份/安装文件读取失败
fail_user_lines_missing 存在用户新增行未出现在合并结果中(唯一的真实失败)

drift 不等于 failure:该校验器进程退出码仍为 0,但输出中新增了顶层聚合字段,便于上层工作流区分处理(Bug #3657 Finding 1):

{
  "checked": 3,
  "failures": 0,
  "drifted": 1,
  "drifted_files": ["agents/gsd-executor.md"],
  "results": [
    { "file": "agents/gsd-executor.md", "status": "ok", "missing": [], "reason": "ok_pristine_drift_detected" }
  ]
}
  • 每条 per-file 结果结构不变(status/missing/reason),保持向后兼容;
  • drifted(数字计数)与 drifted_files(相对路径数组)为纯增量字段,干净运行(无任何漂移)时也恒存在且分别为 0[]

五、命令行使用方式与退出码

校验器为无依赖的 Node 脚本,位于仓库 get-shit-done/bin/ 下(安装后位于 ${GSD_HOME}/get-shit-done/bin/,亦可通过 SDK 的 sdk/dist/cli.js verify-reapply 暴露):

node get-shit-done/bin/verify-reapply-patches.cjs \
  --patches-dir <path>      # gsd-local-patches/
  --config-dir <path>       # ~/.claude(或运行时等价配置目录)
  [--pristine-dir <path>]   # gsd-pristine/;缺省时退化为 over-broad 启发
  [--json]                  # 输出结构化 JSON 而非人类可读文本

退出码约定:

  • 0:所有用户新增行均存活,门禁通过(含「检测到 drift 并跳过」的场景);
  • 1:至少一个文件存在真实缺失(fail_user_lines_missing 等失败 reason);
  • 2:用法/结构错误(如缺少 --patches-dir / --config-dir、补丁目录或配置目录不存在、未知参数)。

参数解析与各失败路径的健壮性设计在源码 parseArgsverifyFile 的 installed-path 检查段有完整注释说明:installed 路径必须存在、必须是普通文件、必须可读,任何一项不满足都以带诊断的 fail 结果返回,而不是直接让整个门禁崩溃丢失结构化输出。

六、工作流集成:Step 5a 的 drift 检查门

重放工作流 的 Step 5「Hunk Verification Gate」是双层门禁:

  • 5a:确定性校验器(binding gate,#2969)。此前 Step 5 依赖 LLM 自由文本的 verified: yes/no 逐 hunk 自报,而 #2969 追踪到大量「实际丢内容仍填 yes」的假通过,因此改为用脚本做结构化的逐行子串检查、任何 miss 都非零退出。
  • 5b:Hunk Verification Table 复核(advisory gate,#1999)。以 Step 4 生成的必填表格作为纵深防御,防止脚本本身出 bug 或 pristine 缺失时回归被静默放行。

本次修复为 5a 增加了 drift check(Bug #3657 Finding 2)。关键点在于:verify-reapply-patches 检测到 drift 时退出码是 0,仅仅看退出码会漏掉「文件被跳过、未真正校验」这一事实,所以工作流改为解析 JSON 顶层字段:

DRIFTED_COUNT="$(echo "$VERIFY_OUTPUT" | node -e "...String(d.drifted||0)...")"
DRIFTED_FILES="$(echo "$VERIFY_OUTPUT" | node -e "...(d.drifted_files||[]).forEach(...)...")"

随后执行判定:DRIFTED_COUNT > 0,STOP 并 HALT——置 DRIFT_DETECTED=trueexit 1,不得进入 5b 或清理步骤,同时给出三条可选处理路径:

  1. (a) 把 pristine 快照重新锚定(re-anchor)到 backup-meta.json 记录的那一版;
  2. (b) 从备份恢复受影响文件、手工重新合并用户自定义(cp {patches_dir}/{file} {installed_path});
  3. (c) 若接受上游变更,把 drift 文件的 pristine_hashes 项更新为当前磁盘哈希,再跑 /gsd:update --reapply 用刷新后的基线复检。

值得强调的是 drift-check 在源码中被结构性约束在 If VERIFY_STATUS is non-zero 检查之前执行(因为 drift 即便在退出码为 0 时也会出现),这一顺序也由测试强制锁定。工作流 Step 2 还提供了另一条更可靠的 git-aware 基线路径:当配置目录是 git 仓库(HAS_GIT=true)时,用 pristine_hashes 里记录的哈希去历史提交中定位 blob SHA-256 匹配的基线 commit,从而绕开「磁盘快照被多轮更新覆盖」的问题。

七、回归测试如何证明「不误报、也不放过」

bug-3657-verify-reapply-patches-pristine-drift.test.cjs 用真实可运行的 fixture(临时目录 + 构造 patches/installed/pristine + backup-meta.json)对修复做了成体系验证:

  • 核心回归:磁盘 pristine 为 v_new(哈希失配)、备份含 v_old 行 + 用户真实自定义行、合并结果包含 v_new 行 + 用户自定义行 → 断言退出码 0failures: 0、per-file reason 恰为 OK_PRISTINE_DRIFT_DETECTEDmissing 为空;
  • 反假阴性(counter-test):pristine 哈希匹配但用户行真实丢失 → 仍必须 exit 1 并报 fail_user_lines_missing,证明哈希保护不会压制真实失败;
  • backup-meta.json(旧安装器场景):保持修复前行为,照常 diff 并捕获丢失行;
  • 多文件混合:一个 drift 文件 + 一个真实失败文件 → 恰好 1 个 failure,drift 与真实失败的判定相互独立、按文件隔离;
  • REASON 枚举 shape-lock:用 Object.keys(REASON).sort() 深比较锁定完整 7 码集合,任何后续新增都要求同步更新该断言;
  • 报告结构(Finding 1):单文件/多文件 drift 时 drifted 计数与 drifted_files 聚合正确、干净运行恒为 0/[]
  • 工作流结构(Finding 2):断言 reapply-patches.md 源文件包含 Step 5a: drift check 区块、引用 DRIFTED_COUNTdrifted_files、设置 DRIFT_DETECTED 信号,且该区块必须出现在 If VERIFY_STATUS is non-zero 检查之前。

同一主题在仓库中还有多组配套测试可互相印证:tests/bug-2969-verify-reapply-patches.test.cjs(确定性门禁替代 LLM 自报)、tests/bug-2424-reapply-patches-baseline-detection.test.cjs(基线检测)、tests/bug-2998-pristine-dir-populated.test.cjsgsd-pristine/ 由安装器真正落盘),共同构成从「备份-基线-合并-校验」的完整证据链。

八、小结:这套修复的设计取舍

回顾整个修复,可以提炼出几条清晰的工程原则:

  1. 校验器绝不信任「当前磁盘状态天然正确」——pristine 是否与备份同代,必须由 backup-meta.json 里记录的 SHA-256 来裁决;
  2. 假报警要避免,但只能以「不改变错误方向」的方式避免——drift 时跳过 stale pristine 并回退 over-broad 模式,宁可偏严也不让真实丢失静默通过;
  3. 门禁信号必须结构化——用冻结的 reason 枚举 + 顶层 drifted/drifted_files 字段代替散落文本,让上层工作流(和测试)能够精确区分「失败」与「跳过待处理」;
  4. 纵深防御——确定性脚本(5a)与人工/LLM 复核表(5b)互为兜底,偏好「可恢复的假阳性停机」而非「不可恢复的静默成功」。

如果你的开发环境同样面临「本地修改回填 + 上游频繁刷新基线」的场景,这套「备份时记录基线哈希、比对前先验哈希、失配即降级并显式上报」的模式是一个可以直接借鉴的实现范式;相关的完整代码、测试与工作流说明均可在本仓库的 verify-reapply-patches.cjsreapply-patches.mdbug-3657 回归测试 中持续追踪。

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.14 K
2.74 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
857
1.35 K
docsdocs
暂无描述
Markdown
897
5.81 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
531
595
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
920
1.84 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.63 K
1.02 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.36 K
1.46 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
1.02 K
518
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
547
389