feat: add guarded production Flow v2 authoritative simulation

This commit is contained in:
plx
2026-08-16 01:22:48 +00:00
parent fc092a96b6
commit ac385342f5
4 changed files with 379 additions and 86 deletions

View File

@@ -1,95 +1,226 @@
#!/usr/bin/env python3
"""Read-only audit of the exact Operations boundary in V1 and authoritative V2."""
"""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
ROOT = Path(__file__).resolve().parents[1]
if str(ROOT) not in sys.path:
sys.path.insert(0, str(ROOT))
from app.config import settings
from app.operations_service import get_operations_summary
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", "do_now"),
"Panoramic": ("fd79b9a1-07e6-4f61-95e8-09eab89c155e", "FOLLOW_UP_CUSTOMER_REVIEW", "do_now"),
"ENGEXICON": ("61f1c955-a372-4ea7-b9b0-b8528d74a141", "PREPARE_ORDER", "do_now"),
"CONSTRURECUP": ("e3b23ac5-84db-4763-8a31-a684e873032c", "PREPARE_ORDER", "do_now"),
"X MAT canonical": ("dc89a466-db24-401b-bfe9-d47644b2d0c8", None, "not_current"),
"X MAT duplicate": ("1816a06e-9a69-4a9b-9279-1263156892d3", None, "suppressed"),
"RZSOLAR canonical": ("fd221608-e007-4043-a23d-07e0c119a345", "REVIEW_REQUIRED", "review"),
"RZSOLAR duplicate": ("434124fb-ac19-4d78-909a-55761d7e8daa", None, "suppressed"),
}
def _all(summary):
return list(summary.get("work_items") or []) + list(summary.get("waiting_items") or []) + list(summary.get("backlog_items") or []) + list(summary.get("not_current_items") or [])
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 _metrics(summary):
items = _all(summary)
queues = [str(item.get("operational_queue") or "") for item in items]
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 _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,
) -> dict[str, Any]:
v1_current = {_key(item): item for item in _all(v1) if item.get("operational_queue") in CURRENT}
v2_current = {_key(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 = {"work_item_key": key, "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 = [{"work_item_key": key, "opportunity_id": item.get("opportunity_id"),
"action": item.get("action_code"), "source_refs": item.get("source_refs") or []}
for key, item in v2_current.items() if key not in v1_current]
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_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")
passed = actual_action == expected_action and actual_queue == expected_queue
named_cases[name] = {"opportunity_id": oid, "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)
return {
"current": sum(queue in CURRENT for queue in queues),
"do_now": queues.count("do_now"), "review": queues.count("review"),
"waiting": queues.count("waiting"), "backlog": queues.count("backlog"),
"not_current": queues.count("not_current"),
"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,
}
def _identity(item):
return str(item.get("canonical_opportunity_id") or item.get("opportunity_id") or item.get("process_key") or item.get("work_item_key"))
def _classification(old, new):
if new is not None:
return "REPLACED_BY_CORRECT_ACTION"
decision = old.get("decision") or {}
code = str(decision.get("reason_code") or old.get("eligibility_reason_code") or "").upper()
if "DUPLICATE" in code:
return "DUPLICATE_SUPPRESSED"
if "SATISF" in code or "ANSWERED" in code:
return "SATISFIED"
if "SUPERSE" in code or "TERMINAL" in code:
return "SUPERSEDED"
if "PREMATURE" in code:
return "PREMATURE_REMOVED"
if old.get("source") in {"task", "opportunity"}:
return "LEGACY_ONLY"
return "UNSAFE_FALSE_NEGATIVE"
def simulate():
original = settings.blif_flow_v2_mode
try:
settings.blif_flow_v2_mode = "off"
v1 = get_operations_summary(limit=200)
settings.blif_flow_v2_mode = "authoritative"
v2 = get_operations_summary(limit=200)
finally:
settings.blif_flow_v2_mode = original
v1_current = {_identity(item): item for item in _all(v1) if item.get("operational_queue") in CURRENT}
v2_current = {_identity(item): item for item in _all(v2) if item.get("operational_queue") in CURRENT}
removed = []
for key, old in v1_current.items():
new = v2_current.get(key)
if new is None or new.get("action_code") != old.get("action_code"):
removed.append({"identity": key, "v1_action": old.get("action_code"),
"v2_action": (new or {}).get("action_code"),
"classification": _classification(old, new)})
added = [{"identity": key, "action": item.get("action_code")}
for key, item in v2_current.items()
if key not in v1_current or v1_current[key].get("action_code") != item.get("action_code")]
result = {"v1": _metrics(v1), "authoritative_v2": _metrics(v2),
"cards_removed": removed, "cards_added_or_replaced": added,
"unsafe_false_negatives": sum(row["classification"] == "UNSAFE_FALSE_NEGATIVE" for row in removed)}
JSON_PATH.write_text(json.dumps(result, indent=2, ensure_ascii=False, default=str) + "\n")
lines = ["BLIF Flow v2 authoritative Operations simulation", "", f"V1: {result['v1']}",
f"Authoritative V2: {result['authoritative_v2']}",
f"Cards removed: {len(removed)}", f"Cards added/replaced: {len(added)}",
f"UNSAFE_FALSE_NEGATIVE: {result['unsafe_false_negatives']}"]
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")
return result
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)
return 2 if unsafe or missing or duplicates or failed_named 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()
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"])),
)
_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__":
audit = simulate()
print(json.dumps(audit, indent=2, ensure_ascii=False, default=str))
if audit["unsafe_false_negatives"]:
raise SystemExit(2)
raise SystemExit(main())