feat: convert 新增 --verify 對帳模式 + 修復 Windows CRLF 使 hash 失準
- convert.py --verify:純唯讀 re-hash 全部登記檔並掃未登記檔, 報 missing/mismatch/unregistered,不一致退出碼 1(供刪除/竄改後稽核) - 修 write_text 在 Windows 轉 \n→\r\n 使磁碟 bytes 與登記 SHA-256 不符 (converted md 與 web 快照原始檔),改 write_bytes 寫入所登記的那份 bytes - selfcheck 補 clean/missing/mismatch/unregistered 四情境(全程唯讀斷言) - AGENTS.md §14 raw 完整性與修復流程 + §1.4 hash==磁碟 bytes 不變式 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -158,6 +158,45 @@ def main():
|
||||
assert (root / "raw/manifest.json").read_text(encoding="utf-8") == before
|
||||
print("PASS: 冪等(同 hash 跳過)")
|
||||
|
||||
# --verify 對帳(純唯讀):clean 通過、三類意外都抓到、且完全不改 manifest。
|
||||
# root 已有 docx/pdf/pptx 抽出的 assets(不入帳本)——clean 案例即證明 assets 不被誤報。
|
||||
def run_verify(rt):
|
||||
mm = json.loads((rt / "raw/manifest.json").read_text(encoding="utf-8"))
|
||||
err, code = io.StringIO(), 0
|
||||
with contextlib.redirect_stderr(err), contextlib.redirect_stdout(io.StringIO()):
|
||||
try:
|
||||
convert.verify(rt, mm)
|
||||
except SystemExit as e:
|
||||
code = e.code
|
||||
return code, err.getvalue()
|
||||
|
||||
before = (root / "raw/manifest.json").read_text(encoding="utf-8")
|
||||
code, _ = run_verify(root)
|
||||
assert code == 0, "clean 帳本應通過對帳(含未入帳本的 assets)"
|
||||
|
||||
rroot = tmp / "root_del" # missing:刪掉一個 converted .md
|
||||
shutil.copytree(root, rroot)
|
||||
gone = next(p for p in (rroot / "raw/converted").glob("*.md"))
|
||||
gone.unlink()
|
||||
code, log = run_verify(rroot)
|
||||
assert code == 1 and "missing" in log and gone.name in log, log
|
||||
|
||||
rroot = tmp / "root_mut" # mismatch:就地改動一個 original 的 bytes
|
||||
shutil.copytree(root, rroot)
|
||||
tampered = next(p for p in (rroot / "raw/originals").iterdir() if p.is_file())
|
||||
tampered.write_bytes(tampered.read_bytes() + b"tampered")
|
||||
code, log = run_verify(rroot)
|
||||
assert code == 1 and "mismatch" in log, log
|
||||
|
||||
rroot = tmp / "root_unreg" # unregistered:raw/originals 塞入未登記檔
|
||||
shutil.copytree(root, rroot)
|
||||
(rroot / "raw/originals/未登記 檔.docx").write_bytes(b"stray")
|
||||
code, log = run_verify(rroot)
|
||||
assert code == 1 and "unregistered" in log and "未登記 檔.docx" in log, log
|
||||
|
||||
assert (root / "raw/manifest.json").read_text(encoding="utf-8") == before # 全程唯讀
|
||||
print("PASS: --verify 對帳(clean/missing/mismatch/unregistered,純唯讀)")
|
||||
|
||||
# dry-run:全新沙盒不落地
|
||||
root2 = tmp / "root2"
|
||||
for sub in ("raw/originals", "raw/converted"):
|
||||
|
||||
Reference in New Issue
Block a user