← Files VeraARCHIVED FILE

modules/comunicazione-professionale/scripts/record_visual_assessment.py

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

↓ Download file

#!/usr/bin/env python3
"""Record a model-led assessment of exact rendered professional visuals."""

from __future__ import annotations

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

from workflow_core import (
    atomic_write_json,
    canonical_digest,
    load_json,
    prompt_template_digest,
    recompute_contribution_digest,
    utc_now,
    validate_schema,
    verify_model_phase_packet,
    verify_visual_manifest,
    verify_visual_preview_manifest,
    workflow_lock,
)

__all__ = ["record_visual_assessment", "main"]

LOGGER = logging.getLogger(__name__)


def record_visual_assessment(
    run_dir: Path,
    assessment_path: Path,
    *,
    provider: str,
    model: str,
    qa_preview: bool,
) -> Path:
    """Bind one semantic visual assessment to exact current image bytes."""

    root = run_dir.resolve()
    with workflow_lock(root):
        assessment = load_json(assessment_path)
        validate_schema(assessment, "visual_assessment.schema.json")
        protocol = assessment["assessment_protocol"]
        template_digest = prompt_template_digest(
            "visual_assessment", protocol["assessment_template_version"]
        )
        if protocol["template_sha256"] != template_digest:
            raise ValueError("Visual-assessment template digest mismatch")
        workbench = load_json(root / "content_workbench.json")
        recompute_contribution_digest(root)
        expected_state = "qa_preview" if qa_preview else "accepted_semantics"
        manifest_digest = (
            verify_visual_preview_manifest(root)
            if qa_preview
            else verify_visual_manifest(root)
        )
        phase_packet = verify_model_phase_packet(root, "visual_assessment")
        if phase_packet.get("render_state") != expected_state:
            raise ValueError("Visual model phase packet render_state mismatch")
        if phase_packet.get("manifest_digest") != manifest_digest:
            raise ValueError(
                "Visual model phase packet is stale for the current render"
            )
        if assessment["run_id"] != workbench["run_id"]:
            raise ValueError("Visual assessment run_id does not match contribution")
        if assessment["render_state"] != expected_state:
            raise ValueError("Visual assessment render_state does not match manifest")
        if assessment["assessed_manifest_digest"] != manifest_digest:
            raise ValueError("Visual assessment is stale for the current render")
        prior_sessions = {
            workbench["model_provenance"]["generator"]["session_id"],
            workbench["model_provenance"]["claim_assessor"]["assessor_session_id"],
            workbench["model_provenance"]["editorial_assessor"]["assessor_session_id"],
            workbench["model_provenance"]["editorial_assessor"][
                "qualification_session_id"
            ],
        }
        if protocol["assessor_session_id"] in prior_sessions:
            raise ValueError(
                "Visual assessment must use a host session distinct from generation, claim, editorial, and benchmark passes"
            )
        preview_record_path = root / "visual_preview_assessment_record.json"
        if not qa_preview and preview_record_path.is_file():
            preview_record = load_json(preview_record_path)
            preview_session = (
                preview_record.get("assessment", {})
                .get("assessment_protocol", {})
                .get("assessor_session_id")
            )
            if preview_session == protocol["assessor_session_id"]:
                raise ValueError(
                    "Release visual assessment must use a fresh session after preview assessment"
                )
        slide_count = len(workbench["contribution"]["visual_story"]["slides"])
        assessed_indices = [
            row["slide_index"] for row in assessment["slide_assessments"]
        ]
        if assessed_indices != list(range(1, slide_count + 1)):
            raise ValueError(
                "Visual assessment must cover every slide once and in order"
            )
        if assessment["verdict"] == "ready" and any(
            row["verdict"] in {"weak", "redundant"}
            for row in assessment["slide_assessments"]
        ):
            raise ValueError(
                "Ready visual assessment cannot contain weak or redundant slides"
            )
        manifest_name = (
            "visual_preview_manifest.json" if qa_preview else "visual_manifest.json"
        )
        manifest = load_json(root / manifest_name)
        documents = [
            (row["path"], row["layout_validation"]["page_count"])
            for row in manifest["outputs"]
            if row.get("kind") == "client_circular_pdf"
        ]
        assessed_documents = [
            (row["path"], row["assessed_page_count"])
            for row in assessment["document_assessments"]
        ]
        if assessed_documents != documents:
            raise ValueError(
                "Visual assessment must cover every rendered document page once and in order"
            )
        if assessment["verdict"] == "ready" and any(
            row["verdict"] != "ready" for row in assessment["document_assessments"]
        ):
            raise ValueError(
                "Ready visual assessment cannot contain a non-ready document"
            )
        record: dict[str, Any] = {
            "schema_version": 1,
            "workflow": "comunicazione-professionale",
            "run_id": workbench["run_id"],
            "assessment": assessment,
            "model_provenance": {
                "provider": provider,
                "model": model,
                "template_version": protocol["assessment_template_version"],
                "template_sha256": template_digest,
                "assessor_session_id": protocol["assessor_session_id"],
                "execution_mode": "isolated_host_session_attestation",
                "provider_authenticated": False,
            },
            "recorded_at": utc_now(),
        }
        record["record_digest"] = canonical_digest(record)
        output = root / (
            "visual_preview_assessment_record.json"
            if qa_preview
            else "visual_assessment_record.json"
        )
        if output.exists():
            raise ValueError(
                "Visual assessment already exists; supersede the contribution for a new exact render"
            )
        return atomic_write_json(output, record)


def main(argv: list[str] | None = None) -> int:
    """Record one exact-render visual assessment."""

    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("--run-dir", type=Path, required=True)
    parser.add_argument("--assessment", type=Path, required=True)
    parser.add_argument("--provider", required=True)
    parser.add_argument("--model", required=True)
    parser.add_argument("--qa-preview", action="store_true")
    args = parser.parse_args(argv)
    try:
        path = record_visual_assessment(
            args.run_dir,
            args.assessment,
            provider=args.provider,
            model=args.model,
            qa_preview=args.qa_preview,
        )
    except (OSError, ValueError) as exc:
        LOGGER.error("VISUAL_ASSESSMENT_RECORD_FAILED: %s", exc)
        return 1
    LOGGER.info("Recorded visual assessment: %s", path)
    return 0


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

SHA-256: d86621b311eb8bedce1414000e99289cfefbbf1123598e3a4eca1701e51e7642