← Files LegalQuants TransactionalARCHIVED FILE

skills/conform/scripts/conform_preflight.py

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

↓ Download file

#!/usr/bin/env python3
"""Ledger preflight for /conform: a hard stop, not a soft warning.

Recomputes each document's content hash using the same raw-bytes SHA-256
approach `/definition-check` uses for `source.sha256`
(`../definition-check/scripts/definition_check/ooxml.py:_hash_file`,
replicated here rather than imported across a skill boundary), compares it
against each ledger's recorded `source.sha256`, and validates schema version,
run status, and review completion. Emits a structured JSON stop-reason object
on any failure and exits non-zero with a distinguishable exit code and
`stop_reason` string per failure class. Emits a small JSON success summary
otherwise.
"""

from __future__ import annotations

import argparse
import json
from hashlib import sha256
from pathlib import Path
from typing import Any

from _runtime_gate import require_supported_python

require_supported_python()

REQUIRED_SCHEMA_VERSION = "0.14.0"
COMPLETED_RUN_STATUSES = frozenset({"completed", "completed_reduced_assurance"})
REVIEW_STAGES = ("semantic_review", "occurrence_review", "reference_review")
_HASH_CHUNK_BYTES = 1024 * 1024

EXIT_CODES = {
    "missing_document": 10,
    "missing_ledger": 11,
    "stale_schema_version": 12,
    "hash_mismatch": 13,
    "incomplete_run": 14,
    "incomplete_review": 15,
}


class PreflightFailure(Exception):
    """Raised for one distinguishable, lawyer-explainable stop reason."""

    def __init__(
        self, stop_reason: str, document: str, message: str, **detail: Any
    ) -> None:
        super().__init__(message)
        self.stop_reason = stop_reason
        self.document = document
        self.message = message
        self.detail = detail

    def to_dict(self) -> dict[str, Any]:
        payload: dict[str, Any] = {
            "status": "failed",
            "stop_reason": self.stop_reason,
            "document": self.document,
            "message": self.message,
        }
        if self.detail:
            payload["detail"] = self.detail
        return payload


def _hash_file(path: Path) -> str:
    """Raw SHA-256 of file bytes, chunked identically to definition-check's ooxml._hash_file."""

    digest = sha256()
    with path.open("rb") as stream:
        for chunk in iter(lambda: stream.read(_HASH_CHUNK_BYTES), b""):
            digest.update(chunk)
    return digest.hexdigest()


def _load_ledger(role: str, ledger_path: Path) -> dict[str, Any]:
    if not ledger_path.is_file():
        raise PreflightFailure(
            "missing_ledger",
            role,
            f"No current definition check ledger found for the {role} document. "
            f"Run /definition-check on it first.",
            ledger_path=str(ledger_path),
        )
    try:
        text = ledger_path.read_text(encoding="utf-8")
    except OSError as exc:
        raise PreflightFailure(
            "missing_ledger",
            role,
            f"The {role} document's definition-check ledger could not be read.",
            ledger_path=str(ledger_path),
            error=str(exc),
        ) from exc
    try:
        ledger = json.loads(text)
    except json.JSONDecodeError as exc:
        raise PreflightFailure(
            "missing_ledger",
            role,
            f"The {role} document's definition-check ledger is not valid JSON.",
            ledger_path=str(ledger_path),
            error=str(exc),
        ) from exc
    if not isinstance(ledger, dict):
        raise PreflightFailure(
            "missing_ledger",
            role,
            f"The {role} document's definition-check ledger is not a JSON object.",
            ledger_path=str(ledger_path),
        )
    return ledger


def check_document(role: str, docx_path: str, ledger_path: str) -> dict[str, Any]:
    """Run the full preflight for one document; raise PreflightFailure on any stop condition."""

    docx = Path(docx_path)
    if not docx.is_file():
        raise PreflightFailure(
            "missing_document",
            role,
            f"The {role} document was not found at the supplied path.",
            docx_path=str(docx),
        )

    ledger = _load_ledger(role, Path(ledger_path))

    schema_version = ledger.get("schema_version")
    if schema_version != REQUIRED_SCHEMA_VERSION:
        raise PreflightFailure(
            "stale_schema_version",
            role,
            f"The {role} document's definition-check ledger uses schema "
            f"{schema_version!r}, not the current {REQUIRED_SCHEMA_VERSION}. "
            f"Re-run /definition-check to regenerate it.",
            found_schema_version=schema_version,
            required_schema_version=REQUIRED_SCHEMA_VERSION,
        )

    current_hash = _hash_file(docx)
    raw_source_block = ledger.get("source")
    source_block: dict[str, Any] = (
        raw_source_block if isinstance(raw_source_block, dict) else {}
    )
    recorded_hash = source_block.get("sha256")
    if recorded_hash != current_hash:
        raise PreflightFailure(
            "hash_mismatch",
            role,
            f"The {role} document has changed since it was last checked. "
            f"Re-run /definition-check before conforming.",
            recorded_sha256=recorded_hash,
            current_sha256=current_hash,
        )

    run_status = ledger.get("run_status")
    if run_status not in COMPLETED_RUN_STATUSES:
        raise PreflightFailure(
            "incomplete_run",
            role,
            f"The {role} document's earlier definition check did not finish "
            f"({run_status!r}). Re-run /definition-check.",
            run_status=run_status,
        )

    incomplete_stages = []
    for stage in REVIEW_STAGES:
        stage_block = ledger.get(stage)
        status = stage_block.get("status") if isinstance(stage_block, dict) else None
        if status != "complete":
            incomplete_stages.append({"stage": stage, "status": status})
    if incomplete_stages:
        raise PreflightFailure(
            "incomplete_review",
            role,
            f"The {role} document's definition check is only partly done. "
            f"/conform cannot rely on it yet; re-run /definition-check.",
            incomplete_stages=incomplete_stages,
        )

    return {
        "document_id": ledger.get("source", {}).get("document_id"),
        "name": ledger.get("source", {}).get("name"),
        "sha256": current_hash,
        "schema_version": schema_version,
        "run_status": run_status,
        "coverage": source_block.get("coverage", {}),
        "methods_run": ledger.get("methods_run", []),
        "methods_not_run": ledger.get("methods_not_run", []),
    }


def run_preflight(
    source_docx: str, source_ledger: str, core_docx: str, core_ledger: str
) -> dict[str, Any]:
    """Run the complete two-document preflight; raise PreflightFailure on any stop condition."""

    source_summary = check_document("source", source_docx, source_ledger)
    core_summary = check_document("core", core_docx, core_ledger)
    return {
        "status": "ok",
        "source": source_summary,
        "core": core_summary,
    }


def _parse_args(argv: list[str] | None) -> argparse.Namespace:
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("--source-docx", required=True)
    parser.add_argument("--source-ledger", required=True)
    parser.add_argument("--core-docx", required=True)
    parser.add_argument("--core-ledger", required=True)
    parser.add_argument(
        "--output",
        help="Optional path to also write the JSON result to, in addition to stdout.",
    )
    return parser.parse_args(argv)


def main(argv: list[str] | None = None) -> int:
    args = _parse_args(argv)
    try:
        result = run_preflight(
            args.source_docx, args.source_ledger, args.core_docx, args.core_ledger
        )
        exit_code = 0
    except PreflightFailure as exc:
        result = exc.to_dict()
        exit_code = EXIT_CODES[exc.stop_reason]

    payload = json.dumps(result, indent=2, sort_keys=True)
    print(payload)
    if args.output:
        Path(args.output).write_text(payload + "\n", encoding="utf-8")
    return exit_code


if __name__ == "__main__":
    raise SystemExit(main())

SHA-256: 4b6f6773ba4e9ad9ebc86485db6d0944bb9903d22ce7cb831785095957594be5