← Files VeraARCHIVED FILE

modules/comunicazione-professionale/scripts/record_creative_direction_decision.py

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

↓ Download file

#!/usr/bin/env python3
"""Record a selected Creative Production direction or explicit internal fallback."""

from __future__ import annotations

import argparse
import logging
import re
from pathlib import Path
from typing import Any

from PIL import Image
from workflow_core import (
    atomic_copy_file,
    atomic_write_json,
    canonical_digest,
    file_digest,
    load_json,
    recompute_contribution_digest,
    utc_now,
    validate_input_integrity,
    validate_schema,
    workflow_lock,
)

__all__ = ["record_creative_direction_decision", "main"]

LOGGER = logging.getLogger(__name__)


def _verify_handoff(root: Path) -> tuple[dict[str, Any], dict[str, Any]]:
    workbench = load_json(root / "content_workbench.json")
    version = int(workbench["version"])
    handoff = load_json(root / "creative-direction" / f"handoff-v{version:03d}.json")
    validate_schema(handoff, "creative_direction_handoff.schema.json")
    stable = {key: value for key, value in handoff.items() if key != "handoff_digest"}
    if canonical_digest(stable) != handoff["handoff_digest"]:
        raise ValueError("Creative direction handoff digest mismatch")
    expected_binding = {
        "input_digest": workbench["input_digest"],
        "contribution_digest": recompute_contribution_digest(root),
        "visual_story_digest": canonical_digest(
            workbench["contribution"]["visual_story"]
        ),
    }
    if handoff["binding"] != expected_binding:
        raise ValueError("Creative direction handoff is stale")
    return workbench, handoff


def _snapshot_directions(
    root: Path,
    directions: list[dict[str, Any]],
    *,
    version: int,
) -> list[dict[str, Any]]:
    item_ids = [row["item_id"] for row in directions]
    if len(item_ids) != len(set(item_ids)):
        raise ValueError("Creative Production result repeats an item id")
    references = root / "creative-direction" / "references"
    references.mkdir(parents=True, exist_ok=True)
    if references.is_symlink() or not references.resolve().is_relative_to(root):
        raise ValueError("Creative Production reference directory is unsafe")
    directory = references / f"v{version:03d}"
    if directory.exists():
        raise ValueError("Creative Production direction snapshots already exist")
    prepared: list[tuple[dict[str, Any], Path, Path]] = []
    for index, row in enumerate(directions, start=1):
        source = Path(row["image_path"]).expanduser()
        source_resolved = source.resolve(strict=True)
        if source.is_symlink() or not source_resolved.is_file():
            raise ValueError("Creative Production reference must be a regular file")
        safe_item = re.sub(r"[^A-Za-z0-9_.-]+", "-", row["item_id"]).strip("-.")
        suffix = source_resolved.suffix.lower() or ".png"
        target = directory / f"direction-{index:02d}-{safe_item or 'item'}{suffix}"
        prepared.append((row, source_resolved, target))
    target_names = [target.name for _, _, target in prepared]
    if len(target_names) != len(set(target_names)):
        raise ValueError("Creative Production direction snapshot names collide")

    directory.mkdir()
    snapshots: list[dict[str, Any]] = []
    try:
        for row, source_resolved, target in prepared:
            atomic_copy_file(source_resolved, target)
            with Image.open(target) as image:
                if image.size != (1080, 1350):
                    raise ValueError(
                        "Creative Production reference must use the 1080 x 1350 target"
                    )
                image.verify()
            snapshots.append(
                {
                    "item_id": row["item_id"],
                    "item_revision": row["item_revision"],
                    "title": row["title"],
                    "snapshot_path": target.relative_to(root).as_posix(),
                    "sha256": file_digest(target),
                    "size_bytes": target.stat().st_size,
                }
            )
    except (OSError, ValueError):
        for target in directory.iterdir():
            if target.is_file() and not target.is_symlink():
                target.unlink()
        directory.rmdir()
        raise
    return snapshots


def _remove_snapshots(root: Path, snapshots: list[dict[str, Any]]) -> None:
    """Remove only snapshots created by the current failed transaction."""

    directories: set[Path] = set()
    for row in snapshots:
        target = (root / row["snapshot_path"]).resolve()
        if not target.is_relative_to(root) or target.is_symlink():
            continue
        directories.add(target.parent)
        if target.is_file():
            target.unlink()
    for directory in sorted(
        directories, key=lambda path: len(path.parts), reverse=True
    ):
        if directory.is_dir() and not any(directory.iterdir()):
            directory.rmdir()


def record_creative_direction_decision(
    run_dir: Path,
    decision_path: Path,
    *,
    recorded_by: str,
    confirmed_by_user: bool,
) -> Path:
    """Persist one user-selected direction and renderer-compatible translation."""

    if not confirmed_by_user:
        raise ValueError("Creative direction selection requires --confirmed-by-user")
    root = run_dir.resolve()
    with workflow_lock(root):
        validate_input_integrity(root)
        intake = load_json(root / "run_intake.json")
        if not intake["external_routes"]["creative_production"]["selected"]:
            raise ValueError("Creative Production was not selected for this run")
        workbench, handoff = _verify_handoff(root)
        input_decision = load_json(decision_path)
        validate_schema(
            input_decision, "creative_direction_selection_input.schema.json"
        )
        if input_decision["run_id"] != workbench["run_id"]:
            raise ValueError("Creative direction decision run_id mismatch")
        if input_decision["handoff_digest"] != handoff["handoff_digest"]:
            raise ValueError("Creative direction decision targets another handoff")
        version = int(workbench["version"])
        target = root / "creative-direction" / f"decision-v{version:03d}.json"
        if target.exists():
            raise ValueError(
                "Creative direction decision already exists for this version"
            )
        selection = input_decision["selection"]
        snapshots: list[dict[str, Any]] = []
        if input_decision["outcome"] == "selected":
            if "directions" not in selection:
                raise ValueError("Selected outcome requires board directions")
            item_ids = [row["item_id"] for row in selection["directions"]]
            if selection["selected_item_id"] not in item_ids:
                raise ValueError("Selected item is not present in the board result")
            translation = selection["translation"]
            if translation["contribution_change_required"]:
                raise ValueError(
                    "Selected direction requires a superseding contribution and fresh assessments"
                )
            stored_selection: dict[str, Any] = {
                key: selection[key]
                for key in (
                    "board_id",
                    "board_revision",
                    "selected_item_id",
                    "selection_rationale",
                    "translation",
                )
            }
            snapshots = _snapshot_directions(
                root,
                selection["directions"],
                version=version,
            )
            stored_selection["directions"] = snapshots
        else:
            if "reason" not in selection:
                raise ValueError("Fallback outcome requires a fallback reason")
            stored_selection = selection
        output: dict[str, Any] = {
            "schema_version": 1,
            "workflow": "comunicazione-professionale",
            "run_id": workbench["run_id"],
            "recorded_at": utc_now(),
            "recorded_by": recorded_by,
            "confirmed_by_user": True,
            "outcome": input_decision["outcome"],
            "binding": {
                "handoff_digest": handoff["handoff_digest"],
                **handoff["binding"],
            },
            "selection": stored_selection,
        }
        try:
            output["decision_digest"] = canonical_digest(output)
            validate_schema(output, "creative_direction_decision.schema.json")
            return atomic_write_json(target, output)
        except (OSError, ValueError):
            _remove_snapshots(root, snapshots)
            raise


def main(argv: list[str] | None = None) -> int:
    """Record a selected Creative Production direction or fallback."""

    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("--run-dir", type=Path, required=True)
    parser.add_argument("--decision", type=Path, required=True)
    parser.add_argument("--recorded-by", required=True)
    parser.add_argument("--confirmed-by-user", action="store_true")
    args = parser.parse_args(argv)
    try:
        output = record_creative_direction_decision(
            args.run_dir,
            args.decision,
            recorded_by=args.recorded_by,
            confirmed_by_user=args.confirmed_by_user,
        )
    except (OSError, ValueError, KeyError) as exc:
        LOGGER.error("CREATIVE_DIRECTION_DECISION_FAILED: %s", exc)
        return 1
    LOGGER.info("Recorded Creative Production direction decision: %s", output)
    return 0


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

SHA-256: 4bffbd20a0700c483987652cb350ebd13eb214ba02a3aeaeec82029d7c5697ba