diff --git a/TODO_NEXT_STEPS.txt b/TODO_NEXT_STEPS.txt index 9e32802..af36268 100644 --- a/TODO_NEXT_STEPS.txt +++ b/TODO_NEXT_STEPS.txt @@ -6,6 +6,7 @@ 【已完成】 - V10.565 補 PChome 覆蓋率操作建議:`/api/ai/pchome-match/backfill/status` 會把低覆蓋率拆成 `operation_backlog`,分別列出刷新舊 identity、重評近門檻、補抓未配對、人工覆核、單位價覆核與過期搜尋救援預覽;同時回傳 `recommended_next_action`,Dashboard 狀態摘要會顯示「建議執行比價補強 / 刷新過期 identity / 處理覆核」等下一步,讓覆蓋率 KPI 直接連到可執行行動。 - V10.563 收斂正式 preview 假可救候選:M.A.C 超持妝輕透濾鏡蜜粉若只有 PChome 端出現明確色號(例如 `#絕絕紫`),會標成 `variant_selection_review` 並維持 `true_low_confidence`,不再佔 recoverable 池;SAUGELLA 賽吉兒菁萃潔浴凝露新增潤澤 / 日用型 / 加強 / 黃金女郎型變體互斥,避免同品線不同私密清潔款式被誤救成 matched。 + - V10.566 新增市場情報 Professional Source Governance:把 robots/REP、sitemap/lastmod、JSON-LD / schema.org structured data、canonical URL、rate limit、公開資料邊界、provenance、snapshot hash 與 idempotency key 變成可審核 source contract。新增 `/api/market_intel/mcp_professional_source_governance` 與市場情報頁卡片、deployment readiness smoke target;API/UI 只審核操作員貼回的治理摘要,不抓外站、不讀 robots/sitemap、不開 DB、不寫檔、不掛 scheduler,後續 fetch target review 才能引用通過治理的來源。 - V10.561 補 PChome 比價補強前端分段回饋:Dashboard 的 PChome 卡片從「補抓產線」改為「比價補強產線」,按鈕與確認文案同步說明會先刷新舊 identity、再重評近門檻與補抓未配對;結果區新增刷新 / 重評 / 補抓三段 matched/total 摘要,避免後端已完成分段統計但操作員仍只看到一個籠統成功數。 - V10.560 串起手動 PChome 比價補強三段式流程:`/api/ai/pchome-match/backfill` 現在不只跑近門檻重評與未配對補抓,也會先用小批次 `run_expired_identity_refresh()` 刷新已知 `identity_v2` 舊價格,讓操作員按一次補強就能同時處理「舊 identity 新鮮度」、「near-threshold low_score」與「pending identity」三條主線。結果 payload 新增 `stale_identity_refresh` 分段統計,方便後續 Dashboard / 簡報 / AI 決策知道覆蓋率改善是來自刷新、重評或補抓。 - V10.559 收斂 retryable 有效身份新鮮度:`_fetch_retryable_candidate_skus()` 不再把 `expires_at IS NULL` 的舊 PChome `identity_v2` 當成有效阻擋條件,只有明確 `expires_at > CURRENT_TIMESTAMP` 的新鮮 identity 才會阻止 near-threshold revalidation。未知新鮮度仍走 V10.551 的 expired / recovery 刷新入口,重評後仍必須通過最新版 matcher、hard-veto、auto write safety 與既有正式候選覆寫保護,避免為了拉覆蓋率犧牲準確率。 diff --git a/config.py b/config.py index 1c9a849..8b35fce 100644 --- a/config.py +++ b/config.py @@ -402,7 +402,7 @@ YOUTUBE_API_KEY = os.getenv('YOUTUBE_API_KEY', '') # ========================================== # 系統版本與路徑 # ========================================== -SYSTEM_VERSION = "V10.565" +SYSTEM_VERSION = "V10.566" 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 70842e3..2af64de 100644 --- a/docs/adr/ADR-035-cross-platform-market-campaign-intelligence.md +++ b/docs/adr/ADR-035-cross-platform-market-campaign-intelligence.md @@ -178,6 +178,7 @@ EwoooC 目前已有 MOMO EDM / 節慶活動資料、`promo_products`、PChome - 2026-05-31 追加 MCP fetch candidate queue writer review decision gate:`services.market_intel.mcp_fetch_candidate_queue_writer_review_decision`、`services.market_intel.mcp_fetch_candidate_queue_writer_review_decision_gates`、`services.market_intel.mcp_fetch_candidate_queue_writer_review_decision_sample` 與 `/api/market_intel/mcp_fetch_candidate_queue_writer_review_decision` 在 review inventory 通過後審核 operator candidate queue review decision 摘要,檢查 decision identity、target table、row count、dedupe keys、`needs_review` 現態、允許決策集合、evidence refs、matched row exact-identity/variant/overwrite guard、operator confirmations 與 forbidden API actions;API/UI 不讀 approval token、不執行 CLI、不開 DB、不寫 decision record、不更新 review_state、不寫 match result、不補 queue、不掛 scheduler,只放行到 decision approval / writer preflight 設計。 - 2026-05-31 追加 MCP fetch candidate queue writer review decision approval gate:`services.market_intel.mcp_fetch_candidate_queue_writer_review_decision_approval`、`services.market_intel.mcp_fetch_candidate_queue_writer_review_decision_approval_gates`、`services.market_intel.mcp_fetch_candidate_queue_writer_review_decision_approval_sample` 與 `/api/market_intel/mcp_fetch_candidate_queue_writer_review_decision_approval` 在 review decision 通過後只審核 operator human approval 摘要,確認 decision linkage、approval identity、target table、row count、dedupe keys、`approved_for_writer_preflight` approval result、decision/approval evidence refs、artifact paths、matched row exact-identity/variant/overwrite guard、operator confirmations 與 forbidden API actions;API/UI 不讀 approval token、不執行 CLI、不開 DB、不寫 approval record、不寫 decision record、不更新 review_state、不寫 match result、不補 queue、不掛 scheduler,只放行到後續 writer preflight 設計。此 endpoint 已拆入 `routes.market_intel_mcp_review_routes`,避免 `routes.market_intel_mcp_run_routes` 超過 800 行治理門檻。 - 2026-05-31 追加 MCP fetch candidate queue writer review decision approval writer preflight gate:`services.market_intel.mcp_fetch_candidate_queue_writer_review_decision_approval_writer_preflight`、對應 gates/sample 與 `/api/market_intel/mcp_fetch_candidate_queue_writer_review_decision_approval_writer_preflight` 在 human approval 通過後只審核 operator writer preflight 摘要,確認 approval linkage、writer_preflight_id、target operation、row count、dedupe keys、approved decision 到 target review_state 的逐列映射、decision/approval/preflight evidence refs、matched row exact-identity/variant/overwrite guard 與 operator boundary;API/UI 不讀 approval token、不執行 CLI、不開 DB、不寫 preflight/approval/decision/match、不更新 review_state、不補 queue、不掛 scheduler,只放行到後續 CLI review / run package 設計。 +- 2026-06-01 追加 Professional Source Governance gate:`services.market_intel.mcp_professional_source_governance`、對應 gates/sample 與 `/api/market_intel/mcp_professional_source_governance` 將 robots/REP、sitemap/lastmod、JSON-LD / schema.org structured data、canonical URL、rate limit、公開資料邊界、provenance、snapshot hash 與 idempotency key 整理為 source contract。此 gate 只審核 operator source governance 摘要,不抓外站、不讀 robots/sitemap、不開 DB、不寫檔、不掛 scheduler;後續 fetch target review 才能引用通過治理的公開來源。 - 2026-05-18 追加 scheduler attach plan preview:`services.market_intel.scheduler_plan` 與 `/api/market_intel/scheduler_plan` 描述未來 `campaign_discovery_daily`、`campaign_product_probe`、`product_match_review_seed` 三個 job 的 cadence、gate、fallback 與安全邊界。此階段不註冊 scheduler job、不啟動 crawler、不連外、不寫 DB;排程掛載必須等 migration、seed、MCP fetch gate、manual sample 與人工批准全過。 - 2026-05-18 追加 match review plan preview:`services.market_intel.match_review_plan` 與 `/api/market_intel/match_review_plan` 定義商品比對訊號、分數門檻、`needs_review → confirmed/rejected` HITL 流程與安全邊界。此階段不建立 review queue、不自動 confirmed、不寫 `market_product_matches`、不呼叫 MCP;價格只能作為輔助訊號,不能單獨決定同品比對。 - 2026-05-18 追加 opportunity plan preview:`services.market_intel.opportunity_plan` 與 `/api/market_intel/opportunity_plan` 定義競品低價威脅、促銷缺口、深折重疊、活動即將結束四類規則與分級策略。此階段不建立 opportunity queue、不派送 Telegram、不產生 AI 摘要、不寫 DB;高風險項必須先有 confirmed match 與 DB evidence 才能升級。 diff --git a/docs/memory/code_modularization_inventory_20260430.md b/docs/memory/code_modularization_inventory_20260430.md index f41202d..4953b5b 100644 --- a/docs/memory/code_modularization_inventory_20260430.md +++ b/docs/memory/code_modularization_inventory_20260430.md @@ -55,6 +55,7 @@ - 2026-05-31 追記:同步市場情報 MCP fetch candidate queue writer review inventory gate 後的 `services/market_intel/deployment_readiness.py` 行數;本次新增 `services/market_intel/mcp_fetch_candidate_queue_writer_review_inventory.py`(462 行)、`services/market_intel/mcp_fetch_candidate_queue_writer_review_inventory_gates.py`(183 行)與 `services/market_intel/mcp_fetch_candidate_queue_writer_review_inventory_sample.py`(107 行),全部低於 600 行提醒門檻;`routes/market_intel_mcp_run_routes.py` 目前 717 行,仍低於 800 行但後續新增 MCP gate 應持續評估拆第二個 route extension。 - 2026-05-31 追記:同步市場情報 MCP fetch candidate queue writer review decision gate 後的 `services/market_intel/deployment_readiness.py` 行數;本次新增 `services/market_intel/mcp_fetch_candidate_queue_writer_review_decision.py`(498 行)、`services/market_intel/mcp_fetch_candidate_queue_writer_review_decision_gates.py`(241 行)與 `services/market_intel/mcp_fetch_candidate_queue_writer_review_decision_sample.py`(118 行),全部低於 600 行提醒門檻;`routes/market_intel_mcp_run_routes.py` 目前 772 行,仍低於 800 行但已接近門檻,下一段 MCP route 應優先拆第二個 route extension。 - 2026-05-31 追記:同步市場情報 MCP fetch candidate queue writer review decision approval gate 後的 `services/market_intel/deployment_readiness.py` 行數;本次新增 `services/market_intel/mcp_fetch_candidate_queue_writer_review_decision_approval.py`(560 行)、`services/market_intel/mcp_fetch_candidate_queue_writer_review_decision_approval_gates.py`(255 行)、`services/market_intel/mcp_fetch_candidate_queue_writer_review_decision_approval_sample.py`(140 行)與 `routes/market_intel_mcp_review_routes.py`(64 行),全部低於 600 行提醒門檻;`routes/market_intel_mcp_run_routes.py` 維持 770 行,本次未再加 endpoint,改以第二個 MCP review route extension 承接。 +- 2026-06-01 追記:同步市場情報 Professional Source Governance gate 後的 `services/market_intel/deployment_readiness.py` 行數;本次新增 `services/market_intel/mcp_professional_source_governance.py`(391 行)、`services/market_intel/mcp_professional_source_governance_gates.py`(266 行)、`services/market_intel/mcp_professional_source_governance_sample.py`(175 行)與 `routes/market_intel_mcp_review_routes.py`(165 行),全部低於 600 行提醒門檻;`services/market_intel/deployment_readiness.py` 仍是既有 P2 大檔,只加 preview-safe check 與 smoke target,後續需延續小 service + route extension 模式。 - 2026-05-24 追記:同步背景 Code Review 111 fallback 保護合併後的 `services/code_review_pipeline_service.py` 行數;此處只更新 inventory,不變更 Code Review 行為。 - 2026-05-21 追記:同步 PChome/LUDEYA 商品線名稱漂移比對更新後的 `services/marketplace_product_matcher.py` 行數;此處只更新 inventory,不變更模組化決策。 - 2026-05-21 追記:同步 MAC/Yuskin/AHC 名稱漂移與 bundle equivalent matcher 更新後的 `services/marketplace_product_matcher.py` 行數;此處只更新 inventory,不變更模組化決策。 @@ -107,7 +108,7 @@ | 805 | `routes/bot_api_routes.py` | P2 Bot API Blueprint | route glue / bot action service | | 1319 | `routes/market_intel_review_report_routes.py` | P2 market intel review report Blueprint | review report route glue / export payload / phase handoff orchestration | | 917 | `routes/market_intel_routes.py` | P2 market intel Blueprint | page route / API route glue / MCP gate route registration helper | -| 1914 | `services/market_intel/deployment_readiness.py` | P2 market intel deployment readiness | preflight gates / readiness payload / route contract helpers | +| 1965 | `services/market_intel/deployment_readiness.py` | P2 market intel deployment readiness | preflight gates / readiness payload / route contract helpers | | 846 | `services/market_intel/candidate_queue_review_ai_summary_persistence_telegram_dispatch_report_catalog_record_run_receipt.py` | P2 market intel review receipt pipeline | AI summary / persistence / Telegram dispatch / report catalog run receipt orchestration | ## 市場情報開發前置禁區 diff --git a/docs/memory/current_execution_queue_20260524.md b/docs/memory/current_execution_queue_20260524.md index 9a3e3db..8100ed9 100644 --- a/docs/memory/current_execution_queue_20260524.md +++ b/docs/memory/current_execution_queue_20260524.md @@ -103,6 +103,7 @@ - 2026-05-31 起,`V10.505` 新增市場情報 MCP Fetch Candidate Queue Writer Review Decision gate:在 review inventory 通過後只審核 operator candidate queue review decision 摘要,要求 decision identity、target table、row count、dedupe keys、`needs_review` 現態、允許決策、evidence refs、matched row exact-identity/variant/overwrite guard 與 operator confirmation 對齊;仍不讀 token、不執行 CLI、不開 DB、不寫 decision record、不更新 review_state、不寫 match result、不補 queue、不掛 scheduler,只放行到 decision approval / writer preflight 設計。 - 2026-05-31 起,`V10.506` 新增市場情報 MCP Fetch Candidate Queue Writer Review Decision Approval gate:在 review decision 通過後只審核 operator human approval 摘要,要求 decision linkage、approval identity、target table、row count、dedupe keys、`approved_for_writer_preflight` approval result、decision/approval evidence refs、artifact paths、matched row exact-identity/variant/overwrite guard 與 operator confirmation 對齊;仍不讀 token、不執行 CLI、不開 DB、不寫 approval record、不寫 decision record、不更新 review_state、不寫 match result、不補 queue、不掛 scheduler,只放行到後續 writer preflight 設計。 - 2026-05-31 起,`V10.509` 新增市場情報 MCP Fetch Candidate Queue Writer Review Decision Approval Writer Preflight gate:在 human approval 通過後只審核 operator writer preflight 摘要,要求 approval linkage、writer_preflight_id、target operation、row count、dedupe keys、approved decision 到 target review_state 的逐列映射、decision/approval/preflight evidence refs、matched row exact-identity/variant/overwrite guard 與 operator boundary;仍不讀 token、不執行 CLI、不開 DB、不寫 preflight/approval/decision/match、不更新 review_state、不補 queue、不掛 scheduler,只放行到後續 CLI review / run package 設計。 +- 2026-06-01 起,`V10.566` 新增市場情報 Professional Source Governance gate:將 robots/REP、sitemap/lastmod、JSON-LD / schema.org structured data、canonical URL、rate limit、公開資料邊界、provenance、snapshot hash 與 idempotency key 納入 source contract,並接上 `/api/market_intel/mcp_professional_source_governance`、UI preview panel、deployment readiness check 與 production smoke target;仍不抓外站、不讀 robots/sitemap、不開 DB、不寫檔、不掛 scheduler。 ## 3. 12 Agent 決策信封整合 diff --git a/docs/memory/history_logs.md b/docs/memory/history_logs.md index d189102..e4e0cd9 100644 --- a/docs/memory/history_logs.md +++ b/docs/memory/history_logs.md @@ -15,6 +15,7 @@ ### 2026-06-01:PChome 比價新鮮度操作閉環 - **V10.565 PChome 覆蓋率操作建議**: 補強 `/api/ai/pchome-match/backfill/status`,將低覆蓋率拆成 `operation_backlog`:刷新舊 identity、重評近門檻、補抓未配對、人工覆核、單位價覆核與過期搜尋救援預覽;並新增 `recommended_next_action`,Dashboard 狀態摘要會直接顯示建議下一步,避免使用者只看到低覆蓋率卻不知道該按哪條產線。 - **V10.563 正式 preview 假可救候選收斂**: 針對正式 `retryable_candidate_preview` 露出的 M.A.C 蜜粉與 SAUGELLA 菁萃潔浴凝露案例補 guard。M.A.C 單邊明確色號(如 `#絕絕紫`)會進 `variant_selection_review`,維持 `true_low_confidence`;SAUGELLA 潤澤 / 日用型 / 加強 / 黃金女郎型互斥,直接 hard veto,避免同品線不同私密清潔款式被當成 recoverable low_score。 +- **V10.566 市場情報 Professional Source Governance**: 新增 `/api/market_intel/mcp_professional_source_governance`、preview service/gates/sample 與市場情報頁卡片,將 robots/REP、sitemap/lastmod、JSON-LD / schema.org structured data、canonical URL、rate limit、公開資料邊界、provenance、snapshot hash 與 idempotency key 納入 source contract。此 gate 只審核操作員治理摘要,不抓外站、不讀 robots/sitemap、不開 DB、不寫檔、不掛 scheduler;deployment readiness 同步新增 preview-safe 檢查與 production smoke target。 - **V10.561 PChome 比價補強前端分段回饋**: Dashboard 的 PChome 操作卡改名為「比價補強產線」,手動按鈕與確認文案同步說明三段流程;結果摘要會顯示刷新、重評、補抓各自的 matched/total,讓操作員能判斷覆蓋率改善來自舊 identity 新鮮度回補、近門檻 matcher 回刷,或 pending 商品 fresh search 補抓。 - **V10.560 手動 PChome 比價補強三段式串接**: `/api/ai/pchome-match/backfill` 與每日 scheduler 口徑對齊,手動執行時先小批次刷新過期 `identity_v2`,再跑近門檻候選重評,最後補抓高優先未配對商品。回傳結果新增 `stale_identity_refresh` 分段統計,讓後續 Dashboard、簡報與 AI 決策能區分覆蓋率改善來自舊 identity 新鮮度回補、matcher 回刷,還是 fresh search 補抓。 - **V10.559 retryable 有效身份新鮮度收斂**: `_fetch_retryable_candidate_skus()` 的既有 identity 阻擋條件改成只接受 `cp.expires_at > CURRENT_TIMESTAMP`,不再讓 `expires_at IS NULL` 的未知新鮮度舊配對壓住近門檻候選回刷。未知新鮮度仍由 expired identity refresh / recovery 路徑處理,最後寫入仍必須通過現行 matcher、hard-veto、auto write safety 與 stronger existing production match 保護。 diff --git a/routes/market_intel_mcp_review_routes.py b/routes/market_intel_mcp_review_routes.py index 83860cb..ce8eac1 100644 --- a/routes/market_intel_mcp_review_routes.py +++ b/routes/market_intel_mcp_review_routes.py @@ -13,6 +13,9 @@ from services.market_intel.mcp_fetch_candidate_queue_writer_review_decision_appr from services.market_intel.mcp_fetch_candidate_queue_writer_review_decision_approval_writer_preflight import ( build_mcp_fetch_candidate_queue_writer_review_decision_approval_writer_preflight_preview, ) +from services.market_intel.mcp_professional_source_governance import ( + build_mcp_professional_source_governance_preview, +) @market_intel_bp.route( @@ -129,3 +132,34 @@ def market_intel_mcp_fetch_candidate_queue_writer_review_decision_approval_write phase=service.phase, ) ) + + +@market_intel_bp.route( + "/api/market_intel/mcp_professional_source_governance", + methods=["GET", "POST"], +) +@login_required +def market_intel_mcp_professional_source_governance(): + operator_source_governance = None + if request.method == "POST": + payload = request.get_json(silent=True) or {} + package = ( + payload.get("professional_source_governance_package") + or payload.get("source_governance_package") + or payload.get("operator_source_governance") + or payload.get("market_source_governance") + or payload + ) + operator_source_governance = ( + package.get("operator_source_governance") + or package.get("source_governance") + or package + ) + + service = MarketIntelService() + return jsonify( + build_mcp_professional_source_governance_preview( + operator_source_governance=operator_source_governance, + phase=service.phase, + ) + ) diff --git a/services/market_intel/deployment_readiness.py b/services/market_intel/deployment_readiness.py index 2502197..5862bcf 100644 --- a/services/market_intel/deployment_readiness.py +++ b/services/market_intel/deployment_readiness.py @@ -117,6 +117,9 @@ from services.market_intel.mcp_fetch_candidate_queue_writer_review_decision_appr from services.market_intel.mcp_fetch_candidate_queue_writer_review_decision_approval_writer_preflight import ( build_mcp_fetch_candidate_queue_writer_review_decision_approval_writer_preflight_preview, ) +from services.market_intel.mcp_professional_source_governance import ( + build_mcp_professional_source_governance_preview, +) from services.market_intel.mcp_manual_fetch_handoff import ( build_mcp_manual_fetch_handoff_preview, ) @@ -313,6 +316,11 @@ PRODUCTION_SMOKE_TARGETS = ( ) + PRODUCTION_SMOKE_TARGETS[-1:] ) +PRODUCTION_SMOKE_TARGETS = ( + PRODUCTION_SMOKE_TARGETS[:-1] + + ("/api/market_intel/mcp_professional_source_governance",) + + PRODUCTION_SMOKE_TARGETS[-1:] +) def _run_review_preview_safe(payload, mode): @@ -426,6 +434,11 @@ def build_deployment_readiness_preview(*, service, market_intel_tables, schema_s phase=service.phase, ) ) + mcp_professional_source_governance = ( + build_mcp_professional_source_governance_preview( + phase=service.phase, + ) + ) scheduler_plan = service.build_scheduler_plan() manual_sample_plan = service.build_manual_sample_plan() manual_sample_acceptance = service.build_manual_sample_acceptance() @@ -1526,6 +1539,23 @@ def build_deployment_readiness_preview(*, service, market_intel_tables, schema_s mcp_fetch_candidate_queue_writer_review_decision_approval_writer_preflight, "mcp_fetch_candidate_queue_writer_review_decision_approval_writer_preflight_preview", ), + "mcp_professional_source_governance_preview_safe": bool( + mcp_professional_source_governance["mode"] + == "mcp_professional_source_governance_preview" + and not mcp_professional_source_governance["network_request_allowed"] + and not mcp_professional_source_governance["external_network_executed"] + and not mcp_professional_source_governance["api_uses_external_network"] + and not mcp_professional_source_governance["api_fetches_robots_txt"] + and not mcp_professional_source_governance["api_fetches_sitemap"] + and not mcp_professional_source_governance["api_fetches_source_url"] + and not mcp_professional_source_governance[ + "api_opens_database_connection" + ] + and not mcp_professional_source_governance["api_writes_database"] + and not mcp_professional_source_governance["api_writes_file"] + and not mcp_professional_source_governance["payload_persisted"] + and not mcp_professional_source_governance["scheduler_attached"] + ), "candidate_queue_writer_postwrite_smoke_planned_safe": bool( candidate_queue_writer_postwrite_smoke["mode"] == "candidate_queue_writer_postwrite_smoke_planned" @@ -1857,6 +1887,7 @@ def build_deployment_readiness_preview(*, service, market_intel_tables, schema_s "mcp_fetch_candidate_queue_writer_review_decision": mcp_fetch_candidate_queue_writer_review_decision, "mcp_fetch_candidate_queue_writer_review_decision_approval": mcp_fetch_candidate_queue_writer_review_decision_approval, "mcp_fetch_candidate_queue_writer_review_decision_approval_writer_preflight": mcp_fetch_candidate_queue_writer_review_decision_approval_writer_preflight, + "mcp_professional_source_governance": mcp_professional_source_governance, "scheduler_plan": scheduler_plan, "manual_sample_plan": manual_sample_plan, "manual_sample_acceptance": manual_sample_acceptance, diff --git a/services/market_intel/mcp_professional_source_governance.py b/services/market_intel/mcp_professional_source_governance.py new file mode 100644 index 0000000..8bfd396 --- /dev/null +++ b/services/market_intel/mcp_professional_source_governance.py @@ -0,0 +1,391 @@ +"""市場情報專業來源治理 gate。 + +本模組把主流市場資料採集做法轉成可審核合約: +robots/REP、sitemap、structured data、canonical URL、rate limit、 +公開資料邊界、provenance、snapshot hash 與 idempotency。 + +API/UI 只審核操作員提供的治理摘要;不抓外站、不讀 robots/sitemap、 +不開 DB、不寫檔、不掛 scheduler。 +""" + +from urllib.parse import urlparse + +from services.market_intel.mcp_fetch_candidate_queue_writer_post_closeout_inventory_review import ( + FORBIDDEN_SECRET_KEYS, + SAFE_SECRET_METADATA_KEYS, + _as_dict, + _as_list, + _blocked_side_effects, + _contains_forbidden_key, + _safe_int, + _safe_path, + _safe_text, +) +from services.market_intel.mcp_fetch_candidate_queue_writer_run_readiness import ( + ARTIFACT_PREFIX, +) +from services.market_intel.mcp_professional_source_governance_gates import ( + CONTRACT_SCOPE, + SOURCE_POLICY_VERSION, + _is_public_http_url, + build_professional_source_governance_gates, +) +from services.market_intel.mcp_professional_source_governance_sample import ( + build_sample_professional_source_governance_package, +) + + +_BLOCKED_SOURCE_GOVERNANCE_SIDE_EFFECT_KEYS = ( + "allow_api_database_write", + "allow_api_execution", + "allow_api_file_write", + "allow_api_network_fetch", + "allow_database_write", + "allow_external_network", + "allow_scheduler_attach", + "api_fetches_robots_txt", + "api_fetches_sitemap", + "api_fetches_source_url", + "api_opens_database_connection", + "api_uses_external_network", + "api_writes_database", + "api_writes_file", + "database_commit_executed", + "database_write_executed", + "external_network_executed", + "fetch_executed", + "file_written", + "network_request_allowed", + "payload_persisted", + "ready_for_api_database_write", + "real_write_allowed_by_api", + "scheduler_attached", + "write_database", + "writes_executed", + "would_write_database", +) +_RAW_PAYLOAD_KEYS = ( + "body_html", + "full_response_body", + "html", + "page_body", + "page_html", + "raw_html", + "raw_page_html", + "response_body", +) + + +def _safe_float(value): + try: + return float(value or 0) + except (TypeError, ValueError): + return 0.0 + + +def _contains_raw_payload(value): + if isinstance(value, dict): + for key, nested in value.items(): + if str(key).lower() in _RAW_PAYLOAD_KEYS and bool(nested): + return True + if _contains_raw_payload(nested): + return True + if isinstance(value, list): + return any(_contains_raw_payload(item) for item in value) + return False + + +def _blocked_source_governance_side_effects(payload): + found = list(_blocked_side_effects(payload)) + + def visit(value, path): + if isinstance(value, dict): + for key, item in value.items(): + normalized_key = str(key).lower() + key_path = f"{path}.{key}" if path else key + if ( + normalized_key in _BLOCKED_SOURCE_GOVERNANCE_SIDE_EFFECT_KEYS + and bool(item) + ): + found.append(key_path) + visit(item, key_path) + elif isinstance(value, list): + for index, item in enumerate(value): + visit(item, f"{path}[{index}]") + + visit(payload, "") + return sorted(set(found)) + + +def _normalize_host(value): + if not value: + return None + parsed = urlparse(value) + return parsed.netloc.lower() or None + + +def _source_summary(source): + source = _as_dict(source) + source_url = _safe_text(source.get("source_url"), 500) + canonical_url = _safe_text(source.get("canonical_url"), 500) + robots_url = _safe_text(source.get("robots_url"), 500) + sitemap_url = _safe_text(source.get("sitemap_url"), 500) + structured_data_types = [ + _safe_text(item, 80) + for item in _as_list(source.get("structured_data_types")) + if _safe_text(item, 80) + ] + max_requests = _safe_int(source.get("max_requests_per_run")) + crawl_delay_seconds = _safe_float(source.get("crawl_delay_seconds")) + evidence_artifact_path = _safe_text(source.get("evidence_artifact_path")) + source_host = _normalize_host(source_url) + canonical_host = _normalize_host(canonical_url) + return { + "platform_code": _safe_text(source.get("platform_code"), 80), + "source_key": _safe_text(source.get("source_key"), 160), + "source_url": source_url, + "canonical_url": canonical_url, + "robots_url": robots_url, + "sitemap_url": sitemap_url, + "lastmod_source": _safe_text(source.get("lastmod_source"), 160), + "source_url_safe": _is_public_http_url(source_url), + "canonical_url_safe": _is_public_http_url(canonical_url), + "robots_url_safe": _is_public_http_url(robots_url), + "sitemap_url_safe": _is_public_http_url(sitemap_url), + "canonical_host_matches_source": bool( + source_host and canonical_host and source_host == canonical_host + ), + "robots_policy_checked": bool(source.get("robots_policy_checked")), + "robots_allowed": bool(source.get("robots_allowed")), + "tos_public_page_checked": bool(source.get("tos_public_page_checked")), + "login_required": bool(source.get("login_required")), + "member_or_order_data": bool(source.get("member_or_order_data")), + "cart_order_or_pii": bool(source.get("cart_order_or_pii")), + "anti_bot_bypass_required": bool(source.get("anti_bot_bypass_required")), + "structured_data_preferred": bool(source.get("structured_data_preferred")), + "json_ld_first": bool(source.get("json_ld_first")), + "dom_selector_fallback_allowed": bool( + source.get("dom_selector_fallback_allowed") + ), + "structured_data_types": structured_data_types, + "selector_version": _safe_text(source.get("selector_version"), 120), + "crawl_delay_seconds": crawl_delay_seconds, + "max_requests_per_run": max_requests, + "public_cache_ttl_hours": _safe_int(source.get("public_cache_ttl_hours")), + "evidence_artifact_path": evidence_artifact_path, + "evidence_artifact_path_safe": _safe_path( + evidence_artifact_path, + prefixes=(ARTIFACT_PREFIX,), + suffixes=(".json",), + ), + "provenance_required": bool(source.get("provenance_required")), + "snapshot_hash_required": bool(source.get("snapshot_hash_required")), + "idempotency_key_strategy": _safe_text( + source.get("idempotency_key_strategy"), 160 + ), + } + + +def _operator_confirmations(payload): + confirmations = _as_dict(payload.get("operator_confirmations")) + return { + "human_reviewed_source_policy": bool( + confirmations.get("human_reviewed_source_policy") + ), + "robots_and_tos_checked_by_operator": bool( + confirmations.get("robots_and_tos_checked_by_operator") + ), + "public_pages_only": bool(confirmations.get("public_pages_only")), + "no_login_or_member_data": bool( + confirmations.get("no_login_or_member_data") + ), + "no_cart_order_or_pii": bool(confirmations.get("no_cart_order_or_pii")), + "no_antibot_bypass": bool(confirmations.get("no_antibot_bypass")), + "structured_data_first": bool(confirmations.get("structured_data_first")), + "provenance_required": bool(confirmations.get("provenance_required")), + "no_api_network_fetch": bool(confirmations.get("no_api_network_fetch")), + "no_database_write": bool(confirmations.get("no_database_write")), + "no_scheduler_attach": bool(confirmations.get("no_scheduler_attach")), + "no_secret_payload": bool(confirmations.get("no_secret_payload")), + } + + +def _governance_summary(payload): + payload = _as_dict(payload) + sources = [_source_summary(source) for source in _as_list(payload.get("sources"))] + blocked_side_effects = _blocked_source_governance_side_effects(payload) + raw_payload_submitted = _contains_raw_payload(payload) + secret_or_token_submitted = _contains_forbidden_key( + payload, + FORBIDDEN_SECRET_KEYS, + safe_keys=SAFE_SECRET_METADATA_KEYS + | { + "no_api_network_fetch", + "no_secret_payload", + "source_contract_version", + }, + ) + return { + "governance_id": _safe_text(payload.get("governance_id"), 160), + "governance_scope": _safe_text(payload.get("governance_scope"), 160), + "policy_version": _safe_text(payload.get("policy_version"), 120), + "source_contract_version": _safe_text( + payload.get("source_contract_version"), 120 + ), + "sources": sources, + "source_count": len(sources), + "platform_count": len( + {source["platform_code"] for source in sources if source["platform_code"]} + ), + "robots_checked_count": len( + [ + source + for source in sources + if source["robots_policy_checked"] and source["robots_allowed"] + ] + ), + "structured_data_ready_count": len( + [ + source + for source in sources + if source["structured_data_preferred"] and source["json_ld_first"] + ] + ), + "min_crawl_delay_seconds": min( + [source["crawl_delay_seconds"] for source in sources] or [0.0] + ), + "max_requests_per_run": max( + [source["max_requests_per_run"] for source in sources] or [0] + ), + "operator_confirmations": _operator_confirmations(payload), + "raw_payload_submitted_to_api": raw_payload_submitted, + "secret_or_token_submitted_to_api": secret_or_token_submitted, + "blocked_side_effects": blocked_side_effects, + "api_uses_external_network": bool(payload.get("api_uses_external_network")), + "api_fetches_robots_txt": bool(payload.get("api_fetches_robots_txt")), + "api_fetches_sitemap": bool(payload.get("api_fetches_sitemap")), + "api_fetches_source_url": bool(payload.get("api_fetches_source_url")), + "api_opens_database_connection": bool( + payload.get("api_opens_database_connection") + ), + "api_writes_database": bool(payload.get("api_writes_database")), + "api_writes_file": bool(payload.get("api_writes_file")), + "scheduler_attached": bool(payload.get("scheduler_attached")), + } + + +def _source_contract(): + return { + "contract_scope": CONTRACT_SCOPE, + "policy_version": SOURCE_POLICY_VERSION, + "mainstream_practices": [ + { + "key": "robots_exclusion_protocol", + "label": "先人工確認 robots.txt / REP,不由 API 自動抓取或繞過", + "reference_url": "https://www.rfc-editor.org/rfc/rfc9309", + }, + { + "key": "sitemap_provenance", + "label": "以 sitemap / lastmod 作為活動來源發現與更新依據之一", + "reference_url": "https://www.sitemaps.org/protocol.html", + }, + { + "key": "structured_data_first", + "label": "優先解析 JSON-LD / schema.org Product、Offer、ItemList", + "reference_url": "https://developers.google.com/search/docs/appearance/structured-data/product-snippet", + }, + { + "key": "canonical_public_url", + "label": "所有來源需保留 canonical URL、公開 URL 與 host provenance", + "reference_url": "https://developers.google.com/search/docs/crawling-indexing/consolidate-duplicate-urls", + }, + { + "key": "bronze_silver_gold", + "label": "raw evidence、normalized source、reviewed product/match 分層保存", + "reference_url": "internal:market_intel_lakehouse_contract", + }, + ], + "required_source_fields": [ + "platform_code", + "source_key", + "source_url", + "canonical_url", + "robots_url", + "sitemap_url", + "robots_policy_checked", + "robots_allowed", + "structured_data_types", + "crawl_delay_seconds", + "max_requests_per_run", + "evidence_artifact_path", + "snapshot_hash_required", + "idempotency_key_strategy", + ], + "forbidden_data": [ + "login_page", + "member_profile", + "cart_or_checkout", + "order_data", + "personal_data", + "cookie_or_token", + "anti_bot_bypass", + ], + "next_gate": "mcp_fetch_target_review_with_source_governance", + } + + +def build_mcp_professional_source_governance_preview( + *, operator_source_governance=None, phase=None +): + payload_received = operator_source_governance is not None + governance_payload = _as_dict(operator_source_governance) + governance = _governance_summary(governance_payload) + gates = build_professional_source_governance_gates( + package_received=payload_received, + governance=governance, + ) + blocked_reasons = [gate["key"] for gate in gates if not gate["passed"]] + accepted = bool(payload_received and not blocked_reasons) + return { + "mode": ( + "mcp_professional_source_governance" + if accepted + else "mcp_professional_source_governance_preview" + ), + "phase": phase, + "source_governance_payload_received": payload_received, + "mcp_professional_source_governance_accepted": accepted, + "ready_for_mcp_fetch_source_contract": accepted, + "ready_for_api_database_write": False, + "ready_for_scheduler_attach": False, + "network_request_allowed": False, + "external_network_executed": False, + "api_uses_external_network": False, + "api_fetches_robots_txt": False, + "api_fetches_sitemap": False, + "api_fetches_source_url": False, + "api_opens_database_connection": False, + "api_writes_database": False, + "api_writes_file": False, + "database_connection_opened": False, + "database_write_executed": False, + "database_commit_executed": False, + "file_written": False, + "payload_persisted": False, + "scheduler_attached": False, + "gate_count": len(gates), + "passed_gate_count": len([gate for gate in gates if gate["passed"]]), + "blocked_reasons": blocked_reasons, + "gates": gates, + "source_governance_summary": governance, + "source_contract": _source_contract(), + "sources": governance["sources"], + "next_operator_steps": [ + "人工保留 robots / sitemap / ToS / public URL 審核證據。", + "將通過治理的來源餵給後續 fetch target review,不由 API 直接抓外站。", + "正式 fetch 前仍需 MCP readiness、外部 MCP health、manual run package 與 receipt gate。", + ], + "sample_professional_source_governance_package": ( + build_sample_professional_source_governance_package() + ), + } diff --git a/services/market_intel/mcp_professional_source_governance_gates.py b/services/market_intel/mcp_professional_source_governance_gates.py new file mode 100644 index 0000000..fb1b97b --- /dev/null +++ b/services/market_intel/mcp_professional_source_governance_gates.py @@ -0,0 +1,266 @@ +"""Gate checks for professional market source governance.""" + +from urllib.parse import urlparse + + +CONTRACT_SCOPE = "market_intel_public_campaign_source_governance" +SOURCE_POLICY_VERSION = "source_governance_v1" +SUPPORTED_PLATFORMS = ("momo", "pchome", "coupang", "shopee") +ALLOWED_STRUCTURED_DATA_TYPES = { + "AggregateOffer", + "BreadcrumbList", + "ItemList", + "Offer", + "Product", +} +PRIVATE_URL_MARKERS = ( + "/account", + "/cart", + "/checkout", + "/login", + "/member", + "/members", + "/my", + "/order", + "/orders", + "/profile", + "/signin", + "/user", +) +MIN_CRAWL_DELAY_SECONDS = 1.0 +MAX_REQUESTS_PER_RUN = 50 + + +def _is_public_http_url(value): + if not isinstance(value, str) or not value.strip(): + return False + parsed = urlparse(value.strip()) + if parsed.scheme not in ("http", "https"): + return False + if not parsed.netloc: + return False + lowered_path = (parsed.path or "").lower() + return not any(marker in lowered_path for marker in PRIVATE_URL_MARKERS) + + +def _unique_source_keys(sources): + keys = [ + f"{source['platform_code']}:{source['source_key']}" + for source in sources + if source["platform_code"] and source["source_key"] + ] + return bool(keys and len(keys) == len(set(keys)) == len(sources)) + + +def _all_sources_have_allowed_structured_data(sources): + return bool( + sources + and all( + source["structured_data_preferred"] + and source["json_ld_first"] + and bool( + set(source["structured_data_types"]).intersection( + ALLOWED_STRUCTURED_DATA_TYPES + ) + ) + for source in sources + ) + ) + + +def _rate_limits_safe(sources): + return bool( + sources + and all( + source["crawl_delay_seconds"] >= MIN_CRAWL_DELAY_SECONDS + and 0 < source["max_requests_per_run"] <= MAX_REQUESTS_PER_RUN + for source in sources + ) + ) + + +def _source_url_set_safe(source): + return bool( + source["source_url_safe"] + and source["canonical_url_safe"] + and source["robots_url_safe"] + and source["sitemap_url_safe"] + ) + + +def _source_boundaries_safe(source): + return bool( + source["tos_public_page_checked"] + and not source["login_required"] + and not source["member_or_order_data"] + and not source["cart_order_or_pii"] + and not source["anti_bot_bypass_required"] + ) + + +def build_professional_source_governance_gates( + *, + package_received, + governance, +): + """Return gate rows. The caller decides whether failed rows block acceptance.""" + sources = governance["sources"] + confirmations = governance["operator_confirmations"] + operator_boundaries_confirmed = bool( + confirmations["human_reviewed_source_policy"] + and confirmations["robots_and_tos_checked_by_operator"] + and confirmations["public_pages_only"] + and confirmations["no_login_or_member_data"] + and confirmations["no_cart_order_or_pii"] + and confirmations["no_antibot_bypass"] + and confirmations["structured_data_first"] + and confirmations["provenance_required"] + and confirmations["no_api_network_fetch"] + and confirmations["no_database_write"] + and confirmations["no_scheduler_attach"] + and confirmations["no_secret_payload"] + ) + return [ + { + "key": "source_governance_payload_received", + "label": "已提供 operator source governance 摘要", + "passed": package_received, + }, + { + "key": "source_governance_scope_safe", + "label": "governance scope 必須鎖定公開活動來源治理", + "passed": governance["governance_scope"] == CONTRACT_SCOPE, + }, + { + "key": "source_governance_policy_version_recorded", + "label": "來源治理 policy version 必須可稽核", + "passed": governance["policy_version"] == SOURCE_POLICY_VERSION, + }, + { + "key": "source_governance_sources_present", + "label": "至少一個平台公開來源必須被審核", + "passed": bool(sources), + }, + { + "key": "source_governance_supported_platforms", + "label": "來源平台必須在 MOMO / PChome / Coupang / Shopee 白名單內", + "passed": bool( + sources + and all( + source["platform_code"] in SUPPORTED_PLATFORMS + for source in sources + ) + ), + }, + { + "key": "source_governance_unique_source_keys", + "label": "每個 platform/source_key 必須唯一", + "passed": _unique_source_keys(sources), + }, + { + "key": "source_governance_public_urls_only", + "label": "source、canonical、robots 與 sitemap URL 必須是公開 http/https URL", + "passed": bool( + sources and all(_source_url_set_safe(source) for source in sources) + ), + }, + { + "key": "source_governance_robots_policy_checked_and_allowed", + "label": "操作員必須先確認 robots policy 且來源允許抓取", + "passed": bool( + sources + and all( + source["robots_policy_checked"] and source["robots_allowed"] + for source in sources + ) + ), + }, + { + "key": "source_governance_sitemap_or_lastmod_recorded", + "label": "每個來源需記錄 sitemap 或 lastmod provenance", + "passed": bool( + sources + and all(source["sitemap_url"] or source["lastmod_source"] for source in sources) + ), + }, + { + "key": "source_governance_structured_data_first", + "label": "優先讀 JSON-LD / schema.org Product、Offer 或 ItemList", + "passed": _all_sources_have_allowed_structured_data(sources), + }, + { + "key": "source_governance_rate_limits_safe", + "label": "每來源 crawl delay 至少 1 秒且單次 request budget 不超過 50", + "passed": _rate_limits_safe(sources), + }, + { + "key": "source_governance_public_data_only_no_login_or_pii", + "label": "不得碰登入、會員、購物車、訂單或個資頁", + "passed": bool( + sources and all(_source_boundaries_safe(source) for source in sources) + ), + }, + { + "key": "source_governance_no_antibot_bypass", + "label": "不得要求帳號池、繞反爬或破解保護", + "passed": bool( + sources + and all(not source["anti_bot_bypass_required"] for source in sources) + ), + }, + { + "key": "source_governance_provenance_and_hash_required", + "label": "每筆來源需有 provenance、snapshot hash 與 idempotency key 策略", + "passed": bool( + sources + and all( + source["provenance_required"] + and source["snapshot_hash_required"] + and source["idempotency_key_strategy"] + for source in sources + ) + ), + }, + { + "key": "source_governance_evidence_paths_safe", + "label": "治理證據 artifact path 必須在 artifacts/market_intel/*.json", + "passed": bool( + sources + and all(source["evidence_artifact_path_safe"] for source in sources) + ), + }, + { + "key": "source_governance_operator_boundaries_confirmed", + "label": "操作員確認 API 不連外、不寫 DB、不掛 scheduler", + "passed": operator_boundaries_confirmed, + }, + { + "key": "source_governance_no_raw_payload", + "label": "API payload 不得貼入 raw HTML、頁面本文或完整 response body", + "passed": not governance["raw_payload_submitted_to_api"], + }, + { + "key": "source_governance_no_secret_or_token_key", + "label": "API payload 不得包含 cookie、secret、password 或 token key", + "passed": not governance["secret_or_token_submitted_to_api"], + }, + { + "key": "source_governance_side_effect_free", + "label": "payload 不得要求 API 連外、寫檔、寫 DB、執行 CLI 或掛 scheduler", + "passed": not governance["blocked_side_effects"], + }, + { + "key": "source_governance_api_preview_only", + "label": "本 endpoint 只能審核治理合約,不抓 robots/sitemap/source URL", + "passed": bool( + not governance["api_uses_external_network"] + and not governance["api_fetches_robots_txt"] + and not governance["api_fetches_sitemap"] + and not governance["api_fetches_source_url"] + and not governance["api_opens_database_connection"] + and not governance["api_writes_database"] + and not governance["api_writes_file"] + and not governance["scheduler_attached"] + ), + }, + ] diff --git a/services/market_intel/mcp_professional_source_governance_sample.py b/services/market_intel/mcp_professional_source_governance_sample.py new file mode 100644 index 0000000..cf7da89 --- /dev/null +++ b/services/market_intel/mcp_professional_source_governance_sample.py @@ -0,0 +1,175 @@ +"""Sample package for professional source governance review.""" + +from copy import deepcopy + +from services.market_intel.mcp_fetch_candidate_queue_writer_run_readiness import ( + ARTIFACT_PREFIX, +) + + +_SAMPLE_PROFESSIONAL_SOURCE_GOVERNANCE_PACKAGE = { + "operator_source_governance": { + "governance_id": "market-intel-professional-source-governance-sample", + "governance_scope": "market_intel_public_campaign_source_governance", + "policy_version": "source_governance_v1", + "source_contract_version": "market_source_contract_v1", + "sources": [ + { + "platform_code": "momo", + "source_key": "momo_edm", + "source_url": "https://www.momoshop.com.tw/edm/cmmedm.jsp", + "canonical_url": "https://www.momoshop.com.tw/edm/cmmedm.jsp", + "robots_url": "https://www.momoshop.com.tw/robots.txt", + "sitemap_url": "https://www.momoshop.com.tw/sitemap.xml", + "lastmod_source": "sitemap_or_http_last_modified", + "robots_policy_checked": True, + "robots_allowed": True, + "tos_public_page_checked": True, + "login_required": False, + "member_or_order_data": False, + "cart_order_or_pii": False, + "anti_bot_bypass_required": False, + "structured_data_preferred": True, + "json_ld_first": True, + "dom_selector_fallback_allowed": True, + "structured_data_types": ["ItemList", "Product", "Offer"], + "selector_version": "momo_campaign_source_v1", + "crawl_delay_seconds": 2.5, + "max_requests_per_run": 12, + "public_cache_ttl_hours": 24, + "evidence_artifact_path": ( + ARTIFACT_PREFIX + "professional-source-governance-momo-edm.json" + ), + "provenance_required": True, + "snapshot_hash_required": True, + "idempotency_key_strategy": ( + "platform_code:source_key:canonical_url_hash" + ), + }, + { + "platform_code": "pchome", + "source_key": "pchome_home", + "source_url": "https://24h.pchome.com.tw/", + "canonical_url": "https://24h.pchome.com.tw/", + "robots_url": "https://24h.pchome.com.tw/robots.txt", + "sitemap_url": "https://24h.pchome.com.tw/sitemap.xml", + "lastmod_source": "sitemap_or_http_last_modified", + "robots_policy_checked": True, + "robots_allowed": True, + "tos_public_page_checked": True, + "login_required": False, + "member_or_order_data": False, + "cart_order_or_pii": False, + "anti_bot_bypass_required": False, + "structured_data_preferred": True, + "json_ld_first": True, + "dom_selector_fallback_allowed": True, + "structured_data_types": ["ItemList", "Product", "Offer"], + "selector_version": "pchome_campaign_source_v1", + "crawl_delay_seconds": 2.0, + "max_requests_per_run": 10, + "public_cache_ttl_hours": 24, + "evidence_artifact_path": ( + ARTIFACT_PREFIX + "professional-source-governance-pchome-home.json" + ), + "provenance_required": True, + "snapshot_hash_required": True, + "idempotency_key_strategy": ( + "platform_code:source_key:canonical_url_hash" + ), + }, + { + "platform_code": "coupang", + "source_key": "coupang_tw_home", + "source_url": "https://www.tw.coupang.com/", + "canonical_url": "https://www.tw.coupang.com/", + "robots_url": "https://www.tw.coupang.com/robots.txt", + "sitemap_url": "https://www.tw.coupang.com/sitemap.xml", + "lastmod_source": "sitemap_or_http_last_modified", + "robots_policy_checked": True, + "robots_allowed": True, + "tos_public_page_checked": True, + "login_required": False, + "member_or_order_data": False, + "cart_order_or_pii": False, + "anti_bot_bypass_required": False, + "structured_data_preferred": True, + "json_ld_first": True, + "dom_selector_fallback_allowed": True, + "structured_data_types": ["ItemList", "Product", "Offer"], + "selector_version": "coupang_tw_campaign_source_v1", + "crawl_delay_seconds": 3.0, + "max_requests_per_run": 8, + "public_cache_ttl_hours": 24, + "evidence_artifact_path": ( + ARTIFACT_PREFIX + + "professional-source-governance-coupang-home.json" + ), + "provenance_required": True, + "snapshot_hash_required": True, + "idempotency_key_strategy": ( + "platform_code:source_key:canonical_url_hash" + ), + }, + { + "platform_code": "shopee", + "source_key": "shopee_mall", + "source_url": "https://shopee.tw/mall", + "canonical_url": "https://shopee.tw/mall", + "robots_url": "https://shopee.tw/robots.txt", + "sitemap_url": "https://shopee.tw/sitemap.xml", + "lastmod_source": "sitemap_or_http_last_modified", + "robots_policy_checked": True, + "robots_allowed": True, + "tos_public_page_checked": True, + "login_required": False, + "member_or_order_data": False, + "cart_order_or_pii": False, + "anti_bot_bypass_required": False, + "structured_data_preferred": True, + "json_ld_first": True, + "dom_selector_fallback_allowed": True, + "structured_data_types": ["ItemList", "Product", "Offer"], + "selector_version": "shopee_mall_campaign_source_v1", + "crawl_delay_seconds": 3.0, + "max_requests_per_run": 6, + "public_cache_ttl_hours": 24, + "evidence_artifact_path": ( + ARTIFACT_PREFIX + + "professional-source-governance-shopee-mall.json" + ), + "provenance_required": True, + "snapshot_hash_required": True, + "idempotency_key_strategy": ( + "platform_code:source_key:canonical_url_hash" + ), + }, + ], + "operator_confirmations": { + "human_reviewed_source_policy": True, + "robots_and_tos_checked_by_operator": True, + "public_pages_only": True, + "no_login_or_member_data": True, + "no_cart_order_or_pii": True, + "no_antibot_bypass": True, + "structured_data_first": True, + "provenance_required": True, + "no_api_network_fetch": True, + "no_database_write": True, + "no_scheduler_attach": True, + "no_secret_payload": True, + }, + "api_uses_external_network": False, + "api_fetches_robots_txt": False, + "api_fetches_sitemap": False, + "api_fetches_source_url": False, + "api_opens_database_connection": False, + "api_writes_database": False, + "api_writes_file": False, + "scheduler_attached": False, + } +} + + +def build_sample_professional_source_governance_package(): + return deepcopy(_SAMPLE_PROFESSIONAL_SOURCE_GOVERNANCE_PACKAGE) diff --git a/services/market_intel/phase.py b/services/market_intel/phase.py index a12c800..16b60f4 100644 --- a/services/market_intel/phase.py +++ b/services/market_intel/phase.py @@ -1,3 +1,3 @@ """市場情報 rollout phase 單一來源。""" -MARKET_INTEL_PHASE = "phase_139_market_intel_mcp_fetch_candidate_queue_writer_review_decision_approval_writer_preflight" +MARKET_INTEL_PHASE = "phase_140_market_intel_professional_source_governance" diff --git a/templates/market_intel/disabled.html b/templates/market_intel/disabled.html index ea554ce..3c1c2f0 100644 --- a/templates/market_intel/disabled.html +++ b/templates/market_intel/disabled.html @@ -1150,6 +1150,32 @@ +
+
+
+

MCP / SOURCE GOVERNANCE

+

Professional Source Governance

+
+ +
+
+ loading +
+
+
讀取 Professional Source Governance 中...
+
+
+ +
+ +
+
+
+
@@ -1677,6 +1703,7 @@ const mcpFetchCandidateQueueWriterReviewDecisionRoot = document.querySelector('[data-market-intel-mcp-fetch-candidate-queue-writer-review-decision]'); const mcpFetchCandidateQueueWriterReviewDecisionApprovalRoot = document.querySelector('[data-market-intel-mcp-fetch-candidate-queue-writer-review-decision-approval]'); const mcpFetchCandidateQueueWriterReviewDecisionApprovalWriterPreflightRoot = document.querySelector('[data-market-intel-mcp-fetch-candidate-queue-writer-review-decision-approval-writer-preflight]'); + const mcpProfessionalSourceGovernanceRoot = document.querySelector('[data-market-intel-mcp-professional-source-governance]'); const manualSampleRoot = document.querySelector('[data-market-intel-manual-sample]'); const sampleAcceptanceRoot = document.querySelector('[data-market-intel-sample-acceptance]'); const sampleReviewRoot = document.querySelector('[data-market-intel-sample-review]'); @@ -1693,7 +1720,7 @@ const liveInventoryRoot = document.querySelector('[data-market-intel-live-inventory]'); const approvalRoot = document.querySelector('[data-market-intel-approval]'); const deployRoot = document.querySelector('[data-market-intel-deploy]'); - if (!root && !writerRoot && !cliRoot && !dbProbeRoot && !seedDiffRoot && !legacyBridgeRoot && !mcpReadinessRoot && !mcpPreflightRoot && !mcpActivationRoot && !mcpFetchGateRoot && !mcpCompletionRoot && !mcpActivationEvidenceRoot && !mcpRuntimeSmokeRoot && !mcpRuntimePromotionRoot && !mcpManualFetchHandoffRoot && !mcpFetchTargetReviewRoot && !mcpFetchRunPackageRoot && !mcpFetchRunReadinessRoot && !mcpFetchRunReceiptRoot && !mcpFetchResultParserReviewRoot && !mcpFetchCandidateHandoffReviewRoot && !mcpFetchCandidateQueueReviewRoot && !mcpFetchCandidateQueueWriterPreflightRoot && !mcpFetchCandidateQueueWriterCliReviewRoot && !mcpFetchCandidateQueueWriterRunPackageReviewRoot && !mcpFetchCandidateQueueWriterRunReadinessRoot && !mcpFetchCandidateQueueWriterRunReceiptReviewRoot && !mcpFetchCandidateQueueWriterRunCloseoutReviewRoot && !mcpFetchCandidateQueueWriterPostCloseoutInventoryReviewRoot && !mcpFetchCandidateQueueWriterReviewHandoffRoot && !mcpFetchCandidateQueueWriterReviewInventoryRoot && !mcpFetchCandidateQueueWriterReviewDecisionRoot && !mcpFetchCandidateQueueWriterReviewDecisionApprovalRoot && !mcpFetchCandidateQueueWriterReviewDecisionApprovalWriterPreflightRoot && !manualSampleRoot && !sampleAcceptanceRoot && !sampleReviewRoot && !schedulerRoot && !matchReviewRoot && !opportunityRoot && !opportunityScoringRoot && !opportunityEvidenceRoot && !opportunityAlertRoot && !migrationRoot && !migrationDrillRoot && !catalogReviewRoot && !liveSmokeRoot && !liveInventoryRoot && !approvalRoot && !deployRoot) return; + if (!root && !writerRoot && !cliRoot && !dbProbeRoot && !seedDiffRoot && !legacyBridgeRoot && !mcpReadinessRoot && !mcpPreflightRoot && !mcpActivationRoot && !mcpFetchGateRoot && !mcpCompletionRoot && !mcpActivationEvidenceRoot && !mcpRuntimeSmokeRoot && !mcpRuntimePromotionRoot && !mcpManualFetchHandoffRoot && !mcpFetchTargetReviewRoot && !mcpFetchRunPackageRoot && !mcpFetchRunReadinessRoot && !mcpFetchRunReceiptRoot && !mcpFetchResultParserReviewRoot && !mcpFetchCandidateHandoffReviewRoot && !mcpFetchCandidateQueueReviewRoot && !mcpFetchCandidateQueueWriterPreflightRoot && !mcpFetchCandidateQueueWriterCliReviewRoot && !mcpFetchCandidateQueueWriterRunPackageReviewRoot && !mcpFetchCandidateQueueWriterRunReadinessRoot && !mcpFetchCandidateQueueWriterRunReceiptReviewRoot && !mcpFetchCandidateQueueWriterRunCloseoutReviewRoot && !mcpFetchCandidateQueueWriterPostCloseoutInventoryReviewRoot && !mcpFetchCandidateQueueWriterReviewHandoffRoot && !mcpFetchCandidateQueueWriterReviewInventoryRoot && !mcpFetchCandidateQueueWriterReviewDecisionRoot && !mcpFetchCandidateQueueWriterReviewDecisionApprovalRoot && !mcpFetchCandidateQueueWriterReviewDecisionApprovalWriterPreflightRoot && !mcpProfessionalSourceGovernanceRoot && !manualSampleRoot && !sampleAcceptanceRoot && !sampleReviewRoot && !schedulerRoot && !matchReviewRoot && !opportunityRoot && !opportunityScoringRoot && !opportunityEvidenceRoot && !opportunityAlertRoot && !migrationRoot && !migrationDrillRoot && !catalogReviewRoot && !liveSmokeRoot && !liveInventoryRoot && !approvalRoot && !deployRoot) return; const meta = root ? root.querySelector('[data-market-intel-preview-meta]') : null; const body = root ? root.querySelector('[data-market-intel-preview-body]') : null; @@ -1878,6 +1905,12 @@ const mcpFetchCandidateQueueWriterReviewDecisionApprovalWriterPreflightReview = mcpFetchCandidateQueueWriterReviewDecisionApprovalWriterPreflightRoot ? mcpFetchCandidateQueueWriterReviewDecisionApprovalWriterPreflightRoot.querySelector('[data-market-intel-mcp-fetch-candidate-queue-writer-review-decision-approval-writer-preflight-review]') : null; const mcpFetchCandidateQueueWriterReviewDecisionApprovalWriterPreflightRefresh = mcpFetchCandidateQueueWriterReviewDecisionApprovalWriterPreflightRoot ? mcpFetchCandidateQueueWriterReviewDecisionApprovalWriterPreflightRoot.querySelector('[data-market-intel-mcp-fetch-candidate-queue-writer-review-decision-approval-writer-preflight-refresh]') : null; const mcpFetchCandidateQueueWriterReviewDecisionApprovalWriterPreflightEndpoint = "{{ url_for('market_intel.market_intel_mcp_fetch_candidate_queue_writer_review_decision_approval_writer_preflight') }}"; + const mcpProfessionalSourceGovernanceMeta = mcpProfessionalSourceGovernanceRoot ? mcpProfessionalSourceGovernanceRoot.querySelector('[data-market-intel-mcp-professional-source-governance-meta]') : null; + const mcpProfessionalSourceGovernanceBody = mcpProfessionalSourceGovernanceRoot ? mcpProfessionalSourceGovernanceRoot.querySelector('[data-market-intel-mcp-professional-source-governance-body]') : null; + const mcpProfessionalSourceGovernanceInput = mcpProfessionalSourceGovernanceRoot ? mcpProfessionalSourceGovernanceRoot.querySelector('[data-market-intel-mcp-professional-source-governance-input]') : null; + const mcpProfessionalSourceGovernanceReview = mcpProfessionalSourceGovernanceRoot ? mcpProfessionalSourceGovernanceRoot.querySelector('[data-market-intel-mcp-professional-source-governance-review]') : null; + const mcpProfessionalSourceGovernanceRefresh = mcpProfessionalSourceGovernanceRoot ? mcpProfessionalSourceGovernanceRoot.querySelector('[data-market-intel-mcp-professional-source-governance-refresh]') : null; + const mcpProfessionalSourceGovernanceEndpoint = "{{ url_for('market_intel.market_intel_mcp_professional_source_governance') }}"; const manualSampleMeta = manualSampleRoot ? manualSampleRoot.querySelector('[data-market-intel-manual-sample-meta]') : null; const manualSampleBody = manualSampleRoot ? manualSampleRoot.querySelector('[data-market-intel-manual-sample-body]') : null; const manualSampleRefresh = manualSampleRoot ? manualSampleRoot.querySelector('[data-market-intel-manual-sample-refresh]') : null; @@ -5767,6 +5800,126 @@ } }; + const renderMcpProfessionalSourceGovernanceMeta = data => { + const summary = data.source_governance_summary || {}; + mcpProfessionalSourceGovernanceMeta.innerHTML = [ + `mode=${data.mode || 'unknown'}`, + `accepted=${data.mcp_professional_source_governance_accepted ? 'yes' : 'no'}`, + `gates=${data.passed_gate_count || 0}/${data.gate_count || 0}`, + `sources=${summary.source_count || 0}`, + `platforms=${summary.platform_count || 0}`, + `network=${data.external_network_executed ? 'executed' : 'blocked'}` + ].map(item => `${escapeHtml(item)}`).join(''); + }; + + const renderMcpProfessionalSourceGovernanceBody = data => { + const blockers = (data.blocked_reasons || []).join(' / '); + const gates = data.gates || []; + const sources = data.sources || data.source_governance_summary?.sources || []; + const contract = data.source_contract || {}; + const practices = contract.mainstream_practices || []; + const steps = data.next_operator_steps || []; + const renderCheck = (key, label, status) => ` +
+
+ ${escapeHtml(key)} + ${escapeHtml(label || '')} +
+ ${escapeHtml(status)} +
+ `; + mcpProfessionalSourceGovernanceBody.innerHTML = ` +
此治理層把 robots、sitemap、structured data、canonical URL、rate limit、公開資料邊界與 provenance 變成 source contract;API 不抓外站、不讀 robots/sitemap、不寫 DB、不掛 scheduler。${blockers ? `阻擋:${escapeHtml(blockers)}` : ''}
+
+
+

SOURCE GATES

+
${ + gates.length + ? gates.map(item => renderCheck(item.key, item.label, item.passed ? 'PASS' : 'BLOCK')).join('') + : '
尚未提供 source gates。
' + }
+
+
+

PUBLIC SOURCES

+
${ + sources.length + ? sources.map(source => renderCheck( + `${source.platform_code || 'unknown'}:${source.source_key || 'missing'}`, + `${source.canonical_url || source.source_url || 'missing'} / delay=${source.crawl_delay_seconds || 0}s / requests=${source.max_requests_per_run || 0}`, + source.source_url_safe && source.robots_allowed && source.structured_data_preferred && source.evidence_artifact_path_safe ? 'READY' : 'BLOCK' + )).join('') + : '
尚未提供公開來源。
' + }
+
+
+

MAINSTREAM PRACTICES

+
${ + practices.length + ? practices.map(item => renderCheck(item.key, item.label, 'LOCKED')).join('') + : '
尚未提供專業做法合約。
' + }
+
+
+

BOUNDARY / NEXT

+
+ ${renderCheck('next_gate', contract.next_gate || 'missing', contract.next_gate ? 'NEXT' : 'BLOCK')} + ${renderCheck('api_boundary', 'no external fetch / no robots fetch / no sitemap fetch / no DB write / no file write / no scheduler', data.api_uses_external_network || data.api_fetches_robots_txt || data.api_fetches_sitemap || data.api_fetches_source_url || data.api_writes_database || data.api_writes_file || data.scheduler_attached ? 'BLOCK' : 'CLOSED')} + ${steps.map((item, index) => renderCheck(`step_${index + 1}`, item, 'NEXT')).join('')} +
+
+
+ `; + if (mcpProfessionalSourceGovernanceInput && !mcpProfessionalSourceGovernanceInput.value.trim() && data.sample_professional_source_governance_package) { + mcpProfessionalSourceGovernanceInput.value = JSON.stringify(data.sample_professional_source_governance_package, null, 2); + } + }; + + const loadMcpProfessionalSourceGovernance = async () => { + if (!mcpProfessionalSourceGovernanceMeta || !mcpProfessionalSourceGovernanceBody) return; + mcpProfessionalSourceGovernanceBody.innerHTML = '
讀取 Professional Source Governance 中...
'; + try { + const response = await fetch(mcpProfessionalSourceGovernanceEndpoint, { credentials: 'same-origin' }); + if (!response.ok) throw new Error(`HTTP ${response.status}`); + const data = await response.json(); + renderMcpProfessionalSourceGovernanceMeta(data); + renderMcpProfessionalSourceGovernanceBody(data); + } catch (error) { + mcpProfessionalSourceGovernanceMeta.innerHTML = 'error'; + mcpProfessionalSourceGovernanceBody.innerHTML = `
Professional Source Governance 讀取失敗:${escapeHtml(error.message)}
`; + } + }; + + const reviewMcpProfessionalSourceGovernance = async () => { + if (!mcpProfessionalSourceGovernanceMeta || !mcpProfessionalSourceGovernanceBody || !mcpProfessionalSourceGovernanceInput) return; + let parsed; + try { + parsed = JSON.parse(mcpProfessionalSourceGovernanceInput.value || '{}'); + } catch (error) { + mcpProfessionalSourceGovernanceMeta.innerHTML = 'json_error'; + mcpProfessionalSourceGovernanceBody.innerHTML = `
JSON 格式錯誤:${escapeHtml(error.message)}
`; + return; + } + mcpProfessionalSourceGovernanceBody.innerHTML = '
審核 Professional Source Governance 中...
'; + try { + const response = await fetch(mcpProfessionalSourceGovernanceEndpoint, { + method: 'POST', + credentials: 'same-origin', + headers: { + 'Content-Type': 'application/json', + 'X-CSRFToken': csrfToken + }, + body: JSON.stringify({ professional_source_governance_package: parsed }) + }); + const data = await response.json(); + if (!response.ok && !data.mode) throw new Error(`HTTP ${response.status}`); + renderMcpProfessionalSourceGovernanceMeta(data); + renderMcpProfessionalSourceGovernanceBody(data); + } catch (error) { + mcpProfessionalSourceGovernanceMeta.innerHTML = 'error'; + mcpProfessionalSourceGovernanceBody.innerHTML = `
Professional Source Governance 審核失敗:${escapeHtml(error.message)}
`; + } + }; + const renderManualSampleMeta = data => { manualSampleMeta.innerHTML = [ `mode=${data.mode || 'unknown'}`, @@ -15308,6 +15461,12 @@ if (mcpFetchCandidateQueueWriterReviewDecisionApprovalWriterPreflightReview) { mcpFetchCandidateQueueWriterReviewDecisionApprovalWriterPreflightReview.addEventListener('click', reviewMcpFetchCandidateQueueWriterReviewDecisionApprovalWriterPreflight); } + if (mcpProfessionalSourceGovernanceRefresh) { + mcpProfessionalSourceGovernanceRefresh.addEventListener('click', loadMcpProfessionalSourceGovernance); + } + if (mcpProfessionalSourceGovernanceReview) { + mcpProfessionalSourceGovernanceReview.addEventListener('click', reviewMcpProfessionalSourceGovernance); + } if (manualSampleRefresh) { manualSampleRefresh.addEventListener('click', loadManualSample); } @@ -15585,6 +15744,7 @@ loadMcpFetchCandidateQueueWriterReviewDecision(); loadMcpFetchCandidateQueueWriterReviewDecisionApproval(); loadMcpFetchCandidateQueueWriterReviewDecisionApprovalWriterPreflight(); + loadMcpProfessionalSourceGovernance(); loadManualSample(); loadSampleAcceptance(); loadSampleReview(); diff --git a/tests/test_market_intel_skeleton.py b/tests/test_market_intel_skeleton.py index 11ec66c..97a6ff8 100644 --- a/tests/test_market_intel_skeleton.py +++ b/tests/test_market_intel_skeleton.py @@ -74,6 +74,9 @@ from services.market_intel.mcp_fetch_candidate_queue_writer_review_decision_appr from services.market_intel.mcp_fetch_candidate_queue_writer_review_decision_approval_writer_preflight import ( build_mcp_fetch_candidate_queue_writer_review_decision_approval_writer_preflight_preview, ) +from services.market_intel.mcp_professional_source_governance import ( + build_mcp_professional_source_governance_preview, +) from services.market_intel.mcp_fetch_target_review import ( build_mcp_fetch_target_review_preview, ) @@ -1375,6 +1378,23 @@ def test_market_intel_preview_template_uses_safe_fetch_false_endpoint(): "data-market-intel-mcp-fetch-candidate-queue-writer-review-decision-approval-writer-preflight-next" in template ) + assert ( + "market_intel.market_intel_mcp_professional_source_governance" + in template + ) + assert "data-market-intel-mcp-professional-source-governance" in template + assert ( + "data-market-intel-mcp-professional-source-governance-gates" + in template + ) + assert ( + "data-market-intel-mcp-professional-source-governance-sources" + in template + ) + assert ( + "data-market-intel-mcp-professional-source-governance-next" + in template + ) assert "market_intel.market_intel_manual_sample_plan" in template assert "market_intel.market_intel_manual_sample_acceptance" in template assert "market_intel.market_intel_manual_sample_review" in template @@ -1845,7 +1865,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_139_market_intel_mcp_fetch_candidate_queue_writer_review_decision_approval_writer_preflight" + assert bridge["phase"] == "phase_140_market_intel_professional_source_governance" assert bridge["execute_requested"] is False assert bridge["read_only_query_executed"] is False assert bridge["database_connection_opened"] is False @@ -2003,7 +2023,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_139_market_intel_mcp_fetch_candidate_queue_writer_review_decision_approval_writer_preflight" + assert contract["phase"] == "phase_140_market_intel_professional_source_governance" assert contract["caller"] == "market_intel" assert contract["contract_ready"] is True assert contract["blocked_reasons"] == [] @@ -2136,7 +2156,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_139_market_intel_mcp_fetch_candidate_queue_writer_review_decision_approval_writer_preflight" + assert data["phase"] == "phase_140_market_intel_professional_source_governance" assert data["deployment_actions_executed"] is False assert data["docker_command_executed"] is False assert data["ssh_command_executed"] is False @@ -2149,7 +2169,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_139_market_intel_mcp_fetch_candidate_queue_writer_review_decision_approval_writer_preflight" + assert gate["phase"] == "phase_140_market_intel_professional_source_governance" assert gate["fetch_requested"] is True assert gate["manual_fetch_gate_open"] is False assert gate["network_request_allowed"] is False @@ -2219,7 +2239,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_139_market_intel_mcp_fetch_candidate_queue_writer_review_decision_approval_writer_preflight" + assert data["phase"] == "phase_140_market_intel_professional_source_governance" assert data["fetch_requested"] is False assert data["network_request_allowed"] is False assert data["external_network_executed"] is False @@ -2233,7 +2253,7 @@ def test_mcp_completion_audit_summarizes_external_and_internal_state(monkeypatch audit = MarketIntelService().build_mcp_completion_audit() assert audit["mode"] == "mcp_completion_audit_preview" - assert audit["phase"] == "phase_139_market_intel_mcp_fetch_candidate_queue_writer_review_decision_approval_writer_preflight" + assert audit["phase"] == "phase_140_market_intel_professional_source_governance" assert audit["audit_ready_for_operator_review"] is True assert audit["audit_preview_safe"] is True assert audit["external_mcp_runtime_complete"] is False @@ -2307,7 +2327,7 @@ def test_mcp_completion_audit_route_is_preview_only(): assert response.status_code == 200 assert data["mode"] == "mcp_completion_audit_preview" - assert data["phase"] == "phase_139_market_intel_mcp_fetch_candidate_queue_writer_review_decision_approval_writer_preflight" + assert data["phase"] == "phase_140_market_intel_professional_source_governance" assert data["audit_preview_safe"] is True assert data["external_mcp_runtime_complete"] is False assert data["internal_mcp_contract_complete"] is True @@ -2324,11 +2344,11 @@ def test_mcp_completion_audit_route_is_preview_only(): def test_mcp_activation_evidence_preview_is_safe_without_payload(): evidence = build_mcp_activation_evidence_preview( - phase="phase_139_market_intel_mcp_fetch_candidate_queue_writer_review_decision_approval_writer_preflight" + phase="phase_140_market_intel_professional_source_governance" ) assert evidence["mode"] == "mcp_activation_evidence_preview" - assert evidence["phase"] == "phase_139_market_intel_mcp_fetch_candidate_queue_writer_review_decision_approval_writer_preflight" + assert evidence["phase"] == "phase_140_market_intel_professional_source_governance" assert evidence["evidence_payload_received"] is False assert evidence["activation_evidence_accepted"] is False assert evidence["ready_for_runtime_promotion"] is False @@ -2353,7 +2373,7 @@ def test_mcp_activation_evidence_accepts_redacted_runtime_evidence(): ] evidence = build_mcp_activation_evidence_preview( evidence=sample, - phase="phase_139_market_intel_mcp_fetch_candidate_queue_writer_review_decision_approval_writer_preflight", + phase="phase_140_market_intel_professional_source_governance", ) assert evidence["mode"] == "mcp_activation_evidence_review" @@ -2411,12 +2431,12 @@ def test_mcp_activation_evidence_route_get_and_post_are_preview_only(): assert get_response.status_code == 200 assert get_data["mode"] == "mcp_activation_evidence_preview" - assert get_data["phase"] == "phase_139_market_intel_mcp_fetch_candidate_queue_writer_review_decision_approval_writer_preflight" + assert get_data["phase"] == "phase_140_market_intel_professional_source_governance" assert get_data["api_executes_health_check"] is False assert get_data["api_writes_database"] is False assert post_response.status_code == 200 assert post_data["mode"] == "mcp_activation_evidence_review" - assert post_data["phase"] == "phase_139_market_intel_mcp_fetch_candidate_queue_writer_review_decision_approval_writer_preflight" + assert post_data["phase"] == "phase_140_market_intel_professional_source_governance" assert post_data["activation_evidence_accepted"] is True assert post_data["payload_persisted"] is False assert post_data["api_opens_database_connection"] is False @@ -2425,11 +2445,11 @@ def test_mcp_activation_evidence_route_get_and_post_are_preview_only(): def test_mcp_runtime_smoke_receipt_preview_is_safe_without_payload(): receipt = build_mcp_runtime_smoke_receipt_preview( - phase="phase_139_market_intel_mcp_fetch_candidate_queue_writer_review_decision_approval_writer_preflight" + phase="phase_140_market_intel_professional_source_governance" ) assert receipt["mode"] == "mcp_runtime_smoke_receipt_preview" - assert receipt["phase"] == "phase_139_market_intel_mcp_fetch_candidate_queue_writer_review_decision_approval_writer_preflight" + assert receipt["phase"] == "phase_140_market_intel_professional_source_governance" assert receipt["receipt_payload_received"] is False assert receipt["runtime_smoke_receipt_accepted"] is False assert receipt["ready_for_completion_runtime_promotion"] is False @@ -2457,7 +2477,7 @@ def test_mcp_runtime_smoke_receipt_accepts_complete_readiness_receipt(): ] receipt = build_mcp_runtime_smoke_receipt_preview( receipt=sample, - phase="phase_139_market_intel_mcp_fetch_candidate_queue_writer_review_decision_approval_writer_preflight", + phase="phase_140_market_intel_professional_source_governance", ) assert receipt["mode"] == "mcp_runtime_smoke_receipt_review" @@ -2522,12 +2542,12 @@ def test_mcp_runtime_smoke_receipt_route_get_and_post_are_preview_only(): assert get_response.status_code == 200 assert get_data["mode"] == "mcp_runtime_smoke_receipt_preview" - assert get_data["phase"] == "phase_139_market_intel_mcp_fetch_candidate_queue_writer_review_decision_approval_writer_preflight" + assert get_data["phase"] == "phase_140_market_intel_professional_source_governance" assert get_data["api_executes_health_check"] is False assert get_data["api_writes_database"] is False assert post_response.status_code == 200 assert post_data["mode"] == "mcp_runtime_smoke_receipt_review" - assert post_data["phase"] == "phase_139_market_intel_mcp_fetch_candidate_queue_writer_review_decision_approval_writer_preflight" + assert post_data["phase"] == "phase_140_market_intel_professional_source_governance" assert post_data["runtime_smoke_receipt_accepted"] is True assert post_data["receipt_persisted"] is False assert post_data["api_opens_database_connection"] is False @@ -2536,11 +2556,11 @@ def test_mcp_runtime_smoke_receipt_route_get_and_post_are_preview_only(): def test_mcp_runtime_promotion_preview_is_safe_without_payload(): promotion = build_mcp_runtime_promotion_preview( - phase="phase_139_market_intel_mcp_fetch_candidate_queue_writer_review_decision_approval_writer_preflight" + phase="phase_140_market_intel_professional_source_governance" ) assert promotion["mode"] == "mcp_runtime_promotion_preview" - assert promotion["phase"] == "phase_139_market_intel_mcp_fetch_candidate_queue_writer_review_decision_approval_writer_preflight" + assert promotion["phase"] == "phase_140_market_intel_professional_source_governance" assert promotion["promotion_payload_received"] is False assert promotion["runtime_promotion_accepted"] is False assert promotion["ready_for_completion_runtime_promotion"] is False @@ -2568,7 +2588,7 @@ def test_mcp_runtime_promotion_accepts_evidence_and_receipt_package(): promotion = build_mcp_runtime_promotion_preview( activation_evidence=sample["activation_evidence"], runtime_receipt=sample["runtime_receipt"], - phase="phase_139_market_intel_mcp_fetch_candidate_queue_writer_review_decision_approval_writer_preflight", + phase="phase_140_market_intel_professional_source_governance", ) assert promotion["mode"] == "mcp_runtime_promotion_review" @@ -2628,12 +2648,12 @@ def test_mcp_runtime_promotion_route_get_and_post_are_preview_only(): assert get_response.status_code == 200 assert get_data["mode"] == "mcp_runtime_promotion_preview" - assert get_data["phase"] == "phase_139_market_intel_mcp_fetch_candidate_queue_writer_review_decision_approval_writer_preflight" + assert get_data["phase"] == "phase_140_market_intel_professional_source_governance" assert get_data["api_executes_health_check"] is False assert get_data["api_writes_database"] is False assert post_response.status_code == 200 assert post_data["mode"] == "mcp_runtime_promotion_review" - assert post_data["phase"] == "phase_139_market_intel_mcp_fetch_candidate_queue_writer_review_decision_approval_writer_preflight" + assert post_data["phase"] == "phase_140_market_intel_professional_source_governance" assert post_data["runtime_promotion_accepted"] is True assert post_data["promotion_persisted"] is False assert post_data["api_opens_database_connection"] is False @@ -2642,11 +2662,11 @@ def test_mcp_runtime_promotion_route_get_and_post_are_preview_only(): def test_mcp_manual_fetch_handoff_preview_is_safe_without_payload(): handoff = build_mcp_manual_fetch_handoff_preview( - phase="phase_139_market_intel_mcp_fetch_candidate_queue_writer_review_decision_approval_writer_preflight", + phase="phase_140_market_intel_professional_source_governance", ) assert handoff["mode"] == "mcp_manual_fetch_handoff_preview" - assert handoff["phase"] == "phase_139_market_intel_mcp_fetch_candidate_queue_writer_review_decision_approval_writer_preflight" + assert handoff["phase"] == "phase_140_market_intel_professional_source_governance" assert handoff["handoff_payload_received"] is False assert handoff["manual_fetch_handoff_accepted"] is False assert handoff["ready_for_manual_fetch_gate_operator_review"] is False @@ -2674,7 +2694,7 @@ def test_mcp_manual_fetch_handoff_accepts_promotion_and_acknowledgements(): handoff = build_mcp_manual_fetch_handoff_preview( promotion_package=sample["promotion_package"], operator_acknowledgements=sample["operator_acknowledgements"], - phase="phase_139_market_intel_mcp_fetch_candidate_queue_writer_review_decision_approval_writer_preflight", + phase="phase_140_market_intel_professional_source_governance", ) assert handoff["mode"] == "mcp_manual_fetch_handoff_review" @@ -2736,12 +2756,12 @@ def test_mcp_manual_fetch_handoff_route_get_and_post_are_preview_only(): assert get_response.status_code == 200 assert get_data["mode"] == "mcp_manual_fetch_handoff_preview" - assert get_data["phase"] == "phase_139_market_intel_mcp_fetch_candidate_queue_writer_review_decision_approval_writer_preflight" + assert get_data["phase"] == "phase_140_market_intel_professional_source_governance" assert get_data["api_executes_health_check"] is False assert get_data["api_writes_database"] is False assert post_response.status_code == 200 assert post_data["mode"] == "mcp_manual_fetch_handoff_review" - assert post_data["phase"] == "phase_139_market_intel_mcp_fetch_candidate_queue_writer_review_decision_approval_writer_preflight" + assert post_data["phase"] == "phase_140_market_intel_professional_source_governance" assert post_data["manual_fetch_handoff_accepted"] is True assert post_data["manual_fetch_gate_opened_by_api"] is False assert post_data["network_request_allowed"] is False @@ -2752,11 +2772,11 @@ def test_mcp_manual_fetch_handoff_route_get_and_post_are_preview_only(): def test_mcp_fetch_target_review_preview_is_safe_without_payload(): review = build_mcp_fetch_target_review_preview( - phase="phase_139_market_intel_mcp_fetch_candidate_queue_writer_review_decision_approval_writer_preflight", + phase="phase_140_market_intel_professional_source_governance", ) assert review["mode"] == "mcp_fetch_target_review_preview" - assert review["phase"] == "phase_139_market_intel_mcp_fetch_candidate_queue_writer_review_decision_approval_writer_preflight" + assert review["phase"] == "phase_140_market_intel_professional_source_governance" assert review["target_payload_received"] is False assert review["mcp_fetch_target_review_accepted"] is False assert review["ready_for_manual_fetch_run_package_review"] is False @@ -2786,7 +2806,7 @@ def test_mcp_fetch_target_review_accepts_sample_targets(): review = build_mcp_fetch_target_review_preview( handoff_package=sample["handoff_package"], target_review=sample["target_review"], - phase="phase_139_market_intel_mcp_fetch_candidate_queue_writer_review_decision_approval_writer_preflight", + phase="phase_140_market_intel_professional_source_governance", ) assert review["mode"] == "mcp_fetch_target_review" @@ -2864,13 +2884,13 @@ def test_mcp_fetch_target_review_route_get_and_post_are_preview_only(): assert get_response.status_code == 200 assert get_data["mode"] == "mcp_fetch_target_review_preview" - assert get_data["phase"] == "phase_139_market_intel_mcp_fetch_candidate_queue_writer_review_decision_approval_writer_preflight" + assert get_data["phase"] == "phase_140_market_intel_professional_source_governance" assert get_data["api_executes_health_check"] is False assert get_data["api_writes_database"] is False assert get_data["api_uses_external_network"] is False assert post_response.status_code == 200 assert post_data["mode"] == "mcp_fetch_target_review" - assert post_data["phase"] == "phase_139_market_intel_mcp_fetch_candidate_queue_writer_review_decision_approval_writer_preflight" + assert post_data["phase"] == "phase_140_market_intel_professional_source_governance" assert post_data["mcp_fetch_target_review_accepted"] is True assert post_data["manual_fetch_gate_opened_by_api"] is False assert post_data["network_request_allowed"] is False @@ -2882,11 +2902,11 @@ def test_mcp_fetch_target_review_route_get_and_post_are_preview_only(): def test_mcp_fetch_run_package_preview_is_safe_without_payload(): package = build_mcp_fetch_run_package_preview( - phase="phase_139_market_intel_mcp_fetch_candidate_queue_writer_review_decision_approval_writer_preflight", + phase="phase_140_market_intel_professional_source_governance", ) assert package["mode"] == "mcp_fetch_run_package_preview" - assert package["phase"] == "phase_139_market_intel_mcp_fetch_candidate_queue_writer_review_decision_approval_writer_preflight" + assert package["phase"] == "phase_140_market_intel_professional_source_governance" assert package["run_payload_received"] is False assert package["mcp_fetch_run_package_accepted"] is False assert package["ready_for_manual_fetch_run_readiness_review"] is False @@ -2918,7 +2938,7 @@ def test_mcp_fetch_run_package_accepts_sample_package(): package = build_mcp_fetch_run_package_preview( target_review_package=sample["target_review_package"], operator_run_controls=sample["operator_run_controls"], - phase="phase_139_market_intel_mcp_fetch_candidate_queue_writer_review_decision_approval_writer_preflight", + phase="phase_140_market_intel_professional_source_governance", ) assert package["mode"] == "mcp_fetch_run_package_review" @@ -2993,13 +3013,13 @@ def test_mcp_fetch_run_package_route_get_and_post_are_preview_only(): assert get_response.status_code == 200 assert get_data["mode"] == "mcp_fetch_run_package_preview" - assert get_data["phase"] == "phase_139_market_intel_mcp_fetch_candidate_queue_writer_review_decision_approval_writer_preflight" + assert get_data["phase"] == "phase_140_market_intel_professional_source_governance" assert get_data["api_executes_cli"] is False assert get_data["api_writes_database"] is False assert get_data["api_uses_external_network"] is False assert post_response.status_code == 200 assert post_data["mode"] == "mcp_fetch_run_package_review" - assert post_data["phase"] == "phase_139_market_intel_mcp_fetch_candidate_queue_writer_review_decision_approval_writer_preflight" + assert post_data["phase"] == "phase_140_market_intel_professional_source_governance" assert post_data["mcp_fetch_run_package_accepted"] is True assert post_data["ready_for_manual_fetch_operator_run"] is False assert post_data["manual_fetch_gate_opened_by_api"] is False @@ -3013,11 +3033,11 @@ def test_mcp_fetch_run_package_route_get_and_post_are_preview_only(): def test_mcp_fetch_run_readiness_preview_is_safe_without_payload(): readiness = build_mcp_fetch_run_readiness_preview( - phase="phase_139_market_intel_mcp_fetch_candidate_queue_writer_review_decision_approval_writer_preflight", + phase="phase_140_market_intel_professional_source_governance", ) assert readiness["mode"] == "mcp_fetch_run_readiness_preview" - assert readiness["phase"] == "phase_139_market_intel_mcp_fetch_candidate_queue_writer_review_decision_approval_writer_preflight" + assert readiness["phase"] == "phase_140_market_intel_professional_source_governance" assert readiness["run_readiness_payload_received"] is False assert readiness["mcp_fetch_run_readiness_accepted"] is False assert readiness["run_readiness_ready"] is False @@ -3052,11 +3072,11 @@ def test_mcp_fetch_run_readiness_accepts_sample_package(): run_package=sample["run_package"], run_package_result=sample["run_package_result"], operator_readiness=sample["operator_readiness"], - phase="phase_139_market_intel_mcp_fetch_candidate_queue_writer_review_decision_approval_writer_preflight", + phase="phase_140_market_intel_professional_source_governance", ) assert readiness["mode"] == "mcp_fetch_run_readiness_review" - assert readiness["phase"] == "phase_139_market_intel_mcp_fetch_candidate_queue_writer_review_decision_approval_writer_preflight" + assert readiness["phase"] == "phase_140_market_intel_professional_source_governance" assert readiness["mcp_fetch_run_readiness_accepted"] is True assert readiness["run_readiness_ready"] is True assert readiness["ready_for_manual_fetch_operator_run"] is True @@ -3132,13 +3152,13 @@ def test_mcp_fetch_run_readiness_route_get_and_post_are_preview_only(): assert get_response.status_code == 200 assert get_data["mode"] == "mcp_fetch_run_readiness_preview" - assert get_data["phase"] == "phase_139_market_intel_mcp_fetch_candidate_queue_writer_review_decision_approval_writer_preflight" + assert get_data["phase"] == "phase_140_market_intel_professional_source_governance" assert get_data["api_executes_cli"] is False assert get_data["api_writes_database"] is False assert get_data["api_uses_external_network"] is False assert post_response.status_code == 200 assert post_data["mode"] == "mcp_fetch_run_readiness_review" - assert post_data["phase"] == "phase_139_market_intel_mcp_fetch_candidate_queue_writer_review_decision_approval_writer_preflight" + assert post_data["phase"] == "phase_140_market_intel_professional_source_governance" assert post_data["mcp_fetch_run_readiness_accepted"] is True assert post_data["ready_for_manual_fetch_operator_run"] is True assert post_data["manual_fetch_gate_opened_by_api"] is False @@ -3152,11 +3172,11 @@ def test_mcp_fetch_run_readiness_route_get_and_post_are_preview_only(): def test_mcp_fetch_run_receipt_preview_is_safe_without_payload(): receipt = build_mcp_fetch_run_receipt_preview( - phase="phase_139_market_intel_mcp_fetch_candidate_queue_writer_review_decision_approval_writer_preflight", + phase="phase_140_market_intel_professional_source_governance", ) assert receipt["mode"] == "mcp_fetch_run_receipt_preview" - assert receipt["phase"] == "phase_139_market_intel_mcp_fetch_candidate_queue_writer_review_decision_approval_writer_preflight" + assert receipt["phase"] == "phase_140_market_intel_professional_source_governance" assert receipt["run_receipt_payload_received"] is False assert receipt["mcp_fetch_run_receipt_accepted"] is False assert receipt["run_receipt_ready"] is False @@ -3193,11 +3213,11 @@ def test_mcp_fetch_run_receipt_accepts_sample_receipt(): run_readiness_package=sample["run_readiness_package"], run_readiness_result=sample["run_readiness_result"], manual_fetch_receipt=sample["manual_fetch_receipt"], - phase="phase_139_market_intel_mcp_fetch_candidate_queue_writer_review_decision_approval_writer_preflight", + phase="phase_140_market_intel_professional_source_governance", ) assert receipt["mode"] == "mcp_fetch_run_receipt_review" - assert receipt["phase"] == "phase_139_market_intel_mcp_fetch_candidate_queue_writer_review_decision_approval_writer_preflight" + assert receipt["phase"] == "phase_140_market_intel_professional_source_governance" assert receipt["mcp_fetch_run_receipt_accepted"] is True assert receipt["run_receipt_ready"] is True assert receipt["operator_shell_fetch_receipt_received"] is True @@ -3276,13 +3296,13 @@ def test_mcp_fetch_run_receipt_route_get_and_post_are_preview_only(): assert get_response.status_code == 200 assert get_data["mode"] == "mcp_fetch_run_receipt_preview" - assert get_data["phase"] == "phase_139_market_intel_mcp_fetch_candidate_queue_writer_review_decision_approval_writer_preflight" + assert get_data["phase"] == "phase_140_market_intel_professional_source_governance" assert get_data["api_executes_cli"] is False assert get_data["api_writes_database"] is False assert get_data["api_uses_external_network"] is False assert post_response.status_code == 200 assert post_data["mode"] == "mcp_fetch_run_receipt_review" - assert post_data["phase"] == "phase_139_market_intel_mcp_fetch_candidate_queue_writer_review_decision_approval_writer_preflight" + assert post_data["phase"] == "phase_140_market_intel_professional_source_governance" assert post_data["mcp_fetch_run_receipt_accepted"] is True assert post_data["ready_for_manual_fetch_result_parser_review"] is True assert post_data["ready_for_api_database_write"] is False @@ -3298,11 +3318,11 @@ def test_mcp_fetch_run_receipt_route_get_and_post_are_preview_only(): def test_mcp_fetch_result_parser_review_preview_is_safe_without_payload(): parser = build_mcp_fetch_result_parser_review_preview( - phase="phase_139_market_intel_mcp_fetch_candidate_queue_writer_review_decision_approval_writer_preflight", + phase="phase_140_market_intel_professional_source_governance", ) assert parser["mode"] == "mcp_fetch_result_parser_review_preview" - assert parser["phase"] == "phase_139_market_intel_mcp_fetch_candidate_queue_writer_review_decision_approval_writer_preflight" + assert parser["phase"] == "phase_140_market_intel_professional_source_governance" assert parser["parser_payload_received"] is False assert parser["mcp_fetch_result_parser_review_accepted"] is False assert parser["result_parser_review_ready"] is False @@ -3341,11 +3361,11 @@ def test_mcp_fetch_result_parser_review_accepts_sample_result(): run_receipt_package=sample["run_receipt_package"], run_receipt_result=sample["run_receipt_result"], parser_result=sample["parser_result"], - phase="phase_139_market_intel_mcp_fetch_candidate_queue_writer_review_decision_approval_writer_preflight", + phase="phase_140_market_intel_professional_source_governance", ) assert parser["mode"] == "mcp_fetch_result_parser_review" - assert parser["phase"] == "phase_139_market_intel_mcp_fetch_candidate_queue_writer_review_decision_approval_writer_preflight" + assert parser["phase"] == "phase_140_market_intel_professional_source_governance" assert parser["mcp_fetch_result_parser_review_accepted"] is True assert parser["result_parser_review_ready"] is True assert parser["ready_for_manual_fetch_candidate_handoff_review"] is True @@ -3430,13 +3450,13 @@ def test_mcp_fetch_result_parser_review_route_get_and_post_are_preview_only(): assert get_response.status_code == 200 assert get_data["mode"] == "mcp_fetch_result_parser_review_preview" - assert get_data["phase"] == "phase_139_market_intel_mcp_fetch_candidate_queue_writer_review_decision_approval_writer_preflight" + assert get_data["phase"] == "phase_140_market_intel_professional_source_governance" assert get_data["api_executes_cli"] is False assert get_data["api_writes_database"] is False assert get_data["api_uses_external_network"] is False assert post_response.status_code == 200 assert post_data["mode"] == "mcp_fetch_result_parser_review" - assert post_data["phase"] == "phase_139_market_intel_mcp_fetch_candidate_queue_writer_review_decision_approval_writer_preflight" + assert post_data["phase"] == "phase_140_market_intel_professional_source_governance" assert post_data["mcp_fetch_result_parser_review_accepted"] is True assert post_data["ready_for_manual_fetch_candidate_handoff_review"] is True assert post_data["ready_for_api_database_write"] is False @@ -3452,11 +3472,11 @@ def test_mcp_fetch_result_parser_review_route_get_and_post_are_preview_only(): def test_mcp_fetch_candidate_handoff_review_preview_is_safe_without_payload(): handoff = build_mcp_fetch_candidate_handoff_review_preview( - phase="phase_139_market_intel_mcp_fetch_candidate_queue_writer_review_decision_approval_writer_preflight", + phase="phase_140_market_intel_professional_source_governance", ) assert handoff["mode"] == "mcp_fetch_candidate_handoff_review_preview" - assert handoff["phase"] == "phase_139_market_intel_mcp_fetch_candidate_queue_writer_review_decision_approval_writer_preflight" + assert handoff["phase"] == "phase_140_market_intel_professional_source_governance" assert handoff["candidate_handoff_payload_received"] is False assert handoff["mcp_fetch_candidate_handoff_review_accepted"] is False assert handoff["candidate_handoff_review_ready"] is False @@ -3495,11 +3515,11 @@ def test_mcp_fetch_candidate_handoff_review_accepts_sample_handoff(): parser_review_package=sample["parser_review_package"], parser_review_result=sample["parser_review_result"], candidate_handoff=sample["candidate_handoff"], - phase="phase_139_market_intel_mcp_fetch_candidate_queue_writer_review_decision_approval_writer_preflight", + phase="phase_140_market_intel_professional_source_governance", ) assert handoff["mode"] == "mcp_fetch_candidate_handoff_review" - assert handoff["phase"] == "phase_139_market_intel_mcp_fetch_candidate_queue_writer_review_decision_approval_writer_preflight" + assert handoff["phase"] == "phase_140_market_intel_professional_source_governance" assert handoff["mcp_fetch_candidate_handoff_review_accepted"] is True assert handoff["candidate_handoff_review_ready"] is True assert handoff["ready_for_manual_candidate_queue_review"] is True @@ -3589,13 +3609,13 @@ def test_mcp_fetch_candidate_handoff_review_route_get_and_post_are_preview_only( assert get_response.status_code == 200 assert get_data["mode"] == "mcp_fetch_candidate_handoff_review_preview" - assert get_data["phase"] == "phase_139_market_intel_mcp_fetch_candidate_queue_writer_review_decision_approval_writer_preflight" + assert get_data["phase"] == "phase_140_market_intel_professional_source_governance" assert get_data["api_executes_cli"] is False assert get_data["api_writes_database"] is False assert get_data["api_uses_external_network"] is False assert post_response.status_code == 200 assert post_data["mode"] == "mcp_fetch_candidate_handoff_review" - assert post_data["phase"] == "phase_139_market_intel_mcp_fetch_candidate_queue_writer_review_decision_approval_writer_preflight" + assert post_data["phase"] == "phase_140_market_intel_professional_source_governance" assert post_data["mcp_fetch_candidate_handoff_review_accepted"] is True assert post_data["ready_for_manual_candidate_queue_review"] is True assert post_data["ready_for_candidate_queue_writer_preflight"] is False @@ -3612,11 +3632,11 @@ def test_mcp_fetch_candidate_handoff_review_route_get_and_post_are_preview_only( def test_mcp_fetch_candidate_queue_review_preview_is_safe_without_payload(): review = build_mcp_fetch_candidate_queue_review_preview( - phase="phase_139_market_intel_mcp_fetch_candidate_queue_writer_review_decision_approval_writer_preflight", + phase="phase_140_market_intel_professional_source_governance", ) assert review["mode"] == "mcp_fetch_candidate_queue_review_preview" - assert review["phase"] == "phase_139_market_intel_mcp_fetch_candidate_queue_writer_review_decision_approval_writer_preflight" + assert review["phase"] == "phase_140_market_intel_professional_source_governance" assert review["candidate_queue_review_payload_received"] is False assert review["mcp_fetch_candidate_queue_review_accepted"] is False assert review["candidate_queue_review_ready"] is False @@ -3653,11 +3673,11 @@ def test_mcp_fetch_candidate_queue_review_accepts_sample_review(): handoff_review_package=sample["handoff_review_package"], handoff_review_result=sample["handoff_review_result"], candidate_queue_review=sample["candidate_queue_review"], - phase="phase_139_market_intel_mcp_fetch_candidate_queue_writer_review_decision_approval_writer_preflight", + phase="phase_140_market_intel_professional_source_governance", ) assert review["mode"] == "mcp_fetch_candidate_queue_review" - assert review["phase"] == "phase_139_market_intel_mcp_fetch_candidate_queue_writer_review_decision_approval_writer_preflight" + assert review["phase"] == "phase_140_market_intel_professional_source_governance" assert review["mcp_fetch_candidate_queue_review_accepted"] is True assert review["candidate_queue_review_ready"] is True assert review["ready_for_candidate_queue_writer_preflight"] is True @@ -3748,13 +3768,13 @@ def test_mcp_fetch_candidate_queue_review_route_get_and_post_are_preview_only(): assert get_response.status_code == 200 assert get_data["mode"] == "mcp_fetch_candidate_queue_review_preview" - assert get_data["phase"] == "phase_139_market_intel_mcp_fetch_candidate_queue_writer_review_decision_approval_writer_preflight" + assert get_data["phase"] == "phase_140_market_intel_professional_source_governance" assert get_data["api_executes_cli"] is False assert get_data["api_writes_database"] is False assert get_data["api_uses_external_network"] is False assert post_response.status_code == 200 assert post_data["mode"] == "mcp_fetch_candidate_queue_review" - assert post_data["phase"] == "phase_139_market_intel_mcp_fetch_candidate_queue_writer_review_decision_approval_writer_preflight" + assert post_data["phase"] == "phase_140_market_intel_professional_source_governance" assert post_data["mcp_fetch_candidate_queue_review_accepted"] is True assert post_data["ready_for_candidate_queue_writer_preflight"] is True assert post_data["ready_for_api_database_write"] is False @@ -3770,13 +3790,13 @@ def test_mcp_fetch_candidate_queue_review_route_get_and_post_are_preview_only(): def test_mcp_fetch_candidate_queue_writer_preflight_preview_is_safe_without_payload(): preflight = build_mcp_fetch_candidate_queue_writer_preflight_preview( - phase="phase_139_market_intel_mcp_fetch_candidate_queue_writer_review_decision_approval_writer_preflight", + phase="phase_140_market_intel_professional_source_governance", ) assert preflight["mode"] == "mcp_fetch_candidate_queue_writer_preflight_preview" assert ( preflight["phase"] - == "phase_139_market_intel_mcp_fetch_candidate_queue_writer_review_decision_approval_writer_preflight" + == "phase_140_market_intel_professional_source_governance" ) assert preflight["writer_preflight_payload_received"] is False assert preflight["mcp_fetch_candidate_queue_writer_preflight_accepted"] is False @@ -3814,13 +3834,13 @@ def test_mcp_fetch_candidate_queue_writer_preflight_accepts_sample_preflight(): queue_review_package=sample["queue_review_package"], queue_review_result=sample["queue_review_result"], writer_preflight=sample["writer_preflight"], - phase="phase_139_market_intel_mcp_fetch_candidate_queue_writer_review_decision_approval_writer_preflight", + phase="phase_140_market_intel_professional_source_governance", ) assert preflight["mode"] == "mcp_fetch_candidate_queue_writer_preflight" assert ( preflight["phase"] - == "phase_139_market_intel_mcp_fetch_candidate_queue_writer_review_decision_approval_writer_preflight" + == "phase_140_market_intel_professional_source_governance" ) assert preflight["mcp_fetch_candidate_queue_writer_preflight_accepted"] is True assert preflight["candidate_queue_writer_preflight_ready"] is True @@ -3924,7 +3944,7 @@ def test_mcp_fetch_candidate_queue_writer_preflight_route_get_and_post_preview_o assert get_data["mode"] == "mcp_fetch_candidate_queue_writer_preflight_preview" assert ( get_data["phase"] - == "phase_139_market_intel_mcp_fetch_candidate_queue_writer_review_decision_approval_writer_preflight" + == "phase_140_market_intel_professional_source_governance" ) assert get_data["api_executes_cli"] is False assert get_data["api_writes_database"] is False @@ -3933,7 +3953,7 @@ def test_mcp_fetch_candidate_queue_writer_preflight_route_get_and_post_preview_o assert post_data["mode"] == "mcp_fetch_candidate_queue_writer_preflight" assert ( post_data["phase"] - == "phase_139_market_intel_mcp_fetch_candidate_queue_writer_review_decision_approval_writer_preflight" + == "phase_140_market_intel_professional_source_governance" ) assert post_data["mcp_fetch_candidate_queue_writer_preflight_accepted"] is True assert post_data["ready_for_candidate_queue_writer_cli_review"] is True @@ -3949,13 +3969,13 @@ def test_mcp_fetch_candidate_queue_writer_preflight_route_get_and_post_preview_o def test_mcp_fetch_candidate_queue_writer_cli_review_preview_is_safe_without_payload(): review = build_mcp_fetch_candidate_queue_writer_cli_review_preview( - phase="phase_139_market_intel_mcp_fetch_candidate_queue_writer_review_decision_approval_writer_preflight", + phase="phase_140_market_intel_professional_source_governance", ) assert review["mode"] == "mcp_fetch_candidate_queue_writer_cli_review_preview" assert ( review["phase"] - == "phase_139_market_intel_mcp_fetch_candidate_queue_writer_review_decision_approval_writer_preflight" + == "phase_140_market_intel_professional_source_governance" ) assert review["writer_cli_review_payload_received"] is False assert review["mcp_fetch_candidate_queue_writer_cli_review_accepted"] is False @@ -3991,13 +4011,13 @@ def test_mcp_fetch_candidate_queue_writer_cli_review_accepts_sample_review(): writer_preflight_package=sample["writer_preflight_package"], writer_preflight_result=sample["writer_preflight_result"], writer_cli_review=sample["writer_cli_review"], - phase="phase_139_market_intel_mcp_fetch_candidate_queue_writer_review_decision_approval_writer_preflight", + phase="phase_140_market_intel_professional_source_governance", ) assert review["mode"] == "mcp_fetch_candidate_queue_writer_cli_review" assert ( review["phase"] - == "phase_139_market_intel_mcp_fetch_candidate_queue_writer_review_decision_approval_writer_preflight" + == "phase_140_market_intel_professional_source_governance" ) assert review["mcp_fetch_candidate_queue_writer_cli_review_accepted"] is True assert review["candidate_queue_writer_cli_review_ready"] is True @@ -4098,7 +4118,7 @@ def test_mcp_fetch_candidate_queue_writer_cli_review_route_get_and_post_preview_ assert get_data["mode"] == "mcp_fetch_candidate_queue_writer_cli_review_preview" assert ( get_data["phase"] - == "phase_139_market_intel_mcp_fetch_candidate_queue_writer_review_decision_approval_writer_preflight" + == "phase_140_market_intel_professional_source_governance" ) assert get_data["api_executes_cli"] is False assert get_data["api_reads_approval_token"] is False @@ -4107,7 +4127,7 @@ def test_mcp_fetch_candidate_queue_writer_cli_review_route_get_and_post_preview_ assert post_data["mode"] == "mcp_fetch_candidate_queue_writer_cli_review" assert ( post_data["phase"] - == "phase_139_market_intel_mcp_fetch_candidate_queue_writer_review_decision_approval_writer_preflight" + == "phase_140_market_intel_professional_source_governance" ) assert post_data["mcp_fetch_candidate_queue_writer_cli_review_accepted"] is True assert post_data["ready_for_candidate_queue_writer_run_package_review"] is True @@ -4124,7 +4144,7 @@ def test_mcp_fetch_candidate_queue_writer_cli_review_route_get_and_post_preview_ def test_mcp_fetch_candidate_queue_writer_run_package_review_preview_is_safe_without_payload(): review = build_mcp_fetch_candidate_queue_writer_run_package_review_preview( - phase="phase_139_market_intel_mcp_fetch_candidate_queue_writer_review_decision_approval_writer_preflight", + phase="phase_140_market_intel_professional_source_governance", ) assert review["mode"] == ( @@ -4132,7 +4152,7 @@ def test_mcp_fetch_candidate_queue_writer_run_package_review_preview_is_safe_wit ) assert ( review["phase"] - == "phase_139_market_intel_mcp_fetch_candidate_queue_writer_review_decision_approval_writer_preflight" + == "phase_140_market_intel_professional_source_governance" ) assert review["writer_run_package_review_payload_received"] is False assert ( @@ -4182,13 +4202,13 @@ def test_mcp_fetch_candidate_queue_writer_run_package_review_accepts_sample_revi writer_cli_review_package=sample["writer_cli_review_package"], writer_cli_review_result=sample["writer_cli_review_result"], writer_run_package_review=sample["writer_run_package_review"], - phase="phase_139_market_intel_mcp_fetch_candidate_queue_writer_review_decision_approval_writer_preflight", + phase="phase_140_market_intel_professional_source_governance", ) assert review["mode"] == "mcp_fetch_candidate_queue_writer_run_package_review" assert ( review["phase"] - == "phase_139_market_intel_mcp_fetch_candidate_queue_writer_review_decision_approval_writer_preflight" + == "phase_140_market_intel_professional_source_governance" ) assert ( review["mcp_fetch_candidate_queue_writer_run_package_review_accepted"] @@ -4312,7 +4332,7 @@ def test_mcp_fetch_candidate_queue_writer_run_package_review_route_get_and_post_ ) assert ( get_data["phase"] - == "phase_139_market_intel_mcp_fetch_candidate_queue_writer_review_decision_approval_writer_preflight" + == "phase_140_market_intel_professional_source_governance" ) assert get_data["api_executes_cli"] is False assert get_data["api_reads_approval_token"] is False @@ -4324,7 +4344,7 @@ def test_mcp_fetch_candidate_queue_writer_run_package_review_route_get_and_post_ ) assert ( post_data["phase"] - == "phase_139_market_intel_mcp_fetch_candidate_queue_writer_review_decision_approval_writer_preflight" + == "phase_140_market_intel_professional_source_governance" ) assert ( post_data["mcp_fetch_candidate_queue_writer_run_package_review_accepted"] @@ -4344,7 +4364,7 @@ def test_mcp_fetch_candidate_queue_writer_run_package_review_route_get_and_post_ def test_mcp_fetch_candidate_queue_writer_run_readiness_preview_is_safe_without_payload(): readiness = build_mcp_fetch_candidate_queue_writer_run_readiness_preview( - phase="phase_139_market_intel_mcp_fetch_candidate_queue_writer_review_decision_approval_writer_preflight", + phase="phase_140_market_intel_professional_source_governance", ) assert readiness["mode"] == ( @@ -4352,7 +4372,7 @@ def test_mcp_fetch_candidate_queue_writer_run_readiness_preview_is_safe_without_ ) assert ( readiness["phase"] - == "phase_139_market_intel_mcp_fetch_candidate_queue_writer_review_decision_approval_writer_preflight" + == "phase_140_market_intel_professional_source_governance" ) assert readiness["writer_run_readiness_payload_received"] is False assert ( @@ -4398,13 +4418,13 @@ def test_mcp_fetch_candidate_queue_writer_run_readiness_accepts_sample_review(): ], writer_run_package_review_result=sample["writer_run_package_review_result"], operator_readiness=sample["operator_readiness"], - phase="phase_139_market_intel_mcp_fetch_candidate_queue_writer_review_decision_approval_writer_preflight", + phase="phase_140_market_intel_professional_source_governance", ) assert readiness["mode"] == "mcp_fetch_candidate_queue_writer_run_readiness_review" assert ( readiness["phase"] - == "phase_139_market_intel_mcp_fetch_candidate_queue_writer_review_decision_approval_writer_preflight" + == "phase_140_market_intel_professional_source_governance" ) assert ( readiness["mcp_fetch_candidate_queue_writer_run_readiness_accepted"] @@ -4520,7 +4540,7 @@ def test_mcp_fetch_candidate_queue_writer_run_readiness_route_get_and_post_previ ) assert ( get_data["phase"] - == "phase_139_market_intel_mcp_fetch_candidate_queue_writer_review_decision_approval_writer_preflight" + == "phase_140_market_intel_professional_source_governance" ) assert get_data["api_executes_cli"] is False assert get_data["api_reads_approval_token"] is False @@ -4532,7 +4552,7 @@ def test_mcp_fetch_candidate_queue_writer_run_readiness_route_get_and_post_previ ) assert ( post_data["phase"] - == "phase_139_market_intel_mcp_fetch_candidate_queue_writer_review_decision_approval_writer_preflight" + == "phase_140_market_intel_professional_source_governance" ) assert ( post_data["mcp_fetch_candidate_queue_writer_run_readiness_accepted"] @@ -4552,7 +4572,7 @@ def test_mcp_fetch_candidate_queue_writer_run_readiness_route_get_and_post_previ def test_mcp_fetch_candidate_queue_writer_run_receipt_review_preview_is_safe_without_payload(): receipt = build_mcp_fetch_candidate_queue_writer_run_receipt_review_preview( - phase="phase_139_market_intel_mcp_fetch_candidate_queue_writer_review_decision_approval_writer_preflight", + phase="phase_140_market_intel_professional_source_governance", ) assert receipt["mode"] == ( @@ -4560,7 +4580,7 @@ def test_mcp_fetch_candidate_queue_writer_run_receipt_review_preview_is_safe_wit ) assert ( receipt["phase"] - == "phase_139_market_intel_mcp_fetch_candidate_queue_writer_review_decision_approval_writer_preflight" + == "phase_140_market_intel_professional_source_governance" ) assert receipt["writer_run_receipt_review_payload_received"] is False assert ( @@ -4605,13 +4625,13 @@ def test_mcp_fetch_candidate_queue_writer_run_receipt_review_accepts_sample_revi writer_run_readiness_package=sample["writer_run_readiness_package"], writer_run_readiness_result=sample["writer_run_readiness_result"], writer_run_receipt=sample["writer_run_receipt"], - phase="phase_139_market_intel_mcp_fetch_candidate_queue_writer_review_decision_approval_writer_preflight", + phase="phase_140_market_intel_professional_source_governance", ) assert receipt["mode"] == "mcp_fetch_candidate_queue_writer_run_receipt_review" assert ( receipt["phase"] - == "phase_139_market_intel_mcp_fetch_candidate_queue_writer_review_decision_approval_writer_preflight" + == "phase_140_market_intel_professional_source_governance" ) assert ( receipt["mcp_fetch_candidate_queue_writer_run_receipt_review_accepted"] @@ -4743,7 +4763,7 @@ def test_mcp_fetch_candidate_queue_writer_run_receipt_review_route_get_and_post_ ) assert ( get_data["phase"] - == "phase_139_market_intel_mcp_fetch_candidate_queue_writer_review_decision_approval_writer_preflight" + == "phase_140_market_intel_professional_source_governance" ) assert get_data["api_executes_cli"] is False assert get_data["api_reads_approval_token"] is False @@ -4755,7 +4775,7 @@ def test_mcp_fetch_candidate_queue_writer_run_receipt_review_route_get_and_post_ ) assert ( post_data["phase"] - == "phase_139_market_intel_mcp_fetch_candidate_queue_writer_review_decision_approval_writer_preflight" + == "phase_140_market_intel_professional_source_governance" ) assert ( post_data["mcp_fetch_candidate_queue_writer_run_receipt_review_accepted"] @@ -4775,7 +4795,7 @@ def test_mcp_fetch_candidate_queue_writer_run_receipt_review_route_get_and_post_ def test_mcp_fetch_candidate_queue_writer_run_closeout_review_preview_is_safe_without_payload(): closeout = build_mcp_fetch_candidate_queue_writer_run_closeout_review_preview( - phase="phase_139_market_intel_mcp_fetch_candidate_queue_writer_review_decision_approval_writer_preflight", + phase="phase_140_market_intel_professional_source_governance", ) assert closeout["mode"] == ( @@ -4783,7 +4803,7 @@ def test_mcp_fetch_candidate_queue_writer_run_closeout_review_preview_is_safe_wi ) assert ( closeout["phase"] - == "phase_139_market_intel_mcp_fetch_candidate_queue_writer_review_decision_approval_writer_preflight" + == "phase_140_market_intel_professional_source_governance" ) assert closeout["writer_run_closeout_review_payload_received"] is False assert ( @@ -4836,13 +4856,13 @@ def test_mcp_fetch_candidate_queue_writer_run_closeout_review_accepts_sample_rev ], writer_run_receipt_review_result=sample["writer_run_receipt_review_result"], operator_closeout=sample["operator_closeout"], - phase="phase_139_market_intel_mcp_fetch_candidate_queue_writer_review_decision_approval_writer_preflight", + phase="phase_140_market_intel_professional_source_governance", ) assert closeout["mode"] == "mcp_fetch_candidate_queue_writer_run_closeout_review" assert ( closeout["phase"] - == "phase_139_market_intel_mcp_fetch_candidate_queue_writer_review_decision_approval_writer_preflight" + == "phase_140_market_intel_professional_source_governance" ) assert ( closeout["mcp_fetch_candidate_queue_writer_run_closeout_review_accepted"] @@ -4983,7 +5003,7 @@ def test_mcp_fetch_candidate_queue_writer_run_closeout_review_route_get_and_post ) assert ( get_data["phase"] - == "phase_139_market_intel_mcp_fetch_candidate_queue_writer_review_decision_approval_writer_preflight" + == "phase_140_market_intel_professional_source_governance" ) assert get_data["api_executes_cli"] is False assert get_data["api_reads_approval_token"] is False @@ -4995,7 +5015,7 @@ def test_mcp_fetch_candidate_queue_writer_run_closeout_review_route_get_and_post ) assert ( post_data["phase"] - == "phase_139_market_intel_mcp_fetch_candidate_queue_writer_review_decision_approval_writer_preflight" + == "phase_140_market_intel_professional_source_governance" ) assert ( post_data["mcp_fetch_candidate_queue_writer_run_closeout_review_accepted"] @@ -5019,7 +5039,7 @@ def test_mcp_fetch_candidate_queue_writer_run_closeout_review_route_get_and_post def test_mcp_fetch_candidate_queue_writer_post_closeout_inventory_review_preview_is_safe_without_payload(): inventory = ( build_mcp_fetch_candidate_queue_writer_post_closeout_inventory_review_preview( - phase="phase_139_market_intel_mcp_fetch_candidate_queue_writer_review_decision_approval_writer_preflight", + phase="phase_140_market_intel_professional_source_governance", ) ) @@ -5028,7 +5048,7 @@ def test_mcp_fetch_candidate_queue_writer_post_closeout_inventory_review_preview ) assert ( inventory["phase"] - == "phase_139_market_intel_mcp_fetch_candidate_queue_writer_review_decision_approval_writer_preflight" + == "phase_140_market_intel_professional_source_governance" ) assert inventory["post_closeout_inventory_review_payload_received"] is False assert ( @@ -5084,7 +5104,7 @@ def test_mcp_fetch_candidate_queue_writer_post_closeout_inventory_review_accepts "writer_run_closeout_review_result" ], operator_inventory=sample["operator_inventory"], - phase="phase_139_market_intel_mcp_fetch_candidate_queue_writer_review_decision_approval_writer_preflight", + phase="phase_140_market_intel_professional_source_governance", ) ) @@ -5094,7 +5114,7 @@ def test_mcp_fetch_candidate_queue_writer_post_closeout_inventory_review_accepts ) assert ( inventory["phase"] - == "phase_139_market_intel_mcp_fetch_candidate_queue_writer_review_decision_approval_writer_preflight" + == "phase_140_market_intel_professional_source_governance" ) assert ( inventory[ @@ -5244,7 +5264,7 @@ def test_mcp_fetch_candidate_queue_writer_post_closeout_inventory_review_route_g ) assert ( get_data["phase"] - == "phase_139_market_intel_mcp_fetch_candidate_queue_writer_review_decision_approval_writer_preflight" + == "phase_140_market_intel_professional_source_governance" ) assert get_data["api_executes_cli"] is False assert get_data["api_reads_approval_token"] is False @@ -5257,7 +5277,7 @@ def test_mcp_fetch_candidate_queue_writer_post_closeout_inventory_review_route_g ) assert ( post_data["phase"] - == "phase_139_market_intel_mcp_fetch_candidate_queue_writer_review_decision_approval_writer_preflight" + == "phase_140_market_intel_professional_source_governance" ) assert ( post_data[ @@ -5280,7 +5300,7 @@ def test_mcp_fetch_candidate_queue_writer_post_closeout_inventory_review_route_g def test_mcp_fetch_candidate_queue_writer_review_handoff_preview_is_safe_without_payload(): handoff = build_mcp_fetch_candidate_queue_writer_review_handoff_preview( - phase="phase_139_market_intel_mcp_fetch_candidate_queue_writer_review_decision_approval_writer_preflight", + phase="phase_140_market_intel_professional_source_governance", ) assert handoff["mode"] == ( @@ -5288,7 +5308,7 @@ def test_mcp_fetch_candidate_queue_writer_review_handoff_preview_is_safe_without ) assert ( handoff["phase"] - == "phase_139_market_intel_mcp_fetch_candidate_queue_writer_review_decision_approval_writer_preflight" + == "phase_140_market_intel_professional_source_governance" ) assert handoff["review_handoff_payload_received"] is False assert ( @@ -5336,13 +5356,13 @@ def test_mcp_fetch_candidate_queue_writer_review_handoff_accepts_sample_review() "writer_post_closeout_inventory_review_result" ], operator_handoff=sample["operator_handoff"], - phase="phase_139_market_intel_mcp_fetch_candidate_queue_writer_review_decision_approval_writer_preflight", + phase="phase_140_market_intel_professional_source_governance", ) assert handoff["mode"] == "mcp_fetch_candidate_queue_writer_review_handoff" assert ( handoff["phase"] - == "phase_139_market_intel_mcp_fetch_candidate_queue_writer_review_decision_approval_writer_preflight" + == "phase_140_market_intel_professional_source_governance" ) assert ( handoff["mcp_fetch_candidate_queue_writer_review_handoff_accepted"] @@ -5500,7 +5520,7 @@ def test_mcp_fetch_candidate_queue_writer_review_handoff_route_get_and_post_prev ) assert ( get_data["phase"] - == "phase_139_market_intel_mcp_fetch_candidate_queue_writer_review_decision_approval_writer_preflight" + == "phase_140_market_intel_professional_source_governance" ) assert get_data["api_executes_cli"] is False assert get_data["api_reads_approval_token"] is False @@ -5510,7 +5530,7 @@ def test_mcp_fetch_candidate_queue_writer_review_handoff_route_get_and_post_prev assert post_data["mode"] == "mcp_fetch_candidate_queue_writer_review_handoff" assert ( post_data["phase"] - == "phase_139_market_intel_mcp_fetch_candidate_queue_writer_review_decision_approval_writer_preflight" + == "phase_140_market_intel_professional_source_governance" ) assert ( post_data["mcp_fetch_candidate_queue_writer_review_handoff_accepted"] @@ -5533,7 +5553,7 @@ def test_mcp_fetch_candidate_queue_writer_review_handoff_route_get_and_post_prev def test_mcp_fetch_candidate_queue_writer_review_inventory_preview_is_safe_without_payload(): inventory = build_mcp_fetch_candidate_queue_writer_review_inventory_preview( - phase="phase_139_market_intel_mcp_fetch_candidate_queue_writer_review_decision_approval_writer_preflight", + phase="phase_140_market_intel_professional_source_governance", ) assert inventory["mode"] == ( @@ -5541,7 +5561,7 @@ def test_mcp_fetch_candidate_queue_writer_review_inventory_preview_is_safe_witho ) assert ( inventory["phase"] - == "phase_139_market_intel_mcp_fetch_candidate_queue_writer_review_decision_approval_writer_preflight" + == "phase_140_market_intel_professional_source_governance" ) assert inventory["review_inventory_payload_received"] is False assert ( @@ -5585,13 +5605,13 @@ def test_mcp_fetch_candidate_queue_writer_review_inventory_accepts_sample_invent writer_review_handoff_package=sample["writer_review_handoff_package"], writer_review_handoff_result=sample["writer_review_handoff_result"], operator_review_inventory=sample["operator_review_inventory"], - phase="phase_139_market_intel_mcp_fetch_candidate_queue_writer_review_decision_approval_writer_preflight", + phase="phase_140_market_intel_professional_source_governance", ) assert inventory["mode"] == "mcp_fetch_candidate_queue_writer_review_inventory" assert ( inventory["phase"] - == "phase_139_market_intel_mcp_fetch_candidate_queue_writer_review_decision_approval_writer_preflight" + == "phase_140_market_intel_professional_source_governance" ) assert ( inventory["mcp_fetch_candidate_queue_writer_review_inventory_accepted"] @@ -5727,7 +5747,7 @@ def test_mcp_fetch_candidate_queue_writer_review_inventory_route_get_and_post_pr ) assert ( get_data["phase"] - == "phase_139_market_intel_mcp_fetch_candidate_queue_writer_review_decision_approval_writer_preflight" + == "phase_140_market_intel_professional_source_governance" ) assert get_data["api_executes_cli"] is False assert get_data["api_reads_approval_token"] is False @@ -5737,7 +5757,7 @@ def test_mcp_fetch_candidate_queue_writer_review_inventory_route_get_and_post_pr assert post_data["mode"] == "mcp_fetch_candidate_queue_writer_review_inventory" assert ( post_data["phase"] - == "phase_139_market_intel_mcp_fetch_candidate_queue_writer_review_decision_approval_writer_preflight" + == "phase_140_market_intel_professional_source_governance" ) assert ( post_data["mcp_fetch_candidate_queue_writer_review_inventory_accepted"] @@ -5914,7 +5934,7 @@ def test_mcp_fetch_candidate_queue_writer_review_inventory_route_get_and_post_pr ) assert ( get_data["phase"] - == "phase_139_market_intel_mcp_fetch_candidate_queue_writer_review_decision_approval_writer_preflight" + == "phase_140_market_intel_professional_source_governance" ) assert get_data["api_executes_cli"] is False assert get_data["api_reads_approval_token"] is False @@ -5924,7 +5944,7 @@ def test_mcp_fetch_candidate_queue_writer_review_inventory_route_get_and_post_pr assert post_data["mode"] == "mcp_fetch_candidate_queue_writer_review_inventory" assert ( post_data["phase"] - == "phase_139_market_intel_mcp_fetch_candidate_queue_writer_review_decision_approval_writer_preflight" + == "phase_140_market_intel_professional_source_governance" ) assert ( post_data["mcp_fetch_candidate_queue_writer_review_inventory_accepted"] @@ -6167,7 +6187,7 @@ def test_mcp_fetch_candidate_queue_writer_review_decision_route_get_and_post_pre ) assert ( get_data["phase"] - == "phase_139_market_intel_mcp_fetch_candidate_queue_writer_review_decision_approval_writer_preflight" + == "phase_140_market_intel_professional_source_governance" ) assert get_data["api_executes_cli"] is False assert get_data["api_reads_approval_token"] is False @@ -6177,7 +6197,7 @@ def test_mcp_fetch_candidate_queue_writer_review_decision_route_get_and_post_pre assert post_data["mode"] == "mcp_fetch_candidate_queue_writer_review_decision" assert ( post_data["phase"] - == "phase_139_market_intel_mcp_fetch_candidate_queue_writer_review_decision_approval_writer_preflight" + == "phase_140_market_intel_professional_source_governance" ) assert ( post_data["mcp_fetch_candidate_queue_writer_review_decision_accepted"] @@ -6428,7 +6448,7 @@ def test_mcp_fetch_candidate_queue_writer_review_decision_approval_route_get_and ) assert ( get_data["phase"] - == "phase_139_market_intel_mcp_fetch_candidate_queue_writer_review_decision_approval_writer_preflight" + == "phase_140_market_intel_professional_source_governance" ) assert get_data["api_executes_cli"] is False assert get_data["api_reads_approval_token"] is False @@ -6441,7 +6461,7 @@ def test_mcp_fetch_candidate_queue_writer_review_decision_approval_route_get_and ) assert ( post_data["phase"] - == "phase_139_market_intel_mcp_fetch_candidate_queue_writer_review_decision_approval_writer_preflight" + == "phase_140_market_intel_professional_source_governance" ) assert ( post_data[ @@ -6737,7 +6757,7 @@ def test_mcp_fetch_candidate_queue_writer_review_decision_approval_writer_prefli ) assert ( get_data["phase"] - == "phase_139_market_intel_mcp_fetch_candidate_queue_writer_review_decision_approval_writer_preflight" + == "phase_140_market_intel_professional_source_governance" ) assert get_data["api_executes_cli"] is False assert get_data["api_reads_approval_token"] is False @@ -6749,7 +6769,7 @@ def test_mcp_fetch_candidate_queue_writer_review_decision_approval_writer_prefli ) assert ( post_data["phase"] - == "phase_139_market_intel_mcp_fetch_candidate_queue_writer_review_decision_approval_writer_preflight" + == "phase_140_market_intel_professional_source_governance" ) assert ( post_data[ @@ -6776,7 +6796,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_139_market_intel_mcp_fetch_candidate_queue_writer_review_decision_approval_writer_preflight" + assert plan["phase"] == "phase_140_market_intel_professional_source_governance" assert plan["ready_for_manual_sample_fetch"] is False assert plan["sample_fetch_executed"] is False assert plan["external_network_executed"] is False @@ -6824,7 +6844,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_139_market_intel_mcp_fetch_candidate_queue_writer_review_decision_approval_writer_preflight" + assert data["phase"] == "phase_140_market_intel_professional_source_governance" assert data["sample_fetch_executed"] is False assert data["external_network_executed"] is False assert data["database_write_executed"] is False @@ -6835,7 +6855,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_139_market_intel_mcp_fetch_candidate_queue_writer_review_decision_approval_writer_preflight" + assert acceptance["phase"] == "phase_140_market_intel_professional_source_governance" assert acceptance["contract_ready"] is True assert acceptance["sample_result_loaded"] is False assert acceptance["sample_result_accepted"] is False @@ -6877,7 +6897,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_139_market_intel_mcp_fetch_candidate_queue_writer_review_decision_approval_writer_preflight" + assert data["phase"] == "phase_140_market_intel_professional_source_governance" assert data["sample_result_loaded"] is False assert data["candidate_import_allowed"] is False assert data["external_network_executed"] is False @@ -6889,7 +6909,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_139_market_intel_mcp_fetch_candidate_queue_writer_review_decision_approval_writer_preflight" + assert review["phase"] == "phase_140_market_intel_professional_source_governance" assert review["contract_ready"] is True assert review["sample_result_loaded"] is False assert review["sample_result_reviewed"] is False @@ -7000,7 +7020,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_139_market_intel_mcp_fetch_candidate_queue_writer_review_decision_approval_writer_preflight" + assert data["phase"] == "phase_140_market_intel_professional_source_governance" assert data["sample_result_loaded"] is False assert data["sample_result_reviewed"] is False assert data["candidate_import_allowed"] is False @@ -7039,7 +7059,7 @@ def test_manual_sample_review_evaluation_preview_accepts_payload_without_persist ) assert review["mode"] == "manual_sample_review_evaluation_preview" - assert review["phase"] == "phase_139_market_intel_mcp_fetch_candidate_queue_writer_review_decision_approval_writer_preflight" + assert review["phase"] == "phase_140_market_intel_professional_source_governance" assert review["review_request_type"] == "operator_posted_json" assert review["payload_received"] is True assert review["payload_valid_json_object"] is True @@ -7101,7 +7121,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_139_market_intel_mcp_fetch_candidate_queue_writer_review_decision_approval_writer_preflight" + assert data["phase"] == "phase_140_market_intel_professional_source_governance" assert data["payload_received"] is True assert data["payload_valid_json_object"] is True assert data["payload_persisted"] is False @@ -7181,7 +7201,7 @@ def test_manual_sample_candidate_handoff_preview_creates_candidates_without_pers ) assert handoff["mode"] == "manual_sample_candidate_handoff_preview" - assert handoff["phase"] == "phase_139_market_intel_mcp_fetch_candidate_queue_writer_review_decision_approval_writer_preflight" + assert handoff["phase"] == "phase_140_market_intel_professional_source_governance" assert handoff["payload_received"] is True assert handoff["payload_valid_json_object"] is True assert handoff["payload_persisted"] is False @@ -7245,7 +7265,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_139_market_intel_mcp_fetch_candidate_queue_writer_review_decision_approval_writer_preflight" + assert data["phase"] == "phase_140_market_intel_professional_source_governance" assert data["payload_received"] is True assert data["handoff_ready"] is True assert data["candidate_handoff_created"] is True @@ -7304,7 +7324,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_139_market_intel_mcp_fetch_candidate_queue_writer_review_decision_approval_writer_preflight" + assert queue_draft["phase"] == "phase_140_market_intel_professional_source_governance" assert queue_draft["payload_received"] is True assert queue_draft["payload_valid_json_object"] is True assert queue_draft["payload_persisted"] is False @@ -7378,7 +7398,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_139_market_intel_mcp_fetch_candidate_queue_writer_review_decision_approval_writer_preflight" + assert data["phase"] == "phase_140_market_intel_professional_source_governance" assert data["payload_received"] is True assert data["handoff_ready"] is True assert data["queue_draft_ready"] is True @@ -7441,7 +7461,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_139_market_intel_mcp_fetch_candidate_queue_writer_review_decision_approval_writer_preflight" + assert approval["phase"] == "phase_140_market_intel_professional_source_governance" assert approval["payload_received"] is True assert approval["payload_valid_json_object"] is True assert approval["payload_persisted"] is False @@ -7519,7 +7539,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_139_market_intel_mcp_fetch_candidate_queue_writer_review_decision_approval_writer_preflight" + assert data["phase"] == "phase_140_market_intel_professional_source_governance" assert data["payload_received"] is True assert data["approval_preview_created"] is True assert data["approval_request_created"] is False @@ -7582,7 +7602,7 @@ def test_manual_sample_candidate_queue_transaction_preview_blocks_execution(): ) assert transaction["mode"] == "manual_sample_candidate_queue_transaction_preview" - assert transaction["phase"] == "phase_139_market_intel_mcp_fetch_candidate_queue_writer_review_decision_approval_writer_preflight" + assert transaction["phase"] == "phase_140_market_intel_professional_source_governance" assert transaction["payload_received"] is True assert transaction["payload_valid_json_object"] is True assert transaction["payload_persisted"] is False @@ -7662,7 +7682,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_139_market_intel_mcp_fetch_candidate_queue_writer_review_decision_approval_writer_preflight" + assert data["phase"] == "phase_140_market_intel_professional_source_governance" assert data["payload_received"] is True assert data["transaction_preview_created"] is True assert data["transaction_ready"] is False @@ -13373,7 +13393,7 @@ def test_candidate_queue_review_ai_summary_persistence_telegram_dispatch_report_ "candidate_queue_review_ai_summary_persistence_telegram_dispatch_report_input_preview" ) assert data["phase"] == ( - "phase_139_market_intel_mcp_fetch_candidate_queue_writer_review_decision_approval_writer_preflight" + "phase_140_market_intel_professional_source_governance" ) assert data["telegram_dispatch_report_input_ready"] is False assert data["summary_persistence_telegram_dispatch_report_input_ready"] is False @@ -13448,7 +13468,7 @@ def test_candidate_queue_review_ai_summary_persistence_telegram_dispatch_archive "candidate_queue_review_ai_summary_persistence_telegram_dispatch_archive_summary_preview" ) assert data["phase"] == ( - "phase_139_market_intel_mcp_fetch_candidate_queue_writer_review_decision_approval_writer_preflight" + "phase_140_market_intel_professional_source_governance" ) assert data["telegram_dispatch_archive_summary_ready"] is False assert ( @@ -13720,7 +13740,7 @@ def test_candidate_queue_review_ai_summary_persistence_telegram_dispatch_report_ "candidate_queue_review_ai_summary_persistence_telegram_dispatch_report_input_preview" ) assert data["phase"] == ( - "phase_139_market_intel_mcp_fetch_candidate_queue_writer_review_decision_approval_writer_preflight" + "phase_140_market_intel_professional_source_governance" ) assert data["telegram_dispatch_report_input_ready"] is False assert data["summary_persistence_telegram_dispatch_report_input_ready"] is False @@ -14008,7 +14028,7 @@ def test_candidate_queue_review_ai_summary_persistence_telegram_dispatch_report_ "candidate_queue_review_ai_summary_persistence_telegram_dispatch_report_run_package_preview" ) assert data["phase"] == ( - "phase_139_market_intel_mcp_fetch_candidate_queue_writer_review_decision_approval_writer_preflight" + "phase_140_market_intel_professional_source_governance" ) assert data["telegram_dispatch_report_run_package_ready"] is False assert ( @@ -14318,7 +14338,7 @@ def test_candidate_queue_review_ai_summary_persistence_telegram_dispatch_report_ "candidate_queue_review_ai_summary_persistence_telegram_dispatch_report_run_readiness_preview" ) assert data["phase"] == ( - "phase_139_market_intel_mcp_fetch_candidate_queue_writer_review_decision_approval_writer_preflight" + "phase_140_market_intel_professional_source_governance" ) assert data["telegram_dispatch_report_run_readiness_ready"] is False assert ( @@ -14621,7 +14641,7 @@ def test_candidate_queue_review_ai_summary_persistence_telegram_dispatch_report_ "candidate_queue_review_ai_summary_persistence_telegram_dispatch_report_run_receipt_preview" ) assert data["phase"] == ( - "phase_139_market_intel_mcp_fetch_candidate_queue_writer_review_decision_approval_writer_preflight" + "phase_140_market_intel_professional_source_governance" ) assert data["telegram_dispatch_report_run_receipt_passed"] is False assert ( @@ -14880,7 +14900,7 @@ def test_candidate_queue_review_ai_summary_persistence_telegram_dispatch_report_ "candidate_queue_review_ai_summary_persistence_telegram_dispatch_report_closeout_preview" ) assert data["phase"] == ( - "phase_139_market_intel_mcp_fetch_candidate_queue_writer_review_decision_approval_writer_preflight" + "phase_140_market_intel_professional_source_governance" ) assert data["telegram_dispatch_report_closeout_passed"] is False assert ( @@ -15153,7 +15173,7 @@ def test_candidate_queue_review_ai_summary_persistence_telegram_dispatch_report_ "candidate_queue_review_ai_summary_persistence_telegram_dispatch_report_archive_preview" ) assert data["phase"] == ( - "phase_139_market_intel_mcp_fetch_candidate_queue_writer_review_decision_approval_writer_preflight" + "phase_140_market_intel_professional_source_governance" ) assert data["telegram_dispatch_report_archive_passed"] is False assert ( @@ -15401,7 +15421,7 @@ def test_candidate_queue_review_ai_summary_persistence_telegram_dispatch_report_ "candidate_queue_review_ai_summary_persistence_telegram_dispatch_report_archive_summary_preview" ) assert data["phase"] == ( - "phase_139_market_intel_mcp_fetch_candidate_queue_writer_review_decision_approval_writer_preflight" + "phase_140_market_intel_professional_source_governance" ) assert data["telegram_dispatch_report_archive_summary_passed"] is False assert ( @@ -15631,7 +15651,7 @@ def test_candidate_queue_review_ai_summary_persistence_telegram_dispatch_report_ "candidate_queue_review_ai_summary_persistence_telegram_dispatch_report_catalog_handoff_preview" ) assert data["phase"] == ( - "phase_139_market_intel_mcp_fetch_candidate_queue_writer_review_decision_approval_writer_preflight" + "phase_140_market_intel_professional_source_governance" ) assert data["telegram_dispatch_report_catalog_handoff_passed"] is False assert data["summary_persistence_telegram_dispatch_report_catalog_handoff_passed"] is False @@ -15868,7 +15888,7 @@ def test_candidate_queue_review_ai_summary_persistence_telegram_dispatch_report_ "candidate_queue_review_ai_summary_persistence_telegram_dispatch_report_catalog_index_preview" ) assert data["phase"] == ( - "phase_139_market_intel_mcp_fetch_candidate_queue_writer_review_decision_approval_writer_preflight" + "phase_140_market_intel_professional_source_governance" ) assert data["telegram_dispatch_report_catalog_index_passed"] is False assert data["summary_persistence_telegram_dispatch_report_catalog_index_passed"] is False @@ -16110,7 +16130,7 @@ def test_candidate_queue_review_ai_summary_persistence_telegram_dispatch_report_ "candidate_queue_review_ai_summary_persistence_telegram_dispatch_report_catalog_write_preflight_preview" ) assert data["phase"] == ( - "phase_139_market_intel_mcp_fetch_candidate_queue_writer_review_decision_approval_writer_preflight" + "phase_140_market_intel_professional_source_governance" ) assert data["telegram_dispatch_report_catalog_write_preflight_passed"] is False assert ( @@ -16386,7 +16406,7 @@ def test_candidate_queue_review_ai_summary_persistence_telegram_dispatch_report_ "candidate_queue_review_ai_summary_persistence_telegram_dispatch_report_catalog_record_write_preview" ) assert data["phase"] == ( - "phase_139_market_intel_mcp_fetch_candidate_queue_writer_review_decision_approval_writer_preflight" + "phase_140_market_intel_professional_source_governance" ) assert data["telegram_dispatch_report_catalog_record_write_passed"] is False assert ( @@ -16663,7 +16683,7 @@ def test_candidate_queue_review_ai_summary_persistence_telegram_dispatch_report_ "candidate_queue_review_ai_summary_persistence_telegram_dispatch_report_catalog_record_run_package_preview" ) assert data["phase"] == ( - "phase_139_market_intel_mcp_fetch_candidate_queue_writer_review_decision_approval_writer_preflight" + "phase_140_market_intel_professional_source_governance" ) assert data["telegram_dispatch_report_catalog_record_run_package_passed"] is False assert ( @@ -16942,7 +16962,7 @@ def test_candidate_queue_review_ai_summary_persistence_telegram_dispatch_report_ "candidate_queue_review_ai_summary_persistence_telegram_dispatch_report_catalog_record_run_readiness_preview" ) assert data["phase"] == ( - "phase_139_market_intel_mcp_fetch_candidate_queue_writer_review_decision_approval_writer_preflight" + "phase_140_market_intel_professional_source_governance" ) assert data["telegram_dispatch_report_catalog_record_run_readiness_passed"] is False assert ( @@ -17271,7 +17291,7 @@ def test_candidate_queue_review_ai_summary_persistence_telegram_dispatch_report_ "candidate_queue_review_ai_summary_persistence_telegram_dispatch_report_catalog_record_run_receipt_preview" ) assert data["phase"] == ( - "phase_139_market_intel_mcp_fetch_candidate_queue_writer_review_decision_approval_writer_preflight" + "phase_140_market_intel_professional_source_governance" ) assert data["telegram_dispatch_report_catalog_record_run_receipt_passed"] is False assert ( @@ -17533,7 +17553,7 @@ def test_candidate_queue_review_ai_summary_persistence_telegram_dispatch_report_ "candidate_queue_review_ai_summary_persistence_telegram_dispatch_report_catalog_record_commit_preview" ) assert data["phase"] == ( - "phase_139_market_intel_mcp_fetch_candidate_queue_writer_review_decision_approval_writer_preflight" + "phase_140_market_intel_professional_source_governance" ) assert data["telegram_dispatch_report_catalog_record_commit_passed"] is False assert ( @@ -17797,7 +17817,7 @@ def test_candidate_queue_review_ai_summary_persistence_telegram_dispatch_report_ "candidate_queue_review_ai_summary_persistence_telegram_dispatch_report_catalog_record_closeout_preview" ) assert data["phase"] == ( - "phase_139_market_intel_mcp_fetch_candidate_queue_writer_review_decision_approval_writer_preflight" + "phase_140_market_intel_professional_source_governance" ) assert data["telegram_dispatch_report_catalog_record_closeout_passed"] is False assert ( @@ -18067,7 +18087,7 @@ def test_candidate_queue_review_ai_summary_persistence_telegram_dispatch_report_ "candidate_queue_review_ai_summary_persistence_telegram_dispatch_report_catalog_record_archive_preview" ) assert data["phase"] == ( - "phase_139_market_intel_mcp_fetch_candidate_queue_writer_review_decision_approval_writer_preflight" + "phase_140_market_intel_professional_source_governance" ) assert data["telegram_dispatch_report_catalog_record_archive_passed"] is False assert ( @@ -18355,7 +18375,7 @@ def test_candidate_queue_review_ai_summary_persistence_telegram_dispatch_report_ "candidate_queue_review_ai_summary_persistence_telegram_dispatch_report_catalog_record_archive_summary_preview" ) assert data["phase"] == ( - "phase_139_market_intel_mcp_fetch_candidate_queue_writer_review_decision_approval_writer_preflight" + "phase_140_market_intel_professional_source_governance" ) assert data["telegram_dispatch_report_catalog_record_archive_summary_passed"] is False assert ( @@ -18642,7 +18662,7 @@ def test_candidate_queue_review_ai_summary_persistence_telegram_dispatch_report_ "candidate_queue_review_ai_summary_persistence_telegram_dispatch_report_catalog_record_final_closeout_preview" ) assert data["phase"] == ( - "phase_139_market_intel_mcp_fetch_candidate_queue_writer_review_decision_approval_writer_preflight" + "phase_140_market_intel_professional_source_governance" ) assert data["telegram_dispatch_report_catalog_record_final_closeout_passed"] is False assert ( @@ -18728,7 +18748,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_139_market_intel_mcp_fetch_candidate_queue_writer_review_decision_approval_writer_preflight" + assert data["phase"] == "phase_140_market_intel_professional_source_governance" assert data["execute_requested"] is False assert data["read_only_query_executed"] is False assert data["database_connection_opened"] is False @@ -18785,7 +18805,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_139_market_intel_mcp_fetch_candidate_queue_writer_review_decision_approval_writer_preflight" + assert data["phase"] == "phase_140_market_intel_professional_source_governance" assert data["execute_requested"] is True assert data["apply_real_write_requested"] is True assert data["approval_token_present"] is False @@ -18874,7 +18894,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_139_market_intel_mcp_fetch_candidate_queue_writer_review_decision_approval_writer_preflight" + assert data["phase"] == "phase_140_market_intel_professional_source_governance" assert data["execute_requested"] is False assert data["read_only_query_executed"] is False assert data["database_connection_opened"] is False @@ -18928,7 +18948,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_139_market_intel_mcp_fetch_candidate_queue_writer_review_decision_approval_writer_preflight" + assert data["phase"] == "phase_140_market_intel_professional_source_governance" assert data["operator_drill_ready"] is True assert data["api_executes_cli"] is False assert data["api_reads_approval_token"] is False @@ -18984,7 +19004,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_139_market_intel_mcp_fetch_candidate_queue_writer_review_decision_approval_writer_preflight" + assert data["phase"] == "phase_140_market_intel_professional_source_governance" assert data["package_ready"] is True assert data["package_artifact_created"] is False assert data["api_writes_file"] is False @@ -19050,7 +19070,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_139_market_intel_mcp_fetch_candidate_queue_writer_review_decision_approval_writer_preflight" + assert data["phase"] == "phase_140_market_intel_professional_source_governance" 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 @@ -19352,7 +19372,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_139_market_intel_mcp_fetch_candidate_queue_writer_review_decision_approval_writer_preflight" + assert data["phase"] == "phase_140_market_intel_professional_source_governance" assert data["receipt_passed"] is True assert data["ready_for_api_database_write"] is False assert data["ready_for_scheduler_attach"] is False @@ -19400,7 +19420,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_139_market_intel_mcp_fetch_candidate_queue_writer_review_decision_approval_writer_preflight" + assert data["phase"] == "phase_140_market_intel_professional_source_governance" assert data["closeout_passed"] is True assert data["ready_for_next_manual_phase"] is True assert data["ready_for_api_database_write"] is False @@ -19449,7 +19469,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_139_market_intel_mcp_fetch_candidate_queue_writer_review_decision_approval_writer_preflight" + assert data["phase"] == "phase_140_market_intel_professional_source_governance" assert data["handoff_ready"] is True assert data["ready_for_manual_queue_review"] is True assert data["ready_for_api_database_write"] is False @@ -19507,7 +19527,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_139_market_intel_mcp_fetch_candidate_queue_writer_review_decision_approval_writer_preflight" + assert data["phase"] == "phase_140_market_intel_professional_source_governance" assert data["execute_requested"] is False assert data["review_inventory_ready"] is False assert data["ready_for_human_decision_review"] is False @@ -19573,7 +19593,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_139_market_intel_mcp_fetch_candidate_queue_writer_review_decision_approval_writer_preflight" + assert data["phase"] == "phase_140_market_intel_professional_source_governance" 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 @@ -19644,7 +19664,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_139_market_intel_mcp_fetch_candidate_queue_writer_review_decision_approval_writer_preflight" + assert data["phase"] == "phase_140_market_intel_professional_source_governance" 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 @@ -19720,7 +19740,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_139_market_intel_mcp_fetch_candidate_queue_writer_review_decision_approval_writer_preflight" + assert data["phase"] == "phase_140_market_intel_professional_source_governance" assert data["transaction_preview_created"] is False assert data["transaction_ready"] is False assert data["ready_for_manual_shell_update_window"] is False @@ -19802,7 +19822,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_139_market_intel_mcp_fetch_candidate_queue_writer_review_decision_approval_writer_preflight" + assert data["phase"] == "phase_140_market_intel_professional_source_governance" assert data["execute_requested"] is True assert data["apply_real_write_requested"] is True assert data["approval_token_present"] is False @@ -19888,7 +19908,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_139_market_intel_mcp_fetch_candidate_queue_writer_review_decision_approval_writer_preflight" + assert data["phase"] == "phase_140_market_intel_professional_source_governance" assert data["execute_requested"] is True assert data["apply_real_write_requested"] is True assert data["read_only_query_executed"] is False @@ -19971,7 +19991,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_139_market_intel_mcp_fetch_candidate_queue_writer_review_decision_approval_writer_preflight" + assert data["phase"] == "phase_140_market_intel_professional_source_governance" assert data["execute_requested"] is False assert data["read_only_query_executed"] is False assert data["database_connection_opened"] is False @@ -20054,7 +20074,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_139_market_intel_mcp_fetch_candidate_queue_writer_review_decision_approval_writer_preflight" + assert data["phase"] == "phase_140_market_intel_professional_source_governance" 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 @@ -20140,7 +20160,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_139_market_intel_mcp_fetch_candidate_queue_writer_review_decision_approval_writer_preflight" + assert data["phase"] == "phase_140_market_intel_professional_source_governance" assert data["package_ready"] is False assert data["package_artifact_created"] is False assert data["ready_for_api_review_state_update"] is False @@ -20231,7 +20251,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_139_market_intel_mcp_fetch_candidate_queue_writer_review_decision_approval_writer_preflight" + "phase_140_market_intel_professional_source_governance" ) assert data["ready_for_cli_operator_run"] is False assert data["ready_for_api_review_state_update"] is False @@ -20341,7 +20361,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_139_market_intel_mcp_fetch_candidate_queue_writer_review_decision_approval_writer_preflight" + "phase_140_market_intel_professional_source_governance" ) assert data["receipt_passed"] is False assert data["ready_for_api_review_state_update"] is False @@ -20427,7 +20447,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_139_market_intel_mcp_fetch_candidate_queue_writer_review_decision_approval_writer_preflight" + "phase_140_market_intel_professional_source_governance" ) assert data["closeout_passed"] is False assert data["ready_for_api_review_state_update"] is False @@ -20484,7 +20504,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_139_market_intel_mcp_fetch_candidate_queue_writer_review_decision_approval_writer_preflight" + "phase_140_market_intel_professional_source_governance" ) assert data["post_closeout_inventory_ready"] is False assert data["ready_for_api_review_state_update"] is False @@ -20537,7 +20557,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_139_market_intel_mcp_fetch_candidate_queue_writer_review_decision_approval_writer_preflight" + assert data["phase"] == "phase_140_market_intel_professional_source_governance" 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 @@ -20590,7 +20610,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_139_market_intel_mcp_fetch_candidate_queue_writer_review_decision_approval_writer_preflight" + assert data["phase"] == "phase_140_market_intel_professional_source_governance" assert data["archive_summary_ready"] is False assert data["summary_input_ready"] is False assert data["ready_for_ai_summary_generation"] is False @@ -20651,7 +20671,7 @@ def test_candidate_queue_review_ai_summary_preflight_route_is_post_only_and_no_w assert get_response.status_code == 405 assert response.status_code == 200 assert data["mode"] == "candidate_queue_review_ai_summary_preflight_preview" - assert data["phase"] == "phase_139_market_intel_mcp_fetch_candidate_queue_writer_review_decision_approval_writer_preflight" + assert data["phase"] == "phase_140_market_intel_professional_source_governance" assert data["ai_summary_preflight_ready"] is False assert data["ready_for_manual_ollama_summary_run"] is False assert data["ready_for_ai_summary_generation"] is False @@ -20720,7 +20740,7 @@ def test_candidate_queue_review_ai_summary_run_package_route_is_post_only_and_no assert get_response.status_code == 405 assert response.status_code == 200 assert data["mode"] == "candidate_queue_review_ai_summary_run_package_preview" - assert data["phase"] == "phase_139_market_intel_mcp_fetch_candidate_queue_writer_review_decision_approval_writer_preflight" + assert data["phase"] == "phase_140_market_intel_professional_source_governance" assert data["ai_summary_run_package_ready"] is False assert data["ready_for_manual_ollama_summary_run"] is False assert data["ready_for_ai_summary_generation"] is False @@ -20792,7 +20812,7 @@ def test_candidate_queue_review_ai_summary_output_receipt_route_is_post_only_and assert get_response.status_code == 405 assert response.status_code == 200 assert data["mode"] == "candidate_queue_review_ai_summary_output_receipt_preview" - assert data["phase"] == "phase_139_market_intel_mcp_fetch_candidate_queue_writer_review_decision_approval_writer_preflight" + assert data["phase"] == "phase_140_market_intel_professional_source_governance" assert data["ai_summary_output_receipt_ready"] is False assert data["ready_for_summary_persistence_review"] is False assert data["manual_ai_summary_output_provided"] is False @@ -20865,7 +20885,7 @@ def test_candidate_queue_review_ai_summary_persistence_preflight_route_is_post_o "candidate_queue_review_ai_summary_persistence_preflight_preview" ) assert data["phase"] == ( - "phase_139_market_intel_mcp_fetch_candidate_queue_writer_review_decision_approval_writer_preflight" + "phase_140_market_intel_professional_source_governance" ) assert data["summary_persistence_preflight_ready"] is False assert data["ready_for_summary_transaction_preview"] is False @@ -20936,7 +20956,7 @@ def test_candidate_queue_review_ai_summary_persistence_transaction_route_is_post "candidate_queue_review_ai_summary_persistence_transaction_preview" ) assert data["phase"] == ( - "phase_139_market_intel_mcp_fetch_candidate_queue_writer_review_decision_approval_writer_preflight" + "phase_140_market_intel_professional_source_governance" ) assert data["summary_persistence_transaction_ready"] is False assert data["ready_for_summary_persistence_writer_gate"] is False @@ -21001,7 +21021,7 @@ def test_candidate_queue_review_ai_summary_persistence_writer_preflight_route_is "candidate_queue_review_ai_summary_persistence_writer_preflight_preview" ) assert data["phase"] == ( - "phase_139_market_intel_mcp_fetch_candidate_queue_writer_review_decision_approval_writer_preflight" + "phase_140_market_intel_professional_source_governance" ) assert data["summary_persistence_writer_preflight_ready"] is False assert data["ready_for_summary_persistence_run_package"] is False @@ -21072,7 +21092,7 @@ def test_candidate_queue_review_ai_summary_persistence_run_package_route_is_post "candidate_queue_review_ai_summary_persistence_run_package_preview" ) assert data["phase"] == ( - "phase_139_market_intel_mcp_fetch_candidate_queue_writer_review_decision_approval_writer_preflight" + "phase_140_market_intel_professional_source_governance" ) assert data["package_ready"] is False assert data["ready_for_summary_persistence_run_readiness"] is False @@ -21145,7 +21165,7 @@ def test_candidate_queue_review_ai_summary_persistence_run_readiness_route_is_po "candidate_queue_review_ai_summary_persistence_run_readiness_preview" ) assert data["phase"] == ( - "phase_139_market_intel_mcp_fetch_candidate_queue_writer_review_decision_approval_writer_preflight" + "phase_140_market_intel_professional_source_governance" ) assert data["run_readiness_ready"] is False assert data["summary_persistence_run_readiness_ready"] is False @@ -21222,7 +21242,7 @@ def test_candidate_queue_review_ai_summary_persistence_run_receipt_route_is_post "candidate_queue_review_ai_summary_persistence_run_receipt_preview" ) assert data["phase"] == ( - "phase_139_market_intel_mcp_fetch_candidate_queue_writer_review_decision_approval_writer_preflight" + "phase_140_market_intel_professional_source_governance" ) assert data["run_receipt_passed"] is False assert data["summary_persistence_run_receipt_passed"] is False @@ -21299,7 +21319,7 @@ def test_candidate_queue_review_ai_summary_persistence_run_closeout_route_is_pos "candidate_queue_review_ai_summary_persistence_run_closeout_preview" ) assert data["phase"] == ( - "phase_139_market_intel_mcp_fetch_candidate_queue_writer_review_decision_approval_writer_preflight" + "phase_140_market_intel_professional_source_governance" ) assert data["closeout_passed"] is False assert data["summary_persistence_closeout_passed"] is False @@ -21376,7 +21396,7 @@ def test_candidate_queue_review_ai_summary_persistence_telegram_dispatch_gate_ro "candidate_queue_review_ai_summary_persistence_telegram_dispatch_gate_preview" ) assert data["phase"] == ( - "phase_139_market_intel_mcp_fetch_candidate_queue_writer_review_decision_approval_writer_preflight" + "phase_140_market_intel_professional_source_governance" ) assert data["telegram_dispatch_gate_passed"] is False assert data["summary_persistence_telegram_dispatch_gate_passed"] is False @@ -21450,7 +21470,7 @@ def test_candidate_queue_review_ai_summary_persistence_telegram_dispatch_run_pac "candidate_queue_review_ai_summary_persistence_telegram_dispatch_run_package_preview" ) assert data["phase"] == ( - "phase_139_market_intel_mcp_fetch_candidate_queue_writer_review_decision_approval_writer_preflight" + "phase_140_market_intel_professional_source_governance" ) assert data["telegram_dispatch_run_package_ready"] is False assert data["summary_persistence_telegram_dispatch_run_package_ready"] is False @@ -21529,7 +21549,7 @@ def test_candidate_queue_review_ai_summary_persistence_telegram_dispatch_run_rea "candidate_queue_review_ai_summary_persistence_telegram_dispatch_run_readiness_preview" ) assert data["phase"] == ( - "phase_139_market_intel_mcp_fetch_candidate_queue_writer_review_decision_approval_writer_preflight" + "phase_140_market_intel_professional_source_governance" ) assert data["telegram_dispatch_run_readiness_ready"] is False assert ( @@ -21616,7 +21636,7 @@ def test_candidate_queue_review_ai_summary_persistence_telegram_dispatch_run_rec "candidate_queue_review_ai_summary_persistence_telegram_dispatch_run_receipt_preview" ) assert data["phase"] == ( - "phase_139_market_intel_mcp_fetch_candidate_queue_writer_review_decision_approval_writer_preflight" + "phase_140_market_intel_professional_source_governance" ) assert data["telegram_dispatch_run_receipt_passed"] is False assert data["summary_persistence_telegram_dispatch_run_receipt_passed"] is False @@ -21697,7 +21717,7 @@ def test_candidate_queue_review_ai_summary_persistence_telegram_dispatch_closeou "candidate_queue_review_ai_summary_persistence_telegram_dispatch_closeout_preview" ) assert data["phase"] == ( - "phase_139_market_intel_mcp_fetch_candidate_queue_writer_review_decision_approval_writer_preflight" + "phase_140_market_intel_professional_source_governance" ) assert data["telegram_dispatch_closeout_passed"] is False assert data["summary_persistence_telegram_dispatch_closeout_passed"] is False @@ -21779,7 +21799,7 @@ def test_candidate_queue_review_ai_summary_persistence_telegram_dispatch_archive "candidate_queue_review_ai_summary_persistence_telegram_dispatch_archive_preview" ) assert data["phase"] == ( - "phase_139_market_intel_mcp_fetch_candidate_queue_writer_review_decision_approval_writer_preflight" + "phase_140_market_intel_professional_source_governance" ) assert data["telegram_dispatch_archive_ready"] is False assert data["summary_persistence_telegram_dispatch_archive_ready"] is False @@ -21862,7 +21882,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_139_market_intel_mcp_fetch_candidate_queue_writer_review_decision_approval_writer_preflight" + assert data["phase"] == "phase_140_market_intel_professional_source_governance" assert data["receipt_passed"] is True assert data["ready_for_next_manual_review"] is True assert data["ready_for_api_database_write"] is False @@ -21887,7 +21907,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_139_market_intel_mcp_fetch_candidate_queue_writer_review_decision_approval_writer_preflight" + assert plan["phase"] == "phase_140_market_intel_professional_source_governance" assert plan["ready_to_attach_scheduler"] is False assert plan["scheduler_attached"] is False assert plan["scheduler_registration_executed"] is False @@ -21925,7 +21945,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_139_market_intel_mcp_fetch_candidate_queue_writer_review_decision_approval_writer_preflight" + assert data["phase"] == "phase_140_market_intel_professional_source_governance" assert data["scheduler_registration_executed"] is False assert data["crawler_job_started"] is False assert data["external_network_executed"] is False @@ -21936,7 +21956,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_139_market_intel_mcp_fetch_candidate_queue_writer_review_decision_approval_writer_preflight" + assert plan["phase"] == "phase_140_market_intel_professional_source_governance" assert plan["ready_for_review_queue"] is False assert plan["review_queue_created"] is False assert plan["auto_match_executed"] is False @@ -21972,7 +21992,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_139_market_intel_mcp_fetch_candidate_queue_writer_review_decision_approval_writer_preflight" + assert data["phase"] == "phase_140_market_intel_professional_source_governance" assert data["review_queue_created"] is False assert data["auto_confirm_executed"] is False assert data["external_network_executed"] is False @@ -21983,7 +22003,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_139_market_intel_mcp_fetch_candidate_queue_writer_review_decision_approval_writer_preflight" + assert plan["phase"] == "phase_140_market_intel_professional_source_governance" assert plan["ready_for_opportunity_queue"] is False assert plan["opportunity_queue_created"] is False assert plan["threat_alert_dispatched"] is False @@ -22024,7 +22044,7 @@ def test_opportunity_plan_route_is_preview_only(): assert response.status_code == 200 assert data["mode"] == "opportunity_plan_preview" - assert data["phase"] == "phase_139_market_intel_mcp_fetch_candidate_queue_writer_review_decision_approval_writer_preflight" + assert data["phase"] == "phase_140_market_intel_professional_source_governance" assert data["opportunity_queue_created"] is False assert data["threat_alert_dispatched"] is False assert data["ai_summary_generated"] is False @@ -22035,7 +22055,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_139_market_intel_mcp_fetch_candidate_queue_writer_review_decision_approval_writer_preflight" + assert plan["phase"] == "phase_140_market_intel_professional_source_governance" assert plan["ready_for_scoring_job"] is False assert plan["scoring_job_created"] is False assert plan["score_calculation_executed"] is False @@ -22083,7 +22103,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_139_market_intel_mcp_fetch_candidate_queue_writer_review_decision_approval_writer_preflight" + assert data["phase"] == "phase_140_market_intel_professional_source_governance" assert data["scoring_job_created"] is False assert data["score_calculation_executed"] is False assert data["sample_scores_generated"] is False @@ -22095,7 +22115,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_139_market_intel_mcp_fetch_candidate_queue_writer_review_decision_approval_writer_preflight" + assert plan["phase"] == "phase_140_market_intel_professional_source_governance" assert plan["ready_for_evidence_bundle"] is False assert plan["evidence_bundle_created"] is False assert plan["evidence_query_executed"] is False @@ -22141,7 +22161,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_139_market_intel_mcp_fetch_candidate_queue_writer_review_decision_approval_writer_preflight" + assert data["phase"] == "phase_140_market_intel_professional_source_governance" assert data["evidence_bundle_created"] is False assert data["evidence_query_executed"] is False assert data["sample_evidence_generated"] is False @@ -22154,7 +22174,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_139_market_intel_mcp_fetch_candidate_queue_writer_review_decision_approval_writer_preflight" + assert plan["phase"] == "phase_140_market_intel_professional_source_governance" assert plan["ready_for_alert_candidates"] is False assert plan["alert_candidate_created"] is False assert plan["alert_queue_created"] is False @@ -22239,7 +22259,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_139_market_intel_mcp_fetch_candidate_queue_writer_review_decision_approval_writer_preflight" + assert data["phase"] == "phase_140_market_intel_professional_source_governance" assert data["alert_candidate_created"] is False assert data["alert_queue_created"] is False assert data["review_queue_created"] is False @@ -22317,7 +22337,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_139_market_intel_mcp_fetch_candidate_queue_writer_review_decision_approval_writer_preflight" + assert data["phase"] == "phase_140_market_intel_professional_source_governance" assert data["deployment_actions_executed"] is False assert data["docker_command_executed"] is False assert data["ssh_command_executed"] is False @@ -22332,7 +22352,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_139_market_intel_mcp_fetch_candidate_queue_writer_review_decision_approval_writer_preflight" + assert readiness["phase"] == "phase_140_market_intel_professional_source_governance" assert readiness["execute_requested"] is False assert readiness["router_enabled"] is False assert readiness["external_mcp_complete"] is False @@ -22803,6 +22823,18 @@ def test_deployment_readiness_reports_app_only_release_gate(): ] is True ) + assert ( + readiness["checks"]["mcp_professional_source_governance_preview_safe"] + is True + ) + assert ( + readiness["mcp_professional_source_governance"]["mode"] + == "mcp_professional_source_governance_preview" + ) + assert ( + readiness["mcp_professional_source_governance"]["api_fetches_source_url"] + is False + ) assert readiness["checks"]["scheduler_plan_preview_safe"] is True assert readiness["checks"]["manual_sample_plan_preview_safe"] is True assert readiness["checks"]["manual_sample_acceptance_preview_safe"] is True @@ -23206,6 +23238,10 @@ def test_deployment_readiness_reports_app_only_release_gate(): "/api/market_intel/mcp_fetch_candidate_queue_writer_review_decision_approval_writer_preflight" in readiness["production_smoke_targets"] ) + assert ( + "/api/market_intel/mcp_professional_source_governance" + in readiness["production_smoke_targets"] + ) assert "/api/market_intel/scheduler_plan" in readiness["production_smoke_targets"] assert "/api/market_intel/manual_sample_plan" in readiness["production_smoke_targets"] assert "/api/market_intel/manual_sample_acceptance" in readiness["production_smoke_targets"] @@ -27247,7 +27283,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_139_market_intel_mcp_fetch_candidate_queue_writer_review_decision_approval_writer_preflight" + assert drill["phase"] == "phase_140_market_intel_professional_source_governance" assert drill["execute_requested"] is False assert drill["schema_state"] == "planned_no_db_probe" assert drill["drill_ready_for_operator_review"] is True @@ -27362,7 +27398,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_139_market_intel_mcp_fetch_candidate_queue_writer_review_decision_approval_writer_preflight" + assert data["phase"] == "phase_140_market_intel_professional_source_governance" assert data["execute_requested"] is False assert data["migration_executed"] is False assert data["rollback_executed"] is False @@ -27374,7 +27410,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_139_market_intel_mcp_fetch_candidate_queue_writer_review_decision_approval_writer_preflight" + assert review["phase"] == "phase_140_market_intel_professional_source_governance" assert review["execute_requested"] is False assert review["catalog_state"] == "planned_no_probe" assert review["seed_state"] == "planned_no_probe" @@ -27489,7 +27525,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_139_market_intel_mcp_fetch_candidate_queue_writer_review_decision_approval_writer_preflight" + assert data["phase"] == "phase_140_market_intel_professional_source_governance" assert data["execute_requested"] is False assert data["catalog_state"] == "planned_no_probe" assert data["migration_executed"] is False @@ -27502,7 +27538,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_139_market_intel_mcp_fetch_candidate_queue_writer_review_decision_approval_writer_preflight" + assert smoke["phase"] == "phase_140_market_intel_professional_source_governance" assert smoke["execute_requested"] is False assert smoke["smoke_result"] == "planned_no_execution" assert smoke["live_smoke_passed"] is False @@ -27564,7 +27600,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_139_market_intel_mcp_fetch_candidate_queue_writer_review_decision_approval_writer_preflight" + assert data["phase"] == "phase_140_market_intel_professional_source_governance" assert data["execute_requested"] is False assert data["smoke_result"] == "planned_no_execution" assert data["migration_executed"] is False @@ -27577,7 +27613,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_139_market_intel_mcp_fetch_candidate_queue_writer_review_decision_approval_writer_preflight" + assert inventory["phase"] == "phase_140_market_intel_professional_source_governance" assert inventory["execute_requested"] is False assert inventory["read_only_query_executed"] is False assert inventory["database_connection_opened"] is False @@ -27721,7 +27757,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_139_market_intel_mcp_fetch_candidate_queue_writer_review_decision_approval_writer_preflight" + assert data["phase"] == "phase_140_market_intel_professional_source_governance" assert data["execute_requested"] is False assert data["read_only_query_executed"] is False assert data["database_write_executed"] is False @@ -27957,7 +27993,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_139_market_intel_mcp_fetch_candidate_queue_writer_review_decision_approval_writer_preflight" + assert data["phase"] == "phase_140_market_intel_professional_source_governance" assert data["execute_requested"] is False assert data["apply_real_write_requested"] is False assert data["writes_executed"] is False @@ -27986,7 +28022,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_139_market_intel_mcp_fetch_candidate_queue_writer_review_decision_approval_writer_preflight" + assert data["phase"] == "phase_140_market_intel_professional_source_governance" assert data["execute_requested"] is False assert data["apply_real_write_requested"] is False assert data["approval_token_present"] is False @@ -28000,3 +28036,160 @@ def test_review_decision_writer_cli_script_outputs_blocked_gate_without_login_en assert data["scheduler_attached"] is False assert data["writes_executed"] is False assert data["exit_code"] == 0 + + +def test_mcp_professional_source_governance_preview_is_safe_without_payload(): + governance = build_mcp_professional_source_governance_preview( + phase="phase_140_market_intel_professional_source_governance", + ) + + assert governance["mode"] == "mcp_professional_source_governance_preview" + assert governance["phase"] == "phase_140_market_intel_professional_source_governance" + assert governance["source_governance_payload_received"] is False + assert governance["mcp_professional_source_governance_accepted"] is False + assert governance["ready_for_mcp_fetch_source_contract"] is False + assert governance["network_request_allowed"] is False + assert governance["external_network_executed"] is False + assert governance["api_uses_external_network"] is False + assert governance["api_fetches_robots_txt"] is False + assert governance["api_fetches_sitemap"] is False + assert governance["api_fetches_source_url"] is False + assert governance["api_opens_database_connection"] is False + assert governance["api_writes_database"] is False + assert governance["api_writes_file"] is False + assert governance["payload_persisted"] is False + assert governance["scheduler_attached"] is False + assert "source_governance_payload_received" in governance["blocked_reasons"] + assert "operator_source_governance" in governance[ + "sample_professional_source_governance_package" + ] + assert governance["source_contract"]["policy_version"] == "source_governance_v1" + + +def test_mcp_professional_source_governance_accepts_sample_package(): + sample = ( + build_mcp_professional_source_governance_preview() + ["sample_professional_source_governance_package"] + ) + governance = build_mcp_professional_source_governance_preview( + operator_source_governance=sample["operator_source_governance"], + phase="phase_140_market_intel_professional_source_governance", + ) + + assert governance["mode"] == "mcp_professional_source_governance" + assert governance["phase"] == "phase_140_market_intel_professional_source_governance" + assert governance["mcp_professional_source_governance_accepted"] is True + assert governance["ready_for_mcp_fetch_source_contract"] is True + assert governance["ready_for_api_database_write"] is False + assert governance["blocked_reasons"] == [] + assert governance["passed_gate_count"] == governance["gate_count"] + summary = governance["source_governance_summary"] + assert summary["source_count"] == 4 + assert summary["platform_count"] == 4 + assert summary["robots_checked_count"] == 4 + assert summary["structured_data_ready_count"] == 4 + assert summary["min_crawl_delay_seconds"] >= 1 + assert all(source["evidence_artifact_path_safe"] for source in governance["sources"]) + assert all(source["snapshot_hash_required"] for source in governance["sources"]) + assert governance["api_fetches_robots_txt"] is False + assert governance["api_fetches_source_url"] is False + assert governance["database_write_executed"] is False + assert governance["scheduler_attached"] is False + + +def test_mcp_professional_source_governance_blocks_unsafe_source(): + sample = json.loads( + json.dumps( + build_mcp_professional_source_governance_preview() + ["sample_professional_source_governance_package"] + ) + ) + payload = sample["operator_source_governance"] + first_source = payload["sources"][0] + first_source["source_url"] = "https://www.momoshop.com.tw/member/orders" + first_source["robots_allowed"] = False + first_source["crawl_delay_seconds"] = 0 + first_source["max_requests_per_run"] = 500 + first_source["structured_data_types"] = ["Article"] + first_source["login_required"] = True + first_source["anti_bot_bypass_required"] = True + first_source["evidence_artifact_path"] = "../unsafe.json" + first_source["snapshot_hash_required"] = False + payload["raw_html"] = "raw" + payload["cookie"] = "do-not-submit" + payload["api_fetches_source_url"] = True + payload["api_writes_database"] = True + payload["operator_confirmations"]["no_antibot_bypass"] = False + payload["operator_confirmations"]["no_api_network_fetch"] = False + + governance = build_mcp_professional_source_governance_preview( + operator_source_governance=payload, + ) + + assert governance["mcp_professional_source_governance_accepted"] is False + assert "source_governance_public_urls_only" in governance["blocked_reasons"] + assert ( + "source_governance_robots_policy_checked_and_allowed" + in governance["blocked_reasons"] + ) + assert "source_governance_structured_data_first" in governance["blocked_reasons"] + assert "source_governance_rate_limits_safe" in governance["blocked_reasons"] + assert ( + "source_governance_public_data_only_no_login_or_pii" + in governance["blocked_reasons"] + ) + assert "source_governance_no_antibot_bypass" in governance["blocked_reasons"] + assert ( + "source_governance_provenance_and_hash_required" + in governance["blocked_reasons"] + ) + assert "source_governance_evidence_paths_safe" in governance["blocked_reasons"] + assert ( + "source_governance_operator_boundaries_confirmed" + in governance["blocked_reasons"] + ) + assert "source_governance_no_raw_payload" in governance["blocked_reasons"] + assert "source_governance_no_secret_or_token_key" in governance["blocked_reasons"] + assert "source_governance_side_effect_free" in governance["blocked_reasons"] + assert "source_governance_api_preview_only" in governance["blocked_reasons"] + assert governance["api_uses_external_network"] is False + assert governance["api_writes_database"] is False + assert governance["scheduler_attached"] is False + + +def test_mcp_professional_source_governance_route_get_and_post_preview_only(): + from routes.market_intel_routes import market_intel_bp + + app = Flask(__name__) + app.secret_key = "test-secret" + app.register_blueprint(market_intel_bp) + client = app.test_client() + with client.session_transaction() as session: + session["logged_in"] = True + + get_response = client.get("/api/market_intel/mcp_professional_source_governance") + get_data = get_response.get_json() + sample = get_data["sample_professional_source_governance_package"] + post_response = client.post( + "/api/market_intel/mcp_professional_source_governance", + json={"professional_source_governance_package": sample}, + ) + post_data = post_response.get_json() + + assert get_response.status_code == 200 + assert get_data["mode"] == "mcp_professional_source_governance_preview" + assert get_data["phase"] == "phase_140_market_intel_professional_source_governance" + assert get_data["api_uses_external_network"] is False + assert get_data["api_writes_database"] is False + assert post_response.status_code == 200 + assert post_data["mode"] == "mcp_professional_source_governance" + assert post_data["phase"] == "phase_140_market_intel_professional_source_governance" + assert post_data["mcp_professional_source_governance_accepted"] is True + assert post_data["ready_for_mcp_fetch_source_contract"] is True + assert post_data["network_request_allowed"] is False + assert post_data["api_fetches_robots_txt"] is False + assert post_data["api_fetches_source_url"] is False + assert post_data["api_opens_database_connection"] is False + assert post_data["api_writes_database"] is False + assert post_data["payload_persisted"] is False + assert post_data["scheduler_attached"] is False