← Files VeraARCHIVED FILE

modules/variance-analysis/scripts/model_use.py

14.4 KB · Oct 2, 2026 · 00:29 UTC

↓ Download file

#!/usr/bin/env python3
"""Seal and extract purpose-bound post-mapping context for Variance Analysis."""

from __future__ import annotations

# Direct CLI calls must select dependencies before importing workflow modules.
if __name__ == "__main__":
    import runpy as _runpy
    from pathlib import Path as _Path

    for _parent in _Path(__file__).resolve().parents:
        _launcher = _parent / "scripts" / "self_relaunch.py"
        if not _launcher.is_file():
            _launcher = _parent / "clara" / "scripts" / "self_relaunch.py"
        if _launcher.is_file():
            _runpy.run_path(str(_launcher))["ensure_running_in_managed_venv"](__file__)
            break
    else:
        # Standalone components retain their host's dependency setup.
        if any(
            (_p / "components.json").is_file()
            for _p in _Path(__file__).resolve().parents
        ):
            raise SystemExit(
                "Managed Python launcher is missing; rebuild the plugin package."
            )


import argparse
import hashlib
import json
import re
import sys
from collections.abc import Mapping, Sequence
from datetime import date, datetime
from pathlib import Path
from typing import Any

PLUGIN_ROOT = Path(__file__).resolve().parents[1]
for _vendor_root in (
    PLUGIN_ROOT / "vendor" / "modules",
    PLUGIN_ROOT.parent.parent / "vendor" / "modules",
    PLUGIN_ROOT.parent / "_shared" / "vendor" / "modules",
):
    if (_vendor_root / "vera_assurance").is_dir():
        if str(_vendor_root) not in sys.path:
            sys.path.insert(0, str(_vendor_root))
        break

from vera_assurance import AssuranceContractError, load_client_engagement_context_file

SCHEMA_VERSION = "vera.model_use_manifest.v1"
REQUEST_SCHEMA_VERSION = "vera.model_use_request.v1"
MANIFEST_NAME = "model_use_manifest.json"
REQUEST_DIR_NAME = "model_drilldowns"
SHA256_RE = re.compile(r"^[0-9a-f]{64}$")


class ModelUseError(ValueError):
    """Raised when a model-use manifest or drilldown request is invalid."""


def _canonical_json_sha256(value: object) -> str:
    encoded = json.dumps(
        value,
        ensure_ascii=False,
        sort_keys=True,
        separators=(",", ":"),
    ).encode("utf-8")
    return hashlib.sha256(encoded).hexdigest()


def _snapshot(path: Path) -> dict[str, Any]:
    resolved = Path(path).resolve(strict=True)
    digest = hashlib.sha256()
    byte_count = 0
    with resolved.open("rb") as handle:
        for block in iter(lambda: handle.read(1024 * 1024), b""):
            byte_count += len(block)
            digest.update(block)
    return {"byte_count": byte_count, "sha256": digest.hexdigest()}


def _mapped_columns(recipe: Mapping[str, Any]) -> list[str]:
    mappings = recipe.get("mappings")
    if not isinstance(mappings, Mapping):
        raise ModelUseError("Variance recipe mappings must be an object")
    names: list[str] = []
    for key in (
        "period_column",
        "amount_column",
        "units_column",
        "discount_column",
        "cogs_column",
        "date_column",
    ):
        value = mappings.get(key)
        if isinstance(value, str) and value:
            names.append(value)
    for key in ("dimensions", "calculation_grain"):
        values = mappings.get(key)
        if isinstance(values, list):
            names.extend(str(value) for value in values if str(value))
    return list(dict.fromkeys(names))


def write_model_use_manifest(
    *,
    input_path: Path,
    output_dir: Path,
    recipe: Mapping[str, Any],
    recipe_path: Path,
    source_population_rows: int,
    in_scope_rows: int,
    default_artifact_paths: Sequence[Path],
) -> Path:
    """Write a hash-bound post-mapping model-use manifest."""

    output_dir = Path(output_dir).resolve()
    recipe_snapshot = _snapshot(recipe_path)
    source_columns = _source_columns(input_path)
    mapped_columns = _mapped_columns(recipe)
    artifacts: list[dict[str, Any]] = []
    seen: set[str] = set()
    for artifact_path in default_artifact_paths:
        path = Path(artifact_path).resolve()
        if not path.is_file() or path.name in {MANIFEST_NAME, "variance_audit.json"}:
            continue
        try:
            relative = path.relative_to(output_dir).as_posix()
        except ValueError as exc:
            raise ModelUseError(
                "default model artifact is outside the run output"
            ) from exc
        if relative in seen or path.suffix.lower() not in {
            ".csv",
            ".json",
            ".md",
            ".xlsx",
        }:
            continue
        seen.add(relative)
        artifacts.append(
            {
                "artifact_id": path.stem,
                "path": relative,
                **_snapshot(path),
                "purpose": "mapped_result_context_or_lineage",
            }
        )
    content: dict[str, Any] = {
        "schema_version": SCHEMA_VERSION,
        "workflow_id": "variance-analysis",
        "phase": "post_semantic_mapping",
        "source_population": {
            "processing_scope": "complete_reviewed_in_scope_population",
            "source_input_rows": int(source_population_rows),
            "in_scope_rows": int(in_scope_rows),
            "source_snapshot": _snapshot(input_path),
            "source_column_count": len(source_columns),
        },
        "semantic_boundary": {
            "status": "reviewed_recipe_mapping_and_filters",
            "recipe_snapshot": recipe_snapshot,
            "mapped_columns": mapped_columns,
            "unmapped_column_count": sum(
                column not in mapped_columns for column in source_columns
            ),
            "default_source_access": "mapped_results_and_contexts_first",
            "automatic_anonymization": False,
            "automatic_pseudonymization": False,
        },
        "default_model_use": {
            "artifacts": artifacts,
            "raw_source_rows_included": False,
        },
        "evidence_drilldown": {
            "mode": "explicit_exact_filter_over_in_scope_source",
            "request_schema_version": REQUEST_SCHEMA_VERSION,
            "receipt_directory": REQUEST_DIR_NAME,
            "requires": [
                "specific_professional_question",
                "at_least_one_exact_row_filter",
                "explicit_mapped_output_columns",
                "reason",
            ],
            "allowed_columns": mapped_columns,
            "match_behavior": "all_exact_matches_no_sampling",
        },
        "runtime_account_boundary": {
            "selected_by": "firm_or_user",
            "verified_by_vera": False,
            "per_case_record_required": False,
        },
    }
    manifest = {**content, "content_sha256": _canonical_json_sha256(content)}
    path = output_dir / MANIFEST_NAME
    path.write_text(
        json.dumps(manifest, ensure_ascii=False, indent=2, sort_keys=True) + "\n",
        encoding="utf-8",
    )
    return path


def _source_columns(path: Path) -> list[str]:
    from variance_core import get_schema_and_column_names, read_table

    columns, _schema = get_schema_and_column_names(read_table(Path(path)))
    return columns


def validate_manifest(value: object) -> dict[str, Any]:
    if not isinstance(value, Mapping):
        raise ModelUseError("model-use manifest must be an object")
    manifest = dict(value)
    if manifest.get("schema_version") != SCHEMA_VERSION:
        raise ModelUseError("model-use manifest schema_version is invalid")
    if manifest.get("workflow_id") != "variance-analysis":
        raise ModelUseError("model-use manifest workflow_id is invalid")
    supplied = manifest.pop("content_sha256", None)
    if not isinstance(supplied, str) or not SHA256_RE.fullmatch(supplied):
        raise ModelUseError("model-use manifest content_sha256 is invalid")
    if _canonical_json_sha256(manifest) != supplied:
        raise ModelUseError("model-use manifest content_sha256 is stale")
    manifest["content_sha256"] = supplied
    return manifest


def _json_safe(value: Any) -> Any:
    if isinstance(value, (date, datetime)):
        return value.isoformat()
    return value


def _filters(values: Sequence[str]) -> list[tuple[str, str]]:
    filters: list[tuple[str, str]] = []
    for value in values:
        column, separator, expected = value.partition("=")
        column = column.strip()
        if not separator or not column:
            raise ModelUseError("each --where must use column=exact_value")
        filters.append((column, expected))
    return filters


def extract_source_rows(
    *,
    manifest_path: Path,
    input_path: Path,
    recipe_path: Path,
    reason: str,
    where: Sequence[str],
    columns: Sequence[str],
) -> dict[str, Any]:
    """Write all exact in-scope source matches for one bounded question."""

    from variance_core import prepare_period_comparison_buckets, read_table

    from modules.chart_harness import apply_recipe_cohorts, apply_recipe_filters

    manifest_path = Path(manifest_path).resolve()
    manifest = validate_manifest(json.loads(manifest_path.read_text(encoding="utf-8")))
    input_path = Path(input_path).resolve(strict=True)
    if _snapshot(input_path) != manifest["source_population"]["source_snapshot"]:
        raise ModelUseError("Variance source no longer matches the sealed manifest")
    reason = reason.strip()
    if not reason or len(reason) > 1_000:
        raise ModelUseError("reason must contain 1 to 1000 characters")
    exact_filters = _filters(where)
    requested_columns = list(
        dict.fromkeys(item.strip() for item in columns if item.strip())
    )
    if not exact_filters:
        raise ModelUseError("at least one --where is required")
    if not requested_columns:
        raise ModelUseError("at least one --column is required")
    if len(exact_filters) > 20 or len(requested_columns) > 30:
        raise ModelUseError("drilldown request exceeds the supported selector bounds")
    allowed = set(manifest["evidence_drilldown"]["allowed_columns"])
    referenced = {*requested_columns, *(column for column, _value in exact_filters)}
    unknown = sorted(referenced - allowed)
    if unknown:
        raise ModelUseError(
            f"drilldown columns are not in the reviewed mapping: {unknown}"
        )
    recipe = json.loads(Path(recipe_path).read_text(encoding="utf-8"))
    if not isinstance(recipe, dict):
        raise ModelUseError("used recipe must be an object")
    if _snapshot(recipe_path) != manifest["semantic_boundary"].get("recipe_snapshot"):
        raise ModelUseError("Variance recipe no longer matches the reviewed recipe")
    frame = read_table(input_path)
    source_rows = frame.height
    frame, recipe = prepare_period_comparison_buckets(frame, recipe)
    frame, _filter_audit = apply_recipe_filters(frame, recipe)
    frame, _cohort_audit = apply_recipe_cohorts(
        frame,
        recipe,
        period_column=str(recipe["mappings"]["period_column"]),
        value_column=str(recipe["mappings"]["amount_column"]),
        current_period=str(recipe["mappings"]["comparison_period"]),
        previous_period=str(recipe["mappings"]["baseline_period"]),
    )
    in_scope_rows = frame.height
    import polars as pl

    for column, expected in exact_filters:
        frame = frame.filter(pl.col(column).cast(pl.Utf8) == expected)
    matched = [
        {column: _json_safe(row[column]) for column in requested_columns}
        for row in frame.select(requested_columns).to_dicts()
    ]
    request = {
        "schema_version": REQUEST_SCHEMA_VERSION,
        "workflow_id": "variance-analysis",
        "manifest_sha256": manifest["content_sha256"],
        "reason": reason,
        "where": [
            {"column": column, "value": value} for column, value in exact_filters
        ],
        "columns": requested_columns,
    }
    request_sha256 = _canonical_json_sha256(request)
    content = {
        **request,
        "request_sha256": request_sha256,
        "full_source_rows_scanned_locally": source_rows,
        "in_scope_rows_scanned_locally": in_scope_rows,
        "matched_row_count": len(matched),
        "match_behavior": "all_exact_matches_no_sampling",
        "rows": matched,
    }
    output = {**content, "content_sha256": _canonical_json_sha256(content)}
    request_dir = manifest_path.parent / REQUEST_DIR_NAME
    request_dir.mkdir(parents=True, exist_ok=True)
    output_path = request_dir / f"source_rows_{request_sha256[:24]}.json"
    rendered = json.dumps(output, ensure_ascii=False, indent=2, sort_keys=True) + "\n"
    if output_path.exists() and output_path.read_text(encoding="utf-8") != rendered:
        raise ModelUseError("existing drilldown artifact does not match")
    if not output_path.exists():
        output_path.write_text(rendered, encoding="utf-8")
    return {
        "ok": True,
        "artifact_path": str(output_path),
        "request_sha256": request_sha256,
        "matched_row_count": len(matched),
        "full_source_rows_scanned_locally": source_rows,
        "in_scope_rows_scanned_locally": in_scope_rows,
    }


def main(argv: Sequence[str] | None = None) -> int:
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("--manifest", type=Path, required=True)
    parser.add_argument("--input", type=Path, required=True)
    parser.add_argument("--recipe", type=Path, required=True)
    parser.add_argument("--reason", required=True)
    parser.add_argument("--where", action="append", default=[])
    parser.add_argument("--column", action="append", default=[])
    parser.add_argument("--client-engagement", type=Path)
    args = parser.parse_args(argv)
    try:
        from variance_core import is_vera_managed_host

        if is_vera_managed_host() and args.client_engagement is None:
            raise ModelUseError("Vera source drilldown requires --client-engagement")
        if args.client_engagement is not None:
            load_client_engagement_context_file(
                args.client_engagement,
                expected_workflow_id="variance-analysis",
                input_paths=[args.input, args.recipe, args.manifest],
                output_dir=args.manifest.parent,
            )
        result = extract_source_rows(
            manifest_path=args.manifest,
            input_path=args.input,
            recipe_path=args.recipe,
            reason=args.reason,
            where=args.where,
            columns=args.column,
        )
    except (AssuranceContractError, ModelUseError, OSError, ValueError) as exc:
        print(json.dumps({"ok": False, "error": str(exc)}, sort_keys=True))
        return 2
    print(json.dumps(result, sort_keys=True))
    return 0


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

SHA-256: 7a80f663ca259c02cb87de06faee36484627216ef6153c43ae9d56e4c6db3ba6