Files
pp-qa-km/tools/selfcheck_diag_refs.py
LittleYellow 04f4fd5ead feat: 新增 diag_refs — source_ref↔manifest 對帳診斷
lint 的「source_ref 不在 manifest」只報對不上、不辨成因。diag_refs 把每個
對不上的 source_ref 分成五類(hash 不符/路徑字串岔開/檔名 hash 後綴岔開/
檔在磁碟未登記/完全無對應),各附修法,讓「補帳本(§14.3)」與「修規則
(§12)」的相反處置不再混為一談。純唯讀、結束碼 0/1,可當搬機或改 manifest
後的閘門。附 selfcheck 驗五型別分類、結束碼與零寫檔。README §3.3/§6/§7/§8 同步。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 23:45:23 +08:00

115 lines
4.8 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Phase 4 自檢:沙盒植入五種 source_ref 岔開型別hash 不符、路徑正規化、
檔名 hash 後綴岔開、檔在磁碟未登記、完全無對應)+一個正常解得開的 ref
驗證 diag_refs 逐一分類正確、報告涵蓋各型別、結束碼 1、且不修改任何檔案。
另驗全部解得開時結束碼 0。
執行:.venv/Scripts/python tools/selfcheck_diag_refs.py
"""
import io
import json
import pathlib
import shutil
import sys
import tempfile
from contextlib import redirect_stdout
sys.path.insert(0, str(pathlib.Path(__file__).parent))
import kb
import diag_refs
SHA_A = "abcd1234" + "0" * 56
SHA_B = "beef5678" + "0" * 56
# 帳本 key 一律 posix 正斜線+中文/連字號檔名(真實 convert 產出的形狀)
K_OK = "raw/converted/www-pluspay-faq-06b02fd9.md"
K_STEM = "raw/converted/www-pluspay-terms-11112222.md" # 同幹、不同 hash 後綴
FILES = {
K_OK: {"kind": "converted", "sha256": SHA_A, "original_path": "raw/originals/faq.html",
"original_sha256": SHA_A, "converter": "from_web", "converted_at": "2026-07-01T00:00:00"},
K_STEM: {"kind": "converted", "sha256": SHA_B, "original_path": "raw/originals/terms.html",
"original_sha256": SHA_B, "converter": "from_web", "converted_at": "2026-07-01T00:00:00"},
}
# ref → 預期分類。unregistered 的檔會實際落地absent 的不落地。
CASES = {
"resolved": f"{K_OK}#abcd1234",
"hash-mismatch": f"{K_OK}#deadbeef",
"normalize": r"raw\converted\www-pluspay-faq-06b02fd9.md#abcd1234", # 反斜線
"stem": "raw/converted/www-pluspay-terms-99998888.md#cccccccc", # 同幹、異後綴、非 key
"unregistered": "raw/converted/www-pluspay-instructions-77776666.md#77777777",
"absent": "raw/converted/www-pluspay-ghost-00001111.md#cafebabe",
}
def unit():
"""classify_ref 純函式逐型別驗證。"""
on_disk = {"raw/converted/www-pluspay-instructions-77776666.md"}
exists = lambda p: p in on_disk
for expect, ref in CASES.items():
cat, detail = diag_refs.classify_ref(ref, FILES, exists)
assert cat == expect, f"{ref!r} 應分類為 {expect},實得 {cat}{detail}"
print("PASS: classify_ref 六型別(含 resolved分類正確")
def build(root):
for sub in ("raw/converted", "wiki/summaries", "wiki/entities", "wiki/concepts"):
(root / sub).mkdir(parents=True)
(root / "raw/manifest.json").write_text(
json.dumps({"version": 1, "files": FILES}), encoding="utf-8")
# unregistered 的檔要真的存在於磁碟,才會被判為「檔在、帳本無」而非 absent
(root / "raw/converted/www-pluspay-instructions-77776666.md").write_text(
"# 未登記\n", encoding="utf-8")
today = "2026-07-01"
for i, (name, ref) in enumerate(CASES.items()):
meta = {"type": "entity", "title": f"{name}", "description": "測試頁",
"tags": ["t"], "timestamp": today, "sources": [ref], "status": "draft"}
(root / f"wiki/entities/p{i}.md").write_text(kb.dump_page(meta, "內文。"), encoding="utf-8")
def run(argv):
buf = io.StringIO()
code = 0
try:
with redirect_stdout(buf):
diag_refs.main(argv)
except SystemExit as e:
code = e.code
return code, buf.getvalue()
def main():
unit()
tmp = pathlib.Path(tempfile.mkdtemp(prefix="ppqa-diag-"))
try:
root = tmp / "repo"
build(root)
before = {p: p.read_bytes() for p in root.rglob("*")
if p.is_file()}
code, out = run(["--root", str(root)])
assert code == 1, f"有岔開應以結束碼 1實得 {code}\n{out}"
for cat in ("hash-mismatch", "normalize", "stem", "unregistered", "absent"):
assert diag_refs.CAT_TITLE[cat].split("")[0] in out, f"報告缺型別 {cat}\n{out}"
assert "對不上5" in out, out
after = {p: p.read_bytes() for p in root.rglob("*") if p.is_file()}
assert before == after, "diag_refs 修改了檔案!"
print("PASS: 整合——五型別全報、結束碼 1、零寫檔")
# 全部解得開 → 結束碼 0
good = {"type": "entity", "title": "好頁", "description": "d", "tags": ["t"],
"timestamp": "2026-07-01", "sources": [f"{K_OK}#abcd1234"], "status": "draft"}
for p in (root / "wiki/entities").glob("*.md"):
p.unlink()
(root / "wiki/entities/ok.md").write_text(kb.dump_page(good, "內文。"), encoding="utf-8")
code, out = run(["--root", str(root)])
assert code == 0, f"全部解得開應以結束碼 0實得 {code}\n{out}"
assert "全部解得開" in out, out
print("PASS: 全部解得開時結束碼 0")
print("ALL PASS")
finally:
shutil.rmtree(tmp, ignore_errors=True)
if __name__ == "__main__":
main()