#!/usr/bin/env python3 # -*- coding: utf-8 -*- """门户单文件 · 契约生成的结论段 (用户批 2026-09-06: 门户正文切到契约生成). 门户浅色壳由 Codex 侧生成 (本库生成器是深色版, 不可重生成), 因此采用**就地幂等注入**: 在
正文末尾插入/替换
(由 derived/portal_claims.json 渲染). 用法: python scripts/guanlan_portal_inject_claims.py [--out ] ; 幂等: 再跑只替换同 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'
]*>.*?
', 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'{e(p["claim_id"])}{e(p["verdict"])}{e(p["text"])}{e(", ".join(p.get("systems") or []) or "—")}{e(str(p.get("window_days") or "—"))}' 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'
{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))})' + "".join(f'' for c in v) + '
claim裁决结论缺项
{e(c["claim_id"])}{e(c["verdict"])}{e(c["title"])}{e(c.get("missing") or "—")}
' 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'
  • {e(c["claim_id"])} {e(str(c.get("verdict_raw") or ""))[:40]} — {e(_t(c)[:110])}
  • ' for c in wd) return (f'
    本场结论 · 契约生成
    ' f'

    定论与准定论·预警 ({len(pc["paragraphs"])} 条)

    由结构化事实契约 {cs[:16]} 渲染 (共 {con.get("n_claims", "?")} 条); 数据、时间窗、结论只经契约更新, 本段禁手改 (release-manifest.content_contract)。

    ' f'
    {rows}
    claim裁决结论系统窗 (天)
    ' f'

    候选 · 参考 · 证据不足 ({len(rest)} 条, 按系统)

    {groups}' f'

    已撤回 ({len(wd)} 条)

      {withdrawn}
    ') 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'(
    ]*>)(.*?)(
    \s*