From 03ab276032bce2afa2244be874c95eda25d8f80f Mon Sep 17 00:00:00 2001 From: OoO Date: Tue, 19 May 2026 21:57:44 +0800 Subject: [PATCH] =?UTF-8?q?[V10.279]=20=E6=94=B6=E6=96=82=20Code=20Review?= =?UTF-8?q?=20Ollama=20=E8=B7=AF=E5=BE=91=E4=B8=A6=E8=A3=9C=20Phase80=20ga?= =?UTF-8?q?te=20|=20code=5Freview,=20market=5Fintel?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env.example | 7 +- TODO_NEXT_STEPS.txt | 2 + config.py | 2 +- docs/AI_INTELLIGENCE_MODULE_SOT.md | 3 +- ...s-platform-market-campaign-intelligence.md | 1 + services/code_review_pipeline_service.py | 24 ++- services/market_intel/deployment_readiness.py | 46 +++++- services/market_intel/phase.py | 2 +- templates/market_intel/disabled.html | 119 +++++++++++++++ tests/test_code_review_claude_routing.py | 48 ++++++ tests/test_market_intel_skeleton.py | 140 +++++++++--------- 11 files changed, 309 insertions(+), 85 deletions(-) diff --git a/.env.example b/.env.example index 8776405..45f5b6f 100644 --- a/.env.example +++ b/.env.example @@ -213,8 +213,11 @@ BOT_API_TOKEN=your_bot_api_token_here # [必填] Post-deploy AI code review pipeline 自動修復主開關 # ADR-020 規定預設 true(任何 finding 一律自動觸發 AiderHeal,安全網=Git+CI/CD 回滾) # 僅在需要短期關閉自動修復鏈時設為 false -CODE_REVIEW_OLLAMA_MODEL=qwen3:14b -CODE_REVIEW_OLLAMA_TIMEOUT=120 +CODE_REVIEW_OLLAMA_MODEL=qwen2.5-coder:7b +CODE_REVIEW_OLLAMA_TIMEOUT=45 +CODE_REVIEW_HERMES_TIMEOUT=35 +CODE_REVIEW_HERMES_MAX_FILES=3 +CODE_REVIEW_HERMES_MAX_CHARS=2500 CODE_REVIEW_AUTO_FIX_ENABLED=true # [選填] 僅本機開發可設 true;正式環境不得允許不安全 internal webhook diff --git a/TODO_NEXT_STEPS.txt b/TODO_NEXT_STEPS.txt index d7df312..a6063d6 100644 --- a/TODO_NEXT_STEPS.txt +++ b/TODO_NEXT_STEPS.txt @@ -4,6 +4,7 @@ ================================================================================ 【已完成】 + - V10.279 收斂 Code Review Ollama-first 路徑:OpenClaw assessment 預設改 `qwen2.5-coder:7b` + 45s/host timeout,Hermes scan 改 compact snippet + 35s/host timeout,避免三主機各卡 120s 後被迫觸發 Gemini 備援。 - V10.278 補 PChome 競價摘要 30 分鐘共享快取與 feeder/backfill 主動清除,並新增市場情報 `candidate_queue_review_ai_summary_preflight` 預覽 gate;API 只檢查未來摘要輸入與 Ollama-first/Gemini-backup-only policy,不呼叫 LLM、不派 Telegram、不寫 DB、不掛 scheduler。 - V10.276 修正 ElephantAlpha 價格類 Hermes prefetch timeout:`price_drop` / `market_opportunity` trigger 直接把 SQL 命中的 MOMO / PChome 價差實證轉成 HITL action lines,完整 Hermes LLM prefetch 預設關閉;無 DB 實證仍只記 suppressed telemetry / cooldown,不寫 `human_review`、不發空 Telegram。 - V10.266 強化核心 MOMO/PChome 比價鏈路:新增 `marketplace_product_matcher.py` 身份比對、只讓 `identity_v2` 且分數 ≥ 0.76 的高信心配對進 Dashboard/AI/Excel/Daily/Growth/PPT,並建立 `competitor_intel_repository.py` 統一圖表與簡報資料出口;同品牌但不同型號、不同組數、套組/單品或多品項不一致會進待審,不進正式比價。 @@ -156,6 +157,7 @@ - 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.276。 + - Phase 80 candidate queue review AI summary preflight:新增 `services/market_intel/candidate_queue_review_ai_summary_preflight.py`、POST `/api/market_intel/manual_sample_review/candidate_queue_review_ai_summary_preflight` 與 UI preflight 按鈕,在 archive summary 後檢查 Ollama-first 三主機級聯與 Gemini 備援邊界;API/UI 不呼叫 LLM、不派送 Telegram、不寫檔、不讀 token、不執行 CLI、不更新 review_state、不寫 DB、不 commit、不掛 scheduler;版本同步至 V10.278。 - 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 c4f703d..463d731 100644 --- a/config.py +++ b/config.py @@ -320,7 +320,7 @@ YOUTUBE_API_KEY = os.getenv('YOUTUBE_API_KEY', '') # ========================================== # 系統版本與路徑 # ========================================== -SYSTEM_VERSION = "V10.278" +SYSTEM_VERSION = "V10.279" LOG_FILE_PATH = os.path.join(BASE_DIR, 'logs/system.log') public_url = PUBLIC_URL # 用於模板顯示 diff --git a/docs/AI_INTELLIGENCE_MODULE_SOT.md b/docs/AI_INTELLIGENCE_MODULE_SOT.md index d119af4..aa8a213 100644 --- a/docs/AI_INTELLIGENCE_MODULE_SOT.md +++ b/docs/AI_INTELLIGENCE_MODULE_SOT.md @@ -2,7 +2,7 @@ > **最後更新**: 2026-05-19 (台北時間) > **狀態**: 🟢 四 AI Agent 自動化閉環已落地;LLM 路由紅線升級為 Ollama-first 三主機級聯,Gemini 僅備援 / 鎖定場景 -> **適用版本**: V10.278 +> **適用版本**: V10.279 --- @@ -17,6 +17,7 @@ - NemoTron qwen3 dispatch 的 `/api/chat` tool-calling 路徑也必須同一請求最多嘗試三台 Ollama,第一台失敗要 `mark_unhealthy()` 後再試下一台,最後才 fallback NIM。 - PPT vision、PPT 文案 final fallback、MCP 離線 final fallback 等特殊 Ollama 路徑也不得只打單一 host;如需 `/api/generate`,一律透過 `OllamaService.generate()`。 - Code Review pipeline 也必須 Ollama-first:Hermes scan 與 OpenClaw assessment 都走 `OllamaService` 三主機 retry;Gemini telemetry 只能以 `code_review_openclaw_gemini` 出現,表示 Ollama/可選 Claude 備援都失敗後才啟用。 +- Code Review 的 OpenClaw assessment 預設使用 `qwen2.5-coder:7b` 與 45s/host timeout;Hermes scan 只送 compact snippet(預設 3 檔、每檔 2500 字)並使用 35s/host timeout,避免三主機各卡 120s 後把正常 code review 推進 Gemini 備援。 - OpenClaw Telegram Q&A 主路徑也不得綁單一 host:`_call_qwen3_qa()` 必須透過 `OllamaService` 跑 GCP-A → GCP-B → 111,並把實際落點寫入 `ai_calls.provider`。 - OpenClaw Telegram 圖片商品辨識也必須 Ollama-first:`_identify_product_name_with_ollama_vision()` 透過 `OllamaService` 嘗試 GCP-A → GCP-B → 111;Gemini 只允許以 `openclaw_bot_image_gemini` caller 作為失敗後備援。 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 c6bbf91..1b05f17 100644 --- a/docs/adr/ADR-035-cross-platform-market-campaign-intelligence.md +++ b/docs/adr/ADR-035-cross-platform-market-campaign-intelligence.md @@ -205,6 +205,7 @@ EwoooC 目前已有 MOMO EDM / 節慶活動資料、`promo_products`、PChome - 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。 +- 2026-05-19 追加 candidate queue review AI summary preflight:`services.market_intel.candidate_queue_review_ai_summary_preflight` 與 `/api/market_intel/manual_sample_review/candidate_queue_review_ai_summary_preflight` 在 archive summary 後檢查未來 AI 摘要前置條件、Ollama-first 三主機級聯與 Gemini 備援邊界。此 preflight 不產生 AI 摘要;API/UI 不呼叫 LLM、不派送 Telegram、不寫檔、不讀 approval token、不執行 CLI、不更新 `review_state`、不寫 DB、不 commit、不掛 scheduler;188 不可作為 Ollama 節點。 ### Phase 4:Coupang / Shopee Adapter diff --git a/services/code_review_pipeline_service.py b/services/code_review_pipeline_service.py index 7cf3311..7ab0b6b 100644 --- a/services/code_review_pipeline_service.py +++ b/services/code_review_pipeline_service.py @@ -45,9 +45,12 @@ GEMINI_API_KEY = os.getenv("GEMINI_API_KEY", "") REVIEW_MODEL = os.getenv("OPENCLAW_MODEL", "gemini-2.5-flash") CODE_REVIEW_OLLAMA_MODEL = os.getenv( "CODE_REVIEW_OLLAMA_MODEL", - os.getenv("OPENCLAW_OLLAMA_MODEL", "qwen3:14b"), + os.getenv("OPENCLAW_OLLAMA_MODEL", "qwen2.5-coder:7b"), ) -CODE_REVIEW_OLLAMA_TIMEOUT = int(os.getenv("CODE_REVIEW_OLLAMA_TIMEOUT", "120")) +CODE_REVIEW_OLLAMA_TIMEOUT = int(os.getenv("CODE_REVIEW_OLLAMA_TIMEOUT", "45")) +CODE_REVIEW_HERMES_TIMEOUT = int(os.getenv("CODE_REVIEW_HERMES_TIMEOUT", "35")) +CODE_REVIEW_HERMES_MAX_FILES = int(os.getenv("CODE_REVIEW_HERMES_MAX_FILES", "3")) +CODE_REVIEW_HERMES_MAX_CHARS = int(os.getenv("CODE_REVIEW_HERMES_MAX_CHARS", "2500")) INTERNAL_TOKEN = os.getenv("INTERNAL_WEBHOOK_TOKEN", "") AUTO_FIX_ENABLED = os.getenv("CODE_REVIEW_AUTO_FIX_ENABLED", "true").lower() == "true" ALLOW_INSECURE_WEBHOOK = os.getenv("MOMO_ALLOW_INSECURE_INTERNAL_WEBHOOK_FOR_DEV", "").lower() == "true" @@ -207,10 +210,13 @@ class CodeReviewPipeline: def _hermes_scan(self, files: Dict[str, str]) -> List[Dict]: """走 OllamaService 三主機級聯:GCP-A → GCP-B → 111。""" try: - files_text = "\n\n".join( - f"### {name}\n```python\n{content}\n```" - for name, content in list(files.items())[:4] - ) + compact_files = [] + for name, content in list(files.items())[:CODE_REVIEW_HERMES_MAX_FILES]: + clipped = content[:CODE_REVIEW_HERMES_MAX_CHARS] + if len(content) > CODE_REVIEW_HERMES_MAX_CHARS: + clipped += "\n... (截斷,僅掃描前段重點)" + compact_files.append(f"### {name}\n```python\n{clipped}\n```") + files_text = "\n\n".join(compact_files) prompt = f"""你是資深程式碼審查工程師,請掃描以下程式碼並列出所有問題。 @@ -234,14 +240,16 @@ class CodeReviewPipeline: model=_HERMES_MODEL, request_id=f"cr-{self.commit_sha[:8]}", meta={'commit': self.commit_sha[:8], 'branch': self.branch, - 'files': len(files), 'route': 'ollama_first'}, + 'files': len(files), 'route': 'ollama_first', + 'max_files': CODE_REVIEW_HERMES_MAX_FILES, + 'max_chars': CODE_REVIEW_HERMES_MAX_CHARS}, ) as _ctx: ollama = OllamaService(model=_HERMES_MODEL) resp = ollama.generate( prompt=prompt, model=_HERMES_MODEL, temperature=0.1, - timeout=CODE_REVIEW_OLLAMA_TIMEOUT, + timeout=CODE_REVIEW_HERMES_TIMEOUT, ) actual_provider = get_provider_tag(resp.host or '') _ctx.set_provider(actual_provider) diff --git a/services/market_intel/deployment_readiness.py b/services/market_intel/deployment_readiness.py index 22f205f..ec41853 100644 --- a/services/market_intel/deployment_readiness.py +++ b/services/market_intel/deployment_readiness.py @@ -27,10 +27,44 @@ from services.market_intel.candidate_queue_review_decision_writer_run_closeout i 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 +from services.market_intel.candidate_queue_review_ai_summary_preflight import build_candidate_queue_review_ai_summary_preflight -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_archive_summary", "/api/market_intel/manual_sample_review/candidate_queue_review_decision_writer_status") +BLOCKED_RUN_REVIEW_KEYS = ( + "ready_for_api_database_write", + "ready_for_scheduler_attach", + "ready_for_ai_summary_generation", + "ready_for_llm_call", + "ready_for_telegram_dispatch", + "api_executes_cli", + "api_reads_approval_token", + "api_writes_file", + "api_writes_database", + "api_updates_review_state", + "approval_record_written", + "decision_record_written", + "summary_file_written", + "summary_record_written", + "summary_manifest_written", + "ai_summary_generated", + "llm_call_executed", + "ollama_call_executed", + "gemini_call_executed", + "telegram_dispatched", + "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_archive_summary", "/api/market_intel/manual_sample_review/candidate_queue_review_ai_summary_preflight", "/api/market_intel/manual_sample_review/candidate_queue_review_decision_writer_status") def _run_review_preview_safe(payload, mode): @@ -178,6 +212,9 @@ def build_deployment_readiness_preview( candidate_queue_review_archive_summary = build_candidate_queue_review_archive_summary( review_completion_archive=candidate_queue_review_completion_archive, ) + candidate_queue_review_ai_summary_preflight = build_candidate_queue_review_ai_summary_preflight( + archive_summary=candidate_queue_review_archive_summary, + ) checks = { "schema_smoke_passed": bool(schema_smoke["passed"]), "feature_flags_default_safe": bool( @@ -456,6 +493,10 @@ def build_deployment_readiness_preview( candidate_queue_review_archive_summary, "candidate_queue_review_archive_summary_preview", ), + "candidate_queue_review_ai_summary_preflight_preview_safe": _run_review_preview_safe( + candidate_queue_review_ai_summary_preflight, + "candidate_queue_review_ai_summary_preflight_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", @@ -702,6 +743,7 @@ def build_deployment_readiness_preview( "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_ai_summary_preflight": candidate_queue_review_ai_summary_preflight, "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 ed895a3..2051009 100644 --- a/services/market_intel/phase.py +++ b/services/market_intel/phase.py @@ -1,3 +1,3 @@ """市場情報 rollout phase 單一來源。""" -MARKET_INTEL_PHASE = "phase_79_candidate_queue_review_archive_summary" +MARKET_INTEL_PHASE = "phase_80_candidate_queue_review_ai_summary_preflight" diff --git a/templates/market_intel/disabled.html b/templates/market_intel/disabled.html index a3be18d..7ec050b 100644 --- a/templates/market_intel/disabled.html +++ b/templates/market_intel/disabled.html @@ -676,6 +676,9 @@ + @@ -1029,6 +1032,7 @@ const sampleCandidateQueueReviewDecisionPostCloseoutInventory = sampleReviewRoot ? sampleReviewRoot.querySelector('[data-market-intel-sample-candidate-queue-review-decision-post-closeout-inventory]') : null; const sampleCandidateQueueReviewCompletionArchive = sampleReviewRoot ? sampleReviewRoot.querySelector('[data-market-intel-sample-candidate-queue-review-completion-archive]') : null; const sampleCandidateQueueReviewArchiveSummary = sampleReviewRoot ? sampleReviewRoot.querySelector('[data-market-intel-sample-candidate-queue-review-archive-summary]') : null; + const sampleCandidateQueueReviewAiSummaryPreflight = sampleReviewRoot ? sampleReviewRoot.querySelector('[data-market-intel-sample-candidate-queue-review-ai-summary-preflight]') : null; const sampleReviewEndpoint = "{{ url_for('market_intel.market_intel_manual_sample_review') }}"; const sampleReviewEvaluateEndpoint = "{{ url_for('market_intel.market_intel_manual_sample_review_evaluate') }}"; const sampleCandidateHandoffEndpoint = "{{ url_for('market_intel.market_intel_manual_sample_candidate_handoff') }}"; @@ -1059,6 +1063,7 @@ const sampleCandidateQueueReviewDecisionPostCloseoutInventoryEndpoint = "{{ url_for('market_intel_review.market_intel_manual_sample_candidate_queue_review_decision_post_closeout_inventory') }}"; const sampleCandidateQueueReviewCompletionArchiveEndpoint = "{{ url_for('market_intel_review.market_intel_manual_sample_candidate_queue_review_completion_archive') }}"; const sampleCandidateQueueReviewArchiveSummaryEndpoint = "{{ url_for('market_intel_review.market_intel_manual_sample_candidate_queue_review_archive_summary') }}"; + const sampleCandidateQueueReviewAiSummaryPreflightEndpoint = "{{ url_for('market_intel_review.market_intel_manual_sample_candidate_queue_review_ai_summary_preflight') }}"; const schedulerMeta = schedulerRoot ? schedulerRoot.querySelector('[data-market-intel-scheduler-meta]') : null; const schedulerBody = schedulerRoot ? schedulerRoot.querySelector('[data-market-intel-scheduler-body]') : null; const schedulerRefresh = schedulerRoot ? schedulerRoot.querySelector('[data-market-intel-scheduler-refresh]') : null; @@ -4724,6 +4729,117 @@ } }; + const renderCandidateQueueReviewAiSummaryPreflight = data => { + const blockers = (data.blocked_reasons || []).join(' / '); + const gates = data.gates || []; + const routePolicy = data.model_route_policy || {}; + const cascade = routePolicy.primary_cascade || []; + sampleReviewMeta.innerHTML = [ + `mode=${data.mode || 'unknown'}`, + `preflight=${data.ai_summary_preflight_ready ? 'ready' : 'blocked'}`, + `manual_ollama=${data.ready_for_manual_ollama_summary_run ? 'ready' : 'blocked'}`, + `llm=${data.llm_call_executed ? 'called' : 'blocked'}`, + `telegram=${data.telegram_dispatched ? 'sent' : 'blocked'}` + ].map(item => `${escapeHtml(item)}`).join(''); + sampleReviewBody.innerHTML = ` +
此卡只檢查 AI summary preflight;後續摘要必須 Ollama-first,Gemini 只能在 Ollama cascade 全失敗後備援。API/UI 不呼叫模型、不派送 Telegram、不寫檔、不讀 token、不寫 DB、不掛 scheduler。${blockers ? `阻擋:${escapeHtml(blockers)}` : ''}
+
+
+

PREFLIGHT GATES

+
${ + gates.map(gate => ` +
+
+ ${escapeHtml(gate.key)} + ${escapeHtml(gate.label)} +
+ ${gate.passed ? 'PASS' : 'BLOCK'} +
+ `).join('') || '
尚未提供 preflight gate。
' + }
+
+
+

MODEL ROUTE

+
+ ${[ + ['primary_policy', routePolicy.primary_policy || 'missing'], + ['fallback_policy', routePolicy.fallback_policy || 'missing'], + ['forbidden_node', routePolicy.app_host_forbidden_as_ollama_node || 'missing'] + ].map(([key, value]) => ` +
+
${escapeHtml(key)}
+ ${escapeHtml(String(value))} +
+ `).join('')} + ${cascade.map(node => ` +
+
+ ${escapeHtml(node.label || node.key || 'ollama')} + ${escapeHtml(node.host || '')} +
+ OLLAMA +
+ `).join('')} +
+
+
+

NO-SIDE-EFFECTS

+
+ ${[ + ['ready_for_ai_summary_generation', data.ready_for_ai_summary_generation], + ['ready_for_llm_call', data.ready_for_llm_call], + ['ready_for_telegram_dispatch', data.ready_for_telegram_dispatch], + ['ai_summary_generated', data.ai_summary_generated], + ['llm_call_executed', data.llm_call_executed], + ['ollama_call_executed', data.ollama_call_executed], + ['gemini_call_executed', data.gemini_call_executed], + ['telegram_dispatched', data.telegram_dispatched], + ['summary_file_written', data.summary_file_written], + ['database_write_executed', data.database_write_executed], + ['scheduler_attached', data.scheduler_attached] + ].map(([key, value]) => ` +
+
${escapeHtml(key)}
+ ${escapeHtml(String(value))} +
+ `).join('')} +
+
+
+ `; + }; + + const loadCandidateQueueReviewAiSummaryPreflight = async () => { + if (!sampleReviewMeta || !sampleReviewBody || !sampleReviewInput) return; + let parsed; + try { + parsed = JSON.parse(sampleReviewInput.value || '{}'); + } catch (error) { + sampleReviewMeta.innerHTML = 'json_error'; + sampleReviewBody.innerHTML = `
JSON 格式錯誤:${escapeHtml(error.message)}
`; + return; + } + const body = parsed && parsed.sample_result ? parsed : { sample_result: parsed }; + sampleReviewBody.innerHTML = '
檢查 queue review AI summary preflight 中...
'; + try { + const response = await fetch(sampleCandidateQueueReviewAiSummaryPreflightEndpoint, { + method: 'POST', + credentials: 'same-origin', + headers: { + 'Content-Type': 'application/json', + 'X-CSRFToken': csrfToken + }, + body: JSON.stringify(body) + }); + const data = await response.json(); + if (!response.ok && !data.mode) throw new Error(`HTTP ${response.status}`); + renderCandidateQueueReviewAiSummaryPreflight(data); + } catch (error) { + sampleReviewMeta.innerHTML = 'error'; + sampleReviewBody.innerHTML = `
queue review AI summary preflight 失敗:${escapeHtml(error.message)}
`; + } + }; + const renderCandidateQueueReviewDecisionWriter = data => { const blockers = (data.blocked_reasons || []).join(' / '); const summary = data.statement_summary || {}; @@ -6415,6 +6531,9 @@ if (sampleCandidateQueueReviewArchiveSummary) { sampleCandidateQueueReviewArchiveSummary.addEventListener('click', loadCandidateQueueReviewArchiveSummary); } + if (sampleCandidateQueueReviewAiSummaryPreflight) { + sampleCandidateQueueReviewAiSummaryPreflight.addEventListener('click', loadCandidateQueueReviewAiSummaryPreflight); + } if (schedulerRefresh) { schedulerRefresh.addEventListener('click', loadScheduler); } diff --git a/tests/test_code_review_claude_routing.py b/tests/test_code_review_claude_routing.py index 94ca656..75d3c0b 100644 --- a/tests/test_code_review_claude_routing.py +++ b/tests/test_code_review_claude_routing.py @@ -189,6 +189,54 @@ def test_default_uses_ollama_first(monkeypatch): fake_elephant.generate.assert_not_called() +def test_code_review_ollama_defaults_use_fast_local_model(monkeypatch): + """未設定 env 時,Code Review 用 coder 7B 與短 timeout,避免 120s*3 後才備援。""" + for key in ( + "CODE_REVIEW_OLLAMA_MODEL", + "OPENCLAW_OLLAMA_MODEL", + "CODE_REVIEW_OLLAMA_TIMEOUT", + "CODE_REVIEW_HERMES_TIMEOUT", + "CODE_REVIEW_HERMES_MAX_FILES", + "CODE_REVIEW_HERMES_MAX_CHARS", + ): + monkeypatch.delenv(key, raising=False) + + svc_mod = _reload_pipeline() + + assert svc_mod.CODE_REVIEW_OLLAMA_MODEL == "qwen2.5-coder:7b" + assert svc_mod.CODE_REVIEW_OLLAMA_TIMEOUT == 45 + assert svc_mod.CODE_REVIEW_HERMES_TIMEOUT == 35 + assert svc_mod.CODE_REVIEW_HERMES_MAX_FILES == 3 + assert svc_mod.CODE_REVIEW_HERMES_MAX_CHARS == 2500 + + +def test_hermes_scan_uses_compact_prompt_and_short_timeout(monkeypatch): + """Hermes scan 只送 compact snippet,避免大檔讓三主機各卡 120 秒。""" + monkeypatch.setenv("CODE_REVIEW_HERMES_TIMEOUT", "7") + monkeypatch.setenv("CODE_REVIEW_HERMES_MAX_FILES", "2") + monkeypatch.setenv("CODE_REVIEW_HERMES_MAX_CHARS", "20") + _stub_logger(monkeypatch) + + svc_mod = _reload_pipeline() + _ollama_cls, fake_ollama = _stub_ollama(monkeypatch, content="[]") + pipeline = _make_pipeline(svc_mod) + + result = pipeline._hermes_scan({ + "services/a.py": "A" * 80, + "services/b.py": "B" * 80, + "services/c.py": "C" * 80, + }) + + assert result == [] + kwargs = fake_ollama.generate.call_args.kwargs + assert kwargs["timeout"] == 7 + prompt = kwargs["prompt"] + assert "services/a.py" in prompt + assert "services/b.py" in prompt + assert "services/c.py" not in prompt + assert "截斷" in prompt + + def test_flag_true_still_uses_ollama_before_claude(monkeypatch): """flag=true 也不得跳過 Ollama;Ollama 成功時 Claude/Gemini 都不呼叫""" monkeypatch.setenv('CODE_REVIEW_USE_CLAUDE', 'true') diff --git a/tests/test_market_intel_skeleton.py b/tests/test_market_intel_skeleton.py index 2722b9f..5ab5199 100644 --- a/tests/test_market_intel_skeleton.py +++ b/tests/test_market_intel_skeleton.py @@ -1119,7 +1119,7 @@ def test_legacy_source_bridge_default_is_planned_only(): bridge = MarketIntelService().build_legacy_source_bridge() assert bridge["mode"] == "legacy_source_bridge_planned" - assert bridge["phase"] == "phase_79_candidate_queue_review_archive_summary" + assert bridge["phase"] == "phase_80_candidate_queue_review_ai_summary_preflight" assert bridge["execute_requested"] is False assert bridge["read_only_query_executed"] is False assert bridge["database_connection_opened"] is False @@ -1277,7 +1277,7 @@ def test_mcp_tool_contract_preview_is_read_only_and_whitelisted(): contract = MarketIntelService().build_mcp_tool_contract() assert contract["mode"] == "mcp_tool_contract_preview" - assert contract["phase"] == "phase_79_candidate_queue_review_archive_summary" + assert contract["phase"] == "phase_80_candidate_queue_review_ai_summary_preflight" assert contract["caller"] == "market_intel" assert contract["contract_ready"] is True assert contract["blocked_reasons"] == [] @@ -1410,7 +1410,7 @@ def test_mcp_activation_runbook_route_is_preview_only(): assert response.status_code == 200 assert data["mode"] == "mcp_activation_runbook_preview" - assert data["phase"] == "phase_79_candidate_queue_review_archive_summary" + assert data["phase"] == "phase_80_candidate_queue_review_ai_summary_preflight" assert data["deployment_actions_executed"] is False assert data["docker_command_executed"] is False assert data["ssh_command_executed"] is False @@ -1423,7 +1423,7 @@ def test_mcp_fetch_gate_default_blocks_external_fetch(): gate = MarketIntelService().build_mcp_fetch_gate(fetch_requested=True) assert gate["mode"] == "mcp_fetch_gate_planned" - assert gate["phase"] == "phase_79_candidate_queue_review_archive_summary" + assert gate["phase"] == "phase_80_candidate_queue_review_ai_summary_preflight" assert gate["fetch_requested"] is True assert gate["manual_fetch_gate_open"] is False assert gate["network_request_allowed"] is False @@ -1493,7 +1493,7 @@ def test_mcp_fetch_gate_route_is_preview_only(): assert response.status_code == 200 assert data["mode"] == "mcp_fetch_gate_planned" - assert data["phase"] == "phase_79_candidate_queue_review_archive_summary" + assert data["phase"] == "phase_80_candidate_queue_review_ai_summary_preflight" assert data["fetch_requested"] is False assert data["network_request_allowed"] is False assert data["external_network_executed"] is False @@ -1505,7 +1505,7 @@ def test_manual_sample_plan_preview_blocks_fetch_and_write(): plan = MarketIntelService().build_manual_sample_plan() assert plan["mode"] == "manual_sample_fetch_plan_preview" - assert plan["phase"] == "phase_79_candidate_queue_review_archive_summary" + assert plan["phase"] == "phase_80_candidate_queue_review_ai_summary_preflight" assert plan["ready_for_manual_sample_fetch"] is False assert plan["sample_fetch_executed"] is False assert plan["external_network_executed"] is False @@ -1553,7 +1553,7 @@ def test_manual_sample_plan_route_is_preview_only(): assert response.status_code == 200 assert data["mode"] == "manual_sample_fetch_plan_preview" - assert data["phase"] == "phase_79_candidate_queue_review_archive_summary" + assert data["phase"] == "phase_80_candidate_queue_review_ai_summary_preflight" assert data["sample_fetch_executed"] is False assert data["external_network_executed"] is False assert data["database_write_executed"] is False @@ -1564,7 +1564,7 @@ def test_manual_sample_acceptance_preview_blocks_candidate_import(): acceptance = MarketIntelService().build_manual_sample_acceptance() assert acceptance["mode"] == "manual_sample_acceptance_preview" - assert acceptance["phase"] == "phase_79_candidate_queue_review_archive_summary" + assert acceptance["phase"] == "phase_80_candidate_queue_review_ai_summary_preflight" assert acceptance["contract_ready"] is True assert acceptance["sample_result_loaded"] is False assert acceptance["sample_result_accepted"] is False @@ -1606,7 +1606,7 @@ def test_manual_sample_acceptance_route_is_preview_only(): assert response.status_code == 200 assert data["mode"] == "manual_sample_acceptance_preview" - assert data["phase"] == "phase_79_candidate_queue_review_archive_summary" + assert data["phase"] == "phase_80_candidate_queue_review_ai_summary_preflight" assert data["sample_result_loaded"] is False assert data["candidate_import_allowed"] is False assert data["external_network_executed"] is False @@ -1618,7 +1618,7 @@ def test_manual_sample_review_preview_is_planned_until_result_loaded(): review = MarketIntelService().build_manual_sample_review() assert review["mode"] == "manual_sample_review_preview" - assert review["phase"] == "phase_79_candidate_queue_review_archive_summary" + assert review["phase"] == "phase_80_candidate_queue_review_ai_summary_preflight" assert review["contract_ready"] is True assert review["sample_result_loaded"] is False assert review["sample_result_reviewed"] is False @@ -1729,7 +1729,7 @@ def test_manual_sample_review_route_is_preview_only(): assert response.status_code == 200 assert data["mode"] == "manual_sample_review_preview" - assert data["phase"] == "phase_79_candidate_queue_review_archive_summary" + assert data["phase"] == "phase_80_candidate_queue_review_ai_summary_preflight" assert data["sample_result_loaded"] is False assert data["sample_result_reviewed"] is False assert data["candidate_import_allowed"] is False @@ -1768,7 +1768,7 @@ def test_manual_sample_review_evaluation_preview_accepts_payload_without_persist ) assert review["mode"] == "manual_sample_review_evaluation_preview" - assert review["phase"] == "phase_79_candidate_queue_review_archive_summary" + assert review["phase"] == "phase_80_candidate_queue_review_ai_summary_preflight" assert review["review_request_type"] == "operator_posted_json" assert review["payload_received"] is True assert review["payload_valid_json_object"] is True @@ -1830,7 +1830,7 @@ def test_manual_sample_review_evaluate_route_is_post_only_and_no_write(): assert response.status_code == 200 assert data["mode"] == "manual_sample_review_evaluation_preview" - assert data["phase"] == "phase_79_candidate_queue_review_archive_summary" + assert data["phase"] == "phase_80_candidate_queue_review_ai_summary_preflight" assert data["payload_received"] is True assert data["payload_valid_json_object"] is True assert data["payload_persisted"] is False @@ -1910,7 +1910,7 @@ def test_manual_sample_candidate_handoff_preview_creates_candidates_without_pers ) assert handoff["mode"] == "manual_sample_candidate_handoff_preview" - assert handoff["phase"] == "phase_79_candidate_queue_review_archive_summary" + assert handoff["phase"] == "phase_80_candidate_queue_review_ai_summary_preflight" assert handoff["payload_received"] is True assert handoff["payload_valid_json_object"] is True assert handoff["payload_persisted"] is False @@ -1974,7 +1974,7 @@ def test_manual_sample_candidate_handoff_route_is_post_only_and_no_write(): assert response.status_code == 200 assert data["mode"] == "manual_sample_candidate_handoff_preview" - assert data["phase"] == "phase_79_candidate_queue_review_archive_summary" + assert data["phase"] == "phase_80_candidate_queue_review_ai_summary_preflight" assert data["payload_received"] is True assert data["handoff_ready"] is True assert data["candidate_handoff_created"] is True @@ -2033,7 +2033,7 @@ def test_manual_sample_candidate_queue_draft_preview_builds_review_items_without ) assert queue_draft["mode"] == "manual_sample_candidate_queue_draft_preview" - assert queue_draft["phase"] == "phase_79_candidate_queue_review_archive_summary" + assert queue_draft["phase"] == "phase_80_candidate_queue_review_ai_summary_preflight" assert queue_draft["payload_received"] is True assert queue_draft["payload_valid_json_object"] is True assert queue_draft["payload_persisted"] is False @@ -2107,7 +2107,7 @@ def test_manual_sample_candidate_queue_draft_route_is_post_only_and_no_write(): assert response.status_code == 200 assert data["mode"] == "manual_sample_candidate_queue_draft_preview" - assert data["phase"] == "phase_79_candidate_queue_review_archive_summary" + assert data["phase"] == "phase_80_candidate_queue_review_ai_summary_preflight" assert data["payload_received"] is True assert data["handoff_ready"] is True assert data["queue_draft_ready"] is True @@ -2170,7 +2170,7 @@ def test_manual_sample_candidate_queue_approval_preview_blocks_write_and_maps_ro ) assert approval["mode"] == "manual_sample_candidate_queue_approval_preview" - assert approval["phase"] == "phase_79_candidate_queue_review_archive_summary" + assert approval["phase"] == "phase_80_candidate_queue_review_ai_summary_preflight" assert approval["payload_received"] is True assert approval["payload_valid_json_object"] is True assert approval["payload_persisted"] is False @@ -2248,7 +2248,7 @@ def test_manual_sample_candidate_queue_approval_route_is_post_only_and_no_write( assert response.status_code == 200 assert data["mode"] == "manual_sample_candidate_queue_approval_preview" - assert data["phase"] == "phase_79_candidate_queue_review_archive_summary" + assert data["phase"] == "phase_80_candidate_queue_review_ai_summary_preflight" assert data["payload_received"] is True assert data["approval_preview_created"] is True assert data["approval_request_created"] is False @@ -2311,7 +2311,7 @@ def test_manual_sample_candidate_queue_transaction_preview_blocks_execution(): ) assert transaction["mode"] == "manual_sample_candidate_queue_transaction_preview" - assert transaction["phase"] == "phase_79_candidate_queue_review_archive_summary" + assert transaction["phase"] == "phase_80_candidate_queue_review_ai_summary_preflight" assert transaction["payload_received"] is True assert transaction["payload_valid_json_object"] is True assert transaction["payload_persisted"] is False @@ -2391,7 +2391,7 @@ def test_manual_sample_candidate_queue_transaction_route_is_post_only_and_no_wri assert response.status_code == 200 assert data["mode"] == "manual_sample_candidate_queue_transaction_preview" - assert data["phase"] == "phase_79_candidate_queue_review_archive_summary" + assert data["phase"] == "phase_80_candidate_queue_review_ai_summary_preflight" assert data["payload_received"] is True assert data["transaction_preview_created"] is True assert data["transaction_ready"] is False @@ -5314,7 +5314,7 @@ def test_candidate_queue_writer_preflight_route_is_post_only_and_no_write(): assert response.status_code == 200 assert data["mode"] == "candidate_queue_writer_preflight_planned" - assert data["phase"] == "phase_79_candidate_queue_review_archive_summary" + assert data["phase"] == "phase_80_candidate_queue_review_ai_summary_preflight" assert data["execute_requested"] is False assert data["read_only_query_executed"] is False assert data["database_connection_opened"] is False @@ -5371,7 +5371,7 @@ def test_candidate_queue_writer_status_route_never_leaks_approval_token(monkeypa assert response.status_code == 200 assert data["mode"] == "candidate_queue_writer_cli_blocked" - assert data["phase"] == "phase_79_candidate_queue_review_archive_summary" + assert data["phase"] == "phase_80_candidate_queue_review_ai_summary_preflight" assert data["execute_requested"] is True assert data["apply_real_write_requested"] is True assert data["approval_token_present"] is False @@ -5460,7 +5460,7 @@ def test_candidate_queue_writer_postwrite_smoke_route_is_post_only_and_no_write( assert response.status_code == 200 assert data["mode"] == "candidate_queue_writer_postwrite_smoke_planned" - assert data["phase"] == "phase_79_candidate_queue_review_archive_summary" + assert data["phase"] == "phase_80_candidate_queue_review_ai_summary_preflight" assert data["execute_requested"] is False assert data["read_only_query_executed"] is False assert data["database_connection_opened"] is False @@ -5514,7 +5514,7 @@ def test_candidate_queue_writer_operator_drill_route_is_post_only_and_no_write() assert response.status_code == 200 assert data["mode"] == "candidate_queue_writer_operator_drill_preview" - assert data["phase"] == "phase_79_candidate_queue_review_archive_summary" + assert data["phase"] == "phase_80_candidate_queue_review_ai_summary_preflight" assert data["operator_drill_ready"] is True assert data["api_executes_cli"] is False assert data["api_reads_approval_token"] is False @@ -5570,7 +5570,7 @@ def test_candidate_queue_writer_run_package_route_is_post_only_and_no_write(): assert response.status_code == 200 assert data["mode"] == "candidate_queue_writer_run_package_preview" - assert data["phase"] == "phase_79_candidate_queue_review_archive_summary" + assert data["phase"] == "phase_80_candidate_queue_review_ai_summary_preflight" assert data["package_ready"] is True assert data["package_artifact_created"] is False assert data["api_writes_file"] is False @@ -5636,7 +5636,7 @@ def test_candidate_queue_writer_run_readiness_route_is_post_only_and_no_write(): assert response.status_code == 200 assert data["mode"] == "candidate_queue_writer_run_readiness_preview" - assert data["phase"] == "phase_79_candidate_queue_review_archive_summary" + assert data["phase"] == "phase_80_candidate_queue_review_ai_summary_preflight" assert data["ready_for_cli_operator_run"] is True assert data["ready_for_api_database_write"] is False assert data["api_executes_cli"] is False @@ -5938,7 +5938,7 @@ def test_candidate_queue_writer_run_receipt_route_accepts_inline_payload_no_writ assert response.status_code == 200 assert data["mode"] == "candidate_queue_writer_run_receipt_preview" - assert data["phase"] == "phase_79_candidate_queue_review_archive_summary" + assert data["phase"] == "phase_80_candidate_queue_review_ai_summary_preflight" assert data["receipt_passed"] is True assert data["ready_for_api_database_write"] is False assert data["ready_for_scheduler_attach"] is False @@ -5986,7 +5986,7 @@ def test_candidate_queue_writer_run_closeout_route_is_post_only_and_no_write(): assert response.status_code == 200 assert data["mode"] == "candidate_queue_writer_run_closeout_preview" - assert data["phase"] == "phase_79_candidate_queue_review_archive_summary" + assert data["phase"] == "phase_80_candidate_queue_review_ai_summary_preflight" assert data["closeout_passed"] is True assert data["ready_for_next_manual_phase"] is True assert data["ready_for_api_database_write"] is False @@ -6035,7 +6035,7 @@ def test_candidate_queue_review_handoff_route_is_post_only_and_no_write(): assert response.status_code == 200 assert data["mode"] == "candidate_queue_review_handoff_preview" - assert data["phase"] == "phase_79_candidate_queue_review_archive_summary" + assert data["phase"] == "phase_80_candidate_queue_review_ai_summary_preflight" assert data["handoff_ready"] is True assert data["ready_for_manual_queue_review"] is True assert data["ready_for_api_database_write"] is False @@ -6093,7 +6093,7 @@ def test_candidate_queue_review_inventory_route_is_post_only_and_no_write(): assert get_response.status_code == 405 assert response.status_code == 200 assert data["mode"] == "candidate_queue_review_inventory_preview" - assert data["phase"] == "phase_79_candidate_queue_review_archive_summary" + assert data["phase"] == "phase_80_candidate_queue_review_ai_summary_preflight" assert data["execute_requested"] is False assert data["review_inventory_ready"] is False assert data["ready_for_human_decision_review"] is False @@ -6159,7 +6159,7 @@ def test_candidate_queue_review_decision_route_is_post_only_and_no_write(): assert get_response.status_code == 405 assert response.status_code == 200 assert data["mode"] == "candidate_queue_review_decision_preview" - assert data["phase"] == "phase_79_candidate_queue_review_archive_summary" + assert data["phase"] == "phase_80_candidate_queue_review_ai_summary_preflight" assert data["decision_ready"] is False assert data["ready_for_human_decision_record"] is False assert data["ready_for_api_review_state_update"] is False @@ -6230,7 +6230,7 @@ def test_candidate_queue_review_decision_approval_route_is_post_only_and_no_writ assert get_response.status_code == 405 assert response.status_code == 200 assert data["mode"] == "candidate_queue_review_decision_approval_preview" - assert data["phase"] == "phase_79_candidate_queue_review_archive_summary" + assert data["phase"] == "phase_80_candidate_queue_review_ai_summary_preflight" assert data["approval_ready"] is False assert data["ready_for_review_state_transaction_preview"] is False assert data["ready_for_cli_decision_writer"] is False @@ -6306,7 +6306,7 @@ def test_candidate_queue_review_decision_transaction_route_is_post_only_and_no_w assert get_response.status_code == 405 assert response.status_code == 200 assert data["mode"] == "candidate_queue_review_decision_transaction_preview" - assert data["phase"] == "phase_79_candidate_queue_review_archive_summary" + assert data["phase"] == "phase_80_candidate_queue_review_ai_summary_preflight" assert data["transaction_preview_created"] is False assert data["transaction_ready"] is False assert data["ready_for_manual_shell_update_window"] is False @@ -6388,7 +6388,7 @@ def test_candidate_queue_review_decision_writer_status_route_is_post_only_and_no assert get_response.status_code == 405 assert response.status_code == 200 assert data["mode"] == "candidate_queue_review_decision_writer_cli_blocked" - assert data["phase"] == "phase_79_candidate_queue_review_archive_summary" + assert data["phase"] == "phase_80_candidate_queue_review_ai_summary_preflight" assert data["execute_requested"] is True assert data["apply_real_write_requested"] is True assert data["approval_token_present"] is False @@ -6474,7 +6474,7 @@ def test_candidate_queue_review_decision_writer_preflight_route_is_post_only_and assert data["mode"] == ( "candidate_queue_review_decision_writer_preflight_preview" ) - assert data["phase"] == "phase_79_candidate_queue_review_archive_summary" + assert data["phase"] == "phase_80_candidate_queue_review_ai_summary_preflight" assert data["execute_requested"] is True assert data["apply_real_write_requested"] is True assert data["read_only_query_executed"] is False @@ -6557,7 +6557,7 @@ def test_candidate_queue_review_decision_writer_postwrite_smoke_route_is_post_on assert data["mode"] == ( "candidate_queue_review_decision_writer_postwrite_smoke_planned" ) - assert data["phase"] == "phase_79_candidate_queue_review_archive_summary" + assert data["phase"] == "phase_80_candidate_queue_review_ai_summary_preflight" assert data["execute_requested"] is False assert data["read_only_query_executed"] is False assert data["database_connection_opened"] is False @@ -6640,7 +6640,7 @@ def test_candidate_queue_review_decision_writer_operator_drill_route_is_post_onl assert data["mode"] == ( "candidate_queue_review_decision_writer_operator_drill_preview" ) - assert data["phase"] == "phase_79_candidate_queue_review_archive_summary" + assert data["phase"] == "phase_80_candidate_queue_review_ai_summary_preflight" assert data["operator_drill_ready"] is False assert data["ready_for_api_review_state_update"] is False assert data["ready_for_api_database_write"] is False @@ -6726,7 +6726,7 @@ def test_candidate_queue_review_decision_writer_run_package_route_is_post_only_a assert data["mode"] == ( "candidate_queue_review_decision_writer_run_package_preview" ) - assert data["phase"] == "phase_79_candidate_queue_review_archive_summary" + assert data["phase"] == "phase_80_candidate_queue_review_ai_summary_preflight" assert data["package_ready"] is False assert data["package_artifact_created"] is False assert data["ready_for_api_review_state_update"] is False @@ -6817,7 +6817,7 @@ def test_candidate_queue_review_decision_writer_run_readiness_route_is_post_only "candidate_queue_review_decision_writer_run_readiness_preview" ) assert data["phase"] == ( - "phase_79_candidate_queue_review_archive_summary" + "phase_80_candidate_queue_review_ai_summary_preflight" ) assert data["ready_for_cli_operator_run"] is False assert data["ready_for_api_review_state_update"] is False @@ -6927,7 +6927,7 @@ def test_candidate_queue_review_decision_writer_run_receipt_route_is_post_only_a "candidate_queue_review_decision_writer_run_receipt_preview" ) assert data["phase"] == ( - "phase_79_candidate_queue_review_archive_summary" + "phase_80_candidate_queue_review_ai_summary_preflight" ) assert data["receipt_passed"] is False assert data["ready_for_api_review_state_update"] is False @@ -7013,7 +7013,7 @@ def test_candidate_queue_review_decision_writer_run_closeout_route_is_post_only_ "candidate_queue_review_decision_writer_run_closeout_preview" ) assert data["phase"] == ( - "phase_79_candidate_queue_review_archive_summary" + "phase_80_candidate_queue_review_ai_summary_preflight" ) assert data["closeout_passed"] is False assert data["ready_for_api_review_state_update"] is False @@ -7070,7 +7070,7 @@ def test_candidate_queue_review_decision_post_closeout_inventory_route_is_post_o "candidate_queue_review_decision_post_closeout_inventory_preview" ) assert data["phase"] == ( - "phase_79_candidate_queue_review_archive_summary" + "phase_80_candidate_queue_review_ai_summary_preflight" ) assert data["post_closeout_inventory_ready"] is False assert data["ready_for_api_review_state_update"] is False @@ -7123,7 +7123,7 @@ def test_candidate_queue_review_completion_archive_route_is_post_only_and_no_wri assert get_response.status_code == 405 assert response.status_code == 200 assert data["mode"] == "candidate_queue_review_completion_archive_preview" - assert data["phase"] == "phase_79_candidate_queue_review_archive_summary" + assert data["phase"] == "phase_80_candidate_queue_review_ai_summary_preflight" assert data["review_completion_archive_ready"] is False assert data["archive_manifest_ready"] is False assert data["ready_for_api_review_state_update"] is False @@ -7176,7 +7176,7 @@ def test_candidate_queue_review_archive_summary_route_is_post_only_and_no_write( assert get_response.status_code == 405 assert response.status_code == 200 assert data["mode"] == "candidate_queue_review_archive_summary_preview" - assert data["phase"] == "phase_79_candidate_queue_review_archive_summary" + assert data["phase"] == "phase_80_candidate_queue_review_ai_summary_preflight" assert data["archive_summary_ready"] is False assert data["summary_input_ready"] is False assert data["ready_for_ai_summary_generation"] is False @@ -7237,7 +7237,7 @@ def test_candidate_queue_writer_run_receipt_route_is_post_only_and_no_write(): assert response.status_code == 200 assert data["mode"] == "candidate_queue_writer_run_receipt_preview" - assert data["phase"] == "phase_79_candidate_queue_review_archive_summary" + assert data["phase"] == "phase_80_candidate_queue_review_ai_summary_preflight" assert data["receipt_passed"] is True assert data["ready_for_next_manual_review"] is True assert data["ready_for_api_database_write"] is False @@ -7262,7 +7262,7 @@ def test_scheduler_plan_preview_blocks_job_attachment(): plan = MarketIntelService().build_scheduler_plan() assert plan["mode"] == "scheduler_attach_plan_preview" - assert plan["phase"] == "phase_79_candidate_queue_review_archive_summary" + assert plan["phase"] == "phase_80_candidate_queue_review_ai_summary_preflight" assert plan["ready_to_attach_scheduler"] is False assert plan["scheduler_attached"] is False assert plan["scheduler_registration_executed"] is False @@ -7300,7 +7300,7 @@ def test_scheduler_plan_route_is_preview_only(): assert response.status_code == 200 assert data["mode"] == "scheduler_attach_plan_preview" - assert data["phase"] == "phase_79_candidate_queue_review_archive_summary" + assert data["phase"] == "phase_80_candidate_queue_review_ai_summary_preflight" assert data["scheduler_registration_executed"] is False assert data["crawler_job_started"] is False assert data["external_network_executed"] is False @@ -7311,7 +7311,7 @@ def test_match_review_plan_preview_blocks_auto_confirm(): plan = MarketIntelService().build_match_review_plan() assert plan["mode"] == "match_review_plan_preview" - assert plan["phase"] == "phase_79_candidate_queue_review_archive_summary" + assert plan["phase"] == "phase_80_candidate_queue_review_ai_summary_preflight" assert plan["ready_for_review_queue"] is False assert plan["review_queue_created"] is False assert plan["auto_match_executed"] is False @@ -7347,7 +7347,7 @@ def test_match_review_plan_route_is_preview_only(): assert response.status_code == 200 assert data["mode"] == "match_review_plan_preview" - assert data["phase"] == "phase_79_candidate_queue_review_archive_summary" + assert data["phase"] == "phase_80_candidate_queue_review_ai_summary_preflight" assert data["review_queue_created"] is False assert data["auto_confirm_executed"] is False assert data["external_network_executed"] is False @@ -7358,7 +7358,7 @@ def test_opportunity_plan_preview_blocks_alerts_and_ai_summary(): plan = MarketIntelService().build_opportunity_plan() assert plan["mode"] == "opportunity_plan_preview" - assert plan["phase"] == "phase_79_candidate_queue_review_archive_summary" + assert plan["phase"] == "phase_80_candidate_queue_review_ai_summary_preflight" assert plan["ready_for_opportunity_queue"] is False assert plan["opportunity_queue_created"] is False assert plan["threat_alert_dispatched"] is False @@ -7399,7 +7399,7 @@ def test_opportunity_plan_route_is_preview_only(): assert response.status_code == 200 assert data["mode"] == "opportunity_plan_preview" - assert data["phase"] == "phase_79_candidate_queue_review_archive_summary" + assert data["phase"] == "phase_80_candidate_queue_review_ai_summary_preflight" assert data["opportunity_queue_created"] is False assert data["threat_alert_dispatched"] is False assert data["ai_summary_generated"] is False @@ -7410,7 +7410,7 @@ def test_opportunity_scoring_plan_preview_blocks_scoring_and_alerts(): plan = MarketIntelService().build_opportunity_scoring_plan() assert plan["mode"] == "opportunity_scoring_plan_preview" - assert plan["phase"] == "phase_79_candidate_queue_review_archive_summary" + assert plan["phase"] == "phase_80_candidate_queue_review_ai_summary_preflight" assert plan["ready_for_scoring_job"] is False assert plan["scoring_job_created"] is False assert plan["score_calculation_executed"] is False @@ -7458,7 +7458,7 @@ def test_opportunity_scoring_plan_route_is_preview_only(): assert response.status_code == 200 assert data["mode"] == "opportunity_scoring_plan_preview" - assert data["phase"] == "phase_79_candidate_queue_review_archive_summary" + assert data["phase"] == "phase_80_candidate_queue_review_ai_summary_preflight" assert data["scoring_job_created"] is False assert data["score_calculation_executed"] is False assert data["sample_scores_generated"] is False @@ -7470,7 +7470,7 @@ def test_opportunity_evidence_plan_preview_blocks_queries_and_alerts(): plan = MarketIntelService().build_opportunity_evidence_plan() assert plan["mode"] == "opportunity_evidence_plan_preview" - assert plan["phase"] == "phase_79_candidate_queue_review_archive_summary" + assert plan["phase"] == "phase_80_candidate_queue_review_ai_summary_preflight" assert plan["ready_for_evidence_bundle"] is False assert plan["evidence_bundle_created"] is False assert plan["evidence_query_executed"] is False @@ -7516,7 +7516,7 @@ def test_opportunity_evidence_plan_route_is_preview_only(): assert response.status_code == 200 assert data["mode"] == "opportunity_evidence_plan_preview" - assert data["phase"] == "phase_79_candidate_queue_review_archive_summary" + assert data["phase"] == "phase_80_candidate_queue_review_ai_summary_preflight" assert data["evidence_bundle_created"] is False assert data["evidence_query_executed"] is False assert data["sample_evidence_generated"] is False @@ -7529,7 +7529,7 @@ def test_opportunity_alert_plan_preview_blocks_dispatch_and_llm_calls(): plan = MarketIntelService().build_opportunity_alert_plan() assert plan["mode"] == "opportunity_alert_plan_preview" - assert plan["phase"] == "phase_79_candidate_queue_review_archive_summary" + assert plan["phase"] == "phase_80_candidate_queue_review_ai_summary_preflight" assert plan["ready_for_alert_candidates"] is False assert plan["alert_candidate_created"] is False assert plan["alert_queue_created"] is False @@ -7614,7 +7614,7 @@ def test_opportunity_alert_plan_route_is_preview_only(): assert response.status_code == 200 assert data["mode"] == "opportunity_alert_plan_preview" - assert data["phase"] == "phase_79_candidate_queue_review_archive_summary" + assert data["phase"] == "phase_80_candidate_queue_review_ai_summary_preflight" assert data["alert_candidate_created"] is False assert data["alert_queue_created"] is False assert data["review_queue_created"] is False @@ -7692,7 +7692,7 @@ def test_mcp_deploy_preflight_route_is_preview_only(): assert response.status_code == 200 assert data["mode"] == "mcp_external_deploy_preflight_preview" - assert data["phase"] == "phase_79_candidate_queue_review_archive_summary" + assert data["phase"] == "phase_80_candidate_queue_review_ai_summary_preflight" assert data["deployment_actions_executed"] is False assert data["docker_command_executed"] is False assert data["ssh_command_executed"] is False @@ -7707,7 +7707,7 @@ def test_mcp_readiness_default_is_planned_only(monkeypatch): readiness = MarketIntelService().build_mcp_readiness() assert readiness["mode"] == "mcp_readiness_planned" - assert readiness["phase"] == "phase_79_candidate_queue_review_archive_summary" + assert readiness["phase"] == "phase_80_candidate_queue_review_ai_summary_preflight" assert readiness["execute_requested"] is False assert readiness["router_enabled"] is False assert readiness["external_mcp_complete"] is False @@ -9878,7 +9878,7 @@ def test_migration_apply_drill_planned_is_safe_and_manual_only(): drill = MarketIntelService().build_migration_apply_drill() assert drill["mode"] == "migration_apply_drill_preview" - assert drill["phase"] == "phase_79_candidate_queue_review_archive_summary" + assert drill["phase"] == "phase_80_candidate_queue_review_ai_summary_preflight" assert drill["execute_requested"] is False assert drill["schema_state"] == "planned_no_db_probe" assert drill["drill_ready_for_operator_review"] is True @@ -9993,7 +9993,7 @@ def test_migration_apply_drill_route_is_preview_only(): assert response.status_code == 200 assert data["mode"] == "migration_apply_drill_preview" - assert data["phase"] == "phase_79_candidate_queue_review_archive_summary" + assert data["phase"] == "phase_80_candidate_queue_review_ai_summary_preflight" assert data["execute_requested"] is False assert data["migration_executed"] is False assert data["rollback_executed"] is False @@ -10005,7 +10005,7 @@ def test_migration_catalog_review_planned_is_safe_and_diagnostic(): review = MarketIntelService().build_migration_catalog_review() assert review["mode"] == "migration_catalog_review_preview" - assert review["phase"] == "phase_79_candidate_queue_review_archive_summary" + assert review["phase"] == "phase_80_candidate_queue_review_ai_summary_preflight" assert review["execute_requested"] is False assert review["catalog_state"] == "planned_no_probe" assert review["seed_state"] == "planned_no_probe" @@ -10120,7 +10120,7 @@ def test_migration_catalog_review_route_is_preview_only(): assert response.status_code == 200 assert data["mode"] == "migration_catalog_review_preview" - assert data["phase"] == "phase_79_candidate_queue_review_archive_summary" + assert data["phase"] == "phase_80_candidate_queue_review_ai_summary_preflight" assert data["execute_requested"] is False assert data["catalog_state"] == "planned_no_probe" assert data["migration_executed"] is False @@ -10133,7 +10133,7 @@ def test_migration_live_smoke_planned_is_preview_only(): smoke = MarketIntelService().build_migration_live_smoke() assert smoke["mode"] == "migration_live_smoke_preview" - assert smoke["phase"] == "phase_79_candidate_queue_review_archive_summary" + assert smoke["phase"] == "phase_80_candidate_queue_review_ai_summary_preflight" assert smoke["execute_requested"] is False assert smoke["smoke_result"] == "planned_no_execution" assert smoke["live_smoke_passed"] is False @@ -10195,7 +10195,7 @@ def test_migration_live_smoke_route_is_preview_only(): assert response.status_code == 200 assert data["mode"] == "migration_live_smoke_preview" - assert data["phase"] == "phase_79_candidate_queue_review_archive_summary" + assert data["phase"] == "phase_80_candidate_queue_review_ai_summary_preflight" assert data["execute_requested"] is False assert data["smoke_result"] == "planned_no_execution" assert data["migration_executed"] is False @@ -10208,7 +10208,7 @@ def test_live_db_inventory_planned_is_preview_only(): inventory = MarketIntelService().build_live_db_inventory() assert inventory["mode"] == "live_db_inventory_planned" - assert inventory["phase"] == "phase_79_candidate_queue_review_archive_summary" + assert inventory["phase"] == "phase_80_candidate_queue_review_ai_summary_preflight" assert inventory["execute_requested"] is False assert inventory["read_only_query_executed"] is False assert inventory["database_connection_opened"] is False @@ -10352,7 +10352,7 @@ def test_live_db_inventory_route_is_preview_only(): assert response.status_code == 200 assert data["mode"] == "live_db_inventory_planned" - assert data["phase"] == "phase_79_candidate_queue_review_archive_summary" + assert data["phase"] == "phase_80_candidate_queue_review_ai_summary_preflight" assert data["execute_requested"] is False assert data["read_only_query_executed"] is False assert data["database_write_executed"] is False @@ -10579,7 +10579,7 @@ def test_candidate_queue_writer_cli_script_outputs_blocked_gate(tmp_path): assert result.returncode == 0 assert data["mode"] == "candidate_queue_writer_cli_blocked" - assert data["phase"] == "phase_79_candidate_queue_review_archive_summary" + assert data["phase"] == "phase_80_candidate_queue_review_ai_summary_preflight" assert data["execute_requested"] is False assert data["apply_real_write_requested"] is False assert data["writes_executed"] is False @@ -10608,7 +10608,7 @@ def test_review_decision_writer_cli_script_outputs_blocked_gate_without_login_en assert result.returncode == 0 assert data["mode"] == "candidate_queue_review_decision_writer_cli_blocked" - assert data["phase"] == "phase_79_candidate_queue_review_archive_summary" + assert data["phase"] == "phase_80_candidate_queue_review_ai_summary_preflight" assert data["execute_requested"] is False assert data["apply_real_write_requested"] is False assert data["approval_token_present"] is False