diff --git a/TODO_NEXT_STEPS.txt b/TODO_NEXT_STEPS.txt
index 4084c64..9157019 100644
--- a/TODO_NEXT_STEPS.txt
+++ b/TODO_NEXT_STEPS.txt
@@ -4,7 +4,7 @@
================================================================================
【已完成】
- - V10.490 新增市場情報 MCP Fetch Result Parser Review 安全預覽 gate:只審核操作員貼回的 parser 結構化摘要,對齊 receipt source/path、候選必要欄位、公開 URL、小批次上限與 raw HTML/secret/side-effect 風險;API 不讀 artifact、不執行 parser CLI、不抓外站、不寫 DB、不掛 scheduler。
+ - V10.491 新增市場情報 MCP Fetch Result Parser Review 安全預覽 gate:只審核操作員貼回的 parser 結構化摘要,對齊 receipt source/path、候選必要欄位、公開 URL、小批次上限與 raw HTML/secret/side-effect 風險;API 不讀 artifact、不執行 parser CLI、不抓外站、不寫 DB、不掛 scheduler。
- V10.489 補 PChome 低分同款人工覆核回收與 gate-pass 風險邊界:TS6 超美白香氛誘霜 120g/ml、W 修護保養蝸牛特潤修護面膜 6 片、Derma 大地 Eco 植萃護膚油 2 入,從低信心升成 `identity_review` 人工覆核候選;Clarins 輕盈美體護理油 vs 身體調和護理油、台塑生醫嬰兒沐浴/洗髮組合數量反轉、isLeaf 私密慕絲香型數量不一致改 hard veto;HOOOME 大理石暖燈 vs 泛稱經典款改留 `variant_selection_review`。正式價差表仍需人工採用才會寫入。Production 已部署 `/health=V10.489`;500 筆 read-only audit 由 V10.486 基線 `gate_pass=129 / identity_veto=1 / still_low=370` 收斂為 `gate_pass=124 / identity_veto=4 / still_low=372`。測試:完整 `pytest` 1289 passed / 9 skipped。
- V10.488 新增市場情報 MCP Fetch Run Receipt 安全預覽 gate,只審核操作員 dry-run receipt,不執行 CLI、不抓外站、不寫 DB。
- V10.486 補 PChome near-threshold 風險邊界:NEW DIRECTIONS 甜杏仁油 vs 酪梨油直接 `core_ingredient_line_conflict` hard veto;COCODOR 經典擴香瓶多款任選 vs generic、KAMERIA 足膜任選三款 vs 單一涼感足膜、Hakugen 白元入浴劑橘盒/綠盒不同變體都保留 `variant_selection_review`,不進可採用 gate。Production 已部署 `/health=V10.486`;240 筆 near-threshold audit `gate_pass 83→79`、`identity_veto 0→1`、`still_low 157→160`。測試:`tests/test_marketplace_product_matcher.py`、`tests/test_competitor_match_attempts_persistence.py`、`tests/test_competitor_match_attempt_rescore_audit.py` 通過。
diff --git a/config.py b/config.py
index e4fb321..22d56e8 100644
--- a/config.py
+++ b/config.py
@@ -350,7 +350,7 @@ YOUTUBE_API_KEY = os.getenv('YOUTUBE_API_KEY', '')
# ==========================================
# 系統版本與路徑
# ==========================================
-SYSTEM_VERSION = "V10.490"
+SYSTEM_VERSION = "V10.491"
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 db7e5d3..202f0af 100644
--- a/docs/adr/ADR-035-cross-platform-market-campaign-intelligence.md
+++ b/docs/adr/ADR-035-cross-platform-market-campaign-intelligence.md
@@ -163,6 +163,7 @@ EwoooC 目前已有 MOMO EDM / 節慶活動資料、`promo_products`、PChome
- 2026-05-24 追加 MCP fetch run package gate:`services.market_intel.mcp_fetch_run_package` 與 `/api/market_intel/mcp_fetch_run_package` 將已通過的 target review 轉成 command argv preview、receipt path 與操作員 run controls 審核包;路由拆至 `routes.market_intel_mcp_run_routes`,主 Blueprint 僅掛 extension import。此階段不執行 CLI、不發外部 request、不寫 artifact、不開 DB、不掛 scheduler,只放行到後續 run readiness review。
- 2026-05-24 追加 MCP fetch run readiness gate:`services.market_intel.mcp_fetch_run_readiness` 與 `/api/market_intel/mcp_fetch_run_readiness` 在 run package 後檢查 command preview、receipt path、artifact path、節流/timeout/dry-run-first、secret payload 與操作員 shell-only 邊界;此階段不執行 CLI、不發外部 request、不寫 artifact、不開 DB、不掛 scheduler,只放行到人工 shell dry-run 與後續 receipt review。
- 2026-05-29 追加 MCP fetch run receipt gate:`services.market_intel.mcp_fetch_run_receipt` 與 `/api/market_intel/mcp_fetch_run_receipt` 在操作員 shell 完成 dry-run fetch 後審核 receipt id、artifact path、platform/source/receipt path 對帳、公開 URL、request/error budget、secret 外洩與 API/DB/scheduler 副作用旗標;API/UI 不執行 CLI、不發外部 request、不保存 receipt、不開 DB、不寫入、不掛 scheduler,只放行到後續 result parser review。
+- 2026-05-31 追加 MCP fetch result parser review gate:`services.market_intel.mcp_fetch_result_parser_review` 與 `/api/market_intel/mcp_fetch_result_parser_review` 在 receipt 通過後審核操作員貼回的 parser 結構化摘要,檢查 receipt source/receipt path 對帳、parser artifact path、活動/商品候選必要欄位、公開 URL、小批次候選上限、raw HTML/secret 外洩與 API/DB/scheduler 副作用旗標;API/UI 不讀 artifact、不執行 parser CLI、不發外部 request、不保存 parser result、不寫入、不掛 scheduler,只放行到候選 handoff 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/current_execution_queue_20260524.md b/docs/memory/current_execution_queue_20260524.md
index 0bc9bf9..9480ada 100644
--- a/docs/memory/current_execution_queue_20260524.md
+++ b/docs/memory/current_execution_queue_20260524.md
@@ -86,6 +86,7 @@
- 2026-05-29 起,`V10.488` 新增市場情報 MCP Fetch Run Receipt 安全預覽 gate,只審核操作員 dry-run receipt,不執行 CLI、不抓外站、不寫 DB。
- 2026-05-29 起,`V10.489` 補 PChome 低分同款人工覆核回收與 gate-pass 風險邊界:TS6 超美白香氛誘霜 120g/ml、W 修護保養蝸牛特潤修護面膜 6 片、Derma 大地 Eco 植萃護膚油 2 入,從低信心升成 `identity_review` 候選;Clarins 輕盈美體護理油 vs 身體調和護理油、台塑生醫嬰兒沐浴/洗髮組合數量反轉、isLeaf 私密慕絲香型數量不一致改 hard veto;HOOOME 大理石暖燈 vs 泛稱經典款只留 `variant_selection_review`,不進 total-price accepted。Production 已部署 `/health=V10.489`;500 筆 read-only audit 由 V10.486 基線 `gate_pass=129 / identity_veto=1 / still_low=370` 收斂為 `gate_pass=124 / identity_veto=4 / still_low=372`,三應用容器 healthy、`momo-db` 未 recreate。
- 2026-05-31 起,`V10.490` 補 ElephantAlpha / OpenClaw 舊策略 action 相容:正式日誌觀察到 `agent=openclaw action=generate_market_strategy` 被當未知步驟丟錯;此類 OpenClaw strategy 產生步驟已定義為 advisory skipped,只記 warning,不觸發 circuit breaker、不執行外部策略、不影響價格行動。測試覆蓋 `generate_market_strategy` 與 `generate_resource_optimization_strategy`,未知 action 仍維持 fail-fast。
+- 2026-05-31 起,`V10.491` 新增市場情報 MCP Fetch Result Parser Review gate:在 receipt gate 後只審核操作員 shell parser 貼回的結構化摘要,對齊 source/path、公開 URL、候選必要欄位、小批次上限、raw HTML/secret/side-effect 風險;仍不讀 artifact、不執行 CLI、不連外、不寫 DB、不掛 scheduler。
## 3. 12 Agent 決策信封整合
diff --git a/docs/memory/history_logs.md b/docs/memory/history_logs.md
index f8d97b0..77f388d 100644
--- a/docs/memory/history_logs.md
+++ b/docs/memory/history_logs.md
@@ -13,6 +13,7 @@
## 📅 詳細更新日誌 (考古存檔)
### 2026-05-24:PChome 近門檻身份回收第二輪
+- **V10.491 市場情報 MCP Fetch Result Parser Review gate**: 新增 `/api/market_intel/mcp_fetch_result_parser_review` 與 UI preview,只審核操作員 shell parser 後貼回的結構化摘要;API 不讀 artifact、不執行 parser CLI、不抓外站、不寫檔、不開 DB、不掛 scheduler,且會阻擋 raw HTML/body/snapshot、secret/token 欄位與 side-effect flags。
- **V10.489 PChome 低分同款人工覆核回收與 gate-pass 風險邊界**: `marketplace_product_matcher` 新增三個窄範圍 focused identity:TS6 超美白香氛誘霜 120g/ml、W 修護保養蝸牛特潤修護面膜 6 片、Derma 大地 Eco 植萃護膚油 2 入。這些樣本只升到 `identity_review / manual_review`,不進 `price_alert_exact`;同版補 Clarins 身體油不同線、命名組合品數量反轉、isLeaf 香型數量不一致 hard veto,HOOOME 大理石暖燈單側設計差留人工覆核。
- **V10.488 市場情報 MCP Fetch Run Receipt gate**: 新增 `/api/market_intel/mcp_fetch_run_receipt` 與 UI preview,只審核操作員 shell dry-run 後貼回的 receipt;API 不執行 CLI、不抓外站、不寫檔、不開 DB、不掛 scheduler,且會阻擋 secret/token 欄位與 side-effect flags。
- **V10.473 背景 embedding 讀取 host_health skip**: `OllamaService.generate_embedding(..., allow_111_fallback=False)` 會先查最近 `host_health_probes`;若 GCP-A/GCP-B 在 20 分鐘視窗內已由 runtime probe 標成 unhealthy,背景 embedding 直接跳過該節點並開短暫 GCP circuit,不等待 30 秒 timeout、不落 111。DB 讀取失敗時 fail-open 回原本 retry,避免觀測層阻斷 embedding。
diff --git a/routes/market_intel_mcp_run_routes.py b/routes/market_intel_mcp_run_routes.py
index 4cd2707..c4c534d 100644
--- a/routes/market_intel_mcp_run_routes.py
+++ b/routes/market_intel_mcp_run_routes.py
@@ -16,6 +16,9 @@ from services.market_intel.mcp_fetch_run_readiness import (
from services.market_intel.mcp_fetch_run_receipt import (
build_mcp_fetch_run_receipt_preview,
)
+from services.market_intel.mcp_fetch_result_parser_review import (
+ build_mcp_fetch_result_parser_review_preview,
+)
@market_intel_bp.route("/api/market_intel/mcp_fetch_run_package", methods=["GET", "POST"])
@@ -112,3 +115,46 @@ def market_intel_mcp_fetch_run_receipt():
phase=service.phase,
)
)
+
+
+@market_intel_bp.route(
+ "/api/market_intel/mcp_fetch_result_parser_review",
+ methods=["GET", "POST"],
+)
+@login_required
+def market_intel_mcp_fetch_result_parser_review():
+ run_receipt_package = {}
+ run_receipt_result = None
+ parser_result = None
+ if request.method == "POST":
+ payload = request.get_json(silent=True) or {}
+ package = (
+ payload.get("parser_review")
+ or payload.get("result_parser_review")
+ or payload
+ )
+ run_receipt_package = (
+ package.get("run_receipt_package")
+ or package.get("run_receipt")
+ or package.get("receipt_review")
+ or {}
+ )
+ run_receipt_result = (
+ package.get("run_receipt_result")
+ or package.get("mcp_fetch_run_receipt")
+ )
+ parser_result = (
+ package.get("parser_result")
+ or package.get("fetch_result_parser")
+ or package.get("parsed_result")
+ )
+
+ service = MarketIntelService()
+ return jsonify(
+ build_mcp_fetch_result_parser_review_preview(
+ run_receipt_package=run_receipt_package,
+ run_receipt_result=run_receipt_result,
+ parser_result=parser_result,
+ phase=service.phase,
+ )
+ )
diff --git a/services/market_intel/deployment_readiness.py b/services/market_intel/deployment_readiness.py
index 62cb961..62941e9 100644
--- a/services/market_intel/deployment_readiness.py
+++ b/services/market_intel/deployment_readiness.py
@@ -72,6 +72,9 @@ from services.market_intel.mcp_fetch_run_readiness import (
from services.market_intel.mcp_fetch_run_receipt import (
build_mcp_fetch_run_receipt_preview,
)
+from services.market_intel.mcp_fetch_result_parser_review import (
+ build_mcp_fetch_result_parser_review_preview,
+)
from services.market_intel.mcp_manual_fetch_handoff import (
build_mcp_manual_fetch_handoff_preview,
)
@@ -189,6 +192,11 @@ PRODUCTION_SMOKE_TARGETS = (
+ ("/api/market_intel/mcp_fetch_run_receipt",)
+ PRODUCTION_SMOKE_TARGETS[-1:]
)
+PRODUCTION_SMOKE_TARGETS = (
+ PRODUCTION_SMOKE_TARGETS[:-1]
+ + ("/api/market_intel/mcp_fetch_result_parser_review",)
+ + PRODUCTION_SMOKE_TARGETS[-1:]
+)
def _run_review_preview_safe(payload, mode):
return bool(payload["mode"] == mode and all(not payload.get(key) for key in BLOCKED_RUN_REVIEW_KEYS))
def build_deployment_readiness_preview(*, service, market_intel_tables, schema_smoke_builder):
@@ -225,6 +233,9 @@ def build_deployment_readiness_preview(*, service, market_intel_tables, schema_s
mcp_fetch_run_receipt = build_mcp_fetch_run_receipt_preview(
phase=service.phase,
)
+ mcp_fetch_result_parser_review = build_mcp_fetch_result_parser_review_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()
@@ -556,6 +567,26 @@ def build_deployment_readiness_preview(*, service, market_intel_tables, schema_s
and not mcp_fetch_run_receipt["file_written"]
and not mcp_fetch_run_receipt["scheduler_attached"]
),
+ "mcp_fetch_result_parser_review_preview_safe": bool(
+ mcp_fetch_result_parser_review["mode"]
+ == "mcp_fetch_result_parser_review_preview"
+ and not mcp_fetch_result_parser_review["payload_persisted"]
+ and not mcp_fetch_result_parser_review["result_parser_persisted"]
+ and not mcp_fetch_result_parser_review["result_parser_file_written"]
+ and not mcp_fetch_result_parser_review["candidate_handoff_persisted"]
+ and not mcp_fetch_result_parser_review["manual_fetch_gate_opened_by_api"]
+ and not mcp_fetch_result_parser_review["network_request_allowed"]
+ and not mcp_fetch_result_parser_review["parser_executed_by_api"]
+ and not mcp_fetch_result_parser_review["api_executes_cli"]
+ and not mcp_fetch_result_parser_review["api_opens_database_connection"]
+ and not mcp_fetch_result_parser_review["api_writes_database"]
+ and not mcp_fetch_result_parser_review["api_uses_external_network"]
+ and not mcp_fetch_result_parser_review["database_write_executed"]
+ and not mcp_fetch_result_parser_review["fetch_executed"]
+ and not mcp_fetch_result_parser_review["cli_executed"]
+ and not mcp_fetch_result_parser_review["file_written"]
+ and not mcp_fetch_result_parser_review["scheduler_attached"]
+ ),
"scheduler_plan_preview_safe": bool(
scheduler_plan["mode"] == "scheduler_attach_plan_preview"
and not scheduler_plan["scheduler_registration_executed"]
@@ -995,6 +1026,7 @@ def build_deployment_readiness_preview(*, service, market_intel_tables, schema_s
"mcp_fetch_run_package": mcp_fetch_run_package,
"mcp_fetch_run_readiness": mcp_fetch_run_readiness,
"mcp_fetch_run_receipt": mcp_fetch_run_receipt,
+ "mcp_fetch_result_parser_review": mcp_fetch_result_parser_review,
"scheduler_plan": scheduler_plan,
"manual_sample_plan": manual_sample_plan,
"manual_sample_acceptance": manual_sample_acceptance,
diff --git a/services/market_intel/mcp_fetch_result_parser_review.py b/services/market_intel/mcp_fetch_result_parser_review.py
new file mode 100644
index 0000000..5c893b5
--- /dev/null
+++ b/services/market_intel/mcp_fetch_result_parser_review.py
@@ -0,0 +1,786 @@
+"""市場情報 MCP manual fetch result parser review preview。
+
+本模組只審核操作員解析 dry-run fetch 結果後貼回的結構化摘要;
+不讀 artifact、不執行 parser CLI、不發 HTTP request、不寫 DB、不掛 scheduler。
+"""
+
+import ipaddress
+from urllib.parse import urlparse
+
+from services.market_intel.mcp_fetch_run_package import RUN_ARTIFACT_DIR_PREFIX
+from services.market_intel.mcp_fetch_run_receipt import (
+ build_mcp_fetch_run_receipt_preview,
+)
+
+
+MAX_CANDIDATES_PER_SOURCE = 10
+
+FORBIDDEN_SECRET_KEYS = (
+ "approval_token",
+ "approval-token",
+ "api_key",
+ "authorization",
+ "bearer",
+ "client_secret",
+ "cookie",
+ "password",
+ "secret",
+ "session_cookie",
+ "token",
+)
+
+SAFE_SECRET_METADATA_KEYS = {
+ "operator_confirmed_no_secret_payload",
+}
+
+SAFE_RAW_METADATA_KEYS = {
+ "operator_confirmed_no_raw_html",
+}
+
+FORBIDDEN_RAW_PAYLOAD_KEYS = (
+ "body_html",
+ "document_html",
+ "html",
+ "page_html",
+ "raw_body",
+ "raw_html",
+ "raw_snapshot",
+ "response_body",
+)
+
+_BLOCKED_SIDE_EFFECT_KEYS = (
+ "allow_api_execution",
+ "allow_database_write",
+ "allow_external_network_in_api",
+ "allow_scheduler_attach",
+ "api_executed_cli",
+ "api_executes_cli",
+ "api_executes_docker",
+ "api_executes_health_check",
+ "api_executes_ssh",
+ "api_opens_database_connection",
+ "api_uses_external_network",
+ "api_writes_database",
+ "attach_scheduler",
+ "candidate_handoff_persisted",
+ "cli_executed",
+ "command_executed",
+ "database_commit_executed",
+ "database_session_created",
+ "database_write_executed",
+ "external_network_executed",
+ "fetch_executed",
+ "file_written",
+ "manual_fetch_gate_opened_by_api",
+ "network_request_allowed",
+ "parser_artifact_written_by_api",
+ "parser_executed_by_api",
+ "payload_persisted",
+ "result_parser_file_written",
+ "result_parser_persisted",
+ "scheduler_attached",
+ "write_database",
+ "writes_executed",
+ "would_write_database",
+)
+
+
+def _as_dict(value):
+ return value if isinstance(value, dict) else {}
+
+
+def _as_list(value):
+ if value is None:
+ return []
+ if isinstance(value, (list, tuple, set)):
+ return list(value)
+ return [value]
+
+
+def _safe_int(value):
+ try:
+ return int(value or 0)
+ except (TypeError, ValueError):
+ return 0
+
+
+def _has_text(value):
+ return bool(isinstance(value, str) and value.strip())
+
+
+def _safe_text(value, limit=500):
+ if value is None:
+ return None
+ text = str(value).strip()
+ return text[:limit] if text else None
+
+
+def _safe_artifact_path(value, *, require_json=False):
+ if not isinstance(value, str):
+ return False
+ normalized = value.strip().replace("\\", "/")
+ if not normalized or normalized.startswith("/"):
+ return False
+ parts = [part for part in normalized.split("/") if part]
+ if any(part in (".", "..") for part in parts):
+ return False
+ if not normalized.startswith(RUN_ARTIFACT_DIR_PREFIX):
+ return False
+ return not require_json or normalized.endswith(".json")
+
+
+def _contains_forbidden_key(value, forbidden_keys, *, safe_keys=None):
+ safe_keys = safe_keys or set()
+ if isinstance(value, dict):
+ for key, nested in value.items():
+ normalized_key = str(key).lower()
+ if normalized_key in safe_keys and isinstance(nested, bool):
+ continue
+ if any(forbidden_key in normalized_key for forbidden_key in forbidden_keys):
+ return True
+ if _contains_forbidden_key(nested, forbidden_keys, safe_keys=safe_keys):
+ return True
+ elif isinstance(value, list):
+ return any(
+ _contains_forbidden_key(item, forbidden_keys, safe_keys=safe_keys)
+ for item in value
+ )
+ return False
+
+
+def _blocked_side_effects(payload):
+ found = []
+
+ 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_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 found
+
+
+def _is_public_http_url(value):
+ if not isinstance(value, str):
+ return False
+ parsed = urlparse(value.strip())
+ if parsed.scheme not in {"http", "https"} or not parsed.netloc:
+ return False
+ if parsed.username or parsed.password:
+ return False
+ host = (parsed.hostname or "").lower()
+ if host in {"localhost", "127.0.0.1", "::1"}:
+ return False
+ try:
+ ip = ipaddress.ip_address(host)
+ except ValueError:
+ return bool(host and "." in host)
+ return not (
+ ip.is_private
+ or ip.is_loopback
+ or ip.is_link_local
+ or ip.is_reserved
+ or ip.is_multicast
+ )
+
+
+def _run_receipt_from_inputs(run_receipt_package, run_receipt_result, phase):
+ if isinstance(run_receipt_result, dict) and run_receipt_result:
+ return run_receipt_result
+
+ run_receipt_package = _as_dict(run_receipt_package)
+ return build_mcp_fetch_run_receipt_preview(
+ run_readiness_package=(
+ run_receipt_package.get("run_readiness_package")
+ or run_receipt_package.get("run_readiness")
+ or run_receipt_package.get("readiness_package")
+ ),
+ run_readiness_result=(
+ run_receipt_package.get("run_readiness_result")
+ or run_receipt_package.get("mcp_fetch_run_readiness")
+ ),
+ manual_fetch_receipt=(
+ run_receipt_package.get("manual_fetch_receipt")
+ or run_receipt_package.get("fetch_receipt")
+ or run_receipt_package.get("receipt")
+ ),
+ phase=phase,
+ )
+
+
+def _sample_parser_result_package():
+ receipt_preview = build_mcp_fetch_run_receipt_preview()
+ run_receipt_package = receipt_preview["sample_run_receipt_package"]
+ run_receipt_result = build_mcp_fetch_run_receipt_preview(
+ run_readiness_package=run_receipt_package["run_readiness_package"],
+ run_readiness_result=run_receipt_package["run_readiness_result"],
+ manual_fetch_receipt=run_receipt_package["manual_fetch_receipt"],
+ )
+ receipt_sources = (
+ run_receipt_result.get("manual_fetch_receipt_summary", {}).get("sources")
+ or []
+ )
+ parsed_sources = []
+ for source in receipt_sources:
+ platform_code = source.get("platform_code")
+ source_key = source.get("source_key")
+ receipt_path = source.get("receipt_path")
+ parser_path = str(receipt_path or "").replace(
+ "_receipt.json",
+ "_parser_review.json",
+ )
+ campaign_key = f"{platform_code}:{source_key}:sample-campaign"
+ product_id = f"{platform_code}-{source_key}-sample-product"
+ parsed_sources.append(
+ {
+ "platform_code": platform_code,
+ "source_key": source_key,
+ "receipt_path": receipt_path,
+ "parser_artifact_path": parser_path,
+ "parser_status": "parsed_preview",
+ "parser_executed_by": "operator_shell",
+ "campaign_candidate_count": 1,
+ "product_candidate_count": 1,
+ "campaign_candidates": [
+ {
+ "candidate_type": "campaign",
+ "platform_code": platform_code,
+ "source_key": source_key,
+ "campaign_key": campaign_key,
+ "campaign_name": f"{source_key} sample campaign",
+ "campaign_url": source.get("source_url"),
+ "campaign_type": "manual_fetch_sample",
+ "confidence_band": "parser_preview",
+ }
+ ],
+ "product_candidates": [
+ {
+ "candidate_type": "product",
+ "platform_code": platform_code,
+ "source_key": source_key,
+ "platform_product_id": product_id,
+ "name": f"{source_key} sample product",
+ "product_url": source.get("source_url"),
+ "campaign_key": campaign_key,
+ "price": 100,
+ "currency": "TWD",
+ "confidence_band": "parser_preview",
+ }
+ ],
+ "api_executes_cli": False,
+ "api_uses_external_network": False,
+ "database_write_executed": False,
+ "scheduler_attached": False,
+ }
+ )
+
+ return {
+ "run_receipt_package": run_receipt_package,
+ "run_receipt_result": run_receipt_result,
+ "parser_result": {
+ "parser_run_id": "market-intel-parser-sample",
+ "parser_artifact_path": (
+ RUN_ARTIFACT_DIR_PREFIX
+ + "sample_market_intel_manual_fetch/parser_review.json"
+ ),
+ "parser_executed_by": "operator_shell",
+ "operator_confirmed_no_raw_html": True,
+ "operator_confirmed_no_secret_payload": True,
+ "operator_confirmed_no_api_execution": True,
+ "operator_confirmed_no_database_write": True,
+ "operator_confirmed_no_scheduler_attach": True,
+ "summary": {
+ "source_count": len(parsed_sources),
+ "campaign_candidate_count": len(parsed_sources),
+ "product_candidate_count": len(parsed_sources),
+ },
+ "parsed_sources": parsed_sources,
+ },
+ }
+
+
+def _receipt_summary(run_receipt):
+ run_receipt = _as_dict(run_receipt)
+ receipt = _as_dict(run_receipt.get("manual_fetch_receipt_summary"))
+ side_effects_clear = bool(
+ not run_receipt.get("manual_fetch_gate_opened_by_api")
+ and not run_receipt.get("network_request_allowed")
+ and not run_receipt.get("api_executes_cli")
+ and not run_receipt.get("api_opens_database_connection")
+ and not run_receipt.get("api_writes_database")
+ and not run_receipt.get("api_uses_external_network")
+ and not run_receipt.get("database_write_executed")
+ and not run_receipt.get("fetch_executed_by_api")
+ and not run_receipt.get("file_written")
+ and not run_receipt.get("scheduler_attached")
+ )
+ source_keys = sorted(
+ tuple(item)
+ for item in _as_list(receipt.get("source_keys"))
+ if isinstance(item, (list, tuple)) and len(item) == 2
+ )
+ receipt_paths = sorted(
+ path for path in _as_list(receipt.get("receipt_paths")) if path
+ )
+ return {
+ "mode": run_receipt.get("mode"),
+ "accepted": bool(run_receipt.get("mcp_fetch_run_receipt_accepted")),
+ "ready_for_manual_fetch_result_parser_review": bool(
+ run_receipt.get("ready_for_manual_fetch_result_parser_review")
+ ),
+ "source_count": _safe_int(run_receipt.get("receipt_source_count")),
+ "request_count": _safe_int(run_receipt.get("request_count")),
+ "error_count": _safe_int(run_receipt.get("error_count")),
+ "source_keys": source_keys,
+ "receipt_paths": receipt_paths,
+ "side_effects_clear": side_effects_clear,
+ "blocked_reasons": run_receipt.get("blocked_reasons", []),
+ }
+
+
+def _campaign_candidate_summary(candidate):
+ candidate = _as_dict(candidate)
+ return {
+ "candidate_type": candidate.get("candidate_type"),
+ "platform_code": _safe_text(candidate.get("platform_code"), 80),
+ "source_key": _safe_text(candidate.get("source_key"), 160),
+ "campaign_key": _safe_text(candidate.get("campaign_key"), 240),
+ "campaign_name": _safe_text(candidate.get("campaign_name"), 300),
+ "campaign_url": _safe_text(candidate.get("campaign_url"), 500),
+ "campaign_type": _safe_text(candidate.get("campaign_type"), 120),
+ "confidence_band": _safe_text(candidate.get("confidence_band"), 80),
+ "url_public": _is_public_http_url(candidate.get("campaign_url")),
+ "required_fields_present": bool(
+ candidate.get("candidate_type") == "campaign"
+ and _has_text(candidate.get("platform_code"))
+ and _has_text(candidate.get("source_key"))
+ and _has_text(candidate.get("campaign_key"))
+ and _has_text(candidate.get("campaign_name"))
+ and _has_text(candidate.get("campaign_url"))
+ ),
+ }
+
+
+def _product_candidate_summary(candidate):
+ candidate = _as_dict(candidate)
+ price = candidate.get("price")
+ price_ok = isinstance(price, (int, float)) and price >= 0
+ return {
+ "candidate_type": candidate.get("candidate_type"),
+ "platform_code": _safe_text(candidate.get("platform_code"), 80),
+ "source_key": _safe_text(candidate.get("source_key"), 160),
+ "platform_product_id": _safe_text(candidate.get("platform_product_id"), 240),
+ "name": _safe_text(candidate.get("name"), 300),
+ "product_url": _safe_text(candidate.get("product_url"), 500),
+ "campaign_key": _safe_text(candidate.get("campaign_key"), 240),
+ "price": price,
+ "currency": _safe_text(candidate.get("currency"), 20),
+ "confidence_band": _safe_text(candidate.get("confidence_band"), 80),
+ "url_public": _is_public_http_url(candidate.get("product_url")),
+ "price_valid": price_ok,
+ "required_fields_present": bool(
+ candidate.get("candidate_type") == "product"
+ and _has_text(candidate.get("platform_code"))
+ and _has_text(candidate.get("source_key"))
+ and _has_text(candidate.get("platform_product_id"))
+ and _has_text(candidate.get("name"))
+ and _has_text(candidate.get("product_url"))
+ and price_ok
+ ),
+ }
+
+
+def _parser_result_summary(parser_result):
+ parser_result = _as_dict(parser_result)
+ parsed_sources = [_as_dict(item) for item in _as_list(
+ parser_result.get("parsed_sources")
+ )]
+ source_summaries = []
+ campaign_candidates = []
+ product_candidates = []
+ for source in parsed_sources:
+ campaigns = [
+ _campaign_candidate_summary(item)
+ for item in _as_list(source.get("campaign_candidates"))
+ ]
+ products = [
+ _product_candidate_summary(item)
+ for item in _as_list(source.get("product_candidates"))
+ ]
+ campaign_candidates.extend(campaigns)
+ product_candidates.extend(products)
+ source_summaries.append(
+ {
+ "platform_code": _safe_text(source.get("platform_code"), 80),
+ "source_key": _safe_text(source.get("source_key"), 160),
+ "receipt_path": _safe_text(source.get("receipt_path"), 500),
+ "receipt_path_safe": _safe_artifact_path(
+ source.get("receipt_path"),
+ require_json=True,
+ ),
+ "parser_artifact_path": _safe_text(
+ source.get("parser_artifact_path"),
+ 500,
+ ),
+ "parser_artifact_path_safe": _safe_artifact_path(
+ source.get("parser_artifact_path"),
+ require_json=True,
+ ),
+ "parser_status": _safe_text(source.get("parser_status"), 80),
+ "parser_executed_by": _safe_text(source.get("parser_executed_by"), 80),
+ "campaign_candidate_count": _safe_int(
+ source.get("campaign_candidate_count")
+ ),
+ "product_candidate_count": _safe_int(
+ source.get("product_candidate_count")
+ ),
+ "observed_campaign_candidate_count": len(campaigns),
+ "observed_product_candidate_count": len(products),
+ "campaign_candidates": campaigns,
+ "product_candidates": products,
+ }
+ )
+
+ summary = _as_dict(parser_result.get("summary"))
+ campaign_count = len(campaign_candidates)
+ product_count = len(product_candidates)
+ return {
+ "provided_keys": sorted(parser_result.keys()),
+ "parser_run_id": _safe_text(parser_result.get("parser_run_id"), 160),
+ "parser_artifact_path": _safe_text(parser_result.get("parser_artifact_path"), 500),
+ "parser_artifact_path_safe": _safe_artifact_path(
+ parser_result.get("parser_artifact_path"),
+ require_json=True,
+ ),
+ "parser_executed_by": _safe_text(parser_result.get("parser_executed_by"), 80),
+ "operator_confirmed_no_raw_html": bool(
+ parser_result.get("operator_confirmed_no_raw_html")
+ ),
+ "operator_confirmed_no_secret_payload": bool(
+ parser_result.get("operator_confirmed_no_secret_payload")
+ ),
+ "operator_confirmed_no_api_execution": bool(
+ parser_result.get("operator_confirmed_no_api_execution")
+ ),
+ "operator_confirmed_no_database_write": bool(
+ parser_result.get("operator_confirmed_no_database_write")
+ ),
+ "operator_confirmed_no_scheduler_attach": bool(
+ parser_result.get("operator_confirmed_no_scheduler_attach")
+ ),
+ "source_count": len(source_summaries),
+ "summary_source_count": _safe_int(summary.get("source_count")),
+ "campaign_candidate_count": campaign_count,
+ "summary_campaign_candidate_count": _safe_int(
+ summary.get("campaign_candidate_count")
+ ),
+ "product_candidate_count": product_count,
+ "summary_product_candidate_count": _safe_int(
+ summary.get("product_candidate_count")
+ ),
+ "candidate_count": campaign_count + product_count,
+ "source_keys": sorted(
+ (
+ item["platform_code"],
+ item["source_key"],
+ )
+ for item in source_summaries
+ if item["platform_code"] and item["source_key"]
+ ),
+ "receipt_paths": sorted(
+ item["receipt_path"] for item in source_summaries if item["receipt_path"]
+ ),
+ "source_summaries": source_summaries,
+ "campaign_candidates": campaign_candidates,
+ "product_candidates": product_candidates,
+ "all_parser_artifact_paths_safe": bool(
+ source_summaries
+ and all(item["parser_artifact_path_safe"] for item in source_summaries)
+ ),
+ "all_receipt_paths_safe": bool(
+ source_summaries and all(item["receipt_path_safe"] for item in source_summaries)
+ ),
+ "all_status_parsed_preview": bool(
+ source_summaries
+ and all(item["parser_status"] == "parsed_preview" for item in source_summaries)
+ ),
+ "all_executed_by_operator_shell": bool(
+ parser_result.get("parser_executed_by") == "operator_shell"
+ and source_summaries
+ and all(item["parser_executed_by"] == "operator_shell" for item in source_summaries)
+ ),
+ "all_candidate_urls_public": bool(
+ campaign_candidates + product_candidates
+ and all(item["url_public"] for item in campaign_candidates + product_candidates)
+ ),
+ "all_candidate_required_fields_present": bool(
+ campaign_candidates + product_candidates
+ and all(
+ item["required_fields_present"]
+ for item in campaign_candidates + product_candidates
+ )
+ ),
+ "source_counts_match_candidates": bool(
+ source_summaries
+ and all(
+ item["campaign_candidate_count"]
+ == item["observed_campaign_candidate_count"]
+ and item["product_candidate_count"]
+ == item["observed_product_candidate_count"]
+ for item in source_summaries
+ )
+ ),
+ "summary_counts_match_candidates": bool(
+ _safe_int(summary.get("source_count")) == len(source_summaries)
+ and _safe_int(summary.get("campaign_candidate_count")) == campaign_count
+ and _safe_int(summary.get("product_candidate_count")) == product_count
+ ),
+ "raw_payload_submitted_to_api": _contains_forbidden_key(
+ parser_result,
+ FORBIDDEN_RAW_PAYLOAD_KEYS,
+ safe_keys=SAFE_RAW_METADATA_KEYS,
+ ),
+ "secret_or_token_submitted_to_api": _contains_forbidden_key(
+ parser_result,
+ FORBIDDEN_SECRET_KEYS,
+ safe_keys=SAFE_SECRET_METADATA_KEYS,
+ ),
+ "blocked_side_effects": _blocked_side_effects(parser_result),
+ }
+
+
+def _parser_gates(
+ *,
+ run_receipt_received,
+ parser_result_received,
+ parser_result_valid_object,
+ receipt,
+ parser,
+):
+ receipt_source_keys = set(receipt["source_keys"])
+ parser_source_keys = set(parser["source_keys"])
+ receipt_paths = set(receipt["receipt_paths"])
+ parser_receipt_paths = set(parser["receipt_paths"])
+ max_candidates = max(1, receipt["source_count"]) * MAX_CANDIDATES_PER_SOURCE
+ operator_boundaries_confirmed = bool(
+ parser["operator_confirmed_no_raw_html"]
+ and parser["operator_confirmed_no_secret_payload"]
+ and parser["operator_confirmed_no_api_execution"]
+ and parser["operator_confirmed_no_database_write"]
+ and parser["operator_confirmed_no_scheduler_attach"]
+ )
+ return [
+ {
+ "key": "run_receipt_payload_or_result_received",
+ "label": "已提供 run receipt package 或已審核結果",
+ "passed": run_receipt_received,
+ },
+ {
+ "key": "run_receipt_accepted",
+ "label": "run receipt gate 必須已通過",
+ "passed": receipt["accepted"],
+ },
+ {
+ "key": "run_receipt_ready_for_parser_review",
+ "label": "receipt 只放行到 result parser review",
+ "passed": receipt["ready_for_manual_fetch_result_parser_review"],
+ },
+ {
+ "key": "run_receipt_side_effect_free",
+ "label": "receipt 結果未顯示 API 執行、連外、寫檔、寫 DB 或掛 scheduler",
+ "passed": receipt["side_effects_clear"],
+ },
+ {
+ "key": "parser_result_received",
+ "label": "已提供 parser result 結構化摘要",
+ "passed": parser_result_received,
+ },
+ {
+ "key": "parser_result_valid_object",
+ "label": "parser result payload 必須是 JSON object",
+ "passed": parser_result_valid_object,
+ },
+ {
+ "key": "parser_identity_recorded",
+ "label": "parser result 必須記錄 parser_run_id 與 artifact path",
+ "passed": bool(parser["parser_run_id"] and parser["parser_artifact_path_safe"]),
+ },
+ {
+ "key": "parser_sources_match_receipt_sources",
+ "label": "parser source 必須完全對齊 receipt platform/source",
+ "passed": bool(receipt_source_keys and parser_source_keys == receipt_source_keys),
+ },
+ {
+ "key": "parser_receipt_paths_match_receipt",
+ "label": "parser 引用的 receipt path 必須完全對齊 receipt gate",
+ "passed": bool(receipt_paths and parser_receipt_paths == receipt_paths),
+ },
+ {
+ "key": "parser_artifact_paths_safe",
+ "label": "parser artifact path 必須在 market intel manual fetch 相對目錄內",
+ "passed": bool(
+ parser["parser_artifact_path_safe"]
+ and parser["all_parser_artifact_paths_safe"]
+ and parser["all_receipt_paths_safe"]
+ ),
+ },
+ {
+ "key": "parser_status_parsed_preview",
+ "label": "每個 source 都必須回報 parsed_preview",
+ "passed": parser["all_status_parsed_preview"],
+ },
+ {
+ "key": "parser_executed_by_operator_shell",
+ "label": "parser result 必須標明由操作員 shell 產生,不是 API",
+ "passed": parser["all_executed_by_operator_shell"],
+ },
+ {
+ "key": "parser_candidate_counts_within_preview_limit",
+ "label": "候選數需維持小批次 preview 上限",
+ "passed": bool(0 < parser["candidate_count"] <= max_candidates),
+ },
+ {
+ "key": "parser_candidate_counts_match_summary",
+ "label": "summary / source count 必須與候選陣列數量一致",
+ "passed": bool(
+ parser["source_counts_match_candidates"]
+ and parser["summary_counts_match_candidates"]
+ ),
+ },
+ {
+ "key": "parser_candidate_required_fields_present",
+ "label": "活動/商品候選必須具備必要識別欄位",
+ "passed": parser["all_candidate_required_fields_present"],
+ },
+ {
+ "key": "parser_candidate_urls_public_http",
+ "label": "活動/商品候選 URL 必須是公開 http(s) URL,且不含帳密",
+ "passed": parser["all_candidate_urls_public"],
+ },
+ {
+ "key": "parser_operator_boundaries_confirmed",
+ "label": "操作員確認無 raw HTML、無 secret、無 API/DB/scheduler 副作用",
+ "passed": operator_boundaries_confirmed,
+ },
+ {
+ "key": "parser_no_raw_payload",
+ "label": "parser result 不得回貼 raw HTML/body/snapshot",
+ "passed": not parser["raw_payload_submitted_to_api"],
+ },
+ {
+ "key": "parser_no_secret_or_token_key",
+ "label": "parser result 不得包含 secret、cookie、password 或 token key",
+ "passed": not parser["secret_or_token_submitted_to_api"],
+ },
+ {
+ "key": "parser_side_effect_free",
+ "label": "parser result 不得要求 API 執行、連外、寫檔、寫 DB 或掛 scheduler",
+ "passed": not parser["blocked_side_effects"],
+ },
+ ]
+
+
+def build_mcp_fetch_result_parser_review_preview(
+ *,
+ run_receipt_package=None,
+ run_receipt_result=None,
+ parser_result=None,
+ phase=None,
+):
+ """建立 fetch result parser review;不讀 artifact 或執行 parser。"""
+ run_receipt_package = _as_dict(run_receipt_package)
+ run_receipt_result_received = bool(
+ isinstance(run_receipt_result, dict) and run_receipt_result
+ )
+ parser_result_valid_object = isinstance(parser_result, dict) if (
+ parser_result is not None
+ ) else True
+ parser_payload = _as_dict(parser_result)
+ run_receipt = _run_receipt_from_inputs(
+ run_receipt_package,
+ run_receipt_result,
+ phase,
+ )
+ run_receipt_received = bool(run_receipt_package or run_receipt_result_received)
+ parser_payload_received = bool(
+ run_receipt_received or parser_payload or parser_result is not None
+ )
+ parser_result_received = bool(parser_payload)
+ receipt = _receipt_summary(run_receipt)
+ parser = _parser_result_summary(parser_payload)
+ gates = _parser_gates(
+ run_receipt_received=run_receipt_received,
+ parser_result_received=parser_result_received,
+ parser_result_valid_object=parser_result_valid_object,
+ receipt=receipt,
+ parser=parser,
+ )
+ blocked_reasons = [gate["key"] for gate in gates if not gate["passed"]]
+ accepted = bool(parser_payload_received and not blocked_reasons)
+
+ return {
+ "mode": (
+ "mcp_fetch_result_parser_review"
+ if parser_payload_received
+ else "mcp_fetch_result_parser_review_preview"
+ ),
+ "phase": phase,
+ "parser_payload_received": parser_payload_received,
+ "run_receipt_received": run_receipt_received,
+ "parser_result_received": parser_result_received,
+ "parser_result_valid_object": parser_result_valid_object,
+ "run_receipt_accepted": receipt["accepted"],
+ "mcp_fetch_result_parser_review_accepted": accepted,
+ "result_parser_review_ready": accepted,
+ "ready_for_manual_fetch_candidate_handoff_review": accepted,
+ "ready_for_api_database_write": False,
+ "ready_for_scheduler_attach": False,
+ "manual_fetch_gate_opened_by_api": False,
+ "network_request_allowed": False,
+ "parser_executed_by_api": False,
+ "fetch_executed": False,
+ "cli_executed": False,
+ "database_write_executed": False,
+ "scheduler_attached": False,
+ "receipt_source_count": receipt["source_count"],
+ "parser_source_count": parser["source_count"],
+ "candidate_count": parser["candidate_count"],
+ "campaign_candidate_count": parser["campaign_candidate_count"],
+ "product_candidate_count": parser["product_candidate_count"],
+ "gate_count": len(gates),
+ "passed_gate_count": sum(1 for gate in gates if gate["passed"]),
+ "blocked_reasons": blocked_reasons,
+ "gates": gates,
+ "run_receipt_summary": receipt,
+ "parser_result_summary": parser,
+ "sample_parser_result_package": _sample_parser_result_package(),
+ "next_operator_steps": [
+ "parser review 通過後,只代表可進候選 handoff review,不代表可寫 market_*",
+ "候選 handoff、DB import、scheduler attach、AI/Telegram 摘要都必須另開獨立 gate",
+ "API/UI 仍不得讀 artifact、不得執行 parser CLI、不得抓外站、不得寫 DB、不得掛 scheduler",
+ ],
+ "payload_persisted": False,
+ "result_parser_persisted": False,
+ "result_parser_file_written": False,
+ "candidate_handoff_persisted": False,
+ "api_executes_health_check": False,
+ "api_executes_docker": False,
+ "api_executes_ssh": False,
+ "api_executes_cli": False,
+ "api_opens_database_connection": False,
+ "api_writes_database": False,
+ "api_uses_external_network": False,
+ "database_session_created": False,
+ "database_commit_executed": False,
+ "external_network_executed": False,
+ "file_written": False,
+ "writes_executed": False,
+ "would_write_database": False,
+ }
diff --git a/services/market_intel/phase.py b/services/market_intel/phase.py
index 276d32f..ed2ab4f 100644
--- a/services/market_intel/phase.py
+++ b/services/market_intel/phase.py
@@ -1,3 +1,3 @@
"""市場情報 rollout phase 單一來源。"""
-MARKET_INTEL_PHASE = "phase_124_market_intel_mcp_fetch_run_receipt"
+MARKET_INTEL_PHASE = "phase_125_market_intel_mcp_fetch_result_parser_review"
diff --git a/templates/market_intel/disabled.html b/templates/market_intel/disabled.html
index 3331654..ad7a18e 100644
--- a/templates/market_intel/disabled.html
+++ b/templates/market_intel/disabled.html
@@ -760,6 +760,32 @@
+
+
+
+
MCP / FETCH RESULT PARSER
+
MCP Fetch Result Parser 審核
+
+
+
+
+ loading
+
+
+
讀取 MCP Fetch Result Parser 審核中...
+
+
+
+
@@ -1272,6 +1298,7 @@
const mcpFetchRunPackageRoot = document.querySelector('[data-market-intel-mcp-fetch-run-package]');
const mcpFetchRunReadinessRoot = document.querySelector('[data-market-intel-mcp-fetch-run-readiness]');
const mcpFetchRunReceiptRoot = document.querySelector('[data-market-intel-mcp-fetch-run-receipt]');
+ const mcpFetchResultParserReviewRoot = document.querySelector('[data-market-intel-mcp-fetch-result-parser-review]');
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]');
@@ -1288,7 +1315,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 && !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 && !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;
@@ -1383,6 +1410,12 @@
const mcpFetchRunReceiptReview = mcpFetchRunReceiptRoot ? mcpFetchRunReceiptRoot.querySelector('[data-market-intel-mcp-fetch-run-receipt-review]') : null;
const mcpFetchRunReceiptRefresh = mcpFetchRunReceiptRoot ? mcpFetchRunReceiptRoot.querySelector('[data-market-intel-mcp-fetch-run-receipt-refresh]') : null;
const mcpFetchRunReceiptEndpoint = "{{ url_for('market_intel.market_intel_mcp_fetch_run_receipt') }}";
+ const mcpFetchResultParserReviewMeta = mcpFetchResultParserReviewRoot ? mcpFetchResultParserReviewRoot.querySelector('[data-market-intel-mcp-fetch-result-parser-review-meta]') : null;
+ const mcpFetchResultParserReviewBody = mcpFetchResultParserReviewRoot ? mcpFetchResultParserReviewRoot.querySelector('[data-market-intel-mcp-fetch-result-parser-review-body]') : null;
+ const mcpFetchResultParserReviewInput = mcpFetchResultParserReviewRoot ? mcpFetchResultParserReviewRoot.querySelector('[data-market-intel-mcp-fetch-result-parser-review-input]') : null;
+ const mcpFetchResultParserReviewReview = mcpFetchResultParserReviewRoot ? mcpFetchResultParserReviewRoot.querySelector('[data-market-intel-mcp-fetch-result-parser-review-review]') : null;
+ const mcpFetchResultParserReviewRefresh = mcpFetchResultParserReviewRoot ? mcpFetchResultParserReviewRoot.querySelector('[data-market-intel-mcp-fetch-result-parser-review-refresh]') : null;
+ const mcpFetchResultParserReviewEndpoint = "{{ url_for('market_intel.market_intel_mcp_fetch_result_parser_review') }}";
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;
@@ -3320,6 +3353,146 @@
}
};
+ const renderMcpFetchResultParserReviewMeta = data => {
+ mcpFetchResultParserReviewMeta.innerHTML = [
+ `mode=${data.mode || 'unknown'}`,
+ `accepted=${data.mcp_fetch_result_parser_review_accepted ? 'yes' : 'no'}`,
+ `gates=${data.passed_gate_count || 0}/${data.gate_count || 0}`,
+ `sources=${data.parser_source_count || 0}`,
+ `candidates=${data.candidate_count || 0}`,
+ `api_network=${data.network_request_allowed ? 'open' : 'closed'}`
+ ].map(item => `
${escapeHtml(item)}`).join('');
+ };
+
+ const renderMcpFetchResultParserReviewBody = data => {
+ const blockers = (data.blocked_reasons || []).join(' / ');
+ const gates = data.gates || [];
+ const parser = data.parser_result_summary || {};
+ const receipt = data.run_receipt_summary || {};
+ const sources = parser.source_summaries || [];
+ const campaigns = parser.campaign_candidates || [];
+ const products = parser.product_candidates || [];
+ const steps = data.next_operator_steps || [];
+ const renderCheck = (key, label, status) => `
+
+
+ ${escapeHtml(key)}
+ ${escapeHtml(label || '')}
+
+
${escapeHtml(status)}
+
+ `;
+ mcpFetchResultParserReviewBody.innerHTML = `
+
此 parser review 只審核操作員貼回的結構化摘要;API 不讀 artifact、不執行 parser、不抓外站、不寫 DB、不掛 scheduler。${blockers ? `阻擋:${escapeHtml(blockers)}` : ''}
+
+
+
PARSER GATES
+
${
+ gates.length
+ ? gates.map(item => renderCheck(item.key, item.label, item.passed ? 'PASS' : 'BLOCK')).join('')
+ : '
尚未提供 parser gates。
'
+ }
+
+
+
PARSER SUMMARY
+
+ ${renderCheck('parser_run_id', parser.parser_run_id || 'missing', parser.parser_run_id ? 'RECORDED' : 'MISSING')}
+ ${renderCheck('artifact_path', parser.parser_artifact_path || 'missing', parser.parser_artifact_path_safe ? 'SAFE' : 'BLOCK')}
+ ${renderCheck('counts', `campaigns=${parser.campaign_candidate_count || 0} / products=${parser.product_candidate_count || 0}`, parser.summary_counts_match_candidates ? 'MATCHED' : 'PENDING')}
+ ${renderCheck('raw_payload', 'no raw HTML/body/snapshot submitted to API', parser.raw_payload_submitted_to_api ? 'BLOCK' : 'CLEAR')}
+
+
+
+
SOURCE PARSING
+
${
+ sources.length
+ ? sources.map(item => renderCheck(
+ `${item.platform_code || 'unknown'}:${item.source_key || 'source'}`,
+ `${item.parser_artifact_path || 'no artifact'} / campaigns=${item.observed_campaign_candidate_count || 0} / products=${item.observed_product_candidate_count || 0}`,
+ item.parser_status === 'parsed_preview' ? 'PARSED' : 'BLOCK'
+ )).join('')
+ : '
尚未提供 parsed source summary。
'
+ }
+
+
+
CANDIDATE PREVIEW
+
${
+ campaigns.concat(products).slice(0, 8).map(item => renderCheck(
+ item.campaign_key || item.platform_product_id || 'candidate',
+ item.campaign_name || item.name || item.campaign_url || item.product_url || '',
+ item.required_fields_present ? 'READY' : 'BLOCK'
+ )).join('') || '
尚未提供活動或商品候選。
'
+ }
+
+
+
RECEIPT / NEXT
+
+ ${renderCheck(
+ 'run_receipt',
+ `${data.run_receipt_accepted ? 'accepted' : 'pending'} / sources=${receipt.source_count || 0}`,
+ data.run_receipt_accepted ? 'ACCEPTED' : 'PENDING'
+ )}
+ ${renderCheck(
+ 'api_boundary',
+ 'no parser CLI / no artifact read / no external request / no DB / no scheduler',
+ data.api_executes_cli ? 'BLOCK' : 'CLOSED'
+ )}
+ ${steps.map((item, index) => renderCheck(`step_${index + 1}`, item, 'NEXT')).join('')}
+
+
+
+ `;
+ if (mcpFetchResultParserReviewInput && !mcpFetchResultParserReviewInput.value.trim() && data.sample_parser_result_package) {
+ mcpFetchResultParserReviewInput.value = JSON.stringify(data.sample_parser_result_package, null, 2);
+ }
+ };
+
+ const loadMcpFetchResultParserReview = async () => {
+ if (!mcpFetchResultParserReviewMeta || !mcpFetchResultParserReviewBody) return;
+ mcpFetchResultParserReviewBody.innerHTML = '
讀取 MCP Fetch Result Parser 審核中...
';
+ try {
+ const response = await fetch(mcpFetchResultParserReviewEndpoint, { credentials: 'same-origin' });
+ if (!response.ok) throw new Error(`HTTP ${response.status}`);
+ const data = await response.json();
+ renderMcpFetchResultParserReviewMeta(data);
+ renderMcpFetchResultParserReviewBody(data);
+ } catch (error) {
+ mcpFetchResultParserReviewMeta.innerHTML = '
error';
+ mcpFetchResultParserReviewBody.innerHTML = `
MCP Fetch Result Parser 審核讀取失敗:${escapeHtml(error.message)}
`;
+ }
+ };
+
+ const reviewMcpFetchResultParserReview = async () => {
+ if (!mcpFetchResultParserReviewMeta || !mcpFetchResultParserReviewBody || !mcpFetchResultParserReviewInput) return;
+ let parsed;
+ try {
+ parsed = JSON.parse(mcpFetchResultParserReviewInput.value || '{}');
+ } catch (error) {
+ mcpFetchResultParserReviewMeta.innerHTML = '
json_error';
+ mcpFetchResultParserReviewBody.innerHTML = `
JSON 格式錯誤:${escapeHtml(error.message)}
`;
+ return;
+ }
+ mcpFetchResultParserReviewBody.innerHTML = '
審核 MCP Fetch Result Parser 中...
';
+ try {
+ const response = await fetch(mcpFetchResultParserReviewEndpoint, {
+ method: 'POST',
+ credentials: 'same-origin',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'X-CSRFToken': csrfToken
+ },
+ body: JSON.stringify({ parser_review: parsed })
+ });
+ const data = await response.json();
+ if (!response.ok && !data.mode) throw new Error(`HTTP ${response.status}`);
+ renderMcpFetchResultParserReviewMeta(data);
+ renderMcpFetchResultParserReviewBody(data);
+ } catch (error) {
+ mcpFetchResultParserReviewMeta.innerHTML = '
error';
+ mcpFetchResultParserReviewBody.innerHTML = `
MCP Fetch Result Parser 審核失敗:${escapeHtml(error.message)}
`;
+ }
+ };
+
const renderManualSampleMeta = data => {
manualSampleMeta.innerHTML = [
`mode=${data.mode || 'unknown'}`,
@@ -12771,6 +12944,12 @@
if (mcpFetchRunReceiptReview) {
mcpFetchRunReceiptReview.addEventListener('click', reviewMcpFetchRunReceipt);
}
+ if (mcpFetchResultParserReviewRefresh) {
+ mcpFetchResultParserReviewRefresh.addEventListener('click', loadMcpFetchResultParserReview);
+ }
+ if (mcpFetchResultParserReviewReview) {
+ mcpFetchResultParserReviewReview.addEventListener('click', reviewMcpFetchResultParserReview);
+ }
if (manualSampleRefresh) {
manualSampleRefresh.addEventListener('click', loadManualSample);
}
@@ -13033,6 +13212,7 @@
loadMcpFetchRunPackage();
loadMcpFetchRunReadiness();
loadMcpFetchRunReceipt();
+ loadMcpFetchResultParserReview();
loadManualSample();
loadSampleAcceptance();
loadSampleReview();
diff --git a/tests/test_market_intel_skeleton.py b/tests/test_market_intel_skeleton.py
index b95debc..0fae4f4 100644
--- a/tests/test_market_intel_skeleton.py
+++ b/tests/test_market_intel_skeleton.py
@@ -29,6 +29,9 @@ from services.market_intel.mcp_fetch_run_readiness import (
from services.market_intel.mcp_fetch_run_receipt import (
build_mcp_fetch_run_receipt_preview,
)
+from services.market_intel.mcp_fetch_result_parser_review import (
+ build_mcp_fetch_result_parser_review_preview,
+)
from services.market_intel.mcp_fetch_target_review import (
build_mcp_fetch_target_review_preview,
)
@@ -991,6 +994,13 @@ def test_market_intel_preview_template_uses_safe_fetch_false_endpoint():
assert "data-market-intel-mcp-fetch-run-receipt-receipt" in template
assert "data-market-intel-mcp-fetch-run-receipt-sources" in template
assert "data-market-intel-mcp-fetch-run-receipt-next" in template
+ assert "market_intel.market_intel_mcp_fetch_result_parser_review" in template
+ assert "data-market-intel-mcp-fetch-result-parser-review" in template
+ assert "data-market-intel-mcp-fetch-result-parser-review-gates" in template
+ assert "data-market-intel-mcp-fetch-result-parser-review-parser" in template
+ assert "data-market-intel-mcp-fetch-result-parser-review-sources" in template
+ assert "data-market-intel-mcp-fetch-result-parser-review-candidates" in template
+ assert "data-market-intel-mcp-fetch-result-parser-review-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
@@ -1461,7 +1471,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_124_market_intel_mcp_fetch_run_receipt"
+ assert bridge["phase"] == "phase_125_market_intel_mcp_fetch_result_parser_review"
assert bridge["execute_requested"] is False
assert bridge["read_only_query_executed"] is False
assert bridge["database_connection_opened"] is False
@@ -1619,7 +1629,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_124_market_intel_mcp_fetch_run_receipt"
+ assert contract["phase"] == "phase_125_market_intel_mcp_fetch_result_parser_review"
assert contract["caller"] == "market_intel"
assert contract["contract_ready"] is True
assert contract["blocked_reasons"] == []
@@ -1752,7 +1762,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_124_market_intel_mcp_fetch_run_receipt"
+ assert data["phase"] == "phase_125_market_intel_mcp_fetch_result_parser_review"
assert data["deployment_actions_executed"] is False
assert data["docker_command_executed"] is False
assert data["ssh_command_executed"] is False
@@ -1765,7 +1775,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_124_market_intel_mcp_fetch_run_receipt"
+ assert gate["phase"] == "phase_125_market_intel_mcp_fetch_result_parser_review"
assert gate["fetch_requested"] is True
assert gate["manual_fetch_gate_open"] is False
assert gate["network_request_allowed"] is False
@@ -1835,7 +1845,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_124_market_intel_mcp_fetch_run_receipt"
+ assert data["phase"] == "phase_125_market_intel_mcp_fetch_result_parser_review"
assert data["fetch_requested"] is False
assert data["network_request_allowed"] is False
assert data["external_network_executed"] is False
@@ -1849,7 +1859,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_124_market_intel_mcp_fetch_run_receipt"
+ assert audit["phase"] == "phase_125_market_intel_mcp_fetch_result_parser_review"
assert audit["audit_ready_for_operator_review"] is True
assert audit["audit_preview_safe"] is True
assert audit["external_mcp_runtime_complete"] is False
@@ -1923,7 +1933,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_124_market_intel_mcp_fetch_run_receipt"
+ assert data["phase"] == "phase_125_market_intel_mcp_fetch_result_parser_review"
assert data["audit_preview_safe"] is True
assert data["external_mcp_runtime_complete"] is False
assert data["internal_mcp_contract_complete"] is True
@@ -1940,11 +1950,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_124_market_intel_mcp_fetch_run_receipt"
+ phase="phase_125_market_intel_mcp_fetch_result_parser_review"
)
assert evidence["mode"] == "mcp_activation_evidence_preview"
- assert evidence["phase"] == "phase_124_market_intel_mcp_fetch_run_receipt"
+ assert evidence["phase"] == "phase_125_market_intel_mcp_fetch_result_parser_review"
assert evidence["evidence_payload_received"] is False
assert evidence["activation_evidence_accepted"] is False
assert evidence["ready_for_runtime_promotion"] is False
@@ -1969,7 +1979,7 @@ def test_mcp_activation_evidence_accepts_redacted_runtime_evidence():
]
evidence = build_mcp_activation_evidence_preview(
evidence=sample,
- phase="phase_124_market_intel_mcp_fetch_run_receipt",
+ phase="phase_125_market_intel_mcp_fetch_result_parser_review",
)
assert evidence["mode"] == "mcp_activation_evidence_review"
@@ -2027,12 +2037,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_124_market_intel_mcp_fetch_run_receipt"
+ assert get_data["phase"] == "phase_125_market_intel_mcp_fetch_result_parser_review"
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_124_market_intel_mcp_fetch_run_receipt"
+ assert post_data["phase"] == "phase_125_market_intel_mcp_fetch_result_parser_review"
assert post_data["activation_evidence_accepted"] is True
assert post_data["payload_persisted"] is False
assert post_data["api_opens_database_connection"] is False
@@ -2041,11 +2051,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_124_market_intel_mcp_fetch_run_receipt"
+ phase="phase_125_market_intel_mcp_fetch_result_parser_review"
)
assert receipt["mode"] == "mcp_runtime_smoke_receipt_preview"
- assert receipt["phase"] == "phase_124_market_intel_mcp_fetch_run_receipt"
+ assert receipt["phase"] == "phase_125_market_intel_mcp_fetch_result_parser_review"
assert receipt["receipt_payload_received"] is False
assert receipt["runtime_smoke_receipt_accepted"] is False
assert receipt["ready_for_completion_runtime_promotion"] is False
@@ -2073,7 +2083,7 @@ def test_mcp_runtime_smoke_receipt_accepts_complete_readiness_receipt():
]
receipt = build_mcp_runtime_smoke_receipt_preview(
receipt=sample,
- phase="phase_124_market_intel_mcp_fetch_run_receipt",
+ phase="phase_125_market_intel_mcp_fetch_result_parser_review",
)
assert receipt["mode"] == "mcp_runtime_smoke_receipt_review"
@@ -2138,12 +2148,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_124_market_intel_mcp_fetch_run_receipt"
+ assert get_data["phase"] == "phase_125_market_intel_mcp_fetch_result_parser_review"
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_124_market_intel_mcp_fetch_run_receipt"
+ assert post_data["phase"] == "phase_125_market_intel_mcp_fetch_result_parser_review"
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
@@ -2152,11 +2162,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_124_market_intel_mcp_fetch_run_receipt"
+ phase="phase_125_market_intel_mcp_fetch_result_parser_review"
)
assert promotion["mode"] == "mcp_runtime_promotion_preview"
- assert promotion["phase"] == "phase_124_market_intel_mcp_fetch_run_receipt"
+ assert promotion["phase"] == "phase_125_market_intel_mcp_fetch_result_parser_review"
assert promotion["promotion_payload_received"] is False
assert promotion["runtime_promotion_accepted"] is False
assert promotion["ready_for_completion_runtime_promotion"] is False
@@ -2184,7 +2194,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_124_market_intel_mcp_fetch_run_receipt",
+ phase="phase_125_market_intel_mcp_fetch_result_parser_review",
)
assert promotion["mode"] == "mcp_runtime_promotion_review"
@@ -2244,12 +2254,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_124_market_intel_mcp_fetch_run_receipt"
+ assert get_data["phase"] == "phase_125_market_intel_mcp_fetch_result_parser_review"
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_124_market_intel_mcp_fetch_run_receipt"
+ assert post_data["phase"] == "phase_125_market_intel_mcp_fetch_result_parser_review"
assert post_data["runtime_promotion_accepted"] is True
assert post_data["promotion_persisted"] is False
assert post_data["api_opens_database_connection"] is False
@@ -2258,11 +2268,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_124_market_intel_mcp_fetch_run_receipt",
+ phase="phase_125_market_intel_mcp_fetch_result_parser_review",
)
assert handoff["mode"] == "mcp_manual_fetch_handoff_preview"
- assert handoff["phase"] == "phase_124_market_intel_mcp_fetch_run_receipt"
+ assert handoff["phase"] == "phase_125_market_intel_mcp_fetch_result_parser_review"
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
@@ -2290,7 +2300,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_124_market_intel_mcp_fetch_run_receipt",
+ phase="phase_125_market_intel_mcp_fetch_result_parser_review",
)
assert handoff["mode"] == "mcp_manual_fetch_handoff_review"
@@ -2352,12 +2362,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_124_market_intel_mcp_fetch_run_receipt"
+ assert get_data["phase"] == "phase_125_market_intel_mcp_fetch_result_parser_review"
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_124_market_intel_mcp_fetch_run_receipt"
+ assert post_data["phase"] == "phase_125_market_intel_mcp_fetch_result_parser_review"
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
@@ -2368,11 +2378,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_124_market_intel_mcp_fetch_run_receipt",
+ phase="phase_125_market_intel_mcp_fetch_result_parser_review",
)
assert review["mode"] == "mcp_fetch_target_review_preview"
- assert review["phase"] == "phase_124_market_intel_mcp_fetch_run_receipt"
+ assert review["phase"] == "phase_125_market_intel_mcp_fetch_result_parser_review"
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
@@ -2402,7 +2412,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_124_market_intel_mcp_fetch_run_receipt",
+ phase="phase_125_market_intel_mcp_fetch_result_parser_review",
)
assert review["mode"] == "mcp_fetch_target_review"
@@ -2480,13 +2490,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_124_market_intel_mcp_fetch_run_receipt"
+ assert get_data["phase"] == "phase_125_market_intel_mcp_fetch_result_parser_review"
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_124_market_intel_mcp_fetch_run_receipt"
+ assert post_data["phase"] == "phase_125_market_intel_mcp_fetch_result_parser_review"
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
@@ -2498,11 +2508,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_124_market_intel_mcp_fetch_run_receipt",
+ phase="phase_125_market_intel_mcp_fetch_result_parser_review",
)
assert package["mode"] == "mcp_fetch_run_package_preview"
- assert package["phase"] == "phase_124_market_intel_mcp_fetch_run_receipt"
+ assert package["phase"] == "phase_125_market_intel_mcp_fetch_result_parser_review"
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
@@ -2534,7 +2544,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_124_market_intel_mcp_fetch_run_receipt",
+ phase="phase_125_market_intel_mcp_fetch_result_parser_review",
)
assert package["mode"] == "mcp_fetch_run_package_review"
@@ -2609,13 +2619,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_124_market_intel_mcp_fetch_run_receipt"
+ assert get_data["phase"] == "phase_125_market_intel_mcp_fetch_result_parser_review"
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_124_market_intel_mcp_fetch_run_receipt"
+ assert post_data["phase"] == "phase_125_market_intel_mcp_fetch_result_parser_review"
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
@@ -2629,11 +2639,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_124_market_intel_mcp_fetch_run_receipt",
+ phase="phase_125_market_intel_mcp_fetch_result_parser_review",
)
assert readiness["mode"] == "mcp_fetch_run_readiness_preview"
- assert readiness["phase"] == "phase_124_market_intel_mcp_fetch_run_receipt"
+ assert readiness["phase"] == "phase_125_market_intel_mcp_fetch_result_parser_review"
assert readiness["run_readiness_payload_received"] is False
assert readiness["mcp_fetch_run_readiness_accepted"] is False
assert readiness["run_readiness_ready"] is False
@@ -2668,11 +2678,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_124_market_intel_mcp_fetch_run_receipt",
+ phase="phase_125_market_intel_mcp_fetch_result_parser_review",
)
assert readiness["mode"] == "mcp_fetch_run_readiness_review"
- assert readiness["phase"] == "phase_124_market_intel_mcp_fetch_run_receipt"
+ assert readiness["phase"] == "phase_125_market_intel_mcp_fetch_result_parser_review"
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
@@ -2748,13 +2758,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_124_market_intel_mcp_fetch_run_receipt"
+ assert get_data["phase"] == "phase_125_market_intel_mcp_fetch_result_parser_review"
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_124_market_intel_mcp_fetch_run_receipt"
+ assert post_data["phase"] == "phase_125_market_intel_mcp_fetch_result_parser_review"
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
@@ -2768,11 +2778,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_124_market_intel_mcp_fetch_run_receipt",
+ phase="phase_125_market_intel_mcp_fetch_result_parser_review",
)
assert receipt["mode"] == "mcp_fetch_run_receipt_preview"
- assert receipt["phase"] == "phase_124_market_intel_mcp_fetch_run_receipt"
+ assert receipt["phase"] == "phase_125_market_intel_mcp_fetch_result_parser_review"
assert receipt["run_receipt_payload_received"] is False
assert receipt["mcp_fetch_run_receipt_accepted"] is False
assert receipt["run_receipt_ready"] is False
@@ -2809,11 +2819,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_124_market_intel_mcp_fetch_run_receipt",
+ phase="phase_125_market_intel_mcp_fetch_result_parser_review",
)
assert receipt["mode"] == "mcp_fetch_run_receipt_review"
- assert receipt["phase"] == "phase_124_market_intel_mcp_fetch_run_receipt"
+ assert receipt["phase"] == "phase_125_market_intel_mcp_fetch_result_parser_review"
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
@@ -2892,13 +2902,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_124_market_intel_mcp_fetch_run_receipt"
+ assert get_data["phase"] == "phase_125_market_intel_mcp_fetch_result_parser_review"
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_124_market_intel_mcp_fetch_run_receipt"
+ assert post_data["phase"] == "phase_125_market_intel_mcp_fetch_result_parser_review"
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
@@ -2912,11 +2922,165 @@ def test_mcp_fetch_run_receipt_route_get_and_post_are_preview_only():
assert post_data["api_uses_external_network"] is False
+def test_mcp_fetch_result_parser_review_preview_is_safe_without_payload():
+ parser = build_mcp_fetch_result_parser_review_preview(
+ phase="phase_125_market_intel_mcp_fetch_result_parser_review",
+ )
+
+ assert parser["mode"] == "mcp_fetch_result_parser_review_preview"
+ assert parser["phase"] == "phase_125_market_intel_mcp_fetch_result_parser_review"
+ 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
+ assert parser["ready_for_manual_fetch_candidate_handoff_review"] is False
+ assert parser["ready_for_api_database_write"] is False
+ assert parser["ready_for_scheduler_attach"] is False
+ assert parser["manual_fetch_gate_opened_by_api"] is False
+ assert parser["network_request_allowed"] is False
+ assert parser["parser_executed_by_api"] is False
+ assert parser["fetch_executed"] is False
+ assert parser["cli_executed"] is False
+ assert parser["payload_persisted"] is False
+ assert parser["result_parser_persisted"] is False
+ assert parser["result_parser_file_written"] is False
+ assert parser["candidate_handoff_persisted"] is False
+ assert parser["api_executes_cli"] is False
+ assert parser["api_opens_database_connection"] is False
+ assert parser["api_writes_database"] is False
+ assert parser["api_uses_external_network"] is False
+ assert parser["database_write_executed"] is False
+ assert parser["external_network_executed"] is False
+ assert parser["file_written"] is False
+ assert parser["scheduler_attached"] is False
+ assert "run_receipt_payload_or_result_received" in parser["blocked_reasons"]
+ assert "parser_result_received" in parser["blocked_reasons"]
+ assert "run_receipt_package" in parser["sample_parser_result_package"]
+ assert "parser_result" in parser["sample_parser_result_package"]
+
+
+def test_mcp_fetch_result_parser_review_accepts_sample_result():
+ sample = (
+ build_mcp_fetch_result_parser_review_preview()
+ ["sample_parser_result_package"]
+ )
+ parser = build_mcp_fetch_result_parser_review_preview(
+ run_receipt_package=sample["run_receipt_package"],
+ run_receipt_result=sample["run_receipt_result"],
+ parser_result=sample["parser_result"],
+ phase="phase_125_market_intel_mcp_fetch_result_parser_review",
+ )
+
+ assert parser["mode"] == "mcp_fetch_result_parser_review"
+ assert parser["phase"] == "phase_125_market_intel_mcp_fetch_result_parser_review"
+ 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
+ assert parser["ready_for_api_database_write"] is False
+ assert parser["blocked_reasons"] == []
+ assert parser["passed_gate_count"] == parser["gate_count"]
+ assert parser["run_receipt_accepted"] is True
+ assert parser["parser_source_count"] >= 8
+ assert parser["candidate_count"] == (
+ parser["campaign_candidate_count"] + parser["product_candidate_count"]
+ )
+ assert parser["parser_result_summary"]["all_candidate_urls_public"] is True
+ assert parser["parser_result_summary"]["all_candidate_required_fields_present"] is True
+ assert parser["parser_result_summary"]["raw_payload_submitted_to_api"] is False
+ assert parser["manual_fetch_gate_opened_by_api"] is False
+ assert parser["network_request_allowed"] is False
+ assert parser["parser_executed_by_api"] is False
+ assert parser["fetch_executed"] is False
+ assert parser["cli_executed"] is False
+ assert parser["result_parser_file_written"] is False
+ assert parser["file_written"] is False
+ assert parser["database_write_executed"] is False
+ assert parser["api_uses_external_network"] is False
+ assert parser["scheduler_attached"] is False
+
+
+def test_mcp_fetch_result_parser_review_blocks_unsafe_parser_payload():
+ sample = json.loads(
+ json.dumps(
+ build_mcp_fetch_result_parser_review_preview()
+ ["sample_parser_result_package"]
+ )
+ )
+ parser_result = sample["parser_result"]
+ parser_result["approval_token"] = "do-not-submit"
+ parser_result["raw_html"] = "raw"
+ parser_result["api_writes_database"] = True
+ parser_result["operator_confirmed_no_api_execution"] = False
+ first_source = parser_result["parsed_sources"][0]
+ first_source["parser_artifact_path"] = "../unsafe.json"
+ first_source["campaign_candidates"][0]["campaign_url"] = "http://127.0.0.1/private"
+ first_source["product_candidates"][0].pop("platform_product_id")
+
+ parser = build_mcp_fetch_result_parser_review_preview(
+ run_receipt_package=sample["run_receipt_package"],
+ run_receipt_result=sample["run_receipt_result"],
+ parser_result=parser_result,
+ )
+
+ assert parser["mcp_fetch_result_parser_review_accepted"] is False
+ assert parser["run_receipt_accepted"] is True
+ assert "parser_artifact_paths_safe" in parser["blocked_reasons"]
+ assert "parser_candidate_required_fields_present" in parser["blocked_reasons"]
+ assert "parser_candidate_urls_public_http" in parser["blocked_reasons"]
+ assert "parser_operator_boundaries_confirmed" in parser["blocked_reasons"]
+ assert "parser_no_raw_payload" in parser["blocked_reasons"]
+ assert "parser_no_secret_or_token_key" in parser["blocked_reasons"]
+ assert "parser_side_effect_free" in parser["blocked_reasons"]
+ assert parser["network_request_allowed"] is False
+ assert parser["api_writes_database"] is False
+ assert parser["scheduler_attached"] is False
+
+
+def test_mcp_fetch_result_parser_review_route_get_and_post_are_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_fetch_result_parser_review")
+ get_data = get_response.get_json()
+ sample = get_data["sample_parser_result_package"]
+ post_response = client.post(
+ "/api/market_intel/mcp_fetch_result_parser_review",
+ json={"parser_review": sample},
+ )
+ post_data = post_response.get_json()
+
+ assert get_response.status_code == 200
+ assert get_data["mode"] == "mcp_fetch_result_parser_review_preview"
+ assert get_data["phase"] == "phase_125_market_intel_mcp_fetch_result_parser_review"
+ 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_125_market_intel_mcp_fetch_result_parser_review"
+ 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
+ assert post_data["manual_fetch_gate_opened_by_api"] is False
+ assert post_data["network_request_allowed"] is False
+ assert post_data["parser_executed_by_api"] is False
+ assert post_data["fetch_executed"] is False
+ assert post_data["cli_executed"] is False
+ assert post_data["result_parser_persisted"] is False
+ assert post_data["api_opens_database_connection"] is False
+ assert post_data["api_uses_external_network"] is False
+
+
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_124_market_intel_mcp_fetch_run_receipt"
+ assert plan["phase"] == "phase_125_market_intel_mcp_fetch_result_parser_review"
assert plan["ready_for_manual_sample_fetch"] is False
assert plan["sample_fetch_executed"] is False
assert plan["external_network_executed"] is False
@@ -2964,7 +3128,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_124_market_intel_mcp_fetch_run_receipt"
+ assert data["phase"] == "phase_125_market_intel_mcp_fetch_result_parser_review"
assert data["sample_fetch_executed"] is False
assert data["external_network_executed"] is False
assert data["database_write_executed"] is False
@@ -2975,7 +3139,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_124_market_intel_mcp_fetch_run_receipt"
+ assert acceptance["phase"] == "phase_125_market_intel_mcp_fetch_result_parser_review"
assert acceptance["contract_ready"] is True
assert acceptance["sample_result_loaded"] is False
assert acceptance["sample_result_accepted"] is False
@@ -3017,7 +3181,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_124_market_intel_mcp_fetch_run_receipt"
+ assert data["phase"] == "phase_125_market_intel_mcp_fetch_result_parser_review"
assert data["sample_result_loaded"] is False
assert data["candidate_import_allowed"] is False
assert data["external_network_executed"] is False
@@ -3029,7 +3193,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_124_market_intel_mcp_fetch_run_receipt"
+ assert review["phase"] == "phase_125_market_intel_mcp_fetch_result_parser_review"
assert review["contract_ready"] is True
assert review["sample_result_loaded"] is False
assert review["sample_result_reviewed"] is False
@@ -3140,7 +3304,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_124_market_intel_mcp_fetch_run_receipt"
+ assert data["phase"] == "phase_125_market_intel_mcp_fetch_result_parser_review"
assert data["sample_result_loaded"] is False
assert data["sample_result_reviewed"] is False
assert data["candidate_import_allowed"] is False
@@ -3179,7 +3343,7 @@ def test_manual_sample_review_evaluation_preview_accepts_payload_without_persist
)
assert review["mode"] == "manual_sample_review_evaluation_preview"
- assert review["phase"] == "phase_124_market_intel_mcp_fetch_run_receipt"
+ assert review["phase"] == "phase_125_market_intel_mcp_fetch_result_parser_review"
assert review["review_request_type"] == "operator_posted_json"
assert review["payload_received"] is True
assert review["payload_valid_json_object"] is True
@@ -3241,7 +3405,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_124_market_intel_mcp_fetch_run_receipt"
+ assert data["phase"] == "phase_125_market_intel_mcp_fetch_result_parser_review"
assert data["payload_received"] is True
assert data["payload_valid_json_object"] is True
assert data["payload_persisted"] is False
@@ -3321,7 +3485,7 @@ def test_manual_sample_candidate_handoff_preview_creates_candidates_without_pers
)
assert handoff["mode"] == "manual_sample_candidate_handoff_preview"
- assert handoff["phase"] == "phase_124_market_intel_mcp_fetch_run_receipt"
+ assert handoff["phase"] == "phase_125_market_intel_mcp_fetch_result_parser_review"
assert handoff["payload_received"] is True
assert handoff["payload_valid_json_object"] is True
assert handoff["payload_persisted"] is False
@@ -3385,7 +3549,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_124_market_intel_mcp_fetch_run_receipt"
+ assert data["phase"] == "phase_125_market_intel_mcp_fetch_result_parser_review"
assert data["payload_received"] is True
assert data["handoff_ready"] is True
assert data["candidate_handoff_created"] is True
@@ -3444,7 +3608,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_124_market_intel_mcp_fetch_run_receipt"
+ assert queue_draft["phase"] == "phase_125_market_intel_mcp_fetch_result_parser_review"
assert queue_draft["payload_received"] is True
assert queue_draft["payload_valid_json_object"] is True
assert queue_draft["payload_persisted"] is False
@@ -3518,7 +3682,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_124_market_intel_mcp_fetch_run_receipt"
+ assert data["phase"] == "phase_125_market_intel_mcp_fetch_result_parser_review"
assert data["payload_received"] is True
assert data["handoff_ready"] is True
assert data["queue_draft_ready"] is True
@@ -3581,7 +3745,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_124_market_intel_mcp_fetch_run_receipt"
+ assert approval["phase"] == "phase_125_market_intel_mcp_fetch_result_parser_review"
assert approval["payload_received"] is True
assert approval["payload_valid_json_object"] is True
assert approval["payload_persisted"] is False
@@ -3659,7 +3823,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_124_market_intel_mcp_fetch_run_receipt"
+ assert data["phase"] == "phase_125_market_intel_mcp_fetch_result_parser_review"
assert data["payload_received"] is True
assert data["approval_preview_created"] is True
assert data["approval_request_created"] is False
@@ -3722,7 +3886,7 @@ def test_manual_sample_candidate_queue_transaction_preview_blocks_execution():
)
assert transaction["mode"] == "manual_sample_candidate_queue_transaction_preview"
- assert transaction["phase"] == "phase_124_market_intel_mcp_fetch_run_receipt"
+ assert transaction["phase"] == "phase_125_market_intel_mcp_fetch_result_parser_review"
assert transaction["payload_received"] is True
assert transaction["payload_valid_json_object"] is True
assert transaction["payload_persisted"] is False
@@ -3802,7 +3966,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_124_market_intel_mcp_fetch_run_receipt"
+ assert data["phase"] == "phase_125_market_intel_mcp_fetch_result_parser_review"
assert data["payload_received"] is True
assert data["transaction_preview_created"] is True
assert data["transaction_ready"] is False
@@ -9513,7 +9677,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_124_market_intel_mcp_fetch_run_receipt"
+ "phase_125_market_intel_mcp_fetch_result_parser_review"
)
assert data["telegram_dispatch_report_input_ready"] is False
assert data["summary_persistence_telegram_dispatch_report_input_ready"] is False
@@ -9588,7 +9752,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_124_market_intel_mcp_fetch_run_receipt"
+ "phase_125_market_intel_mcp_fetch_result_parser_review"
)
assert data["telegram_dispatch_archive_summary_ready"] is False
assert (
@@ -9860,7 +10024,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_124_market_intel_mcp_fetch_run_receipt"
+ "phase_125_market_intel_mcp_fetch_result_parser_review"
)
assert data["telegram_dispatch_report_input_ready"] is False
assert data["summary_persistence_telegram_dispatch_report_input_ready"] is False
@@ -10148,7 +10312,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_124_market_intel_mcp_fetch_run_receipt"
+ "phase_125_market_intel_mcp_fetch_result_parser_review"
)
assert data["telegram_dispatch_report_run_package_ready"] is False
assert (
@@ -10458,7 +10622,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_124_market_intel_mcp_fetch_run_receipt"
+ "phase_125_market_intel_mcp_fetch_result_parser_review"
)
assert data["telegram_dispatch_report_run_readiness_ready"] is False
assert (
@@ -10761,7 +10925,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_124_market_intel_mcp_fetch_run_receipt"
+ "phase_125_market_intel_mcp_fetch_result_parser_review"
)
assert data["telegram_dispatch_report_run_receipt_passed"] is False
assert (
@@ -11020,7 +11184,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_124_market_intel_mcp_fetch_run_receipt"
+ "phase_125_market_intel_mcp_fetch_result_parser_review"
)
assert data["telegram_dispatch_report_closeout_passed"] is False
assert (
@@ -11293,7 +11457,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_124_market_intel_mcp_fetch_run_receipt"
+ "phase_125_market_intel_mcp_fetch_result_parser_review"
)
assert data["telegram_dispatch_report_archive_passed"] is False
assert (
@@ -11541,7 +11705,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_124_market_intel_mcp_fetch_run_receipt"
+ "phase_125_market_intel_mcp_fetch_result_parser_review"
)
assert data["telegram_dispatch_report_archive_summary_passed"] is False
assert (
@@ -11771,7 +11935,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_124_market_intel_mcp_fetch_run_receipt"
+ "phase_125_market_intel_mcp_fetch_result_parser_review"
)
assert data["telegram_dispatch_report_catalog_handoff_passed"] is False
assert data["summary_persistence_telegram_dispatch_report_catalog_handoff_passed"] is False
@@ -12008,7 +12172,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_124_market_intel_mcp_fetch_run_receipt"
+ "phase_125_market_intel_mcp_fetch_result_parser_review"
)
assert data["telegram_dispatch_report_catalog_index_passed"] is False
assert data["summary_persistence_telegram_dispatch_report_catalog_index_passed"] is False
@@ -12250,7 +12414,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_124_market_intel_mcp_fetch_run_receipt"
+ "phase_125_market_intel_mcp_fetch_result_parser_review"
)
assert data["telegram_dispatch_report_catalog_write_preflight_passed"] is False
assert (
@@ -12526,7 +12690,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_124_market_intel_mcp_fetch_run_receipt"
+ "phase_125_market_intel_mcp_fetch_result_parser_review"
)
assert data["telegram_dispatch_report_catalog_record_write_passed"] is False
assert (
@@ -12803,7 +12967,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_124_market_intel_mcp_fetch_run_receipt"
+ "phase_125_market_intel_mcp_fetch_result_parser_review"
)
assert data["telegram_dispatch_report_catalog_record_run_package_passed"] is False
assert (
@@ -13082,7 +13246,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_124_market_intel_mcp_fetch_run_receipt"
+ "phase_125_market_intel_mcp_fetch_result_parser_review"
)
assert data["telegram_dispatch_report_catalog_record_run_readiness_passed"] is False
assert (
@@ -13411,7 +13575,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_124_market_intel_mcp_fetch_run_receipt"
+ "phase_125_market_intel_mcp_fetch_result_parser_review"
)
assert data["telegram_dispatch_report_catalog_record_run_receipt_passed"] is False
assert (
@@ -13673,7 +13837,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_124_market_intel_mcp_fetch_run_receipt"
+ "phase_125_market_intel_mcp_fetch_result_parser_review"
)
assert data["telegram_dispatch_report_catalog_record_commit_passed"] is False
assert (
@@ -13937,7 +14101,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_124_market_intel_mcp_fetch_run_receipt"
+ "phase_125_market_intel_mcp_fetch_result_parser_review"
)
assert data["telegram_dispatch_report_catalog_record_closeout_passed"] is False
assert (
@@ -14207,7 +14371,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_124_market_intel_mcp_fetch_run_receipt"
+ "phase_125_market_intel_mcp_fetch_result_parser_review"
)
assert data["telegram_dispatch_report_catalog_record_archive_passed"] is False
assert (
@@ -14495,7 +14659,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_124_market_intel_mcp_fetch_run_receipt"
+ "phase_125_market_intel_mcp_fetch_result_parser_review"
)
assert data["telegram_dispatch_report_catalog_record_archive_summary_passed"] is False
assert (
@@ -14782,7 +14946,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_124_market_intel_mcp_fetch_run_receipt"
+ "phase_125_market_intel_mcp_fetch_result_parser_review"
)
assert data["telegram_dispatch_report_catalog_record_final_closeout_passed"] is False
assert (
@@ -14868,7 +15032,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_124_market_intel_mcp_fetch_run_receipt"
+ assert data["phase"] == "phase_125_market_intel_mcp_fetch_result_parser_review"
assert data["execute_requested"] is False
assert data["read_only_query_executed"] is False
assert data["database_connection_opened"] is False
@@ -14925,7 +15089,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_124_market_intel_mcp_fetch_run_receipt"
+ assert data["phase"] == "phase_125_market_intel_mcp_fetch_result_parser_review"
assert data["execute_requested"] is True
assert data["apply_real_write_requested"] is True
assert data["approval_token_present"] is False
@@ -15014,7 +15178,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_124_market_intel_mcp_fetch_run_receipt"
+ assert data["phase"] == "phase_125_market_intel_mcp_fetch_result_parser_review"
assert data["execute_requested"] is False
assert data["read_only_query_executed"] is False
assert data["database_connection_opened"] is False
@@ -15068,7 +15232,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_124_market_intel_mcp_fetch_run_receipt"
+ assert data["phase"] == "phase_125_market_intel_mcp_fetch_result_parser_review"
assert data["operator_drill_ready"] is True
assert data["api_executes_cli"] is False
assert data["api_reads_approval_token"] is False
@@ -15124,7 +15288,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_124_market_intel_mcp_fetch_run_receipt"
+ assert data["phase"] == "phase_125_market_intel_mcp_fetch_result_parser_review"
assert data["package_ready"] is True
assert data["package_artifact_created"] is False
assert data["api_writes_file"] is False
@@ -15190,7 +15354,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_124_market_intel_mcp_fetch_run_receipt"
+ assert data["phase"] == "phase_125_market_intel_mcp_fetch_result_parser_review"
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
@@ -15492,7 +15656,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_124_market_intel_mcp_fetch_run_receipt"
+ assert data["phase"] == "phase_125_market_intel_mcp_fetch_result_parser_review"
assert data["receipt_passed"] is True
assert data["ready_for_api_database_write"] is False
assert data["ready_for_scheduler_attach"] is False
@@ -15540,7 +15704,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_124_market_intel_mcp_fetch_run_receipt"
+ assert data["phase"] == "phase_125_market_intel_mcp_fetch_result_parser_review"
assert data["closeout_passed"] is True
assert data["ready_for_next_manual_phase"] is True
assert data["ready_for_api_database_write"] is False
@@ -15589,7 +15753,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_124_market_intel_mcp_fetch_run_receipt"
+ assert data["phase"] == "phase_125_market_intel_mcp_fetch_result_parser_review"
assert data["handoff_ready"] is True
assert data["ready_for_manual_queue_review"] is True
assert data["ready_for_api_database_write"] is False
@@ -15647,7 +15811,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_124_market_intel_mcp_fetch_run_receipt"
+ assert data["phase"] == "phase_125_market_intel_mcp_fetch_result_parser_review"
assert data["execute_requested"] is False
assert data["review_inventory_ready"] is False
assert data["ready_for_human_decision_review"] is False
@@ -15713,7 +15877,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_124_market_intel_mcp_fetch_run_receipt"
+ assert data["phase"] == "phase_125_market_intel_mcp_fetch_result_parser_review"
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
@@ -15784,7 +15948,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_124_market_intel_mcp_fetch_run_receipt"
+ assert data["phase"] == "phase_125_market_intel_mcp_fetch_result_parser_review"
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
@@ -15860,7 +16024,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_124_market_intel_mcp_fetch_run_receipt"
+ assert data["phase"] == "phase_125_market_intel_mcp_fetch_result_parser_review"
assert data["transaction_preview_created"] is False
assert data["transaction_ready"] is False
assert data["ready_for_manual_shell_update_window"] is False
@@ -15942,7 +16106,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_124_market_intel_mcp_fetch_run_receipt"
+ assert data["phase"] == "phase_125_market_intel_mcp_fetch_result_parser_review"
assert data["execute_requested"] is True
assert data["apply_real_write_requested"] is True
assert data["approval_token_present"] is False
@@ -16028,7 +16192,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_124_market_intel_mcp_fetch_run_receipt"
+ assert data["phase"] == "phase_125_market_intel_mcp_fetch_result_parser_review"
assert data["execute_requested"] is True
assert data["apply_real_write_requested"] is True
assert data["read_only_query_executed"] is False
@@ -16111,7 +16275,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_124_market_intel_mcp_fetch_run_receipt"
+ assert data["phase"] == "phase_125_market_intel_mcp_fetch_result_parser_review"
assert data["execute_requested"] is False
assert data["read_only_query_executed"] is False
assert data["database_connection_opened"] is False
@@ -16194,7 +16358,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_124_market_intel_mcp_fetch_run_receipt"
+ assert data["phase"] == "phase_125_market_intel_mcp_fetch_result_parser_review"
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
@@ -16280,7 +16444,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_124_market_intel_mcp_fetch_run_receipt"
+ assert data["phase"] == "phase_125_market_intel_mcp_fetch_result_parser_review"
assert data["package_ready"] is False
assert data["package_artifact_created"] is False
assert data["ready_for_api_review_state_update"] is False
@@ -16371,7 +16535,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_124_market_intel_mcp_fetch_run_receipt"
+ "phase_125_market_intel_mcp_fetch_result_parser_review"
)
assert data["ready_for_cli_operator_run"] is False
assert data["ready_for_api_review_state_update"] is False
@@ -16481,7 +16645,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_124_market_intel_mcp_fetch_run_receipt"
+ "phase_125_market_intel_mcp_fetch_result_parser_review"
)
assert data["receipt_passed"] is False
assert data["ready_for_api_review_state_update"] is False
@@ -16567,7 +16731,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_124_market_intel_mcp_fetch_run_receipt"
+ "phase_125_market_intel_mcp_fetch_result_parser_review"
)
assert data["closeout_passed"] is False
assert data["ready_for_api_review_state_update"] is False
@@ -16624,7 +16788,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_124_market_intel_mcp_fetch_run_receipt"
+ "phase_125_market_intel_mcp_fetch_result_parser_review"
)
assert data["post_closeout_inventory_ready"] is False
assert data["ready_for_api_review_state_update"] is False
@@ -16677,7 +16841,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_124_market_intel_mcp_fetch_run_receipt"
+ assert data["phase"] == "phase_125_market_intel_mcp_fetch_result_parser_review"
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
@@ -16730,7 +16894,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_124_market_intel_mcp_fetch_run_receipt"
+ assert data["phase"] == "phase_125_market_intel_mcp_fetch_result_parser_review"
assert data["archive_summary_ready"] is False
assert data["summary_input_ready"] is False
assert data["ready_for_ai_summary_generation"] is False
@@ -16791,7 +16955,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_124_market_intel_mcp_fetch_run_receipt"
+ assert data["phase"] == "phase_125_market_intel_mcp_fetch_result_parser_review"
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
@@ -16860,7 +17024,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_124_market_intel_mcp_fetch_run_receipt"
+ assert data["phase"] == "phase_125_market_intel_mcp_fetch_result_parser_review"
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
@@ -16932,7 +17096,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_124_market_intel_mcp_fetch_run_receipt"
+ assert data["phase"] == "phase_125_market_intel_mcp_fetch_result_parser_review"
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
@@ -17005,7 +17169,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_124_market_intel_mcp_fetch_run_receipt"
+ "phase_125_market_intel_mcp_fetch_result_parser_review"
)
assert data["summary_persistence_preflight_ready"] is False
assert data["ready_for_summary_transaction_preview"] is False
@@ -17076,7 +17240,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_124_market_intel_mcp_fetch_run_receipt"
+ "phase_125_market_intel_mcp_fetch_result_parser_review"
)
assert data["summary_persistence_transaction_ready"] is False
assert data["ready_for_summary_persistence_writer_gate"] is False
@@ -17141,7 +17305,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_124_market_intel_mcp_fetch_run_receipt"
+ "phase_125_market_intel_mcp_fetch_result_parser_review"
)
assert data["summary_persistence_writer_preflight_ready"] is False
assert data["ready_for_summary_persistence_run_package"] is False
@@ -17212,7 +17376,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_124_market_intel_mcp_fetch_run_receipt"
+ "phase_125_market_intel_mcp_fetch_result_parser_review"
)
assert data["package_ready"] is False
assert data["ready_for_summary_persistence_run_readiness"] is False
@@ -17285,7 +17449,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_124_market_intel_mcp_fetch_run_receipt"
+ "phase_125_market_intel_mcp_fetch_result_parser_review"
)
assert data["run_readiness_ready"] is False
assert data["summary_persistence_run_readiness_ready"] is False
@@ -17362,7 +17526,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_124_market_intel_mcp_fetch_run_receipt"
+ "phase_125_market_intel_mcp_fetch_result_parser_review"
)
assert data["run_receipt_passed"] is False
assert data["summary_persistence_run_receipt_passed"] is False
@@ -17439,7 +17603,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_124_market_intel_mcp_fetch_run_receipt"
+ "phase_125_market_intel_mcp_fetch_result_parser_review"
)
assert data["closeout_passed"] is False
assert data["summary_persistence_closeout_passed"] is False
@@ -17516,7 +17680,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_124_market_intel_mcp_fetch_run_receipt"
+ "phase_125_market_intel_mcp_fetch_result_parser_review"
)
assert data["telegram_dispatch_gate_passed"] is False
assert data["summary_persistence_telegram_dispatch_gate_passed"] is False
@@ -17590,7 +17754,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_124_market_intel_mcp_fetch_run_receipt"
+ "phase_125_market_intel_mcp_fetch_result_parser_review"
)
assert data["telegram_dispatch_run_package_ready"] is False
assert data["summary_persistence_telegram_dispatch_run_package_ready"] is False
@@ -17669,7 +17833,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_124_market_intel_mcp_fetch_run_receipt"
+ "phase_125_market_intel_mcp_fetch_result_parser_review"
)
assert data["telegram_dispatch_run_readiness_ready"] is False
assert (
@@ -17756,7 +17920,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_124_market_intel_mcp_fetch_run_receipt"
+ "phase_125_market_intel_mcp_fetch_result_parser_review"
)
assert data["telegram_dispatch_run_receipt_passed"] is False
assert data["summary_persistence_telegram_dispatch_run_receipt_passed"] is False
@@ -17837,7 +18001,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_124_market_intel_mcp_fetch_run_receipt"
+ "phase_125_market_intel_mcp_fetch_result_parser_review"
)
assert data["telegram_dispatch_closeout_passed"] is False
assert data["summary_persistence_telegram_dispatch_closeout_passed"] is False
@@ -17919,7 +18083,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_124_market_intel_mcp_fetch_run_receipt"
+ "phase_125_market_intel_mcp_fetch_result_parser_review"
)
assert data["telegram_dispatch_archive_ready"] is False
assert data["summary_persistence_telegram_dispatch_archive_ready"] is False
@@ -18002,7 +18166,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_124_market_intel_mcp_fetch_run_receipt"
+ assert data["phase"] == "phase_125_market_intel_mcp_fetch_result_parser_review"
assert data["receipt_passed"] is True
assert data["ready_for_next_manual_review"] is True
assert data["ready_for_api_database_write"] is False
@@ -18027,7 +18191,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_124_market_intel_mcp_fetch_run_receipt"
+ assert plan["phase"] == "phase_125_market_intel_mcp_fetch_result_parser_review"
assert plan["ready_to_attach_scheduler"] is False
assert plan["scheduler_attached"] is False
assert plan["scheduler_registration_executed"] is False
@@ -18065,7 +18229,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_124_market_intel_mcp_fetch_run_receipt"
+ assert data["phase"] == "phase_125_market_intel_mcp_fetch_result_parser_review"
assert data["scheduler_registration_executed"] is False
assert data["crawler_job_started"] is False
assert data["external_network_executed"] is False
@@ -18076,7 +18240,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_124_market_intel_mcp_fetch_run_receipt"
+ assert plan["phase"] == "phase_125_market_intel_mcp_fetch_result_parser_review"
assert plan["ready_for_review_queue"] is False
assert plan["review_queue_created"] is False
assert plan["auto_match_executed"] is False
@@ -18112,7 +18276,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_124_market_intel_mcp_fetch_run_receipt"
+ assert data["phase"] == "phase_125_market_intel_mcp_fetch_result_parser_review"
assert data["review_queue_created"] is False
assert data["auto_confirm_executed"] is False
assert data["external_network_executed"] is False
@@ -18123,7 +18287,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_124_market_intel_mcp_fetch_run_receipt"
+ assert plan["phase"] == "phase_125_market_intel_mcp_fetch_result_parser_review"
assert plan["ready_for_opportunity_queue"] is False
assert plan["opportunity_queue_created"] is False
assert plan["threat_alert_dispatched"] is False
@@ -18164,7 +18328,7 @@ def test_opportunity_plan_route_is_preview_only():
assert response.status_code == 200
assert data["mode"] == "opportunity_plan_preview"
- assert data["phase"] == "phase_124_market_intel_mcp_fetch_run_receipt"
+ assert data["phase"] == "phase_125_market_intel_mcp_fetch_result_parser_review"
assert data["opportunity_queue_created"] is False
assert data["threat_alert_dispatched"] is False
assert data["ai_summary_generated"] is False
@@ -18175,7 +18339,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_124_market_intel_mcp_fetch_run_receipt"
+ assert plan["phase"] == "phase_125_market_intel_mcp_fetch_result_parser_review"
assert plan["ready_for_scoring_job"] is False
assert plan["scoring_job_created"] is False
assert plan["score_calculation_executed"] is False
@@ -18223,7 +18387,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_124_market_intel_mcp_fetch_run_receipt"
+ assert data["phase"] == "phase_125_market_intel_mcp_fetch_result_parser_review"
assert data["scoring_job_created"] is False
assert data["score_calculation_executed"] is False
assert data["sample_scores_generated"] is False
@@ -18235,7 +18399,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_124_market_intel_mcp_fetch_run_receipt"
+ assert plan["phase"] == "phase_125_market_intel_mcp_fetch_result_parser_review"
assert plan["ready_for_evidence_bundle"] is False
assert plan["evidence_bundle_created"] is False
assert plan["evidence_query_executed"] is False
@@ -18281,7 +18445,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_124_market_intel_mcp_fetch_run_receipt"
+ assert data["phase"] == "phase_125_market_intel_mcp_fetch_result_parser_review"
assert data["evidence_bundle_created"] is False
assert data["evidence_query_executed"] is False
assert data["sample_evidence_generated"] is False
@@ -18294,7 +18458,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_124_market_intel_mcp_fetch_run_receipt"
+ assert plan["phase"] == "phase_125_market_intel_mcp_fetch_result_parser_review"
assert plan["ready_for_alert_candidates"] is False
assert plan["alert_candidate_created"] is False
assert plan["alert_queue_created"] is False
@@ -18379,7 +18543,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_124_market_intel_mcp_fetch_run_receipt"
+ assert data["phase"] == "phase_125_market_intel_mcp_fetch_result_parser_review"
assert data["alert_candidate_created"] is False
assert data["alert_queue_created"] is False
assert data["review_queue_created"] is False
@@ -18457,7 +18621,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_124_market_intel_mcp_fetch_run_receipt"
+ assert data["phase"] == "phase_125_market_intel_mcp_fetch_result_parser_review"
assert data["deployment_actions_executed"] is False
assert data["docker_command_executed"] is False
assert data["ssh_command_executed"] is False
@@ -18472,7 +18636,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_124_market_intel_mcp_fetch_run_receipt"
+ assert readiness["phase"] == "phase_125_market_intel_mcp_fetch_result_parser_review"
assert readiness["execute_requested"] is False
assert readiness["router_enabled"] is False
assert readiness["external_mcp_complete"] is False
@@ -23288,7 +23452,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_124_market_intel_mcp_fetch_run_receipt"
+ assert drill["phase"] == "phase_125_market_intel_mcp_fetch_result_parser_review"
assert drill["execute_requested"] is False
assert drill["schema_state"] == "planned_no_db_probe"
assert drill["drill_ready_for_operator_review"] is True
@@ -23403,7 +23567,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_124_market_intel_mcp_fetch_run_receipt"
+ assert data["phase"] == "phase_125_market_intel_mcp_fetch_result_parser_review"
assert data["execute_requested"] is False
assert data["migration_executed"] is False
assert data["rollback_executed"] is False
@@ -23415,7 +23579,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_124_market_intel_mcp_fetch_run_receipt"
+ assert review["phase"] == "phase_125_market_intel_mcp_fetch_result_parser_review"
assert review["execute_requested"] is False
assert review["catalog_state"] == "planned_no_probe"
assert review["seed_state"] == "planned_no_probe"
@@ -23530,7 +23694,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_124_market_intel_mcp_fetch_run_receipt"
+ assert data["phase"] == "phase_125_market_intel_mcp_fetch_result_parser_review"
assert data["execute_requested"] is False
assert data["catalog_state"] == "planned_no_probe"
assert data["migration_executed"] is False
@@ -23543,7 +23707,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_124_market_intel_mcp_fetch_run_receipt"
+ assert smoke["phase"] == "phase_125_market_intel_mcp_fetch_result_parser_review"
assert smoke["execute_requested"] is False
assert smoke["smoke_result"] == "planned_no_execution"
assert smoke["live_smoke_passed"] is False
@@ -23605,7 +23769,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_124_market_intel_mcp_fetch_run_receipt"
+ assert data["phase"] == "phase_125_market_intel_mcp_fetch_result_parser_review"
assert data["execute_requested"] is False
assert data["smoke_result"] == "planned_no_execution"
assert data["migration_executed"] is False
@@ -23618,7 +23782,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_124_market_intel_mcp_fetch_run_receipt"
+ assert inventory["phase"] == "phase_125_market_intel_mcp_fetch_result_parser_review"
assert inventory["execute_requested"] is False
assert inventory["read_only_query_executed"] is False
assert inventory["database_connection_opened"] is False
@@ -23762,7 +23926,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_124_market_intel_mcp_fetch_run_receipt"
+ assert data["phase"] == "phase_125_market_intel_mcp_fetch_result_parser_review"
assert data["execute_requested"] is False
assert data["read_only_query_executed"] is False
assert data["database_write_executed"] is False
@@ -23998,7 +24162,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_124_market_intel_mcp_fetch_run_receipt"
+ assert data["phase"] == "phase_125_market_intel_mcp_fetch_result_parser_review"
assert data["execute_requested"] is False
assert data["apply_real_write_requested"] is False
assert data["writes_executed"] is False
@@ -24027,7 +24191,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_124_market_intel_mcp_fetch_run_receipt"
+ assert data["phase"] == "phase_125_market_intel_mcp_fetch_result_parser_review"
assert data["execute_requested"] is False
assert data["apply_real_write_requested"] is False
assert data["approval_token_present"] is False