From ca95b95bb774735c921a19c6e6540391dc5b3a57 Mon Sep 17 00:00:00 2001 From: Your Name Date: Thu, 2 Jul 2026 19:19:32 +0800 Subject: [PATCH] feat(stockplatform): surface source contract receipt --- .../awoooi_priority_work_order_readback.py | 108 +++++++ ...ublic_api_controlled_recovery_preflight.py | 31 +++ ...ockplatform_public_api_runtime_readback.py | 263 +++++++++++++++++- ...awoooi_priority_work_order_readback_api.py | 28 ++ ...ublic_api_controlled_recovery_preflight.py | 35 +++ ...ockplatform_public_api_runtime_readback.py | 28 ++ apps/web/messages/en.json | 8 +- apps/web/messages/zh-TW.json | 8 +- .../app/[locale]/awooop/work-items/page.tsx | 55 +++- 9 files changed, 540 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 64e0b5a3d..bfd82736c 100644 --- a/apps/api/src/services/awoooi_priority_work_order_readback.py +++ b/apps/api/src/services/awoooi_priority_work_order_readback.py @@ -998,6 +998,9 @@ def apply_stockplatform_public_api_runtime_readback( data_readiness_readback = _dict( runtime_readback.get("data_readiness_control_readback") ) + source_contract_readback = _dict( + runtime_readback.get("source_contract_control_readback") + ) data_dependency_classification = str( runtime_rollups.get("data_dependency_classification") or runtime_readback_body.get("data_dependency_classification") @@ -1073,6 +1076,24 @@ def apply_stockplatform_public_api_runtime_readback( state["stockplatform_postgres_readiness_or_data_source_postgres_ready"] = bool( data_readiness_readback.get("postgres_ready") is True ) + state["stockplatform_source_contract_readback_status"] = str( + source_contract_readback.get("status") or "unknown" + ) + state["stockplatform_source_contract_readback_present"] = bool( + source_contract_readback.get("readback_present") is True + ) + state["stockplatform_source_contract_receipt_provided"] = bool( + source_contract_readback.get("receipt_provided") is True + ) + state["stockplatform_source_contract_ready"] = bool( + source_contract_readback.get("source_contract_ready") is True + ) + state["stockplatform_source_contract_non_ok_source_count"] = _int( + source_contract_readback.get("non_ok_source_count") + ) + state["stockplatform_source_contract_blocking_sources"] = _list( + source_contract_readback.get("blocking_sources") + ) for item in _list(payload.get("in_progress_or_blocked_in_priority_order")): workplan = _dict(item) @@ -1137,6 +1158,24 @@ def apply_stockplatform_public_api_runtime_readback( evidence[ "stockplatform_postgres_readiness_or_data_source_postgres_ready" ] = state["stockplatform_postgres_readiness_or_data_source_postgres_ready"] + evidence["stockplatform_source_contract_readback_status"] = state[ + "stockplatform_source_contract_readback_status" + ] + evidence["stockplatform_source_contract_readback_present"] = state[ + "stockplatform_source_contract_readback_present" + ] + evidence["stockplatform_source_contract_receipt_provided"] = state[ + "stockplatform_source_contract_receipt_provided" + ] + evidence["stockplatform_source_contract_ready"] = state[ + "stockplatform_source_contract_ready" + ] + evidence["stockplatform_source_contract_non_ok_source_count"] = state[ + "stockplatform_source_contract_non_ok_source_count" + ] + evidence["stockplatform_source_contract_blocking_sources"] = state[ + "stockplatform_source_contract_blocking_sources" + ] evidence["stockplatform_recovery_control_path_status"] = ( recovery_control_path_status ) @@ -1235,6 +1274,9 @@ def apply_stockplatform_public_api_controlled_recovery_preflight( data_readiness_readback = _dict( recovery_preflight.get("data_readiness_control_readback") ) + source_contract_readback = _dict( + recovery_preflight.get("source_contract_control_readback") + ) candidate_packaged = bool( rollups.get("controlled_recovery_candidate_packaged") is True ) @@ -1262,6 +1304,24 @@ def apply_stockplatform_public_api_controlled_recovery_preflight( state["stockplatform_postgres_readiness_or_data_source_postgres_ready"] = bool( data_readiness_readback.get("postgres_ready") is True ) + state["stockplatform_source_contract_readback_status"] = str( + source_contract_readback.get("status") or "unknown" + ) + state["stockplatform_source_contract_readback_present"] = bool( + source_contract_readback.get("readback_present") is True + ) + state["stockplatform_source_contract_receipt_provided"] = bool( + source_contract_readback.get("receipt_provided") is True + ) + state["stockplatform_source_contract_ready"] = bool( + source_contract_readback.get("source_contract_ready") is True + ) + state["stockplatform_source_contract_non_ok_source_count"] = _int( + source_contract_readback.get("non_ok_source_count") + ) + state["stockplatform_source_contract_blocking_sources"] = _list( + source_contract_readback.get("blocking_sources") + ) state["stockplatform_public_api_controlled_recovery_active_blockers"] = _strings( recovery_preflight.get("active_blockers") ) @@ -1315,6 +1375,24 @@ def apply_stockplatform_public_api_controlled_recovery_preflight( evidence[ "stockplatform_postgres_readiness_or_data_source_postgres_ready" ] = state["stockplatform_postgres_readiness_or_data_source_postgres_ready"] + evidence["stockplatform_source_contract_readback_status"] = state[ + "stockplatform_source_contract_readback_status" + ] + evidence["stockplatform_source_contract_readback_present"] = state[ + "stockplatform_source_contract_readback_present" + ] + evidence["stockplatform_source_contract_receipt_provided"] = state[ + "stockplatform_source_contract_receipt_provided" + ] + evidence["stockplatform_source_contract_ready"] = state[ + "stockplatform_source_contract_ready" + ] + evidence["stockplatform_source_contract_non_ok_source_count"] = state[ + "stockplatform_source_contract_non_ok_source_count" + ] + evidence["stockplatform_source_contract_blocking_sources"] = state[ + "stockplatform_source_contract_blocking_sources" + ] evidence["stockplatform_controlled_recovery_candidate_packaged"] = ( candidate_packaged ) @@ -1406,6 +1484,15 @@ def apply_stockplatform_public_api_controlled_recovery_preflight( state.get("stockplatform_postgres_readiness_or_data_source_receipt_provided") is True ) + summary["stockplatform_source_contract_readback_status"] = str( + state.get("stockplatform_source_contract_readback_status") or "unknown" + ) + summary["stockplatform_source_contract_receipt_provided"] = bool( + state.get("stockplatform_source_contract_receipt_provided") is True + ) + summary["stockplatform_source_contract_non_ok_source_count"] = _int( + state.get("stockplatform_source_contract_non_ok_source_count") + ) def apply_harbor_registry_controlled_recovery_preflight( @@ -3265,6 +3352,18 @@ def _refresh_rollups_after_stockplatform_overlay( state.get("stockplatform_postgres_readiness_or_data_source_postgres_ready") is True ) + rollups["stockplatform_source_contract_readback_present"] = bool( + state.get("stockplatform_source_contract_readback_present") is True + ) + rollups["stockplatform_source_contract_receipt_provided"] = bool( + state.get("stockplatform_source_contract_receipt_provided") is True + ) + rollups["stockplatform_source_contract_ready"] = bool( + state.get("stockplatform_source_contract_ready") is True + ) + rollups["stockplatform_source_contract_non_ok_source_count"] = _int( + state.get("stockplatform_source_contract_non_ok_source_count") + ) rollups["stockplatform_freshness_status"] = str( state.get("stockplatform_freshness_status") or "unknown" ) @@ -3321,6 +3420,15 @@ def _refresh_rollups_after_stockplatform_overlay( state.get("stockplatform_postgres_readiness_or_data_source_receipt_provided") is True ) + summary["stockplatform_source_contract_readback_status"] = str( + state.get("stockplatform_source_contract_readback_status") or "unknown" + ) + summary["stockplatform_source_contract_receipt_provided"] = bool( + state.get("stockplatform_source_contract_receipt_provided") is True + ) + summary["stockplatform_source_contract_non_ok_source_count"] = _int( + state.get("stockplatform_source_contract_non_ok_source_count") + ) summary["stockplatform_freshness_status"] = str( state.get("stockplatform_freshness_status") or "unknown" ) 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 59ed8a3f0..152ce4459 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 @@ -51,6 +51,9 @@ def _build_payload( data_readiness_control_readback = _dict( runtime.get("data_readiness_control_readback") ) + source_contract_control_readback = _dict( + runtime.get("source_contract_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 @@ -128,10 +131,23 @@ def _build_payload( "data_readiness_control_receipt_provided": ( data_readiness_control_readback.get("receipt_provided") is True ), + "source_contract_control_readback_status": ( + source_contract_control_readback.get("status") + ), + "source_contract_receipt_provided": ( + source_contract_control_readback.get("receipt_provided") is True + ), + "source_contract_non_ok_source_count": _int( + source_contract_control_readback.get("non_ok_source_count") + ), + "source_contract_blocker_count": len( + _strings(source_contract_control_readback.get("active_blockers")) + ), "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, + "source_contract_control_readback": source_contract_control_readback, "controlled_apply_policy": { "risk_level": "high", "break_glass_required": False, @@ -201,6 +217,21 @@ def _build_payload( "postgres_readiness_or_data_source_receipt_provided": ( data_readiness_control_readback.get("receipt_provided") is True ), + "source_contract_readback_present": ( + source_contract_control_readback.get("readback_present") is True + ), + "source_contract_receipt_provided": ( + source_contract_control_readback.get("receipt_provided") is True + ), + "source_contract_ready": ( + source_contract_control_readback.get("source_contract_ready") is True + ), + "source_contract_non_ok_source_count": _int( + source_contract_control_readback.get("non_ok_source_count") + ), + "source_contract_blocker_count": len( + _strings(source_contract_control_readback.get("active_blockers")) + ), }, "operation_boundaries": { "read_only_public_https_probe": True, 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 d2f95a9cc..de1f733fa 100644 --- a/apps/api/src/services/stockplatform_public_api_runtime_readback.py +++ b/apps/api/src/services/stockplatform_public_api_runtime_readback.py @@ -25,6 +25,8 @@ _DATA_READINESS_SCHEMA_VERSION = ( _DATA_READINESS_RECEIPT_KEY = ( "stockplatform_postgres_readiness_or_data_source_readback" ) +_SOURCE_CONTRACT_SCHEMA_VERSION = "stockplatform_source_contract_control_readback_v1" +_SOURCE_CONTRACT_RECEIPT_KEY = "stockplatform_source_contract_readback" _DEFAULT_OPERATIONS_DIR = default_operations_dir(Path(__file__)) _RECOVERY_RECEIPT_FILE = ( "stockplatform-public-api-runtime-recovery-control-receipt.snapshot.json" @@ -148,10 +150,20 @@ def _build_payload( postgres_not_ready=postgres_not_ready, data_dependency_blockers=active_blockers, ) + source_contract_control_readback = _source_contract_control_readback( + 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, + source_contract_control_readback=source_contract_control_readback, ) recovery_control_path_blockers = _strings( recovery_control_path.get("active_blockers") @@ -230,6 +242,22 @@ def _build_payload( "data_readiness_control_readback_present": ( data_readiness_control_readback.get("readback_present") is True ), + "source_contract_control_readback_status": ( + source_contract_control_readback.get("status") + ), + "source_contract_control_readback_present": ( + source_contract_control_readback.get("readback_present") is True + ), + "source_contract_receipt_provided": ( + source_contract_control_readback.get("receipt_provided") is True + ), + "source_contract_non_ok_source_count": _int_or_none( + source_contract_control_readback.get("non_ok_source_count") + ) + or 0, + "source_contract_blocker_count": len( + _strings(source_contract_control_readback.get("active_blockers")) + ), }, "rollups": { "runtime_ready": ready, @@ -266,8 +294,26 @@ def _build_payload( "postgres_readiness_or_data_source_receipt_provided": ( data_readiness_control_readback.get("receipt_provided") is True ), + "source_contract_readback_present": ( + source_contract_control_readback.get("readback_present") is True + ), + "source_contract_receipt_provided": ( + source_contract_control_readback.get("receipt_provided") is True + ), + "source_contract_ready": ( + source_contract_control_readback.get("source_contract_ready") + is True + ), + "source_contract_non_ok_source_count": _int_or_none( + source_contract_control_readback.get("non_ok_source_count") + ) + or 0, + "source_contract_blocker_count": len( + _strings(source_contract_control_readback.get("active_blockers")) + ), }, "data_readiness_control_readback": data_readiness_control_readback, + "source_contract_control_readback": source_contract_control_readback, "recovery_control_path": recovery_control_path, "probes": probes, "operation_boundaries": { @@ -293,6 +339,7 @@ def _recovery_control_path_readback( runtime_ready: bool, receipt: dict[str, Any], data_readiness_control_readback: dict[str, Any], + source_contract_control_readback: dict[str, Any], ) -> dict[str, Any]: if runtime_ready: return { @@ -302,10 +349,12 @@ def _recovery_control_path_readback( "provided_receipts": _provided_receipts_with_live_readback( _strings(receipt.get("provided_receipts")), data_readiness_control_readback, + source_contract_control_readback, ), "missing_receipts": _missing_receipts_after_live_readback( _strings(receipt.get("missing_receipts")), data_readiness_control_readback, + source_contract_control_readback, ), "safe_recovery_channels": [ "keep_monitoring_public_api_runtime_readback", @@ -315,10 +364,12 @@ def _recovery_control_path_readback( provided_receipts = _provided_receipts_with_live_readback( _strings(receipt.get("provided_receipts")), data_readiness_control_readback, + source_contract_control_readback, ) missing_receipts = _missing_receipts_after_live_readback( _strings(receipt.get("missing_receipts")), data_readiness_control_readback, + source_contract_control_readback, ) active_blockers = _recovery_control_active_blockers( receipt_blockers=_strings(receipt.get("active_blockers")), @@ -339,6 +390,11 @@ def _recovery_control_path_readback( if data_readiness_control_readback.get("receipt_provided") is True else "" ), + "live_source_contract_receipt_key": ( + _SOURCE_CONTRACT_RECEIPT_KEY + if source_contract_control_readback.get("receipt_provided") is True + else "" + ), "safe_recovery_channels": _strings( receipt.get("safe_recovery_channels") ), @@ -468,25 +524,151 @@ def _data_readiness_control_readback( } +def _source_contract_control_readback( + *, + 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 + freshness_sources = _sources(freshness_json.get("sources")) + ingestion_sources = _sources(ingestion_json.get("sources")) + all_sources = [ + *_source_entries(freshness_sources, "freshness"), + *_source_entries(ingestion_sources, "ingestion"), + ] + blocking_sources = _blocking_source_entries( + source_entries=all_sources, + freshness_blockers=_strings(freshness_json.get("blockers")), + ingestion_blockers=_strings(ingestion_json.get("blockers")), + ) + data_ready = ( + readback_present + and freshness_json.get("status") == "ok" + and ingestion_json.get("status") == "ok" + ) + 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 == ( + "freshness_or_ingestion_not_ready" + ): + status = "blocked_stockplatform_source_contract_not_ready" + safe_next_step = ( + "diff_stockplatform_source_contract_blocking_sources_then_rerun_" + "public_api_verifier" + ) + elif readback_present and postgres_not_ready: + status = "stockplatform_source_contract_not_required_postgres_path" + safe_next_step = ( + "run_stockplatform_production_migration_path_after_control_channel_" + "readback_no_manual_db_write" + ) + elif readback_present: + status = "stockplatform_source_contract_review_required" + safe_next_step = "review_stockplatform_data_dependency_classification" + else: + status = "blocked_stockplatform_source_contract_readback_unavailable" + safe_next_step = ( + "restore_public_api_freshness_ingestion_readback_then_rerun_" + "source_contract_readback" + ) + + active_blockers = [ + blocker + for blocker in data_dependency_blockers + if blocker.startswith("stockplatform_freshness_") + or blocker.startswith("stockplatform_ingestion_") + ] + 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") + if readback_present and not all_sources and not active_blockers and not data_ready: + missing_evidence.append("source_contract_sources_or_blockers") + + return { + "schema_version": _SOURCE_CONTRACT_SCHEMA_VERSION, + "receipt_key": _SOURCE_CONTRACT_RECEIPT_KEY, + "status": status, + "source": "stockplatform_public_api_freshness_ingestion_source_contract", + "readback_present": readback_present, + "receipt_provided": readback_present, + "safe_next_step": safe_next_step, + "source_contract_ready": data_ready, + "data_dependency_classification": data_dependency_classification, + "postgres_not_ready": postgres_not_ready, + "freshness_status": str(freshness_json.get("status") or "unknown"), + "ingestion_status": str(ingestion_json.get("status") or "unknown"), + "latest_trading_date": str( + freshness_json.get("latest_trading_date") + or ingestion_json.get("latest_trading_date") + or "" + ), + "freshness_source_count": len(freshness_sources), + "ingestion_source_count": len(ingestion_sources), + "source_count": len(all_sources), + "sla_source_count": _sla_source_count(freshness_sources) + + _sla_source_count(ingestion_sources), + "non_ok_source_count": len(blocking_sources), + "source_contract_blockers": _unique_strings( + [ + *(f"freshness:{blocker}" for blocker in _strings(freshness_json.get("blockers"))), + *(f"ingestion:{blocker}" for blocker in _strings(ingestion_json.get("blockers"))), + ] + ), + "blocking_sources": blocking_sources, + "active_blockers": active_blockers, + "missing_evidence": missing_evidence, + "post_apply_verifier_required": not data_ready, + "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], + source_contract_control_readback: dict[str, Any], ) -> list[str]: + receipts = list(provided_receipts) if data_readiness_control_readback.get("receipt_provided") is not True: - return provided_receipts - return _unique_strings([*provided_receipts, _DATA_READINESS_RECEIPT_KEY]) + receipts = receipts + else: + receipts.append(_DATA_READINESS_RECEIPT_KEY) + if source_contract_control_readback.get("receipt_provided") is True: + receipts.append(_SOURCE_CONTRACT_RECEIPT_KEY) + return _unique_strings(receipts) def _missing_receipts_after_live_readback( missing_receipts: list[str], data_readiness_control_readback: dict[str, Any], + source_contract_control_readback: dict[str, Any], ) -> list[str]: - if data_readiness_control_readback.get("receipt_provided") is not True: - return missing_receipts + provided = set() + if data_readiness_control_readback.get("receipt_provided") is True: + provided.add(_DATA_READINESS_RECEIPT_KEY) + if source_contract_control_readback.get("receipt_provided") is True: + provided.add(_SOURCE_CONTRACT_RECEIPT_KEY) return [ receipt for receipt in missing_receipts - if receipt != _DATA_READINESS_RECEIPT_KEY + if receipt not in provided ] @@ -679,6 +861,77 @@ def _sources(value: Any) -> list[dict[str, Any]]: return [item for item in value if isinstance(item, dict)] +def _source_entries( + sources: list[dict[str, Any]], + category: str, +) -> list[dict[str, Any]]: + entries: list[dict[str, Any]] = [] + for source in sources: + source_id = str( + source.get("source") + or source.get("name") + or source.get("id") + or source.get("table") + or "unknown" + ) + status = str(source.get("status") or "unknown") + reason = str( + source.get("reason") + or source.get("message") + or source.get("blocker") + or "" + ) + entries.append( + { + "source": source_id, + "category": category, + "status": status, + "reason": reason, + "has_sla": isinstance(source.get("sla"), dict), + } + ) + return entries + + +def _blocking_source_entries( + *, + source_entries: list[dict[str, Any]], + freshness_blockers: list[str], + ingestion_blockers: list[str], +) -> list[dict[str, Any]]: + blocking = [ + { + **entry, + "classification": f"{entry['category']}_source_non_ok", + } + for entry in source_entries + if entry.get("status") != "ok" + ] + if blocking: + return blocking + return [ + { + "source": blocker, + "category": "freshness", + "status": "blocked", + "reason": blocker, + "has_sla": False, + "classification": "freshness_blocker_without_source_entry", + } + for blocker in freshness_blockers + ] + [ + { + "source": blocker, + "category": "ingestion", + "status": "blocked", + "reason": blocker, + "has_sla": False, + "classification": "ingestion_blocker_without_source_entry", + } + for blocker in ingestion_blockers + ] + + def _sla_source_count(sources: list[dict[str, Any]]) -> int: return sum(1 for source in sources if isinstance(source.get("sla"), dict)) 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 c533d5bd4..9dbace3c1 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 @@ -1343,6 +1343,13 @@ def test_awoooi_priority_work_order_readback_routes_stockplatform_data_dependenc assert evidence[ "stockplatform_postgres_readiness_or_data_source_postgres_ready" ] is False + assert evidence["stockplatform_source_contract_readback_status"] == ( + "stockplatform_source_contract_not_required_postgres_path" + ) + assert evidence["stockplatform_source_contract_receipt_provided"] is True + assert evidence["stockplatform_source_contract_non_ok_source_count"] == 2 + assert payload["rollups"]["stockplatform_source_contract_receipt_provided"] is True + assert payload["summary"]["stockplatform_source_contract_non_ok_source_count"] == 2 assert payload["rollups"][ "stockplatform_postgres_readiness_or_data_source_readback_present" ] is True @@ -1749,6 +1756,27 @@ def _stockplatform_runtime_data_dependency_blocked() -> dict: "postgres_ready": False, "postgres_not_ready": True, }, + "source_contract_control_readback": { + "schema_version": "stockplatform_source_contract_control_readback_v1", + "receipt_key": "stockplatform_source_contract_readback", + "status": "stockplatform_source_contract_not_required_postgres_path", + "readback_present": True, + "receipt_provided": True, + "source_contract_ready": False, + "non_ok_source_count": 2, + "blocking_sources": [ + { + "source": "postgres_not_ready", + "category": "freshness", + "status": "blocked", + }, + { + "source": "postgres_not_ready", + "category": "ingestion", + "status": "blocked", + }, + ], + }, } 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 85bf0f601..463e7e4f7 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 @@ -169,6 +169,16 @@ def test_stockplatform_controlled_recovery_preflight_prefers_live_data_classific "data_dependency_control_path_readback_required_before_source_" "freshness_or_postgres_contract_apply" ) + assert payload["source_contract_control_readback"]["schema_version"] == ( + "stockplatform_source_contract_control_readback_v1" + ) + assert payload["source_of_truth_diff"][ + "source_contract_control_readback_status" + ] == "blocked_stockplatform_source_contract_not_ready" + assert payload["source_of_truth_diff"]["source_contract_receipt_provided"] is True + assert payload["rollups"]["source_contract_readback_present"] is True + assert payload["rollups"]["source_contract_receipt_provided"] is True + assert payload["rollups"]["source_contract_non_ok_source_count"] == 2 assert payload["controlled_action_candidates"][1]["id"] == ( "source_freshness_contract_preflight" ) @@ -338,4 +348,29 @@ def _runtime_data_dependency_non_postgres_blocked() -> dict: "postgres_ready": False, "postgres_not_ready": False, } + payload["source_contract_control_readback"] = { + "schema_version": "stockplatform_source_contract_control_readback_v1", + "receipt_key": "stockplatform_source_contract_readback", + "status": "blocked_stockplatform_source_contract_not_ready", + "readback_present": True, + "receipt_provided": True, + "source_contract_ready": False, + "non_ok_source_count": 2, + "active_blockers": [ + "stockplatform_freshness_core_margin_short_daily_missing", + "stockplatform_freshness_ai_recommendations_stale", + ], + "blocking_sources": [ + { + "source": "core.margin_short_daily", + "category": "freshness", + "status": "missing", + }, + { + "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 025eaa688..828c1dd20 100644 --- a/apps/api/tests/test_stockplatform_public_api_runtime_readback.py +++ b/apps/api/tests/test_stockplatform_public_api_runtime_readback.py @@ -207,6 +207,34 @@ def test_stockplatform_public_api_runtime_readback_routes_data_dependency_withou "freshness_sla_source_count" ] == 2 assert payload["data_readiness_control_readback"]["receipt_provided"] is True + source_contract = payload["source_contract_control_readback"] + assert source_contract["schema_version"] == ( + "stockplatform_source_contract_control_readback_v1" + ) + assert source_contract["receipt_key"] == "stockplatform_source_contract_readback" + assert source_contract["status"] == "blocked_stockplatform_source_contract_not_ready" + assert source_contract["receipt_provided"] is True + assert source_contract["latest_trading_date"] == "2026-07-02" + assert source_contract["source_count"] == 3 + assert source_contract["sla_source_count"] == 2 + assert source_contract["non_ok_source_count"] == 2 + assert source_contract["blocking_sources"][0]["source"] == ( + "core.margin_short_daily" + ) + assert source_contract["operation_boundaries"]["public_https_probe_only"] is True + assert source_contract["operation_boundaries"]["database_write_or_restore_performed"] is False + assert source_contract["operation_boundaries"]["secret_value_collection_allowed"] is False + assert payload["readback"]["source_contract_control_readback_status"] == ( + "blocked_stockplatform_source_contract_not_ready" + ) + assert payload["readback"]["source_contract_receipt_provided"] is True + assert payload["readback"]["source_contract_non_ok_source_count"] == 2 + assert payload["rollups"]["source_contract_readback_present"] is True + assert payload["rollups"]["source_contract_receipt_provided"] is True + assert payload["rollups"]["source_contract_non_ok_source_count"] == 2 + assert "stockplatform_source_contract_readback" in payload[ + "recovery_control_path" + ]["provided_receipts"] 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" diff --git a/apps/web/messages/en.json b/apps/web/messages/en.json index 652277c98..08861439c 100644 --- a/apps/web/messages/en.json +++ b/apps/web/messages/en.json @@ -9194,21 +9194,25 @@ "runtime": "Runtime", "receipt": "Receipt", "dataDependency": "Data dependency", - "postgres": "Postgres" + "sourceContract": "Source contract", + "verifier": "Verifier" }, "metricDetails": { "runtime": "API is readable; data is not green", "receipt": "Converted into machine-readable receipt", "dataDependency": "Source contract verifier is still missing", - "postgres": "No manual DB write to fake green" + "sourceContract": "Shows non-OK sources", + "verifier": "P0 clears only after green" }, "values": { "received": "received", "missing": "missing", "ready": "ready", "notReady": "not ready", + "pending": "pending", "blocked": "blocked", "review": "review", + "sourcesNeedWork": "{count} sources", "sourceContractGap": "source contract gap", "postgresGap": "Postgres gap" }, diff --git a/apps/web/messages/zh-TW.json b/apps/web/messages/zh-TW.json index e981eb8f4..113d53a84 100644 --- a/apps/web/messages/zh-TW.json +++ b/apps/web/messages/zh-TW.json @@ -9194,21 +9194,25 @@ "runtime": "Runtime", "receipt": "Receipt", "dataDependency": "Data dependency", - "postgres": "Postgres" + "sourceContract": "Source contract", + "verifier": "Verifier" }, "metricDetails": { "runtime": "API 可讀,資料仍未綠燈", "receipt": "已轉成機器可讀 receipt", "dataDependency": "缺資料來源合約驗證", - "postgres": "不靠手寫 DB 造綠燈" + "sourceContract": "列出不合格來源", + "verifier": "綠燈後才解除 P0" }, "values": { "received": "已收件", "missing": "缺收件", "ready": "ready", "notReady": "not ready", + "pending": "pending", "blocked": "blocked", "review": "review", + "sourcesNeedWork": "{count} 個來源", "sourceContractGap": "來源合約缺口", "postgresGap": "Postgres 缺口" }, 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 bc14e4be3..10c609a94 100644 --- a/apps/web/src/app/[locale]/awooop/work-items/page.tsx +++ b/apps/web/src/app/[locale]/awooop/work-items/page.tsx @@ -1133,6 +1133,9 @@ type PriorityWorkOrderResponse = { 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; + stockplatform_source_contract_readback_status?: string | null; + stockplatform_source_contract_receipt_provided?: boolean | null; + stockplatform_source_contract_non_ok_source_count?: number | null; } | null; in_progress_or_blocked_in_priority_order?: Array<{ evidence?: { @@ -1179,6 +1182,11 @@ type PriorityWorkOrderResponse = { 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; + stockplatform_source_contract_readback_status?: string | null; + stockplatform_source_contract_readback_present?: boolean | null; + stockplatform_source_contract_receipt_provided?: boolean | null; + stockplatform_source_contract_ready?: boolean | null; + stockplatform_source_contract_non_ok_source_count?: number | null; } | null; }>; next_execution_order?: string[] | null; @@ -8731,6 +8739,7 @@ function StockPlatformP0ReceiptPanel({ const summary = priority?.summary; const evidence = priority?.in_progress_or_blocked_in_priority_order?.find( (item) => + item.evidence?.stockplatform_source_contract_readback_status || item.evidence?.stockplatform_postgres_readiness_or_data_source_readback_status || item.evidence?.stockplatform_controlled_recovery_preflight_status || item.evidence?.stockplatform_public_api_runtime_status @@ -8759,9 +8768,18 @@ function StockPlatformP0ReceiptPanel({ 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 ?? + const sourceContractStatus = + summary?.stockplatform_source_contract_readback_status ?? + evidence?.stockplatform_source_contract_readback_status ?? + "--"; + const sourceContractReceiptProvided = + summary?.stockplatform_source_contract_receipt_provided ?? + evidence?.stockplatform_source_contract_receipt_provided ?? false; + const sourceContractNonOkCount = + summary?.stockplatform_source_contract_non_ok_source_count ?? + evidence?.stockplatform_source_contract_non_ok_source_count ?? + 0; const nextOrder = priority?.next_execution_order?.[0] ?? ""; const runtimeLabel = runtimeStatus.includes("_ready") ? t("values.ready") @@ -8775,7 +8793,14 @@ function StockPlatformP0ReceiptPanel({ : dataDependency === "none" ? t("values.ready") : t("values.review"); - const postgresLabel = postgresReady ? t("values.ready") : t("values.notReady"); + const sourceContractLabel = sourceContractStatus.includes("_ready") + ? t("values.ready") + : sourceContractReceiptProvided + ? t("values.sourcesNeedWork", { count: sourceContractNonOkCount }) + : t("values.missing"); + const verifierLabel = runtimeStatus.includes("_ready") + ? t("values.ready") + : t("values.pending"); const nextActionLabel = dataDependency === "freshness_or_ingestion_not_ready" ? t("actions.sourceContractReadback") @@ -8813,20 +8838,20 @@ function StockPlatformP0ReceiptPanel({ tone: stockplatformCompactTone(receiptProvided), }, { - key: "data", - label: t("metrics.dataDependency"), - value: dataDependencyLabel, - detail: t("metricDetails.dataDependency"), - icon: SearchCheck, - tone: stockplatformCompactTone(readbackStatus), + key: "source", + label: t("metrics.sourceContract"), + value: sourceContractLabel, + detail: t("metricDetails.sourceContract"), + icon: ShieldCheck, + tone: stockplatformCompactTone(sourceContractStatus), }, { - key: "postgres", - label: t("metrics.postgres"), - value: postgresLabel, - detail: t("metricDetails.postgres"), - icon: Database, - tone: stockplatformCompactTone(postgresReady), + key: "verifier", + label: t("metrics.verifier"), + value: verifierLabel, + detail: t("metricDetails.verifier"), + icon: CheckCircle2, + tone: stockplatformCompactTone(runtimeStatus.includes("_ready")), }, ];