← Files LegalQuants TransactionalARCHIVED FILE

skills/diligence/scripts/verify_finding_quotes.py

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

↓ Download file

#!/usr/bin/env python3
"""Verify every present finding quote against its manifest source document.

Confirmed matches receive a deterministic quote receipt. A missing or
non-matching quote becomes unresolved with a rejected receipt; stale independent
checker state is removed. File identity or I/O drift fails closed before output.
"""

from __future__ import annotations

import argparse
import hashlib
import importlib.util
import json
import sys
from pathlib import Path

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


def load(path, label, required):
    try:
        value = json.loads(Path(path).read_text(encoding="utf-8"))
    except (OSError, json.JSONDecodeError) as exc:
        sys.exit(f"verify_finding_quotes: cannot read {label}: {exc}")
    for key in required:
        if key not in value:
            sys.exit(f"verify_finding_quotes: {label} missing key {key!r}")
    return value


def hash_id(path):
    digest = hashlib.sha256()
    with path.open("rb") as handle:
        for chunk in iter(lambda: handle.read(1 << 20), b""):
            digest.update(chunk)
    return "sha256:" + digest.hexdigest()[:12]


def source_path(root, relative):
    rel = Path(relative)
    if rel.is_absolute() or ".." in rel.parts:
        raise ValueError(f"unsafe manifest path: {relative}")
    candidate = (root / rel).resolve()
    try:
        candidate.relative_to(root)
    except ValueError as exc:
        raise ValueError(f"manifest path escapes room root: {relative}") from exc
    return candidate


def main():
    parser = argparse.ArgumentParser(description=__doc__.splitlines()[0])
    parser.add_argument("--findings", required=True)
    parser.add_argument("--manifest", required=True)
    parser.add_argument("--room-root", required=True)
    parser.add_argument("--extractor", choices=["auto", "stdlib"], default="auto")
    parser.add_argument("--out", required=True)
    args = parser.parse_args()

    findings = load(args.findings, "findings", ["findings"])
    manifest = load(args.manifest, "manifest", ["documents", "counts"])
    root = Path(args.room_root).expanduser().resolve()
    if not root.is_dir():
        sys.exit(f"verify_finding_quotes: room root is not a directory: {root}")
    documents = {row["id"]: row for row in manifest["documents"]}
    sources = {}
    errors = []
    for doc_id, document in sorted(documents.items()):
        try:
            path = source_path(root, document["path"])
            if not path.is_file():
                errors.append(f"source document does not exist: {document['path']}")
            elif hash_id(path) != doc_id:
                errors.append(f"manifest identity changed for {document['path']}")
            else:
                sources[doc_id] = path
        except (OSError, ValueError) as exc:
            errors.append(str(exc))
    if errors:
        sys.exit("verify_finding_quotes: " + "; ".join(sorted(errors)))

    text_cache = {}
    confirmed = 0
    rejected = 0
    for row in findings["findings"]:
        if row.get("status") != "present":
            continue
        row.pop("verification", None)
        finding_id = row.get("finding_id", "<no id>")
        doc_id = row.get("doc_id")
        path = sources.get(doc_id)
        if path is None:
            sys.exit(
                f"verify_finding_quotes: finding {finding_id} cites "
                f"unknown document {doc_id}"
            )
        if path not in text_cache:
            try:
                text_cache[path] = verify_quotes.normalize(
                    verify_quotes.extract_text(str(path), args.extractor)
                )
            except OSError as exc:
                sys.exit(f"verify_finding_quotes: cannot extract {path}: {exc}")
        quote = str(row.get("quote") or "")
        normalized = verify_quotes.normalize(quote)
        if normalized and normalized in text_cache[path]:
            row["quote_verification"] = {
                "checker": "deterministic-text-match",
                "status": "confirmed",
            }
            confirmed += 1
        else:
            reason = (
                "empty quote" if not normalized else "quote not found in source text"
            )
            row["status"] = "unresolved"
            row["quote_verification"] = {"reason": reason, "status": "rejected"}
            rejected += 1

    output = Path(args.out)
    output.parent.mkdir(parents=True, exist_ok=True)
    with output.open("w", encoding="utf-8", newline="\n") as handle:
        handle.write(json.dumps(findings, indent=2, sort_keys=True) + "\n")
    print(f"Wrote {args.out}: {confirmed} confirmed, {rejected} changed to unresolved")


if __name__ == "__main__":
    main()

SHA-256: dd4616b0c39dc4f823a53496dc9ee95ceca94dda457bf8d3c4c1ce6c77742aa9