AI重写代码前应先冻结副作用记录(stdout、sidecar文件、累加器),再小步提取格式化工具,防止隐藏bug被掩盖。
模型可以在几秒内重写一个凌乱的模块。但这种额外的速度并不是合并的信号。先冻结 stdout、sidecar 文件和可变累加器,再提取一个 formatter。
在这些可观测对象被固定之后,再提取一个 formatter。把其余未测试的树保持原样。
这个工作流是一道关卡,不是氛围检查。它适用于将 I/O 与字符串构建混在一起的未测试 Python 代码。它不适用于新设计或 broad API 重构。
未测试的树隐藏着顺序、编码和变更bug。Formatter 的变更可能改变 JSON key 的顺序。Helper 提取可能跳过必需的 sidecar 写入。
排序清理可能打乱稳定的发票标识符顺序。AI 辅助的 diff 使这种失败变得更廉价,从而更容易被忽略。
生成的补丁通常看起来流畅且完整。表征测试通常还不存在。审阅者于是批准了语气而非精确的字节。
对策是一份记录好的 side-effect ledger。记录当前代码已经发出的每个可观测对象。在恰好一个小变更之后断言该 ledger。
当前的 AI 编码 thread 奖励看起来自信的输出。它们很少奖励冻结的字节而非流畅的文章。本文将那种自信视为不可信的输入。
将以下模块视为 fixture,而非生产历史。它混合了解析、变更、打印和 sidecar 文件。这种混合才是真正的 bug 表面。
# invoice_summary.py
from __future__ import annotations
import json
from pathlib import Path
from typing import Any
TOTALS: dict[str, float] = {}
def run(data_dir: str, out_dir: str) -> dict[str, Any]:
root = Path(data_dir)
dest = Path(out_dir)
dest.mkdir(parents=True, exist_ok=True)
rows: list[dict[str, Any]] = []
for path in root.glob("*.json"):
payload = json.loads(path.read_text(encoding="utf-8"))
code = str(payload.get("code") or path.stem)
amount = float(payload.get("amount") or 0)
TOTALS[code] = TOTALS.get(code, 0.0) + amount
rows.append({"code": code, "amount": amount, "file": path.name})
rows.sort(key=lambda r: (r["code"], r["file"]))
lines = []
for row in rows:
lines.append(f"{row['code']}:{row['amount']:.2f}")
text = "\n".join(lines) + "\n"
print(text, end="")
(dest / "summary.txt").write_text(text, encoding="utf-8")
(dest / "summary.ok").write_bytes(b"ok\n")
return {"count": len(rows), "codes": [r["code"] for r in rows]}
注意模块作用域中进程级别的 TOTALS 映射。注意 glob 顺序馈入后续的显式排序。注意 print 输出加两个磁盘 artifact。
模型会想要一次性简化所有这些。拒绝任何在第一轮就那么宽的补丁。下一个 diff 只涉及一个纯 helper。
构建一个冻结四个独立可观测对象的 ledger。不要 mock Path,也不要 stub print。使用临时目录并捕获 stdout。
# test_invoice_summary_ledger.py
from __future__ import annotations
import io
import json
import sys
from pathlib import Path
import invoice_summary as mod
def write_fixture(root: Path) -> None:
(root / "b.json").write_text(
json.dumps({"code": "B-9", "amount": 10}), encoding="utf-8"
)
(root / "a.json").write_text(
json.dumps({"code": "A-1", "amount": 2.5}), encoding="utf-8"
)
(root / "a2.json").write_text(
json.dumps({"code": "A-1", "amount": 0.5}), encoding="utf-8"
)
def capture_run(data: Path, out: Path) -> tuple[dict, str]:
buf = io.StringIO()
old = sys.stdout
sys.stdout = buf
try:
result = mod.run(str(data), str(out))
finally:
sys.stdout = old
return result, buf.getvalue()
def test_side_effect_ledger(tmp_path: Path) -> None:
mod.TOTALS.clear()
data = tmp_path / "in"
out = tmp_path / "out"
data.mkdir()
write_fixture(data)
result, stdout = capture_run(data, out)
ledger = {
"stdout": stdout,
"summary": (out / "summary.txt").read_text(encoding="utf-8"),
"ok_bytes": (out / "summary.ok").read_bytes(),
"totals": dict(mod.TOTALS),
"result": result,
}
assert ledger["stdout"] == "A-1:2.50\nA-1:0.50\nB-9:10.00\n"
assert ledger["summary"] == ledger["stdout"]
assert ledger["ok_bytes"] == b"ok\n"
assert ledger["totals"] == {"A-1": 3.0, "B-9": 10.0}
assert ledger["result"] == {
"count": 3,
"codes": ["A-1", "A-1", "B-9"],
}
在任何提取尝试开始之前运行该 ledger。
python -m pytest test_invoice_summary_ledger.py -q
该 ledger 是此文件的合并契约。重复的 code 在行列表中保持重复。Totals 仍然合并而 sidecar 字节保持 ASCII。
Formatter 提取不能"修复"那些行为。重复的发票行是当前的产品行为。静默清理将是一次未审阅的语义变更。
将此表用作变更预算。在发送模型 prompt 之前阅读每一行。
一个允许变更的行就是全部预算。两个 helper 提取属于后续 pull request。注释只允许放在新函数旁边。
按照严格顺序遵循以下七个步骤。
首先,将凌乱的文件复制到一个专用分支上。
用微小的显式 fixture 添加 ledger 测试。
运行 pytest 并在当前字节上确认绿色 baseline。
编写一个禁止额外编辑的单提取 prompt。
将模型 diff 应用到第二个工作树上。
重新运行 ledger 并拒绝 stdout 或 sidecar 漂移。
只合并 helper 加 ledger 测试。
第四步是免费 coding 模型可以帮忙的地方。它不能替代第三步或第六步。本地 pytest 仍然是唯一的合并 oracle。
将此 prompt 标记为暂未执行的指导。仅在 ledger 变绿之后粘贴。
Extract a pure function format_rows(rows) -> str from run().
Do not rename run.
Do not clear TOTALS.
Do not change print, glob, or sidecar writes.
Do not dedupe codes.
Keep the sort key (code, file).
Keep two-decimal amounts and a trailing newline.
Return a unified diff for invoice_summary.py only.
该 prompt 是变更范围周围的栅栏。模型会在没有显式栅栏的情况下扩展清理。如果 diff 也编辑了测试文件,则丢弃该补丁。
表征 loop 需要一个一次性的提议环境。本地 pytest 应该留在你自己的机器上。模型只负责提议 formatter 提取。
披露:本文是 MonkeyCode 产品推广的一部分。
MonkeyCode 提供免费模型访问和免费服务器选项。将这对用于提议步骤,而非 ledger 执行。将 secrets 远离该服务器并保持 fixture 是合成的。
如果 diff 编辑了超过 format_rows 的内容,则丢弃该补丁。用相同的栅栏重新 prompt,不涉及额外文件。不要因为推理是免费的就接受宽泛清理。
如果你尝试免费模型访问,将 ledger 保持为合并关卡。不要将免费服务器当作测试运行器的替代品。
最小的安全变更如下所示。任何更大的变更都是不同的任务和不同的审阅。
def format_rows(rows: list[dict[str, Any]]) -> str:
lines = [f"{row['code']}:{row['amount']:.2f}" for row in rows]
return "\n".join(lines) + "\n"
def run(data_dir: str, out_dir: str) -> dict[str, Any]:
root = Path(data_dir)
dest = Path(out_dir)
dest.mkdir(parents=True, exist_ok=True)
rows: list[dict[str, Any]] = []
for path in root.glob("*.json"):
payload = json.loads(path.read_text(encoding="utf-8"))
code = str(payload.get("code") or path.stem)
amount = float(payload.get("amount") or 0)
TOTALS[code] = TOTALS.get(code, 0.0) + amount
rows.append({"code": code, "amount": amount, "file": path.name})
rows.sort(key=lambda r: (r["code"], r["file"]))
text = format_rows(rows)
print(text, end="")
(dest / "summary.txt").write_text(text, encoding="utf-8")
(dest / "summary.ok").write_bytes(b"ok\n")
return {"count": len(rows), "codes": [r["code"] for r in rows]}
在补丁干净应用后检查三个事实。format_rows 必须保持为没有 I/O 的纯函数。run 必须仍然变更 TOTALS 并写入两个文件。
立即在打了补丁的树上重新运行 ledger。匹配 stdout 在提取后不是可选项。匹配 sidecar 字节在这里也不是可选项。
这些失败在模型 diff 中经常出现。在重试之前将每一个映射到 ledger 断言。
去重的 codes:两个 A-1 行必须留在 stdout 中。
浮点漂移:金额保持两位小数,包括 10.00。
换行漂移:连接的文本保持尾部换行符。
Sidecar 漂移:summary.ok 保持恰好 ok 加换行符。
全局清理:TOTALS 不能在提取内部重置。
排序漂移:顺序保持按 (code, file),而非 glob 遍历。
每个失败都是字节不匹配,而非风格争论。在更新栅栏后用更严格的 prompt 重试。不要放松 ledger 来让模型通过。
此方法根本不证明功能正确性。它只证明当前字节保持不变。Fixture 中错误的 totals 在提取后仍然是错误的。
Ledger 不固定挂钟时间。它不固定排序前的 glob 顺序。它不固定源 fixture 内部的 JSON key 顺序。
不要将此用于安全审阅。不要将此用于 TOTALS 上的并发写入。不要将绿色 ledger 视为重写包的许可证。
网络化发票源不在此范围内。数据库事务和多进程累加器也是如此。这些需要不同于 sidecar 文件的 oracle。
如果模块已经有真实的契约测试,则跳过此方法。如果变更是语义修复而非提取,则跳过。如果 sidecar 是活锁文件,则跳过。
如果你不能在本地树上运行 pytest,则跳过。远程模型不是你的测试运行器。如果生产 secrets 不能被降级为 fixture,则跳过。
对于无法廉价快照的二进制格式,跳过。当法律输出必须在同一补丁中变更时跳过。只有在新测试下才能将语义修复与提取结合。
从 ledger 开始,然后提取一个纯函数。在任何合并之前重新运行四个可观测对象。那个序列就是此文件的全部方法。
免费模型可以起草 helper 文本。Ledger 仍然决定 diff 是否可以落地。当可以的时候,将提议和 oracle 放在不同的机器上。