Codex 改完代码怎么审
先找回同一份 diff,再核对每条结论为何成立。
- 记录基点和目标快照,让报告能复查
- finding 包含短行号跨度、引文和复现条件
- 机械校验通过与代码安全分别判断
- 修复后重查 diff 和测试,再决定合并发布
1. 从能复查的差异开始
Codex 改完代码后,我会先确认报告对应哪一份文件。分支又推进了一次,编辑器里还留着未保存的修改,或者测试跑在另一个目录,都可能让一句通过失去意义。审查时保留基点、目标版本和实际测试记录,后面讨论问题才有共同的依据。
这篇沿着一次标签显示改动讲清检查顺序,并附一个 Python 标准库实现的离线报告校验器。它会检查报告中的文件、行号和引文是否对得上冻结快照。发现漏洞、证明复现成立、决定严重度和批准发布,仍需另外完成。文中的四级标签、报告结构和验收流程由本文设计。
本文于 2026 年 10 月 5 日核对官方资料,在 Linux 执行了 codex-cli 0.159.2 的版本、帮助和参数冲突检查。没有调用真实模型、读取密钥或进行漏洞利用。七张图为原创示意图;网页交互与真实审查界面未纳入本次 GUI 验证。
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...HEADGit 的三点比较使用共同祖先作为一端,区别于直接比较两个端点。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对状态中的新增文件,单独打开检查。关注临时日志、生成产物、配置副本是否被意外加入。空白检查通过只说明没有发现该类格式问题。代码审查结束后再记录一次状态;若期间出现新修改,确认报告还能覆盖当前内容。
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,实际显示为空。这个例子只涉及显示行为;预期成立还依赖产品约定。如果调用方本来就要隐藏零数量,报告可能误判,复核时需要把上下文补上。
--- 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 和缺失原因。
判断误报时保留理由,便于下次审查复用。例如某个输入已被调用方限制,应引用限制发生的位置,并确认所有入口都会经过它。无法稳定复现的现象可以继续调查,但不要为了让报告完整而补写不存在的观察记录。
去重同样值得做。多个说法可能指向同一条件分支,应合并为一条能处理的记录。本文程序拒绝相同标识,以及相同文件、跨度和引文的重复记录;语义相近但引文不同的问题仍靠人工合并。一次空 findings 只表示这次报告没列出问题,不能用于宣布代码安全。
7. 测试证据要对应最终文件版本
先读项目已有的测试说明、构建配置和相关测试,再选择命令。记录工作目录、命令、退出码和必要输出。单元测试覆盖局部分支,集成测试检查组件连接,界面问题还需要观察真实交互;各自的覆盖边界要写清楚。
遇到失败,区分原有失败、本次回归和环境阻塞。缺少服务或依赖时,把未运行项留下,别用另一条较窄的命令冒充完整验收。修复后保留同一输入的前后对比,再运行受影响的回归测试。测试新增了快照或锁文件,也要把这些变化纳入最终 diff。
依赖和配置常常藏在实现旁边。核对包版本及锁文件是否一致,检查安装脚本、构建插件、CI 工作流、网络地址和权限变化。自动生成的大文件可以分段看,不能因为篇幅长就整段跳过。升级依赖的必要性、兼容性测试和回滚方法应随修改一起交接。
8. 仓库内容、秘密与审批都要有边界
陌生仓库的说明、注释、Issue 摘录和日志可能含有指令式文字。让代理忽略审批、上传环境变量或执行额外脚本的内容,应按待审数据处理。AGENTS.md 能承载项目约定,但其中写一个禁止目录,不能替代工具层面的访问限制。
先阅读依赖脚本,再决定是否运行;把审查安排在权限受控的副本中,减少能接触的秘密。日志只保留复现所需片段,去掉令牌、身份信息和线上载荷。发现凭据泄露时停止扩散,记录脱敏位置并按团队流程处置,不把完整秘密粘贴到公开审查评论。
沙箱约束文件与网络等能力,审批处理需要进一步判断的动作。代码审查与动作审批中的自动审查承担不同工作。官方沙箱说明指出自动审批审查不会改变沙箱边界;安全指南也提醒容器内可见的凭据仍可能面临风险。不要为了跑通审查关闭所有保护,遇到拒绝应缩小任务或补齐必要授权。
9. 用离线程序检查报告是否自洽
review_guard.py 读取受信 scope、冻结快照和待审报告。scope 保存基点标识、精确文件白名单、文件哈希和新增侧变化行。它必须由宿主或人工核对后提供,不能把模型生成的范围当成自己的验证依据。fixture 中全为 a 的标识是合成值,不对应真实 Git 提交。
校验器拒绝越界路径、符号链接、超出文件的行号、未覆盖变化行的跨度、空引文、未知字段、重复记录和不完整复现。引文必须出现在给定跨度内,但程序不会理解引文含义,也不会执行复现步骤。severity 只检查标签是否合法,不判断等级是否合理。
这个版本面向受信、静止的小型文本快照。它不解析任意 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 生成器完整源码
"""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")展开 报告校验器完整源码
"""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 个测试的完整源码
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 准备最小修复和相关测试。先说明允许变更的文件、要保留的行为和完成标准;若修复引入新依赖或扩大权限,重新讨论范围。自动修复产生了新版本,原审查结论不能自动覆盖它。
修复也可能只是改变了测试断言,让旧错误看上去通过。因此要回到最初的预期行为,检查断言是否仍能发现原来的问题。若产品决定接受新的行为,记录这次决定和适用范围,再调整测试,后续维护者才知道变化的原因。
最终交接保留基点与目标快照、已处理问题、实际测试和剩余限制。合并、发布、生产操作或权限调整由获得相应授权的人决定。需要继续验证的项目写清下一步条件,避免一句已完成掩盖尚未运行的集成测试。
我更愿意收到一份短而具体的报告:哪一行有问题,什么输入会触发,怎样确认,改后验证了什么。有了这些证据,团队才能复查结论、安排处理顺序,并在版本变化后知道哪些检查需要重做。
12. 来源与验证记录
本文由 pan 撰写,命令接口以本机 0.159.2 帮助为证,并对照下列官方资料。网页会随产品更新,实际使用时以安装版本和生效策略为准。流程、图示、JSON 契约和 Python 实现均为本文教学设计。
- OpenAI:Developer commands,review 参数、互斥条件及非交互入口
- OpenAI:Code review,审查范围、报告标准与修复权限
- OpenAI:Sandbox,沙箱和审批边界
- OpenAI:Agent approvals & security,不可信输入与环境隔离
- Git:git-diff 与 git-status,差异端点与工作树状态
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 对象格式。
这个例子刻意只支持一个普通 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,避免把未完成审查当成通过。