← Files LegalQuants TransactionalARCHIVED FILE

skills/diligence/scripts/render_crosswalk.py

39.8 KB · Oct 3, 2026 · 06:34 UTC

↓ Download file

#!/usr/bin/env python3
# ruff: noqa: E501 -- self-contained HTML, CSS, and JavaScript literals stay readable.
"""Render the Gate 3 factual issue/agreement crosswalk.

The renderer consumes only the frozen framework, checked findings ledger,
manifest, confirmed family map, and gap report. It refuses an incomplete
issue × substantive-unit matrix through reconcile_counts.py. The output is a
self-contained, deterministic HTML review surface plus a JSON receipt.

Usage:
    python3 render_crosswalk.py --framework framework.json \
        --findings findings.json --manifest manifest.json \
        --families families.confirmed.json --gaps gap-report.json \
        [--source-prefix ../room] --out crosswalk.html \
        --receipt crosswalk-receipt.json
"""

from __future__ import annotations

import argparse
import hashlib
import html
import importlib.util
import json
import posixpath
import sys
from collections import Counter, defaultdict
from pathlib import Path
from typing import Any

_RECONCILE_SPEC = importlib.util.spec_from_file_location(
    "diligence_reconcile_counts", Path(__file__).with_name("reconcile_counts.py")
)
if _RECONCILE_SPEC is None or _RECONCILE_SPEC.loader is None:
    raise RuntimeError("cannot load sibling reconcile_counts.py")
reconcile_counts = importlib.util.module_from_spec(_RECONCILE_SPEC)
_RECONCILE_SPEC.loader.exec_module(reconcile_counts)

_REVIEW_UI_SPEC = importlib.util.spec_from_file_location(
    "diligence_review_ui", Path(__file__).with_name("review_ui.py")
)
if _REVIEW_UI_SPEC is None or _REVIEW_UI_SPEC.loader is None:
    raise RuntimeError("cannot load sibling review_ui.py")
review_ui = importlib.util.module_from_spec(_REVIEW_UI_SPEC)
_REVIEW_UI_SPEC.loader.exec_module(review_ui)
BRAND_CSS = review_ui.BRAND_CSS
CONTRACT_ID = review_ui.CONTRACT_ID
THEME_JS = review_ui.THEME_JS
masthead = review_ui.masthead

_REVIEW_COPIES_SPEC = importlib.util.spec_from_file_location(
    "diligence_review_copies", Path(__file__).with_name("review_copies.py")
)
if _REVIEW_COPIES_SPEC is None or _REVIEW_COPIES_SPEC.loader is None:
    raise RuntimeError("cannot load sibling review_copies.py")
review_copies = importlib.util.module_from_spec(_REVIEW_COPIES_SPEC)
sys.modules[_REVIEW_COPIES_SPEC.name] = review_copies
_REVIEW_COPIES_SPEC.loader.exec_module(review_copies)

STATUS_LABELS = {
    "present": "Found",
    "absent": "Not found",
    "unresolved": "Needs a decision",
}
STATUS_CLASSES = {
    "present": "found",
    "absent": "not-found",
    "unresolved": "needs-review",
}
PRESCRIPTIVE_PHRASES = (
    "buyer should",
    "seller should",
    "we recommend",
    "we advise",
    "recommended action",
)


def read_json(path: Path, label: str, required: tuple[str, ...]) -> dict:
    try:
        data = json.loads(path.read_text(encoding="utf-8"))
    except (OSError, json.JSONDecodeError) as exc:
        raise ValueError(f"cannot read {label} at {path}: {exc}") from exc
    for key in required:
        if key not in data:
            raise ValueError(f"{label} missing key {key!r}")
    return data


def sha256(path: Path) -> str:
    digest = hashlib.sha256()
    with path.open("rb") as handle:
        for chunk in iter(lambda: handle.read(1024 * 1024), b""):
            digest.update(chunk)
    return digest.hexdigest()


def esc(value) -> str:
    return html.escape(str(value or ""), quote=True)


def pretty_filename(path: str) -> str:
    stem = posixpath.splitext(posixpath.basename(path))[0]
    words = stem.replace("-", " ").replace("_", " ").split()
    acronyms = {"msa", "psa", "ssa", "ela", "iaas", "dpa", "sow", "nda"}
    return " ".join(
        word.upper() if word.lower() in acronyms else word.capitalize()
        for word in words
    )


def source_href(prefix: str, path: str) -> str:
    return review_ui.source_href(prefix, path, link_without_prefix=True)


def document_anchor(document: dict) -> str:
    path_digest = hashlib.sha256(document.get("path", "").encode("utf-8")).hexdigest()[
        :8
    ]
    doc_id = str(document.get("id", "document")).removeprefix("sha256:")
    return f"review-document-{doc_id}-{path_digest}"


def render_review_document(document, source_prefix, validation, *, title=None):
    heading = title or pretty_filename(document.get("path", "")) or "Document"
    href = source_href(source_prefix, document.get("path", ""))
    component = review_copies.render_review_copy_component(
        validation,
        document.get("id", ""),
        path=document.get("path", ""),
        title=heading,
    )
    return (
        f'<article id="{esc(document_anchor(document))}" class="source-review-item" '
        'data-review-document="true" tabindex="-1">'
        f'{component}<p class="original-file"><a href="{esc(href)}" '
        'target="_blank" rel="noopener">Open original file ↗</a></p></article>'
    )


def status_counts(rows: list[dict]) -> Counter:
    return Counter(row.get("status") for row in rows)


def badge(status: str, count: int | None = None) -> str:
    suffix = f" {count}" if count is not None else ""
    return (
        f'<span class="pill {STATUS_CLASSES[status]}">'
        f"{esc(STATUS_LABELS[status])}{suffix}</span>"
    )


def build_model(manifest, families, framework, findings, gaps, reconciliation):
    documents = {row["id"]: row for row in manifest["documents"]}
    family_by_unit = {
        row.get("family_id", ""): row for row in families.get("families", [])
    }
    issues = []
    issue_by_id = {}
    for lens_order, lens in enumerate(framework["lenses"]):
        for issue_order, item in enumerate(lens.get("items", [])):
            issue = {
                "issue_id": item["issue_id"],
                "question": item["question"],
                "lens_id": lens["lens_id"],
                "lens_name": lens.get("name", lens["lens_id"]),
                "lens_order": lens_order,
                "issue_order": issue_order,
            }
            issues.append(issue)
            issue_by_id[issue["issue_id"]] = issue

    reviewable = reconciliation["reviewable_units"]
    units: list[dict[str, Any]] = []
    unit_by_id = {}
    for unit_id in reviewable:
        family = family_by_unit.get(unit_id)
        if family:
            members = sorted(
                family.get("members", []),
                key=lambda row: (row.get("order", 0), row.get("id", "")),
            )
        else:
            members = [{"id": unit_id, "role": "standalone", "order": 0}]
        member_rows = []
        for member in members:
            document = documents.get(member.get("id", ""))
            if not document or document.get("review_role") == "runner-control":
                continue
            member_rows.append(
                {
                    "id": member["id"],
                    "path": document["path"],
                    "role": member.get("role", "member"),
                }
            )
        base = documents.get(unit_id) or documents[member_rows[0]["id"]]
        unit = {
            "unit_id": unit_id,
            "name": pretty_filename(base["path"]),
            "path": base["path"],
            "members": member_rows,
            "parked": unit_id in reconciliation["parked_units"],
        }
        units.append(unit)
        unit_by_id[unit_id] = unit
    units.sort(key=lambda row: (row["name"].lower(), row["unit_id"]))

    rows = []
    by_issue = defaultdict(list)
    by_unit = defaultdict(list)
    for raw in findings.get("findings", []):
        row = dict(raw)
        row["document"] = documents[row["doc_id"]]
        row["unit"] = unit_by_id[row["unit_id"]]
        row["issue"] = issue_by_id[row["issue_id"]]
        rows.append(row)
        by_issue[row["issue_id"]].append(row)
        by_unit[row["unit_id"]].append(row)
    for issue_id in by_issue:
        by_issue[issue_id].sort(key=lambda row: row["unit"]["name"].lower())
    issue_order = {row["issue_id"]: index for index, row in enumerate(issues)}
    for unit_id in by_unit:
        by_unit[unit_id].sort(key=lambda row: issue_order[row["issue_id"]])
    return {
        "documents": documents,
        "issues": issues,
        "units": units,
        "rows": rows,
        "by_issue": by_issue,
        "by_unit": by_unit,
        "gaps": gaps.get("entries", []),
    }


def receipt_text(row: dict) -> str:
    quote_receipt = row.get("quote_verification") or {}
    checker_receipt = row.get("verification") or {}
    quote_checker = quote_receipt.get("checker") or "deterministic text match"
    finding_checker = checker_receipt.get("checker") or "independent checker"
    return f"Quote verified · {quote_checker}; finding checked · {finding_checker}"


def finding_search_text(row: dict) -> str:
    document = row["document"]
    unit = row["unit"]
    title = unit["name"]
    if row["doc_id"] != row["unit_id"]:
        title += f" · evidence in {pretty_filename(document['path'])}"
    return " ".join(
        str(value or "")
        for value in (
            row["issue"]["question"],
            title,
            document["path"],
            row.get("section"),
            row.get("characterization"),
            row.get("quote"),
        )
    ).lower()


def finding_card(row: dict, compact: bool = False) -> str:
    status = row["status"]
    document = row["document"]
    unit = row["unit"]
    title = unit["name"]
    if row["doc_id"] != row["unit_id"]:
        title += f" · evidence in {pretty_filename(document['path'])}"
    search = finding_search_text(row)
    out = [
        f'<article class="finding-card {STATUS_CLASSES[status]}" '
        f'data-status="{esc(status)}" data-search="{esc(search)}">',
        '<div class="finding-head">',
        f'<div><div class="finding-title">{esc(title)}</div>'
        f'<div class="fileline">{esc(document["path"])}</div></div>',
        badge(status),
        "</div>",
    ]
    if row.get("characterization"):
        out.append(f'<p class="characterization">{esc(row["characterization"])}</p>')
    if status == "present" and not compact:
        out.append(f"<blockquote>{esc(row['quote'])}</blockquote>")
        out.append(
            f'<p class="cite">{esc(row.get("section"))} · {esc(document["path"])}</p>'
        )
        out.append('<div class="receipt-line">')
        out.append(f'<span class="verification">✓ {esc(receipt_text(row))}</span>')
        if row.get("current_position"):
            out.append('<span class="current">Current governing position</span>')
        out.append("</div>")
    elif status == "unresolved":
        out.append(
            '<p class="scope-note">No affirmative contract conclusion is presented '
            "for this result.</p>"
        )
    out.append(
        f'<p class="source-access"><a href="#{esc(document_anchor(document))}" '
        'data-review-jump="true">Review document on this page ↓</a></p>'
    )
    out.append("</article>")
    return "\n".join(out)


def render_issue_group(issue, rows):
    counts = status_counts(rows)
    search = " ".join(
        [issue["question"], issue["lens_name"]]
        + [finding_search_text(row) for row in rows]
    ).lower()
    out = [
        f'<details class="crosswalk-group" data-search="{esc(search)}">',
        "<summary>",
        '<div class="summary-main">',
        f'<span class="eyebrow">{esc(issue["lens_name"])}</span>',
        f'<span class="group-title">{esc(issue["question"])}</span>',
        "</div>",
        '<div class="pills">',
        badge("present", counts["present"]),
        badge("unresolved", counts["unresolved"]),
        badge("absent", counts["absent"]),
        "</div></summary>",
        '<div class="group-body">',
    ]
    for label, status in (("Found", "present"), ("Needs a decision", "unresolved")):
        selected = [row for row in rows if row["status"] == status]
        if selected:
            out.append(f"<h4>{label} · {len(selected)}</h4>")
            out.extend(finding_card(row) for row in selected)
    absent = [row for row in rows if row["status"] == "absent"]
    if absent:
        out.append(
            f'<details class="negative-drawer"><summary>Also checked: {len(absent)} '
            "agreement unit(s) with nothing found</summary>"
        )
        out.append(
            '<p class="scope-note">“Not found” is limited to the supplied visible '
            "text in these reviewed agreement units.</p>"
        )
        out.extend(finding_card(row, compact=True) for row in absent)
        out.append("</details>")
    out.append("</div></details>")
    return "\n".join(out)


def member_links(unit):
    links = []
    for member in unit["members"]:
        links.append(
            f'<a href="#{esc(document_anchor(member))}" data-review-jump="true">'
            f'Review {esc(member["path"])}</a> <span class="role">{esc(member["role"])}</span>'
        )
    return "<br>".join(links)


def render_unit_group(unit, rows, source_prefix, validation):
    counts = status_counts(rows)
    search = " ".join(
        [unit["name"]]
        + [member["path"] for member in unit["members"]]
        + [finding_search_text(row) for row in rows]
    ).lower()
    out = [
        f'<details class="crosswalk-group" data-search="{esc(search)}">',
        "<summary>",
        '<div class="summary-main"><span class="eyebrow">Agreement unit</span>',
        f'<span class="group-title">{esc(unit["name"])}</span>',
        f'<span class="memberline">{len(unit["members"])} source file(s)</span></div>',
        '<div class="pills">',
    ]
    if unit["parked"]:
        out.append('<span class="pill needs-review">Parked</span>')
    else:
        out.extend(
            [
                badge("present", counts["present"]),
                badge("unresolved", counts["unresolved"]),
                badge("absent", counts["absent"]),
            ]
        )
    out.extend(
        [
            "</div></summary>",
            '<div class="group-body">',
            f'<div class="member-box"><b>Unit documents</b><br>{member_links(unit)}</div>',
        ]
    )
    for member in unit["members"]:
        out.append(
            render_review_document(
                member,
                source_prefix,
                validation,
                title=pretty_filename(member["path"]),
            )
        )
    if unit["parked"]:
        out.append(
            '<p class="scope-note">This unit was parked and no substantive result is '
            "presented for any issue.</p>"
        )
    else:
        for label, status in (("Found", "present"), ("Needs a decision", "unresolved")):
            selected = [row for row in rows if row["status"] == status]
            if selected:
                out.append(f"<h4>{label} · {len(selected)}</h4>")
                for row in selected:
                    out.append(
                        f'<div class="issue-context">{esc(row["issue"]["question"])}</div>'
                    )
                    out.append(finding_card(row))
        absent = [row for row in rows if row["status"] == "absent"]
        if absent:
            out.append(
                f'<details class="negative-drawer"><summary>Also checked: {len(absent)} '
                "issue(s) with nothing found</summary>"
            )
            out.append(
                '<p class="scope-note">These negatives are limited to this unit’s '
                "supplied visible text.</p>"
            )
            for row in absent:
                out.append(
                    f'<div class="issue-context">{esc(row["issue"]["question"])}</div>'
                )
                out.append(finding_card(row, compact=True))
            out.append("</details>")
    out.append("</div></details>")
    return "\n".join(out)


CSS = (
    BRAND_CSS
    + r"""
:root{--desk:var(--lq-background);--paper:var(--lq-surface);--ink:var(--lq-foreground);
--muted:var(--lq-muted-foreground);--line:var(--lq-border);--blue:var(--lq-primary);
--blue-soft:var(--lq-accent);--green:var(--lq-success);--green-bg:var(--lq-success-surface);
--amber:var(--lq-attention);--amber-bg:var(--lq-attention-surface);--gray:var(--lq-surface-muted);
--focus:var(--lq-ring);--serif:var(--lq-font-serif);--sans:var(--lq-font-sans);--mono:var(--lq-font-mono)}
*{box-sizing:border-box}[hidden]{display:none!important}html{scroll-behavior:smooth}
body{margin:0;background:var(--desk);color:var(--ink);font:15px/1.5 var(--sans)}a{color:var(--blue);text-underline-offset:3px}
.skip{position:absolute;left:-9999px;top:4px;background:var(--paper);padding:6px 10px;z-index:20}.skip:focus{left:4px}
header,main,footer{max-width:1080px;margin:0 auto;padding-left:20px;padding-right:20px}header{padding-top:20px}
.machine-note{display:inline-block;background:var(--blue-soft);color:var(--blue);border:1px solid var(--blue);border-radius:3px;padding:3px 8px;font-size:12px;font-weight:500}
h1{font:500 28px/1.2 var(--serif);margin:7px 0 2px}.sub,.scope-note{color:var(--muted);font-size:13px}.sub{margin:0 0 12px}
.boundary{background:var(--paper);border:1px solid var(--line);border-left:4px solid var(--blue);padding:11px 14px;margin:12px 0}
.statline{display:grid;grid-template-columns:repeat(5,minmax(0,1fr));gap:8px;margin:14px 0}.stat{background:var(--paper);border:1px solid var(--line);padding:9px 11px}.stat strong{display:block;font:500 22px var(--serif)}.stat span{font-size:12px;color:var(--muted)}
.toolbar{display:flex;gap:8px;align-items:center;flex-wrap:wrap}.toolbar input{flex:1;min-width:240px}.toolbar input,.clear{background:var(--paper);color:var(--ink);border:1px solid var(--line);border-radius:4px;padding:8px 10px;font:14px var(--sans)}.clear{cursor:pointer}.filter-count{font-size:12px;color:var(--muted);margin-left:auto}
.tabs{display:flex;border-bottom:2px solid var(--ink);margin:10px 0 14px;overflow-x:auto}.tab{border:0;border-bottom:3px solid transparent;background:none;color:var(--muted);padding:8px 16px;font:500 16px var(--serif);cursor:pointer;white-space:nowrap;margin-bottom:-2px}.tab[aria-selected=true]{color:var(--ink);border-bottom-color:var(--blue)}
h2{font:500 22px var(--serif);margin:20px 0 2px}h3{font:500 18px var(--serif)}h4{font:500 15px var(--serif);margin:17px 0 8px}.lens-heading{font:500 12px var(--mono);letter-spacing:.1em;text-transform:uppercase;color:var(--muted);margin:22px 0 8px}
.crosswalk-group{background:var(--paper);border:1px solid var(--line);border-radius:2px;margin:0 0 8px}.crosswalk-group>summary{display:flex;gap:16px;align-items:center;justify-content:space-between;cursor:pointer;padding:11px 13px}.summary-main{display:flex;flex-direction:column;min-width:0}.eyebrow{font:11px var(--mono);color:var(--muted);text-transform:uppercase;letter-spacing:.09em}.group-title{font:500 16px/1.3 var(--serif)}.memberline{font-size:12px;color:var(--muted)}
.pills,.receipt-line{display:flex;gap:5px;flex-wrap:wrap;align-items:center}.pill{border-radius:12px;padding:2px 8px;font-size:12px;white-space:nowrap}.pill.found{background:var(--green-bg);color:var(--green)}.pill.needs-review{background:var(--amber-bg);color:var(--amber)}.pill.not-found{background:var(--gray);color:var(--muted)}
.group-body{border-top:1px solid var(--line);padding:12px 14px 15px}.finding-card{border:1px solid var(--line);border-left:4px solid var(--line);padding:10px 12px;margin:8px 0;background:var(--paper)}.finding-card.found{border-left-color:var(--green)}.finding-card.needs-review{border-left-color:var(--amber)}.finding-head{display:flex;gap:12px;justify-content:space-between;align-items:flex-start}.finding-title{font-weight:500}.fileline,.cite,.receipt-line,.role{font:11.5px var(--mono);color:var(--muted)}.characterization{margin:6px 0}
blockquote{font:15.5px/1.5 var(--serif);margin:9px 0;padding:8px 12px;background:var(--gray);border-left:3px solid var(--blue)}.verification{color:var(--green)}.current{background:var(--blue-soft);color:var(--blue);border-radius:9px;padding:1px 7px}.source-access{margin:7px 0 0;font-size:13px}.negative-drawer{margin:13px 0 0;border-top:1px dashed var(--line);padding-top:8px}.negative-drawer>summary{cursor:pointer;font-weight:500}.issue-context{font:500 14px var(--serif);margin:12px 0 2px}.member-box,.plain-card{background:var(--gray);padding:10px 12px;margin:3px 0 12px}.plain-card{background:var(--paper);border:1px solid var(--line)}.role{margin-left:5px}
.review-copy-alert{background:var(--amber-bg);color:var(--amber);border:1px solid var(--amber);padding:12px 14px;margin:12px 0}.source-review-item{margin:14px 0;padding:14px;border:1px solid var(--line);background:var(--gray)}.source-review-item:target{outline:3px solid var(--focus);outline-offset:3px}.lq-review-copy h3{margin-bottom:6px}.lq-review-copy-receipt{font:11.5px/1.5 var(--mono);color:var(--muted);overflow-wrap:anywhere}.lq-review-copy-frame,.lq-review-copy-pdf{display:block;width:100%;min-height:430px;border:1px solid var(--line);background:var(--paper)}.lq-review-copy-image{display:block;max-width:100%;height:auto;margin:8px auto;border:1px solid var(--line)}.lq-needs-rendering{padding:12px 14px;background:var(--amber-bg);color:var(--amber);border-left:4px solid var(--amber)}.original-file{margin:9px 0 0;font-size:13px}
table{width:100%;border-collapse:collapse;background:var(--paper)}th,td{text-align:left;vertical-align:top;border-bottom:1px solid var(--line);padding:8px 10px}th{font:500 13px var(--serif)}.scroll{overflow-x:auto}.gap{border-left:4px solid var(--amber);padding:10px 12px;margin:8px 0;background:var(--paper);border-top:1px solid var(--line);border-right:1px solid var(--line);border-bottom:1px solid var(--line)}.gap p{margin:4px 0}.technical{font:12px/1.5 var(--mono);overflow-wrap:anywhere}.audit details{background:var(--paper);border:1px solid var(--line);padding:9px 12px;margin:8px 0}.audit summary{cursor:pointer;font-weight:500}.refusal{background:var(--amber-bg);color:var(--amber);border:1px solid var(--amber);padding:12px 14px;margin:12px 0}
footer{padding-top:12px;padding-bottom:40px;color:var(--muted);font-size:13px}button:focus-visible,input:focus-visible,summary:focus-visible,a:focus-visible{outline:2px solid var(--focus);outline-offset:2px}
@media(max-width:760px){header,main,footer{padding-left:12px;padding-right:12px}.statline{grid-template-columns:repeat(2,minmax(0,1fr))}.crosswalk-group>summary,.finding-head{align-items:flex-start;flex-direction:column}.pills{justify-content:flex-start}.tabs{padding-bottom:1px}}
@media(prefers-reduced-motion:reduce){*{scroll-behavior:auto!important}}
"""
)

JS = (
    r"""
"use strict";
const tabs=[...document.querySelectorAll('.tab')],panels=[...document.querySelectorAll('[role=tabpanel]')];
const search=document.getElementById('search'),count=document.getElementById('filter-count');
function selectTab(name){tabs.forEach(tab=>{const on=tab.dataset.tab===name;tab.setAttribute('aria-selected',String(on));tab.tabIndex=on?0:-1});panels.forEach(panel=>panel.hidden=panel.dataset.panel!==name);search.disabled=!['issues','agreements'].includes(name);applySearch()}
function applySearch(){const panel=panels.find(row=>!row.hidden),q=search.value.trim().toLowerCase();if(!panel)return;const groups=[...panel.querySelectorAll('.crosswalk-group')];let shown=0;groups.forEach(group=>{const on=!q||group.dataset.search.includes(q);group.hidden=!on;if(on){shown+=1;if(q)group.open=true}});count.textContent=groups.length?`${shown} of ${groups.length} groups shown`:''}
tabs.forEach(tab=>tab.addEventListener('click',()=>selectTab(tab.dataset.tab)));document.querySelector('.tabs').addEventListener('keydown',event=>{if(!['ArrowLeft','ArrowRight'].includes(event.key))return;const current=tabs.findIndex(tab=>tab.getAttribute('aria-selected')==='true'),next=(current+(event.key==='ArrowRight'?1:-1)+tabs.length)%tabs.length;tabs[next].focus();selectTab(tabs[next].dataset.tab);event.preventDefault()});
search.addEventListener('input',applySearch);document.getElementById('clear-search').addEventListener('click',()=>{search.value='';applySearch();search.focus()});document.addEventListener('keydown',event=>{if(event.key==='/'&&document.activeElement!==search&&!search.disabled){event.preventDefault();search.focus()}});selectTab('issues');
document.addEventListener('click',event=>{const link=event.target.closest('[data-review-jump=true]');if(!link)return;const target=document.querySelector(link.getAttribute('href'));if(!target)return;selectTab(target.closest('#panel-scope')?'scope':'agreements');const group=target.closest('details.crosswalk-group');if(group){group.hidden=false;group.open=true}setTimeout(()=>target.focus(),0)});
"""
    + THEME_JS
)


def factual_errors(findings):
    errors = []
    for row in findings.get("findings", []):
        characterization = str(row.get("characterization") or "").lower()
        for phrase in PRESCRIPTIVE_PHRASES:
            if phrase in characterization:
                errors.append(
                    f"{row.get('finding_id', '<no id>')}: characterization contains prescriptive phrase {phrase!r}"
                )
    return errors


def source_path_errors(manifest, source_prefix):
    errors = []
    try:
        review_ui.safe_source_prefix(source_prefix)
    except ValueError as error:
        errors.append(str(error))
    for document in manifest.get("documents", []):
        try:
            review_ui.safe_source_path(document.get("path"))
        except ValueError:
            errors.append(
                f"manifest document {document.get('id', '<no id>')} has an unsafe source path"
            )
    return errors


def render_html(
    manifest,
    framework,
    findings,
    model,
    reconciliation,
    inputs,
    source_prefix,
    review_validation,
):
    counts = status_counts(model["rows"])
    controls = [
        row
        for row in manifest["documents"]
        if row.get("review_role", "substantive") == "runner-control"
    ]
    substantive = [row for row in manifest["documents"] if row not in controls]
    reviewed_units = [row for row in model["units"] if not row["parked"]]
    render_blockers = [
        row
        for row in review_validation.sidecar.get("documents", [])
        if row.get("status") != "ready"
    ]
    review_alert = ""
    if render_blockers:
        label = (
            "One file needs rendering"
            if len(render_blockers) == 1
            else f"{len(render_blockers)} files need rendering"
        )
        review_alert = (
            f'<div class="review-copy-alert" role="alert"><b>{esc(label)}.</b> '
            "The affected results are not ready for reliance until a verified "
            "in-page copy is available.</div>"
        )
    parts = [
        "<!doctype html>",
        f'<html lang="en" data-lq-review-ui="{CONTRACT_ID}"><head><meta charset="utf-8">',
        '<meta name="viewport" content="width=device-width,initial-scale=1">',
        "<title>Diligence factual crosswalk</title>",
        f"<style>{CSS}</style></head><body>",
        '<a class="skip" href="#panel-issues">Skip to crosswalk</a>',
        masthead("Final factual review"),
        "<header>",
        '<span class="machine-note">Factual results · verify before relying</span>',
        f"<h1>{esc(manifest.get('root_label', 'Data room'))}</h1>",
        f'<p class="sub">{len(manifest["documents"])} files accounted for · '
        f"{len(reviewed_units)} agreement units reviewed · {len(model['issues'])} approved issues</p>",
        '<div class="boundary"><b>What this page does.</b> It maps the approved issue list to the supplied agreement units and shows checked evidence. It does not rank risk, recommend a transaction response, or decide legal effect.</div>',
        '<div class="statline">',
        f'<div class="stat"><strong>{len(manifest["documents"])}</strong><span>files in corpus</span></div>',
        f'<div class="stat"><strong>{len(reviewed_units)}</strong><span>agreement units reviewed</span></div>',
        f'<div class="stat"><strong>{counts["present"]}</strong><span>matches found</span></div>',
        f'<div class="stat"><strong>{counts["unresolved"]}</strong><span>results needing a decision</span></div>',
        f'<div class="stat"><strong>{counts["absent"]}</strong><span>nothing found in reviewed text</span></div>',
        "</div>",
        review_alert,
        '<div class="toolbar"><input id="search" type="search" aria-label="Search issues, agreements, and evidence" placeholder="Search issues, agreements, or quoted text… ( / )"><button class="clear" id="clear-search" type="button">Clear</button><span class="filter-count" id="filter-count" aria-live="polite"></span></div>',
        '<div class="tabs" role="tablist" aria-label="Crosswalk orientation">',
        '<button class="tab" type="button" role="tab" data-tab="issues" aria-selected="true" aria-controls="panel-issues">Issues</button>',
        '<button class="tab" type="button" role="tab" data-tab="agreements" aria-selected="false" aria-controls="panel-agreements" tabindex="-1">Agreements</button>',
        '<button class="tab" type="button" role="tab" data-tab="scope" aria-selected="false" aria-controls="panel-scope" tabindex="-1">Scope &amp; gaps</button>',
        '<button class="tab" type="button" role="tab" data-tab="audit" aria-selected="false" aria-controls="panel-audit" tabindex="-1">Audit</button>',
        "</div></header><main>",
        '<section id="panel-issues" role="tabpanel" data-panel="issues">',
        '<h2>Issues</h2><p class="scope-note">Open an issue to see where language was found, results needing a decision, and reviewed units where nothing was found.</p>',
    ]
    last_lens = None
    for issue in model["issues"]:
        if issue["lens_id"] != last_lens:
            parts.append(f'<div class="lens-heading">{esc(issue["lens_name"])}</div>')
            last_lens = issue["lens_id"]
        parts.append(render_issue_group(issue, model["by_issue"][issue["issue_id"]]))
    parts.append("</section>")
    parts.append(
        '<section id="panel-agreements" role="tabpanel" data-panel="agreements" hidden>'
    )
    parts.append(
        '<h2>Agreements</h2><p class="scope-note">Open an agreement unit to see every approved issue tested against it.</p>'
    )
    for unit in model["units"]:
        parts.append(
            render_unit_group(
                unit,
                model["by_unit"][unit["unit_id"]],
                source_prefix,
                review_validation,
            )
        )
    parts.append("</section>")

    parts.append('<section id="panel-scope" role="tabpanel" data-panel="scope" hidden>')
    parts.append("<h2>Scope and gaps</h2>")
    parts.append(
        f'<div class="plain-card"><b>Corpus accounting.</b> {len(controls)} runner-control input(s) '
        f"governed the scope or issue framework but were not substantive sample units. "
        f"{len(substantive)} substantive source file(s) formed {len(model['units'])} agreement unit(s); "
        f"{reconciliation['totals']['parked']} unit(s) were parked.</div>"
    )
    parts.append(
        '<div class="scroll"><table><thead><tr><th>File</th><th>Run role</th><th>Readability</th></tr></thead><tbody>'
    )
    for document in manifest["documents"]:
        role = (
            "Runner-control input"
            if document in controls
            else "Substantive agreement material"
        )
        parts.append(
            f'<tr><td><a href="#{esc(document_anchor(document))}" data-review-jump="true">'
            f"Review {esc(document['path'])}</a></td><td>{role}</td>"
            f"<td>{esc(document.get('readability'))}</td></tr>"
        )
    parts.append("</tbody></table></div>")
    if controls:
        parts.append("<h3>Review instructions and control inputs</h3>")
        for document in sorted(controls, key=lambda row: (row["path"], row["id"])):
            parts.append(
                render_review_document(
                    document,
                    source_prefix,
                    review_validation,
                    title=pretty_filename(document["path"]),
                )
            )
    parts.append(
        f"<h3>Missing, unreadable, or duplicate materials · {len(model['gaps'])}</h3>"
    )
    for gap in model["gaps"]:
        parts.append(
            f'<article class="gap"><b>{esc(str(gap.get("type", "gap")).replace("-", " ").title())}</b>'
            f'<p>{esc(gap.get("detail"))}</p><p class="cite">Evidence: {esc(gap.get("evidence"))}</p></article>'
        )
    if not model["gaps"]:
        parts.append('<p class="scope-note">No gap entries were recorded.</p>')
    parts.append(
        '<div class="plain-card"><b>Meaning of “Not found.”</b> The approved issue test found no responsive language in the supplied visible text for that reviewed agreement unit. It does not establish portfolio-wide absence and does not cover missing agreements or materials.</div>'
    )
    parts.append("</section>")

    parts.append(
        '<section id="panel-audit" role="tabpanel" data-panel="audit" class="audit" hidden>'
    )
    parts.append(
        '<h2>Audit</h2><p class="scope-note">Technical provenance remains available without dominating the legal review.</p>'
    )
    parts.append(
        f'<div class="plain-card technical">Framework version: {esc(framework.get("framework_version"))}<br>'
        f"Review plan: {esc(findings.get('review_plan_id') or 'not recorded')}<br>"
        f"Result equation: {counts['present']} found + {counts['absent']} not found + "
        f"{counts['unresolved']} needs a decision = {len(model['rows'])} results<br>"
        f"Expected matrix: {len(model['issues'])} issues × {len(reviewed_units)} reviewed agreement units "
        f"= {len(model['issues']) * len(reviewed_units)} results<br>"
        f"Review-copy receipt: {esc(review_validation.sidecar.get('digest'))}<br>"
        f"Review-copy status: {esc(review_validation.sidecar.get('status'))}</div>"
    )
    parts.append('<details><summary>Input hashes</summary><div class="technical">')
    for record in inputs:
        parts.append(f"{esc(record['label'])} · SHA-256 {esc(record['sha256'])}<br>")
    parts.append("</div></details>")
    parts.append(
        "<details><summary>Assurance boundary</summary><p>The matrix is complete for the approved framework and reviewable substantive units. Present results carry separate quote and checker receipts. Missing, unreadable, runner-control, parked, and needs-a-decision lanes remain visible. The page itself performs no new substantive review.</p></details>"
    )
    parts.append(
        "<details><summary>Fields intentionally not used</summary><p>Materiality bands and ranking fields, if present in the source ledger, do not affect grouping, visibility, checker selection, or ordering on this factual surface.</p></details>"
    )
    parts.append("</section></main>")
    parts.append(
        "<footer>This is a factual review surface. Analysis, prioritization, or transaction advice requires a separate instruction.</footer>"
    )
    parts.append(f"<script>{JS}</script></body></html>")
    return "\n".join(parts) + "\n"


def refusal_html(label: str, errors: list[str]) -> str:
    items = "".join(f"<li>{esc(error)}</li>" for error in errors)
    return (
        f'<!doctype html><html lang="en" data-lq-review-ui="{CONTRACT_ID}"><head><meta charset="utf-8">'
        f'<meta name="viewport" content="width=device-width,initial-scale=1"><style>{CSS}</style>'
        f"<title>Diligence crosswalk refused</title></head><body>{masthead('Review paused')}"
        f"<main><header><h1>{esc(label)}</h1>"
        '<div class="refusal" role="alert"><b>Crosswalk not issued.</b> The factual coverage or assurance '
        f"checks failed:<ul>{items}</ul></div></header></main><script>{THEME_JS}</script></body></html>\n"
    )


def main():
    parser = argparse.ArgumentParser(
        description="Render the factual diligence crosswalk."
    )
    parser.add_argument("--framework", required=True, type=Path)
    parser.add_argument("--findings", required=True, type=Path)
    parser.add_argument("--manifest", required=True, type=Path)
    parser.add_argument("--families", required=True, type=Path)
    parser.add_argument("--gaps", required=True, type=Path)
    parser.add_argument("--source-prefix", default="")
    parser.add_argument("--review-copies", required=True, type=Path)
    parser.add_argument("--document-root", required=True, type=Path)
    parser.add_argument("--out", required=True, type=Path)
    parser.add_argument("--receipt", required=True, type=Path)
    args = parser.parse_args()

    paths = (
        ("Manifest", args.manifest, ("documents", "counts")),
        ("Confirmed family map", args.families, ("families",)),
        ("Approved framework", args.framework, ("framework_version", "lenses")),
        ("Checked findings ledger", args.findings, ("findings",)),
        ("Gap report", args.gaps, ("entries",)),
    )
    try:
        loaded = [read_json(path, label, required) for label, path, required in paths]
    except ValueError as exc:
        print(f"render_crosswalk: {exc}", file=sys.stderr)
        raise SystemExit(2) from exc
    manifest, families, framework, findings, gaps = loaded
    inputs = [{"label": label, "sha256": sha256(path)} for label, path, _ in paths]
    if args.review_copies.is_file():
        inputs.append(
            {"label": "Review-copy sidecar", "sha256": sha256(args.review_copies)}
        )
    reconciliation = reconcile_counts.reconcile(manifest, findings, framework, families)
    errors = (
        list(reconciliation["errors"])
        + factual_errors(findings)
        + source_path_errors(manifest, args.source_prefix)
    )
    review_validation = review_copies.revalidate_review_copies(
        args.review_copies, args.manifest, args.document_root
    )
    if args.review_copies.parent.resolve() != args.out.parent.resolve():
        errors.append("review-copies sidecar and HTML output must share a directory")
    if not review_validation.integrity_ok:
        errors.extend(
            f"review-copy validation failed: {error}"
            for error in review_validation.errors
        )
    if not reconciliation["ok"]:
        errors.extend(reconciliation["lines"])
    errors = sorted(set(errors))

    args.out.parent.mkdir(parents=True, exist_ok=True)
    args.receipt.parent.mkdir(parents=True, exist_ok=True)
    if errors:
        output = refusal_html(manifest.get("root_label", "Data room"), errors)
    else:
        model = build_model(
            manifest, families, framework, findings, gaps, reconciliation
        )
        output = render_html(
            manifest,
            framework,
            findings,
            model,
            reconciliation,
            inputs,
            args.source_prefix,
            review_validation,
        )
    with args.out.open("w", encoding="utf-8", newline="\n") as handle:
        handle.write(output)
    counts = Counter(row.get("status") for row in findings.get("findings", []))
    receipt = {
        "artifact": "diligence-factual-crosswalk-receipt",
        "version": 1,
        "status": "fail" if errors or not review_validation.ready else "pass",
        "errors": errors,
        "inputs": inputs,
        "output": {"bytes": args.out.stat().st_size, "sha256": sha256(args.out)},
        "counts": {
            **reconciliation["totals"],
            "found": counts["present"],
            "not_found": counts["absent"],
            "needs_review": counts["unresolved"],
        },
        "checks": {
            "complete_issue_unit_cross_product": reconciliation["ok"],
            "runner_control_excluded_from_substantive_units": not any(
                "runner-control" in error for error in reconciliation["errors"]
            ),
            "present_results_have_quote_locator_and_two_receipts": not any(
                "receipt" in error or "empty quote" in error or "empty section" in error
                for error in reconciliation["errors"]
            ),
            "factual_characterizations": not factual_errors(findings),
            "review_copy_integrity": review_validation.integrity_ok,
            "review_copies_ready": review_validation.ready,
        },
    }
    with args.receipt.open("w", encoding="utf-8", newline="\n") as handle:
        handle.write(json.dumps(receipt, indent=2, sort_keys=True) + "\n")
    print(
        json.dumps(
            {"status": receipt["status"], "counts": receipt["counts"]},
            indent=2,
            sort_keys=True,
        )
    )
    raise SystemExit(1 if errors or not review_validation.ready else 0)


if __name__ == "__main__":
    main()

SHA-256: 6172c4bfde41fc212007bfc5627ef2140027be50b51a00142e98fbca89411f30