#!/usr/bin/env python3 """Simulate authoritative BLIF Flow v2 without changing application mode.""" from __future__ import annotations import argparse import json import os from pathlib import Path import sys from typing import Any JSON_PATH = Path("/tmp/blif_flow_v2_authoritative_simulation.json") TEXT_PATH = Path("/tmp/blif_flow_v2_authoritative_operations.txt") CURRENT = {"do_now", "review", "blocked", "exception"} CLASSIFICATIONS = { "SATISFIED", "SUPERSEDED", "DUPLICATE_SUPPRESSED", "PREMATURE_REMOVED", "REPLACED_BY_CORRECT_ACTION", "LEGACY_ONLY", "UNSAFE_FALSE_NEGATIVE", } NAMED = { "Instalbeira": ("5c33db95-fab8-477a-bddd-0b9cc8f91302", "CREATE_PROFORMA", "CREATE_PROFORMA", "do_now"), "Panoramic": ("fd79b9a1-07e6-4f61-95e8-09eab89c155e", None, "FOLLOW_UP_CUSTOMER_REVIEW", "do_now"), "ENGEXICON": ("61f1c955-a372-4ea7-b9b0-b8528d74a141", "PREPARE_ORDER", "PREPARE_ORDER", "do_now"), "CONSTRURECUP": ("e3b23ac5-84db-4763-8a31-a684e873032c", "PREPARE_ORDER", "PREPARE_ORDER", "do_now"), "X MAT canonical": ("dc89a466-db24-401b-bfe9-d47644b2d0c8", None, None, "not_current"), "X MAT duplicate": ("1816a06e-9a69-4a9b-9279-1263156892d3", None, None, "suppressed"), "RZSOLAR canonical": ("fd221608-e007-4043-a23d-07e0c119a345", "REVIEW_REQUIRED", "REVIEW_RECONSTRUCTED_PROCESS", "review"), "RZSOLAR duplicate": ("434124fb-ac19-4d78-909a-55761d7e8daa", "REVIEW_REQUIRED", None, "suppressed"), } def parse_args(argv: list[str] | None = None) -> argparse.Namespace: parser = argparse.ArgumentParser( description="Read-only V1 versus simulated authoritative BLIF Flow v2 Operations audit." ) parser.add_argument( "--production-readonly-simulation", action="store_true", help="Required opt-in when current_database() is clientflow; requires compare mode and a read-only transaction.", ) return parser.parse_args(argv) def _load_runtime(): """Application imports are intentionally delayed until after argparse.""" root = Path(__file__).resolve().parents[1] if str(root) not in sys.path: sys.path.insert(0, str(root)) from app.db import engine from app.operations_service import get_operations_summary from app.opportunity_next_action_service import get_opportunity_next_actions_for_mode return engine, get_operations_summary, get_opportunity_next_actions_for_mode def _all(summary: dict[str, Any]) -> list[dict[str, Any]]: if "simulation_all_items" in summary: return list(summary.get("simulation_all_items") or []) return sum((list(summary.get(key) or []) for key in ("work_items", "waiting_items", "backlog_items", "not_current_items")), []) def _metrics(summary: dict[str, Any]) -> dict[str, int]: queues = [str(item.get("operational_queue") or "") for item in _all(summary)] return {"current": sum(q in CURRENT for q in queues), "do_now": queues.count("do_now"), "review": queues.count("review"), "waiting": queues.count("waiting"), "backlog": queues.count("backlog"), "blocked": queues.count("blocked"), "not_current": queues.count("not_current")} def _key(item: dict[str, Any]) -> str: return str(item.get("work_item_key") or item.get("process_key") or item.get("opportunity_id") or item.get("id")) def _material_identity(item: dict[str, Any]) -> str: return str(item.get("material_process_key") or item.get("canonical_opportunity_id") or item.get("opportunity_id") or item.get("process_key") or _key(item)) def _classification(old: dict[str, Any], new: dict[str, Any] | None) -> tuple[str, str]: if new is not None: return "REPLACED_BY_CORRECT_ACTION", "Flow v2 selected a different current action for the same work item." decision = old.get("decision") if isinstance(old.get("decision"), dict) else {} code = str(decision.get("reason_code") or old.get("eligibility_reason_code") or "").upper() if "DUPLICATE" in code: return "DUPLICATE_SUPPRESSED", "The local opportunity is a duplicate material representation." if "SATISF" in code or "ANSWERED" in code: return "SATISFIED", "Later factual evidence satisfies the old obligation." if "SUPERSE" in code or "TERMINAL" in code: return "SUPERSEDED", "A later factual state supersedes the legacy card." if "PREMATURE" in code: return "PREMATURE_REMOVED", "The legacy action is premature for the factual state." if old.get("source") in {"task", "opportunity"}: return "LEGACY_ONLY", "The card is supported only by legacy operational representation." return "UNSAFE_FALSE_NEGATIVE", "No safe factual explanation was found for removing this current card." def build_report( *, identity: dict[str, Any], configured_mode: str, v1: dict[str, Any], v2: dict[str, Any], named_decisions: dict[str, dict[str, Any]], missing_projections: int, projection_state_counts: dict[str, int] | None = None, ) -> dict[str, Any]: v1_current = {_material_identity(item): item for item in _all(v1) if item.get("operational_queue") in CURRENT} v2_current = {_material_identity(item): item for item in _all(v2) if item.get("operational_queue") in CURRENT} removed, changed = [], [] for key, old in v1_current.items(): new = v2_current.get(key) if new is not None and new.get("action_code") == old.get("action_code"): continue classification, reason = _classification(old, new) row = {"material_process_identity": key, "work_item_key": _key(old), "opportunity_id": old.get("opportunity_id"), "v1_action": old.get("action_code"), "simulated_v2_action": (new or {}).get("action_code"), "classification": classification, "reason": reason, "source_refs": old.get("source_refs") or []} (changed if new is not None else removed).append(row) added = [] for key, item in v2_current.items(): if key in v1_current: continue decision = item.get("decision") if isinstance(item.get("decision"), dict) else {} winning = list(decision.get("obligation_source_refs") or []) reason_code = decision.get("reason_code") obligation_kind = ("VALID_ACTIVE_OBLIGATION" if str(reason_code).startswith(("ACTIVE_", "DUE_", "EXPLICIT_")) else "FACTUAL_V2_BUSINESS_ACTION") added.append({ "material_process_identity": key, "work_item_key": _key(item), "opportunity_id": item.get("opportunity_id"), "canonical_opportunity_id": item.get("canonical_opportunity_id"), "material_process_key": item.get("material_process_key"), "business_state": decision.get("business_state"), "business_next_action": decision.get("business_next_action"), "effective_action": decision.get("effective_action") or item.get("action_code"), "queue": item.get("operational_queue"), "reason_code": reason_code, "winning_obligation_task_id": (winning[0].get("id") if winning else None), "task_status": (winning[0].get("status") if winning else None), "source_system": (winning[0].get("source_system") if winning else None), "authoritative_basis": obligation_kind, "why_absent_from_v1": "NO_V1_CURRENT_CARD_FOR_MATERIAL_PROCESS", "source_refs": item.get("source_refs") or [], "non_winning_obligations": [ref for ref in decision.get("obligation_audit_refs") or [] if ref.get("classification") != "VALID_ACTIVE_OBLIGATION"], }) material_counts: dict[str, int] = {} for item in v2_current.values(): material = str(item.get("canonical_opportunity_id") or item.get("opportunity_id") or item.get("process_key")) material_counts[material] = material_counts.get(material, 0) + 1 duplicates = [{"material_process": key, "count": count} for key, count in material_counts.items() if count > 1] named_cases = {} for name, (oid, expected_business, expected_action, expected_queue) in NAMED.items(): decision = named_decisions.get(oid) or {} suppressed = decision.get("suppress_current_card") is True actual_queue = "suppressed" if suppressed else decision.get("operational_queue") actual_action = decision.get("effective_action") actual_business = decision.get("business_next_action") passed = actual_business == expected_business and actual_action == expected_action and actual_queue == expected_queue named_cases[name] = {"opportunity_id": oid, "expected_business_next_action": expected_business, "actual_business_next_action": actual_business, "expected_action": expected_action, "expected_queue": expected_queue, "actual_action": actual_action, "actual_queue": actual_queue, "passed": passed} fiscal = [] for item in v2_current.values(): decision = item.get("decision") if isinstance(item.get("decision"), dict) else {} if item.get("action_code") == "VALIDATE_FISCAL_CUSTOMER": fiscal.append({"opportunity_id": item.get("opportunity_id"), "business_action": decision.get("business_next_action"), "reason_code": decision.get("reason_code"), "reason": decision.get("description")}) unsafe = sum(row["classification"] == "UNSAFE_FALSE_NEGATIVE" for row in removed) invalid_obligation_cards = [] for item in v2_current.values(): decision = item.get("decision") if isinstance(item.get("decision"), dict) else {} refs = decision.get("obligation_source_refs") or [] if refs and any(str(ref.get("status") or "").lower() != "pending" for ref in refs): invalid_obligation_cards.append({"opportunity_id": item.get("opportunity_id"), "effective_action": item.get("action_code"), "refs": refs}) grouped_added = {code: [row for row in added if row["effective_action"] == code] for code in ("SEND_INFO", "SEND_QUOTE", "SEND_PROFORMA", "REVIEW_REQUIRED")} state_counts = projection_state_counts or {} waiting_projection_count = sum(state_counts.get(state, 0) for state in ("AWAITING_CUSTOMER", "AWAITING_PAYMENT")) rendered_waiting = _metrics(v2)["waiting"] represented_waiting_states = sum( 1 for item in _all(v2) if (item.get("decision") or {}).get("business_state") in {"AWAITING_CUSTOMER", "AWAITING_PAYMENT"} ) return { "identity": identity, "configured_mode": configured_mode, "v1_metrics": _metrics(v1), "authoritative_metrics": _metrics(v2), "semantic_changes": len(removed) + len(changed) + len(added), "removed_cards": removed, "added_cards": added, "changed_actions": changed, "duplicate_cards": duplicates, "duplicate_current_cards": len(duplicates), "missing_projections": missing_projections, "unsafe_false_negatives": unsafe, "named_cases": named_cases, "fiscal_validation_cases": fiscal, "added_card_diagnostics": added, "grouped_added_diagnostics": grouped_added, "waiting_state_diagnostics": { "projection_count": waiting_projection_count, "represented_processes": represented_waiting_states, "rendered_waiting_cards": rendered_waiting, "promoted_by_due_obligation": max(0, represented_waiting_states - rendered_waiting), "rendering_policy": "Waiting states carry no invented internal action; they render in waiting only when selected as Operations processes.", "lost": max(0, waiting_projection_count - represented_waiting_states), }, "invalid_obligation_current_cards": invalid_obligation_cards, } def _write_reports(report: dict[str, Any]) -> None: JSON_PATH.write_text(json.dumps(report, indent=2, ensure_ascii=False, default=str) + "\n") lines = ["BLIF Flow v2 authoritative Operations simulation", "", f"Identity: {report['identity']}", f"Configured mode: {report['configured_mode']}", f"V1: {report['v1_metrics']}", f"Simulated authoritative V2: {report['authoritative_metrics']}", f"Removed: {len(report['removed_cards'])}", f"Added: {len(report['added_cards'])}", f"Changed actions: {len(report['changed_actions'])}", f"Duplicate current cards: {report['duplicate_current_cards']}", f"Missing projections: {report['missing_projections']}", f"UNSAFE_FALSE_NEGATIVE: {report['unsafe_false_negatives']}", "", "Named cases:"] lines.extend(f"- {name}: {case}" for name, case in report["named_cases"].items()) lines.extend(["", f"Fiscal validation cases: {len(report['fiscal_validation_cases'])}"]) TEXT_PATH.write_text("\n".join(lines) + "\n") def exit_code(report: dict[str, Any]) -> int: failed_named = any(not case.get("passed") for case in report.get("named_cases", {}).values()) unsafe = int(report.get("unsafe_false_negatives") or 0) missing = int(report.get("missing_projections") or 0) duplicates = int(report.get("duplicate_current_cards") or 0) invalid_obligations = len(report.get("invalid_obligation_current_cards") or []) return 2 if unsafe or missing or duplicates or failed_named or invalid_obligations else 0 def run(args: argparse.Namespace, *, runtime_loader=_load_runtime) -> dict[str, Any]: engine, get_operations, get_decisions = runtime_loader() configured_mode = os.environ.get("BLIF_FLOW_V2_MODE") conn = engine.connect() try: # One explicit transaction and one factual snapshot for identity, V1, # V2, projection completeness, and named-case decisions. conn.exec_driver_sql("BEGIN READ ONLY") identity = dict(conn.exec_driver_sql(""" SELECT current_database() AS database, current_user AS user, current_setting('transaction_read_only') AS transaction_read_only """).mappings().one()) production = identity.get("database") == "clientflow" if production and not args.production_readonly_simulation: raise RuntimeError("production simulation requires --production-readonly-simulation") if args.production_readonly_simulation: if identity.get("database") != "clientflow": raise RuntimeError("production simulation requires current_database() = clientflow") if identity.get("transaction_read_only") != "on": raise RuntimeError("production simulation requires transaction_read_only = on") if configured_mode is None: raise RuntimeError("production simulation requires explicit BLIF_FLOW_V2_MODE=compare") if configured_mode.strip().lower() != "compare": raise RuntimeError("production simulation requires BLIF_FLOW_V2_MODE=compare") v1 = get_operations(limit=200, flow_mode="off", connection=conn) v2 = get_operations(limit=200, flow_mode="authoritative", connection=conn) projection_counts = conn.exec_driver_sql(""" SELECT (SELECT count(*) FROM opportunities) AS opportunities, (SELECT count(*) FROM opportunity_flow_state_v2) AS projections """).mappings().one() state_rows = conn.exec_driver_sql(""" SELECT business_state, count(*) AS count FROM opportunity_flow_state_v2 GROUP BY business_state """).mappings().all() named_ids = [value[0] for value in NAMED.values()] named_decisions = get_decisions(named_ids, flow_mode="authoritative", connection=conn) if production and os.environ.get("BLIF_FLOW_V2_MODE") != configured_mode: raise RuntimeError("configured BLIF_FLOW_V2_MODE changed during simulation") report = build_report( identity=identity, configured_mode=configured_mode or "unset", v1=v1, v2=v2, named_decisions=named_decisions, missing_projections=max(0, int(projection_counts["opportunities"]) - int(projection_counts["projections"])), projection_state_counts={str(row["business_state"]): int(row["count"]) for row in state_rows}, ) _write_reports(report) return report finally: conn.exec_driver_sql("ROLLBACK") conn.close() def main(argv: list[str] | None = None, *, runtime_loader=_load_runtime) -> int: args = parse_args(argv) # --help exits before runtime_loader/application imports. report = run(args, runtime_loader=runtime_loader) print(json.dumps(report, indent=2, ensure_ascii=False, default=str)) return exit_code(report) if __name__ == "__main__": raise SystemExit(main())