Codex 改完代码怎么审

先找回同一份 diff,再核对每条结论为何成立。

你能带走什么
  • 记录基点和目标快照,让报告能复查
  • finding 包含短行号跨度、引文和复现条件
  • 机械校验通过与代码安全分别判断
  • 修复后重查 diff 和测试,再决定合并发布

1. 从能复查的差异开始

Codex 改完代码后,我会先确认报告对应哪一份文件。分支又推进了一次,编辑器里还留着未保存的修改,或者测试跑在另一个目录,都可能让一句通过失去意义。审查时保留基点、目标版本和实际测试记录,后面讨论问题才有共同的依据。

这篇沿着一次标签显示改动讲清检查顺序,并附一个 Python 标准库实现的离线报告校验器。它会检查报告中的文件、行号和引文是否对得上冻结快照。发现漏洞、证明复现成立、决定严重度和批准发布,仍需另外完成。文中的四级标签、报告结构和验收流程由本文设计。

本文于 2026 年 10 月 5 日核对官方资料,在 Linux 执行了 codex-cli 0.159.2 的版本、帮助和参数冲突检查。没有调用真实模型、读取密钥或进行漏洞利用。七张图为原创示意图;网页交互与真实审查界面未纳入本次 GUI 验证。

FIGURE 01
main 从 A 到 B、C、D;任务分支从 B 到 E、F。共同祖先是 B,审查目标是 F,另行记录未提交快照。
图中的字母代表示意提交。本文流程把范围记录为可复查的快照,具体 SHA 从当前仓库取得。

2. 先固定基点,再决定审查范围

审分支时,记录目标分支名和 HEAD 的完整 SHA,再记录两者的共同祖先。分支名会移动,旧报告只写 main,几天后就很难还原当时比较的内容。下面先查看本地已有引用;是否更新远端引用,应由当前工作流程决定,不在审查开始时隐式改变基线。

终端 · 只读记录基线
git rev-parse --show-toplevel
git status --short
git rev-parse HEAD
git rev-parse main
git merge-base main HEAD
git diff --no-ext-diff --no-textconv main...HEAD --stat
git diff --no-ext-diff --no-textconv main...HEAD

Git 的三点比较使用共同祖先作为一端,区别于直接比较两个端点。Git diff 文档解释了这些形式。main 只是示例名称;找不到引用时先确认目标,不要临时猜一个能运行的分支。本文的 base_commit 表示选定基点,未提交审查另记 HEAD 与内容快照,单个 SHA 无法代表尚未提交的文件。

分支审查前,还要确认仓库是否带有浅克隆限制,以及当前目标是否为合并提交。共同祖先不存在时应先补齐历史;对合并提交要说明采用哪一个父提交视角,不能沿用普通提交的直觉。将重命名、删除和子模块更新单独列出,避免只盯新增行漏掉行为变化。

把允许修改的文件或目录、任务目标和排除项也写清楚。审查范围可以大于修改范围,例如为了判断一个条件分支,需要阅读调用方。遇到无关变更,先问清来源和作用;工作树里可能有用户早先的修改,不能因它超出本轮任务就直接丢弃。

3. 把暂存、新增和工作区改动一起核对

普通 git diff 展示工作树相对暂存区的变化,git diff --cached 检查暂存区相对 HEAD 的变化。未跟踪文件还要从状态列表找出来。依次检查这三类内容,才能判断本轮交付到底包含什么。Git status 文档给出了短格式及未跟踪路径的标记。

终端 · 未提交审查的手工核对
git status --short
git diff --no-ext-diff --no-textconv
git diff --cached --no-ext-diff --no-textconv
git diff --check
git diff --cached --check
FIGURE 02
三列分别表示未暂存、已暂存和未跟踪内容,再汇入 Codex uncommitted 审查范围。
未跟踪文件没有自动出现在普通 git diff 中。新增文件也要核对内容、用途和是否含秘密。

对状态中的新增文件,单独打开检查。关注临时日志、生成产物、配置副本是否被意外加入。空白检查通过只说明没有发现该类格式问题。代码审查结束后再记录一次状态;若期间出现新修改,确认报告还能覆盖当前内容。

4. 按本机帮助选择 review 入口

当前官方命令参考和本机帮助都提供 codex review。--uncommitted 包含已暂存、未暂存与未跟踪变化;--base 选择基准分支;--commit 检查一个提交引入的变化。这些目标与自定义 PROMPT 互斥,--title 需要搭配 --commit。先选一种模式,避免把多个示例连成一条命令。

终端 · 按需选一条;执行审查会调用模型
codex review --uncommitted
codex review --base main
codex review --commit HEAD
codex review "检查当前工作树的标签显示,给出文件行号和复现依据"

脚本化入口还可以使用 exec review。本机帮助列出了 --json 和 -o,前者输出事件 JSONL,后者保存最后一条消息。事件流与本文的报告 JSON 是不同格式,需要人工或受信转换步骤整理,不能直接把事件文件交给下面的校验器。命令定义见官方命令参考。

终端 · 先检查帮助,再按自己的权限配置运行
codex --version
codex review --help
codex exec review --help

# 以下两条是运行示例,本文没有调用模型验证
codex exec review --uncommitted -o review.txt
codex exec review --base main --json > review-events.jsonl

本文实际确认了两组冲突参数返回退出码 2,没有启动审查会话。帮助命令成功仅证明接口存在,不能证明账户、额度、模型权限或某个仓库审查成功。升级后重新检查帮助;若需定制审查标准,可以选择自定义指令模式,并把要比较的范围说明完整。

5. 让 finding 落到具体文件和触发条件

一条可处理的 finding 至少包含文件、行号范围、证据引文、因果解释、最小复现和影响等级。行号应指向此次审查的快照,跨度尽量短,能够看到触发问题的条件即可。只有一句可能有风险,读者很难判断该先补充信息还是改代码。

本文 fixture 把零数量的标签变成空字符串。报告引用新增分支,输入为零,预期显示 0 items,实际显示为空。这个例子只涉及显示行为;预期成立还依赖产品约定。如果调用方本来就要隐藏零数量,报告可能误判,复核时需要把上下文补上。

FIGURE 03
四个区域呈现文件定位、输入输出、因果解释与人工复核;引文存在不等于判断正确。
文件行号、引文和复现条件放在同一条记录中。报告读者可以直接核对证据与行为是否相符。
fixture/change.diff · 人工构造的良性变更
--- a/src/labels.py
+++ b/src/labels.py
@@ -1,2 +1,4 @@
 def count_label(count):
+    if count == 0:
+        return ""
     return f"{count} items"

复现步骤应能在隔离环境里执行,记录前置条件、输入、预期和实际结果。涉及线上数据或权限时,先把问题缩成脱敏样例。官方代码审查说明建议把触发输入与预期、实际行为写入审查标准,也说明审查后的修复仍受通常权限设置约束。

6. 先排误报,再讨论严重度

严重度描述影响,证据强弱描述我们对结论的把握,这两件事需要分别记录。确认输入是否能到达问题分支,检查上游验证和调用约束,再估计受影响的用户或功能。不要因为代码看上去危险,就跳过可达性检查。

教学报告采用 low、medium、high、critical 四级标签,具体含义见图。这套标签没有对应 CVSS 计算,也不声称等同 Codex 的优先级格式。接入团队流程时,沿用团队已有标准,并给出影响依据;需要更多证据的审查保留 unknown 和缺失原因。

FIGURE 04
左侧列出自定义 critical、high、medium、low;右侧核对触发条件和影响,不确定时保留 unknown。
本文四级标签服务于教学报告,没有自动评分公式。实际团队可以使用既有标准并写明依据。

判断误报时保留理由,便于下次审查复用。例如某个输入已被调用方限制,应引用限制发生的位置,并确认所有入口都会经过它。无法稳定复现的现象可以继续调查,但不要为了让报告完整而补写不存在的观察记录。

去重同样值得做。多个说法可能指向同一条件分支,应合并为一条能处理的记录。本文程序拒绝相同标识,以及相同文件、跨度和引文的重复记录;语义相近但引文不同的问题仍靠人工合并。一次空 findings 只表示这次报告没列出问题,不能用于宣布代码安全。

7. 测试证据要对应最终文件版本

先读项目已有的测试说明、构建配置和相关测试,再选择命令。记录工作目录、命令、退出码和必要输出。单元测试覆盖局部分支,集成测试检查组件连接,界面问题还需要观察真实交互;各自的覆盖边界要写清楚。

遇到失败,区分原有失败、本次回归和环境阻塞。缺少服务或依赖时,把未运行项留下,别用另一条较窄的命令冒充完整验收。修复后保留同一输入的前后对比,再运行受影响的回归测试。测试新增了快照或锁文件,也要把这些变化纳入最终 diff。

依赖和配置常常藏在实现旁边。核对包版本及锁文件是否一致,检查安装脚本、构建插件、CI 工作流、网络地址和权限变化。自动生成的大文件可以分段看,不能因为篇幅长就整段跳过。升级依赖的必要性、兼容性测试和回滚方法应随修改一起交接。

8. 仓库内容、秘密与审批都要有边界

陌生仓库的说明、注释、Issue 摘录和日志可能含有指令式文字。让代理忽略审批、上传环境变量或执行额外脚本的内容,应按待审数据处理。AGENTS.md 能承载项目约定,但其中写一个禁止目录,不能替代工具层面的访问限制。

FIGURE 05
受信范围与权限控制、待审仓库内容和受保护资源分为三区,数据区与受保护资源间有明确授权边界。
真实运行还受到组织策略、沙箱与工具权限约束。AGENTS.md 可以承载约定,但文件文字本身无法强制隔离目录。

先阅读依赖脚本,再决定是否运行;把审查安排在权限受控的副本中,减少能接触的秘密。日志只保留复现所需片段,去掉令牌、身份信息和线上载荷。发现凭据泄露时停止扩散,记录脱敏位置并按团队流程处置,不把完整秘密粘贴到公开审查评论。

沙箱约束文件与网络等能力,审批处理需要进一步判断的动作。代码审查与动作审批中的自动审查承担不同工作。官方沙箱说明指出自动审批审查不会改变沙箱边界;安全指南也提醒容器内可见的凭据仍可能面临风险。不要为了跑通审查关闭所有保护,遇到拒绝应缩小任务或补齐必要授权。

9. 用离线程序检查报告是否自洽

review_guard.py 读取受信 scope、冻结快照和待审报告。scope 保存基点标识、精确文件白名单、文件哈希和新增侧变化行。它必须由宿主或人工核对后提供,不能把模型生成的范围当成自己的验证依据。fixture 中全为 a 的标识是合成值,不对应真实 Git 提交。

校验器拒绝越界路径、符号链接、超出文件的行号、未覆盖变化行的跨度、空引文、未知字段、重复记录和不完整复现。引文必须出现在给定跨度内,但程序不会理解引文含义,也不会执行复现步骤。severity 只检查标签是否合法,不判断等级是否合理。

FIGURE 06
受信 scope 与冻结快照进入结构、范围证据、报告完整性三组检查,输出格式状态且永不声称证明安全。
scope 必须由受信宿主提供。本文实现不解析任意 Git patch,不支持删除侧、重命名或二进制文件审查。

这个版本面向受信、静止的小型文本快照。它不解析任意 Git patch,也不处理删除侧、重命名或二进制差异。路径检查到读取之间存在并发替换窗口,哈希只能核对实际读到的版本;因此应在受控目录冻结快照。文件大小在读取后检查,不能把这一点宣传为恶意大文件的内存隔离。

状态 validated 只表示结构与这些机械检查通过。unknown 要有原因,命令退出码为 3;不合格报告退出 2;结构通过退出 0。无论 findings 是否为空,输出的 safety_proven 都是 false。接入 CI 时,把它当作报告格式检查步骤,发布判断需要其他验收证据。

10. 复制三份源码即可离线运行

下面完整给出生成器、校验器和测试。把三份文件保存到同一空目录,使用 Python 3.10 或更新版本执行。生成器创建小型 fixture 目录,包含变更前后文件、diff、scope 和报告;它不调用 Git、网络或模型。校验器仅从参数指定的位置读取,测试里的命令外观字符串始终作为数据。

终端 · 本地离线步骤
python3 make_fixture.py
python3 review_guard.py --root fixture \
  --scope fixture/scope.json --report fixture/report.json
python3 -m unittest -v test_review_guard
展开 fixture 生成器完整源码
make_fixture.py
"""Create a benign, synthetic review snapshot. No Git or model calls."""
import difflib
import hashlib
import json
from pathlib import Path

BEFORE = '''def count_label(count):
    return f"{count} items"
'''
AFTER = '''def count_label(count):
    if count == 0:
        return ""
    return f"{count} items"
'''
BASE = "a" * 40  # Synthetic identifier, not an actual Git commit.


def write_fixture(root):
    root = Path(root)
    (root / "src").mkdir(parents=True, exist_ok=True)
    (root / "before").mkdir(exist_ok=True)
    (root / "src/labels.py").write_text(AFTER, encoding="utf-8")
    (root / "before/labels.py").write_text(BEFORE, encoding="utf-8")
    diff = difflib.unified_diff(
        BEFORE.splitlines(True), AFTER.splitlines(True),
        fromfile="a/src/labels.py", tofile="b/src/labels.py")
    (root / "change.diff").write_text("".join(diff), encoding="utf-8")
    scope = {"schema_version": 1, "base_commit": BASE, "files": {
        "src/labels.py": {
            "sha256": hashlib.sha256(AFTER.encode()).hexdigest(),
            "changed_lines": [2, 3]}}}
    report = {"base_commit": BASE, "status": "complete",
              "limitations": ["Synthetic fixture; no model or security scan."],
              "findings": [{
                  "id": "F001", "file": "src/labels.py",
                  "line_start": 2, "line_end": 3,
                  "title": "Zero count loses its visible label",
                  "severity": "low",
                  "evidence": {"quote": 'return ""',
                               "reason": "The zero branch returns empty text."},
                  "repro": {
                      "steps": ["Call count_label(0) in an isolated unit test."],
                      "expected": "0 items", "observed": "empty string"}}]}
    for name, value in (("scope.json", scope), ("report.json", report)):
        (root / name).write_text(json.dumps(value, indent=2), encoding="utf-8")
    return scope, report


if __name__ == "__main__":
    write_fixture(Path(__file__).resolve().parent / "fixture")
展开 报告校验器完整源码
review_guard.py
"""Offline report-shape validator. It does not discover vulnerabilities."""
import argparse
import hashlib
import json
import re
from pathlib import Path

LIMIT = 1_000_000
SHA = re.compile(r"[0-9a-f]{40}|[0-9a-f]{64}")
DIGEST = re.compile(r"[0-9a-f]{64}")
SAFE = re.compile(r"[A-Za-z0-9_.-]+(?:/[A-Za-z0-9_.-]+)*")


class Invalid(ValueError):
    pass


def need(ok, code):
    if not ok:
        raise Invalid(code)


def obj(value, keys, code):
    need(type(value) is dict and set(value) == set(keys.split()), code)


def text(value, code):
    need(type(value) is str and 0 < len(value.strip()) <= 4000, code)


def no_duplicates(pairs):
    result = {}
    for key, value in pairs:
        need(key not in result, "duplicate_json_key")
        result[key] = value
    return result


def load(path):
    raw = Path(path).read_bytes()
    need(len(raw) <= LIMIT, "input_too_large")
    return json.loads(raw.decode("utf-8"), object_pairs_hook=no_duplicates)


def safe_path(root, name):
    need(type(name) is str and SAFE.fullmatch(name) is not None,
         "unsafe_path")
    need(all(p not in (".", "..") for p in name.split("/")), "unsafe_path")
    path = root
    for part in name.split("/"):
        path = path / part
        need(not path.is_symlink(), "symlink_rejected")
    need(path.resolve().is_relative_to(root), "path_outside_root")
    need(path.is_file(), "snapshot_missing")
    return path


def snapshots(scope, root):
    obj(scope, "schema_version base_commit files", "scope_schema")
    need(type(scope["schema_version"]) is int
         and scope["schema_version"] == 1, "scope_version")
    base = scope["base_commit"]
    need(type(base) is str and SHA.fullmatch(base) is not None, "scope_base")
    need(type(scope["files"]) is dict and bool(scope["files"]), "scope_files")
    result = {}
    for name, spec in scope["files"].items():
        obj(spec, "sha256 changed_lines", "file_schema")
        digest = spec["sha256"]
        need(type(digest) is str and DIGEST.fullmatch(digest) is not None,
             "snapshot_hash_schema")
        raw = safe_path(root, name).read_bytes()
        need(len(raw) <= LIMIT, "snapshot_too_large")
        need(hashlib.sha256(raw).hexdigest() == digest, "snapshot_changed")
        lines = raw.decode("utf-8").splitlines()
        changed = spec["changed_lines"]
        need(type(changed) is list and bool(changed), "changed_lines_schema")
        need(all(type(n) is int and 1 <= n <= len(lines) for n in changed),
             "changed_line_range")
        need(len(changed) == len(set(changed)), "duplicate_changed_line")
        result[name] = (lines, set(changed))
    return result


def validate(report, scope, root):
    root = Path(root).resolve(strict=True)
    files = snapshots(scope, root)
    obj(report, "base_commit status findings limitations", "report_schema")
    need(report["base_commit"] == scope["base_commit"], "base_mismatch")
    need(report["status"] in ("complete", "unknown"), "status_unknown")
    limits = report["limitations"]
    need(type(limits) is list and len(limits) <= 100, "limitations_schema")
    for value in limits:
        text(value, "limitation_empty")
    need(report["status"] != "unknown" or bool(limits), "unknown_no_reason")
    findings = report["findings"]
    need(type(findings) is list and len(findings) <= 1000, "findings_schema")
    ids, places = set(), set()
    for finding in findings:
        obj(finding, "id file line_end line_start title severity evidence repro",
            "finding_schema")
        for key in ("id", "title"):
            text(finding[key], "finding_" + key)
        need(finding["id"] not in ids, "duplicate_id")
        ids.add(finding["id"])
        name = finding["file"]
        need(type(name) is str and SAFE.fullmatch(name) is not None,
             "unsafe_path")
        need(all(p not in (".", "..") for p in name.split("/")), "unsafe_path")
        need(name in files, "file_outside_scope")
        lines, changed = files[name]
        start, end = finding["line_start"], finding["line_end"]
        need(type(start) is int and type(end) is int, "line_type")
        need(1 <= start <= end <= len(lines) and end - start < 10,
             "line_range")
        need(any(n in changed for n in range(start, end + 1)),
             "no_changed_line")
        need(finding["severity"] in ("low", "medium", "high", "critical"),
             "severity_unknown")
        evidence = finding["evidence"]
        obj(evidence, "quote reason", "evidence_schema")
        text(evidence["quote"], "evidence_empty")
        text(evidence["reason"], "reason_empty")
        need(evidence["quote"] in "\n".join(lines[start - 1:end]),
             "evidence_not_in_span")
        key = (name, start, end, evidence["quote"])
        need(key not in places, "duplicate_finding")
        places.add(key)
        repro = finding["repro"]
        obj(repro, "steps expected observed", "repro_schema")
        steps = repro["steps"]
        need(type(steps) is list and 0 < len(steps) <= 16, "repro_steps")
        for step in steps:
            text(step, "repro_step_empty")
        text(repro["expected"], "repro_expected")
        text(repro["observed"], "repro_observed")
    state = "validated" if report["status"] == "complete" else "unknown"
    return {"state": state, "finding_count": len(findings),
            "safety_proven": False,
            "note": "Structure only; evidence meaning and reproduction need review."}


def main(argv=None):
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("--root", required=True)
    parser.add_argument("--scope", required=True)
    parser.add_argument("--report", required=True)
    args = parser.parse_args(argv)
    try:
        result = validate(load(args.report), load(args.scope), args.root)
    except (Invalid, OSError, UnicodeError, ValueError, TypeError, RecursionError) as e:
        code = str(e) if isinstance(e, Invalid) else type(e).__name__
        print(json.dumps({"state": "rejected", "error": code,
                          "safety_proven": False}))
        return 2
    print(json.dumps(result, ensure_ascii=False))
    return 3 if result["state"] == "unknown" else 0


if __name__ == "__main__":
    raise SystemExit(main())
展开 31 个测试的完整源码
test_review_guard.py
import contextlib
import copy
import io
import json
import tempfile
import unittest
from pathlib import Path

from make_fixture import write_fixture
from review_guard import Invalid, load, main, validate


class GuardTests(unittest.TestCase):
    def setUp(self):
        self.temp = tempfile.TemporaryDirectory()
        self.addCleanup(self.temp.cleanup)
        self.root = Path(self.temp.name)
        self.scope, self.report = write_fixture(self.root)

    def check(self, report=None):
        return validate(report or self.report, self.scope, self.root)

    def bad(self, code):
        with self.assertRaisesRegex(Invalid, "^" + code + "$"):
            self.check()

    def finding(self):
        return self.report["findings"][0]

    def test_valid_report(self):
        self.assertEqual(self.check()["state"], "validated")

    def test_valid_is_not_safe(self):
        self.assertFalse(self.check()["safety_proven"])

    def test_empty_is_not_safe(self):
        self.report["findings"] = []
        result = self.check()
        self.assertEqual(result["finding_count"], 0)
        self.assertFalse(result["safety_proven"])

    def test_unknown(self):
        self.report["status"] = "unknown"
        self.assertEqual(self.check()["state"], "unknown")

    def test_unknown_needs_reason(self):
        self.report.update(status="unknown", limitations=[])
        self.bad("unknown_no_reason")

    def test_unknown_status(self):
        self.report["status"] = "safe"
        self.bad("status_unknown")

    def test_unknown_field(self):
        self.report["approved"] = True
        self.bad("report_schema")

    def test_base_mismatch(self):
        self.report["base_commit"] = "b" * 40
        self.bad("base_mismatch")

    def test_file_outside_scope(self):
        self.finding()["file"] = "src/other.py"
        self.bad("file_outside_scope")

    def test_traversal(self):
        self.finding()["file"] = "../labels.py"
        self.bad("unsafe_path")

    def test_absolute(self):
        self.finding()["file"] = "/tmp/labels.py"
        self.bad("unsafe_path")

    def test_windows(self):
        self.finding()["file"] = "C:\\labels.py"
        self.bad("unsafe_path")

    def test_zero_line(self):
        self.finding()["line_start"] = 0
        self.bad("line_range")

    def test_line_past_end(self):
        self.finding()["line_end"] = 999
        self.bad("line_range")

    def test_reversed_lines(self):
        self.finding().update(line_start=3, line_end=2)
        self.bad("line_range")

    def test_bool_line(self):
        self.finding()["line_start"] = True
        self.bad("line_type")

    def test_no_changed_line(self):
        self.finding().update(line_start=4, line_end=4)
        self.bad("no_changed_line")

    def test_empty_evidence(self):
        self.finding()["evidence"]["quote"] = " "
        self.bad("evidence_empty")

    def test_wrong_evidence(self):
        self.finding()["evidence"]["quote"] = "missing_call()"
        self.bad("evidence_not_in_span")

    def test_duplicate_id(self):
        self.report["findings"] *= 2
        self.bad("duplicate_id")

    def test_duplicate_finding(self):
        second = copy.deepcopy(self.finding())
        second["id"] = "F002"
        self.report["findings"].append(second)
        self.bad("duplicate_finding")

    def test_unknown_severity(self):
        self.finding()["severity"] = "P0"
        self.bad("severity_unknown")

    def test_missing_repro(self):
        self.finding()["repro"]["steps"] = []
        self.bad("repro_steps")

    def test_snapshot_changed(self):
        (self.root / "src/labels.py").write_text("changed\n")
        self.bad("snapshot_changed")

    def test_scope_traversal(self):
        spec = self.scope["files"].pop("src/labels.py")
        self.scope["files"]["../labels.py"] = spec
        self.bad("unsafe_path")

    def test_symlink(self):
        path = self.root / "src/labels.py"
        path.unlink()
        path.symlink_to(self.root / "before/labels.py")
        self.bad("symlink_rejected")

    def test_duplicate_json_key(self):
        path = self.root / "duplicate.json"
        path.write_text('{"status":"complete","status":"unknown"}')
        with self.assertRaisesRegex(Invalid, "duplicate_json_key"):
            load(path)

    def test_commands_remain_text(self):
        self.finding()["repro"]["steps"] = ["touch SHOULD_NOT_EXIST"]
        self.assertEqual(self.check()["state"], "validated")
        self.assertFalse((self.root / "SHOULD_NOT_EXIST").exists())

    def run_cli(self):
        path = self.root / "report.json"
        path.write_text(json.dumps(self.report))
        output = io.StringIO()
        with contextlib.redirect_stdout(output):
            status = main(["--root", str(self.root), "--scope",
                           str(self.root / "scope.json"), "--report", str(path)])
        return status, json.loads(output.getvalue())

    def test_cli_success(self):
        self.assertEqual(self.run_cli()[0], 0)

    def test_cli_unknown(self):
        self.report["status"] = "unknown"
        self.assertEqual(self.run_cli()[0], 3)

    def test_cli_reject(self):
        self.report["approved"] = True
        status, result = self.run_cli()
        self.assertEqual(status, 2)
        self.assertEqual(result["state"], "rejected")


if __name__ == "__main__":
    unittest.main()

本次真实执行了 31 个测试,覆盖路径越界、Windows 路径、行号边界、布尔值误作行号、哈希变化、重复 JSON 键、重复 finding、无证据、未知状态及空结果边界。另从最终 HTML 提取三份源码重新执行,结果通过。示例报告的结果是 validated、finding_count 为 1、safety_proven 为 false。

这些测试验证程序声明的契约,没有衡量模型找问题的能力,也没有扫描真实项目。读者若扩展支持更多 diff 类型,先补对应 fixture 和失败测试,再扩大输入范围。本文页面内的源码已经完整,不依赖不存在的下载入口。

11. 把自动修复留在已确认的范围内

确认 finding 后,可以让 Codex 准备最小修复和相关测试。先说明允许变更的文件、要保留的行为和完成标准;若修复引入新依赖或扩大权限,重新讨论范围。自动修复产生了新版本,原审查结论不能自动覆盖它。

FIGURE 07
代理准备 diff 和测试,人决定范围与发布;修改产生新快照后重查差异和回归结果。
人工验收需要看到最终文件版本和测试证据。若环境阻塞,记录未完成项并等待对应决定。

修复也可能只是改变了测试断言,让旧错误看上去通过。因此要回到最初的预期行为,检查断言是否仍能发现原来的问题。若产品决定接受新的行为,记录这次决定和适用范围,再调整测试,后续维护者才知道变化的原因。

最终交接保留基点与目标快照、已处理问题、实际测试和剩余限制。合并、发布、生产操作或权限调整由获得相应授权的人决定。需要继续验证的项目写清下一步条件,避免一句已完成掩盖尚未运行的集成测试。

我更愿意收到一份短而具体的报告:哪一行有问题,什么输入会触发,怎样确认,改后验证了什么。有了这些证据,团队才能复查结论、安排处理顺序,并在版本变化后知道哪些检查需要重做。

12. 来源与验证记录

本文由 pan 撰写,命令接口以本机 0.159.2 帮助为证,并对照下列官方资料。网页会随产品更新,实际使用时以安装版本和生效策略为准。流程、图示、JSON 契约和 Python 实现均为本文教学设计。

13. 补充实验:把离线快照接到真实 Git 对象

前面的生成器刻意使用合成基点,原有校验器也已说明只检查受信快照与报告是否自洽。接下来补上一个独立的小实验:在临时 Git 仓库里创建真实提交,先固定审查端点,再从目标提交的 blob 导出文件。原来的三份 Python 源码保持不变。

共同祖先中的 count_label(0) 返回 0 items。main 从这里增加一份 release.txt,feature 从同一祖先增加隐藏零数量标签的分支。这样的分叉能把比较范围的差别显示出来。所有提交都是本实验新建的教学数据,作者字段为 Provenance Fixture,时间固定为 2026-10-13 00:00:00 UTC,以便复现对象 ID;它们不代表实际项目历史或作者的开发经历。

本补充于 2026 年 10 月 13 日在 Linux、Git 2.52.0、Python 3.12.14 下执行。实验只写入新建的临时仓库和明确指定的结果目录,不安装软件,不读取用户仓库,不调用模型,也不接触生产系统。源码使用 Python 标准库,需要本机已安装 Git。这里固定使用 SHA-1 对象格式。

共同祖先分出 main 与 feature,固定 feature 提交中的 blob 导出快照
补充图 A:原创实验图。图中缩写只帮助阅读,下方记录和检查使用完整对象 ID。

这个例子刻意只支持一个普通 UTF-8 文本文件 src/labels.py,模式为 100644,且祖先和目标内容必须与前面的两份标签源码完全一致。changed_lines 固定为已核对的第 2、3 行,不从任意 patch 推断。重命名、删除侧、二进制、符号链接、可执行文件、子模块、浅仓库、多个共同祖先和合并提交均不在本实验的接入范围内;普通文件的其他内容也会被拒绝。不要把这几段代码直接包装成通用 Git 安全检查器。

14. 完整记录两个端点和两份 diff

Git 的 git-diff 文档说明,A..B 比较 A 与 B 两个端点,A...B 使用共同祖先与 B 比较。这里 main..feature 多出 release.txt 的删除差异,是因为该文件只存在于 main 的端点;feature 并没有执行删除它的提交。main...feature 只显示分叉以后 feature 的标签改动。这个解释针对 git diff,不能直接套到 git log 的点号范围。

记录中的 main、target 和 base 分别是捕获时的主分支端点、审查目标和唯一共同祖先。git-merge-base 文档允许某些历史存在多个最佳共同祖先,因此本实验用 --all 并要求恰好一个。检查器还要求两个端点分别是这个祖先的直接子提交,避免把更复杂的历史误当成本例。

{
  "main": "0e1899c60081fff643e71d270c11aafa8b1e485d",
  "target": "0b8da4cff382b93299301119dfce545a73ae7eee",
  "base": "6b3c22d3e3dcb2df13aa55e46ba2fbd24e6680b5"
}

下面保留完整差异,包含对象 ID、文件头与所有 hunk。实验明确关闭 external diff、textconv、重命名识别和颜色,并固定差异算法及上下文行数。

展开完整 main..feature 输出
diff --git a/release.txt b/release.txt
deleted file mode 100644
index 7e5631dd59966a63149a406eb6b735149b20cb6b..0000000000000000000000000000000000000000
--- a/release.txt
+++ /dev/null
@@ -1 +0,0 @@
-Release notes: keep zero quantities visible.
diff --git a/src/labels.py b/src/labels.py
index 58639ef21d6f4a718712ec4cce15939e1b686366..83967ce91551959e78912d0ad447de5f0c295d74 100644
--- a/src/labels.py
+++ b/src/labels.py
@@ -1,2 +1,4 @@
 def count_label(count):
+    if count == 0:
+        return ""
     return f"{count} items"
展开完整 main...feature 输出
diff --git a/src/labels.py b/src/labels.py
index 58639ef21d6f4a718712ec4cce15939e1b686366..83967ce91551959e78912d0ad447de5f0c295d74 100644
--- a/src/labels.py
+++ b/src/labels.py
@@ -1,2 +1,4 @@
 def count_label(count):
+    if count == 0:
+        return ""
     return f"{count} items"

取得文件时,检查器先用 rev-parse --verify 与 ^{commit} 确认完整 SHA 指向提交,再用 ls-tree 检查路径、类型、模式与 blob ID,最后通过 cat-file blob 读取原始字节。对应规则见 git-rev-parse、git-ls-tree 和 git-cat-file 文档。导出过程不复制当前工作树,也不运行文件过滤器。

Git blob ID 与这里的 SHA-256 用途不同。前者是在指定 Git 对象格式中定位 blob 的对象名;后者由 Python 对导出的原始文件字节计算,用于和原有 scope 对接。二者都不能单独证明来源可信或有人批准了这次审查。目标 SHA 必须先由可信的捕获过程确定,不能直接采用待审报告自报的目标。

完整 Git 来源记录 manifest.json
{
  "schema_version": 1,
  "main_commit": "0e1899c60081fff643e71d270c11aafa8b1e485d",
  "base_commit": "6b3c22d3e3dcb2df13aa55e46ba2fbd24e6680b5",
  "target_commit": "0b8da4cff382b93299301119dfce545a73ae7eee",
  "path": "src/labels.py",
  "blob_oid": "83967ce91551959e78912d0ad447de5f0c295d74",
  "snapshot_sha256": "aba0e362a97271155165d8513fc192137548e3eff82df2a17917caef53271189",
  "three_dot_sha256": "751d5859cd2bd2e73ec86bbe2c55c589005aa9fb9e27483dca3a251ef47d094d"
}
交给原有校验器的 scope.json
{
  "schema_version": 1,
  "base_commit": "6b3c22d3e3dcb2df13aa55e46ba2fbd24e6680b5",
  "files": {
    "src/labels.py": {
      "sha256": "aba0e362a97271155165d8513fc192137548e3eff82df2a17917caef53271189",
      "changed_lines": [
        2,
        3
      ]
    }
  }
}
保持原有契约的 report.json
{
  "base_commit": "6b3c22d3e3dcb2df13aa55e46ba2fbd24e6680b5",
  "status": "complete",
  "limitations": [
    "Local teaching fixture; no model or security scan."
  ],
  "findings": [
    {
      "id": "F001",
      "file": "src/labels.py",
      "line_start": 2,
      "line_end": 3,
      "title": "Zero count loses its visible label",
      "severity": "low",
      "evidence": {
        "quote": "return \"\"",
        "reason": "The zero branch returns empty text."
      },
      "repro": {
        "steps": [
          "Call count_label(0) in an isolated unit test."
        ],
        "expected": "0 items",
        "observed": "empty string"
      }
    }
  ]
}

15. 先核对来源,再运行原有报告检查

新文件 git_provenance.py 负责把记录与 Git 对象重新对上:从外部指定的完整端点计算共同祖先,核对清单里的目标和 blob,再逐字节比较导出快照,最后核对 scope 的基点、哈希与固定变化行。通过后,原来的 review_guard.py 继续检查报告结构、引文和行号。两个步骤各自输出结果,safety_proven 始终为 false。

下面几个反例把边界显示得更清楚。scope 和 report 都改成同一个全 a 基点时,原有离线检查依然返回 validated,符合它既定的工作范围;新检查会用仓库里的共同祖先拒绝这份记录。若连 manifest 也改成全 a,仍然会被拒绝。将目标指向 main、改动快照,或者改动快照后同步更新 scope 哈希,也无法通过本例的 Git 来源检查。

{
  "matching_fake_base_old_guard": "validated",
  "matching_fake_base_provenance": "git_base_mismatch",
  "all_json_fake_base_provenance": "manifest_base_mismatch",
  "wrong_manifest_target": "target_mismatch",
  "wrong_selected_target": "simple_fork_required",
  "tampered_snapshot": "snapshot_not_target_blob",
  "tamper_and_rescope_old_guard": "validated",
  "tamper_and_rescope_provenance": "snapshot_not_target_blob"
}

标签行为另做了一次很小的运行核对。程序只执行本实验自己创建并已核对的两份标签源码,不执行任意仓库代码。相同输入 0 在祖先中打印 '0 items',在固定目标中打印空字符串。两次进程都退出 0;这说明函数运行完成,业务预期是否满足要看具体输出。本例沿用“零数量仍可见”的教学约定。

[
  {
    "version": "ancestor",
    "input": 0,
    "expected": "0 items",
    "exit": 0,
    "stdout": "'0 items'\n",
    "stderr": ""
  },
  {
    "version": "target",
    "input": 0,
    "expected": "0 items",
    "exit": 0,
    "stdout": "''\n",
    "stderr": ""
  }
]

随后 main 又改了一次发布说明,feature 恢复了原先标签行为,两个分支名都移动了。此时按分支名计算的三点 diff 为空;用先前完整 SHA 计算的两份 diff 仍逐字节相同,来源检查也继续通过。固定 SHA 记录的是那次审查对象;它不会让旧结论自动覆盖后来移动到的新提交。保存的 bundle 让本次对象在临时仓库删除后仍可离线复查,实验已从 bundle 恢复到另一个临时仓库重新检查。

{
  "main_after": "00115dfcf209c7efeb498c0cda22e6fb765f8dd1",
  "target_after": "e693420956696987a95109124a98f34748233d76",
  "fixed_diff_unchanged": true,
  "live_diff_empty": true,
  "fixed_provenance": "provenance_verified"
}

这些结果只覆盖新建、静止、受信的小型教学仓库。检查器没有提供对象签名认证、恶意仓库隔离、并发替换防护或资源耗尽防护。已清理的 Git 子进程环境能排除本实验测试过的父进程配置影响,也不能替代操作系统沙箱。实际仓库的信任、权限和发布条件仍需独立确认。

16. 补充源码、固定预期与实际运行记录

先把前面的 make_fixture.py、review_guard.py、test_review_guard.py 原样保存,再把下方四个 Python 文件和 expected.json 保存到同一空目录。expected.json 是本实验核对后固定下来的预期,不应在测试运行中从待审报告重写。运行器拒绝已存在的输出目录;重跑时选择新的目录名。

python3 -B run_git_lab.py --out result
python3 -B -m unittest -v test_review_guard test_git_provenance
python3 -B review_guard.py --root result/snapshot \
  --scope result/scope.json --report result/report.json
python3 -B recheck_bundle.py --evidence result

2026 年 10 月 14 日修正:bundle 复查现在保留报告的未完成状态。来源核对通过后,报告为 complete 时退出 0,为带原因的 unknown 时退出 3;不能只因 Git 来源一致就把未完成的报告当作通过。新增回归测试分别检查这两种状态与退出码,原有 47 个测试及新增 1 个测试均通过。下方保留的 2026 年 10 月 13 日运行记录仍是当时版本的历史输出。

2026 年 10 月 20 日修正:Git 来源检查先确认 scope 和 report 的顶层都是 JSON 对象,再读取 base_commit。此前将任一输入保存为 [],JSON 解码虽能完成,git_provenance.py 却会因调用列表的 get 方法抛出 AttributeError,以退出码 1 结束且没有结构化结果。现在直接入口返回 rejected,error 分别为 scope_schema 或 report_schema,退出码为 2,safety_proven 仍为 false。Python JSON 类型对照说明了解码后的对象与数组类型;语法正确不等于符合本文输入契约。

本轮从页面提取源码,在 Linux、Python 3.12.14、Git 2.52.0 下重跑原有 48 项测试,补入 3 项回归后共 51 项通过。其中用独立 CLI 子进程覆盖 scope 和 report 各自为数组、null、布尔值、整数、小数和字符串的 12 个输入,均得到退出码 2、对应错误码和空标准错误输出;函数级检查也确认这些输入在调用 Git 前被拒绝。完整实验、普通报告检查及 bundle 复查仍通过,既有 unknown 退出码 3 的回归保留。本修正仅完善这个教学程序的错误结果,不扩展为通用仓库检查,不证明代码安全,也未重新验证模型审查、真实用户仓库或网页交互。

Git 子进程使用新建的 HOME、空 hooks 目录和明确的环境白名单,关闭系统与全局配置读取,并固定作者、时间、换行策略与签名选项。测试把父进程的 GIT_DIR、GIT_WORK_TREE、GIT_INDEX_FILE、对象目录、全局配置、hooks 配置、external diff、作者、提交日期和 PATH 改成不适合本实验的值后再次运行,结果仍符合固定预期,父目录里的索引哨兵文件保持原样。它不检查你机器上的任意配置组合。

完整源码:git_provenance.py
"""Provenance checks for this fixed, one-text-file teaching fixture only."""
import argparse
import hashlib
import json
import re
import shutil
import subprocess
import tempfile
from pathlib import Path

from make_fixture import BEFORE, AFTER
from review_guard import Invalid, load, need, obj, safe_path

PATH = 'src/labels.py'
OID = re.compile(r'[0-9a-f]{40}')
# Deliberately use a known search path, not the inherited PATH.
GIT = shutil.which('git', path='/usr/local/bin:/usr/bin:/bin')


def sha(raw):
    return hashlib.sha256(raw).hexdigest()


class Git:
    """Use only our fresh, trusted fixture repo; this is not a sandbox."""
    def __init__(self, repo, home):
        self.repo, self.home = Path(repo).resolve(), Path(home).resolve()
        self.home.mkdir(parents=True, exist_ok=True)
        (self.home / 'hooks').mkdir(exist_ok=True)
        need(GIT is not None, 'git_not_found')
        self.env = {
            'PATH': '/usr/local/bin:/usr/bin:/bin', 'HOME': str(self.home),
            'XDG_CONFIG_HOME': str(self.home), 'LC_ALL': 'C', 'TZ': 'UTC',
            'GIT_CONFIG_NOSYSTEM': '1', 'GIT_CONFIG_GLOBAL': '/dev/null',
            'GIT_ATTR_NOSYSTEM': '1', 'GIT_TERMINAL_PROMPT': '0',
            'GIT_NO_REPLACE_OBJECTS': '1',
            'GIT_AUTHOR_NAME': 'Provenance Fixture',
            'GIT_AUTHOR_EMAIL': 'fixture@example.invalid',
            'GIT_COMMITTER_NAME': 'Provenance Fixture',
            'GIT_COMMITTER_EMAIL': 'fixture@example.invalid',
            'GIT_AUTHOR_DATE': '2026-10-13T00:00:00+00:00',
            'GIT_COMMITTER_DATE': '2026-10-13T00:00:00+00:00',
        }
        self.receipts = []

    def run(self, *args):
        cmd = [GIT, '--no-pager', '--no-replace-objects', '--literal-pathspecs',
               '-c', 'core.hooksPath=' + str(self.home / 'hooks'),
               '-c', 'core.autocrlf=false', '-c', 'core.safecrlf=false',
               '-c', 'commit.gpgSign=false', '-c', 'tag.gpgSign=false',
               '-C', str(self.repo), *args]
        p = subprocess.run(cmd, env=self.env, stdout=subprocess.PIPE,
                           stderr=subprocess.PIPE, timeout=20, check=False)
        self.receipts.append({'args': list(args), 'exit': p.returncode,
                              'stdout': p.stdout.decode('utf-8', 'backslashreplace'),
                              'stderr': p.stderr.decode('utf-8', 'backslashreplace')})
        need(p.returncode == 0, 'git_command_failed:' + args[0])
        return p.stdout

    def commit_id(self, value):
        need(type(value) is str and OID.fullmatch(value) is not None,
             'full_sha1_required')
        actual = self.run('rev-parse', '--verify', '--end-of-options',
                          value + '^{commit}').decode().strip()
        need(actual == value, 'commit_mismatch')
        need(self.run('cat-file', '-t', value) == b'commit\n', 'not_commit')
        return actual

    def blob(self, commit):
        raw = self.run('ls-tree', '-r', '-z', '--full-tree', commit)
        entries = raw.rstrip(b'\0').split(b'\0')
        # Only this exact single-file tree is within this lab's contract.
        need(len(entries) == 1, 'unsupported_tree')
        meta, name = entries[0].split(b'\t', 1)
        mode, kind, oid = meta.split()
        need(name == PATH.encode() and mode == b'100644' and kind == b'blob',
             'unsupported_entry')
        raw = self.run('cat-file', 'blob', oid.decode())
        need(len(raw) <= 4096 and b'\0' not in raw, 'unsupported_text')
        raw.decode('utf-8')
        return oid.decode(), raw

    def diff(self, main, target, dots):
        return self.run('diff', '--no-ext-diff', '--no-textconv', '--no-renames',
                        '--no-color', '--full-index', '--unified=3',
                        '--diff-algorithm=myers', '--no-indent-heuristic',
                        '--src-prefix=a/', '--dst-prefix=b/',
                        main + dots + target, '--')


def check(git, root, scope, report, manifest, selected_main, selected_target):
    """Selected SHAs come from the trusted capture, never from the report."""
    need(type(scope) is dict, 'scope_schema')
    need(type(report) is dict, 'report_schema')
    obj(manifest, 'schema_version main_commit base_commit target_commit path '
        'blob_oid snapshot_sha256 three_dot_sha256', 'manifest_schema')
    need(type(manifest['schema_version']) is int and manifest['schema_version'] == 1,
         'manifest_version')
    git.commit_id(selected_main)
    git.commit_id(selected_target)
    need(git.run('rev-parse', '--is-shallow-repository') == b'false\n',
         'shallow_unsupported')
    bases = git.run('merge-base', '--all', selected_main, selected_target).decode().split()
    need(len(bases) == 1, 'single_merge_base_required')
    base = git.commit_id(bases[0])
    for tip in (selected_main, selected_target):
        need(git.run('rev-list', '--parents', '-n', '1', tip).decode().split()
             == [tip, base], 'simple_fork_required')
    need(manifest['main_commit'] == selected_main, 'main_mismatch')
    need(manifest['target_commit'] == selected_target, 'target_mismatch')
    need(manifest['base_commit'] == base, 'manifest_base_mismatch')
    need(scope.get('base_commit') == base and report.get('base_commit') == base,
         'git_base_mismatch')
    need(manifest['path'] == PATH, 'path_mismatch')
    old_blob, before = git.blob(base)
    blob, after = git.blob(selected_target)
    need(before == BEFORE.encode() and after == AFTER.encode(),
         'unsupported_fixture_content')
    need(git.run('diff', '--no-ext-diff', '--no-textconv', '--no-renames',
                 '--name-status', '-z', base, selected_target, '--')
         == b'M\0src/labels.py\0', 'unsupported_change')
    need(manifest['blob_oid'] == blob, 'blob_mismatch')
    raw = safe_path(Path(root).resolve(strict=True), PATH).read_bytes()
    need(raw == after, 'snapshot_not_target_blob')
    need(manifest['snapshot_sha256'] == sha(after), 'manifest_hash_mismatch')
    need(scope == {'schema_version': 1, 'base_commit': base, 'files': {
        PATH: {'sha256': sha(after), 'changed_lines': [2, 3]}}}, 'scope_mismatch')
    need(manifest['three_dot_sha256'] == sha(git.diff(selected_main, selected_target, '...')),
         'diff_mismatch')
    return {'state': 'provenance_verified', 'base_commit': base,
            'target_commit': selected_target, 'blob_oid': blob,
            'snapshot_sha256': sha(after), 'safety_proven': False}


def main(argv=None):
    p = argparse.ArgumentParser(description=__doc__)
    for name in ('repo', 'root', 'scope', 'report', 'manifest', 'main', 'target'):
        p.add_argument('--' + name, required=True)
    a = p.parse_args(argv)
    try:
        with tempfile.TemporaryDirectory(prefix='git-provenance-home-') as home:
            result = check(Git(a.repo, home), a.root, load(a.scope), load(a.report),
                           load(a.manifest), a.main, a.target)
    except (Invalid, OSError, ValueError, TypeError, KeyError, UnicodeError,
            subprocess.TimeoutExpired) as e:
        code = str(e) if isinstance(e, Invalid) else type(e).__name__
        print(json.dumps({'state': 'rejected', 'error': code, 'safety_proven': False}))
        return 2
    print(json.dumps(result))
    return 0


if __name__ == '__main__':
    raise SystemExit(main())
完整源码:run_git_lab.py
"""Build an isolated, deterministic Git fork; export receipts and a bundle."""
import argparse
import copy
import json
import platform
import subprocess
import sys
import tempfile
from pathlib import Path

from git_provenance import Git, PATH, check, sha
from make_fixture import BEFORE, AFTER, write_fixture
from review_guard import Invalid, validate


def write_json(path, value):
    Path(path).write_text(json.dumps(value, indent=2, ensure_ascii=False) + '\n', encoding='utf-8')


def build(repo, home):
    repo.mkdir()
    g = Git(repo, home)
    g.run('init', '--quiet', '--object-format=sha1', '--initial-branch=main', '--template=')
    (repo / 'src').mkdir()
    (repo / PATH).write_bytes(BEFORE.encode())
    g.run('add', '--', PATH)
    g.run('commit', '--quiet', '-m', 'fixture: visible quantity label')
    ancestor = g.run('rev-parse', 'HEAD').decode().strip()
    (repo / 'release.txt').write_bytes(b'Release notes: keep zero quantities visible.\n')
    g.run('add', '--', 'release.txt')
    g.run('commit', '--quiet', '-m', 'fixture: main release note')
    main = g.run('rev-parse', 'HEAD').decode().strip()
    g.run('checkout', '--quiet', '-b', 'feature', ancestor)
    (repo / PATH).write_bytes(AFTER.encode())
    g.run('add', '--', PATH)
    g.run('commit', '--quiet', '-m', 'fixture: feature hides zero quantity')
    target = g.run('rev-parse', 'HEAD').decode().strip()
    return g, main, target, ancestor


def snapshot(g, out, main, target, base):
    # Keep the old generator untouched, and only borrow its report structure.
    with tempfile.TemporaryDirectory(prefix='report-template-') as tmp:
        _, report = write_fixture(tmp)
    blob, raw = g.blob(target)
    root = out / 'snapshot'
    (root / 'src').mkdir(parents=True)
    (root / PATH).write_bytes(raw)  # Export Git blob bytes, not the worktree.
    scope = {'schema_version': 1, 'base_commit': base, 'files': {
        PATH: {'sha256': sha(raw), 'changed_lines': [2, 3]}}}
    report['base_commit'] = base
    report['limitations'] = ['Local teaching fixture; no model or security scan.']
    manifest = {'schema_version': 1, 'main_commit': main, 'base_commit': base,
                'target_commit': target, 'path': PATH, 'blob_oid': blob,
                'snapshot_sha256': sha(raw), 'three_dot_sha256': sha(g.diff(main, target, '...'))}
    for name, value in (('scope', scope), ('report', report), ('manifest', manifest)):
        write_json(out / (name + '.json'), value)
    return root, scope, report, manifest


def reject(call):
    try:
        call()
    except Invalid as e:
        return str(e)
    raise AssertionError('expected rejection')


def experiment(out):
    # Refuse to reuse a directory, so existing output cannot be overwritten.
    out.mkdir()
    with tempfile.TemporaryDirectory(prefix='git-review-lab-') as tmp:
        tmp = Path(tmp)
        g, main, target, base = build(tmp / 'repo', tmp / 'home')
        root, scope, report, manifest = snapshot(g, out, main, target, base)
        two, three = g.diff(main, target, '..'), g.diff(main, target, '...')
        (out / 'two-dot.diff').write_bytes(two)
        (out / 'three-dot.diff').write_bytes(three)
        assert b'deleted file mode' in two and b'release.txt' in two
        assert b'release.txt' not in three
        valid = check(g, root, scope, report, manifest, main, target)
        guarded = validate(report, scope, root)
        fake_scope, fake_report = copy.deepcopy(scope), copy.deepcopy(report)
        fake_scope['base_commit'] = fake_report['base_commit'] = 'a' * 40
        fake_guard = validate(fake_report, fake_scope, root)
        fake_reject = reject(lambda: check(g, root, fake_scope, fake_report, manifest, main, target))
        fake_manifest = dict(manifest, base_commit='a' * 40)
        all_fake_reject = reject(lambda: check(g, root, fake_scope, fake_report, fake_manifest, main, target))
        wrong_manifest = dict(manifest, target_commit=main)
        wrong_reject = reject(lambda: check(g, root, scope, report, wrong_manifest, main, target))
        wrong_pin_reject = reject(lambda: check(g, root, scope, report, manifest, main, main))
        (root / PATH).write_bytes(AFTER.encode() + b'# snapshot tamper\n')
        tamper_reject = reject(lambda: check(g, root, scope, report, manifest, main, target))
        rescope = copy.deepcopy(scope)
        rescope['files'][PATH]['sha256'] = sha((root / PATH).read_bytes())
        tamper_guard = validate(report, rescope, root)
        tamper_rescope_reject = reject(lambda: check(g, root, rescope, report, manifest, main, target))
        (root / PATH).write_bytes(g.blob(target)[1])
        # Execute only our own two-line/four-line fixture, never arbitrary repo code.
        behavior = []
        for label, raw in [('ancestor', g.blob(base)[1]), ('target', g.blob(target)[1])]:
            file = tmp / (label + '.py')
            file.write_bytes(raw + b'\nprint(repr(count_label(0)))\n')
            p = subprocess.run([sys.executable, '-I', '-B', str(file)], env=g.env,
                               capture_output=True, timeout=20, check=False)
            assert p.returncode == 0
            behavior.append({'version': label, 'input': 0, 'expected': '0 items',
                             'exit': p.returncode, 'stdout': p.stdout.decode(), 'stderr': p.stderr.decode()})
        assert behavior[0]['stdout'] == "'0 items'\n" and behavior[1]['stdout'] == "''\n"
        g.run('checkout', '--quiet', 'main')
        (g.repo / 'release.txt').write_bytes(b'Release notes moved on main.\n')
        g.run('add', '--', 'release.txt')
        g.run('commit', '--quiet', '-m', 'fixture: main moves after capture')
        moved_main = g.run('rev-parse', 'HEAD').decode().strip()
        g.run('checkout', '--quiet', 'feature')
        (g.repo / PATH).write_bytes(BEFORE.encode())
        g.run('add', '--', PATH)
        g.run('commit', '--quiet', '-m', 'fixture: feature moves after capture')
        moved_target = g.run('rev-parse', 'HEAD').decode().strip()
        fixed_after = g.diff(main, target, '...')
        live_after = g.diff('main', 'feature', '...')
        (out / 'fixed-three-dot-after-move.diff').write_bytes(fixed_after)
        (out / 'live-three-dot-after-move.diff').write_bytes(live_after)
        assert two == g.diff(main, target, '..') and three == fixed_after
        assert live_after == b'' and moved_main != main and moved_target != target
        after_move = check(g, root, scope, report, manifest, main, target)
        g.run('bundle', 'create', str(out / 'fixture.bundle'), '--all')
        g.run('bundle', 'verify', str(out / 'fixture.bundle'))
        result = {'runtime': {'git': g.run('--version').decode().strip(),
                             'python': platform.python_version(), 'os': platform.system()},
                  'pins': {'main': main, 'target': target, 'base': base},
                  'provenance': valid, 'guard': guarded,
                  'cases': {'matching_fake_base_old_guard': fake_guard['state'],
                            'matching_fake_base_provenance': fake_reject,
                            'all_json_fake_base_provenance': all_fake_reject,
                            'wrong_manifest_target': wrong_reject,
                            'wrong_selected_target': wrong_pin_reject,
                            'tampered_snapshot': tamper_reject,
                            'tamper_and_rescope_old_guard': tamper_guard['state'],
                            'tamper_and_rescope_provenance': tamper_rescope_reject},
                  'behavior': behavior,
                  'branch_movement': {'main_after': moved_main, 'target_after': moved_target,
                                      'fixed_diff_unchanged': True, 'live_diff_empty': True,
                                      'fixed_provenance': after_move['state']},
                  'diff_sha256': {'two_dot': sha(two), 'three_dot': sha(three)},
                  'bundle_sha256': sha((out / 'fixture.bundle').read_bytes())}
        write_json(out / 'results.json', result)
        write_json(out / 'git-command-receipts.json', g.receipts)
        print(json.dumps(result, indent=2))
        print('\nFULL TWO-DOT DIFF\n' + two.decode(), end='')
        print('\nFULL THREE-DOT DIFF\n' + three.decode(), end='')
        print('\nPASS: fixed objects, snapshot provenance and branch-movement checks')
        return result


if __name__ == '__main__':
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument('--out', required=True, type=Path)
    args = parser.parse_args()
    experiment(args.out.resolve())
完整源码:recheck_bundle.py
"""Recheck the saved teaching bundle in a new temporary bare repository."""
import argparse
import json
import tempfile
from pathlib import Path

from git_provenance import Git, check
from review_guard import load, validate


def main():
    p = argparse.ArgumentParser(description=__doc__)
    p.add_argument('--evidence', required=True, type=Path)
    a = p.parse_args()
    evidence = a.evidence.resolve(strict=True)
    # These recorded expectations are trusted inputs, not report-provided pins.
    expected = load(Path(__file__).with_name('expected.json'))
    pins = expected['pins']
    with tempfile.TemporaryDirectory(prefix='recheck-bundle-') as tmp:
        tmp = Path(tmp)
        repo = tmp / 'repo'
        repo.mkdir()
        g = Git(repo, tmp / 'home')
        g.run('init', '--quiet', '--bare', '--object-format=sha1', '--template=')
        g.run('fetch', '--quiet', str(evidence / 'fixture.bundle'),
              'refs/heads/*:refs/heads/*')
        scope, report = load(evidence / 'scope.json'), load(evidence / 'report.json')
        result = check(g, evidence / 'snapshot', scope, report,
                       load(evidence / 'manifest.json'), pins['main'], pins['target'])
        print(json.dumps(result))
        guarded = validate(report, scope, evidence / 'snapshot')
        print(json.dumps(guarded))
    return 3 if guarded['state'] == 'unknown' else 0


if __name__ == '__main__':
    raise SystemExit(main())
完整源码:test_git_provenance.py
"""Focused contract tests, plus the original guard's tests in a separate file."""
import contextlib
import copy
import io
import json
import subprocess
import sys
import tempfile
import unittest
from pathlib import Path

from git_provenance import Git, PATH, check, main as cli, sha
from make_fixture import AFTER
from review_guard import Invalid, validate
from run_git_lab import build, snapshot, write_json

EXPECTED_PINS = {
    'main': '0e1899c60081fff643e71d270c11aafa8b1e485d',
    'target': '0b8da4cff382b93299301119dfce545a73ae7eee',
    'base': '6b3c22d3e3dcb2df13aa55e46ba2fbd24e6680b5',
}


class ProvenanceTests(unittest.TestCase):
    def setUp(self):
        self.temp = tempfile.TemporaryDirectory(prefix='provenance-test-')
        self.addCleanup(self.temp.cleanup)
        self.tmp = Path(self.temp.name)
        self.g, self.main, self.target, self.base = build(self.tmp / 'repo', self.tmp / 'home')
        self.out = self.tmp / 'out'
        self.out.mkdir()
        self.root, self.scope, self.report, self.manifest = snapshot(
            self.g, self.out, self.main, self.target, self.base)

    def verify(self):
        return check(self.g, self.root, self.scope, self.report, self.manifest,
                     self.main, self.target)

    def rejects(self, code):
        with self.assertRaisesRegex(Invalid, '^' + code + '$'):
            self.verify()

    def test_fixed_sha_expected(self):
        self.assertEqual({'main': self.main, 'target': self.target, 'base': self.base}, EXPECTED_PINS)
        self.assertEqual(self.verify()['state'], 'provenance_verified')

    def test_two_dot_and_three_dot(self):
        two, three = self.g.diff(self.main, self.target, '..'), self.g.diff(self.main, self.target, '...')
        self.assertIn(b'deleted file mode', two)
        self.assertIn(b'release.txt', two)
        self.assertNotIn(b'release.txt', three)
        self.assertEqual(sha(two), '50dbb06dc1d2fab9edada1468b910063199dd8447667a2c3a7a599e60e51ff3b')
        self.assertEqual(sha(three), '751d5859cd2bd2e73ec86bbe2c55c589005aa9fb9e27483dca3a251ef47d094d')

    def test_matching_fake_base(self):
        self.scope['base_commit'] = self.report['base_commit'] = 'a' * 40
        self.assertEqual(validate(self.report, self.scope, self.root)['state'], 'validated')
        self.rejects('git_base_mismatch')

    def test_all_json_fake_base(self):
        self.scope['base_commit'] = self.report['base_commit'] = self.manifest['base_commit'] = 'a' * 40
        self.rejects('manifest_base_mismatch')

    def test_wrong_manifest_target(self):
        self.manifest['target_commit'] = self.main
        self.rejects('target_mismatch')

    def test_wrong_selected_target(self):
        self.target = self.main
        self.rejects('simple_fork_required')

    def test_nonexistent_selected_commit(self):
        self.target = 'a' * 40
        self.rejects('git_command_failed:rev-parse')

    def test_short_sha_rejected(self):
        self.target = self.target[:7]
        self.rejects('full_sha1_required')

    def test_tampered_snapshot(self):
        (self.root / PATH).write_bytes(AFTER.encode() + b'# extra\n')
        self.rejects('snapshot_not_target_blob')

    def test_tampered_snapshot_and_matching_hash(self):
        (self.root / PATH).write_bytes(AFTER.encode() + b'# extra\n')
        self.scope['files'][PATH]['sha256'] = sha((self.root / PATH).read_bytes())
        self.assertEqual(validate(self.report, self.scope, self.root)['state'], 'validated')
        self.rejects('snapshot_not_target_blob')

    def test_wrong_blob(self):
        self.manifest['blob_oid'] = 'a' * 40
        self.rejects('blob_mismatch')

    def test_wrong_changed_lines(self):
        self.scope['files'][PATH]['changed_lines'] = [1, 2, 3]
        self.rejects('scope_mismatch')

    def test_wrong_diff_digest(self):
        self.manifest['three_dot_sha256'] = '0' * 64
        self.rejects('diff_mismatch')

    def test_symlink_snapshot_rejected(self):
        file = self.root / PATH
        file.unlink()
        file.symlink_to(self.g.repo / PATH)
        self.rejects('symlink_rejected')

    def test_cli_receipts(self):
        args = ['--repo', str(self.g.repo), '--root', str(self.root),
                '--scope', str(self.out / 'scope.json'), '--report', str(self.out / 'report.json'),
                '--manifest', str(self.out / 'manifest.json'), '--main', self.main, '--target', self.target]
        for expected, target in [(0, self.target), (2, 'a' * 40)]:
            output = io.StringIO()
            with contextlib.redirect_stdout(output):
                code = cli(args[:-1] + [target])
            self.assertEqual(code, expected)
            self.assertFalse(json.loads(output.getvalue())['safety_proven'])

    def test_nonobject_scope_rejected(self):
        receipt_count = len(self.g.receipts)
        for value in ([], None, True, 0, 1.5, ''):
            with self.subTest(value=value):
                self.scope = value
                self.rejects('scope_schema')
                self.assertEqual(len(self.g.receipts), receipt_count)

    def test_nonobject_report_rejected(self):
        receipt_count = len(self.g.receipts)
        for value in ([], None, True, 0, 1.5, ''):
            with self.subTest(value=value):
                self.report = value
                self.rejects('report_schema')
                self.assertEqual(len(self.g.receipts), receipt_count)

    def test_cli_nonobjects_are_structured_rejections(self):
        command = [
            sys.executable, '-B', str(Path(__file__).with_name('git_provenance.py')),
            '--repo', str(self.g.repo), '--root', str(self.root),
            '--scope', str(self.out / 'scope.json'),
            '--report', str(self.out / 'report.json'),
            '--manifest', str(self.out / 'manifest.json'),
            '--main', self.main, '--target', self.target]
        for name, original in [('scope', self.scope), ('report', self.report)]:
            path = self.out / (name + '.json')
            for value in ([], None, True, 0, 1.5, ''):
                with self.subTest(name=name, value=value):
                    write_json(path, value)
                    result = subprocess.run(command, capture_output=True, timeout=30,
                                            check=False)
                    self.assertEqual(result.returncode, 2, result.stderr.decode())
                    self.assertEqual(result.stderr, b'')
                    self.assertEqual(json.loads(result.stdout), {
                        'state': 'rejected', 'error': name + '_schema',
                        'safety_proven': False})
            write_json(path, original)

    def test_bundle_recheck_preserves_unknown_exit(self):
        self.g.run('bundle', 'create', str(self.out / 'fixture.bundle'), '--all')
        command = [sys.executable, '-B',
                   str(Path(__file__).with_name('recheck_bundle.py')),
                   '--evidence', str(self.out)]
        for status, expected_exit, expected_state in [
                ('complete', 0, 'validated'), ('unknown', 3, 'unknown')]:
            self.report['status'] = status
            write_json(self.out / 'report.json', self.report)
            result = subprocess.run(command, capture_output=True, timeout=60,
                                    check=False)
            self.assertEqual(result.returncode, expected_exit, result.stderr.decode())
            outputs = [json.loads(line) for line in result.stdout.splitlines()]
            self.assertEqual(outputs[-1]['state'], expected_state)
            self.assertTrue(all(item['safety_proven'] is False for item in outputs))

    def test_poisoned_parent_and_branch_movement(self):
        poison = self.tmp / 'poison'
        poison.mkdir()
        sentinel = poison / 'index'
        sentinel.write_bytes(b'do not modify this parent index\n')
        hooks = poison / 'hooks'
        hooks.mkdir()
        hook = hooks / 'pre-commit'
        hook.write_text('#!/bin/sh\nexit 77\n')
        hook.chmod(0o755)
        config = poison / 'config'
        config.write_text('[core]\n\thooksPath = ' + str(hooks) + '\n')
        env = {'PATH': '/no/inherited/executables', 'HOME': str(poison),
               'GIT_DIR': str(poison / 'not-a-repo'), 'GIT_WORK_TREE': str(poison),
               'GIT_COMMON_DIR': str(poison / 'absent'), 'GIT_INDEX_FILE': str(sentinel),
               'GIT_OBJECT_DIRECTORY': str(poison / 'objects'),
               'GIT_ALTERNATE_OBJECT_DIRECTORIES': str(poison / 'alternates'),
               'GIT_CONFIG_GLOBAL': str(config), 'GIT_CONFIG_COUNT': '1',
               'GIT_CONFIG_KEY_0': 'core.hooksPath', 'GIT_CONFIG_VALUE_0': str(hooks),
               'GIT_EXTERNAL_DIFF': '/nonexistent/diff',
               'GIT_AUTHOR_NAME': 'wrong', 'GIT_COMMITTER_DATE': 'invalid'}
        output = self.tmp / 'poison-result'
        p = subprocess.run([sys.executable, '-E', '-B', str(Path(__file__).with_name('run_git_lab.py')),
                            '--out', str(output)], env=env, capture_output=True, timeout=60, check=False)
        self.assertEqual(p.returncode, 0, p.stderr.decode())
        result = json.loads((output / 'results.json').read_text())
        self.assertEqual(result['pins'], EXPECTED_PINS)
        expected = json.loads(Path(__file__).with_name('expected.json').read_text())
        self.assertEqual({key: result[key] for key in expected}, expected)
        self.assertTrue(result['branch_movement']['fixed_diff_unchanged'])
        self.assertTrue(result['branch_movement']['live_diff_empty'])
        self.assertEqual(sentinel.read_bytes(), b'do not modify this parent index\n')
        self.assertFalse((poison / 'objects').exists())


if __name__ == '__main__':
    unittest.main()
完整固定预期:expected.json
{
  "pins": {
    "main": "0e1899c60081fff643e71d270c11aafa8b1e485d",
    "target": "0b8da4cff382b93299301119dfce545a73ae7eee",
    "base": "6b3c22d3e3dcb2df13aa55e46ba2fbd24e6680b5"
  },
  "provenance": {
    "state": "provenance_verified",
    "base_commit": "6b3c22d3e3dcb2df13aa55e46ba2fbd24e6680b5",
    "target_commit": "0b8da4cff382b93299301119dfce545a73ae7eee",
    "blob_oid": "83967ce91551959e78912d0ad447de5f0c295d74",
    "snapshot_sha256": "aba0e362a97271155165d8513fc192137548e3eff82df2a17917caef53271189",
    "safety_proven": false
  },
  "guard": {
    "state": "validated",
    "finding_count": 1,
    "safety_proven": false,
    "note": "Structure only; evidence meaning and reproduction need review."
  },
  "cases": {
    "matching_fake_base_old_guard": "validated",
    "matching_fake_base_provenance": "git_base_mismatch",
    "all_json_fake_base_provenance": "manifest_base_mismatch",
    "wrong_manifest_target": "target_mismatch",
    "wrong_selected_target": "simple_fork_required",
    "tampered_snapshot": "snapshot_not_target_blob",
    "tamper_and_rescope_old_guard": "validated",
    "tamper_and_rescope_provenance": "snapshot_not_target_blob"
  },
  "behavior": [
    {
      "version": "ancestor",
      "input": 0,
      "expected": "0 items",
      "exit": 0,
      "stdout": "'0 items'\n",
      "stderr": ""
    },
    {
      "version": "target",
      "input": 0,
      "expected": "0 items",
      "exit": 0,
      "stdout": "''\n",
      "stderr": ""
    }
  ],
  "branch_movement": {
    "main_after": "00115dfcf209c7efeb498c0cda22e6fb765f8dd1",
    "target_after": "e693420956696987a95109124a98f34748233d76",
    "fixed_diff_unchanged": true,
    "live_diff_empty": true,
    "fixed_provenance": "provenance_verified"
  },
  "diff_sha256": {
    "two_dot": "50dbb06dc1d2fab9edada1468b910063199dd8447667a2c3a7a599e60e51ff3b",
    "three_dot": "751d5859cd2bd2e73ec86bbe2c55c589005aa9fb9e27483dca3a251ef47d094d"
  }
}

本次 31 个原有检查测试与 16 个新增来源测试全部通过。测试覆盖真实对象、两种 diff、相同假基点、错误目标、不存在或缩写的 SHA、快照改动、配套哈希改动、blob 与变化行记录错误、差异哈希错误、符号链接和命令行退出码,以及上述父环境与分支移动实验。下方保留完整输出;测试耗时、临时目录名和解释器路径属于本次运行环境,不是跨机器的固定预期。

完整实验标准输出
{
  "runtime": {
    "git": "git version 2.52.0",
    "python": "3.12.14",
    "os": "Linux"
  },
  "pins": {
    "main": "0e1899c60081fff643e71d270c11aafa8b1e485d",
    "target": "0b8da4cff382b93299301119dfce545a73ae7eee",
    "base": "6b3c22d3e3dcb2df13aa55e46ba2fbd24e6680b5"
  },
  "provenance": {
    "state": "provenance_verified",
    "base_commit": "6b3c22d3e3dcb2df13aa55e46ba2fbd24e6680b5",
    "target_commit": "0b8da4cff382b93299301119dfce545a73ae7eee",
    "blob_oid": "83967ce91551959e78912d0ad447de5f0c295d74",
    "snapshot_sha256": "aba0e362a97271155165d8513fc192137548e3eff82df2a17917caef53271189",
    "safety_proven": false
  },
  "guard": {
    "state": "validated",
    "finding_count": 1,
    "safety_proven": false,
    "note": "Structure only; evidence meaning and reproduction need review."
  },
  "cases": {
    "matching_fake_base_old_guard": "validated",
    "matching_fake_base_provenance": "git_base_mismatch",
    "all_json_fake_base_provenance": "manifest_base_mismatch",
    "wrong_manifest_target": "target_mismatch",
    "wrong_selected_target": "simple_fork_required",
    "tampered_snapshot": "snapshot_not_target_blob",
    "tamper_and_rescope_old_guard": "validated",
    "tamper_and_rescope_provenance": "snapshot_not_target_blob"
  },
  "behavior": [
    {
      "version": "ancestor",
      "input": 0,
      "expected": "0 items",
      "exit": 0,
      "stdout": "'0 items'\n",
      "stderr": ""
    },
    {
      "version": "target",
      "input": 0,
      "expected": "0 items",
      "exit": 0,
      "stdout": "''\n",
      "stderr": ""
    }
  ],
  "branch_movement": {
    "main_after": "00115dfcf209c7efeb498c0cda22e6fb765f8dd1",
    "target_after": "e693420956696987a95109124a98f34748233d76",
    "fixed_diff_unchanged": true,
    "live_diff_empty": true,
    "fixed_provenance": "provenance_verified"
  },
  "diff_sha256": {
    "two_dot": "50dbb06dc1d2fab9edada1468b910063199dd8447667a2c3a7a599e60e51ff3b",
    "three_dot": "751d5859cd2bd2e73ec86bbe2c55c589005aa9fb9e27483dca3a251ef47d094d"
  },
  "bundle_sha256": "6752db9184bd18e62e30ede515b1e41f8c3fa6df837b6c87bf3357f14b192a3b"
}

FULL TWO-DOT DIFF
diff --git a/release.txt b/release.txt
deleted file mode 100644
index 7e5631dd59966a63149a406eb6b735149b20cb6b..0000000000000000000000000000000000000000
--- a/release.txt
+++ /dev/null
@@ -1 +0,0 @@
-Release notes: keep zero quantities visible.
diff --git a/src/labels.py b/src/labels.py
index 58639ef21d6f4a718712ec4cce15939e1b686366..83967ce91551959e78912d0ad447de5f0c295d74 100644
--- a/src/labels.py
+++ b/src/labels.py
@@ -1,2 +1,4 @@
 def count_label(count):
+    if count == 0:
+        return ""
     return f"{count} items"

FULL THREE-DOT DIFF
diff --git a/src/labels.py b/src/labels.py
index 58639ef21d6f4a718712ec4cce15939e1b686366..83967ce91551959e78912d0ad447de5f0c295d74 100644
--- a/src/labels.py
+++ b/src/labels.py
@@ -1,2 +1,4 @@
 def count_label(count):
+    if count == 0:
+        return ""
     return f"{count} items"

PASS: fixed objects, snapshot provenance and branch-movement checks
完整 47 项测试回执
test_all_json_fake_base (test_git_provenance.ProvenanceTests.test_all_json_fake_base) ... ok
test_cli_receipts (test_git_provenance.ProvenanceTests.test_cli_receipts) ... ok
test_fixed_sha_expected (test_git_provenance.ProvenanceTests.test_fixed_sha_expected) ... ok
test_matching_fake_base (test_git_provenance.ProvenanceTests.test_matching_fake_base) ... ok
test_nonexistent_selected_commit (test_git_provenance.ProvenanceTests.test_nonexistent_selected_commit) ... ok
test_poisoned_parent_and_branch_movement (test_git_provenance.ProvenanceTests.test_poisoned_parent_and_branch_movement) ... ok
test_short_sha_rejected (test_git_provenance.ProvenanceTests.test_short_sha_rejected) ... ok
test_symlink_snapshot_rejected (test_git_provenance.ProvenanceTests.test_symlink_snapshot_rejected) ... ok
test_tampered_snapshot (test_git_provenance.ProvenanceTests.test_tampered_snapshot) ... ok
test_tampered_snapshot_and_matching_hash (test_git_provenance.ProvenanceTests.test_tampered_snapshot_and_matching_hash) ... ok
test_two_dot_and_three_dot (test_git_provenance.ProvenanceTests.test_two_dot_and_three_dot) ... ok
test_wrong_blob (test_git_provenance.ProvenanceTests.test_wrong_blob) ... ok
test_wrong_changed_lines (test_git_provenance.ProvenanceTests.test_wrong_changed_lines) ... ok
test_wrong_diff_digest (test_git_provenance.ProvenanceTests.test_wrong_diff_digest) ... ok
test_wrong_manifest_target (test_git_provenance.ProvenanceTests.test_wrong_manifest_target) ... ok
test_wrong_selected_target (test_git_provenance.ProvenanceTests.test_wrong_selected_target) ... ok
test_absolute (test_review_guard.GuardTests.test_absolute) ... ok
test_base_mismatch (test_review_guard.GuardTests.test_base_mismatch) ... ok
test_bool_line (test_review_guard.GuardTests.test_bool_line) ... ok
test_cli_reject (test_review_guard.GuardTests.test_cli_reject) ... ok
test_cli_success (test_review_guard.GuardTests.test_cli_success) ... ok
test_cli_unknown (test_review_guard.GuardTests.test_cli_unknown) ... ok
test_commands_remain_text (test_review_guard.GuardTests.test_commands_remain_text) ... ok
test_duplicate_finding (test_review_guard.GuardTests.test_duplicate_finding) ... ok
test_duplicate_id (test_review_guard.GuardTests.test_duplicate_id) ... ok
test_duplicate_json_key (test_review_guard.GuardTests.test_duplicate_json_key) ... ok
test_empty_evidence (test_review_guard.GuardTests.test_empty_evidence) ... ok
test_empty_is_not_safe (test_review_guard.GuardTests.test_empty_is_not_safe) ... ok
test_file_outside_scope (test_review_guard.GuardTests.test_file_outside_scope) ... ok
test_line_past_end (test_review_guard.GuardTests.test_line_past_end) ... ok
test_missing_repro (test_review_guard.GuardTests.test_missing_repro) ... ok
test_no_changed_line (test_review_guard.GuardTests.test_no_changed_line) ... ok
test_reversed_lines (test_review_guard.GuardTests.test_reversed_lines) ... ok
test_scope_traversal (test_review_guard.GuardTests.test_scope_traversal) ... ok
test_snapshot_changed (test_review_guard.GuardTests.test_snapshot_changed) ... ok
test_symlink (test_review_guard.GuardTests.test_symlink) ... ok
test_traversal (test_review_guard.GuardTests.test_traversal) ... ok
test_unknown (test_review_guard.GuardTests.test_unknown) ... ok
test_unknown_field (test_review_guard.GuardTests.test_unknown_field) ... ok
test_unknown_needs_reason (test_review_guard.GuardTests.test_unknown_needs_reason) ... ok
test_unknown_severity (test_review_guard.GuardTests.test_unknown_severity) ... ok
test_unknown_status (test_review_guard.GuardTests.test_unknown_status) ... ok
test_valid_is_not_safe (test_review_guard.GuardTests.test_valid_is_not_safe) ... ok
test_valid_report (test_review_guard.GuardTests.test_valid_report) ... ok
test_windows (test_review_guard.GuardTests.test_windows) ... ok
test_wrong_evidence (test_review_guard.GuardTests.test_wrong_evidence) ... ok
test_zero_line (test_review_guard.GuardTests.test_zero_line) ... ok

----------------------------------------------------------------------
Ran 47 tests in 0.961s

OK
从 bundle 恢复后的来源 CLI 输出
{"state": "provenance_verified", "base_commit": "6b3c22d3e3dcb2df13aa55e46ba2fbd24e6680b5", "target_commit": "0b8da4cff382b93299301119dfce545a73ae7eee", "blob_oid": "83967ce91551959e78912d0ad447de5f0c295d74", "snapshot_sha256": "aba0e362a97271155165d8513fc192137548e3eff82df2a17917caef53271189", "safety_proven": false}
同一快照的原有 guard CLI 输出
{"state": "validated", "finding_count": 1, "safety_proven": false, "note": "Structure only; evidence meaning and reproduction need review."}
recheck_bundle.py 完整输出
{"state": "provenance_verified", "base_commit": "6b3c22d3e3dcb2df13aa55e46ba2fbd24e6680b5", "target_commit": "0b8da4cff382b93299301119dfce545a73ae7eee", "blob_oid": "83967ce91551959e78912d0ad447de5f0c295d74", "snapshot_sha256": "aba0e362a97271155165d8513fc192137548e3eff82df2a17917caef53271189", "safety_proven": false}
{"state": "validated", "finding_count": 1, "safety_proven": false, "note": "Structure only; evidence meaning and reproduction need review."}

上述实验、测试、两个 CLI 和 bundle 复查脚本均退出 0;实验与 CLI 的标准错误为空,unittest 的测试清单正常写在标准错误流。下面还保留 Git 命令的逐次参数、退出码与完整输出,便于核对对象和 diff 从哪里得到。bundle 的 SHA-256 只记录本次归档字节,其他 Git 版本的打包字节不必与它相同。

完整 Git 命令回执
[
  {
    "args": [
      "init",
      "--quiet",
      "--object-format=sha1",
      "--initial-branch=main",
      "--template="
    ],
    "exit": 0,
    "stdout": "",
    "stderr": ""
  },
  {
    "args": [
      "add",
      "--",
      "src/labels.py"
    ],
    "exit": 0,
    "stdout": "",
    "stderr": ""
  },
  {
    "args": [
      "commit",
      "--quiet",
      "-m",
      "fixture: visible quantity label"
    ],
    "exit": 0,
    "stdout": "",
    "stderr": ""
  },
  {
    "args": [
      "rev-parse",
      "HEAD"
    ],
    "exit": 0,
    "stdout": "6b3c22d3e3dcb2df13aa55e46ba2fbd24e6680b5\n",
    "stderr": ""
  },
  {
    "args": [
      "add",
      "--",
      "release.txt"
    ],
    "exit": 0,
    "stdout": "",
    "stderr": ""
  },
  {
    "args": [
      "commit",
      "--quiet",
      "-m",
      "fixture: main release note"
    ],
    "exit": 0,
    "stdout": "",
    "stderr": ""
  },
  {
    "args": [
      "rev-parse",
      "HEAD"
    ],
    "exit": 0,
    "stdout": "0e1899c60081fff643e71d270c11aafa8b1e485d\n",
    "stderr": ""
  },
  {
    "args": [
      "checkout",
      "--quiet",
      "-b",
      "feature",
      "6b3c22d3e3dcb2df13aa55e46ba2fbd24e6680b5"
    ],
    "exit": 0,
    "stdout": "",
    "stderr": ""
  },
  {
    "args": [
      "add",
      "--",
      "src/labels.py"
    ],
    "exit": 0,
    "stdout": "",
    "stderr": ""
  },
  {
    "args": [
      "commit",
      "--quiet",
      "-m",
      "fixture: feature hides zero quantity"
    ],
    "exit": 0,
    "stdout": "",
    "stderr": ""
  },
  {
    "args": [
      "rev-parse",
      "HEAD"
    ],
    "exit": 0,
    "stdout": "0b8da4cff382b93299301119dfce545a73ae7eee\n",
    "stderr": ""
  },
  {
    "args": [
      "ls-tree",
      "-r",
      "-z",
      "--full-tree",
      "0b8da4cff382b93299301119dfce545a73ae7eee"
    ],
    "exit": 0,
    "stdout": "100644 blob 83967ce91551959e78912d0ad447de5f0c295d74\tsrc/labels.py\u0000",
    "stderr": ""
  },
  {
    "args": [
      "cat-file",
      "blob",
      "83967ce91551959e78912d0ad447de5f0c295d74"
    ],
    "exit": 0,
    "stdout": "def count_label(count):\n    if count == 0:\n        return \"\"\n    return f\"{count} items\"\n",
    "stderr": ""
  },
  {
    "args": [
      "diff",
      "--no-ext-diff",
      "--no-textconv",
      "--no-renames",
      "--no-color",
      "--full-index",
      "--unified=3",
      "--diff-algorithm=myers",
      "--no-indent-heuristic",
      "--src-prefix=a/",
      "--dst-prefix=b/",
      "0e1899c60081fff643e71d270c11aafa8b1e485d...0b8da4cff382b93299301119dfce545a73ae7eee",
      "--"
    ],
    "exit": 0,
    "stdout": "diff --git a/src/labels.py b/src/labels.py\nindex 58639ef21d6f4a718712ec4cce15939e1b686366..83967ce91551959e78912d0ad447de5f0c295d74 100644\n--- a/src/labels.py\n+++ b/src/labels.py\n@@ -1,2 +1,4 @@\n def count_label(count):\n+    if count == 0:\n+        return \"\"\n     return f\"{count} items\"\n",
    "stderr": ""
  },
  {
    "args": [
      "diff",
      "--no-ext-diff",
      "--no-textconv",
      "--no-renames",
      "--no-color",
      "--full-index",
      "--unified=3",
      "--diff-algorithm=myers",
      "--no-indent-heuristic",
      "--src-prefix=a/",
      "--dst-prefix=b/",
      "0e1899c60081fff643e71d270c11aafa8b1e485d..0b8da4cff382b93299301119dfce545a73ae7eee",
      "--"
    ],
    "exit": 0,
    "stdout": "diff --git a/release.txt b/release.txt\ndeleted file mode 100644\nindex 7e5631dd59966a63149a406eb6b735149b20cb6b..0000000000000000000000000000000000000000\n--- a/release.txt\n+++ /dev/null\n@@ -1 +0,0 @@\n-Release notes: keep zero quantities visible.\ndiff --git a/src/labels.py b/src/labels.py\nindex 58639ef21d6f4a718712ec4cce15939e1b686366..83967ce91551959e78912d0ad447de5f0c295d74 100644\n--- a/src/labels.py\n+++ b/src/labels.py\n@@ -1,2 +1,4 @@\n def count_label(count):\n+    if count == 0:\n+        return \"\"\n     return f\"{count} items\"\n",
    "stderr": ""
  },
  {
    "args": [
      "diff",
      "--no-ext-diff",
      "--no-textconv",
      "--no-renames",
      "--no-color",
      "--full-index",
      "--unified=3",
      "--diff-algorithm=myers",
      "--no-indent-heuristic",
      "--src-prefix=a/",
      "--dst-prefix=b/",
      "0e1899c60081fff643e71d270c11aafa8b1e485d...0b8da4cff382b93299301119dfce545a73ae7eee",
      "--"
    ],
    "exit": 0,
    "stdout": "diff --git a/src/labels.py b/src/labels.py\nindex 58639ef21d6f4a718712ec4cce15939e1b686366..83967ce91551959e78912d0ad447de5f0c295d74 100644\n--- a/src/labels.py\n+++ b/src/labels.py\n@@ -1,2 +1,4 @@\n def count_label(count):\n+    if count == 0:\n+        return \"\"\n     return f\"{count} items\"\n",
    "stderr": ""
  },
  {
    "args": [
      "rev-parse",
      "--verify",
      "--end-of-options",
      "0e1899c60081fff643e71d270c11aafa8b1e485d^{commit}"
    ],
    "exit": 0,
    "stdout": "0e1899c60081fff643e71d270c11aafa8b1e485d\n",
    "stderr": ""
  },
  {
    "args": [
      "cat-file",
      "-t",
      "0e1899c60081fff643e71d270c11aafa8b1e485d"
    ],
    "exit": 0,
    "stdout": "commit\n",
    "stderr": ""
  },
  {
    "args": [
      "rev-parse",
      "--verify",
      "--end-of-options",
      "0b8da4cff382b93299301119dfce545a73ae7eee^{commit}"
    ],
    "exit": 0,
    "stdout": "0b8da4cff382b93299301119dfce545a73ae7eee\n",
    "stderr": ""
  },
  {
    "args": [
      "cat-file",
      "-t",
      "0b8da4cff382b93299301119dfce545a73ae7eee"
    ],
    "exit": 0,
    "stdout": "commit\n",
    "stderr": ""
  },
  {
    "args": [
      "rev-parse",
      "--is-shallow-repository"
    ],
    "exit": 0,
    "stdout": "false\n",
    "stderr": ""
  },
  {
    "args": [
      "merge-base",
      "--all",
      "0e1899c60081fff643e71d270c11aafa8b1e485d",
      "0b8da4cff382b93299301119dfce545a73ae7eee"
    ],
    "exit": 0,
    "stdout": "6b3c22d3e3dcb2df13aa55e46ba2fbd24e6680b5\n",
    "stderr": ""
  },
  {
    "args": [
      "rev-parse",
      "--verify",
      "--end-of-options",
      "6b3c22d3e3dcb2df13aa55e46ba2fbd24e6680b5^{commit}"
    ],
    "exit": 0,
    "stdout": "6b3c22d3e3dcb2df13aa55e46ba2fbd24e6680b5\n",
    "stderr": ""
  },
  {
    "args": [
      "cat-file",
      "-t",
      "6b3c22d3e3dcb2df13aa55e46ba2fbd24e6680b5"
    ],
    "exit": 0,
    "stdout": "commit\n",
    "stderr": ""
  },
  {
    "args": [
      "rev-list",
      "--parents",
      "-n",
      "1",
      "0e1899c60081fff643e71d270c11aafa8b1e485d"
    ],
    "exit": 0,
    "stdout": "0e1899c60081fff643e71d270c11aafa8b1e485d 6b3c22d3e3dcb2df13aa55e46ba2fbd24e6680b5\n",
    "stderr": ""
  },
  {
    "args": [
      "rev-list",
      "--parents",
      "-n",
      "1",
      "0b8da4cff382b93299301119dfce545a73ae7eee"
    ],
    "exit": 0,
    "stdout": "0b8da4cff382b93299301119dfce545a73ae7eee 6b3c22d3e3dcb2df13aa55e46ba2fbd24e6680b5\n",
    "stderr": ""
  },
  {
    "args": [
      "ls-tree",
      "-r",
      "-z",
      "--full-tree",
      "6b3c22d3e3dcb2df13aa55e46ba2fbd24e6680b5"
    ],
    "exit": 0,
    "stdout": "100644 blob 58639ef21d6f4a718712ec4cce15939e1b686366\tsrc/labels.py\u0000",
    "stderr": ""
  },
  {
    "args": [
      "cat-file",
      "blob",
      "58639ef21d6f4a718712ec4cce15939e1b686366"
    ],
    "exit": 0,
    "stdout": "def count_label(count):\n    return f\"{count} items\"\n",
    "stderr": ""
  },
  {
    "args": [
      "ls-tree",
      "-r",
      "-z",
      "--full-tree",
      "0b8da4cff382b93299301119dfce545a73ae7eee"
    ],
    "exit": 0,
    "stdout": "100644 blob 83967ce91551959e78912d0ad447de5f0c295d74\tsrc/labels.py\u0000",
    "stderr": ""
  },
  {
    "args": [
      "cat-file",
      "blob",
      "83967ce91551959e78912d0ad447de5f0c295d74"
    ],
    "exit": 0,
    "stdout": "def count_label(count):\n    if count == 0:\n        return \"\"\n    return f\"{count} items\"\n",
    "stderr": ""
  },
  {
    "args": [
      "diff",
      "--no-ext-diff",
      "--no-textconv",
      "--no-renames",
      "--name-status",
      "-z",
      "6b3c22d3e3dcb2df13aa55e46ba2fbd24e6680b5",
      "0b8da4cff382b93299301119dfce545a73ae7eee",
      "--"
    ],
    "exit": 0,
    "stdout": "M\u0000src/labels.py\u0000",
    "stderr": ""
  },
  {
    "args": [
      "diff",
      "--no-ext-diff",
      "--no-textconv",
      "--no-renames",
      "--no-color",
      "--full-index",
      "--unified=3",
      "--diff-algorithm=myers",
      "--no-indent-heuristic",
      "--src-prefix=a/",
      "--dst-prefix=b/",
      "0e1899c60081fff643e71d270c11aafa8b1e485d...0b8da4cff382b93299301119dfce545a73ae7eee",
      "--"
    ],
    "exit": 0,
    "stdout": "diff --git a/src/labels.py b/src/labels.py\nindex 58639ef21d6f4a718712ec4cce15939e1b686366..83967ce91551959e78912d0ad447de5f0c295d74 100644\n--- a/src/labels.py\n+++ b/src/labels.py\n@@ -1,2 +1,4 @@\n def count_label(count):\n+    if count == 0:\n+        return \"\"\n     return f\"{count} items\"\n",
    "stderr": ""
  },
  {
    "args": [
      "rev-parse",
      "--verify",
      "--end-of-options",
      "0e1899c60081fff643e71d270c11aafa8b1e485d^{commit}"
    ],
    "exit": 0,
    "stdout": "0e1899c60081fff643e71d270c11aafa8b1e485d\n",
    "stderr": ""
  },
  {
    "args": [
      "cat-file",
      "-t",
      "0e1899c60081fff643e71d270c11aafa8b1e485d"
    ],
    "exit": 0,
    "stdout": "commit\n",
    "stderr": ""
  },
  {
    "args": [
      "rev-parse",
      "--verify",
      "--end-of-options",
      "0b8da4cff382b93299301119dfce545a73ae7eee^{commit}"
    ],
    "exit": 0,
    "stdout": "0b8da4cff382b93299301119dfce545a73ae7eee\n",
    "stderr": ""
  },
  {
    "args": [
      "cat-file",
      "-t",
      "0b8da4cff382b93299301119dfce545a73ae7eee"
    ],
    "exit": 0,
    "stdout": "commit\n",
    "stderr": ""
  },
  {
    "args": [
      "rev-parse",
      "--is-shallow-repository"
    ],
    "exit": 0,
    "stdout": "false\n",
    "stderr": ""
  },
  {
    "args": [
      "merge-base",
      "--all",
      "0e1899c60081fff643e71d270c11aafa8b1e485d",
      "0b8da4cff382b93299301119dfce545a73ae7eee"
    ],
    "exit": 0,
    "stdout": "6b3c22d3e3dcb2df13aa55e46ba2fbd24e6680b5\n",
    "stderr": ""
  },
  {
    "args": [
      "rev-parse",
      "--verify",
      "--end-of-options",
      "6b3c22d3e3dcb2df13aa55e46ba2fbd24e6680b5^{commit}"
    ],
    "exit": 0,
    "stdout": "6b3c22d3e3dcb2df13aa55e46ba2fbd24e6680b5\n",
    "stderr": ""
  },
  {
    "args": [
      "cat-file",
      "-t",
      "6b3c22d3e3dcb2df13aa55e46ba2fbd24e6680b5"
    ],
    "exit": 0,
    "stdout": "commit\n",
    "stderr": ""
  },
  {
    "args": [
      "rev-list",
      "--parents",
      "-n",
      "1",
      "0e1899c60081fff643e71d270c11aafa8b1e485d"
    ],
    "exit": 0,
    "stdout": "0e1899c60081fff643e71d270c11aafa8b1e485d 6b3c22d3e3dcb2df13aa55e46ba2fbd24e6680b5\n",
    "stderr": ""
  },
  {
    "args": [
      "rev-list",
      "--parents",
      "-n",
      "1",
      "0b8da4cff382b93299301119dfce545a73ae7eee"
    ],
    "exit": 0,
    "stdout": "0b8da4cff382b93299301119dfce545a73ae7eee 6b3c22d3e3dcb2df13aa55e46ba2fbd24e6680b5\n",
    "stderr": ""
  },
  {
    "args": [
      "rev-parse",
      "--verify",
      "--end-of-options",
      "0e1899c60081fff643e71d270c11aafa8b1e485d^{commit}"
    ],
    "exit": 0,
    "stdout": "0e1899c60081fff643e71d270c11aafa8b1e485d\n",
    "stderr": ""
  },
  {
    "args": [
      "cat-file",
      "-t",
      "0e1899c60081fff643e71d270c11aafa8b1e485d"
    ],
    "exit": 0,
    "stdout": "commit\n",
    "stderr": ""
  },
  {
    "args": [
      "rev-parse",
      "--verify",
      "--end-of-options",
      "0b8da4cff382b93299301119dfce545a73ae7eee^{commit}"
    ],
    "exit": 0,
    "stdout": "0b8da4cff382b93299301119dfce545a73ae7eee\n",
    "stderr": ""
  },
  {
    "args": [
      "cat-file",
      "-t",
      "0b8da4cff382b93299301119dfce545a73ae7eee"
    ],
    "exit": 0,
    "stdout": "commit\n",
    "stderr": ""
  },
  {
    "args": [
      "rev-parse",
      "--is-shallow-repository"
    ],
    "exit": 0,
    "stdout": "false\n",
    "stderr": ""
  },
  {
    "args": [
      "merge-base",
      "--all",
      "0e1899c60081fff643e71d270c11aafa8b1e485d",
      "0b8da4cff382b93299301119dfce545a73ae7eee"
    ],
    "exit": 0,
    "stdout": "6b3c22d3e3dcb2df13aa55e46ba2fbd24e6680b5\n",
    "stderr": ""
  },
  {
    "args": [
      "rev-parse",
      "--verify",
      "--end-of-options",
      "6b3c22d3e3dcb2df13aa55e46ba2fbd24e6680b5^{commit}"
    ],
    "exit": 0,
    "stdout": "6b3c22d3e3dcb2df13aa55e46ba2fbd24e6680b5\n",
    "stderr": ""
  },
  {
    "args": [
      "cat-file",
      "-t",
      "6b3c22d3e3dcb2df13aa55e46ba2fbd24e6680b5"
    ],
    "exit": 0,
    "stdout": "commit\n",
    "stderr": ""
  },
  {
    "args": [
      "rev-list",
      "--parents",
      "-n",
      "1",
      "0e1899c60081fff643e71d270c11aafa8b1e485d"
    ],
    "exit": 0,
    "stdout": "0e1899c60081fff643e71d270c11aafa8b1e485d 6b3c22d3e3dcb2df13aa55e46ba2fbd24e6680b5\n",
    "stderr": ""
  },
  {
    "args": [
      "rev-list",
      "--parents",
      "-n",
      "1",
      "0b8da4cff382b93299301119dfce545a73ae7eee"
    ],
    "exit": 0,
    "stdout": "0b8da4cff382b93299301119dfce545a73ae7eee 6b3c22d3e3dcb2df13aa55e46ba2fbd24e6680b5\n",
    "stderr": ""
  },
  {
    "args": [
      "rev-parse",
      "--verify",
      "--end-of-options",
      "0e1899c60081fff643e71d270c11aafa8b1e485d^{commit}"
    ],
    "exit": 0,
    "stdout": "0e1899c60081fff643e71d270c11aafa8b1e485d\n",
    "stderr": ""
  },
  {
    "args": [
      "cat-file",
      "-t",
      "0e1899c60081fff643e71d270c11aafa8b1e485d"
    ],
    "exit": 0,
    "stdout": "commit\n",
    "stderr": ""
  },
  {
    "args": [
      "rev-parse",
      "--verify",
      "--end-of-options",
      "0b8da4cff382b93299301119dfce545a73ae7eee^{commit}"
    ],
    "exit": 0,
    "stdout": "0b8da4cff382b93299301119dfce545a73ae7eee\n",
    "stderr": ""
  },
  {
    "args": [
      "cat-file",
      "-t",
      "0b8da4cff382b93299301119dfce545a73ae7eee"
    ],
    "exit": 0,
    "stdout": "commit\n",
    "stderr": ""
  },
  {
    "args": [
      "rev-parse",
      "--is-shallow-repository"
    ],
    "exit": 0,
    "stdout": "false\n",
    "stderr": ""
  },
  {
    "args": [
      "merge-base",
      "--all",
      "0e1899c60081fff643e71d270c11aafa8b1e485d",
      "0b8da4cff382b93299301119dfce545a73ae7eee"
    ],
    "exit": 0,
    "stdout": "6b3c22d3e3dcb2df13aa55e46ba2fbd24e6680b5\n",
    "stderr": ""
  },
  {
    "args": [
      "rev-parse",
      "--verify",
      "--end-of-options",
      "6b3c22d3e3dcb2df13aa55e46ba2fbd24e6680b5^{commit}"
    ],
    "exit": 0,
    "stdout": "6b3c22d3e3dcb2df13aa55e46ba2fbd24e6680b5\n",
    "stderr": ""
  },
  {
    "args": [
      "cat-file",
      "-t",
      "6b3c22d3e3dcb2df13aa55e46ba2fbd24e6680b5"
    ],
    "exit": 0,
    "stdout": "commit\n",
    "stderr": ""
  },
  {
    "args": [
      "rev-list",
      "--parents",
      "-n",
      "1",
      "0e1899c60081fff643e71d270c11aafa8b1e485d"
    ],
    "exit": 0,
    "stdout": "0e1899c60081fff643e71d270c11aafa8b1e485d 6b3c22d3e3dcb2df13aa55e46ba2fbd24e6680b5\n",
    "stderr": ""
  },
  {
    "args": [
      "rev-list",
      "--parents",
      "-n",
      "1",
      "0b8da4cff382b93299301119dfce545a73ae7eee"
    ],
    "exit": 0,
    "stdout": "0b8da4cff382b93299301119dfce545a73ae7eee 6b3c22d3e3dcb2df13aa55e46ba2fbd24e6680b5\n",
    "stderr": ""
  },
  {
    "args": [
      "rev-parse",
      "--verify",
      "--end-of-options",
      "0e1899c60081fff643e71d270c11aafa8b1e485d^{commit}"
    ],
    "exit": 0,
    "stdout": "0e1899c60081fff643e71d270c11aafa8b1e485d\n",
    "stderr": ""
  },
  {
    "args": [
      "cat-file",
      "-t",
      "0e1899c60081fff643e71d270c11aafa8b1e485d"
    ],
    "exit": 0,
    "stdout": "commit\n",
    "stderr": ""
  },
  {
    "args": [
      "rev-parse",
      "--verify",
      "--end-of-options",
      "0e1899c60081fff643e71d270c11aafa8b1e485d^{commit}"
    ],
    "exit": 0,
    "stdout": "0e1899c60081fff643e71d270c11aafa8b1e485d\n",
    "stderr": ""
  },
  {
    "args": [
      "cat-file",
      "-t",
      "0e1899c60081fff643e71d270c11aafa8b1e485d"
    ],
    "exit": 0,
    "stdout": "commit\n",
    "stderr": ""
  },
  {
    "args": [
      "rev-parse",
      "--is-shallow-repository"
    ],
    "exit": 0,
    "stdout": "false\n",
    "stderr": ""
  },
  {
    "args": [
      "merge-base",
      "--all",
      "0e1899c60081fff643e71d270c11aafa8b1e485d",
      "0e1899c60081fff643e71d270c11aafa8b1e485d"
    ],
    "exit": 0,
    "stdout": "0e1899c60081fff643e71d270c11aafa8b1e485d\n",
    "stderr": ""
  },
  {
    "args": [
      "rev-parse",
      "--verify",
      "--end-of-options",
      "0e1899c60081fff643e71d270c11aafa8b1e485d^{commit}"
    ],
    "exit": 0,
    "stdout": "0e1899c60081fff643e71d270c11aafa8b1e485d\n",
    "stderr": ""
  },
  {
    "args": [
      "cat-file",
      "-t",
      "0e1899c60081fff643e71d270c11aafa8b1e485d"
    ],
    "exit": 0,
    "stdout": "commit\n",
    "stderr": ""
  },
  {
    "args": [
      "rev-list",
      "--parents",
      "-n",
      "1",
      "0e1899c60081fff643e71d270c11aafa8b1e485d"
    ],
    "exit": 0,
    "stdout": "0e1899c60081fff643e71d270c11aafa8b1e485d 6b3c22d3e3dcb2df13aa55e46ba2fbd24e6680b5\n",
    "stderr": ""
  },
  {
    "args": [
      "rev-parse",
      "--verify",
      "--end-of-options",
      "0e1899c60081fff643e71d270c11aafa8b1e485d^{commit}"
    ],
    "exit": 0,
    "stdout": "0e1899c60081fff643e71d270c11aafa8b1e485d\n",
    "stderr": ""
  },
  {
    "args": [
      "cat-file",
      "-t",
      "0e1899c60081fff643e71d270c11aafa8b1e485d"
    ],
    "exit": 0,
    "stdout": "commit\n",
    "stderr": ""
  },
  {
    "args": [
      "rev-parse",
      "--verify",
      "--end-of-options",
      "0b8da4cff382b93299301119dfce545a73ae7eee^{commit}"
    ],
    "exit": 0,
    "stdout": "0b8da4cff382b93299301119dfce545a73ae7eee\n",
    "stderr": ""
  },
  {
    "args": [
      "cat-file",
      "-t",
      "0b8da4cff382b93299301119dfce545a73ae7eee"
    ],
    "exit": 0,
    "stdout": "commit\n",
    "stderr": ""
  },
  {
    "args": [
      "rev-parse",
      "--is-shallow-repository"
    ],
    "exit": 0,
    "stdout": "false\n",
    "stderr": ""
  },
  {
    "args": [
      "merge-base",
      "--all",
      "0e1899c60081fff643e71d270c11aafa8b1e485d",
      "0b8da4cff382b93299301119dfce545a73ae7eee"
    ],
    "exit": 0,
    "stdout": "6b3c22d3e3dcb2df13aa55e46ba2fbd24e6680b5\n",
    "stderr": ""
  },
  {
    "args": [
      "rev-parse",
      "--verify",
      "--end-of-options",
      "6b3c22d3e3dcb2df13aa55e46ba2fbd24e6680b5^{commit}"
    ],
    "exit": 0,
    "stdout": "6b3c22d3e3dcb2df13aa55e46ba2fbd24e6680b5\n",
    "stderr": ""
  },
  {
    "args": [
      "cat-file",
      "-t",
      "6b3c22d3e3dcb2df13aa55e46ba2fbd24e6680b5"
    ],
    "exit": 0,
    "stdout": "commit\n",
    "stderr": ""
  },
  {
    "args": [
      "rev-list",
      "--parents",
      "-n",
      "1",
      "0e1899c60081fff643e71d270c11aafa8b1e485d"
    ],
    "exit": 0,
    "stdout": "0e1899c60081fff643e71d270c11aafa8b1e485d 6b3c22d3e3dcb2df13aa55e46ba2fbd24e6680b5\n",
    "stderr": ""
  },
  {
    "args": [
      "rev-list",
      "--parents",
      "-n",
      "1",
      "0b8da4cff382b93299301119dfce545a73ae7eee"
    ],
    "exit": 0,
    "stdout": "0b8da4cff382b93299301119dfce545a73ae7eee 6b3c22d3e3dcb2df13aa55e46ba2fbd24e6680b5\n",
    "stderr": ""
  },
  {
    "args": [
      "ls-tree",
      "-r",
      "-z",
      "--full-tree",
      "6b3c22d3e3dcb2df13aa55e46ba2fbd24e6680b5"
    ],
    "exit": 0,
    "stdout": "100644 blob 58639ef21d6f4a718712ec4cce15939e1b686366\tsrc/labels.py\u0000",
    "stderr": ""
  },
  {
    "args": [
      "cat-file",
      "blob",
      "58639ef21d6f4a718712ec4cce15939e1b686366"
    ],
    "exit": 0,
    "stdout": "def count_label(count):\n    return f\"{count} items\"\n",
    "stderr": ""
  },
  {
    "args": [
      "ls-tree",
      "-r",
      "-z",
      "--full-tree",
      "0b8da4cff382b93299301119dfce545a73ae7eee"
    ],
    "exit": 0,
    "stdout": "100644 blob 83967ce91551959e78912d0ad447de5f0c295d74\tsrc/labels.py\u0000",
    "stderr": ""
  },
  {
    "args": [
      "cat-file",
      "blob",
      "83967ce91551959e78912d0ad447de5f0c295d74"
    ],
    "exit": 0,
    "stdout": "def count_label(count):\n    if count == 0:\n        return \"\"\n    return f\"{count} items\"\n",
    "stderr": ""
  },
  {
    "args": [
      "diff",
      "--no-ext-diff",
      "--no-textconv",
      "--no-renames",
      "--name-status",
      "-z",
      "6b3c22d3e3dcb2df13aa55e46ba2fbd24e6680b5",
      "0b8da4cff382b93299301119dfce545a73ae7eee",
      "--"
    ],
    "exit": 0,
    "stdout": "M\u0000src/labels.py\u0000",
    "stderr": ""
  },
  {
    "args": [
      "rev-parse",
      "--verify",
      "--end-of-options",
      "0e1899c60081fff643e71d270c11aafa8b1e485d^{commit}"
    ],
    "exit": 0,
    "stdout": "0e1899c60081fff643e71d270c11aafa8b1e485d\n",
    "stderr": ""
  },
  {
    "args": [
      "cat-file",
      "-t",
      "0e1899c60081fff643e71d270c11aafa8b1e485d"
    ],
    "exit": 0,
    "stdout": "commit\n",
    "stderr": ""
  },
  {
    "args": [
      "rev-parse",
      "--verify",
      "--end-of-options",
      "0b8da4cff382b93299301119dfce545a73ae7eee^{commit}"
    ],
    "exit": 0,
    "stdout": "0b8da4cff382b93299301119dfce545a73ae7eee\n",
    "stderr": ""
  },
  {
    "args": [
      "cat-file",
      "-t",
      "0b8da4cff382b93299301119dfce545a73ae7eee"
    ],
    "exit": 0,
    "stdout": "commit\n",
    "stderr": ""
  },
  {
    "args": [
      "rev-parse",
      "--is-shallow-repository"
    ],
    "exit": 0,
    "stdout": "false\n",
    "stderr": ""
  },
  {
    "args": [
      "merge-base",
      "--all",
      "0e1899c60081fff643e71d270c11aafa8b1e485d",
      "0b8da4cff382b93299301119dfce545a73ae7eee"
    ],
    "exit": 0,
    "stdout": "6b3c22d3e3dcb2df13aa55e46ba2fbd24e6680b5\n",
    "stderr": ""
  },
  {
    "args": [
      "rev-parse",
      "--verify",
      "--end-of-options",
      "6b3c22d3e3dcb2df13aa55e46ba2fbd24e6680b5^{commit}"
    ],
    "exit": 0,
    "stdout": "6b3c22d3e3dcb2df13aa55e46ba2fbd24e6680b5\n",
    "stderr": ""
  },
  {
    "args": [
      "cat-file",
      "-t",
      "6b3c22d3e3dcb2df13aa55e46ba2fbd24e6680b5"
    ],
    "exit": 0,
    "stdout": "commit\n",
    "stderr": ""
  },
  {
    "args": [
      "rev-list",
      "--parents",
      "-n",
      "1",
      "0e1899c60081fff643e71d270c11aafa8b1e485d"
    ],
    "exit": 0,
    "stdout": "0e1899c60081fff643e71d270c11aafa8b1e485d 6b3c22d3e3dcb2df13aa55e46ba2fbd24e6680b5\n",
    "stderr": ""
  },
  {
    "args": [
      "rev-list",
      "--parents",
      "-n",
      "1",
      "0b8da4cff382b93299301119dfce545a73ae7eee"
    ],
    "exit": 0,
    "stdout": "0b8da4cff382b93299301119dfce545a73ae7eee 6b3c22d3e3dcb2df13aa55e46ba2fbd24e6680b5\n",
    "stderr": ""
  },
  {
    "args": [
      "ls-tree",
      "-r",
      "-z",
      "--full-tree",
      "6b3c22d3e3dcb2df13aa55e46ba2fbd24e6680b5"
    ],
    "exit": 0,
    "stdout": "100644 blob 58639ef21d6f4a718712ec4cce15939e1b686366\tsrc/labels.py\u0000",
    "stderr": ""
  },
  {
    "args": [
      "cat-file",
      "blob",
      "58639ef21d6f4a718712ec4cce15939e1b686366"
    ],
    "exit": 0,
    "stdout": "def count_label(count):\n    return f\"{count} items\"\n",
    "stderr": ""
  },
  {
    "args": [
      "ls-tree",
      "-r",
      "-z",
      "--full-tree",
      "0b8da4cff382b93299301119dfce545a73ae7eee"
    ],
    "exit": 0,
    "stdout": "100644 blob 83967ce91551959e78912d0ad447de5f0c295d74\tsrc/labels.py\u0000",
    "stderr": ""
  },
  {
    "args": [
      "cat-file",
      "blob",
      "83967ce91551959e78912d0ad447de5f0c295d74"
    ],
    "exit": 0,
    "stdout": "def count_label(count):\n    if count == 0:\n        return \"\"\n    return f\"{count} items\"\n",
    "stderr": ""
  },
  {
    "args": [
      "diff",
      "--no-ext-diff",
      "--no-textconv",
      "--no-renames",
      "--name-status",
      "-z",
      "6b3c22d3e3dcb2df13aa55e46ba2fbd24e6680b5",
      "0b8da4cff382b93299301119dfce545a73ae7eee",
      "--"
    ],
    "exit": 0,
    "stdout": "M\u0000src/labels.py\u0000",
    "stderr": ""
  },
  {
    "args": [
      "ls-tree",
      "-r",
      "-z",
      "--full-tree",
      "0b8da4cff382b93299301119dfce545a73ae7eee"
    ],
    "exit": 0,
    "stdout": "100644 blob 83967ce91551959e78912d0ad447de5f0c295d74\tsrc/labels.py\u0000",
    "stderr": ""
  },
  {
    "args": [
      "cat-file",
      "blob",
      "83967ce91551959e78912d0ad447de5f0c295d74"
    ],
    "exit": 0,
    "stdout": "def count_label(count):\n    if count == 0:\n        return \"\"\n    return f\"{count} items\"\n",
    "stderr": ""
  },
  {
    "args": [
      "ls-tree",
      "-r",
      "-z",
      "--full-tree",
      "6b3c22d3e3dcb2df13aa55e46ba2fbd24e6680b5"
    ],
    "exit": 0,
    "stdout": "100644 blob 58639ef21d6f4a718712ec4cce15939e1b686366\tsrc/labels.py\u0000",
    "stderr": ""
  },
  {
    "args": [
      "cat-file",
      "blob",
      "58639ef21d6f4a718712ec4cce15939e1b686366"
    ],
    "exit": 0,
    "stdout": "def count_label(count):\n    return f\"{count} items\"\n",
    "stderr": ""
  },
  {
    "args": [
      "ls-tree",
      "-r",
      "-z",
      "--full-tree",
      "0b8da4cff382b93299301119dfce545a73ae7eee"
    ],
    "exit": 0,
    "stdout": "100644 blob 83967ce91551959e78912d0ad447de5f0c295d74\tsrc/labels.py\u0000",
    "stderr": ""
  },
  {
    "args": [
      "cat-file",
      "blob",
      "83967ce91551959e78912d0ad447de5f0c295d74"
    ],
    "exit": 0,
    "stdout": "def count_label(count):\n    if count == 0:\n        return \"\"\n    return f\"{count} items\"\n",
    "stderr": ""
  },
  {
    "args": [
      "checkout",
      "--quiet",
      "main"
    ],
    "exit": 0,
    "stdout": "",
    "stderr": ""
  },
  {
    "args": [
      "add",
      "--",
      "release.txt"
    ],
    "exit": 0,
    "stdout": "",
    "stderr": ""
  },
  {
    "args": [
      "commit",
      "--quiet",
      "-m",
      "fixture: main moves after capture"
    ],
    "exit": 0,
    "stdout": "",
    "stderr": ""
  },
  {
    "args": [
      "rev-parse",
      "HEAD"
    ],
    "exit": 0,
    "stdout": "00115dfcf209c7efeb498c0cda22e6fb765f8dd1\n",
    "stderr": ""
  },
  {
    "args": [
      "checkout",
      "--quiet",
      "feature"
    ],
    "exit": 0,
    "stdout": "",
    "stderr": ""
  },
  {
    "args": [
      "add",
      "--",
      "src/labels.py"
    ],
    "exit": 0,
    "stdout": "",
    "stderr": ""
  },
  {
    "args": [
      "commit",
      "--quiet",
      "-m",
      "fixture: feature moves after capture"
    ],
    "exit": 0,
    "stdout": "",
    "stderr": ""
  },
  {
    "args": [
      "rev-parse",
      "HEAD"
    ],
    "exit": 0,
    "stdout": "e693420956696987a95109124a98f34748233d76\n",
    "stderr": ""
  },
  {
    "args": [
      "diff",
      "--no-ext-diff",
      "--no-textconv",
      "--no-renames",
      "--no-color",
      "--full-index",
      "--unified=3",
      "--diff-algorithm=myers",
      "--no-indent-heuristic",
      "--src-prefix=a/",
      "--dst-prefix=b/",
      "0e1899c60081fff643e71d270c11aafa8b1e485d...0b8da4cff382b93299301119dfce545a73ae7eee",
      "--"
    ],
    "exit": 0,
    "stdout": "diff --git a/src/labels.py b/src/labels.py\nindex 58639ef21d6f4a718712ec4cce15939e1b686366..83967ce91551959e78912d0ad447de5f0c295d74 100644\n--- a/src/labels.py\n+++ b/src/labels.py\n@@ -1,2 +1,4 @@\n def count_label(count):\n+    if count == 0:\n+        return \"\"\n     return f\"{count} items\"\n",
    "stderr": ""
  },
  {
    "args": [
      "diff",
      "--no-ext-diff",
      "--no-textconv",
      "--no-renames",
      "--no-color",
      "--full-index",
      "--unified=3",
      "--diff-algorithm=myers",
      "--no-indent-heuristic",
      "--src-prefix=a/",
      "--dst-prefix=b/",
      "main...feature",
      "--"
    ],
    "exit": 0,
    "stdout": "",
    "stderr": ""
  },
  {
    "args": [
      "diff",
      "--no-ext-diff",
      "--no-textconv",
      "--no-renames",
      "--no-color",
      "--full-index",
      "--unified=3",
      "--diff-algorithm=myers",
      "--no-indent-heuristic",
      "--src-prefix=a/",
      "--dst-prefix=b/",
      "0e1899c60081fff643e71d270c11aafa8b1e485d..0b8da4cff382b93299301119dfce545a73ae7eee",
      "--"
    ],
    "exit": 0,
    "stdout": "diff --git a/release.txt b/release.txt\ndeleted file mode 100644\nindex 7e5631dd59966a63149a406eb6b735149b20cb6b..0000000000000000000000000000000000000000\n--- a/release.txt\n+++ /dev/null\n@@ -1 +0,0 @@\n-Release notes: keep zero quantities visible.\ndiff --git a/src/labels.py b/src/labels.py\nindex 58639ef21d6f4a718712ec4cce15939e1b686366..83967ce91551959e78912d0ad447de5f0c295d74 100644\n--- a/src/labels.py\n+++ b/src/labels.py\n@@ -1,2 +1,4 @@\n def count_label(count):\n+    if count == 0:\n+        return \"\"\n     return f\"{count} items\"\n",
    "stderr": ""
  },
  {
    "args": [
      "rev-parse",
      "--verify",
      "--end-of-options",
      "0e1899c60081fff643e71d270c11aafa8b1e485d^{commit}"
    ],
    "exit": 0,
    "stdout": "0e1899c60081fff643e71d270c11aafa8b1e485d\n",
    "stderr": ""
  },
  {
    "args": [
      "cat-file",
      "-t",
      "0e1899c60081fff643e71d270c11aafa8b1e485d"
    ],
    "exit": 0,
    "stdout": "commit\n",
    "stderr": ""
  },
  {
    "args": [
      "rev-parse",
      "--verify",
      "--end-of-options",
      "0b8da4cff382b93299301119dfce545a73ae7eee^{commit}"
    ],
    "exit": 0,
    "stdout": "0b8da4cff382b93299301119dfce545a73ae7eee\n",
    "stderr": ""
  },
  {
    "args": [
      "cat-file",
      "-t",
      "0b8da4cff382b93299301119dfce545a73ae7eee"
    ],
    "exit": 0,
    "stdout": "commit\n",
    "stderr": ""
  },
  {
    "args": [
      "rev-parse",
      "--is-shallow-repository"
    ],
    "exit": 0,
    "stdout": "false\n",
    "stderr": ""
  },
  {
    "args": [
      "merge-base",
      "--all",
      "0e1899c60081fff643e71d270c11aafa8b1e485d",
      "0b8da4cff382b93299301119dfce545a73ae7eee"
    ],
    "exit": 0,
    "stdout": "6b3c22d3e3dcb2df13aa55e46ba2fbd24e6680b5\n",
    "stderr": ""
  },
  {
    "args": [
      "rev-parse",
      "--verify",
      "--end-of-options",
      "6b3c22d3e3dcb2df13aa55e46ba2fbd24e6680b5^{commit}"
    ],
    "exit": 0,
    "stdout": "6b3c22d3e3dcb2df13aa55e46ba2fbd24e6680b5\n",
    "stderr": ""
  },
  {
    "args": [
      "cat-file",
      "-t",
      "6b3c22d3e3dcb2df13aa55e46ba2fbd24e6680b5"
    ],
    "exit": 0,
    "stdout": "commit\n",
    "stderr": ""
  },
  {
    "args": [
      "rev-list",
      "--parents",
      "-n",
      "1",
      "0e1899c60081fff643e71d270c11aafa8b1e485d"
    ],
    "exit": 0,
    "stdout": "0e1899c60081fff643e71d270c11aafa8b1e485d 6b3c22d3e3dcb2df13aa55e46ba2fbd24e6680b5\n",
    "stderr": ""
  },
  {
    "args": [
      "rev-list",
      "--parents",
      "-n",
      "1",
      "0b8da4cff382b93299301119dfce545a73ae7eee"
    ],
    "exit": 0,
    "stdout": "0b8da4cff382b93299301119dfce545a73ae7eee 6b3c22d3e3dcb2df13aa55e46ba2fbd24e6680b5\n",
    "stderr": ""
  },
  {
    "args": [
      "ls-tree",
      "-r",
      "-z",
      "--full-tree",
      "6b3c22d3e3dcb2df13aa55e46ba2fbd24e6680b5"
    ],
    "exit": 0,
    "stdout": "100644 blob 58639ef21d6f4a718712ec4cce15939e1b686366\tsrc/labels.py\u0000",
    "stderr": ""
  },
  {
    "args": [
      "cat-file",
      "blob",
      "58639ef21d6f4a718712ec4cce15939e1b686366"
    ],
    "exit": 0,
    "stdout": "def count_label(count):\n    return f\"{count} items\"\n",
    "stderr": ""
  },
  {
    "args": [
      "ls-tree",
      "-r",
      "-z",
      "--full-tree",
      "0b8da4cff382b93299301119dfce545a73ae7eee"
    ],
    "exit": 0,
    "stdout": "100644 blob 83967ce91551959e78912d0ad447de5f0c295d74\tsrc/labels.py\u0000",
    "stderr": ""
  },
  {
    "args": [
      "cat-file",
      "blob",
      "83967ce91551959e78912d0ad447de5f0c295d74"
    ],
    "exit": 0,
    "stdout": "def count_label(count):\n    if count == 0:\n        return \"\"\n    return f\"{count} items\"\n",
    "stderr": ""
  },
  {
    "args": [
      "diff",
      "--no-ext-diff",
      "--no-textconv",
      "--no-renames",
      "--name-status",
      "-z",
      "6b3c22d3e3dcb2df13aa55e46ba2fbd24e6680b5",
      "0b8da4cff382b93299301119dfce545a73ae7eee",
      "--"
    ],
    "exit": 0,
    "stdout": "M\u0000src/labels.py\u0000",
    "stderr": ""
  },
  {
    "args": [
      "diff",
      "--no-ext-diff",
      "--no-textconv",
      "--no-renames",
      "--no-color",
      "--full-index",
      "--unified=3",
      "--diff-algorithm=myers",
      "--no-indent-heuristic",
      "--src-prefix=a/",
      "--dst-prefix=b/",
      "0e1899c60081fff643e71d270c11aafa8b1e485d...0b8da4cff382b93299301119dfce545a73ae7eee",
      "--"
    ],
    "exit": 0,
    "stdout": "diff --git a/src/labels.py b/src/labels.py\nindex 58639ef21d6f4a718712ec4cce15939e1b686366..83967ce91551959e78912d0ad447de5f0c295d74 100644\n--- a/src/labels.py\n+++ b/src/labels.py\n@@ -1,2 +1,4 @@\n def count_label(count):\n+    if count == 0:\n+        return \"\"\n     return f\"{count} items\"\n",
    "stderr": ""
  },
  {
    "args": [
      "bundle",
      "create",
      "/workspace/shared/codexapp-content-continuation-20261013/git-review-provenance/evidence/fixture.bundle",
      "--all"
    ],
    "exit": 0,
    "stdout": "",
    "stderr": ""
  },
  {
    "args": [
      "bundle",
      "verify",
      "/workspace/shared/codexapp-content-continuation-20261013/git-review-provenance/evidence/fixture.bundle"
    ],
    "exit": 0,
    "stdout": "The bundle contains these 3 refs:\ne693420956696987a95109124a98f34748233d76 refs/heads/feature\n00115dfcf209c7efeb498c0cda22e6fb765f8dd1 refs/heads/main\ne693420956696987a95109124a98f34748233d76 HEAD\nThe bundle records a complete history.\nThe bundle uses this hash algorithm: sha1\n",
    "stderr": "/workspace/shared/codexapp-content-continuation-20261013/git-review-provenance/evidence/fixture.bundle is okay\n"
  },
  {
    "args": [
      "--version"
    ],
    "exit": 0,
    "stdout": "git version 2.52.0\n",
    "stderr": ""
  }
]
独立临时仓库恢复与 CLI 回执
{
  "git": [
    {
      "args": [
        "init",
        "--quiet",
        "--bare",
        "--object-format=sha1",
        "--template="
      ],
      "exit": 0,
      "stdout": "",
      "stderr": ""
    },
    {
      "args": [
        "fetch",
        "--quiet",
        "/workspace/shared/codexapp-content-continuation-20261013/git-review-provenance/evidence/fixture.bundle",
        "refs/heads/*:refs/heads/*"
      ],
      "exit": 0,
      "stdout": "",
      "stderr": ""
    }
  ],
  "checks": [
    {
      "command": [
        "/opt/codex/runtimes/codex-primary-runtime/dependencies/python/bin/python3",
        "-E",
        "-B",
        "lab/git_provenance.py",
        "--repo",
        "/tmp/bundle-recheck-u3tdik59/repo",
        "--root",
        "evidence/snapshot",
        "--scope",
        "evidence/scope.json",
        "--report",
        "evidence/report.json",
        "--manifest",
        "evidence/manifest.json",
        "--main",
        "0e1899c60081fff643e71d270c11aafa8b1e485d",
        "--target",
        "0b8da4cff382b93299301119dfce545a73ae7eee"
      ],
      "exit": 0,
      "stdout": "{\"state\": \"provenance_verified\", \"base_commit\": \"6b3c22d3e3dcb2df13aa55e46ba2fbd24e6680b5\", \"target_commit\": \"0b8da4cff382b93299301119dfce545a73ae7eee\", \"blob_oid\": \"83967ce91551959e78912d0ad447de5f0c295d74\", \"snapshot_sha256\": \"aba0e362a97271155165d8513fc192137548e3eff82df2a17917caef53271189\", \"safety_proven\": false}\n",
      "stderr": ""
    },
    {
      "command": [
        "/opt/codex/runtimes/codex-primary-runtime/dependencies/python/bin/python3",
        "-E",
        "-B",
        "lab/review_guard.py",
        "--root",
        "evidence/snapshot",
        "--scope",
        "evidence/scope.json",
        "--report",
        "evidence/report.json"
      ],
      "exit": 0,
      "stdout": "{\"state\": \"validated\", \"finding_count\": 1, \"safety_proven\": false, \"note\": \"Structure only; evidence meaning and reproduction need review.\"}\n",
      "stderr": ""
    }
  ]
}

本次核对了补充图的 SVG 栅格渲染、HTML 静态结构、嵌入源码与文件的一致性,以及原有 12 节和文章其他字段的保留情况。完整网页未在浏览器中验收,CodexApp 的实际页面、移动端布局与折叠交互也未验证。原文的历史日期和版本记录保持原样,不能把本补充的 Git 实验理解为重新验证了那些 CLI 或产品行为。

没有 findings 就可以直接合并吗?

空结果只说明该次报告没列出问题。仍需核对范围、测试、依赖与配置变化,以及未验证项。

review 可以同时传 --base 和提示词吗?

本文核对的版本将 --uncommitted、--base、--commit 与自定义 PROMPT 设为互斥。选择一个目标模式,或在自定义模式完整说明范围。

校验器会找出漏洞吗?

不会。它检查报告结构、范围、文件哈希和引用位置,不理解影响,不执行复现,不发现漏洞,也不证明安全。

unknown 应怎样处理?

保留缺失证据的原因并安排后续核对。本文程序返回退出码 3,避免把未完成审查当成通过。