← Files VeraARCHIVED FILE
modules/comunicazione-professionale/scripts/prepare_model_phase.py
10.8 KB · Oct 2, 2026 · 00:29 UTC
#!/usr/bin/env python3
"""Prepare exact minimized input packets for downstream model phases."""
from __future__ import annotations
import argparse
import logging
from pathlib import Path
from typing import Any
from workflow_core import (
PLUGIN_ROOT,
atomic_write_json,
canonical_digest,
file_digest,
load_json,
model_phase_packet_path,
prompt_template_digest,
utc_now,
validate_answer_contract,
validate_claim_assurance,
validate_input_integrity,
validate_schema,
verify_history_pseudonymization,
verify_model_phase_packet,
verify_visual_manifest,
verify_visual_preview_manifest,
workflow_lock,
)
__all__ = ["prepare_model_phase", "main"]
LOGGER = logging.getLogger(__name__)
def _input_row(path: Path, role: str) -> dict[str, Any]:
"""Bind one regular file without copying its contents into the packet."""
candidate = path.expanduser()
if candidate.is_symlink():
raise ValueError(f"Model phase input must not be a symlink: {candidate}")
resolved = candidate.resolve(strict=True)
if not resolved.is_file():
raise ValueError(f"Model phase input must be a regular file: {resolved}")
return {
"role": role,
"path": str(resolved),
"sha256": file_digest(resolved),
"size_bytes": resolved.stat().st_size,
}
def _base_packet(root: Path, phase: str) -> dict[str, Any]:
intake = load_json(root / "run_intake.json")
template_version, template_path = {
"claim_assurance": (
"professional-communication-claim-assurance-v2",
PLUGIN_ROOT / "prompts" / "claim-assurance-v2.md",
),
"editorial_assessment": (
"professional-communication-editorial-v4",
PLUGIN_ROOT / "prompts" / "editorial-assessment-v4.md",
),
"visual_assessment": (
"professional-visual-editor-v2",
PLUGIN_ROOT / "prompts" / "visual-assessment-v2.md",
),
}[phase]
return {
"schema_version": 1,
"workflow": "comunicazione-professionale",
"run_id": intake["run_id"],
"input_digest": intake["input_digest"],
"phase": phase,
"created_at": utc_now(),
"history_available_to_phase": False,
"excluded_data_classes": [
"raw prior communications",
"mechanically stripped history intermediates",
"pseudonymized prior communications",
"identity mappings",
"pseudonymization and privacy-review transcripts",
],
"model_pass_template": {
"version": template_version,
"path": str(template_path.resolve()),
"sha256": prompt_template_digest(phase, template_version),
},
}
def _claim_packet(
root: Path,
*,
contribution_path: Path,
answer_contract_path: Path,
) -> dict[str, Any]:
intake = load_json(root / "run_intake.json")
source_register = load_json(root / "source_register.json")
contribution = load_json(contribution_path)
validate_schema(contribution, "model_contribution.schema.json")
answer_contract = load_json(answer_contract_path)
validate_answer_contract(
answer_contract,
intake={
"run_id": intake["run_id"],
"audience": intake["audience"],
"language": intake["language"],
"jurisdiction": intake["jurisdiction"],
},
)
packet = _base_packet(root, "claim_assurance")
packet["allowed_data_classes"] = [
"answer contract",
"proposed contribution",
"selected current source snapshots",
]
packet["allowed_inputs"] = [
_input_row(answer_contract_path, "answer_contract"),
_input_row(contribution_path, "contribution_candidate"),
*[
_input_row(Path(row["snapshot_path"]), f"source:{row['id']}")
for row in source_register["sources"]
],
_input_row(PLUGIN_ROOT / "prompts" / "claim-assurance-v2.md", "phase_prompt"),
_input_row(
PLUGIN_ROOT / "schemas" / "claim_assurance.schema.json",
"output_schema",
),
]
packet["instructions"] = [
"Open only allowed_inputs.",
"Prior communications are style evidence and are not available to claim assurance.",
"Assess source identity, semantic support, reasoning, time and modality, and professional judgment separately.",
]
return packet
def _editorial_packet(
root: Path,
*,
contribution_path: Path,
claim_assurance_path: Path,
) -> dict[str, Any]:
contribution = load_json(contribution_path)
validate_schema(contribution, "model_contribution.schema.json")
assurance = load_json(claim_assurance_path)
validate_schema(assurance, "claim_assurance.schema.json")
claim_packet = verify_model_phase_packet(root, "claim_assurance")
contract_row = next(
row
for row in claim_packet["allowed_inputs"]
if row["role"] == "answer_contract"
)
contract = load_json(Path(contract_row["path"]))
validate_claim_assurance(
assurance,
contribution=contribution,
answer_contract_digest=contract["contract_digest"],
source_register=load_json(root / "source_register.json"),
)
packet = _base_packet(root, "editorial_assessment")
packet["allowed_data_classes"] = [
"proposed contribution",
"completed claim assurance",
]
packet["allowed_inputs"] = [
_input_row(contribution_path, "contribution_candidate"),
_input_row(claim_assurance_path, "claim_assurance"),
_input_row(
PLUGIN_ROOT / "prompts" / "editorial-assessment-v4.md",
"phase_prompt",
),
_input_row(
PLUGIN_ROOT / "schemas" / "editorial_assessment.schema.json",
"output_schema",
),
]
packet["instructions"] = [
"Open only allowed_inputs.",
"Do not open selected current sources or any prior communication; claim assurance already carries the support result.",
"Assess reader value, specificity, professional limits, channels, and proposed slides independently.",
]
return packet
def _visual_packet(root: Path, *, qa_preview: bool) -> dict[str, Any]:
manifest_name = (
"visual_preview_manifest.json" if qa_preview else "visual_manifest.json"
)
manifest_digest = (
verify_visual_preview_manifest(root)
if qa_preview
else verify_visual_manifest(root)
)
manifest = load_json(root / manifest_name)
packet = _base_packet(root, "visual_assessment")
packet["render_state"] = "qa_preview" if qa_preview else "accepted_semantics"
packet["manifest_digest"] = manifest_digest
packet["allowed_data_classes"] = [
"accepted contribution and channel copy",
"exact visual manifest",
"exact rendered PNG and PDF outputs",
]
packet["allowed_inputs"] = [
_input_row(root / "content_workbench.json", "accepted_contribution"),
_input_row(root / manifest_name, "visual_manifest"),
*[
_input_row(
(
Path(row["path"])
if Path(row["path"]).is_absolute()
else root / row["path"]
),
f"render:{index:03d}",
)
for index, row in enumerate(manifest["outputs"], start=1)
],
_input_row(PLUGIN_ROOT / "prompts" / "visual-assessment-v2.md", "phase_prompt"),
_input_row(
PLUGIN_ROOT / "schemas" / "visual_assessment.schema.json",
"output_schema",
),
]
packet["instructions"] = [
"Open only allowed_inputs.",
"Do not open source files, prior communications, identity mappings, or earlier model transcripts.",
"Judge the exact rendered bytes against the accepted copy and manifest.",
]
return packet
def prepare_model_phase(
run_dir: Path,
phase: str,
*,
contribution_path: Path | None = None,
answer_contract_path: Path | None = None,
claim_assurance_path: Path | None = None,
qa_preview: bool = False,
) -> Path:
"""Write and verify one exact minimized phase packet."""
root = run_dir.resolve()
with workflow_lock(root):
validate_input_integrity(root)
verify_history_pseudonymization(root)
if phase == "claim_assurance":
if contribution_path is None or answer_contract_path is None:
raise ValueError(
"Claim assurance requires contribution and answer contract"
)
packet = _claim_packet(
root,
contribution_path=contribution_path,
answer_contract_path=answer_contract_path,
)
elif phase == "editorial_assessment":
if contribution_path is None or claim_assurance_path is None:
raise ValueError(
"Editorial assessment requires contribution and claim assurance"
)
packet = _editorial_packet(
root,
contribution_path=contribution_path,
claim_assurance_path=claim_assurance_path,
)
elif phase == "visual_assessment":
packet = _visual_packet(root, qa_preview=qa_preview)
else:
raise ValueError(f"Unsupported model phase: {phase}")
packet["packet_digest"] = canonical_digest(packet)
path = model_phase_packet_path(root, phase)
atomic_write_json(path, packet)
verify_model_phase_packet(root, phase)
return path
def main(argv: list[str] | None = None) -> int:
"""Prepare one minimized downstream model phase packet."""
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--run-dir", type=Path, required=True)
parser.add_argument(
"--phase",
choices=("claim_assurance", "editorial_assessment", "visual_assessment"),
required=True,
)
parser.add_argument("--contribution", type=Path)
parser.add_argument("--answer-contract", type=Path)
parser.add_argument("--claim-assurance", type=Path)
parser.add_argument("--qa-preview", action="store_true")
args = parser.parse_args(argv)
try:
path = prepare_model_phase(
args.run_dir,
args.phase,
contribution_path=args.contribution,
answer_contract_path=args.answer_contract,
claim_assurance_path=args.claim_assurance,
qa_preview=args.qa_preview,
)
except (OSError, ValueError) as exc:
LOGGER.error("MODEL_PHASE_PREPARATION_FAILED: %s", exc)
return 1
LOGGER.info("Prepared minimized model phase packet: %s", path)
return 0
if __name__ == "__main__":
logging.basicConfig(level=logging.INFO, format="%(message)s")
raise SystemExit(main())
SHA-256: 9514479d1043407f024af9e2333fbcc6a36f580d24ce7b6ee3ea0e44315e731d