feat: convert --verify 對帳 + 修復 CRLF 使 hash 失準 + raw/** .gitattributes (#1)
## 目的 `convert` 後檔案被手動刪除/就地改動時,提供正規的稽核與修復途徑;並修掉一個讓「還原後 re-hash 比對」在 Windows 失效的既有 bug。 ## 變更 - **`convert.py --verify`(純唯讀對帳)**:re-hash 全部登記檔、掃未登記檔,回報 missing / mismatch / unregistered,不一致以退出碼 1 表示(可當 CI/排程閘門)。`converted/assets/*` 與 `.gitkeep` 正確略過。 - **修 CRLF 使 hash 失準**:`write_text` 在 Windows 把 `\n`→`\r\n`,但登記的 SHA-256 算在 `\n` bytes 上 → 磁碟 bytes 與帳本永遠對不上(converted md + web 快照原始檔)。改為 `write_bytes` 寫入所登記的那份 bytes。 - **`.gitattributes`(`raw/** -text`)**:`core.autocrlf=true` 下 checkout 會在 git 層重新引入 CRLF,抵銷上一項修復;關閉 raw 的換行正規化,把「登記 hash == 磁碟 bytes」不變式延伸到 git checkin/checkout。 - **selfcheck**:補 clean / missing / mismatch / unregistered 四情境(全程唯讀斷言;clean 案例含未入帳本的 assets,順帶證明不誤報)。 - **AGENTS.md**:新增 §14「raw 完整性與修復(對帳)」,並於 §1.4 補「登記 hash == 磁碟 bytes」不變式與 `.gitattributes` 機制。 ## 驗證 `.venv/Scripts/python.exe tools/convert/selfcheck.py` → `ALL PASS`。 ## 備註 `--verify` 是唯讀稽核工具,不改任何檔(含 manifest),符合 §7「lint 絕不擅自刪改」精神。修復決策(可衍生 vs 信任根、先 `git restore` 不改帳本、動 manifest 走 PR)詳見 AGENTS.md §14。 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: LittleYellow <crazytea@gmail.com> Reviewed-on: #1
This commit was merged in pull request #1.
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