116 lines
5.3 KiB
Python
116 lines
5.3 KiB
Python
"""Phase 4 自檢:沙盒植入各類違規(schema 缺欄位、壞 source_ref、過期頁、
|
||
攝入缺口、needs_ocr、孤兒頁、相似頁對),驗證 lint 全部抓到、產出報告、
|
||
且不修改任何檔案。LLM 比對以假 LLM 驗證矛盾偵測路徑。
|
||
|
||
執行:.venv/Scripts/python tools/selfcheck_lint.py
|
||
"""
|
||
import json
|
||
import pathlib
|
||
import shutil
|
||
import sys
|
||
import tempfile
|
||
|
||
sys.path.insert(0, str(pathlib.Path(__file__).parent))
|
||
import kb
|
||
import lint
|
||
|
||
CFG = """ollama: {base_url: "http://localhost:11434", timeout_seconds: 5}
|
||
tasks:
|
||
ingest: {model: "fake:test", temperature: 0.2, max_retries: 2}
|
||
lint: {model: "fake:test", temperature: 0.1, max_retries: 2}
|
||
fallback: {model: "fake:fb"}
|
||
limits: {mcp_response_max_tokens: 2000, ingest_chunk_max_chars: 12000}
|
||
"""
|
||
SHA_A = "abcd1234" + "0" * 56
|
||
SHA_B = "beef5678" + "0" * 56
|
||
|
||
|
||
def page(root, rel, meta, body="內文。"):
|
||
p = root / rel
|
||
p.write_text(kb.dump_page(meta, body), encoding="utf-8")
|
||
|
||
|
||
def build(root):
|
||
import datetime
|
||
today = datetime.date.today().isoformat()
|
||
for sub in ("config", "raw/converted", "wiki/summaries", "wiki/entities", "wiki/concepts"):
|
||
(root / sub).mkdir(parents=True)
|
||
(root / "config/models.yaml").write_text(CFG, encoding="utf-8")
|
||
(root / "raw/manifest.json").write_text(json.dumps({"version": 1, "files": {
|
||
"raw/converted/good-src.md": {"kind": "converted", "sha256": SHA_A,
|
||
"original_path": "raw/originals/g.docx",
|
||
"original_sha256": SHA_A, "converter": "from_docx",
|
||
"converted_at": "2026-07-01T00:00:00"},
|
||
"raw/converted/uncovered.md": {"kind": "converted", "sha256": SHA_B,
|
||
"original_path": "raw/originals/u.docx",
|
||
"original_sha256": SHA_B, "converter": "from_docx",
|
||
"converted_at": "2026-07-01T00:00:00"},
|
||
"raw/originals/scan.pdf": {"kind": "original", "sha256": SHA_B,
|
||
"media_type": "pdf", "added_at": "2026-07-01T00:00:00",
|
||
"status": "needs_ocr"},
|
||
}}), encoding="utf-8")
|
||
ref = f"raw/converted/good-src.md#{SHA_A[:8]}"
|
||
good = {"type": "entity", "title": "支付閘道", "description": "支付閘道模組",
|
||
"tags": ["gateway"], "timestamp": today, "sources": [ref], "status": "draft"}
|
||
page(root, "wiki/entities/good.md", good)
|
||
bad = {"type": "entity", "title": "壞頁", "description": "缺 tags",
|
||
"timestamp": today, "sources": ["raw/converted/ghost.md#deadbeef"],
|
||
"status": "draft", "type": "entity"}
|
||
page(root, "wiki/entities/bad-schema.md", bad)
|
||
old = dict(good, title="舊概念", type="concept", timestamp="2024-01-01")
|
||
page(root, "wiki/concepts/old.md", old)
|
||
dup_a = dict(good, title="支付閘道逾時缺陷", tags=["gateway", "timeout"],
|
||
description="逾時缺陷模式")
|
||
dup_b = dict(good, title="支付閘道逾時缺陷模式", tags=["gateway", "timeout"],
|
||
description="逾時的缺陷模式")
|
||
page(root, "wiki/entities/dup-a.md", dup_a, "重試機制在峰值失效。")
|
||
page(root, "wiki/entities/dup-b.md", dup_b, "重試機制在峰值一律生效。")
|
||
(root / "index.md").write_text(
|
||
"- [支付閘道](wiki/entities/good.md)\n- [舊概念](wiki/concepts/old.md)\n"
|
||
"- [A](wiki/entities/dup-a.md)\n- [B](wiki/entities/dup-b.md)\n", encoding="utf-8")
|
||
|
||
|
||
def run_lint(args):
|
||
try:
|
||
lint.main(args)
|
||
return 0
|
||
except SystemExit as e:
|
||
return e.code
|
||
|
||
|
||
def main():
|
||
tmp = pathlib.Path(tempfile.mkdtemp(prefix="ppqa-lint-"))
|
||
real = kb.llm_json
|
||
try:
|
||
root = tmp / "repo"
|
||
build(root)
|
||
before = {p: p.read_bytes() for p in root.rglob("*.md")}
|
||
|
||
code = run_lint(["--no-llm", "--root", str(root)])
|
||
assert code == 1, code
|
||
report = next((root / "reports").glob("lint-*.md")).read_text(encoding="utf-8")
|
||
for expected in ("缺欄位 tags", "source_ref 不在 manifest",
|
||
"old.md", "建議人工複審", "uncovered.md", "攝入缺口",
|
||
"needs_ocr", "bad-schema.md` — 不被 index.md"):
|
||
assert expected in report, f"報告缺少:{expected}\n{report}"
|
||
assert "good.md" not in report
|
||
after = {p: p.read_bytes() for p in root.rglob("*.md") if "reports" not in str(p)}
|
||
assert all(before[p] == after[p] for p in after), "lint 修改了檔案!"
|
||
print("PASS: schema/stale/coverage/orphan 全抓到,報告產出,檔案零修改")
|
||
|
||
kb.llm_json = lambda cfg, task, prompt, validate: (
|
||
{"relation": "contradiction", "explanation": "重試機制生效與否說法相反"}, "fake:test")
|
||
code = run_lint(["--root", str(root)])
|
||
assert code == 1
|
||
report = sorted((root / "reports").glob("lint-*.md"))[-1].read_text(encoding="utf-8")
|
||
assert "疑似矛盾" in report and "dup-a.md ↔ wiki/entities/dup-b.md" in report, report
|
||
print("PASS: LLM 矛盾頁偵測(Phase 6 成功標準的機制驗證)")
|
||
print("ALL PASS")
|
||
finally:
|
||
kb.llm_json = real
|
||
shutil.rmtree(tmp, ignore_errors=True)
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|