From ae5844733db96473d0ee90ace63f40762f50c0fc Mon Sep 17 00:00:00 2001 From: Your Name Date: Thu, 2 Jul 2026 18:39:40 +0800 Subject: [PATCH] feat(stockplatform): surface data readiness receipt --- .../awoooi_priority_work_order_readback.py | 142 +++++++++++- ...ublic_api_controlled_recovery_preflight.py | 37 ++- ...ockplatform_public_api_runtime_readback.py | 214 ++++++++++++++++-- ...awoooi_priority_work_order_readback_api.py | 32 +++ ...ublic_api_controlled_recovery_preflight.py | 87 +++++++ ...ockplatform_public_api_runtime_readback.py | 94 ++++++++ apps/web/messages/en.json | 25 ++ apps/web/messages/zh-TW.json | 25 ++ .../app/[locale]/awooop/work-items/page.tsx | 184 +++++++++++++++ docs/LOGBOOK.md | 19 ++ 10 files changed, 835 insertions(+), 24 deletions(-) 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 18c8e247..17d33516 100644 --- a/apps/api/src/services/awoooi_priority_work_order_readback.py +++ b/apps/api/src/services/awoooi_priority_work_order_readback.py @@ -994,6 +994,9 @@ def apply_stockplatform_public_api_runtime_readback( runtime_blockers = _strings(runtime_readback.get("active_blockers")) runtime_rollups = _dict(runtime_readback.get("rollups")) runtime_readback_body = _dict(runtime_readback.get("readback")) + data_readiness_readback = _dict( + runtime_readback.get("data_readiness_control_readback") + ) data_dependency_classification = str( runtime_rollups.get("data_dependency_classification") or runtime_readback_body.get("data_dependency_classification") @@ -1033,6 +1036,18 @@ def apply_stockplatform_public_api_runtime_readback( data_dependency_classification ) state["stockplatform_public_api_postgres_not_ready"] = postgres_not_ready + state["stockplatform_postgres_readiness_or_data_source_readback_status"] = str( + data_readiness_readback.get("status") or "unknown" + ) + state["stockplatform_postgres_readiness_or_data_source_readback_present"] = bool( + data_readiness_readback.get("readback_present") is True + ) + state["stockplatform_postgres_readiness_or_data_source_receipt_provided"] = bool( + data_readiness_readback.get("receipt_provided") is True + ) + state["stockplatform_postgres_readiness_or_data_source_postgres_ready"] = bool( + data_readiness_readback.get("postgres_ready") is True + ) for item in _list(payload.get("in_progress_or_blocked_in_priority_order")): workplan = _dict(item) @@ -1063,6 +1078,18 @@ def apply_stockplatform_public_api_runtime_readback( data_dependency_classification ) evidence["stockplatform_postgres_not_ready"] = postgres_not_ready + evidence[ + "stockplatform_postgres_readiness_or_data_source_readback_status" + ] = state["stockplatform_postgres_readiness_or_data_source_readback_status"] + evidence[ + "stockplatform_postgres_readiness_or_data_source_readback_present" + ] = state["stockplatform_postgres_readiness_or_data_source_readback_present"] + evidence[ + "stockplatform_postgres_readiness_or_data_source_receipt_provided" + ] = state["stockplatform_postgres_readiness_or_data_source_receipt_provided"] + evidence[ + "stockplatform_postgres_readiness_or_data_source_postgres_ready" + ] = state["stockplatform_postgres_readiness_or_data_source_postgres_ready"] evidence["stockplatform_recovery_control_path_status"] = ( recovery_control_path_status ) @@ -1079,12 +1106,14 @@ def apply_stockplatform_public_api_runtime_readback( ) workplan["reason"] = ( _stockplatform_runtime_drift_reason( + data_dependency_classification=data_dependency_classification, postgres_not_ready=postgres_not_ready, ) ) professional_fix = _dict(workplan.setdefault("professional_fix", {})) professional_fix["action"] = ( _stockplatform_runtime_drift_action( + data_dependency_classification=data_dependency_classification, postgres_not_ready=postgres_not_ready, ) ) @@ -1108,11 +1137,15 @@ def apply_stockplatform_public_api_runtime_readback( "P0-006-STOCKPLATFORM-PUBLIC-API-RUNTIME-READBACK" ) state["next_executable_mainline_state"] = ( - _stockplatform_runtime_next_state(postgres_not_ready=postgres_not_ready) + _stockplatform_runtime_next_state( + data_dependency_classification=data_dependency_classification, + postgres_not_ready=postgres_not_ready, + ) ) payload["status"] = "p0_006_blocked_stockplatform_public_api_runtime_drift" payload["next_execution_order"] = [ _stockplatform_runtime_first_order( + data_dependency_classification=data_dependency_classification, postgres_not_ready=postgres_not_ready, ), ( @@ -1152,6 +1185,9 @@ def apply_stockplatform_public_api_controlled_recovery_preflight( source_diff = _dict(recovery_preflight.get("source_of_truth_diff")) target_selector = _dict(recovery_preflight.get("target_selector")) controlled_policy = _dict(recovery_preflight.get("controlled_apply_policy")) + data_readiness_readback = _dict( + recovery_preflight.get("data_readiness_control_readback") + ) candidate_packaged = bool( rollups.get("controlled_recovery_candidate_packaged") is True ) @@ -1167,6 +1203,18 @@ def apply_stockplatform_public_api_controlled_recovery_preflight( state["stockplatform_public_api_controlled_recovery_data_dependency"] = ( data_dependency_classification ) + state["stockplatform_postgres_readiness_or_data_source_readback_status"] = str( + data_readiness_readback.get("status") or "unknown" + ) + state["stockplatform_postgres_readiness_or_data_source_readback_present"] = bool( + data_readiness_readback.get("readback_present") is True + ) + state["stockplatform_postgres_readiness_or_data_source_receipt_provided"] = bool( + data_readiness_readback.get("receipt_provided") is True + ) + state["stockplatform_postgres_readiness_or_data_source_postgres_ready"] = bool( + data_readiness_readback.get("postgres_ready") is True + ) state["stockplatform_public_api_controlled_recovery_active_blockers"] = _strings( recovery_preflight.get("active_blockers") ) @@ -1208,6 +1256,18 @@ def apply_stockplatform_public_api_controlled_recovery_preflight( evidence["stockplatform_source_of_truth_diff_status"] = str( source_diff.get("status") or "" ) + evidence[ + "stockplatform_postgres_readiness_or_data_source_readback_status" + ] = state["stockplatform_postgres_readiness_or_data_source_readback_status"] + evidence[ + "stockplatform_postgres_readiness_or_data_source_readback_present" + ] = state["stockplatform_postgres_readiness_or_data_source_readback_present"] + evidence[ + "stockplatform_postgres_readiness_or_data_source_receipt_provided" + ] = state["stockplatform_postgres_readiness_or_data_source_receipt_provided"] + evidence[ + "stockplatform_postgres_readiness_or_data_source_postgres_ready" + ] = state["stockplatform_postgres_readiness_or_data_source_postgres_ready"] evidence["stockplatform_controlled_recovery_candidate_packaged"] = ( candidate_packaged ) @@ -1291,6 +1351,14 @@ def apply_stockplatform_public_api_controlled_recovery_preflight( summary["stockplatform_public_api_host_control_path_readback_required"] = bool( rollups.get("host_control_path_readback_required") is True ) + summary["stockplatform_postgres_readiness_or_data_source_readback_status"] = str( + state.get("stockplatform_postgres_readiness_or_data_source_readback_status") + or "unknown" + ) + summary["stockplatform_postgres_readiness_or_data_source_receipt_provided"] = bool( + state.get("stockplatform_postgres_readiness_or_data_source_receipt_provided") + is True + ) def apply_harbor_registry_controlled_recovery_preflight( @@ -2493,19 +2561,32 @@ def _stockplatform_recovery_first_order( ) -def _stockplatform_runtime_next_state(*, postgres_not_ready: bool) -> str: +def _stockplatform_runtime_next_state( + *, + data_dependency_classification: str, + postgres_not_ready: bool, +) -> str: if postgres_not_ready: return ( "blocked_live_stockplatform_public_api_requires_production_" "migration_control_channel_readback" ) + if data_dependency_classification == "freshness_or_ingestion_not_ready": + return ( + "blocked_live_stockplatform_public_api_requires_data_dependency_" + "source_contract_readback" + ) return ( "blocked_live_stockplatform_public_api_requires_separate_runtime_" "control_path_recovery_without_daemon_restart" ) -def _stockplatform_runtime_drift_reason(*, postgres_not_ready: bool) -> str: +def _stockplatform_runtime_drift_reason( + *, + data_dependency_classification: str, + postgres_not_ready: bool, +) -> str: if postgres_not_ready: return ( "StockPlatform public API health is now HTTP 200, but freshness and " @@ -2513,6 +2594,13 @@ def _stockplatform_runtime_drift_reason(*, postgres_not_ready: bool) -> str: "the production migration/control-channel 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 " + "ingestion readbacks are not green. P0-006 must stay on the data " + "dependency/source contract readback path before claiming the reboot " + "SLO lane is only waiting on a fresh boot window." + ) return ( "Committed P0-006 scorecard says StockPlatform freshness and ingestion " "are ok, but live public API readback is blocked. This must remain " @@ -2521,7 +2609,11 @@ def _stockplatform_runtime_drift_reason(*, postgres_not_ready: bool) -> str: ) -def _stockplatform_runtime_drift_action(*, postgres_not_ready: bool) -> str: +def _stockplatform_runtime_drift_action( + *, + data_dependency_classification: str, + postgres_not_ready: bool, +) -> str: if postgres_not_ready: return ( "Recover the StockPlatform production migration/control channel " @@ -2531,6 +2623,15 @@ def _stockplatform_runtime_drift_action(*, postgres_not_ready: bool) -> str: "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 " + "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." + ) return ( "Recover the StockPlatform API container/runtime control path with a " "separate bounded plan, then rerun public API readback. Do not restart " @@ -2539,7 +2640,11 @@ def _stockplatform_runtime_drift_action(*, postgres_not_ready: bool) -> str: ) -def _stockplatform_runtime_first_order(*, postgres_not_ready: bool) -> str: +def _stockplatform_runtime_first_order( + *, + data_dependency_classification: str, + postgres_not_ready: bool, +) -> str: if postgres_not_ready: return ( "P0-006-STOCKPLATFORM-PUBLIC-API-RUNTIME-READBACK: live public " @@ -2547,6 +2652,13 @@ def _stockplatform_runtime_first_order(*, postgres_not_ready: bool) -> str: "postgres_not_ready; run the production migration/control-channel " "path 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 " + "StockPlatform API health is 200 but freshness/ingestion are not " + "green; run the data dependency/source contract readback before " + "claiming the lane is only reboot-window gated." + ) return ( "P0-006-STOCKPLATFORM-PUBLIC-API-RUNTIME-READBACK: live public " "StockPlatform API is blocked while committed scorecard still says " @@ -3094,6 +3206,18 @@ def _refresh_rollups_after_stockplatform_overlay( rollups["stockplatform_public_api_recovery_control_path_blocker_count"] = len( recovery_control_path_blockers ) + rollups["stockplatform_postgres_readiness_or_data_source_readback_present"] = bool( + state.get("stockplatform_postgres_readiness_or_data_source_readback_present") + is True + ) + rollups["stockplatform_postgres_readiness_or_data_source_receipt_provided"] = bool( + state.get("stockplatform_postgres_readiness_or_data_source_receipt_provided") + is True + ) + rollups["stockplatform_postgres_readiness_or_data_source_postgres_ready"] = bool( + state.get("stockplatform_postgres_readiness_or_data_source_postgres_ready") + is True + ) rollups["active_p0_live_active_blocker_count"] = len(active_blockers) rollups["active_p0_event_gated_by_fresh_reboot_window_only"] = ( rollups.get("active_p0_event_gated_by_fresh_reboot_window_only") is True @@ -3118,6 +3242,14 @@ def _refresh_rollups_after_stockplatform_overlay( summary["stockplatform_public_api_recovery_control_path_blockers"] = ( recovery_control_path_blockers ) + summary["stockplatform_postgres_readiness_or_data_source_readback_status"] = str( + state.get("stockplatform_postgres_readiness_or_data_source_readback_status") + or "unknown" + ) + summary["stockplatform_postgres_readiness_or_data_source_receipt_provided"] = bool( + state.get("stockplatform_postgres_readiness_or_data_source_receipt_provided") + is True + ) summary["next_executable_mainline_workplan_id"] = str( state.get("next_executable_mainline_workplan_id") or "" ) 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 4a477fac..59ed8a3f 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 @@ -48,6 +48,9 @@ def _build_payload( runtime_ready = runtime.get("status") == "stockplatform_public_api_runtime_ready" readback = _dict(runtime.get("readback")) rollups = _dict(runtime.get("rollups")) + data_readiness_control_readback = _dict( + runtime.get("data_readiness_control_readback") + ) api_layer_classification = _classify_api_layer(readback, rollups, runtime_ready) data_dependency_classification = _data_dependency_classification(runtime) route_target_ready = route_contract.get("route_target_ready") is True @@ -84,6 +87,7 @@ def _build_payload( candidate_packaged=candidate_packaged, route_target_ready=route_target_ready, api_layer_classification=api_layer_classification, + data_dependency_classification=data_dependency_classification, ), "active_blockers": active_blockers, "active_blocker_count": len(active_blockers), @@ -118,9 +122,16 @@ def _build_payload( "ingestion_http_status": readback.get("ingestion_http_status"), "http_502_count": _int(rollups.get("http_502_count")), "data_dependency_classification": data_dependency_classification, + "data_readiness_control_readback_status": ( + data_readiness_control_readback.get("status") + ), + "data_readiness_control_receipt_provided": ( + data_readiness_control_readback.get("receipt_provided") is True + ), "route_source_file": str(route_contract.get("route_source_file") or ""), "route_target": str(route_contract.get("route_target") or ""), }, + "data_readiness_control_readback": data_readiness_control_readback, "controlled_apply_policy": { "risk_level": "high", "break_glass_required": False, @@ -184,6 +195,12 @@ def _build_payload( _post_apply_verifiers(runtime_ready=runtime_ready) ), "learning_writeback_contract_count": len(_learning_writeback_contracts()), + "postgres_readiness_or_data_source_readback_present": ( + data_readiness_control_readback.get("readback_present") is True + ), + "postgres_readiness_or_data_source_receipt_provided": ( + data_readiness_control_readback.get("receipt_provided") is True + ), }, "operation_boundaries": { "read_only_public_https_probe": True, @@ -258,12 +275,20 @@ def _classify_api_layer( def _data_dependency_classification(runtime: dict[str, Any]) -> str: + runtime_rollups = _dict(runtime.get("rollups")) + runtime_readback = _dict(runtime.get("readback")) + live_classification = str( + runtime_rollups.get("data_dependency_classification") + or runtime_readback.get("data_dependency_classification") + or "" + ) + if live_classification and live_classification != "none": + return live_classification blockers = _strings(runtime.get("active_blockers")) - readback = _dict(runtime.get("readback")) - if readback.get("api_health_http_status") != 200: + if runtime_readback.get("api_health_http_status") != 200: return "none" if ( - readback.get("postgres_not_ready") is True + runtime_readback.get("postgres_not_ready") is True or any("postgres_not_ready" in blocker for blocker in blockers) ): return "postgres_not_ready" @@ -395,6 +420,7 @@ def _safe_next_step( candidate_packaged: bool, route_target_ready: bool, api_layer_classification: str, + data_dependency_classification: str, ) -> str: if runtime_ready: return "stockplatform_public_api_ready_keep_p0_006_reboot_window_gate" @@ -402,6 +428,11 @@ def _safe_next_step( candidate_packaged and api_layer_classification == "api_live_data_dependency_not_ready" ): + if data_dependency_classification != "postgres_not_ready": + return ( + "run_stockplatform_data_dependency_source_contract_readback_" + "then_public_api_verifier" + ) return ( "run_stockplatform_production_migration_path_after_control_channel_" "readback_then_public_api_verifier" 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 97c952a1..200b789d 100644 --- a/apps/api/src/services/stockplatform_public_api_runtime_readback.py +++ b/apps/api/src/services/stockplatform_public_api_runtime_readback.py @@ -19,6 +19,12 @@ from src.services.reboot_auto_recovery_slo_scorecard import ( from src.services.snapshot_paths import default_operations_dir _API_SCHEMA_VERSION = "stockplatform_public_api_runtime_readback_v1" +_DATA_READINESS_SCHEMA_VERSION = ( + "stockplatform_postgres_readiness_or_data_source_readback_v1" +) +_DATA_READINESS_RECEIPT_KEY = ( + "stockplatform_postgres_readiness_or_data_source_readback" +) _DEFAULT_OPERATIONS_DIR = default_operations_dir(Path(__file__)) _RECOVERY_RECEIPT_FILE = ( "stockplatform-public-api-runtime-recovery-control-receipt.snapshot.json" @@ -125,19 +131,30 @@ def _build_payload( ingestion_json=ingestion_json, ) ready = all(checks.values()) - recovery_control_path = _recovery_control_path_readback( - runtime_ready=ready, - receipt=recovery_control_receipt, - ) - recovery_control_path_blockers = _strings( - recovery_control_path.get("active_blockers") - ) - active_blockers = _unique_strings(active_blockers + recovery_control_path_blockers) data_dependency_classification = _data_dependency_classification( active_blockers, api_health_ok=checks["public_api_health_ok"], ) postgres_not_ready = data_dependency_classification == "postgres_not_ready" + data_readiness_control_readback = _data_readiness_control_readback( + checks=checks, + freshness=freshness, + ingestion=ingestion, + freshness_json=freshness_json, + ingestion_json=ingestion_json, + data_dependency_classification=data_dependency_classification, + postgres_not_ready=postgres_not_ready, + data_dependency_blockers=active_blockers, + ) + recovery_control_path = _recovery_control_path_readback( + runtime_ready=ready, + receipt=recovery_control_receipt, + data_readiness_control_readback=data_readiness_control_readback, + ) + recovery_control_path_blockers = _strings( + recovery_control_path.get("active_blockers") + ) + active_blockers = _unique_strings(active_blockers + recovery_control_path_blockers) committed_freshness_ok = committed_stockplatform.get("freshness_status") == "ok" committed_ingestion_ok = committed_stockplatform.get("ingestion_status") == "ok" committed_ok = committed_freshness_ok and committed_ingestion_ok @@ -151,7 +168,7 @@ def _build_payload( "stockplatform_public_api_ready_keep_p0_006_reboot_window_gate" if ready else _blocked_safe_next_step( - postgres_not_ready=postgres_not_ready, + data_dependency_classification=data_dependency_classification, ) ) http_statuses = { @@ -194,6 +211,12 @@ def _build_payload( ), "recovery_control_path_status": recovery_control_path.get("status"), "recovery_control_path_active_blockers": recovery_control_path_blockers, + "data_readiness_control_readback_status": ( + data_readiness_control_readback.get("status") + ), + "data_readiness_control_readback_present": ( + data_readiness_control_readback.get("readback_present") is True + ), }, "rollups": { "runtime_ready": ready, @@ -224,7 +247,14 @@ def _build_payload( ), "data_dependency_classification": data_dependency_classification, "postgres_not_ready": postgres_not_ready, + "postgres_readiness_or_data_source_readback_present": ( + data_readiness_control_readback.get("readback_present") is True + ), + "postgres_readiness_or_data_source_receipt_provided": ( + data_readiness_control_readback.get("receipt_provided") is True + ), }, + "data_readiness_control_readback": data_readiness_control_readback, "recovery_control_path": recovery_control_path, "probes": probes, "operation_boundaries": { @@ -249,20 +279,38 @@ def _recovery_control_path_readback( *, runtime_ready: bool, receipt: dict[str, Any], + data_readiness_control_readback: dict[str, Any], ) -> dict[str, Any]: if runtime_ready: return { "status": "not_required_stockplatform_runtime_ready", "active_blockers": [], "receipt": receipt, + "provided_receipts": _provided_receipts_with_live_readback( + _strings(receipt.get("provided_receipts")), + data_readiness_control_readback, + ), + "missing_receipts": _missing_receipts_after_live_readback( + _strings(receipt.get("missing_receipts")), + data_readiness_control_readback, + ), "safe_recovery_channels": [ "keep_monitoring_public_api_runtime_readback", ], } if receipt: - active_blockers = _unique_strings( - _strings(receipt.get("active_blockers")) - or _strings(receipt.get("missing_receipts")) + provided_receipts = _provided_receipts_with_live_readback( + _strings(receipt.get("provided_receipts")), + data_readiness_control_readback, + ) + missing_receipts = _missing_receipts_after_live_readback( + _strings(receipt.get("missing_receipts")), + data_readiness_control_readback, + ) + active_blockers = _recovery_control_active_blockers( + receipt_blockers=_strings(receipt.get("active_blockers")), + missing_receipts=missing_receipts, + data_readiness_control_readback=data_readiness_control_readback, ) return { "status": str( @@ -271,8 +319,13 @@ def _recovery_control_path_readback( ), "active_blockers": active_blockers, "receipt": receipt, - "provided_receipts": _strings(receipt.get("provided_receipts")), - "missing_receipts": _strings(receipt.get("missing_receipts")), + "provided_receipts": provided_receipts, + "missing_receipts": missing_receipts, + "live_data_readiness_receipt_key": ( + _DATA_READINESS_RECEIPT_KEY + if data_readiness_control_readback.get("receipt_provided") is True + else "" + ), "safe_recovery_channels": _strings( receipt.get("safe_recovery_channels") ), @@ -310,6 +363,130 @@ def _recovery_control_path_readback( } +def _data_readiness_control_readback( + *, + checks: dict[str, bool], + freshness: dict[str, Any], + ingestion: dict[str, Any], + freshness_json: dict[str, Any], + ingestion_json: dict[str, Any], + data_dependency_classification: str, + postgres_not_ready: bool, + data_dependency_blockers: list[str], +) -> dict[str, Any]: + freshness_http_status = freshness.get("http_status") + ingestion_http_status = ingestion.get("http_status") + readback_present = freshness_http_status == 200 and ingestion_http_status == 200 + data_ready = ( + readback_present + and freshness_json.get("status") == "ok" + and ingestion_json.get("status") == "ok" + ) + if data_ready: + status = "stockplatform_data_readiness_ready" + safe_next_step = "run_post_apply_public_api_verifier_and_clear_p0_006_blocker" + elif readback_present and postgres_not_ready: + status = "blocked_stockplatform_postgres_not_ready" + safe_next_step = ( + "run_stockplatform_production_migration_path_after_control_channel_" + "readback_no_manual_db_write" + ) + elif readback_present: + status = "blocked_stockplatform_data_dependency_not_ready" + safe_next_step = ( + "diff_stockplatform_freshness_ingestion_source_contract_then_rerun_" + "public_api_verifier" + ) + else: + status = "blocked_stockplatform_data_readiness_readback_unavailable" + safe_next_step = ( + "restore_public_api_data_endpoints_or_control_channel_readback_then_" + "rerun_data_readiness_readback" + ) + missing_evidence = [] + if freshness_http_status != 200: + missing_evidence.append("freshness_endpoint_http_200") + if ingestion_http_status != 200: + missing_evidence.append("ingestion_endpoint_http_200") + return { + "schema_version": _DATA_READINESS_SCHEMA_VERSION, + "receipt_key": _DATA_READINESS_RECEIPT_KEY, + "status": status, + "source": "stockplatform_public_api_freshness_ingestion_probe", + "readback_present": readback_present, + "receipt_provided": readback_present, + "safe_next_step": safe_next_step, + "data_ready": data_ready, + "postgres_ready": data_ready and not postgres_not_ready, + "postgres_not_ready": postgres_not_ready, + "data_dependency_classification": data_dependency_classification, + "freshness_http_status": freshness_http_status, + "ingestion_http_status": ingestion_http_status, + "freshness_status": str(freshness_json.get("status") or "unknown"), + "ingestion_status": str(ingestion_json.get("status") or "unknown"), + "freshness_blockers": _strings(freshness_json.get("blockers")), + "ingestion_blockers": _strings(ingestion_json.get("blockers")), + "active_blockers": [ + blocker + for blocker in data_dependency_blockers + if blocker.startswith("stockplatform_freshness_") + or blocker.startswith("stockplatform_ingestion_") + ], + "missing_evidence": missing_evidence, + "operation_boundaries": { + "public_https_probe_only": True, + "ssh_used": False, + "docker_command_performed": False, + "database_write_or_restore_performed": False, + "stockplatform_manual_data_write_performed": False, + "secret_value_collection_allowed": False, + "runtime_action_performed": False, + }, + } + + +def _provided_receipts_with_live_readback( + provided_receipts: list[str], + data_readiness_control_readback: dict[str, Any], +) -> list[str]: + if data_readiness_control_readback.get("receipt_provided") is not True: + return provided_receipts + return _unique_strings([*provided_receipts, _DATA_READINESS_RECEIPT_KEY]) + + +def _missing_receipts_after_live_readback( + missing_receipts: list[str], + data_readiness_control_readback: dict[str, Any], +) -> list[str]: + if data_readiness_control_readback.get("receipt_provided") is not True: + return missing_receipts + return [ + receipt + for receipt in missing_receipts + if receipt != _DATA_READINESS_RECEIPT_KEY + ] + + +def _recovery_control_active_blockers( + *, + receipt_blockers: list[str], + missing_receipts: list[str], + data_readiness_control_readback: dict[str, Any], +) -> list[str]: + blockers = receipt_blockers or missing_receipts + if data_readiness_control_readback.get("receipt_provided") is not True: + return _unique_strings(blockers) + if data_readiness_control_readback.get("postgres_not_ready") is True: + return _unique_strings(blockers) + return _unique_strings( + [ + blocker + for blocker in blockers + if blocker != "stockplatform_postgres_not_ready" + ] + ) + + def _load_recovery_control_receipt(operations_dir: Path) -> dict[str, Any]: path = operations_dir / _RECOVERY_RECEIPT_FILE if not path.exists(): @@ -327,12 +504,17 @@ def _load_recovery_control_receipt(operations_dir: Path) -> dict[str, Any]: return payload -def _blocked_safe_next_step(*, postgres_not_ready: bool) -> str: - if postgres_not_ready: +def _blocked_safe_next_step(*, data_dependency_classification: str) -> str: + if data_dependency_classification == "postgres_not_ready": return ( "run_stockplatform_production_migration_path_after_control_channel_" "readback_then_rerun_public_api_readback" ) + if data_dependency_classification == "freshness_or_ingestion_not_ready": + return ( + "run_stockplatform_data_dependency_source_contract_readback_then_" + "rerun_public_api_verifier" + ) return ( "recover_stockplatform_api_container_runtime_or_docker_control_path_" "without_daemon_restart_then_rerun_public_api_readback" 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 8753eba8..baa9c1b2 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 @@ -1255,6 +1255,27 @@ def test_awoooi_priority_work_order_readback_routes_stockplatform_data_dependenc assert evidence["stockplatform_controlled_recovery_data_dependency"] == ( "postgres_not_ready" ) + assert evidence[ + "stockplatform_postgres_readiness_or_data_source_readback_status" + ] == "blocked_stockplatform_postgres_not_ready" + assert evidence[ + "stockplatform_postgres_readiness_or_data_source_readback_present" + ] is True + assert evidence[ + "stockplatform_postgres_readiness_or_data_source_receipt_provided" + ] is True + assert evidence[ + "stockplatform_postgres_readiness_or_data_source_postgres_ready" + ] is False + assert payload["rollups"][ + "stockplatform_postgres_readiness_or_data_source_readback_present" + ] is True + assert payload["rollups"][ + "stockplatform_postgres_readiness_or_data_source_receipt_provided" + ] is True + assert payload["summary"][ + "stockplatform_postgres_readiness_or_data_source_receipt_provided" + ] is True assert "production migration" in in_progress["professional_fix"]["action"] assert "Do not restart Docker daemon" in in_progress["professional_fix"]["action"] assert "manually write DB rows" in in_progress["professional_fix"]["action"] @@ -1581,6 +1602,17 @@ def _stockplatform_runtime_data_dependency_blocked() -> dict: "stockplatform_controlled_deploy_or_ssh_readback_required", ], }, + "data_readiness_control_readback": { + "schema_version": ( + "stockplatform_postgres_readiness_or_data_source_readback_v1" + ), + "receipt_key": "stockplatform_postgres_readiness_or_data_source_readback", + "status": "blocked_stockplatform_postgres_not_ready", + "readback_present": True, + "receipt_provided": True, + "postgres_ready": False, + "postgres_not_ready": True, + }, } 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 496ae354..85bf0f60 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 @@ -102,6 +102,21 @@ def test_stockplatform_controlled_recovery_preflight_packages_data_dependency( assert payload["source_of_truth_diff"]["data_dependency_classification"] == ( "postgres_not_ready" ) + assert payload["source_of_truth_diff"][ + "data_readiness_control_readback_status" + ] == "blocked_stockplatform_postgres_not_ready" + assert payload["source_of_truth_diff"][ + "data_readiness_control_receipt_provided" + ] is True + assert payload["data_readiness_control_readback"]["schema_version"] == ( + "stockplatform_postgres_readiness_or_data_source_readback_v1" + ) + assert payload["rollups"][ + "postgres_readiness_or_data_source_readback_present" + ] is True + assert payload["rollups"][ + "postgres_readiness_or_data_source_receipt_provided" + ] is True assert payload["controlled_apply_policy"]["current_apply_blocker"] == ( "postgres_readiness_and_production_migration_control_path_readback_" "required_before_apply" @@ -131,6 +146,34 @@ def test_stockplatform_controlled_recovery_preflight_packages_data_dependency( assert payload["operation_boundaries"]["database_write_or_restore_performed"] is False +def test_stockplatform_controlled_recovery_preflight_prefers_live_data_classification( + tmp_path: Path, +): + payload = load_latest_stockplatform_public_api_controlled_recovery_preflight( + runtime_readback=_runtime_data_dependency_non_postgres_blocked(), + 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"] == ( + "freshness_or_ingestion_not_ready" + ) + assert payload["safe_next_step"] == ( + "run_stockplatform_data_dependency_source_contract_readback_" + "then_public_api_verifier" + ) + assert payload["controlled_apply_policy"]["current_apply_blocker"] == ( + "data_dependency_control_path_readback_required_before_source_" + "freshness_or_postgres_contract_apply" + ) + assert payload["controlled_action_candidates"][1]["id"] == ( + "source_freshness_contract_preflight" + ) + + def test_stockplatform_controlled_recovery_preflight_endpoint_returns_payload( monkeypatch, tmp_path: Path, @@ -251,4 +294,48 @@ def _runtime_data_dependency_blocked() -> dict: "rollups": { "http_502_count": 0, }, + "data_readiness_control_readback": { + "schema_version": ( + "stockplatform_postgres_readiness_or_data_source_readback_v1" + ), + "receipt_key": "stockplatform_postgres_readiness_or_data_source_readback", + "status": "blocked_stockplatform_postgres_not_ready", + "readback_present": True, + "receipt_provided": True, + "postgres_ready": False, + "postgres_not_ready": True, + }, } + + +def _runtime_data_dependency_non_postgres_blocked() -> dict: + payload = _runtime_data_dependency_blocked() + payload["active_blockers"] = [ + "stockplatform_freshness_core_margin_short_daily_missing", + "stockplatform_freshness_ai_recommendations_stale", + "stockplatform_ingestion_status_not_ok", + "stockplatform_postgres_not_ready", + ] + payload["readback"] = { + "web_health_http_status": 200, + "api_health_http_status": 200, + "freshness_http_status": 200, + "ingestion_http_status": 200, + "data_dependency_classification": "freshness_or_ingestion_not_ready", + "postgres_not_ready": False, + } + payload["rollups"] = { + "http_502_count": 0, + "data_dependency_classification": "freshness_or_ingestion_not_ready", + "postgres_not_ready": False, + } + payload["data_readiness_control_readback"] = { + "schema_version": "stockplatform_postgres_readiness_or_data_source_readback_v1", + "receipt_key": "stockplatform_postgres_readiness_or_data_source_readback", + "status": "blocked_stockplatform_data_dependency_not_ready", + "readback_present": True, + "receipt_provided": True, + "postgres_ready": False, + "postgres_not_ready": False, + } + 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 99ec92c9..61003107 100644 --- a/apps/api/tests/test_stockplatform_public_api_runtime_readback.py +++ b/apps/api/tests/test_stockplatform_public_api_runtime_readback.py @@ -127,6 +127,42 @@ def test_stockplatform_public_api_runtime_readback_routes_postgres_not_ready(): assert payload["readback"]["postgres_not_ready"] is True assert payload["rollups"]["http_502_count"] == 0 assert payload["rollups"]["postgres_not_ready"] is True + assert ( + payload["rollups"][ + "postgres_readiness_or_data_source_readback_present" + ] + is True + ) + assert ( + payload["rollups"][ + "postgres_readiness_or_data_source_receipt_provided" + ] + is True + ) + data_readiness = payload["data_readiness_control_readback"] + assert data_readiness["schema_version"] == ( + "stockplatform_postgres_readiness_or_data_source_readback_v1" + ) + assert data_readiness["receipt_key"] == ( + "stockplatform_postgres_readiness_or_data_source_readback" + ) + assert data_readiness["status"] == "blocked_stockplatform_postgres_not_ready" + assert data_readiness["readback_present"] is True + assert data_readiness["receipt_provided"] is True + assert data_readiness["postgres_ready"] is False + assert data_readiness["postgres_not_ready"] is True + assert data_readiness["operation_boundaries"]["public_https_probe_only"] is True + assert data_readiness["operation_boundaries"]["database_write_or_restore_performed"] is False + assert data_readiness["operation_boundaries"]["secret_value_collection_allowed"] is False + assert "stockplatform_postgres_readiness_or_data_source_readback" in payload[ + "recovery_control_path" + ]["provided_receipts"] + assert "stockplatform_postgres_readiness_or_data_source_readback" not in payload[ + "recovery_control_path" + ]["missing_receipts"] + assert payload["recovery_control_path"]["missing_receipts"] == [ + "post_apply_public_api_verifier_green" + ] assert payload["safe_next_step"] == ( "run_stockplatform_production_migration_path_after_control_channel_" "readback_then_rerun_public_api_readback" @@ -139,6 +175,36 @@ def test_stockplatform_public_api_runtime_readback_routes_postgres_not_ready(): ] +def test_stockplatform_public_api_runtime_readback_routes_data_dependency_without_postgres(): + payload = load_latest_stockplatform_public_api_runtime_readback( + probe=_probe_public_api_ok_data_dependency_blocked + ) + + 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"] == "unknown" + assert payload["readback"]["data_dependency_classification"] == ( + "freshness_or_ingestion_not_ready" + ) + assert payload["readback"]["postgres_not_ready"] is False + assert payload["safe_next_step"] == ( + "run_stockplatform_data_dependency_source_contract_readback_then_" + "rerun_public_api_verifier" + ) + assert payload["data_readiness_control_readback"]["status"] == ( + "blocked_stockplatform_data_dependency_not_ready" + ) + assert payload["data_readiness_control_readback"]["receipt_provided"] is True + assert "stockplatform_postgres_not_ready" not in payload["active_blockers"] + assert payload["recovery_control_path"]["active_blockers"] == [ + "stockplatform_post_apply_public_api_verifier_not_green" + ] + assert payload["recovery_control_path"]["missing_receipts"] == [ + "post_apply_public_api_verifier_green" + ] + + def test_stockplatform_public_api_runtime_endpoint_returns_readback(monkeypatch): monkeypatch.setattr( agents, @@ -201,3 +267,31 @@ def _probe_public_api_ok_postgres_not_ready( "error": "", } return {"http_status": 200, "body": "ok", "error": ""} + + +def _probe_public_api_ok_data_dependency_blocked( + 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": [ + "core_margin_short_daily_missing", + "ai_recommendations_stale", + ], + } + ), + "error": "", + } + if url.endswith("/api/v1/system/ingestion"): + return { + "http_status": 200, + "body": json.dumps({"status": "unknown", "blockers": []}), + "error": "", + } + return {"http_status": 200, "body": "ok", "error": ""} diff --git a/apps/web/messages/en.json b/apps/web/messages/en.json index 0ac8b24f..cdd4f8ef 100644 --- a/apps/web/messages/en.json +++ b/apps/web/messages/en.json @@ -9180,6 +9180,31 @@ "evidenceBoundary": "證據邊界" } }, + "stockplatformP0Receipt": { + "eyebrow": "P0-006 Runtime receipt", + "title": "StockPlatform data dependency readback", + "subtitle": "Shows the public API data readiness receipt, dependency classification, and next controlled step.", + "preflight": "Controlled preflight", + "next": "Current next step", + "empty": "Waiting for priority readback", + "metrics": { + "runtime": "Runtime", + "receipt": "Receipt", + "dataDependency": "Data dependency", + "postgres": "Postgres" + }, + "values": { + "received": "received", + "missing": "missing", + "ready": "ready", + "notReady": "not ready" + }, + "boundaries": { + "publicHttps": "public HTTPS only", + "noDbWrite": "no DB write", + "noSecret": "no secret read" + } + }, "commanderInsertedRequirements": { "eyebrow": "主線優先序", "title": "統帥插入需求工作項", diff --git a/apps/web/messages/zh-TW.json b/apps/web/messages/zh-TW.json index 31535e99..5e7ff8ba 100644 --- a/apps/web/messages/zh-TW.json +++ b/apps/web/messages/zh-TW.json @@ -9180,6 +9180,31 @@ "evidenceBoundary": "證據邊界" } }, + "stockplatformP0Receipt": { + "eyebrow": "P0-006 Runtime receipt", + "title": "StockPlatform 資料依賴回讀", + "subtitle": "顯示公開 API 回讀出的 data readiness receipt、資料依賴分類與下一個受控步驟。", + "preflight": "Controlled preflight", + "next": "目前下一步", + "empty": "等待 priority readback", + "metrics": { + "runtime": "Runtime", + "receipt": "Receipt", + "dataDependency": "Data dependency", + "postgres": "Postgres" + }, + "values": { + "received": "已收件", + "missing": "缺收件", + "ready": "ready", + "notReady": "not ready" + }, + "boundaries": { + "publicHttps": "public HTTPS only", + "noDbWrite": "no DB write", + "noSecret": "no secret read" + } + }, "commanderInsertedRequirements": { "eyebrow": "主線優先序", "title": "統帥插入需求工作項", diff --git a/apps/web/src/app/[locale]/awooop/work-items/page.tsx b/apps/web/src/app/[locale]/awooop/work-items/page.tsx index fb0a9cb8..999e5041 100644 --- a/apps/web/src/app/[locale]/awooop/work-items/page.tsx +++ b/apps/web/src/app/[locale]/awooop/work-items/page.tsx @@ -1127,6 +1127,12 @@ type PriorityWorkOrderResponse = { ai_automation_node_receipt_metadata_only?: boolean | null; ai_automation_node_receipt_lanes?: string[] | null; ai_automation_node_receipt_work_items?: string[] | null; + stockplatform_public_api_runtime_status?: string | null; + stockplatform_public_api_controlled_recovery_preflight_status?: string | null; + stockplatform_public_api_controlled_recovery_data_dependency?: string | null; + stockplatform_public_api_recovery_control_path_status?: string | null; + stockplatform_postgres_readiness_or_data_source_readback_status?: string | null; + stockplatform_postgres_readiness_or_data_source_receipt_provided?: boolean | null; } | null; in_progress_or_blocked_in_priority_order?: Array<{ evidence?: { @@ -1161,8 +1167,21 @@ type PriorityWorkOrderResponse = { controlled_cd_lane_live_gitea_actions_active_process_count?: number | null; controlled_cd_lane_live_metric_blocker_count?: number | null; controlled_cd_lane_live_metric_blockers?: string[] | null; + stockplatform_public_api_runtime_status?: string | null; + stockplatform_public_api_runtime_ready?: boolean | null; + stockplatform_public_api_health_http_status?: number | null; + stockplatform_freshness_http_status?: number | null; + stockplatform_ingestion_http_status?: number | null; + stockplatform_controlled_recovery_preflight_status?: string | null; + stockplatform_controlled_recovery_data_dependency?: string | null; + stockplatform_recovery_control_path_status?: string | null; + stockplatform_postgres_readiness_or_data_source_readback_status?: string | null; + stockplatform_postgres_readiness_or_data_source_readback_present?: boolean | null; + stockplatform_postgres_readiness_or_data_source_receipt_provided?: boolean | null; + stockplatform_postgres_readiness_or_data_source_postgres_ready?: boolean | null; } | null; }>; + next_execution_order?: string[] | null; commander_inserted_requirement_work_items?: CommanderInsertedRequirementWorkItem[]; ai_automation_node_receipts?: AiAutomationNodeReceipt[]; ai_automation_node_receipt_schema?: { @@ -8691,6 +8710,166 @@ function AiLoopLogSourceTagsPanel({ ); } +function stockplatformReceiptTone(value: string | boolean | null | undefined) { + if (value === true || value === "ready" || String(value).includes("_ready")) { + return "border-[#b9d9c2] bg-[#f2fbf3] text-[#236332]"; + } + if (String(value).includes("blocked") || value === false) { + return "border-[#f0c6a8] bg-[#fff8f1] text-[#9a4d16]"; + } + return "border-[#d8d3c7] bg-[#faf9f3] text-[#5f5b52]"; +} + +function StockPlatformP0ReceiptPanel({ + priority, + loading, +}: { + priority: PriorityWorkOrderResponse | null; + loading: boolean; +}) { + const t = useTranslations("awooop.workItems.stockplatformP0Receipt"); + const summary = priority?.summary; + const evidence = priority?.in_progress_or_blocked_in_priority_order?.find( + (item) => + item.evidence?.stockplatform_postgres_readiness_or_data_source_readback_status || + item.evidence?.stockplatform_controlled_recovery_preflight_status || + item.evidence?.stockplatform_public_api_runtime_status + )?.evidence; + const readbackStatus = + summary?.stockplatform_postgres_readiness_or_data_source_readback_status ?? + evidence?.stockplatform_postgres_readiness_or_data_source_readback_status ?? + "--"; + const receiptProvided = + summary?.stockplatform_postgres_readiness_or_data_source_receipt_provided ?? + evidence?.stockplatform_postgres_readiness_or_data_source_receipt_provided ?? + false; + const dataDependency = + summary?.stockplatform_public_api_controlled_recovery_data_dependency ?? + evidence?.stockplatform_controlled_recovery_data_dependency ?? + "--"; + const preflightStatus = + summary?.stockplatform_public_api_controlled_recovery_preflight_status ?? + evidence?.stockplatform_controlled_recovery_preflight_status ?? + "--"; + const runtimeStatus = + summary?.stockplatform_public_api_runtime_status ?? + evidence?.stockplatform_public_api_runtime_status ?? + "--"; + const recoveryStatus = + summary?.stockplatform_public_api_recovery_control_path_status ?? + evidence?.stockplatform_recovery_control_path_status ?? + "--"; + const postgresReady = + evidence?.stockplatform_postgres_readiness_or_data_source_postgres_ready ?? + false; + const nextOrder = priority?.next_execution_order?.[0] ?? ""; + const metrics = [ + { + key: "runtime", + label: t("metrics.runtime"), + value: runtimeStatus, + tone: stockplatformReceiptTone(runtimeStatus), + }, + { + key: "receipt", + label: t("metrics.receipt"), + value: receiptProvided ? t("values.received") : t("values.missing"), + tone: stockplatformReceiptTone(receiptProvided), + }, + { + key: "data", + label: t("metrics.dataDependency"), + value: dataDependency, + tone: stockplatformReceiptTone(readbackStatus), + }, + { + key: "postgres", + label: t("metrics.postgres"), + value: postgresReady ? t("values.ready") : t("values.notReady"), + tone: stockplatformReceiptTone(postgresReady), + }, + ]; + + return ( +
+
+
+
+
+ + {loading ? "--" : readbackStatus} + +
+ +
+ {metrics.map((metric) => ( +
+
{metric.label}
+
+ {loading ? "--" : metric.value} +
+
+ ))} +
+ +
+
+
+ {t("preflight")} +
+
+ {loading ? "--" : preflightStatus} +
+
+ {loading ? "--" : recoveryStatus} +
+
+
+
+ {t("next")} +
+
+ {loading ? "--" : nextOrder || t("empty")} +
+
+
+ +
+ + {t("boundaries.publicHttps")} + + + {t("boundaries.noDbWrite")} + + + {t("boundaries.noSecret")} + +
+
+
+ ); +} + function commanderPriorityTone(priority: string) { switch (priority) { case "P0": @@ -9318,6 +9497,11 @@ export default function AwoooPWorkItemsPage() { loading={priorityWorkOrderLoading && !priorityWorkOrder} /> + +