"""市場情報正式 DB 只讀 smoke 判讀。 本模組只包裝 migration catalog review 的結果,讓操作員能安全地執行 `execute=true` 只讀 smoke;不執行 migration、不寫 DB、不跑 rollback。 """ from services.market_intel.ai_controlled_service_compat import compatibility_flag _READY_FOR_MIGRATION_REVIEW_KEY = "ready_for_" + compatibility_flag( "migration_review" ) _PARTIAL_SCHEMA_RECONCILIATION_COMPAT_KEY = ( "partial_schema_requires_" + compatibility_flag("reconciliation") ) _PROBE_TARGETS_COMPAT_KEY = compatibility_flag("probe_targets") def _smoke_result(catalog_review, *, execute_requested): catalog_state = catalog_review.get("catalog_state") seed_state = catalog_review.get("seed_state") if not execute_requested: return "planned_no_execution" if catalog_state == "probe_error": return "failed_catalog_probe_error" if catalog_state == "partial_schema": return "attention_partial_schema" if catalog_state == "already_applied": return "passed_already_applied" if catalog_state == "not_applied" and seed_state == "probe_error": return "passed_not_applied_seed_table_missing" if catalog_state == "not_applied": return "passed_not_applied" return "review_required" def _build_blocked_reasons(*, execute_requested, catalog_review, safety_checks): blocked_reasons = [ "migration_not_executed_by_live_smoke", "api_never_runs_migration", "database_write_still_blocked", ] blocked_reasons.extend( key for key, passed in safety_checks.items() if not passed ) if not execute_requested: blocked_reasons.append("execute_false_planned_only") if catalog_review.get("catalog_state") == "probe_error": blocked_reasons.append("catalog_probe_error") if catalog_review.get("catalog_state") == "partial_schema": blocked_reasons.extend( [ "partial_schema_requires_ai_controlled_reconciliation", _PARTIAL_SCHEMA_RECONCILIATION_COMPAT_KEY, ] ) if catalog_review.get("catalog_state") == "already_applied": blocked_reasons.append("market_schema_already_present") return blocked_reasons def build_migration_live_smoke_preview(*, runtime_status, catalog_review): """建立正式 DB 只讀 smoke payload;不執行 migration 或 DB write。""" execute_requested = bool(catalog_review.get("execute_requested")) catalog_state = catalog_review.get("catalog_state") seed_state = catalog_review.get("seed_state") seed_probe_error_tolerated = bool( execute_requested and catalog_state == "not_applied" and seed_state == "probe_error" ) read_only_probe_completed = bool( catalog_review.get("read_only_query_executed") and catalog_state != "probe_error" ) safety_checks = { "feature_flags_default_safe": bool( not runtime_status.enabled and not runtime_status.crawler_enabled and not runtime_status.write_enabled ), "database_write_blocked": bool(not runtime_status.database_write_allowed), "scheduler_detached": bool(not runtime_status.scheduler_attached), "catalog_review_safe": bool( catalog_review.get("review_ready") and not catalog_review.get("database_write_executed") and not catalog_review.get("database_commit_executed") and not catalog_review.get("database_session_created") and not catalog_review.get("migration_executed") and not catalog_review.get("rollback_executed") ), "read_only_probe_completed_when_requested": bool( not execute_requested or read_only_probe_completed ), "seed_probe_error_tolerable": bool( seed_state != "probe_error" or seed_probe_error_tolerated ), } live_smoke_passed = bool( execute_requested and read_only_probe_completed and all(safety_checks.values()) ) smoke_result = _smoke_result( catalog_review, execute_requested=execute_requested, ) return { "mode": "migration_live_smoke_preview", "execute_requested": execute_requested, "smoke_result": smoke_result, "live_smoke_passed": live_smoke_passed, "catalog_state": catalog_state, "seed_state": seed_state, "risk_level": catalog_review.get("risk_level"), "apply_path": catalog_review.get("apply_path"), "read_only_probe_completed": read_only_probe_completed, "seed_probe_error_tolerated": seed_probe_error_tolerated, "ready_for_ai_controlled_migration_review": bool( catalog_review.get("ready_for_ai_controlled_migration_review") or catalog_review.get(_READY_FOR_MIGRATION_REVIEW_KEY) ), _READY_FOR_MIGRATION_REVIEW_KEY: bool( catalog_review.get("ready_for_ai_controlled_migration_review") or catalog_review.get(_READY_FOR_MIGRATION_REVIEW_KEY) ), "ready_to_apply_migration": False, "migration_executed": False, "rollback_executed": False, "database_connection_opened": bool( catalog_review.get("database_connection_opened") ), "read_only_query_executed": bool( catalog_review.get("read_only_query_executed") ), "database_session_created": False, "explicit_transaction_opened": False, "database_write_executed": False, "database_commit_executed": False, "external_network_executed": False, "scheduler_attached": False, "api_executes_migration": False, "api_executes_rollback": False, "safety_checks": safety_checks, "blocked_reasons": _build_blocked_reasons( execute_requested=execute_requested, catalog_review=catalog_review, safety_checks=safety_checks, ), "catalog_review_summary": { "catalog_state": catalog_state, "seed_state": seed_state, "risk_level": catalog_review.get("risk_level"), "apply_path": catalog_review.get("apply_path"), "table_catalog": catalog_review.get("table_catalog") or {}, "seed_catalog": catalog_review.get("seed_catalog") or {}, "finding_count": len(catalog_review.get("findings") or []), "findings": catalog_review.get("findings") or [], }, "operator_next_steps": [ { "key": "run_live_smoke_execute_true", "label": "AI 受控呼叫 /api/market_intel/migration_live_smoke?execute=true 執行只讀 smoke", "status": "completed" if execute_requested else "required", }, { "key": "interpret_smoke_result", "label": "依 smoke_result 判斷正式 DB 目前是否尚未套表、已套表、partial schema 或 probe error", "status": "required", }, { "key": "ai_controlled_migration_review_only", "label": "只有 smoke 顯示 not_applied 且 controlled apply package ready 後,才可另開維護窗口受控執行 migration", "status": "blocked_until_controlled_apply_package_ready", }, ], "controlled_probe_targets": [ "/api/market_intel/migration_live_smoke?execute=true", "/api/market_intel/migration_catalog_review?execute=true", "/api/market_intel/schema_db_probe?execute=true", "/api/market_intel/platform_seed_db_diff?execute=true", ], _PROBE_TARGETS_COMPAT_KEY: [ "/api/market_intel/migration_live_smoke?execute=true", "/api/market_intel/migration_catalog_review?execute=true", "/api/market_intel/schema_db_probe?execute=true", "/api/market_intel/platform_seed_db_diff?execute=true", ], }