← Files LegalQuants TransactionalARCHIVED FILE

skills/definition-check/scripts/normalize_terms.py

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

↓ Download file

#!/usr/bin/env python3
"""Normalize defined terms into placeholder variables from versioned ledgers.

Single document:
    normalize_terms.py agreement.docx ledger.json --out normalization.json

Multiple documents (variables become document-qualified, e.g. «A:T001»):
    normalize_terms.py --doc a.docx --ledger a.json --doc b.docx --ledger b.json --out normalization.json

Exit codes: 0 success; 20 unsupported ledger schema; 21 ledger/source binding
mismatch; 22 span integrity failure; 23 unreadable input. Argument errors use
argparse's exit code 2. The source DOCX files are never modified.
"""

from __future__ import annotations

import argparse
import json
import sys
from pathlib import Path

from _runtime_gate import require_supported_python

require_supported_python()

from definition_check.normalize import (
    NORMALIZATION_SCHEMA_VERSION,
    NormalizationError,
    normalize_pairs,
)


def build_parser() -> argparse.ArgumentParser:
    parser = argparse.ArgumentParser(
        description=(
            "Rewrite every accepted usage of a defined term into a placeholder "
            "variable, from 0.14.0 definition-check ledgers. Deterministic; "
            "never modifies the source documents."
        )
    )
    parser.add_argument("docx", nargs="?", type=Path, help="Path to the source DOCX")
    parser.add_argument(
        "ledger", nargs="?", type=Path, help="Path to the definition-check ledger JSON"
    )
    parser.add_argument(
        "--doc",
        action="append",
        default=[],
        type=Path,
        help="Source DOCX for multi-document runs; repeat once per --ledger",
    )
    parser.add_argument(
        "--ledger",
        dest="ledger_flag",
        action="append",
        default=[],
        type=Path,
        help="Ledger JSON for multi-document runs; repeat once per --doc",
    )
    parser.add_argument(
        "--out",
        required=True,
        type=Path,
        help="Output path for the normalization artifact JSON; must not exist",
    )
    return parser


def _resolve_pairs(
    parser: argparse.ArgumentParser, args: argparse.Namespace
) -> list[tuple[Path, Path]]:
    positional = args.docx is not None or args.ledger is not None
    if positional:
        if args.docx is None or args.ledger is None:
            parser.error("single-document mode requires both DOCX and LEDGER")
        if args.doc or args.ledger_flag:
            parser.error("cannot mix positional DOCX LEDGER with --doc/--ledger pairs")
        return [(args.docx, args.ledger)]
    if not args.doc and not args.ledger_flag:
        parser.error("provide DOCX LEDGER or repeated --doc/--ledger pairs")
    if len(args.doc) != len(args.ledger_flag):
        parser.error("--doc and --ledger must repeat in equal pairs")
    return list(zip(args.doc, args.ledger_flag, strict=True))


def main(argv: list[str] | None = None) -> int:
    parser = build_parser()
    args = parser.parse_args(argv)
    pairs = _resolve_pairs(parser, args)
    if args.out.exists():
        parser.error(f"output already exists: {args.out!s}")

    try:
        artifact = normalize_pairs(pairs)
    except NormalizationError as exc:
        print(
            json.dumps(
                {
                    "normalization_schema_version": NORMALIZATION_SCHEMA_VERSION,
                    "error": str(exc),
                },
                ensure_ascii=True,
            ),
            file=sys.stderr,
        )
        return exc.exit_code

    args.out.parent.mkdir(parents=True, exist_ok=True)
    args.out.write_text(
        json.dumps(artifact, indent=2, ensure_ascii=False) + "\n", encoding="utf-8"
    )
    summary = {
        "normalization_schema_version": artifact["normalization_schema_version"],
        "document_count": len(artifact["documents"]),
        "variable_count": sum(len(doc["variables"]) for doc in artifact["documents"]),
        "skipped_usage_count": sum(
            len(doc["skipped_usages"]) for doc in artifact["documents"]
        ),
        "output": str(args.out),
    }
    print(json.dumps(summary, ensure_ascii=True))
    return 0


if __name__ == "__main__":
    sys.exit(main())

SHA-256: 0d0a81e9dca24d1213b76fb8f123ddc73eeea8c8727232e352db803e12b87e6f