diff --git a/routes/ai_routes.py b/routes/ai_routes.py index 8d97dac..5d7456b 100644 --- a/routes/ai_routes.py +++ b/routes/ai_routes.py @@ -2522,11 +2522,15 @@ def api_pchome_growth_ai_automation_readiness(): try: from config import DATABASE_PATH from services.pchome_revenue_growth_service import build_pchome_growth_opportunities - from services.pchome_mapping_backlog_service import build_pchome_growth_ai_automation_readiness + from services.pchome_mapping_backlog_service import ( + build_pchome_direct_mapping_retry_candidate_exception_controlled_apply_receipt_replay_package, + build_pchome_growth_ai_automation_readiness, + ) force_refresh = str(request.args.get('refresh') or '').strip().lower() in {'1', 'true', 'yes'} execute_search = str(request.args.get('execute_search') or '').strip().lower() in {'1', 'true', 'yes'} execute_fetch = str(request.args.get('execute_fetch') or '').strip().lower() in {'1', 'true', 'yes'} + include_receipt_replay = str(request.args.get('include_receipt_replay', 'true') or '').strip().lower() in {'1', 'true', 'yes'} limit = request.args.get('limit', 20, type=int) batch_size = request.args.get('batch_size', 8, type=int) limit = max(5, min(limit, 50)) @@ -2544,11 +2548,23 @@ def api_pchome_growth_ai_automation_readiness(): payload["cache_state"] = "fresh" _set_pchome_growth_cache(payload) + receipt_replay = None + if include_receipt_replay: + replay_engine = _create_icaim_dashboard_engine(DATABASE_PATH) + try: + receipt_replay = build_pchome_direct_mapping_retry_candidate_exception_controlled_apply_receipt_replay_package( + materialize_artifacts=False, + engine=replay_engine, + ) + finally: + replay_engine.dispose() + readiness = build_pchome_growth_ai_automation_readiness( payload, batch_size=batch_size, execute_search=execute_search, execute_fetch=execute_fetch, + controlled_apply_receipt_replay=receipt_replay, ) readiness["source_endpoint"] = "/api/ai/pchome-growth/opportunities" return jsonify(readiness) diff --git a/services/pchome_mapping_backlog_service.py b/services/pchome_mapping_backlog_service.py index 790fa24..774d048 100644 --- a/services/pchome_mapping_backlog_service.py +++ b/services/pchome_mapping_backlog_service.py @@ -5100,6 +5100,7 @@ def build_pchome_growth_ai_automation_readiness( execute_search: bool = False, execute_fetch: bool = False, search_func: Any = None, + controlled_apply_receipt_replay: dict[str, Any] | None = None, ) -> dict[str, Any]: """Build a single read-only product-facing AI automation readiness view.""" mapping_summary = summarize_pchome_mapping_backlog(payload) @@ -5149,6 +5150,16 @@ def build_pchome_growth_ai_automation_readiness( waiting_candidate_count = selected_search_targets if not candidate_decision_count else 0 receipt_count = int(receipt_summary.get("receipt_count") or 0) ready_receipt_count = int(receipt_summary.get("ready_for_auto_persistence_count") or 0) + receipt_replay_summary = (controlled_apply_receipt_replay or {}).get("summary") or {} + receipt_replay_selector_count = int(receipt_replay_summary.get("target_selector_count") or 0) + receipt_replay_readback_pass_count = int(receipt_replay_summary.get("post_apply_readback_pass_count") or 0) + receipt_replay_materialized_count = int(receipt_replay_summary.get("executor_receipt_materialized_count") or 0) + receipt_replay_hash_match_count = int(receipt_replay_summary.get("executor_receipt_hash_match_count") or 0) + controlled_apply_closeout_verified = ( + bool(receipt_replay_selector_count) + and receipt_replay_readback_pass_count == receipt_replay_selector_count + and int(receipt_replay_summary.get("executor_receipt_ready_count") or 0) > 0 + ) exception_count = _summary_exception_count(receipt_summary) + int( decision_summary.get("machine_review_decision_count") or 0 ) @@ -5175,7 +5186,9 @@ def build_pchome_growth_ai_automation_readiness( "writes_database": False, } - if not direct_mapping_count and ready_receipt_count: + if controlled_apply_closeout_verified: + result = "AI_AUTOMATION_CONTROLLED_APPLY_CLOSEOUT_VERIFIED" + elif not direct_mapping_count and ready_receipt_count: result = "AI_AUTOMATION_READY_FOR_CONTROLLED_APPLY" elif exception_resolution_closeout_receipt_count: result = "AI_AUTOMATION_EXCEPTION_RESOLUTION_CLOSEOUT_READY" @@ -5242,10 +5255,21 @@ def build_pchome_growth_ai_automation_readiness( _automation_lane( "controlled_apply", "受控落地", - "blocked_until_verifier", - 0, - "等待 verifier、rollback、readback", - "P1-P4 穩定後才進 P5/P6", + "completed" if controlled_apply_closeout_verified else "blocked_until_verifier", + receipt_replay_selector_count, + ( + f"readback {receipt_replay_readback_pass_count}/{receipt_replay_selector_count}" + f" · receipt {receipt_replay_materialized_count}" + ) if controlled_apply_closeout_verified else "等待 verifier、rollback、readback", + "維持 receipt replay / drift verifier" if controlled_apply_closeout_verified else "P1-P4 穩定後才進 P5/P6", + ), + _automation_lane( + "controlled_apply_receipt_replay", + "落地收據重放", + "completed" if controlled_apply_closeout_verified else "waiting", + receipt_replay_materialized_count, + f"hash match {receipt_replay_hash_match_count}", + "從 artifact + DB readback 自動證明 apply 已收斂", ), ] @@ -5284,6 +5308,11 @@ def build_pchome_growth_ai_automation_readiness( ), "receipt_count": receipt_count, "ready_receipt_count": ready_receipt_count, + "controlled_apply_replay_selector_count": receipt_replay_selector_count, + "controlled_apply_replay_readback_pass_count": receipt_replay_readback_pass_count, + "controlled_apply_receipt_materialized_count": receipt_replay_materialized_count, + "controlled_apply_receipt_hash_match_count": receipt_replay_hash_match_count, + "controlled_apply_closeout_verified_count": 1 if controlled_apply_closeout_verified else 0, "exception_count": exception_count, "ai_exception_count": exception_count, AI_EXCEPTION_REQUIRED_COUNT_KEY: exception_count, @@ -5299,6 +5328,7 @@ def build_pchome_growth_ai_automation_readiness( PRIMARY_HUMAN_GATE_COUNT_KEY: 0, "exception_resolution": "ai_machine_verifiable", "machine_verifiable_decision_required": True, + "controlled_apply_closeout": "receipt_replay_machine_verified" if controlled_apply_closeout_verified else "waiting_for_verifier", }, "ai_exception_auto_resolution": ai_exception_auto_resolution, "manual_policy": { @@ -5321,6 +5351,7 @@ def build_pchome_growth_ai_automation_readiness( "dispatches_telegram": False, "llm_calls_in_preview": False, "gemini_allowed": False, + "reads_database_for_receipt_replay": bool(controlled_apply_receipt_replay), }, } diff --git a/tests/test_pchome_mapping_backlog_report.py b/tests/test_pchome_mapping_backlog_report.py index d691728..e5d34f7 100644 --- a/tests/test_pchome_mapping_backlog_report.py +++ b/tests/test_pchome_mapping_backlog_report.py @@ -1434,6 +1434,40 @@ def test_ai_automation_readiness_makes_automation_visible_without_manual_primary assert readiness["safety"]["llm_calls_in_preview"] is False +def test_ai_automation_readiness_surfaces_controlled_apply_receipt_replay_closeout(): + readiness = build_pchome_growth_ai_automation_readiness( + _payload(), + batch_size=1, + controlled_apply_receipt_replay={ + "result": "DIRECT_MAPPING_RETRY_EXCEPTION_CONTROLLED_APPLY_RECEIPT_REPLAYED", + "summary": { + "target_selector_count": 4, + "post_apply_readback_pass_count": 4, + "executor_receipt_ready_count": 1, + "executor_receipt_materialized_count": 1, + "executor_receipt_hash_match_count": 1, + }, + "safety": { + "writes_database": False, + }, + }, + ) + + lanes = {lane["key"]: lane for lane in readiness["automation_lanes"]} + assert readiness["result"] == "AI_AUTOMATION_CONTROLLED_APPLY_CLOSEOUT_VERIFIED" + assert readiness["summary"]["controlled_apply_replay_selector_count"] == 4 + assert readiness["summary"]["controlled_apply_replay_readback_pass_count"] == 4 + assert readiness["summary"]["controlled_apply_receipt_materialized_count"] == 1 + assert readiness["summary"]["controlled_apply_closeout_verified_count"] == 1 + assert readiness["automation_policy"]["controlled_apply_closeout"] == "receipt_replay_machine_verified" + assert lanes["controlled_apply"]["status"] == "completed" + assert lanes["controlled_apply"]["value"] == 4 + assert lanes["controlled_apply_receipt_replay"]["status"] == "completed" + assert lanes["controlled_apply_receipt_replay"]["value"] == 1 + assert readiness["safety"]["reads_database_for_receipt_replay"] is True + assert readiness["safety"]["writes_database"] is False + + def test_ai_automation_readiness_reports_candidate_decisions_after_controlled_search(): call_count = {"search": 0} @@ -16014,7 +16048,7 @@ def test_ai_automation_readiness_route_defaults_to_no_search_and_uses_cached_pay monkeypatch.setattr(routes, "_create_icaim_dashboard_engine", fail_engine) app = Flask(__name__) - with app.test_request_context("/api/ai/pchome-growth/ai-automation-readiness?batch_size=1"): + with app.test_request_context("/api/ai/pchome-growth/ai-automation-readiness?batch_size=1&include_receipt_replay=false"): response = routes.api_pchome_growth_ai_automation_readiness.__wrapped__() payload = response.get_json()