"""Replayable controlled-apply receipts with bounded DB readback.""" from __future__ import annotations import hashlib import json from pathlib import Path from typing import Any from services.pchome_mapping_backlog.artifacts import ( _canonical_retry_exception_artifact_bytes, _resolve_retry_exception_artifact_path, ) from services.pchome_mapping_backlog.controlled_apply_executor import ( _fetch_pchome_product_match_by_momo_icode, ) from services.pchome_mapping_backlog.policies import ( DIRECT_MAPPING_RETRY_CANDIDATE_EXCEPTION_CONTROLLED_APPLY_RECEIPT_REPLAY_POLICY, ) def _find_retry_exception_artifact_file(root: Path, subdir: str, run_id: str | None = None) -> Path | None: artifact_dir = root / "artifacts" / "pchome_growth" / "retry_exception_closeout" / subdir if run_id: candidate = artifact_dir / f"{run_id}.json" return candidate if candidate.exists() else None candidates = sorted( artifact_dir.glob("*.json"), key=lambda path: path.stat().st_mtime, reverse=True, ) return candidates[0] if candidates else None def _load_retry_exception_json_artifact(path: Path | None) -> dict[str, Any]: if path is None or not path.exists(): return {} try: payload = json.loads(path.read_text(encoding="utf-8")) except (OSError, json.JSONDecodeError): return {} return payload if isinstance(payload, dict) else {} def _retry_exception_controlled_apply_receipt_replay_id( run_id: str, post_apply_readbacks: list[dict[str, Any]], ) -> str: payload = { "run_id": run_id, "post_apply_readbacks": post_apply_readbacks, } digest = hashlib.sha256( json.dumps(payload, ensure_ascii=False, sort_keys=True, default=str).encode("utf-8") ).hexdigest()[:16] return f"pchome-retry-exception-controlled-apply-receipt-replay-{digest}" def build_pchome_direct_mapping_retry_candidate_exception_controlled_apply_receipt_replay_package( *, artifact_root: str | Path | None = None, run_id: str | None = None, materialize_artifacts: bool = False, engine: Any = None, ) -> dict[str, Any]: """Rebuild a machine-verifiable executor closeout receipt from artifacts and DB readback.""" root = Path(artifact_root) if artifact_root is not None else Path.cwd() / "data" verifier_path = _find_retry_exception_artifact_file(root, "verifier_inputs", run_id) identity_path = _find_retry_exception_artifact_file(root, "identity_readback", run_id) preflight_path = _find_retry_exception_artifact_file(root, "controlled_apply_preflight", run_id) verifier_payload = _load_retry_exception_json_artifact(verifier_path) identity_payload = _load_retry_exception_json_artifact(identity_path) preflight_payload = _load_retry_exception_json_artifact(preflight_path) effective_run_id = ( run_id or verifier_payload.get("run_id") or identity_payload.get("run_id") or preflight_payload.get("run_id") or "" ) receipts = list(verifier_payload.get("no_write_verifier_receipts") or []) target_selectors = [ { "selector_id": receipt.get("receipt_id"), "source_receipt_id": receipt.get("receipt_id"), "source_closeout_receipt_id": receipt.get("source_closeout_receipt_id"), "momo_product_id": (receipt.get("subject") or {}).get("momo_product_id"), "momo_product_name": (receipt.get("subject") or {}).get("momo_product_name"), "momo_price": (receipt.get("subject") or {}).get("momo_price"), "target_pchome_product_id": (receipt.get("subject") or {}).get("target_pchome_product_id"), "target_pchome_product_name": (receipt.get("subject") or {}).get("pchome_product_name"), } for receipt in receipts if (receipt.get("subject") or {}).get("momo_product_id") ] missing_artifacts = [ name for name, payload in ( ("verifier_inputs", verifier_payload), ("identity_readback", identity_payload), ("controlled_apply_preflight", preflight_payload), ) if not payload ] post_apply_readbacks: list[dict[str, Any]] = [] if engine is not None and target_selectors: with engine.connect() as conn: for selector in target_selectors: momo_icode = str(selector.get("momo_product_id") or "").strip() expected_pchome_id = str(selector.get("target_pchome_product_id") or "").strip() row = _fetch_pchome_product_match_by_momo_icode(conn, momo_icode) actual_pchome_id = str((row or {}).get("pchome_id") or "").strip() post_apply_readbacks.append({ "selector_id": selector.get("selector_id"), "momo_icode": momo_icode, "expected_pchome_id": expected_pchome_id, "actual_pchome_id": actual_pchome_id, "expected_momo_name": selector.get("momo_product_name"), "actual_momo_name": (row or {}).get("momo_name"), "passed": bool(row) and actual_pchome_id == expected_pchome_id, "writes_database": False, }) readback_pass_count = sum(1 for item in post_apply_readbacks if item.get("passed")) receipt_ready = ( not missing_artifacts and bool(effective_run_id) and bool(target_selectors) and engine is not None and readback_pass_count == len(target_selectors) ) if missing_artifacts: result = "WAITING_FOR_RETRY_EXCEPTION_CONTROLLED_APPLY_REPLAY_ARTIFACTS" elif engine is None: result = "DIRECT_MAPPING_RETRY_EXCEPTION_CONTROLLED_APPLY_RECEIPT_REPLAY_BLOCKED_ENGINE_REQUIRED" elif not target_selectors: result = "WAITING_FOR_RETRY_EXCEPTION_CONTROLLED_APPLY_REPLAY_SELECTORS" elif receipt_ready: result = "DIRECT_MAPPING_RETRY_EXCEPTION_CONTROLLED_APPLY_RECEIPT_REPLAYED" else: result = "DIRECT_MAPPING_RETRY_EXCEPTION_CONTROLLED_APPLY_RECEIPT_REPLAY_READBACK_MISMATCH" receipt_id = _retry_exception_controlled_apply_receipt_replay_id(effective_run_id, post_apply_readbacks) summary = { "source_receipt_count": len(receipts), "target_selector_count": len(target_selectors), "post_apply_readback_count": len(post_apply_readbacks), "post_apply_readback_pass_count": readback_pass_count, "post_apply_readback_fail_count": len(post_apply_readbacks) - readback_pass_count, "missing_artifact_count": len(missing_artifacts), "executor_receipt_ready_count": 1 if receipt_ready else 0, "executor_receipt_materialized_count": 0, "executor_receipt_hash_match_count": 0, "writes_database_count": 0, } safety = { "ai_controlled_apply": True, "receipt_replay": True, "reads_artifact_files": True, "reads_database": engine is not None, "writes_database": False, "writes_database_count": 0, "writes_artifact_count": 0, "syncs_external_offers": False, "dispatches_telegram": False, "gemini_allowed": False, "requires_production_version_truth": True, } rollback_plan = { "rollback_step_count": 1 if receipt_ready else 0, "rollback_steps": [ { "action": "delete_materialized_executor_replay_receipt", "receipt_id": receipt_id, "executes_in_replay": False, "writes_database": False, } ] if receipt_ready else [], "writes_database": False, } receipt_relative_path = ( f"artifacts/pchome_growth/retry_exception_closeout/" f"controlled_apply_executor_replay/{receipt_id}.json" ) receipt_payload = { "artifact_key": "retry_exception_controlled_apply_executor_replay_receipt", "receipt_id": receipt_id, "run_id": effective_run_id, "source_policy": DIRECT_MAPPING_RETRY_CANDIDATE_EXCEPTION_CONTROLLED_APPLY_RECEIPT_REPLAY_POLICY, "source_artifacts": { "verifier_inputs": str(verifier_path.relative_to(root)) if verifier_path else None, "identity_readback": str(identity_path.relative_to(root)) if identity_path else None, "controlled_apply_preflight": str(preflight_path.relative_to(root)) if preflight_path else None, }, "result": result, "summary": summary, "identity_readback": { "momo_product_ids": identity_payload.get("momo_product_ids") or [], "target_pchome_product_ids": identity_payload.get("target_pchome_product_ids") or [], "identity_delta_status": identity_payload.get("identity_delta_status"), }, "target_selectors": target_selectors, "post_apply_readbacks": post_apply_readbacks, "rollback_plan": rollback_plan, "safety": safety, } receipt_bytes = _canonical_retry_exception_artifact_bytes(receipt_payload) receipt_artifact = { "key": "retry_exception_controlled_apply_executor_replay_receipt", "artifact_type": "controlled_apply_executor_replay_receipt", "relative_path": receipt_relative_path, "payload_sha256": hashlib.sha256(receipt_bytes).hexdigest(), "byte_count": len(receipt_bytes), "payload": receipt_payload, "materialized": False, "writes_database": False, } materialized_artifacts: list[dict[str, Any]] = [] if materialize_artifacts and receipt_ready: target_path = _resolve_retry_exception_artifact_path(root, receipt_relative_path) target_path.parent.mkdir(parents=True, exist_ok=True) target_path.write_bytes(receipt_bytes) materialized_artifacts.append({ "key": receipt_artifact["key"], "relative_path": receipt_relative_path, "absolute_path": str(target_path), "payload_sha256": receipt_artifact["payload_sha256"], "written_byte_count": target_path.stat().st_size, "writes_database": False, }) receipt_artifact["materialized"] = True receipt_artifact["absolute_path"] = str(target_path) receipt_path = _resolve_retry_exception_artifact_path(root, receipt_relative_path) actual_sha = hashlib.sha256(receipt_path.read_bytes()).hexdigest() if receipt_path.exists() else "" verifier_checks = [ {"check": "source_artifacts_loaded", "passed": not missing_artifacts, "missing_artifacts": missing_artifacts}, {"check": "target_selectors_present", "passed": bool(target_selectors)}, {"check": "post_apply_readbacks_all_passed", "passed": readback_pass_count == len(target_selectors)}, { "check": "receipt_materialized_when_requested", "passed": (not materialize_artifacts) or (receipt_ready and receipt_path.exists()), }, { "check": "receipt_hash_matches_expected", "passed": (not materialize_artifacts) or (bool(actual_sha) and actual_sha == receipt_artifact["payload_sha256"]), }, {"check": "replay_does_not_write_database", "passed": safety["writes_database"] is False}, ] existing_receipt_hash_match = bool(actual_sha) and actual_sha == receipt_artifact["payload_sha256"] summary["executor_receipt_materialized_count"] = len(materialized_artifacts) or (1 if existing_receipt_hash_match else 0) summary["executor_receipt_hash_match_count"] = 1 if existing_receipt_hash_match else 0 summary["post_executor_receipt_verifier_check_count"] = len(verifier_checks) safety["writes_artifact_count"] = len(materialized_artifacts) return { "policy": DIRECT_MAPPING_RETRY_CANDIDATE_EXCEPTION_CONTROLLED_APPLY_RECEIPT_REPLAY_POLICY, "result": result, "success": not missing_artifacts, "summary": summary, "receipt_replay": { "receipt_id": receipt_id, "run_id": effective_run_id, "stage": "P2_retry_exception_controlled_apply_receipt_replay", "status": result, "materialize_artifacts": bool(materialize_artifacts), "artifact_root": str(root), "ready": receipt_ready, }, "target_selectors": target_selectors, "post_apply_readbacks": post_apply_readbacks, "executor_receipt_artifact": receipt_artifact, "materialized_executor_artifacts": materialized_artifacts, "post_executor_receipt_verifier": { "checks": verifier_checks, "check_count": len(verifier_checks), "passed": all(check.get("passed") is True for check in verifier_checks), "expected_sha256": receipt_artifact["payload_sha256"], "actual_sha256": actual_sha, "hash_match": bool(actual_sha) and actual_sha == receipt_artifact["payload_sha256"], "writes_database": False, }, "rollback_plan": rollback_plan, "source_artifacts": receipt_payload["source_artifacts"], "missing_artifacts": missing_artifacts, "safety": safety, } __all__ = ( "_find_retry_exception_artifact_file", "_load_retry_exception_json_artifact", "_retry_exception_controlled_apply_receipt_replay_id", "build_pchome_direct_mapping_retry_candidate_exception_controlled_apply_receipt_replay_package", )