From 6622aa458b7b02efea7ee4d04475d7d3ac866aae Mon Sep 17 00:00:00 2001 From: OoO Date: Tue, 19 May 2026 21:37:47 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E5=B8=82=E5=A0=B4=E6=83=85?= =?UTF-8?q?=E5=A0=B1=20review=20archive=20summary=20gate?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- TODO_NEXT_STEPS.txt | 1 + config.py | 2 +- ...s-platform-market-campaign-intelligence.md | 1 + routes/README.md | 4 +- routes/market_intel_review_post_routes.py | 97 ++++- .../candidate_queue_review_archive_summary.py | 381 ++++++++++++++++++ services/market_intel/deployment_readiness.py | 11 +- services/market_intel/phase.py | 2 +- 8 files changed, 477 insertions(+), 22 deletions(-) create mode 100644 services/market_intel/candidate_queue_review_archive_summary.py diff --git a/TODO_NEXT_STEPS.txt b/TODO_NEXT_STEPS.txt index b059fad..fd61cb7 100644 --- a/TODO_NEXT_STEPS.txt +++ b/TODO_NEXT_STEPS.txt @@ -153,6 +153,7 @@ - Phase 76 candidate queue review decision writer run closeout:新增 `services/market_intel/candidate_queue_review_decision_writer_run_closeout.py`、POST `/api/market_intel/manual_sample_review/candidate_queue_review_decision_writer_run_closeout` 與 UI closeout 按鈕,在 review_state receipt 通過後整理 closeout gate、操作員 closeout artifact、post-closeout read-only inventory 確認與 promotion 摘要;API/UI 不回吐 receipt 原文、不讀 token、不執行 CLI、不連 DB、不更新 review_state、不 commit、不掛 scheduler;版本同步至 V10.268。 - Phase 77 candidate queue review decision post-closeout inventory:新增 `services/market_intel/candidate_queue_review_decision_post_closeout_inventory.py`、`routes/market_intel_review_post_routes.py`、POST `/api/market_intel/manual_sample_review/candidate_queue_review_decision_post_closeout_inventory` 與 UI inventory 按鈕,在 review_state closeout 後整理 post-write smoke、live inventory、dedupe key 與 review_state 結果;API/UI 不讀 token、不執行 CLI、不更新 review_state、不寫 DB、不 commit、不掛 scheduler;版本同步至 V10.270。 - Phase 78 candidate queue review completion archive:新增 `services/market_intel/candidate_queue_review_completion_archive.py`、POST `/api/market_intel/manual_sample_review/candidate_queue_review_completion_archive` 與 UI archive 按鈕,在 post-closeout inventory 通過後整理 receipt、closeout、inventory、dedupe key、review_state row snapshot 與 artifact path manifest;API/UI 不寫檔、不讀 token、不執行 CLI、不更新 review_state、不寫 DB、不 commit、不掛 scheduler;版本同步至 V10.273。 + - Phase 79 candidate queue review archive summary:新增 `services/market_intel/candidate_queue_review_archive_summary.py`、POST `/api/market_intel/manual_sample_review/candidate_queue_review_archive_summary` 與 UI summary 按鈕,在 review completion archive 後整理可供摘要/報表審核的結構化輸入;API/UI 不呼叫 LLM、不派送 Telegram、不寫檔、不讀 token、不執行 CLI、不更新 review_state、不寫 DB、不 commit、不掛 scheduler;版本同步至 V10.275。 - V10.248 補市場情報 390px preview panel QA:sample review 工具列改為 textarea + 可換行 action rail,移除舊的硬編 8 欄 grid;`check_responsive_overflow` 新增 `--screenshot-all`,本機 390x844 `/market_intel` 真頁面 QA 通過且 overflow=0。 - V10.250 補 Code Review Gemini 備援遙測護欄:Ollama 主路徑失敗時 `fallback_to` 明確指向 `code_review_openclaw_gemini`,測試鎖住「Gemini 不得記成 `code_review_openclaw` 主 caller」;AI Calls 觀測台會把 legacy `code_review_openclaw + gemini` 顯示成 Gemini 備援,避免誤判 Gemini-first。 - Schema smoke:`tests/test_market_intel_skeleton.py` 檢查 `Base.metadata` 內含 ADR-035 八張 `market_*` tables。 diff --git a/config.py b/config.py index f1ee983..d647840 100644 --- a/config.py +++ b/config.py @@ -320,7 +320,7 @@ YOUTUBE_API_KEY = os.getenv('YOUTUBE_API_KEY', '') # ========================================== # 系統版本與路徑 # ========================================== -SYSTEM_VERSION = "V10.274" +SYSTEM_VERSION = "V10.275" LOG_FILE_PATH = os.path.join(BASE_DIR, 'logs/system.log') public_url = PUBLIC_URL # 用於模板顯示 diff --git a/docs/adr/ADR-035-cross-platform-market-campaign-intelligence.md b/docs/adr/ADR-035-cross-platform-market-campaign-intelligence.md index cd613dd..c6bbf91 100644 --- a/docs/adr/ADR-035-cross-platform-market-campaign-intelligence.md +++ b/docs/adr/ADR-035-cross-platform-market-campaign-intelligence.md @@ -204,6 +204,7 @@ EwoooC 目前已有 MOMO EDM / 節慶活動資料、`promo_products`、PChome - 2026-05-19 追加 candidate queue review decision writer run closeout:`services.market_intel.candidate_queue_review_decision_writer_run_closeout` 與 `/api/market_intel/manual_sample_review/candidate_queue_review_decision_writer_run_closeout` 在 review_state receipt 通過後整理 closeout gate、操作員 closeout artifact、post-closeout read-only inventory 確認與 promotion 摘要。此 closeout 只允許放行到後續人工/只讀檢查;API/UI 不回吐 receipt 原文、不讀 approval token、不執行 CLI、不連 DB、不更新 `review_state`、不 commit、不掛 scheduler。 - 2026-05-19 追加 candidate queue review decision post-closeout inventory:`services.market_intel.candidate_queue_review_decision_post_closeout_inventory`、`routes.market_intel_review_post_routes` 與 `/api/market_intel/manual_sample_review/candidate_queue_review_decision_post_closeout_inventory` 在 review_state closeout 後整理 post-write smoke、live inventory、dedupe key 與 review_state 結果。此 inventory 只允許只讀驗證;API/UI 不讀 approval token、不執行 CLI、不更新 `review_state`、不寫 DB、不 commit、不掛 scheduler。 - 2026-05-19 追加 candidate queue review completion archive:`services.market_intel.candidate_queue_review_completion_archive` 與 `/api/market_intel/manual_sample_review/candidate_queue_review_completion_archive` 在 post-closeout inventory 後整理 receipt、closeout、inventory、dedupe key、review_state row snapshot 與 artifact path manifest。此 archive gate 只輸出封存預覽;API/UI 不寫檔、不讀 approval token、不執行 CLI、不更新 `review_state`、不寫 DB、不 commit、不掛 scheduler。 +- 2026-05-19 追加 candidate queue review archive summary:`services.market_intel.candidate_queue_review_archive_summary` 與 `/api/market_intel/manual_sample_review/candidate_queue_review_archive_summary` 在 review completion archive 後整理可供摘要/報表審核的結構化輸入。此 summary gate 不產生 AI 摘要;API/UI 不呼叫 LLM、不派送 Telegram、不寫檔、不讀 approval token、不執行 CLI、不更新 `review_state`、不寫 DB、不 commit、不掛 scheduler。 ### Phase 4:Coupang / Shopee Adapter diff --git a/routes/README.md b/routes/README.md index 3d8c87e..ed5812a 100644 --- a/routes/README.md +++ b/routes/README.md @@ -19,9 +19,9 @@ | `edm_routes.py` | EDM 與節慶儀表板 | `/edm`, `/festival` | | `monthly_routes.py` | 月結分析 | `/monthly_summary_analysis`, `/api/monthly_summary_data` | | `daily_sales_routes.py` | 當日業績 | `/daily_sales`, `/daily_sales/export*` | -| `market_intel_routes.py` | 市場情報 Phase 78 candidate queue review completion archive 主路由 | `/market_intel`, `/market_intel/*`, `/api/market_intel/status`, `/api/market_intel/schema`, `/api/market_intel/schema_smoke`, `/api/market_intel/schema_db_probe`, `/api/market_intel/platform_seed_db_diff`, `/api/market_intel/legacy_source_bridge`, `/api/market_intel/mcp_readiness`, `/api/market_intel/mcp_tool_contract`, `/api/market_intel/mcp_deploy_preflight`, `/api/market_intel/mcp_activation_runbook`, `/api/market_intel/mcp_fetch_gate`, `/api/market_intel/scheduler_plan`, `/api/market_intel/manual_sample_plan`, `/api/market_intel/manual_sample_acceptance`, `/api/market_intel/manual_sample_review`, `/api/market_intel/manual_sample_review/evaluate`, `/api/market_intel/manual_sample_review/candidate_handoff`, `/api/market_intel/manual_sample_review/candidate_queue_draft`, `/api/market_intel/manual_sample_review/candidate_queue_approval`, `/api/market_intel/manual_sample_review/candidate_queue_transaction`, `/api/market_intel/manual_sample_review/candidate_queue_writer_status`, `/api/market_intel/manual_sample_review/candidate_queue_writer_preflight`, `/api/market_intel/manual_sample_review/candidate_queue_writer_postwrite_smoke`, `/api/market_intel/manual_sample_review/candidate_queue_writer_operator_drill`, `/api/market_intel/manual_sample_review/candidate_queue_writer_run_package`, `/api/market_intel/manual_sample_review/candidate_queue_writer_run_readiness`, `/api/market_intel/manual_sample_review/candidate_queue_writer_run_receipt`, `/api/market_intel/manual_sample_review/candidate_queue_writer_run_closeout`, `/api/market_intel/manual_sample_review/candidate_queue_review_handoff`, `/api/market_intel/match_review_plan`, `/api/market_intel/opportunity_plan`, `/api/market_intel/opportunity_scoring_plan`, `/api/market_intel/opportunity_evidence_plan`, `/api/market_intel/opportunity_alert_plan`, `/api/market_intel/adapters`, `/api/market_intel/dry_run_plan`, `/api/market_intel/discovery_plan`, `/api/market_intel/manual_discovery`, `/api/market_intel/candidate_preview`, `/api/market_intel/platform_seed_plan`, `/api/market_intel/platform_seed_write_guard`, `/api/market_intel/platform_seed_writer_plan`, `/api/market_intel/migration_blueprint`, `/api/market_intel/migration_apply_drill`, `/api/market_intel/migration_catalog_review`, `/api/market_intel/migration_live_smoke`, `/api/market_intel/live_db_inventory`, `/api/market_intel/seed_writer_cli_status`, `/api/market_intel/write_approval_runbook`, `/api/market_intel/deployment_readiness` | +| `market_intel_routes.py` | 市場情報 Phase 79 candidate queue review archive summary 主路由 | `/market_intel`, `/market_intel/*`, `/api/market_intel/status`, `/api/market_intel/schema`, `/api/market_intel/schema_smoke`, `/api/market_intel/schema_db_probe`, `/api/market_intel/platform_seed_db_diff`, `/api/market_intel/legacy_source_bridge`, `/api/market_intel/mcp_readiness`, `/api/market_intel/mcp_tool_contract`, `/api/market_intel/mcp_deploy_preflight`, `/api/market_intel/mcp_activation_runbook`, `/api/market_intel/mcp_fetch_gate`, `/api/market_intel/scheduler_plan`, `/api/market_intel/manual_sample_plan`, `/api/market_intel/manual_sample_acceptance`, `/api/market_intel/manual_sample_review`, `/api/market_intel/manual_sample_review/evaluate`, `/api/market_intel/manual_sample_review/candidate_handoff`, `/api/market_intel/manual_sample_review/candidate_queue_draft`, `/api/market_intel/manual_sample_review/candidate_queue_approval`, `/api/market_intel/manual_sample_review/candidate_queue_transaction`, `/api/market_intel/manual_sample_review/candidate_queue_writer_status`, `/api/market_intel/manual_sample_review/candidate_queue_writer_preflight`, `/api/market_intel/manual_sample_review/candidate_queue_writer_postwrite_smoke`, `/api/market_intel/manual_sample_review/candidate_queue_writer_operator_drill`, `/api/market_intel/manual_sample_review/candidate_queue_writer_run_package`, `/api/market_intel/manual_sample_review/candidate_queue_writer_run_readiness`, `/api/market_intel/manual_sample_review/candidate_queue_writer_run_receipt`, `/api/market_intel/manual_sample_review/candidate_queue_writer_run_closeout`, `/api/market_intel/manual_sample_review/candidate_queue_review_handoff`, `/api/market_intel/match_review_plan`, `/api/market_intel/opportunity_plan`, `/api/market_intel/opportunity_scoring_plan`, `/api/market_intel/opportunity_evidence_plan`, `/api/market_intel/opportunity_alert_plan`, `/api/market_intel/adapters`, `/api/market_intel/dry_run_plan`, `/api/market_intel/discovery_plan`, `/api/market_intel/manual_discovery`, `/api/market_intel/candidate_preview`, `/api/market_intel/platform_seed_plan`, `/api/market_intel/platform_seed_write_guard`, `/api/market_intel/platform_seed_writer_plan`, `/api/market_intel/migration_blueprint`, `/api/market_intel/migration_apply_drill`, `/api/market_intel/migration_catalog_review`, `/api/market_intel/migration_live_smoke`, `/api/market_intel/live_db_inventory`, `/api/market_intel/seed_writer_cli_status`, `/api/market_intel/write_approval_runbook`, `/api/market_intel/deployment_readiness` | | `market_intel_review_routes.py` | 市場情報人工 queue review 只讀延伸 API | `/api/market_intel/manual_sample_review/candidate_queue_review_inventory`, `/api/market_intel/manual_sample_review/candidate_queue_review_decision`, `/api/market_intel/manual_sample_review/candidate_queue_review_decision_approval`, `/api/market_intel/manual_sample_review/candidate_queue_review_decision_transaction`, `/api/market_intel/manual_sample_review/candidate_queue_review_decision_writer_status`, `/api/market_intel/manual_sample_review/candidate_queue_review_decision_writer_preflight`, `/api/market_intel/manual_sample_review/candidate_queue_review_decision_writer_postwrite_smoke`, `/api/market_intel/manual_sample_review/candidate_queue_review_decision_writer_operator_drill`, `/api/market_intel/manual_sample_review/candidate_queue_review_decision_writer_run_package`, `/api/market_intel/manual_sample_review/candidate_queue_review_decision_writer_run_readiness`, `/api/market_intel/manual_sample_review/candidate_queue_review_decision_writer_run_receipt`, `/api/market_intel/manual_sample_review/candidate_queue_review_decision_writer_run_closeout` | -| `market_intel_review_post_routes.py` | 市場情報 review_state closeout 後只讀延伸 API(掛在 `market_intel_review_bp`) | `/api/market_intel/manual_sample_review/candidate_queue_review_decision_post_closeout_inventory`, `/api/market_intel/manual_sample_review/candidate_queue_review_completion_archive` | +| `market_intel_review_post_routes.py` | 市場情報 review_state closeout 後只讀延伸 API(掛在 `market_intel_review_bp`) | `/api/market_intel/manual_sample_review/candidate_queue_review_decision_post_closeout_inventory`, `/api/market_intel/manual_sample_review/candidate_queue_review_completion_archive`, `/api/market_intel/manual_sample_review/candidate_queue_review_archive_summary` | | `api_routes.py` | 通用任務與查詢 API | `/api/run_task`, `/api/history/*` | | `export_routes.py` | 匯出功能 | `/api/export/*` | | `import_routes.py` | 匯入功能 | `/api/import_excel`, `/api/import/monthly_summary` | diff --git a/routes/market_intel_review_post_routes.py b/routes/market_intel_review_post_routes.py index 49508d8..ddefec4 100644 --- a/routes/market_intel_review_post_routes.py +++ b/routes/market_intel_review_post_routes.py @@ -11,6 +11,9 @@ from routes.market_intel_review_routes import ( market_intel_review_bp, ) from services.market_intel import MarketIntelService +from services.market_intel.candidate_queue_review_archive_summary import ( + build_candidate_queue_review_archive_summary, +) from services.market_intel.candidate_queue_review_completion_archive import ( build_candidate_queue_review_completion_archive, ) @@ -71,6 +74,38 @@ def _build_post_closeout_inventory_stack( return transaction, receipt, closeout, inventory +def _build_review_completion_archive_stack( + *, + service, + sample_result, + payload_error, + operator_evidence, + writer_output, + smoke_result, + limit, + execute_requested, +): + transaction, receipt, closeout, inventory = _build_post_closeout_inventory_stack( + service=service, + sample_result=sample_result, + payload_error=payload_error, + operator_evidence=operator_evidence, + writer_output=writer_output, + smoke_result=smoke_result, + limit=limit, + execute_requested=execute_requested, + ) + archive = build_candidate_queue_review_completion_archive( + transaction_preview=transaction, + run_receipt=receipt, + run_closeout=closeout, + post_closeout_inventory=inventory, + operator_evidence=operator_evidence, + execute_requested=execute_requested, + ) + return transaction, receipt, closeout, inventory, archive + + @market_intel_review_bp.route( "/api/market_intel/manual_sample_review/" "candidate_queue_review_decision_post_closeout_inventory", @@ -98,6 +133,39 @@ def market_intel_manual_sample_candidate_queue_review_decision_post_closeout_inv return jsonify(data), 400 if payload_error else 200 +@market_intel_review_bp.route( + "/api/market_intel/manual_sample_review/" + "candidate_queue_review_archive_summary", + methods=["POST"], +) +@login_required +def market_intel_manual_sample_candidate_queue_review_archive_summary(): + service = MarketIntelService() + execute_requested = request.args.get("execute", "false").lower() == "true" + sample_result, operator_evidence, writer_output, smoke_result, payload_error, limit = ( + _extract_run_payload() + ) + transaction, receipt, closeout, inventory, archive = ( + _build_review_completion_archive_stack( + service=service, + sample_result=sample_result, + payload_error=payload_error, + operator_evidence=operator_evidence, + writer_output=writer_output, + smoke_result=smoke_result, + limit=limit, + execute_requested=execute_requested, + ) + ) + data = build_candidate_queue_review_archive_summary( + review_completion_archive=archive, + operator_evidence=operator_evidence, + execute_requested=execute_requested, + ) + data["phase"] = service.phase + return jsonify(data), 400 if payload_error else 200 + + @market_intel_review_bp.route( "/api/market_intel/manual_sample_review/" "candidate_queue_review_completion_archive", @@ -110,23 +178,18 @@ def market_intel_manual_sample_candidate_queue_review_completion_archive(): sample_result, operator_evidence, writer_output, smoke_result, payload_error, limit = ( _extract_run_payload() ) - transaction, receipt, closeout, inventory = _build_post_closeout_inventory_stack( - service=service, - sample_result=sample_result, - payload_error=payload_error, - operator_evidence=operator_evidence, - writer_output=writer_output, - smoke_result=smoke_result, - limit=limit, - execute_requested=execute_requested, - ) - data = build_candidate_queue_review_completion_archive( - transaction_preview=transaction, - run_receipt=receipt, - run_closeout=closeout, - post_closeout_inventory=inventory, - operator_evidence=operator_evidence, - execute_requested=execute_requested, + transaction, receipt, closeout, inventory, archive = ( + _build_review_completion_archive_stack( + service=service, + sample_result=sample_result, + payload_error=payload_error, + operator_evidence=operator_evidence, + writer_output=writer_output, + smoke_result=smoke_result, + limit=limit, + execute_requested=execute_requested, + ) ) + data = archive data["phase"] = service.phase return jsonify(data), 400 if payload_error else 200 diff --git a/services/market_intel/candidate_queue_review_archive_summary.py b/services/market_intel/candidate_queue_review_archive_summary.py new file mode 100644 index 0000000..47f4c07 --- /dev/null +++ b/services/market_intel/candidate_queue_review_archive_summary.py @@ -0,0 +1,381 @@ +"""候選審核 queue archive summary 預覽。 + +本模組只整理 review completion archive 後可供摘要/報表審核的結構化輸入; +不呼叫 LLM、不派送 Telegram、不讀 approval token、不執行 CLI、不寫檔、 +不寫 DB、不更新 review_state、不 commit、不掛 scheduler。 +""" + + +FORBIDDEN_TOKEN_KEYWORDS = ( + "approval_token", + "approval-token", + "market_intel_queue_write_approval", +) +SAFE_TOKEN_METADATA_KEYS = { + "approval_token_present", + "approval_token_valid", + "approval_token_secret_configured", +} +SAFE_APPROVAL_ENV_VAR = "MARKET_INTEL_QUEUE_WRITE_APPROVAL" +TARGET_TABLE = "market_alert_review_queue" + + +def _as_dict(value): + return value if isinstance(value, dict) else {} + + +def _as_list(value): + if value is None: + return [] + if isinstance(value, (list, tuple, set)): + return list(value) + return [value] + + +def _contains_forbidden_token_key(value): + if isinstance(value, dict): + for key, nested in value.items(): + normalized_key = str(key).lower() + if normalized_key in SAFE_TOKEN_METADATA_KEYS and isinstance(nested, bool): + continue + if normalized_key == "approval_env_var" and nested == SAFE_APPROVAL_ENV_VAR: + continue + if any(token_key in normalized_key for token_key in FORBIDDEN_TOKEN_KEYWORDS): + return True + if _contains_forbidden_token_key(nested): + return True + elif isinstance(value, list): + return any(_contains_forbidden_token_key(item) for item in value) + return False + + +def _safe_text(value, limit=300): + if value is None: + return None + text = str(value).strip() + return text[:limit] if text else None + + +def _side_effects_clear(*payloads): + blocked_keys = ( + "api_executes_cli", + "api_writes_file", + "api_writes_database", + "api_updates_review_state", + "archive_file_written", + "archive_record_written", + "archive_manifest_written", + "summary_file_written", + "summary_record_written", + "summary_manifest_written", + "database_write_executed", + "database_commit_executed", + "migration_executed", + "external_network_executed", + "scheduler_attached", + "review_state_update_executed", + "llm_call_executed", + "ollama_call_executed", + "gemini_call_executed", + "telegram_dispatched", + "ai_summary_generated", + "writes_executed", + "would_write_database", + ) + return all( + not _as_dict(payload).get(key) + for payload in payloads + for key in blocked_keys + ) + + +def _operator_summary(operator_evidence): + operator_evidence = _as_dict(operator_evidence) + return { + "provided_keys": sorted(operator_evidence.keys()), + "operator_confirmed_ai_summary_review": bool( + operator_evidence.get("operator_confirmed_ai_summary_review") + or operator_evidence.get("operator_confirmed_archive_summary_review") + ), + "operator_confirmed_summary_is_read_only": bool( + operator_evidence.get("operator_confirmed_summary_is_read_only") + or operator_evidence.get("operator_confirmed_archive_is_read_only") + ), + "operator_confirmed_no_llm_call": bool( + operator_evidence.get("operator_confirmed_no_llm_call") + ), + "operator_confirmed_no_telegram_dispatch": bool( + operator_evidence.get("operator_confirmed_no_telegram_dispatch") + ), + "operator_confirmed_no_scheduler_attach": bool( + operator_evidence.get("operator_confirmed_no_scheduler_attach") + ), + "operator_confirmed_no_api_db_write": bool( + operator_evidence.get("operator_confirmed_no_api_db_write") + ), + "archive_summary_artifact_path": _safe_text( + operator_evidence.get("archive_summary_artifact_path") + or operator_evidence.get("ai_summary_input_artifact_path") + ), + "approval_token_submitted_to_api": _contains_forbidden_token_key( + operator_evidence + ), + } + + +def _archive_payload_summary(review_completion_archive): + archive = _as_dict(review_completion_archive) + manifest = _as_dict(archive.get("archive_manifest")) + manifest_inventory = _as_dict(manifest.get("inventory")) + artifact_paths = _as_dict(manifest.get("artifact_paths")) + expected_keys = sorted(str(key) for key in _as_list(archive.get("expected_dedupe_keys"))) + found_keys = sorted(str(key) for key in _as_list(archive.get("found_dedupe_keys"))) + missing_keys = sorted(str(key) for key in _as_list(archive.get("missing_dedupe_keys"))) + state_mismatches = _as_list(archive.get("state_mismatches")) + return { + "provided": bool(archive), + "mode": archive.get("mode"), + "review_completion_archive_ready": bool( + archive.get("review_completion_archive_ready") + ), + "archive_manifest_ready": bool(archive.get("archive_manifest_ready")), + "ready_for_ai_summary_review": bool(archive.get("ready_for_ai_summary_review")), + "expected_dedupe_keys": expected_keys, + "found_dedupe_keys": found_keys, + "missing_dedupe_keys": missing_keys, + "state_mismatches": state_mismatches, + "row_summary_count": int(archive.get("row_summary_count") or 0), + "evidence_bundle_ids": _as_list( + manifest_inventory.get("evidence_bundle_ids") + ), + "artifact_paths": { + "review_state_receipt_artifact_path": artifact_paths.get( + "review_state_receipt_artifact_path" + ), + "review_state_closeout_artifact_path": artifact_paths.get( + "review_state_closeout_artifact_path" + ), + "post_closeout_inventory_artifact_path": artifact_paths.get( + "post_closeout_inventory_artifact_path" + ), + "review_completion_archive_path": artifact_paths.get( + "review_completion_archive_path" + ), + }, + "read_only_query_executed": bool(archive.get("read_only_query_executed")), + "database_connection_opened": bool(archive.get("database_connection_opened")), + "database_write_executed": bool(archive.get("database_write_executed")), + "database_commit_executed": bool(archive.get("database_commit_executed")), + "review_state_update_executed": bool( + archive.get("review_state_update_executed") + ), + "scheduler_attached": bool(archive.get("scheduler_attached")), + "blocked_reasons": _as_list(archive.get("blocked_reasons")), + "safe_boundaries": _as_list(archive.get("safe_boundaries")), + } + + +def _summary_sections(archive): + return [ + { + "key": "review_scope", + "title": "人工 review 範圍", + "facts": [ + f"expected_dedupe_key_count={len(archive['expected_dedupe_keys'])}", + f"found_dedupe_key_count={len(archive['found_dedupe_keys'])}", + f"row_summary_count={archive['row_summary_count']}", + ], + }, + { + "key": "review_state_result", + "title": "review_state 結果", + "facts": [ + f"missing_dedupe_key_count={len(archive['missing_dedupe_keys'])}", + f"state_mismatch_count={len(archive['state_mismatches'])}", + ], + }, + { + "key": "evidence_artifacts", + "title": "封存證據", + "facts": [ + f"artifact_paths_recorded={sum(1 for value in archive['artifact_paths'].values() if value)}", + f"evidence_bundle_count={len(archive['evidence_bundle_ids'])}", + ], + }, + { + "key": "execution_safety", + "title": "執行邊界", + "facts": [ + "llm_call_executed=false", + "telegram_dispatched=false", + "database_write_executed=false", + ], + }, + ] + + +def _summary_gates(*, archive, operator): + artifact_paths = archive["artifact_paths"] + return [ + { + "key": "review_completion_archive_preview_provided", + "label": "必須提供 review completion archive preview", + "passed": bool( + archive["provided"] + and archive["mode"] + == "candidate_queue_review_completion_archive_preview" + ), + }, + { + "key": "review_completion_archive_ready", + "label": "review completion archive 必須已通過", + "passed": bool( + archive["review_completion_archive_ready"] + and archive["archive_manifest_ready"] + and archive["ready_for_ai_summary_review"] + ), + }, + { + "key": "summary_dedupe_scope_complete", + "label": "摘要輸入必須具備完整 expected / found dedupe key", + "passed": bool( + archive["expected_dedupe_keys"] + and archive["expected_dedupe_keys"] == archive["found_dedupe_keys"] + and not archive["missing_dedupe_keys"] + ), + }, + { + "key": "summary_review_state_clean", + "label": "摘要前不可存在 review_state mismatch", + "passed": bool(archive["row_summary_count"] > 0 and not archive["state_mismatches"]), + }, + { + "key": "summary_artifact_paths_recorded", + "label": "摘要輸入必須保留所有上游 artifact path", + "passed": all(artifact_paths.values()), + }, + { + "key": "operator_confirmed_archive_summary_review", + "label": "操作員確認此步只做摘要輸入審核", + "passed": bool( + operator["operator_confirmed_ai_summary_review"] + and operator["operator_confirmed_summary_is_read_only"] + and operator["operator_confirmed_no_llm_call"] + and operator["operator_confirmed_no_telegram_dispatch"] + and operator["operator_confirmed_no_api_db_write"] + and operator["operator_confirmed_no_scheduler_attach"] + ), + }, + { + "key": "summary_artifact_path_recorded", + "label": "摘要輸入 artifact path 必須由操作員提供", + "passed": bool(operator["archive_summary_artifact_path"]), + }, + { + "key": "archive_summary_no_approval_token_submitted_to_api", + "label": "payload 不得包含一次性 approval token key", + "passed": not operator["approval_token_submitted_to_api"], + }, + { + "key": "archive_summary_preview_has_no_side_effects", + "label": "archive summary gate 不得呼叫 LLM、派送 Telegram、寫檔、寫 DB 或掛 scheduler", + "passed": _side_effects_clear(archive), + }, + ] + + +def build_candidate_queue_review_archive_summary( + *, + review_completion_archive, + operator_evidence=None, + execute_requested=False, +): + """建立 archive summary 預覽;不執行 LLM、檔案、網路或 DB 副作用。""" + archive = _archive_payload_summary(review_completion_archive) + operator = _operator_summary(operator_evidence) + gates = _summary_gates(archive=archive, operator=operator) + blocked_reasons = [gate["key"] for gate in gates if not gate["passed"]] + summary_ready = bool(not blocked_reasons) + sections = _summary_sections(archive) + + return { + "mode": "candidate_queue_review_archive_summary_preview", + "target_table": TARGET_TABLE, + "target_operation": "prepare_archive_summary_input", + "execute_requested": bool(execute_requested), + "archive_summary_ready": summary_ready, + "summary_input_ready": summary_ready, + "ready_for_next_manual_phase": summary_ready, + "ready_for_ai_summary_review": summary_ready, + "ready_for_ai_summary_generation": False, + "ready_for_llm_call": False, + "ready_for_telegram_dispatch": False, + "ready_for_api_review_state_update": False, + "ready_for_api_database_write": False, + "ready_for_scheduler_attach": False, + "api_executes_cli": False, + "api_reads_approval_token": False, + "api_writes_file": False, + "api_writes_database": False, + "api_updates_review_state": False, + "approval_record_written": False, + "decision_record_written": False, + "summary_file_written": False, + "summary_record_written": False, + "summary_manifest_written": False, + "ai_summary_generated": False, + "llm_call_executed": False, + "ollama_call_executed": False, + "gemini_call_executed": False, + "telegram_dispatched": False, + "review_state_update_executed": False, + "read_only_query_executed": archive["read_only_query_executed"], + "database_connection_opened": archive["database_connection_opened"], + "database_session_created": False, + "explicit_transaction_opened": False, + "transaction_opened": False, + "transaction_committed": False, + "database_write_executed": False, + "database_commit_executed": False, + "database_rollback_executed": False, + "external_network_executed": False, + "scheduler_attached": False, + "writes_executed": False, + "would_write_database": False, + "expected_dedupe_keys": archive["expected_dedupe_keys"], + "found_dedupe_keys": archive["found_dedupe_keys"], + "missing_dedupe_keys": archive["missing_dedupe_keys"], + "state_mismatches": archive["state_mismatches"], + "row_summary_count": archive["row_summary_count"], + "summary_sections": sections, + "artifact_paths": { + **archive["artifact_paths"], + "archive_summary_artifact_path": operator[ + "archive_summary_artifact_path" + ], + }, + "operator_summary_review": operator, + "blocked_reasons": blocked_reasons, + "gates": gates, + "next_operator_steps": [ + "確認 archive summary input 只包含摘要事實與 artifact path", + "確認本階段未呼叫 LLM、未派送 Telegram、未寫 DB", + "後續若要產生 AI 摘要,必須另開 Ollama-first summary gate", + "若 summary scope 不完整,回到 review completion archive 修正", + ], + "safe_boundaries": [ + "do_not_call_llm_from_archive_summary_gate", + "do_not_dispatch_telegram_from_archive_summary_gate", + "do_not_write_summary_file_from_api", + "do_not_insert_summary_record_from_api", + "do_not_update_review_state_from_archive_summary_gate", + "do_not_read_approval_token_from_archive_summary_gate", + "do_not_execute_cli_from_archive_summary_gate", + "do_not_attach_scheduler_from_archive_summary_gate", + "ollama_first_only_for_future_llm_summary", + "gemini_backup_only_for_future_llm_summary", + "archive_summary_preview_only", + "no_remove_orphans", + "no_momo_db_lifecycle_change", + ], + } diff --git a/services/market_intel/deployment_readiness.py b/services/market_intel/deployment_readiness.py index 7e5b273..22f205f 100644 --- a/services/market_intel/deployment_readiness.py +++ b/services/market_intel/deployment_readiness.py @@ -26,10 +26,11 @@ from services.market_intel.candidate_queue_review_decision_writer_run_receipt im from services.market_intel.candidate_queue_review_decision_writer_run_closeout import build_candidate_queue_review_decision_writer_run_closeout from services.market_intel.candidate_queue_review_decision_post_closeout_inventory import build_candidate_queue_review_decision_post_closeout_inventory from services.market_intel.candidate_queue_review_completion_archive import build_candidate_queue_review_completion_archive +from services.market_intel.candidate_queue_review_archive_summary import build_candidate_queue_review_archive_summary BLOCKED_RUN_REVIEW_KEYS = ("ready_for_api_database_write", "ready_for_scheduler_attach", "api_executes_cli", "api_reads_approval_token", "api_writes_file", "api_writes_database", "api_updates_review_state", "approval_record_written", "decision_record_written", "review_state_update_executed", "database_connection_opened", "database_session_created", "explicit_transaction_opened", "transaction_opened", "transaction_committed", "database_write_executed", "database_commit_executed", "database_rollback_executed", "scheduler_attached", "writes_executed", "would_write_database") -PRODUCTION_SMOKE_TARGETS = ("/health", "/market_intel", "/api/market_intel/status", "/api/market_intel/deployment_readiness", "/api/market_intel/schema_smoke", "/api/market_intel/schema_db_probe", "/api/market_intel/platform_seed_db_diff", "/api/market_intel/legacy_source_bridge", "/api/market_intel/mcp_readiness", "/api/market_intel/mcp_tool_contract", "/api/market_intel/mcp_deploy_preflight", "/api/market_intel/mcp_activation_runbook", "/api/market_intel/mcp_fetch_gate", "/api/market_intel/scheduler_plan", "/api/market_intel/manual_sample_plan", "/api/market_intel/manual_sample_acceptance", "/api/market_intel/manual_sample_review", "/api/market_intel/match_review_plan", "/api/market_intel/opportunity_plan", "/api/market_intel/opportunity_scoring_plan", "/api/market_intel/opportunity_evidence_plan", "/api/market_intel/opportunity_alert_plan", "/api/market_intel/migration_apply_drill", "/api/market_intel/migration_catalog_review", "/api/market_intel/migration_live_smoke", "/api/market_intel/live_db_inventory", "/api/market_intel/manual_sample_review/candidate_queue_writer_postwrite_smoke", "/api/market_intel/manual_sample_review/candidate_queue_writer_operator_drill", "/api/market_intel/manual_sample_review/candidate_queue_writer_run_package", "/api/market_intel/manual_sample_review/candidate_queue_writer_run_readiness", "/api/market_intel/manual_sample_review/candidate_queue_writer_run_receipt", "/api/market_intel/manual_sample_review/candidate_queue_writer_run_closeout", "/api/market_intel/manual_sample_review/candidate_queue_review_handoff", "/api/market_intel/manual_sample_review/candidate_queue_review_inventory", "/api/market_intel/manual_sample_review/candidate_queue_review_decision", "/api/market_intel/manual_sample_review/candidate_queue_review_decision_approval", "/api/market_intel/manual_sample_review/candidate_queue_review_decision_transaction", "/api/market_intel/manual_sample_review/candidate_queue_review_decision_writer_preflight", "/api/market_intel/manual_sample_review/candidate_queue_review_decision_writer_postwrite_smoke", "/api/market_intel/manual_sample_review/candidate_queue_review_decision_writer_operator_drill", "/api/market_intel/manual_sample_review/candidate_queue_review_decision_writer_run_package", "/api/market_intel/manual_sample_review/candidate_queue_review_decision_writer_run_readiness", "/api/market_intel/manual_sample_review/candidate_queue_review_decision_writer_run_receipt", "/api/market_intel/manual_sample_review/candidate_queue_review_decision_writer_run_closeout", "/api/market_intel/manual_sample_review/candidate_queue_review_decision_post_closeout_inventory", "/api/market_intel/manual_sample_review/candidate_queue_review_completion_archive", "/api/market_intel/manual_sample_review/candidate_queue_review_decision_writer_status") +PRODUCTION_SMOKE_TARGETS = ("/health", "/market_intel", "/api/market_intel/status", "/api/market_intel/deployment_readiness", "/api/market_intel/schema_smoke", "/api/market_intel/schema_db_probe", "/api/market_intel/platform_seed_db_diff", "/api/market_intel/legacy_source_bridge", "/api/market_intel/mcp_readiness", "/api/market_intel/mcp_tool_contract", "/api/market_intel/mcp_deploy_preflight", "/api/market_intel/mcp_activation_runbook", "/api/market_intel/mcp_fetch_gate", "/api/market_intel/scheduler_plan", "/api/market_intel/manual_sample_plan", "/api/market_intel/manual_sample_acceptance", "/api/market_intel/manual_sample_review", "/api/market_intel/match_review_plan", "/api/market_intel/opportunity_plan", "/api/market_intel/opportunity_scoring_plan", "/api/market_intel/opportunity_evidence_plan", "/api/market_intel/opportunity_alert_plan", "/api/market_intel/migration_apply_drill", "/api/market_intel/migration_catalog_review", "/api/market_intel/migration_live_smoke", "/api/market_intel/live_db_inventory", "/api/market_intel/manual_sample_review/candidate_queue_writer_postwrite_smoke", "/api/market_intel/manual_sample_review/candidate_queue_writer_operator_drill", "/api/market_intel/manual_sample_review/candidate_queue_writer_run_package", "/api/market_intel/manual_sample_review/candidate_queue_writer_run_readiness", "/api/market_intel/manual_sample_review/candidate_queue_writer_run_receipt", "/api/market_intel/manual_sample_review/candidate_queue_writer_run_closeout", "/api/market_intel/manual_sample_review/candidate_queue_review_handoff", "/api/market_intel/manual_sample_review/candidate_queue_review_inventory", "/api/market_intel/manual_sample_review/candidate_queue_review_decision", "/api/market_intel/manual_sample_review/candidate_queue_review_decision_approval", "/api/market_intel/manual_sample_review/candidate_queue_review_decision_transaction", "/api/market_intel/manual_sample_review/candidate_queue_review_decision_writer_preflight", "/api/market_intel/manual_sample_review/candidate_queue_review_decision_writer_postwrite_smoke", "/api/market_intel/manual_sample_review/candidate_queue_review_decision_writer_operator_drill", "/api/market_intel/manual_sample_review/candidate_queue_review_decision_writer_run_package", "/api/market_intel/manual_sample_review/candidate_queue_review_decision_writer_run_readiness", "/api/market_intel/manual_sample_review/candidate_queue_review_decision_writer_run_receipt", "/api/market_intel/manual_sample_review/candidate_queue_review_decision_writer_run_closeout", "/api/market_intel/manual_sample_review/candidate_queue_review_decision_post_closeout_inventory", "/api/market_intel/manual_sample_review/candidate_queue_review_completion_archive", "/api/market_intel/manual_sample_review/candidate_queue_review_archive_summary", "/api/market_intel/manual_sample_review/candidate_queue_review_decision_writer_status") def _run_review_preview_safe(payload, mode): @@ -174,6 +175,9 @@ def build_deployment_readiness_preview( run_closeout=candidate_queue_review_decision_writer_run_closeout, post_closeout_inventory=candidate_queue_review_decision_post_closeout_inventory, ) + candidate_queue_review_archive_summary = build_candidate_queue_review_archive_summary( + review_completion_archive=candidate_queue_review_completion_archive, + ) checks = { "schema_smoke_passed": bool(schema_smoke["passed"]), "feature_flags_default_safe": bool( @@ -448,6 +452,10 @@ def build_deployment_readiness_preview( candidate_queue_review_completion_archive, "candidate_queue_review_completion_archive_preview", ), + "candidate_queue_review_archive_summary_preview_safe": _run_review_preview_safe( + candidate_queue_review_archive_summary, + "candidate_queue_review_archive_summary_preview", + ), "candidate_queue_review_decision_writer_cli_status_safe": _run_review_preview_safe( candidate_queue_review_decision_writer_status, "candidate_queue_review_decision_writer_cli_blocked", @@ -693,6 +701,7 @@ def build_deployment_readiness_preview( "candidate_queue_review_decision_writer_run_closeout": candidate_queue_review_decision_writer_run_closeout, "candidate_queue_review_decision_post_closeout_inventory": candidate_queue_review_decision_post_closeout_inventory, "candidate_queue_review_completion_archive": candidate_queue_review_completion_archive, + "candidate_queue_review_archive_summary": candidate_queue_review_archive_summary, "candidate_queue_review_decision_writer_status": candidate_queue_review_decision_writer_status, "match_review_plan": match_review_plan, "opportunity_plan": opportunity_plan, diff --git a/services/market_intel/phase.py b/services/market_intel/phase.py index f6a627c..ed895a3 100644 --- a/services/market_intel/phase.py +++ b/services/market_intel/phase.py @@ -1,3 +1,3 @@ """市場情報 rollout phase 單一來源。""" -MARKET_INTEL_PHASE = "phase_78_candidate_queue_review_completion_archive" +MARKET_INTEL_PHASE = "phase_79_candidate_queue_review_archive_summary"