← Files VeraARCHIVED FILE

modules/centrale-rischi-review/scripts/run_analysis.py

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

↓ Download file

#!/usr/bin/env python3
"""Calculate and render one reviewed Centrale Rischi analysis."""

from __future__ import annotations

import argparse
import hashlib
import json
import logging
import sys
from pathlib import Path

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

from centrale_rischi_core import (  # noqa: E402
    COMMENTARY_SCHEMA,
    CentraleRischiContractError,
    build_analysis,
    build_model_context,
    load_json,
    load_source_tables,
    render_html,
    render_markdown,
    sha256_file,
    write_excel,
    write_json,
)
from vera_assurance import (  # noqa: E402
    AssuranceContractError,
    load_client_engagement_context_file,
)

__all__ = ["main"]
LOGGER = logging.getLogger(__name__)


def _resolve_calculation_inputs(
    source_paths: list[Path], recipe_path: Path
) -> tuple[list[Path], list[Path]]:
    """Resolve and verify the inspected workbook for one native PDF source."""

    pdf_paths = [path for path in source_paths if path.suffix.casefold() == ".pdf"]
    if not pdf_paths:
        return source_paths, []
    if len(source_paths) != 1:
        raise CentraleRischiContractError(
            "PDF analysis accepts exactly one source document per run."
        )
    normalized_path = recipe_path.parent / "centrale_rischi_normalized.xlsx"
    normalization_receipt_path = recipe_path.parent / "pdf_normalization_receipt.json"
    recipe = load_json(recipe_path)
    receipt = load_json(normalization_receipt_path)
    source_sha256 = sha256_file(pdf_paths[0])
    if recipe.get("source_kind") != "native_pdf_extraction":
        raise CentraleRischiContractError(
            "A PDF source requires a reviewed native_pdf_extraction recipe."
        )
    if (
        recipe.get("source_document_sha256") != source_sha256
        or receipt.get("source_document_sha256") != source_sha256
    ):
        raise CentraleRischiContractError(
            "The PDF no longer matches the reviewed normalization receipt."
        )
    if (
        receipt.get("schema_version")
        != "vera.centrale_rischi_pdf_normalization_receipt.v1"
        or receipt.get("workflow_id") != "centrale-rischi-review"
        or receipt.get("normalized_workbook_sha256") != sha256_file(normalized_path)
    ):
        raise CentraleRischiContractError(
            "The normalized PDF workbook does not match its inspection receipt."
        )
    return [normalized_path], [normalized_path, normalization_receipt_path]


def main(argv: list[str] | None = None) -> int:
    """Run exact calculations and write the normal review outputs."""

    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("--input", action="append", required=True, type=Path)
    parser.add_argument("--recipe", required=True, type=Path)
    parser.add_argument("--output-dir", required=True, type=Path)
    parser.add_argument("--client-engagement", required=True, type=Path)
    args = parser.parse_args(argv)
    try:
        load_client_engagement_context_file(
            args.client_engagement,
            expected_workflow_id="centrale-rischi-review",
            input_paths=[*args.input, args.recipe],
            output_dir=args.output_dir,
        )
        calculation_inputs, derived_inputs = _resolve_calculation_inputs(
            args.input, args.recipe
        )
        if derived_inputs:
            load_client_engagement_context_file(
                args.client_engagement,
                expected_workflow_id="centrale-rischi-review",
                input_paths=derived_inputs,
                output_dir=args.output_dir,
            )
        analysis = build_analysis(
            load_source_tables(calculation_inputs), load_json(args.recipe)
        )
    except (
        AssuranceContractError,
        CentraleRischiContractError,
        OSError,
        ValueError,
    ) as exc:
        parser.error(str(exc))
    args.output_dir.mkdir(parents=True, exist_ok=True)
    analysis_path = args.output_dir / "centrale_rischi_analysis.json"
    write_json(analysis_path, analysis)
    write_json(args.output_dir / "model_context.json", build_model_context(analysis))
    (args.output_dir / "centrale_rischi_facts.md").write_text(
        render_markdown(analysis), encoding="utf-8"
    )
    (args.output_dir / "centrale_rischi_dashboard.html").write_text(
        render_html(analysis), encoding="utf-8"
    )
    write_excel(args.output_dir / "centrale_rischi_analysis.xlsx", analysis)
    analysis_sha256 = sha256_file(analysis_path)
    write_json(
        args.output_dir / "commentary_template.json",
        {
            "schema_version": COMMENTARY_SCHEMA,
            "workflow_id": "centrale-rischi-review",
            "analysis_sha256": analysis_sha256,
            "evidence_ref_contract": {
                "metric": "metric:<existing metric_id>",
                "control": "control:<existing control_id>",
                "source_row": "row:<existing source_row_locator>",
            },
            "observations": [],
            "hypotheses": [],
            "questions": [],
            "limitations": [],
        },
    )
    write_json(
        args.output_dir / "open_issues_template.json",
        {
            "schema_version": "vera.centrale_rischi_open_issues.v1",
            "workflow_id": "centrale-rischi-review",
            "analysis_sha256": analysis_sha256,
            "status": "open",
            "items": [],
            "allowed_kinds": [
                "documentary",
                "arithmetic",
                "semantic",
                "professional",
            ],
        },
    )
    output_names = (
        "centrale_rischi_analysis.json",
        "model_context.json",
        "centrale_rischi_facts.md",
        "centrale_rischi_dashboard.html",
        "centrale_rischi_analysis.xlsx",
        "commentary_template.json",
        "open_issues_template.json",
    )
    receipt = {
        "schema_version": "vera.centrale_rischi_execution_receipt.v1",
        "workflow_id": "centrale-rischi-review",
        "status": analysis["status"],
        "recipe_sha256": sha256_file(args.recipe),
        "inputs": [
            {
                "input_id": f"input_{index:03d}",
                "sha256": sha256_file(path),
                "byte_count": path.stat().st_size,
            }
            for index, path in enumerate(args.input, start=1)
        ],
        "outputs": [
            {
                "path": name,
                "sha256": sha256_file(args.output_dir / name),
                "byte_count": (args.output_dir / name).stat().st_size,
            }
            for name in output_names
        ],
        "implementation_reason": "Arithmetic, reviewed-value mapping, schema validation and content hashes are deterministic because they are mechanically verifiable; interpretation and materiality remain model-led and professional.",
    }
    receipt["content_sha256"] = hashlib.sha256(
        (
            json.dumps(
                receipt, ensure_ascii=False, sort_keys=True, separators=(",", ":")
            )
            + "\n"
        ).encode("utf-8")
    ).hexdigest()
    write_json(args.output_dir / "execution_receipt.json", receipt)
    LOGGER.info("Wrote Centrale Rischi analysis with status %s.", analysis["status"])
    return 0 if analysis["status"] != "blocked" else 2


if __name__ == "__main__":
    logging.basicConfig(level=logging.INFO, format="%(message)s")
    raise SystemExit(main())

SHA-256: b25a8f5a815094c5b9e874842213d1ef20ee145d15a079efa21f347aa717242a