From 0beb3e4f10f8e962fb1e18d95fcda29f94863e7c Mon Sep 17 00:00:00 2001 From: ogt Date: Thu, 2 Jul 2026 21:20:30 +0800 Subject: [PATCH] fix(stockplatform): classify ai recommendations stale after core recovery --- .../awoooi_priority_work_order_readback.py | 79 ++++++++++++- ...ublic_api_controlled_recovery_preflight.py | 109 ++++++++++++++++++ ...ockplatform_public_api_runtime_readback.py | 54 +++++++++ ...awoooi_priority_work_order_readback_api.py | 83 +++++++++++++ ...ublic_api_controlled_recovery_preflight.py | 90 +++++++++++++++ ...ockplatform_public_api_runtime_readback.py | 97 ++++++++++++++++ docs/LOGBOOK.md | 17 +++ 7 files changed, 528 insertions(+), 1 deletion(-) diff --git a/apps/api/src/services/awoooi_priority_work_order_readback.py b/apps/api/src/services/awoooi_priority_work_order_readback.py index 72421e8bc..4606326e2 100644 --- a/apps/api/src/services/awoooi_priority_work_order_readback.py +++ b/apps/api/src/services/awoooi_priority_work_order_readback.py @@ -2577,6 +2577,11 @@ def _stockplatform_recovery_next_state( "controlled_recovery_preflight_waiting_stockplatform_production_" "migration_control_channel_readback_and_public_api_verifier" ) + if _stockplatform_ai_recommendations_wait(data_dependency_classification): + return ( + "controlled_recovery_preflight_waiting_stockplatform_ai_" + "recommendation_pipeline_readback_and_public_api_verifier" + ) if api_layer == "api_live_data_dependency_not_ready": return ( "controlled_recovery_preflight_waiting_stockplatform_data_dependency_" @@ -2610,6 +2615,15 @@ def _stockplatform_recovery_reason( "KM/RAG/MCP/PlayBook writeback receipts. This is not a generic API " "container recovery task and not a manual end state." ) + if _stockplatform_ai_recommendations_wait(data_dependency_classification): + return ( + "Live StockPlatform API health is reachable and official core data " + "readbacks have recovered, but AI recommendations remain stale. " + "The P0-006 lane now points at the AI recommendation pipeline " + "readback, public API verifier, and KM/RAG/MCP/PlayBook writeback " + "receipts. This is not a generic DB/source repair task and not a " + "manual end state." + ) if api_layer == "api_live_data_dependency_not_ready": return ( "Live StockPlatform API health is reachable, but freshness and/or " @@ -2661,6 +2675,18 @@ def _stockplatform_recovery_action( "hosts, reload Nginx, manually write DB rows, fake freshness, " "trigger unrelated workflows, or read secrets from this lane." ) + if _stockplatform_ai_recommendations_wait(data_dependency_classification): + return ( + "Run the StockPlatform AI recommendation pipeline controlled " + "readback in order: verify core freshness and ingestion are still " + "green, inspect the AI recommendation queue/pipeline receipt in " + "read-only mode, use only the existing bounded pipeline path after " + "dry-run if apply is needed, rerun public freshness/selection " + "brief verifiers, and write metadata-only KM/RAG/MCP/PlayBook " + "receipts. Do not restart Docker daemon, reboot hosts, reload " + "Nginx, write DB rows, fake freshness, trigger unrelated " + "workflows, or read secrets from this lane." + ) if api_layer == "api_live_data_dependency_not_ready": return ( "Run the StockPlatform data dependency controlled preflight in order: " @@ -2697,6 +2723,14 @@ def _stockplatform_recovery_first_order( "production migration/control-channel path with rollback and public " "API verifier; do not manually DB-write or fake freshness." ) + if _stockplatform_ai_recommendations_wait(data_dependency_classification): + return ( + "P0-006-STOCKPLATFORM-PUBLIC-API-CONTROLLED-RECOVERY-PREFLIGHT: " + "StockPlatform API and official core data are recovered, but AI " + "recommendations are stale; run AI recommendation pipeline " + "readback, public API verifier, and metadata writeback before " + "claiming the lane is only reboot-window gated." + ) if api_layer == "api_live_data_dependency_not_ready": return ( "P0-006-STOCKPLATFORM-PUBLIC-API-CONTROLLED-RECOVERY-PREFLIGHT: " @@ -2724,6 +2758,11 @@ def _stockplatform_runtime_next_state( "blocked_live_stockplatform_public_api_requires_production_" "migration_control_channel_readback" ) + if _stockplatform_ai_recommendations_wait(data_dependency_classification): + return ( + "blocked_live_stockplatform_public_api_requires_ai_recommendation_" + "pipeline_readback" + ) if data_dependency_classification == "freshness_or_ingestion_not_ready": return ( "blocked_live_stockplatform_public_api_requires_data_dependency_" @@ -2747,6 +2786,14 @@ def _stockplatform_runtime_drift_reason( "the production migration/control-channel path before claiming the " "reboot SLO lane is only waiting on a fresh boot window." ) + if _stockplatform_ai_recommendations_wait(data_dependency_classification): + return ( + "StockPlatform public API health is now HTTP 200 and official core " + "data readbacks are recovered, but AI recommendations are stale. " + "P0-006 must stay on the AI recommendation pipeline readback and " + "public verifier path before claiming the reboot SLO lane is only " + "waiting on a fresh boot window." + ) if data_dependency_classification == "freshness_or_ingestion_not_ready": return ( "StockPlatform public API health is now HTTP 200, but freshness and " @@ -2776,6 +2823,15 @@ def _stockplatform_runtime_drift_action( "reload Nginx, manually write DB rows, fake freshness, trigger " "unrelated workflows, or read secrets from this lane." ) + if _stockplatform_ai_recommendations_wait(data_dependency_classification): + return ( + "Run the StockPlatform AI recommendation pipeline readback with " + "target selector, source-of-truth diff, dry-run/check-mode, " + "rollback, and public API verifier, then rerun the public " + "readback. Do not restart Docker daemon, reboot hosts, reload " + "Nginx, manually write DB rows, fake freshness, trigger unrelated " + "workflows, or read secrets from this lane." + ) if data_dependency_classification == "freshness_or_ingestion_not_ready": return ( "Run the StockPlatform data dependency/source contract readback " @@ -2805,6 +2861,14 @@ def _stockplatform_runtime_first_order( "postgres_not_ready; run the production migration/control-channel " "path before claiming the lane is only reboot-window gated." ) + if _stockplatform_ai_recommendations_wait(data_dependency_classification): + return ( + "P0-006-STOCKPLATFORM-PUBLIC-API-RUNTIME-READBACK: live public " + "StockPlatform API health is 200 and official core data are " + "recovered, but AI recommendations are stale; run AI recommendation " + "pipeline readback and public verifier before claiming the lane is " + "only reboot-window gated." + ) if data_dependency_classification == "freshness_or_ingestion_not_ready": return ( "P0-006-STOCKPLATFORM-PUBLIC-API-RUNTIME-READBACK: live public " @@ -3519,7 +3583,11 @@ def _stockplatform_external_data_wait_active(state: dict[str, Any]) -> bool: return ( state.get("stockplatform_source_contract_readback_present") is True and state.get("stockplatform_source_contract_ready") is not True - and data_dependency == "freshness_or_ingestion_not_ready" + and data_dependency + in { + "freshness_or_ingestion_not_ready", + "ai_recommendations_stale_after_core_recovered", + } and any( blocker in { @@ -3532,6 +3600,15 @@ def _stockplatform_external_data_wait_active(state: dict[str, Any]) -> bool: ) +def _stockplatform_ai_recommendations_wait( + data_dependency_classification: str, +) -> bool: + return ( + data_dependency_classification + == "ai_recommendations_stale_after_core_recovered" + ) + + def _apply_parallel_mainline_execution_overlay(payload: dict[str, Any]) -> None: state = _dict(payload.setdefault("mainline_execution_state", {})) items = _list(payload.get("commander_inserted_requirement_work_items")) diff --git a/apps/api/src/services/stockplatform_public_api_controlled_recovery_preflight.py b/apps/api/src/services/stockplatform_public_api_controlled_recovery_preflight.py index 152ce4459..af515c8e9 100644 --- a/apps/api/src/services/stockplatform_public_api_controlled_recovery_preflight.py +++ b/apps/api/src/services/stockplatform_public_api_controlled_recovery_preflight.py @@ -27,6 +27,9 @@ _DEFAULT_ROUTE_SOURCE = ( / "templates" / "188-all-sites.conf.j2" ) +_AI_RECOMMENDATIONS_STALE_AFTER_CORE_RECOVERED = ( + "ai_recommendations_stale_after_core_recovered" +) def load_latest_stockplatform_public_api_controlled_recovery_preflight( @@ -383,6 +386,14 @@ def _current_apply_blocker( "postgres_readiness_and_production_migration_control_path_readback_" "required_before_apply" ) + if ( + data_dependency_classification + == _AI_RECOMMENDATIONS_STALE_AFTER_CORE_RECOVERED + ): + return ( + "ai_recommendation_pipeline_readback_required_before_public_api_" + "verifier" + ) if api_layer_classification == "api_live_data_dependency_not_ready": return ( "data_dependency_control_path_readback_required_before_source_" @@ -401,6 +412,15 @@ def _allowed_scope( "km_rag_mcp_playbook_metadata_writeback", ] if api_layer_classification == "api_live_data_dependency_not_ready": + if ( + data_dependency_classification + == _AI_RECOMMENDATIONS_STALE_AFTER_CORE_RECOVERED + ): + return [ + "read_only_ai_recommendation_queue_status", + "read_only_ai_recommendation_pipeline_receipt", + *common, + ] scope = [ "read_only_data_dependency_status_inspection", "read_only_source_freshness_contract_diff", @@ -459,6 +479,14 @@ def _safe_next_step( candidate_packaged and api_layer_classification == "api_live_data_dependency_not_ready" ): + if ( + data_dependency_classification + == _AI_RECOMMENDATIONS_STALE_AFTER_CORE_RECOVERED + ): + return ( + "run_stockplatform_ai_recommendation_pipeline_readback_then_" + "public_api_verifier" + ) if data_dependency_classification != "postgres_not_ready": return ( "run_stockplatform_data_dependency_source_contract_readback_" @@ -489,6 +517,47 @@ def _controlled_work_items( return [] base_status = "queued_waiting_control_path_readback" if candidate_packaged else "blocked_waiting_route_evidence" if api_layer_classification == "api_live_data_dependency_not_ready": + if ( + data_dependency_classification + == _AI_RECOMMENDATIONS_STALE_AFTER_CORE_RECOVERED + ): + return [ + { + "id": "stockplatform-ai-recommendation-control-path-readback", + "title": "StockPlatform AI recommendation control path readback", + "owner_agent": "DevOps Agent", + "status": base_status, + "required_output": "non_secret_pipeline_liveness_and_runtime_contract_match", + }, + { + "id": "stockplatform-ai-recommendation-queue-inspection", + "title": "Read-only AI recommendation queue inspection", + "owner_agent": "SRE Agent", + "status": "waiting_control_path_readback", + "required_output": "ai_recommendations_latest_date_and_queue_receipt", + }, + { + "id": "stockplatform-ai-recommendation-pipeline-preflight", + "title": "AI recommendation pipeline verifier preflight", + "owner_agent": "Data Reliability Agent", + "status": "waiting_read_only_inspection", + "required_output": "dry_run_pipeline_receipt_no_manual_db_write", + }, + { + "id": "stockplatform-api-post-apply-verifier", + "title": "Public API post-apply verifier", + "owner_agent": "Verifier Agent", + "status": "ready", + "required_output": "public_health_freshness_ingestion_green", + }, + { + "id": "stockplatform-ai-learning-writeback", + "title": "KM / RAG / MCP / PlayBook trust writeback", + "owner_agent": "Learning Agent", + "status": "ready", + "required_output": "metadata_only_ai_pipeline_receipt_and_trust_delta", + }, + ] contract_title = "Source freshness and Postgres contract preflight" contract_output = "dry_run_contract_diff_no_db_write" if data_dependency_classification == "postgres_not_ready": @@ -582,6 +651,46 @@ def _controlled_action_candidates( if runtime_ready: return [] if api_layer_classification == "api_live_data_dependency_not_ready": + if ( + data_dependency_classification + == _AI_RECOMMENDATIONS_STALE_AFTER_CORE_RECOVERED + ): + return [ + { + "id": "read_only_ai_recommendation_pipeline_readback", + "execution_route": "stockplatform_ai_recommendation_executor", + "current_status": ( + "queued_waiting_control_path_readback" + if candidate_packaged + else "blocked_waiting_route_evidence" + ), + "check_mode": "read_only_ai_recommendation_queue_and_pipeline_receipt", + "apply_allowed_when": [ + "host_control_path_liveness_readback_ok", + "stockplatform_api_health_ok", + "core_freshness_and_ingestion_ok", + "post_apply_public_verifier_ready", + ], + "forbidden_actions": [ + "secret_read", + "docker_daemon_restart", + "host_reboot", + "database_write", + "fake_freshness_marker", + ], + }, + { + "id": "ai_recommendation_pipeline_verifier_preflight", + "execution_route": "stockplatform_ai_recommendation_executor", + "current_status": "waiting_read_only_pipeline_readback", + "check_mode": "dry_run_ai_recommendation_pipeline_contract", + "bounded_apply": ( + "use the existing AI recommendation pipeline path only " + "after control-channel readback and dry-run; no manual " + "DB rows, no fake freshness marker, no destructive restore" + ), + }, + ] second_candidate = { "id": "source_freshness_contract_preflight", "execution_route": "stockplatform_data_contract_executor", diff --git a/apps/api/src/services/stockplatform_public_api_runtime_readback.py b/apps/api/src/services/stockplatform_public_api_runtime_readback.py index de1f733fa..1af14e624 100644 --- a/apps/api/src/services/stockplatform_public_api_runtime_readback.py +++ b/apps/api/src/services/stockplatform_public_api_runtime_readback.py @@ -27,6 +27,9 @@ _DATA_READINESS_RECEIPT_KEY = ( ) _SOURCE_CONTRACT_SCHEMA_VERSION = "stockplatform_source_contract_control_readback_v1" _SOURCE_CONTRACT_RECEIPT_KEY = "stockplatform_source_contract_readback" +_AI_RECOMMENDATIONS_STALE_AFTER_CORE_RECOVERED = ( + "ai_recommendations_stale_after_core_recovered" +) _DEFAULT_OPERATIONS_DIR = default_operations_dir(Path(__file__)) _RECOVERY_RECEIPT_FILE = ( "stockplatform-public-api-runtime-recovery-control-receipt.snapshot.json" @@ -224,6 +227,10 @@ def _build_payload( freshness_sources ), "data_dependency_classification": data_dependency_classification, + "ai_recommendations_stale_after_core_recovered": ( + data_dependency_classification + == _AI_RECOMMENDATIONS_STALE_AFTER_CORE_RECOVERED + ), "postgres_not_ready": postgres_not_ready, "committed_freshness_status": str( committed_stockplatform.get("freshness_status") or "unknown" @@ -287,6 +294,10 @@ def _build_payload( recovery_control_path_blockers ), "data_dependency_classification": data_dependency_classification, + "ai_recommendations_stale_after_core_recovered": ( + data_dependency_classification + == _AI_RECOMMENDATIONS_STALE_AFTER_CORE_RECOVERED + ), "postgres_not_ready": postgres_not_ready, "postgres_readiness_or_data_source_readback_present": ( data_readiness_control_readback.get("readback_present") is True @@ -454,6 +465,15 @@ def _data_readiness_control_readback( if data_ready: status = "stockplatform_data_readiness_ready" safe_next_step = "run_post_apply_public_api_verifier_and_clear_p0_006_blocker" + elif ( + data_dependency_classification + == _AI_RECOMMENDATIONS_STALE_AFTER_CORE_RECOVERED + ): + status = "blocked_stockplatform_ai_recommendations_stale_after_core_recovered" + safe_next_step = ( + "run_stockplatform_ai_recommendation_pipeline_readback_then_rerun_" + "public_api_verifier" + ) elif readback_present and postgres_not_ready: status = "blocked_stockplatform_postgres_not_ready" safe_next_step = ( @@ -490,6 +510,10 @@ def _data_readiness_control_readback( "postgres_ready": data_ready and not postgres_not_ready, "postgres_not_ready": postgres_not_ready, "data_dependency_classification": data_dependency_classification, + "ai_recommendations_stale_after_core_recovered": ( + data_dependency_classification + == _AI_RECOMMENDATIONS_STALE_AFTER_CORE_RECOVERED + ), "freshness_http_status": freshness_http_status, "ingestion_http_status": ingestion_http_status, "freshness_status": str(freshness_json.get("status") or "unknown"), @@ -556,6 +580,16 @@ def _source_contract_control_readback( if data_ready: status = "stockplatform_source_contract_ready" safe_next_step = "run_post_apply_public_api_verifier_and_clear_p0_006_blocker" + elif ( + readback_present + and data_dependency_classification + == _AI_RECOMMENDATIONS_STALE_AFTER_CORE_RECOVERED + ): + status = "stockplatform_source_contract_core_ready_ai_recommendations_stale" + safe_next_step = ( + "run_stockplatform_ai_recommendation_pipeline_readback_then_rerun_" + "public_api_verifier" + ) elif readback_present and data_dependency_classification == ( "freshness_or_ingestion_not_ready" ): @@ -604,6 +638,10 @@ def _source_contract_control_readback( "safe_next_step": safe_next_step, "source_contract_ready": data_ready, "data_dependency_classification": data_dependency_classification, + "ai_recommendations_stale_after_core_recovered": ( + data_dependency_classification + == _AI_RECOMMENDATIONS_STALE_AFTER_CORE_RECOVERED + ), "postgres_not_ready": postgres_not_ready, "freshness_status": str(freshness_json.get("status") or "unknown"), "ingestion_status": str(ingestion_json.get("status") or "unknown"), @@ -715,6 +753,14 @@ def _blocked_safe_next_step(*, data_dependency_classification: str) -> str: "run_stockplatform_production_migration_path_after_control_channel_" "readback_then_rerun_public_api_readback" ) + if ( + data_dependency_classification + == _AI_RECOMMENDATIONS_STALE_AFTER_CORE_RECOVERED + ): + return ( + "run_stockplatform_ai_recommendation_pipeline_readback_then_rerun_" + "public_api_verifier" + ) if data_dependency_classification == "freshness_or_ingestion_not_ready": return ( "run_stockplatform_data_dependency_source_contract_readback_then_" @@ -735,6 +781,14 @@ def _data_dependency_classification( return "none" if any("postgres_not_ready" in blocker for blocker in active_blockers): return "postgres_not_ready" + data_blockers = [ + blocker + for blocker in active_blockers + if blocker.startswith("stockplatform_freshness_") + or blocker.startswith("stockplatform_ingestion_") + ] + if data_blockers == ["stockplatform_freshness_ai_recommendations_stale"]: + return _AI_RECOMMENDATIONS_STALE_AFTER_CORE_RECOVERED if any( blocker.startswith("stockplatform_freshness_") or blocker.startswith("stockplatform_ingestion_") diff --git a/apps/api/tests/test_awoooi_priority_work_order_readback_api.py b/apps/api/tests/test_awoooi_priority_work_order_readback_api.py index ebeff1886..ba80a4f07 100644 --- a/apps/api/tests/test_awoooi_priority_work_order_readback_api.py +++ b/apps/api/tests/test_awoooi_priority_work_order_readback_api.py @@ -1417,6 +1417,41 @@ def test_awoooi_priority_work_order_readback_allows_parallel_mainline_on_stockpl assert payload["rollups"]["parallel_mainline_work_item_id"] == "CIR-P0-001" +def test_awoooi_priority_work_order_readback_routes_stockplatform_ai_recommendations(): + payload = load_latest_awoooi_priority_work_order_readback() + runtime = _stockplatform_runtime_ai_recommendations_stale_after_core_recovered() + preflight = load_latest_stockplatform_public_api_controlled_recovery_preflight( + runtime_readback=runtime + ) + + apply_stockplatform_public_api_runtime_readback(payload, runtime) + apply_stockplatform_public_api_controlled_recovery_preflight(payload, preflight) + + state = payload["mainline_execution_state"] + in_progress = payload["in_progress_or_blocked_in_priority_order"][0] + evidence = in_progress["evidence"] + assert state["stockplatform_public_api_data_dependency_classification"] == ( + "ai_recommendations_stale_after_core_recovered" + ) + assert state["next_executable_mainline_state"] == ( + "controlled_recovery_preflight_waiting_stockplatform_ai_" + "recommendation_pipeline_readback_and_public_api_verifier" + ) + assert evidence["stockplatform_freshness_status"] == "blocked" + assert evidence["stockplatform_ingestion_status"] == "ok" + assert evidence["stock_blockers"] == ["ai_recommendations_stale"] + assert evidence["stock_ingestion_blockers"] == [] + assert evidence["stockplatform_source_contract_readback_status"] == ( + "stockplatform_source_contract_core_ready_ai_recommendations_stale" + ) + assert "AI recommendation pipeline" in in_progress["professional_fix"]["action"] + assert "AI recommendations are stale" in payload["next_execution_order"][0] + assert payload["mainline_parallel_execution"]["enabled"] is True + assert payload["mainline_parallel_execution"]["current_p0_wait_state"] == ( + "external_stockplatform_data_retry_wait" + ) + + def test_awoooi_priority_work_order_readback_normalizes_runtime_source_truth( monkeypatch: pytest.MonkeyPatch, ): @@ -2010,6 +2045,54 @@ def _stockplatform_runtime_source_contract_blocked() -> dict: return payload +def _stockplatform_runtime_ai_recommendations_stale_after_core_recovered() -> dict: + payload = _stockplatform_runtime_source_contract_blocked() + payload["safe_next_step"] = ( + "run_stockplatform_ai_recommendation_pipeline_readback_then_rerun_" + "public_api_verifier" + ) + payload["active_blockers"] = [ + "stockplatform_freshness_ai_recommendations_stale", + "stockplatform_post_apply_public_api_verifier_not_green", + ] + payload["readback"].update( + { + "freshness_status": "blocked", + "ingestion_status": "ok", + "freshness_blockers": ["ai_recommendations_stale"], + "ingestion_blockers": [], + "data_dependency_classification": ( + "ai_recommendations_stale_after_core_recovered" + ), + "postgres_not_ready": False, + } + ) + payload["rollups"]["data_dependency_classification"] = ( + "ai_recommendations_stale_after_core_recovered" + ) + payload["rollups"]["postgres_not_ready"] = False + payload["data_readiness_control_readback"]["status"] = ( + "blocked_stockplatform_ai_recommendations_stale_after_core_recovered" + ) + payload["data_readiness_control_readback"]["postgres_not_ready"] = False + payload["source_contract_control_readback"]["status"] = ( + "stockplatform_source_contract_core_ready_ai_recommendations_stale" + ) + payload["source_contract_control_readback"]["non_ok_source_count"] = 1 + payload["source_contract_control_readback"]["active_blockers"] = [ + "stockplatform_freshness_ai_recommendations_stale" + ] + payload["source_contract_control_readback"]["blocking_sources"] = [ + { + "source": "ai.recommendations", + "category": "freshness", + "status": "stale", + "classification": "freshness_source_non_ok", + }, + ] + return payload + + def _harbor_registry_ready() -> dict: return { "schema_version": "harbor_registry_controlled_recovery_preflight_v1", diff --git a/apps/api/tests/test_stockplatform_public_api_controlled_recovery_preflight.py b/apps/api/tests/test_stockplatform_public_api_controlled_recovery_preflight.py index 864d77bea..d127fbca9 100644 --- a/apps/api/tests/test_stockplatform_public_api_controlled_recovery_preflight.py +++ b/apps/api/tests/test_stockplatform_public_api_controlled_recovery_preflight.py @@ -184,6 +184,53 @@ def test_stockplatform_controlled_recovery_preflight_prefers_live_data_classific ) +def test_stockplatform_controlled_recovery_preflight_routes_ai_recommendations( + tmp_path: Path, +): + payload = load_latest_stockplatform_public_api_controlled_recovery_preflight( + runtime_readback=_runtime_ai_recommendations_stale_after_core_recovered(), + route_source_path=_route_source(tmp_path), + ) + + assert payload["status"] == ( + "controlled_data_dependency_preflight_packaged_waiting_control_path_readback" + ) + assert payload["api_layer_classification"] == "api_live_data_dependency_not_ready" + assert payload["data_dependency_classification"] == ( + "ai_recommendations_stale_after_core_recovered" + ) + assert payload["safe_next_step"] == ( + "run_stockplatform_ai_recommendation_pipeline_readback_then_" + "public_api_verifier" + ) + assert payload["controlled_apply_policy"]["current_apply_blocker"] == ( + "ai_recommendation_pipeline_readback_required_before_public_api_" + "verifier" + ) + assert "read_only_ai_recommendation_queue_status" in payload[ + "controlled_apply_policy" + ]["allowed_scope"] + assert payload["source_of_truth_diff"][ + "data_readiness_control_readback_status" + ] == "blocked_stockplatform_ai_recommendations_stale_after_core_recovered" + assert payload["source_of_truth_diff"][ + "source_contract_control_readback_status" + ] == "stockplatform_source_contract_core_ready_ai_recommendations_stale" + assert payload["controlled_work_items"][0]["id"] == ( + "stockplatform-ai-recommendation-control-path-readback" + ) + assert payload["controlled_work_items"][2]["id"] == ( + "stockplatform-ai-recommendation-pipeline-preflight" + ) + assert payload["controlled_action_candidates"][0]["id"] == ( + "read_only_ai_recommendation_pipeline_readback" + ) + assert payload["controlled_action_candidates"][1]["id"] == ( + "ai_recommendation_pipeline_verifier_preflight" + ) + assert payload["operation_boundaries"]["database_write_or_restore_performed"] is False + + def test_stockplatform_controlled_recovery_preflight_endpoint_returns_payload( monkeypatch, tmp_path: Path, @@ -386,3 +433,46 @@ def _runtime_data_dependency_non_postgres_blocked() -> dict: ], } return payload + + +def _runtime_ai_recommendations_stale_after_core_recovered() -> dict: + payload = _runtime_data_dependency_non_postgres_blocked() + payload["safe_next_step"] = ( + "run_stockplatform_ai_recommendation_pipeline_readback_then_rerun_" + "public_api_verifier" + ) + payload["active_blockers"] = [ + "stockplatform_freshness_ai_recommendations_stale", + "stockplatform_post_apply_public_api_verifier_not_green", + ] + payload["readback"].update( + { + "freshness_status": "blocked", + "ingestion_status": "ok", + "freshness_blockers": ["ai_recommendations_stale"], + "ingestion_blockers": [], + "data_dependency_classification": ( + "ai_recommendations_stale_after_core_recovered" + ), + } + ) + payload["rollups"]["data_dependency_classification"] = ( + "ai_recommendations_stale_after_core_recovered" + ) + payload["data_readiness_control_readback"]["status"] = ( + "blocked_stockplatform_ai_recommendations_stale_after_core_recovered" + ) + payload["source_contract_control_readback"]["status"] = ( + "stockplatform_source_contract_core_ready_ai_recommendations_stale" + ) + payload["source_contract_control_readback"]["active_blockers"] = [ + "stockplatform_freshness_ai_recommendations_stale" + ] + payload["source_contract_control_readback"]["blocking_sources"] = [ + { + "source": "ai.recommendations", + "category": "freshness", + "status": "stale", + }, + ] + return payload diff --git a/apps/api/tests/test_stockplatform_public_api_runtime_readback.py b/apps/api/tests/test_stockplatform_public_api_runtime_readback.py index 828c1dd20..e706c22f9 100644 --- a/apps/api/tests/test_stockplatform_public_api_runtime_readback.py +++ b/apps/api/tests/test_stockplatform_public_api_runtime_readback.py @@ -244,6 +244,56 @@ def test_stockplatform_public_api_runtime_readback_routes_data_dependency_withou ] +def test_stockplatform_public_api_runtime_readback_routes_ai_stale_after_core_recovered(): + payload = load_latest_stockplatform_public_api_runtime_readback( + probe=_probe_public_api_ok_ai_recommendations_stale_after_core_recovered + ) + + assert payload["status"] == "blocked_stockplatform_public_api_runtime_drift" + assert payload["readback"]["api_health_http_status"] == 200 + assert payload["readback"]["freshness_status"] == "blocked" + assert payload["readback"]["ingestion_status"] == "ok" + assert payload["readback"]["freshness_latest_trading_date"] == "2026-07-02" + assert payload["readback"]["freshness_blockers"] == [ + "ai_recommendations_stale" + ] + assert payload["readback"]["data_dependency_classification"] == ( + "ai_recommendations_stale_after_core_recovered" + ) + assert payload["readback"][ + "ai_recommendations_stale_after_core_recovered" + ] is True + assert payload["safe_next_step"] == ( + "run_stockplatform_ai_recommendation_pipeline_readback_then_rerun_" + "public_api_verifier" + ) + assert payload["data_readiness_control_readback"]["status"] == ( + "blocked_stockplatform_ai_recommendations_stale_after_core_recovered" + ) + assert payload["source_contract_control_readback"]["status"] == ( + "stockplatform_source_contract_core_ready_ai_recommendations_stale" + ) + assert payload["rollups"]["data_dependency_classification"] == ( + "ai_recommendations_stale_after_core_recovered" + ) + assert payload["rollups"][ + "ai_recommendations_stale_after_core_recovered" + ] is True + assert "stockplatform_freshness_core_margin_short_daily_missing" not in payload[ + "active_blockers" + ] + assert "stockplatform_ingestion_core.margin_short_daily_incomplete" not in payload[ + "active_blockers" + ] + assert "stockplatform_postgres_not_ready" not in payload["active_blockers"] + assert "stockplatform_freshness_ai_recommendations_stale" in payload[ + "active_blockers" + ] + assert "stockplatform_post_apply_public_api_verifier_not_green" in payload[ + "active_blockers" + ] + + def test_stockplatform_public_api_runtime_endpoint_returns_readback(monkeypatch): monkeypatch.setattr( agents, @@ -378,3 +428,50 @@ def _probe_public_api_ok_data_dependency_blocked( "error": "", } return {"http_status": 200, "body": "ok", "error": ""} + + +def _probe_public_api_ok_ai_recommendations_stale_after_core_recovered( + url: str, + timeout_seconds: float, +) -> dict: + del timeout_seconds + if url.endswith("/api/v1/system/freshness"): + return { + "http_status": 200, + "body": json.dumps( + { + "status": "blocked", + "blockers": ["ai_recommendations_stale"], + "latest_trading_date": "2026-07-02", + "sources": [ + {"source": "core.price_daily", "status": "ok", "sla": {}}, + { + "source": "core.margin_short_daily", + "latest_date": "2026-07-02", + "status": "ok", + "sla": {}, + }, + { + "source": "ai.recommendations", + "latest_date": "2026-07-01", + "status": "stale", + "sla": {}, + }, + ], + } + ), + "error": "", + } + if url.endswith("/api/v1/system/ingestion"): + return { + "http_status": 200, + "body": json.dumps( + { + "status": "ok", + "blockers": [], + "latest_trading_date": "2026-07-02", + } + ), + "error": "", + } + return {"http_status": 200, "body": "ok", "error": ""} diff --git a/docs/LOGBOOK.md b/docs/LOGBOOK.md index b8dfbf6a3..044420cf8 100644 --- a/docs/LOGBOOK.md +++ b/docs/LOGBOOK.md @@ -16,6 +16,23 @@ **仍維持**: - 未使用 GitHub / `gh` / GitHub API;未讀 secret / token / `.env` / raw sessions / SQLite / auth;未 workflow_dispatch;未重啟主機 / VM / Docker / Nginx / K3s / DB / firewall;未切 DNS / CDN / provider;未宣稱 10 分鐘全主機自動恢復 SLA 已完成。 +## 2026-07-02 — 21:19 P0-006 StockPlatform AI recommendation stale 分類收斂 + +**完成內容**: +- `stockplatform_public_api_runtime_readback` 新增 `ai_recommendations_stale_after_core_recovered` 分類:當 public API、ingestion、官方 core margin/short 已恢復,但 `ai.recommendations` 仍落後交易日時,不再誤導成 generic DB / source freshness repair。 +- `stockplatform_public_api_controlled_recovery_preflight` 已把此分類路由到 AI recommendation pipeline readback / public API verifier / KM-RAG-MCP-PlayBook metadata writeback,不碰 Docker daemon、host reboot、DB write 或 fake freshness。 +- `awoooi-priority-work-order-readback` 已同步 next state / reason / action / next execution order;P0-006 會顯示「官方 core data recovered,只剩 AI recommendations stale」,並保留 parallel mainline execution,避免外部/derived data wait 期間空等。 + +**驗證**: +- `DATABASE_URL=sqlite+aiosqlite:////tmp/awoooi-codex-api-test.db PYTHONPATH=apps/api python3.11 -m pytest -q apps/api/tests/test_stockplatform_public_api_runtime_readback.py apps/api/tests/test_stockplatform_public_api_controlled_recovery_preflight.py apps/api/tests/test_awoooi_priority_work_order_readback_api.py -p no:cacheprovider`:`31 passed`。 +- `python3.11 -m py_compile apps/api/src/services/stockplatform_public_api_runtime_readback.py apps/api/src/services/stockplatform_public_api_controlled_recovery_preflight.py apps/api/src/services/awoooi_priority_work_order_readback.py apps/api/tests/test_stockplatform_public_api_runtime_readback.py apps/api/tests/test_stockplatform_public_api_controlled_recovery_preflight.py apps/api/tests/test_awoooi_priority_work_order_readback_api.py`:通過。 +- `python3.11 ops/runner/guard-gitea-runner-pressure.py --root .`:`GITEA_RUNNER_PRESSURE_GUARD_OK`。 +- `git diff --check`:通過。 +- 本機新 readback 打 live `https://stock.wooo.work`:`runtime_status=stockplatform_public_api_runtime_ready`、`freshness_status=ok`、`ingestion_status=ok`、`active_blockers=[]`。 + +**仍維持**: +- 未讀 secret / token / `.env` / raw sessions / SQLite / auth;未使用 GitHub / gh;未 SSH、未 Docker / K8s / firewall / Wazuh 寫入、未重啟 host / VM / service、未寫 StockPlatform DB、未偽造 freshness marker。 + ## 2026-07-02 — 20:56 CIR-P0-TG-001 Telegram alert AI Loop post-apply verifier readback **完成內容**: