| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253 |
- #!/usr/bin/env python3
- # -*- coding: utf-8 -*-
- """门户单文件 · 契约生成的结论段 (用户批 2026-09-06: 门户正文切到契约生成). 门户浅色壳由 Codex 侧生成 (本库生成器是深色版, 不可重生成),
- 因此采用**就地幂等注入**: 在 <div class="pg" id="findings"> 正文末尾插入/替换 <section id="contract-claims" data-contract="…"> (由 derived/portal_claims.json 渲染).
- 用法: python scripts/guanlan_portal_inject_claims.py <portal.html> [--out <path>] ; 幂等: 再跑只替换同 id 段. 出: 新文件 sha256."""
- import hashlib, html, json, re, sys
- from pathlib import Path
- ROOT = Path(__file__).resolve().parents[1]; PC = ROOT / "outputs/rudong/guanlan/derived/portal_claims.json"
- MARK_RE = re.compile(r'<section id="contract-claims"[^>]*>.*?</section><!--/contract-claims-->', re.S)
- DC = ROOT / "outputs/rudong/guanlan/derived/detail_cards.json"; CONTRACT = ROOT / "outputs/rudong/guanlan/facts_contract_v0.json"
- def section(pc: dict) -> str:
- """契约正文段 (用户裁 2026-09-07 "门户正文全部切到契约生成"): ① 可交付级表 ② 候选/参考/INSUFFICIENT 按系统分组 (折叠) ③ 撤回清单 (带日期). 全部由契约派生物渲染, 禁手改."""
- cs = pc["contract_sha256"]; e = html.escape
- rows = "".join(f'<tr><td><code>{e(p["claim_id"])}</code></td><td>{e(p["verdict"])}</td><td>{e(p["text"])}</td><td>{e(", ".join(p.get("systems") or []) or "—")}</td><td>{e(str(p.get("window_days") or "—"))}</td></tr>' for p in pc["paragraphs"])
- cards = json.loads(DC.read_text(encoding="utf-8")) if DC.is_file() else {"cards": []}
- assert cards.get("contract_sha256", cs) == cs, "detail_cards 与 portal_claims 契约 sha 不一致"
- rest = [c for c in cards["cards"] if c["verdict"] not in ("定论", "准定论·预警")]
- by_sys = {}
- for c in rest: by_sys.setdefault(", ".join(c.get("systems") or []) or "跨系统/口径", []).append(c)
- groups = "".join(f'<details><summary>{e(k)} · {len(v)} 条 ({", ".join(f"{vv} {sum(1 for c in v if c["verdict"] == vv)}" for vv in ("候选", "参考", "INSUFFICIENT") if any(c["verdict"] == vv for c in v))})</summary><table><tr><th>claim</th><th>裁决</th><th>结论</th><th>缺项</th></tr>'
- + "".join(f'<tr><td><code>{e(c["claim_id"])}</code></td><td>{e(c["verdict"])}</td><td>{e(c["title"])}</td><td>{e(c.get("missing") or "—")}</td></tr>' for c in v) + '</table></details>' for k, v in sorted(by_sys.items(), key=lambda kv: -len(kv[1])))
- con = json.loads(CONTRACT.read_text(encoding="utf-8")) if CONTRACT.is_file() else {"claims": []}
- assert con.get("contract_sha256", cs) == cs, "契约文件与派生物 sha 不一致"
- wd = [c for c in con["claims"] if c["verdict"] == "撤回"]
- _t = lambda c: (c.get("display") or {}).get("title_zh_public") or c.get("title_public") or "" # 内部契约 / 云端脱敏面孔 (claims_public.json) 两种形态
- withdrawn = "".join(f'<li><code>{e(c["claim_id"])}</code> {e(str(c.get("verdict_raw") or ""))[:40]} — {e(_t(c)[:110])}</li>' for c in wd)
- return (f'<section id="contract-claims" data-contract="{cs}" data-version="{pc["contract_version"]}"><div class="wrap"><div class="kicker">本场结论 · 契约生成</div>'
- f'<h2>定论与准定论·预警 ({len(pc["paragraphs"])} 条)</h2><p class="sub">由结构化事实契约 {cs[:16]} 渲染 (共 {con.get("n_claims", "?")} 条); 数据、时间窗、结论只经契约更新, 本段禁手改 (release-manifest.content_contract)。</p>'
- f'<div style="overflow:auto"><table><tr><th>claim</th><th>裁决</th><th>结论</th><th>系统</th><th>窗 (天)</th></tr>{rows}</table></div>'
- f'<h2 style="margin-top:22px">候选 · 参考 · 证据不足 ({len(rest)} 条, 按系统)</h2>{groups}'
- f'<h2 style="margin-top:22px">已撤回 ({len(wd)} 条)</h2><ul>{withdrawn}</ul></div></section><!--/contract-claims-->')
- def inject(src: Path, out: Path):
- t = src.read_text(encoding="utf-8"); sec = section(json.loads(PC.read_text(encoding="utf-8")))
- if MARK_RE.search(t): t2 = MARK_RE.sub(lambda m: sec, t, count=1)
- else:
- m = re.search(r'(<div class="pg" id="findings"[^>]*>)(.*?)(</div>\s*<div class="pg" id=")', t, flags=re.S)
- assert m, "门户里找不到 findings 页块"
- t2 = t[:m.end(2)] + sec + t[m.end(2):]
- # newline= 必须给: Windows 上 write_text 会把整份门户的 \\n 变成 \\r\\n —— 20MB 文件整体改写,
- # portal_sha256 指纹随之变化 (2026-09-11 修)。门户行尾约定 = LF。
- out.write_text(t2, encoding="utf-8", newline=""); return hashlib.sha256(t2.encode("utf-8")).hexdigest()
- if __name__ == "__main__":
- src = Path(sys.argv[1]); out = Path(sys.argv[sys.argv.index("--out") + 1]) if "--out" in sys.argv else src
- print(inject(src, out))
|