← Files LegalQuants LitigationARCHIVED FILE

skills/cite-check/scripts/common/contracts.py

28.4 KB · Oct 5, 2026 · 18:33 UTC

↓ Download file

"""Contracts, validation, and safe file helpers for the cite-check runner."""

from __future__ import annotations

import json
import os
import re
import tempfile
from copy import deepcopy
from pathlib import Path
from typing import Any

PACKAGE_DIR = Path(__file__).resolve().parents[2]
SCHEMA_DIR = PACKAGE_DIR / "schemas"


class RunnerError(Exception):
    """A user-actionable runner error whose message contains no document text."""


class ManifestError(RunnerError):
    """The input manifest is invalid or unsafe."""


class RuntimeUnavailable(RunnerError):
    """A requested local runtime cannot be used in this environment."""


# These are deliberately the only validator findings that make a unit
# unreportable.  A source-quality problem belongs to one citation row and is
# represented in the normalized layer instead of turning the whole unit into a
# failed worker receipt.
HARD_ERROR_CODES = frozenset({"not_json", "wrong_unit_id", "nothing_salvageable"})


def _issue(
    code: str,
    message: str,
    *,
    unit_id: str | None = None,
    citation_index: int | None = None,
    repairable: bool = True,
) -> dict[str, Any]:
    value: dict[str, Any] = {
        "code": code,
        "message": message,
        "repairable": repairable,
    }
    if unit_id is not None:
        value["unitId"] = unit_id
    if citation_index is not None:
        value["citationIndex"] = citation_index
    return value


def _parse_worker_output(value: Any) -> tuple[Any, dict[str, Any] | None]:
    """Decode a runtime payload without guessing at non-JSON worker prose."""

    if isinstance(value, (dict, list)):
        return value, None
    if isinstance(value, str):
        try:
            return json.loads(value), None
        except json.JSONDecodeError:
            return None, _issue(
                "not_json",
                "The worker response is not JSON; repair or retry this unit.",
            )
    return None, _issue(
        "nothing_salvageable",
        "The worker response has no salvageable JSON result; retry this unit.",
    )


def _is_source_not_found(value: Any) -> bool:
    return isinstance(value, str) and value.startswith("source_not_found_")


def _citation_flags(
    citation: Any,
    index: int,
    *,
    unit_id: str,
    authority_ids: set[str],
) -> tuple[list[str], list[dict[str, Any]], list[dict[str, Any]]]:
    """Return validator-owned flags without changing worker-authored values."""

    flags: list[str] = []
    warnings: list[dict[str, Any]] = []
    potential_issues: list[dict[str, Any]] = []
    if not isinstance(citation, dict):
        return (
            ["malformed_citation"],
            [],
            [
                _issue(
                    "malformed_citation",
                    "This citation row is not an object; repair the row if possible.",
                    unit_id=unit_id,
                    citation_index=index,
                )
            ],
        )

    required = ("citation_as_written_in_unit", "matched_citation")
    if any(
        not isinstance(citation.get(name), str) or not citation[name]
        for name in required
    ):
        flags.append("malformed_citation")
        potential_issues.append(
            _issue(
                "malformed_citation",
                (
                    "This citation row is missing its written or matched citation; "
                    "repair it if possible."
                ),
                unit_id=unit_id,
                citation_index=index,
            )
        )

    source_id = citation.get("matched_source_id")
    if "matched_source_id" not in citation:
        flags.append("malformed_source_id")
        potential_issues.append(
            _issue(
                "malformed_source_id",
                (
                    "The matched source identifier is missing; use a supplied "
                    "source ID or null."
                ),
                unit_id=unit_id,
                citation_index=index,
            )
        )
    source_id_present = isinstance(source_id, str) and bool(source_id)
    resolution = citation.get("source_resolution")
    if "source_resolution" not in citation:
        flags.append("malformed_citation")
        warnings.append(
            _issue(
                "malformed_citation",
                "The source-resolution outcome is missing; repair the evidence fields.",
                unit_id=unit_id,
                citation_index=index,
            )
        )
    if resolution is not None and source_id_present != (
        resolution == "matched_supplied_source"
    ):
        flags.append("source_resolution_inconsistent")
        warnings.append(
            _issue(
                "source_resolution_inconsistent",
                (
                    "matched_source_id must be non-null exactly when "
                    "source_resolution is matched_supplied_source; consider "
                    "repairing the source receipt."
                ),
                unit_id=unit_id,
                citation_index=index,
            )
        )
    indicators = citation.get("fabrication_indicators")
    if "fabrication_indicators" not in citation:
        flags.append("malformed_citation")
        warnings.append(
            _issue(
                "malformed_citation",
                (
                    "The fabrication-indicator list is missing; repair the "
                    "evidence fields."
                ),
                unit_id=unit_id,
                citation_index=index,
            )
        )
    if (
        isinstance(indicators, list)
        and "not_found_in_external_search" in indicators
        and resolution != "not_supplied_not_found_potential_hallucination"
    ):
        flags.append("source_resolution_indicator_inconsistent")
        potential_issues.append(
            _issue(
                "source_resolution_indicator_inconsistent",
                (
                    "The external-search indicator conflicts with the declared "
                    "source resolution; repair the search outcome fields."
                ),
                unit_id=unit_id,
                citation_index=index,
            )
        )
    if source_id is not None and (not isinstance(source_id, str) or not source_id):
        flags.append("malformed_source_id")
        potential_issues.append(
            _issue(
                "malformed_source_id",
                "The matched source identifier is not usable; repair it if possible.",
                unit_id=unit_id,
                citation_index=index,
            )
        )
    source_in_universe = isinstance(source_id, str) and source_id in authority_ids
    if isinstance(source_id, str) and source_id not in authority_ids:
        flags.append("source_not_in_authority_universe")
        flags.append("source_not_found")
        warnings.append(
            _issue(
                "source_not_in_authority_universe",
                (
                    "The worker named a source outside the supplied authority "
                    "universe; consider repairing the source match."
                ),
                unit_id=unit_id,
                citation_index=index,
            )
        )

    characterization = citation.get("accuracy_of_source_characterization")
    pincite = citation.get("pincite_accuracy")
    quotation = citation.get("accuracy_of_direct_quotation")
    source_claimed_found = (
        source_in_universe
        and not _is_source_not_found(characterization)
        and not _is_source_not_found(pincite)
        and not _is_source_not_found(quotation)
    )
    if source_claimed_found:
        if (
            not isinstance(citation.get("source_excerpt"), str)
            or not citation["source_excerpt"].strip()
        ):
            flags.append("missing_excerpt")
            warnings.append(
                _issue(
                    "missing_excerpt",
                    (
                        "The source was reported as found but no excerpt was "
                        "supplied; consider repairing the receipt before relying "
                        "on it."
                    ),
                    unit_id=unit_id,
                    citation_index=index,
                )
            )
        if (
            not isinstance(citation.get("source_locator"), str)
            or not citation["source_locator"].strip()
        ):
            flags.append("missing_locator")
            warnings.append(
                _issue(
                    "missing_locator",
                    (
                        "The source was reported as found but no stable locator "
                        "was supplied; consider repairing the receipt before "
                        "relying on it."
                    ),
                    unit_id=unit_id,
                    citation_index=index,
                )
            )
    elif (
        source_id is None
        or not authority_ids
        or _is_source_not_found(characterization)
        or _is_source_not_found(pincite)
        or _is_source_not_found(quotation)
    ):
        flags.append("source_not_found")
        warnings.append(
            _issue(
                "source_not_found",
                (
                    "The source could not be verified from the supplied authority "
                    "universe; consider repairing the source receipt or leave it "
                    "claimed but unverified."
                ),
                unit_id=unit_id,
                citation_index=index,
            )
        )

    if pincite == "pincite_inaccurate":
        flags.append("pincite_inaccurate")
        potential_issues.append(
            _issue(
                "pincite_inaccurate",
                (
                    "The worker marked the pinpoint inaccurate; consider repairing "
                    "the citation or confirm the warning with the source."
                ),
                unit_id=unit_id,
                citation_index=index,
            )
        )
    if characterization in {
        "potentially_unfair_or_unreasonable_characterization_of_source",
        "objectively_false_or_unreasonable_characterization_of_source",
    }:
        flags.append("characterization_issue")
        potential_issues.append(
            _issue(
                "characterization_issue",
                (
                    "The worker identified a potentially unfair or objectively "
                    "unreasonable source characterization; consider repairing or "
                    "escalating this citation."
                ),
                unit_id=unit_id,
                citation_index=index,
            )
        )
    if quotation in {
        "quotation_technically_accurate_but_misleading_or_unfair",
        "quotation_objectively_inaccurate",
    }:
        flags.append("quotation_issue")
        potential_issues.append(
            _issue(
                "quotation_issue",
                (
                    "The worker identified a quotation accuracy or fairness "
                    "problem; consider repairing or escalating this citation."
                ),
                unit_id=unit_id,
                citation_index=index,
            )
        )
    return list(dict.fromkeys(flags)), warnings, potential_issues


def normalize_unit_result(
    value: Any,
    expected_unit_id: str,
    *,
    authority_ids: set[str] | None = None,
) -> dict[str, Any] | None:
    """Build the report-facing copy while preserving every worker field.

    The returned object preserves the worker result at the top level while
    citation rows carry only validator-owned ``validation_flags`` in addition
    to the worker's original fields.  No worker value is corrected or silently
    replaced; raw attempts are retained by the orchestration receipt layer.
    """

    candidate, parse_error = _parse_worker_output(value)
    if parse_error is not None or not isinstance(candidate, dict):
        return None
    if candidate.get("unitId") != expected_unit_id:
        return None
    raw_citations = candidate.get("citations")
    if raw_citations is None and candidate.get("disposition") == "no_citations_found":
        raw_citations = []
    if not isinstance(raw_citations, list):
        return None
    citations: list[dict[str, Any]] = []
    warnings: list[dict[str, Any]] = []
    potential_issues: list[dict[str, Any]] = []
    authority_ids = authority_ids or set()
    for index, citation in enumerate(raw_citations):
        if isinstance(citation, dict):
            row = deepcopy(citation)
        else:
            row = {"worker_value": deepcopy(citation)}
        flags, row_warnings, row_issues = _citation_flags(
            citation,
            index,
            unit_id=expected_unit_id,
            authority_ids=authority_ids,
        )
        row["validation_flags"] = flags
        citations.append(row)
        warnings.extend(row_warnings)
        potential_issues.extend(row_issues)
    disposition = candidate.get("disposition")
    if disposition not in {"no_citations_found", "citations_found"}:
        disposition = "citations_found" if citations else "no_citations_found"
        warnings.append(
            _issue(
                "wrapper_repair_disposition",
                (
                    "The worker disposition was not usable; the normalized "
                    "wrapper derived it from the citation rows and recorded the "
                    "repair."
                ),
                unit_id=expected_unit_id,
            )
        )
    if disposition == "no_citations_found" and citations:
        disposition = "citations_found"
        warnings.append(
            _issue(
                "wrapper_repair_disposition",
                (
                    "The worker reported no citations while returning citation "
                    "rows; the normalized wrapper retained the rows and recorded "
                    "the repair."
                ),
                unit_id=expected_unit_id,
            )
        )
    if disposition == "citations_found" and not citations:
        warnings.append(
            _issue(
                "empty_citation_rows",
                (
                    "The worker reported citations but returned no rows; consider "
                    "a targeted receipt repair."
                ),
                unit_id=expected_unit_id,
            )
        )
    normalized = deepcopy(candidate)
    normalized["unitId"] = expected_unit_id
    normalized["disposition"] = disposition
    normalized["citations"] = citations
    normalized["validation"] = {
        "warnings": warnings,
        "potential_issues": potential_issues,
        "hard_errors": [],
    }
    return normalized


def validate_unit_result(
    value: Any,
    expected_unit_id: str,
    *,
    authority_ids: set[str] | None = None,
) -> dict[str, Any]:
    """Return advisory validation and the three report-blocking hard errors."""

    candidate, parse_error = _parse_worker_output(value)
    hard_errors: list[dict[str, Any]] = []
    if parse_error is not None:
        hard_errors.append(parse_error)
    elif not isinstance(candidate, dict):
        hard_errors.append(
            _issue(
                "nothing_salvageable",
                "The worker response has no salvageable unit result; retry this unit.",
            )
        )
    elif candidate.get("unitId") != expected_unit_id:
        hard_errors.append(
            _issue(
                "wrong_unit_id",
                (
                    "The worker returned a different unit ID; retry this unit "
                    "with the original assignment."
                ),
                unit_id=expected_unit_id,
            )
        )
    else:
        citations = candidate.get("citations")
        salvageable = isinstance(citations, list) and (
            bool(citations) or candidate.get("disposition") == "no_citations_found"
        )
        if not salvageable:
            hard_errors.append(
                _issue(
                    "nothing_salvageable",
                    (
                        "The worker object does not contain salvageable citation "
                        "rows or a citation-free disposition; retry this unit."
                    ),
                    unit_id=expected_unit_id,
                )
            )
    normalized = (
        normalize_unit_result(
            candidate,
            expected_unit_id,
            authority_ids=authority_ids,
        )
        if not hard_errors
        else None
    )
    validation = (
        normalized["validation"]
        if normalized is not None
        else {
            "warnings": [],
            "potential_issues": [],
            "hard_errors": hard_errors,
        }
    )
    validation["hard_errors"] = hard_errors
    return {
        "normalized": normalized,
        "warnings": list(validation["warnings"]),
        "potential_issues": list(validation["potential_issues"]),
        "hard_errors": hard_errors,
        "retryable": bool(hard_errors),
        "salvageable": normalized is not None,
    }


def _load_json(path: Path, label: str) -> Any:
    try:
        return json.loads(path.read_text(encoding="utf-8"))
    except OSError as exc:
        raise RunnerError(f"cannot read {label}: {exc.__class__.__name__}") from exc
    except json.JSONDecodeError as exc:
        raise RunnerError(f"invalid JSON in {label} at line {exc.lineno}") from exc


def _schema_error(path: str, message: str) -> str:
    return f"{path or '$'}: {message}"


def _resolve_schema_ref(schema: dict[str, Any], reference: str) -> dict[str, Any]:
    if reference.startswith("#/$defs/"):
        name = reference.removeprefix("#/$defs/")
        value = schema.get("$defs", {}).get(name)
        if isinstance(value, dict):
            return value
    raise ValueError(f"unsupported schema reference {reference}")


def _load_external_schema(reference: str) -> dict[str, Any]:
    """Load one sibling packaged schema referenced by a contract."""

    path = Path(reference)
    if (
        path.name != reference
        or path.suffix != ".json"
        or not reference.endswith(".schema.json")
    ):
        raise ValueError(f"unsupported schema reference {reference}")
    value = _load_json(SCHEMA_DIR / reference, reference)
    if not isinstance(value, dict):
        raise ValueError(f"schema reference {reference} is not an object")
    return value


def _json_unique(items: list[Any]) -> bool:
    try:
        keys = [
            json.dumps(item, sort_keys=True, separators=(",", ":")) for item in items
        ]
    except (TypeError, ValueError):
        return False
    return len(keys) == len(set(keys))


def validate_schema(
    instance: Any,
    schema: dict[str, Any],
    *,
    root: dict[str, Any] | None = None,
    path: str = "",
) -> list[str]:
    """Validate the strict JSON Schema subset used by packaged contracts."""

    root = schema if root is None else root
    if "$ref" in schema:
        reference = schema["$ref"]
        if not isinstance(reference, str):
            return [_schema_error(path, "schema reference must be a string")]
        if not reference.startswith("#/"):
            try:
                external = _load_external_schema(reference)
            except (RunnerError, ValueError) as exc:
                return [_schema_error(path, str(exc))]
            return validate_schema(instance, external, root=external, path=path)
        try:
            return validate_schema(
                instance,
                _resolve_schema_ref(root, reference),
                root=root,
                path=path,
            )
        except ValueError as exc:
            return [_schema_error(path, str(exc))]
    errors: list[str] = []
    if "const" in schema and instance != schema["const"]:
        errors.append(_schema_error(path, f"expected {schema['const']!r}"))
    if "enum" in schema and instance not in schema["enum"]:
        errors.append(_schema_error(path, "value is not an allowed enum member"))
    if "type" in schema:
        expected = schema["type"]
        types = expected if isinstance(expected, list) else [expected]
        valid_type = any(
            (kind == "object" and isinstance(instance, dict))
            or (kind == "array" and isinstance(instance, list))
            or (kind == "string" and isinstance(instance, str))
            or (kind == "boolean" and isinstance(instance, bool))
            or (kind == "null" and instance is None)
            or (
                kind == "integer"
                and isinstance(instance, int)
                and not isinstance(instance, bool)
            )
            or (
                kind == "number"
                and isinstance(instance, (int, float))
                and not isinstance(instance, bool)
            )
            for kind in types
        )
        if not valid_type:
            errors.append(_schema_error(path, f"expected type {expected!r}"))
            return errors
    if isinstance(instance, str):
        if len(instance) < schema.get("minLength", 0):
            errors.append(_schema_error(path, "string is shorter than minLength"))
        if "maxLength" in schema and len(instance) > schema["maxLength"]:
            errors.append(_schema_error(path, "string is longer than maxLength"))
        if "pattern" in schema:
            try:
                if re.search(schema["pattern"], instance) is None:
                    errors.append(_schema_error(path, "string does not match pattern"))
            except re.error:
                errors.append(_schema_error(path, "schema pattern is invalid"))
    if (
        isinstance(instance, (int, float))
        and not isinstance(instance, bool)
        and "minimum" in schema
        and instance < schema["minimum"]
    ):
        errors.append(_schema_error(path, "number is below minimum"))
    if isinstance(instance, list):
        if len(instance) < schema.get("minItems", 0):
            errors.append(_schema_error(path, "array is shorter than minItems"))
        if "maxItems" in schema and len(instance) > schema["maxItems"]:
            errors.append(_schema_error(path, "array is longer than maxItems"))
        if schema.get("uniqueItems") and not _json_unique(instance):
            errors.append(_schema_error(path, "array items must be unique"))
        if isinstance(schema.get("items"), dict):
            for index, item in enumerate(instance):
                errors.extend(
                    validate_schema(
                        item, schema["items"], root=root, path=f"{path}[{index}]"
                    )
                )
    if isinstance(instance, dict):
        properties = schema.get("properties", {})
        for name in schema.get("required", []):
            if name not in instance:
                errors.append(
                    _schema_error(path, f"missing required property {name!r}")
                )
        if schema.get("additionalProperties") is False:
            unknown = sorted(set(instance) - set(properties))
            errors.extend(
                _schema_error(path, f"unknown property {name!r}") for name in unknown
            )
        for name, child_schema in properties.items():
            if name in instance:
                errors.extend(
                    validate_schema(
                        instance[name], child_schema, root=root, path=f"{path}.{name}"
                    )
                )
    if "anyOf" in schema:
        alternatives = [
            validate_schema(instance, option, root=root, path=path)
            for option in schema["anyOf"]
        ]
        if not any(not option_errors for option_errors in alternatives):
            errors.append(_schema_error(path, "does not satisfy anyOf"))
    if "allOf" in schema:
        for option in schema["allOf"]:
            errors.extend(validate_schema(instance, option, root=root, path=path))
    return errors


def _safe_relative(root: Path, value: Any, label: str) -> Path:
    if (
        not isinstance(value, str)
        or not value
        or Path(value).is_absolute()
        or Path(value).drive
    ):
        raise ManifestError(f"unsafe {label} path")
    candidate = (root / value).resolve()
    try:
        candidate.relative_to(root.resolve())
    except ValueError as exc:
        raise ManifestError(f"unsafe {label} path") from exc
    return candidate


def _manifest_paths(manifest: dict[str, Any], root: Path) -> set[Path]:
    path_labels = [
        (manifest["target"]["path"], "target"),
        (manifest["target"]["fullDocumentRef"], "target document"),
    ]
    path_labels.extend(
        (authority["path"], "authority") for authority in manifest["authorities"]
    )
    path_labels.extend(
        (unit["path"], f"unit {unit['unitId']}") for unit in manifest["units"]
    )
    paths: set[Path] = set()
    for value, label in path_labels:
        candidate = _safe_relative(root, value, label)
        try:
            readable = candidate.is_file() and os.access(candidate, os.R_OK)
        except OSError:
            readable = False
        if not readable:
            raise ManifestError(f"{label} source file is missing or unreadable")
        paths.add(candidate)
    return paths


def _validate_manifest(manifest: Any, root: Path) -> dict[str, Any]:
    schema = _load_json(SCHEMA_DIR / "manifest.schema.json", "manifest schema")
    errors = validate_schema(manifest, schema)
    if errors:
        raise ManifestError("manifest schema invalid: " + "; ".join(errors[:4]))
    assert isinstance(manifest, dict)
    _manifest_paths(manifest, root)
    units = manifest["units"]
    authorities = manifest["authorities"]
    unit_ids = [unit["unitId"] for unit in units]
    if len(unit_ids) != len(set(unit_ids)):
        raise ManifestError("manifest contains duplicate unit IDs")
    if not unit_ids:
        raise ManifestError("manifest contains no prepared units")
    unit_id_set = set(unit_ids)
    for unit in units:
        if unit["lineStart"] > unit["lineEnd"]:
            raise ManifestError(f"unit {unit['unitId']} line range is reversed")
        anchor_id = unit.get("footnoteAnchorUnitId")
        if anchor_id is not None and anchor_id not in unit_id_set:
            raise ManifestError(f"unit {unit['unitId']} has an unknown footnote anchor")
    source_ids = [authority["sourceId"] for authority in authorities]
    if len(source_ids) != len(set(source_ids)):
        raise ManifestError("manifest contains duplicate authority IDs")
    return manifest


def _lexical_absolute(path: Path) -> Path:
    """Normalize a path without following any filesystem symlink."""

    return Path(os.path.abspath(os.fspath(path)))


def _ensure_output_parent(path: Path, output_root: Path) -> Path:
    """Create a receipt parent while rejecting symlinked directories."""

    lexical_path = _lexical_absolute(path)
    lexical_root = _lexical_absolute(output_root)
    try:
        relative_parent = lexical_path.parent.relative_to(lexical_root)
    except ValueError as exc:
        raise RunnerError(
            "output path must stay inside the approved output root"
        ) from exc

    current = lexical_root
    if current.is_symlink():
        raise RunnerError("output path contains a symlinked directory")
    for component in relative_parent.parts:
        current /= component
        if current.is_symlink():
            raise RunnerError("output path contains a symlinked directory")
        current.mkdir(exist_ok=True)
        if not current.is_dir():
            raise RunnerError("output path parent is not a directory")

    try:
        lexical_path.parent.resolve().relative_to(lexical_root.resolve())
    except ValueError as exc:
        raise RunnerError(
            "output path resolves outside the approved output root"
        ) from exc
    return lexical_path


def _write_json(
    path: Path,
    value: Any,
    source_paths: set[Path],
    *,
    output_root: Path | None = None,
) -> None:
    """Atomically write JSON without traversing nested output symlinks."""

    lexical_path = _lexical_absolute(path)
    root = output_root or lexical_path.parent
    path = _ensure_output_parent(lexical_path, root)
    if path in {_lexical_absolute(source) for source in source_paths}:
        raise RunnerError("output path overlaps a supplied source")
    temporary: Path | None = None
    try:
        descriptor, name = tempfile.mkstemp(
            prefix=f".{path.name}.tmp-", dir=str(path.parent)
        )
        temporary = Path(name)
        with os.fdopen(descriptor, "w", encoding="utf-8") as stream:
            stream.write(json.dumps(value, ensure_ascii=False, indent=2) + "\n")
        os.replace(temporary, path)
    finally:
        if temporary is not None:
            try:
                temporary.unlink()
            except FileNotFoundError:
                pass

SHA-256: 6361fc30cc030d856e1038c8bd2c39fd4540247e7497914ac89e9a85e876fc38