← Files VeraARCHIVED FILE

modules/treasury-forecast/scripts/run_treasury.py

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

↓ Download file

"""Run or review a treasury forecast inside a bound Studio Archive engagement."""

from __future__ import annotations

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

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

from treasury_core import TreasuryError  # noqa: E402
from treasury_inputs import (  # noqa: E402
    load_inputs,
    manifest_paths,
    read_json,
    write_templates,
)
from treasury_session import (  # noqa: E402
    create_session,
    current_record,
    review_session,
    save_scenario,
)
from vera_assurance import (  # noqa: E402
    AssuranceContractError,
    load_client_engagement_context_file,
)

__all__ = ["main", "validate_context"]


def validate_context(context_path: Path, paths: list[Path] | None = None) -> dict:
    """Require a current running portable archive context on every review action."""
    context = load_client_engagement_context_file(
        context_path,
        expected_workflow_id="treasury-forecast",
        input_paths=paths or [],
    )
    if context["schema_version"] != "vera.client_workflow_context.v2":
        raise TreasuryError("Treasury requires a portable v2 client workflow")
    return context


def main(argv: list[str] | None = None) -> int:
    parser = argparse.ArgumentParser(description=__doc__)
    sub = parser.add_subparsers(dest="command", required=True)
    templates = sub.add_parser(
        "templates", help="Create empty documented CSV templates"
    )
    templates.add_argument("--output", type=Path, required=True)
    prepare = sub.add_parser(
        "prepare", help="Import the fixed tables and create a reviewable draft"
    )
    prepare.add_argument("--manifest", type=Path, required=True)
    for name in ("review", "serve", "context", "scenario"):
        command = sub.add_parser(name)
        if name in {"review", "scenario"}:
            command.add_argument("--request", type=Path, required=True)
        if name == "context":
            command.add_argument("--event-id", action="append", default=[])
        if name == "serve":
            command.add_argument("--port", type=int, default=0)
    for name, command in sub.choices.items():
        if name != "templates":
            command.add_argument("--client-engagement", type=Path, required=True)
    args = parser.parse_args(argv)
    context = None
    try:
        if args.command == "templates":
            write_templates(args.output)
            return 0
        paths = [args.manifest] if args.command == "prepare" else []
        context = validate_context(args.client_engagement, paths)
        output = Path(context["output_dir"])
        if args.command == "prepare":
            manifest = read_json(args.manifest)
            input_root = Path(context["run_root"]) / "inputs"
            sources = [args.manifest, *manifest_paths(manifest, input_root)]
            context = validate_context(args.client_engagement, sources)
            data, previous = load_inputs(
                manifest,
                input_root,
                client_id=context["client_id"],
                engagement_id=context["engagement_id"],
            )
            record = create_session(
                output,
                data,
                previous,
                source_paths=sources,
                context_path=args.client_engagement,
            )
        else:
            state, record = current_record(output)
            source_paths = [Path(row["path"]) for row in state["sources"]]
            validate_context(args.client_engagement, source_paths)
            if args.command == "serve":
                from treasury_server import serve

                serve(
                    output,
                    validate=lambda: validate_context(
                        args.client_engagement, source_paths
                    ),
                    port=args.port,
                )
                return 0
            if args.command == "review":
                request = read_json(args.request)
                record = review_session(
                    output,
                    expected_record_sha256=request["record_sha256"],
                    decisions=request.get("decisions"),
                    review=request.get("review"),
                )
            elif args.command == "context":
                selected = set(args.event_id)
                if len(selected) > 100 or selected - {
                    row["event_id"] for row in record["events"]
                }:
                    raise TreasuryError("Select at most 100 existing event IDs")
                logging.info(
                    "%s",
                    json.dumps(
                        {
                            "record_sha256": record["record_sha256"],
                            "events": [
                                row
                                for row in record["events"]
                                if row["event_id"] in selected
                            ],
                        },
                        ensure_ascii=False,
                    ),
                )
                return 0
            elif args.command == "scenario":
                save_scenario(
                    output,
                    expected_record_sha256=record["record_sha256"],
                    dates=read_json(args.request),
                )
                logging.info("Alternative saved in %s", output)
                return 0
        logging.info(
            "Treasury %s; record %s; outputs %s",
            record["status"],
            record["record_sha256"],
            output,
        )
        return 0
    except (
        TreasuryError,
        AssuranceContractError,
        OSError,
        ValueError,
        KeyError,
    ) as exc:
        logging.error("Treasury blocked: %s", exc)
        if context is not None:
            output = Path(context["output_dir"])
            output.mkdir(parents=True, exist_ok=True)
            (output / "treasury_blocked.json").write_text(
                json.dumps(
                    {
                        "workflow_id": "treasury-forecast",
                        "status": "blocked",
                        "reason": str(exc),
                    },
                    ensure_ascii=False,
                    indent=2,
                )
                + "\n",
                encoding="utf-8",
            )
        return 2


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

SHA-256: 178797a5e0ff17fad59bc8089cef1ecc130c40aa6c7ca07bf0b350a6cce62b75