V10.505 新增市場情報 writer review decision gate
This commit is contained in:
@@ -4,6 +4,7 @@
|
||||
================================================================================
|
||||
|
||||
【已完成】
|
||||
- V10.505 新增市場情報 MCP Fetch Candidate Queue Writer Review Decision 安全預覽 gate:只審核 review inventory 通過後由操作員貼回的人工 candidate queue review decision 摘要,確認 decision identity、target table、row count、dedupe keys、`needs_review` 現態、允許決策集合、evidence refs、matched row exact-identity/variant/overwrite guard、operator confirmation 與 forbidden API actions;API 不讀 token、不執行 CLI、不開 DB、不寫 decision record、不更新 review_state、不寫 match result、不補 queue、不掛 scheduler。UI 同步新增 Decision gates / Inventory link / Decision summary / Decision rows / Boundary next 預覽區。
|
||||
- V10.504 新增市場情報 MCP Fetch Candidate Queue Writer Review Inventory 安全預覽 gate:只審核 writer review handoff 通過後由操作員貼回的 read-only candidate queue inventory 摘要,確認 handoff identity、target table、row count、dedupe keys、review_state、artifact paths、read-only query result、missing/duplicate rows、operator confirmation 與 forbidden API actions;API 不讀 token、不執行 CLI、不開 DB、不寫 queue、不更新 review_state、不做 inventory query、不掛 scheduler。主 gate 拆為 inventory / gates / sample 三檔,避免單檔膨脹。
|
||||
- V10.503 新增市場情報 MCP Fetch Candidate Queue Writer Review Handoff 安全預覽 gate:只審核 post-closeout inventory review 通過後的人工 candidate queue review 交接包,確認 inventory linkage、handoff identity、target table、row count、artifact paths、review contract、forbidden API actions 與 operator confirmation;API 不讀 token、不執行 CLI、不開 DB、不寫 queue、不更新 review_state、不掛 scheduler。
|
||||
- V10.502 修正 AiderHeal 自動修復診斷鏈:先做 ADR-020 檔案白名單再打 110 preflight,`tests/` finding 會明確略過而不誤報 repo preflight;Code Review 完成通知會把全數不在白名單的 finding 標成需人工處理,不再宣稱已觸發 AiderHeal;白名單放行 `services/routes/database` 子目錄 Python 檔,preflight 通知帶 stderr/stdout 細節,健康檢查同時接受 `/health` 回 `ok` 與 `healthy`。
|
||||
|
||||
@@ -350,7 +350,7 @@ YOUTUBE_API_KEY = os.getenv('YOUTUBE_API_KEY', '')
|
||||
# ==========================================
|
||||
# 系統版本與路徑
|
||||
# ==========================================
|
||||
SYSTEM_VERSION = "V10.504"
|
||||
SYSTEM_VERSION = "V10.505"
|
||||
LOG_FILE_PATH = os.path.join(BASE_DIR, 'logs/system.log')
|
||||
public_url = PUBLIC_URL # 用於模板顯示
|
||||
|
||||
|
||||
@@ -175,6 +175,7 @@ EwoooC 目前已有 MOMO EDM / 節慶活動資料、`promo_products`、PChome
|
||||
- 2026-05-31 追加 MCP fetch candidate queue writer post-closeout inventory review gate:`services.market_intel.mcp_fetch_candidate_queue_writer_post_closeout_inventory_review` 與 `/api/market_intel/mcp_fetch_candidate_queue_writer_post_closeout_inventory_review` 在 closeout review 通過後審核 operator live inventory read-only 摘要,檢查 closeout linkage、row count、inventory artifact、closeout review artifact、read-only query result、missing/duplicate rows 與 operator confirmations;API/UI 不讀 approval token、不執行 CLI、不開 DB、不寫 queue、不做 inventory query、不掛 scheduler,只放行到 candidate queue review handoff。
|
||||
- 2026-05-31 追加 MCP fetch candidate queue writer review handoff gate:`services.market_intel.mcp_fetch_candidate_queue_writer_review_handoff` 與 `/api/market_intel/mcp_fetch_candidate_queue_writer_review_handoff` 在 post-closeout inventory review 通過後審核 operator candidate queue review handoff 摘要,檢查 inventory linkage、handoff identity、target table、row count、artifact paths、review contract、forbidden API actions 與 operator confirmations;API/UI 不讀 approval token、不執行 CLI、不開 DB、不寫 queue、不更新 review_state、不掛 scheduler,只放行到人工 candidate queue review。
|
||||
- 2026-05-31 追加 MCP fetch candidate queue writer review inventory gate:`services.market_intel.mcp_fetch_candidate_queue_writer_review_inventory`、`services.market_intel.mcp_fetch_candidate_queue_writer_review_inventory_gates`、`services.market_intel.mcp_fetch_candidate_queue_writer_review_inventory_sample` 與 `/api/market_intel/mcp_fetch_candidate_queue_writer_review_inventory` 在 writer review handoff 通過後審核 operator read-only candidate queue inventory 摘要,檢查 handoff identity、target table、row count、dedupe keys、review_state、artifact paths、read-only query result、missing/duplicate rows 與 operator confirmations;API/UI 不讀 approval token、不執行 CLI、不開 DB、不寫 queue、不更新 review_state、不做 inventory query、不掛 scheduler,只放行到後續人工 candidate queue review。
|
||||
- 2026-05-31 追加 MCP fetch candidate queue writer review decision gate:`services.market_intel.mcp_fetch_candidate_queue_writer_review_decision`、`services.market_intel.mcp_fetch_candidate_queue_writer_review_decision_gates`、`services.market_intel.mcp_fetch_candidate_queue_writer_review_decision_sample` 與 `/api/market_intel/mcp_fetch_candidate_queue_writer_review_decision` 在 review inventory 通過後審核 operator candidate queue review decision 摘要,檢查 decision identity、target table、row count、dedupe keys、`needs_review` 現態、允許決策集合、evidence refs、matched row exact-identity/variant/overwrite guard、operator confirmations 與 forbidden API actions;API/UI 不讀 approval token、不執行 CLI、不開 DB、不寫 decision record、不更新 review_state、不寫 match result、不補 queue、不掛 scheduler,只放行到 decision approval / writer preflight 設計。
|
||||
- 2026-05-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 才能升級。
|
||||
|
||||
@@ -53,6 +53,7 @@
|
||||
- 2026-05-31 追記:同步市場情報 MCP fetch candidate queue writer post-closeout inventory review gate 後的 `services/market_intel/deployment_readiness.py` 行數;本次新增 `services/market_intel/mcp_fetch_candidate_queue_writer_post_closeout_inventory_review.py` 為 649 行,略過 600 行提醒門檻。暫不拆分的理由是 inventory gate 需同時審核 closeout linkage、read-only inventory 摘要、artifact path policy、operator boundary confirmation 與 side-effect blocklist;後續若 candidate queue review handoff 再複用同一套 path/side-effect policy,應抽出 `mcp_fetch_candidate_queue_writer_policy.py`。
|
||||
- 2026-05-31 追記:同步市場情報 MCP fetch candidate queue writer review handoff gate 後的 `services/market_intel/deployment_readiness.py` 行數;本次將 sample payload 抽到 `services/market_intel/mcp_fetch_candidate_queue_writer_review_handoff_sample.py`(105 行),主 gate `services/market_intel/mcp_fetch_candidate_queue_writer_review_handoff.py` 降為 571 行,低於 600 行提醒門檻;後續若 candidate queue review inventory 繼續複用 path/side-effect policy,應抽出 `mcp_fetch_candidate_queue_writer_policy.py`。
|
||||
- 2026-05-31 追記:同步市場情報 MCP fetch candidate queue writer review inventory gate 後的 `services/market_intel/deployment_readiness.py` 行數;本次新增 `services/market_intel/mcp_fetch_candidate_queue_writer_review_inventory.py`(462 行)、`services/market_intel/mcp_fetch_candidate_queue_writer_review_inventory_gates.py`(183 行)與 `services/market_intel/mcp_fetch_candidate_queue_writer_review_inventory_sample.py`(107 行),全部低於 600 行提醒門檻;`routes/market_intel_mcp_run_routes.py` 目前 717 行,仍低於 800 行但後續新增 MCP gate 應持續評估拆第二個 route extension。
|
||||
- 2026-05-31 追記:同步市場情報 MCP fetch candidate queue writer review decision gate 後的 `services/market_intel/deployment_readiness.py` 行數;本次新增 `services/market_intel/mcp_fetch_candidate_queue_writer_review_decision.py`(498 行)、`services/market_intel/mcp_fetch_candidate_queue_writer_review_decision_gates.py`(241 行)與 `services/market_intel/mcp_fetch_candidate_queue_writer_review_decision_sample.py`(118 行),全部低於 600 行提醒門檻;`routes/market_intel_mcp_run_routes.py` 目前 772 行,仍低於 800 行但已接近門檻,下一段 MCP route 應優先拆第二個 route extension。
|
||||
- 2026-05-24 追記:同步背景 Code Review 111 fallback 保護合併後的 `services/code_review_pipeline_service.py` 行數;此處只更新 inventory,不變更 Code Review 行為。
|
||||
- 2026-05-21 追記:同步 PChome/LUDEYA 商品線名稱漂移比對更新後的 `services/marketplace_product_matcher.py` 行數;此處只更新 inventory,不變更模組化決策。
|
||||
- 2026-05-21 追記:同步 MAC/Yuskin/AHC 名稱漂移與 bundle equivalent matcher 更新後的 `services/marketplace_product_matcher.py` 行數;此處只更新 inventory,不變更模組化決策。
|
||||
|
||||
@@ -100,6 +100,7 @@
|
||||
- 2026-05-31 起,`V10.502` 修正 AiderHeal 自動修復診斷鏈:先檢查 ADR-020 檔案白名單再執行 110 preflight,`tests/` finding 會明確略過而不誤報 repo preflight;Code Review 完成通知會把全數不在白名單的 finding 標成需人工處理,不再宣稱已觸發 AiderHeal;白名單放行 `services/routes/database` 子目錄 Python 檔,preflight 通知帶遠端錯誤細節,健康檢查接受 `/health` 回 `healthy`。
|
||||
- 2026-05-31 起,`V10.503` 新增市場情報 MCP Fetch Candidate Queue Writer Review Handoff gate:在 post-closeout inventory review 通過後只審核 operator candidate queue review handoff 摘要,要求 inventory linkage、handoff identity、target table、row count、artifact paths、review contract、forbidden API actions 與 operator confirmation 對齊;仍不讀 token、不執行 CLI、不開 DB、不寫 queue、不更新 review_state、不掛 scheduler,只放行到人工 candidate queue review。
|
||||
- 2026-05-31 起,`V10.504` 新增市場情報 MCP Fetch Candidate Queue Writer Review Inventory gate:在 writer review handoff 通過後只審核 operator read-only candidate queue inventory 摘要,要求 handoff identity、target table、row count、dedupe keys、review_state、artifact paths、read-only query result、missing/duplicate rows 與 operator confirmation 對齊;仍不讀 token、不執行 CLI、不開 DB、不寫 queue、不更新 review_state、不做 inventory query、不掛 scheduler,只放行到後續人工 candidate queue review。
|
||||
- 2026-05-31 起,`V10.505` 新增市場情報 MCP Fetch Candidate Queue Writer Review Decision gate:在 review inventory 通過後只審核 operator candidate queue review decision 摘要,要求 decision identity、target table、row count、dedupe keys、`needs_review` 現態、允許決策、evidence refs、matched row exact-identity/variant/overwrite guard 與 operator confirmation 對齊;仍不讀 token、不執行 CLI、不開 DB、不寫 decision record、不更新 review_state、不寫 match result、不補 queue、不掛 scheduler,只放行到 decision approval / writer preflight 設計。
|
||||
|
||||
## 3. 12 Agent 決策信封整合
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
## 📅 詳細更新日誌 (考古存檔)
|
||||
|
||||
### 2026-05-24:PChome 近門檻身份回收第二輪
|
||||
- **V10.505 市場情報 MCP Fetch Candidate Queue Writer Review Decision gate**: 新增 `/api/market_intel/mcp_fetch_candidate_queue_writer_review_decision` 與 UI preview,只審核 review inventory 通過後的 operator candidate queue review decision 摘要;要求 decision identity、target table、row count、dedupe keys、`needs_review` 現態、允許決策集合、evidence refs、matched row exact-identity/variant/overwrite guard 與 operator confirmation 對齊,且 API 不讀 token、不執行 CLI、不開 DB、不寫 decision record、不更新 review_state、不寫 match result、不補 queue、不掛 scheduler,只放行到 decision approval / writer preflight 設計。
|
||||
- **V10.504 市場情報 MCP Fetch Candidate Queue Writer Review Inventory gate**: 新增 `/api/market_intel/mcp_fetch_candidate_queue_writer_review_inventory` 與 UI preview,只審核 writer review handoff 通過後的 operator read-only candidate queue inventory 摘要;要求 handoff identity、target table、row count、dedupe keys、review_state、artifact paths、read-only query result、missing/duplicate rows 與 operator confirmation 對齊,且 API 不讀 token、不執行 CLI、不開 DB、不寫 queue、不更新 review_state、不做 inventory query、不掛 scheduler,只放行到後續人工 candidate queue review。
|
||||
- **V10.503 市場情報 MCP Fetch Candidate Queue Writer Review Handoff gate**: 新增 `/api/market_intel/mcp_fetch_candidate_queue_writer_review_handoff` 與 UI preview,只審核 post-closeout inventory review 通過後的 operator candidate queue review handoff 摘要;要求 inventory linkage、handoff identity、target table、row count、artifact paths、review contract、forbidden API actions 與 operator confirmation 對齊,且 API 不讀 token、不執行 CLI、不開 DB、不寫 queue、不更新 review_state、不掛 scheduler,只放行到人工 candidate queue review。
|
||||
- **V10.502 AiderHeal 自動修復診斷鏈修正**: `execute_code_fix()` 改為先檢查 ADR-020 檔案白名單再執行 110 preflight,避免 `tests/...` 這類不得自動修的 finding 被誤報成 `/home/wooo/ewoooc` preflight 失敗;Code Review 完成通知會把全數不在白名單的 finding 標成需人工處理,不再宣稱已觸發 AiderHeal;白名單同步放行 `services/routes/database` 子目錄 Python 檔,preflight 通知帶 stderr/stdout 細節,健康檢查接受正式 `/health` 回傳的 `healthy`。
|
||||
|
||||
@@ -52,6 +52,9 @@ from services.market_intel.mcp_fetch_candidate_queue_writer_review_handoff impor
|
||||
from services.market_intel.mcp_fetch_candidate_queue_writer_review_inventory import (
|
||||
build_mcp_fetch_candidate_queue_writer_review_inventory_preview,
|
||||
)
|
||||
from services.market_intel.mcp_fetch_candidate_queue_writer_review_decision import (
|
||||
build_mcp_fetch_candidate_queue_writer_review_decision_preview,
|
||||
)
|
||||
|
||||
|
||||
@market_intel_bp.route("/api/market_intel/mcp_fetch_run_package", methods=["GET", "POST"])
|
||||
@@ -715,3 +718,53 @@ def market_intel_mcp_fetch_candidate_queue_writer_review_inventory():
|
||||
phase=service.phase,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@market_intel_bp.route(
|
||||
"/api/market_intel/mcp_fetch_candidate_queue_writer_review_decision",
|
||||
methods=["GET", "POST"],
|
||||
)
|
||||
@login_required
|
||||
def market_intel_mcp_fetch_candidate_queue_writer_review_decision():
|
||||
writer_review_inventory_package = {}
|
||||
writer_review_inventory_result = None
|
||||
operator_review_decision = None
|
||||
if request.method == "POST":
|
||||
payload = request.get_json(silent=True) or {}
|
||||
package = (
|
||||
payload.get("writer_review_decision_package")
|
||||
or payload.get("candidate_queue_writer_review_decision")
|
||||
or payload.get("review_decision")
|
||||
or payload.get("operator_review_decision")
|
||||
or payload
|
||||
)
|
||||
writer_review_inventory_package = (
|
||||
package.get("writer_review_inventory_package")
|
||||
or package.get("candidate_queue_writer_review_inventory")
|
||||
or package.get("writer_review_inventory")
|
||||
or package.get("review_inventory_package")
|
||||
or package.get("review_inventory")
|
||||
or {}
|
||||
)
|
||||
writer_review_inventory_result = (
|
||||
package.get("writer_review_inventory_result")
|
||||
or package.get("mcp_fetch_candidate_queue_writer_review_inventory")
|
||||
)
|
||||
operator_review_decision = (
|
||||
package.get("operator_review_decision")
|
||||
or package.get("candidate_queue_review_decision")
|
||||
or package.get("writer_review_decision")
|
||||
or package.get("review_decision_payload")
|
||||
or package.get("decision_payload")
|
||||
or package.get("decision")
|
||||
)
|
||||
|
||||
service = MarketIntelService()
|
||||
return jsonify(
|
||||
build_mcp_fetch_candidate_queue_writer_review_decision_preview(
|
||||
writer_review_inventory_package=writer_review_inventory_package,
|
||||
writer_review_inventory_result=writer_review_inventory_result,
|
||||
operator_review_decision=operator_review_decision,
|
||||
phase=service.phase,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -108,6 +108,9 @@ from services.market_intel.mcp_fetch_candidate_queue_writer_review_handoff impor
|
||||
from services.market_intel.mcp_fetch_candidate_queue_writer_review_inventory import (
|
||||
build_mcp_fetch_candidate_queue_writer_review_inventory_preview,
|
||||
)
|
||||
from services.market_intel.mcp_fetch_candidate_queue_writer_review_decision import (
|
||||
build_mcp_fetch_candidate_queue_writer_review_decision_preview,
|
||||
)
|
||||
from services.market_intel.mcp_manual_fetch_handoff import (
|
||||
build_mcp_manual_fetch_handoff_preview,
|
||||
)
|
||||
@@ -287,8 +290,17 @@ PRODUCTION_SMOKE_TARGETS = (
|
||||
+ ("/api/market_intel/mcp_fetch_candidate_queue_writer_review_inventory",)
|
||||
+ PRODUCTION_SMOKE_TARGETS[-1:]
|
||||
)
|
||||
PRODUCTION_SMOKE_TARGETS = (
|
||||
PRODUCTION_SMOKE_TARGETS[:-1]
|
||||
+ ("/api/market_intel/mcp_fetch_candidate_queue_writer_review_decision",)
|
||||
+ 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):
|
||||
"""建立市場情報推版準備狀態;不執行 git、部署或遠端操作。"""
|
||||
status = service.get_runtime_status()
|
||||
@@ -381,6 +393,11 @@ def build_deployment_readiness_preview(*, service, market_intel_tables, schema_s
|
||||
phase=service.phase,
|
||||
)
|
||||
)
|
||||
mcp_fetch_candidate_queue_writer_review_decision = (
|
||||
build_mcp_fetch_candidate_queue_writer_review_decision_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()
|
||||
@@ -1325,6 +1342,76 @@ def build_deployment_readiness_preview(*, service, market_intel_tables, schema_s
|
||||
"candidate_review_state_updated"
|
||||
]
|
||||
),
|
||||
"mcp_fetch_candidate_queue_writer_review_decision_preview_safe": bool(
|
||||
mcp_fetch_candidate_queue_writer_review_decision["mode"]
|
||||
== "mcp_fetch_candidate_queue_writer_review_decision_preview"
|
||||
and not mcp_fetch_candidate_queue_writer_review_decision[
|
||||
"payload_persisted"
|
||||
]
|
||||
and not mcp_fetch_candidate_queue_writer_review_decision[
|
||||
"review_decision_persisted"
|
||||
]
|
||||
and not mcp_fetch_candidate_queue_writer_review_decision[
|
||||
"candidate_queue_review_decision_persisted"
|
||||
]
|
||||
and not mcp_fetch_candidate_queue_writer_review_decision[
|
||||
"candidate_queue_review_decision_file_written"
|
||||
]
|
||||
and not mcp_fetch_candidate_queue_writer_review_decision[
|
||||
"decision_record_written"
|
||||
]
|
||||
and not mcp_fetch_candidate_queue_writer_review_decision[
|
||||
"decision_record_file_written"
|
||||
]
|
||||
and not mcp_fetch_candidate_queue_writer_review_decision[
|
||||
"package_artifact_created"
|
||||
]
|
||||
and not mcp_fetch_candidate_queue_writer_review_decision[
|
||||
"api_writes_file"
|
||||
]
|
||||
and not mcp_fetch_candidate_queue_writer_review_decision[
|
||||
"api_executes_cli"
|
||||
]
|
||||
and not mcp_fetch_candidate_queue_writer_review_decision[
|
||||
"api_reads_approval_token"
|
||||
]
|
||||
and not mcp_fetch_candidate_queue_writer_review_decision[
|
||||
"api_opens_database_connection"
|
||||
]
|
||||
and not mcp_fetch_candidate_queue_writer_review_decision[
|
||||
"api_writes_database"
|
||||
]
|
||||
and not mcp_fetch_candidate_queue_writer_review_decision[
|
||||
"api_updates_review_state"
|
||||
]
|
||||
and not mcp_fetch_candidate_queue_writer_review_decision[
|
||||
"api_writes_decision_record"
|
||||
]
|
||||
and not mcp_fetch_candidate_queue_writer_review_decision[
|
||||
"database_write_executed"
|
||||
]
|
||||
and not mcp_fetch_candidate_queue_writer_review_decision[
|
||||
"database_commit_executed"
|
||||
]
|
||||
and not mcp_fetch_candidate_queue_writer_review_decision[
|
||||
"database_connection_opened"
|
||||
]
|
||||
and not mcp_fetch_candidate_queue_writer_review_decision[
|
||||
"cli_executed"
|
||||
]
|
||||
and not mcp_fetch_candidate_queue_writer_review_decision[
|
||||
"file_written"
|
||||
]
|
||||
and not mcp_fetch_candidate_queue_writer_review_decision[
|
||||
"scheduler_attached"
|
||||
]
|
||||
and not mcp_fetch_candidate_queue_writer_review_decision[
|
||||
"candidate_queue_persisted"
|
||||
]
|
||||
and not mcp_fetch_candidate_queue_writer_review_decision[
|
||||
"candidate_review_state_updated"
|
||||
]
|
||||
),
|
||||
"candidate_queue_writer_postwrite_smoke_planned_safe": bool(
|
||||
candidate_queue_writer_postwrite_smoke["mode"]
|
||||
== "candidate_queue_writer_postwrite_smoke_planned"
|
||||
@@ -1653,6 +1740,7 @@ def build_deployment_readiness_preview(*, service, market_intel_tables, schema_s
|
||||
"mcp_fetch_candidate_queue_writer_post_closeout_inventory_review": mcp_fetch_candidate_queue_writer_post_closeout_inventory_review,
|
||||
"mcp_fetch_candidate_queue_writer_review_handoff": mcp_fetch_candidate_queue_writer_review_handoff,
|
||||
"mcp_fetch_candidate_queue_writer_review_inventory": mcp_fetch_candidate_queue_writer_review_inventory,
|
||||
"mcp_fetch_candidate_queue_writer_review_decision": mcp_fetch_candidate_queue_writer_review_decision,
|
||||
"scheduler_plan": scheduler_plan,
|
||||
"manual_sample_plan": manual_sample_plan,
|
||||
"manual_sample_acceptance": manual_sample_acceptance,
|
||||
|
||||
@@ -0,0 +1,498 @@
|
||||
"""市場情報 MCP fetch candidate queue writer review decision。
|
||||
|
||||
本模組只審核 review inventory 後的人工候選隊列決策摘要;
|
||||
API/UI 不讀 approval token、不執行 CLI、不開 DB、不寫 queue、不做 inventory query、
|
||||
不更新 review_state、不寫 match result、不掛 scheduler。
|
||||
"""
|
||||
|
||||
from services.market_intel.mcp_fetch_candidate_queue_writer_preflight import TARGET_TABLE
|
||||
from services.market_intel.mcp_fetch_candidate_queue_writer_run_readiness import (
|
||||
ARTIFACT_PREFIX,
|
||||
)
|
||||
from services.market_intel.mcp_fetch_candidate_queue_writer_post_closeout_inventory_review import (
|
||||
FORBIDDEN_SECRET_KEYS,
|
||||
SAFE_SECRET_METADATA_KEYS,
|
||||
_as_dict,
|
||||
_as_list,
|
||||
_blocked_side_effects,
|
||||
_contains_forbidden_key,
|
||||
_safe_int,
|
||||
_safe_path,
|
||||
_safe_text,
|
||||
)
|
||||
from services.market_intel.mcp_fetch_candidate_queue_writer_review_inventory import (
|
||||
build_mcp_fetch_candidate_queue_writer_review_inventory_preview,
|
||||
)
|
||||
from services.market_intel.mcp_fetch_candidate_queue_writer_review_decision_gates import (
|
||||
ALLOWED_REVIEW_DECISIONS,
|
||||
build_review_decision_gates,
|
||||
)
|
||||
from services.market_intel.mcp_fetch_candidate_queue_writer_review_decision_sample import (
|
||||
build_sample_writer_review_decision_package,
|
||||
)
|
||||
|
||||
|
||||
_REVIEW_DECISION_BLOCKED_SIDE_EFFECT_KEYS = (
|
||||
"allow_api_candidate_review_update",
|
||||
"allow_api_execution",
|
||||
"allow_api_file_write",
|
||||
"allow_api_inventory_query",
|
||||
"allow_api_match_write",
|
||||
"allow_api_queue_insert",
|
||||
"allow_api_queue_write",
|
||||
"allow_cli_execution",
|
||||
"allow_database_write",
|
||||
"api_candidate_review_update_executed",
|
||||
"api_created_review_decision_file",
|
||||
"api_created_review_queue",
|
||||
"api_executed_candidate_review",
|
||||
"api_inventory_query_executed",
|
||||
"api_review_decision_persisted",
|
||||
"api_review_state_update_executed",
|
||||
"api_updates_review_state",
|
||||
"api_writes_match_result",
|
||||
"api_writes_review_queue",
|
||||
"candidate_queue_review_decision_file_written",
|
||||
"candidate_queue_review_decision_persisted",
|
||||
"candidate_queue_review_started_by_api",
|
||||
"candidate_review_file_written",
|
||||
"candidate_review_queue_written",
|
||||
"decision_record_written",
|
||||
"identity_match_written",
|
||||
"manual_review_record_written",
|
||||
"market_product_match_written",
|
||||
"match_result_written",
|
||||
"queue_review_persisted",
|
||||
"review_decision_file_written",
|
||||
"review_decision_persisted",
|
||||
"review_queue_file_written",
|
||||
"review_queue_row_written",
|
||||
"review_state_update_executed",
|
||||
)
|
||||
|
||||
|
||||
def _blocked_review_decision_side_effects(payload):
|
||||
found = list(_blocked_side_effects(payload))
|
||||
|
||||
def visit(value, path):
|
||||
if isinstance(value, dict):
|
||||
for key, item in value.items():
|
||||
normalized_key = str(key).lower()
|
||||
key_path = f"{path}.{key}" if path else key
|
||||
if (
|
||||
normalized_key in _REVIEW_DECISION_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 sorted(set(found))
|
||||
|
||||
|
||||
def _inventory_from_inputs(inventory_package, inventory_result, phase):
|
||||
if isinstance(inventory_result, dict) and inventory_result:
|
||||
return inventory_result
|
||||
|
||||
inventory_package = _as_dict(inventory_package)
|
||||
return build_mcp_fetch_candidate_queue_writer_review_inventory_preview(
|
||||
writer_review_handoff_package=(
|
||||
inventory_package.get("writer_review_handoff_package")
|
||||
or inventory_package.get("candidate_queue_writer_review_handoff")
|
||||
or inventory_package.get("writer_review_handoff")
|
||||
or inventory_package.get("review_handoff_package")
|
||||
or inventory_package.get("review_handoff")
|
||||
or {}
|
||||
),
|
||||
writer_review_handoff_result=(
|
||||
inventory_package.get("writer_review_handoff_result")
|
||||
or inventory_package.get(
|
||||
"mcp_fetch_candidate_queue_writer_review_handoff"
|
||||
)
|
||||
),
|
||||
operator_review_inventory=(
|
||||
inventory_package.get("operator_review_inventory")
|
||||
or inventory_package.get("candidate_queue_review_inventory")
|
||||
or inventory_package.get("writer_review_inventory")
|
||||
or inventory_package.get("review_inventory_payload")
|
||||
or inventory_package.get("inventory_payload")
|
||||
or inventory_package.get("inventory")
|
||||
),
|
||||
phase=phase,
|
||||
)
|
||||
|
||||
|
||||
def _inventory_summary(inventory_result):
|
||||
inventory_result = _as_dict(inventory_result)
|
||||
inventory = _as_dict(inventory_result.get("operator_review_inventory_summary"))
|
||||
side_effects_clear = bool(
|
||||
not inventory_result.get("payload_persisted")
|
||||
and not inventory_result.get("review_inventory_persisted")
|
||||
and not inventory_result.get("writer_review_inventory_persisted")
|
||||
and not inventory_result.get("candidate_queue_review_inventory_persisted")
|
||||
and not inventory_result.get("candidate_queue_review_inventory_file_written")
|
||||
and not inventory_result.get("review_inventory_file_written")
|
||||
and not inventory_result.get("inventory_file_written")
|
||||
and not inventory_result.get("review_queue_file_written")
|
||||
and not inventory_result.get("candidate_review_file_written")
|
||||
and not inventory_result.get("package_artifact_created")
|
||||
and not inventory_result.get("network_request_allowed")
|
||||
and not inventory_result.get("api_executes_cli")
|
||||
and not inventory_result.get("api_reads_approval_token")
|
||||
and not inventory_result.get("api_opens_database_connection")
|
||||
and not inventory_result.get("api_writes_database")
|
||||
and not inventory_result.get("api_writes_file")
|
||||
and not inventory_result.get("api_uses_external_network")
|
||||
and not inventory_result.get("api_inventory_query_executed")
|
||||
and not inventory_result.get("api_candidate_review_update_executed")
|
||||
and not inventory_result.get("api_updates_review_state")
|
||||
and not inventory_result.get("review_inventory_query_executed_by_api")
|
||||
and not inventory_result.get("database_write_executed")
|
||||
and not inventory_result.get("database_commit_executed")
|
||||
and not inventory_result.get("cli_executed")
|
||||
and not inventory_result.get("file_written")
|
||||
and not inventory_result.get("scheduler_attached")
|
||||
and not inventory_result.get("candidate_queue_created")
|
||||
and not inventory_result.get("candidate_queue_persisted")
|
||||
and not inventory_result.get("candidate_review_state_updated")
|
||||
)
|
||||
return {
|
||||
"mode": inventory_result.get("mode"),
|
||||
"accepted": bool(
|
||||
inventory_result.get(
|
||||
"mcp_fetch_candidate_queue_writer_review_inventory_accepted"
|
||||
)
|
||||
),
|
||||
"ready_for_candidate_queue_review_decision": bool(
|
||||
inventory_result.get("ready_for_candidate_queue_review_decision")
|
||||
),
|
||||
"review_inventory_id": _safe_text(
|
||||
inventory.get("review_inventory_id"), 160
|
||||
),
|
||||
"handoff_id": _safe_text(inventory.get("handoff_id"), 160),
|
||||
"inventory_review_id": _safe_text(inventory.get("inventory_review_id"), 160),
|
||||
"closeout_id": _safe_text(inventory.get("closeout_id"), 160),
|
||||
"run_package_id": _safe_text(inventory.get("run_package_id"), 160),
|
||||
"receipt_id": _safe_text(inventory.get("receipt_id"), 160),
|
||||
"target_table": _safe_text(inventory.get("target_table"), 160),
|
||||
"payload_row_count": _safe_int(inventory.get("payload_row_count")),
|
||||
"expected_payload_row_count": _safe_int(
|
||||
inventory.get("expected_payload_row_count")
|
||||
or inventory.get("payload_row_count")
|
||||
),
|
||||
"found_row_count": _safe_int(inventory.get("found_row_count")),
|
||||
"dedupe_key_count": _safe_int(inventory.get("dedupe_key_count")),
|
||||
"sample_dedupe_keys": [
|
||||
_safe_text(item, 160)
|
||||
for item in _as_list(inventory.get("sample_dedupe_keys"))
|
||||
if _safe_text(item, 160)
|
||||
],
|
||||
"review_state": _safe_text(inventory.get("review_state"), 80),
|
||||
"review_state_breakdown": _as_dict(inventory.get("review_state_breakdown")),
|
||||
"review_queue_artifact_path": _safe_text(
|
||||
inventory.get("review_queue_artifact_path")
|
||||
),
|
||||
"review_inventory_artifact_path": _safe_text(
|
||||
inventory.get("review_inventory_artifact_path")
|
||||
),
|
||||
"read_only_query_result_path": _safe_text(
|
||||
inventory.get("read_only_query_result_path")
|
||||
),
|
||||
"side_effects_clear": side_effects_clear,
|
||||
"blocked_reasons": inventory_result.get("blocked_reasons", []),
|
||||
}
|
||||
|
||||
|
||||
def _decision_row_summary(row):
|
||||
row = _as_dict(row)
|
||||
return {
|
||||
"dedupe_key": _safe_text(row.get("dedupe_key"), 160),
|
||||
"current_review_state": _safe_text(row.get("current_review_state"), 80),
|
||||
"proposed_review_decision": _safe_text(
|
||||
row.get("proposed_review_decision"), 80
|
||||
),
|
||||
"decision_bucket": _safe_text(row.get("decision_bucket"), 120),
|
||||
"evidence_lane": _safe_text(row.get("evidence_lane"), 120),
|
||||
"evidence_ref": _safe_text(row.get("evidence_ref")),
|
||||
"evidence_ref_safe": _safe_path(
|
||||
row.get("evidence_ref"),
|
||||
prefixes=(ARTIFACT_PREFIX,),
|
||||
suffixes=(".json",),
|
||||
),
|
||||
"decision_notes_present": bool(_safe_text(row.get("decision_notes"))),
|
||||
"hard_veto_present": bool(row.get("hard_veto_present")),
|
||||
"stronger_existing_match_conflict": bool(
|
||||
row.get("stronger_existing_match_conflict")
|
||||
),
|
||||
"false_positive_guard_passed": bool(row.get("false_positive_guard_passed")),
|
||||
"variant_conflict_checked": bool(row.get("variant_conflict_checked")),
|
||||
"overwrite_protection_checked": bool(
|
||||
row.get("overwrite_protection_checked")
|
||||
),
|
||||
"exact_identity_confirmed": bool(row.get("exact_identity_confirmed")),
|
||||
}
|
||||
|
||||
|
||||
def _operator_review_decision_summary(operator_decision):
|
||||
operator_decision = _as_dict(operator_decision)
|
||||
confirmations = _as_dict(operator_decision.get("operator_confirmations"))
|
||||
rows = [_decision_row_summary(row) for row in _as_list(
|
||||
operator_decision.get("decision_rows")
|
||||
)]
|
||||
expected_dedupe_keys = [
|
||||
_safe_text(item, 160)
|
||||
for item in _as_list(operator_decision.get("expected_dedupe_keys"))
|
||||
if _safe_text(item, 160)
|
||||
]
|
||||
return {
|
||||
"provided_keys": sorted(operator_decision.keys()),
|
||||
"review_decision_id": _safe_text(
|
||||
operator_decision.get("review_decision_id"), 160
|
||||
),
|
||||
"review_inventory_id": _safe_text(
|
||||
operator_decision.get("review_inventory_id"), 160
|
||||
),
|
||||
"handoff_id": _safe_text(operator_decision.get("handoff_id"), 160),
|
||||
"inventory_review_id": _safe_text(
|
||||
operator_decision.get("inventory_review_id"), 160
|
||||
),
|
||||
"closeout_id": _safe_text(operator_decision.get("closeout_id"), 160),
|
||||
"run_package_id": _safe_text(operator_decision.get("run_package_id"), 160),
|
||||
"receipt_id": _safe_text(operator_decision.get("receipt_id"), 160),
|
||||
"target_table": _safe_text(operator_decision.get("target_table"), 160),
|
||||
"decision_scope": _safe_text(operator_decision.get("decision_scope"), 120),
|
||||
"decision_mode": _safe_text(operator_decision.get("decision_mode"), 120),
|
||||
"expected_current_review_state": _safe_text(
|
||||
operator_decision.get("expected_current_review_state"), 80
|
||||
),
|
||||
"decision_row_count": _safe_int(operator_decision.get("decision_row_count")),
|
||||
"expected_decision_row_count": _safe_int(
|
||||
operator_decision.get("expected_decision_row_count")
|
||||
or operator_decision.get("decision_row_count")
|
||||
),
|
||||
"expected_dedupe_keys": expected_dedupe_keys,
|
||||
"decision_rows": rows,
|
||||
"review_queue_artifact_path": _safe_text(
|
||||
operator_decision.get("review_queue_artifact_path")
|
||||
),
|
||||
"review_inventory_artifact_path": _safe_text(
|
||||
operator_decision.get("review_inventory_artifact_path")
|
||||
),
|
||||
"review_decision_artifact_path": _safe_text(
|
||||
operator_decision.get("review_decision_artifact_path")
|
||||
),
|
||||
"review_queue_artifact_path_safe": _safe_path(
|
||||
operator_decision.get("review_queue_artifact_path"),
|
||||
prefixes=(ARTIFACT_PREFIX,),
|
||||
suffixes=(".json",),
|
||||
),
|
||||
"review_inventory_artifact_path_safe": _safe_path(
|
||||
operator_decision.get("review_inventory_artifact_path"),
|
||||
prefixes=(ARTIFACT_PREFIX,),
|
||||
suffixes=(".json",),
|
||||
),
|
||||
"review_decision_artifact_path_safe": _safe_path(
|
||||
operator_decision.get("review_decision_artifact_path"),
|
||||
prefixes=(ARTIFACT_PREFIX,),
|
||||
suffixes=(".json",),
|
||||
),
|
||||
"review_inventory_checked": bool(
|
||||
confirmations.get("review_inventory_checked")
|
||||
),
|
||||
"candidate_queue_review_only": bool(
|
||||
confirmations.get("candidate_queue_review_only")
|
||||
),
|
||||
"human_decision_only": bool(confirmations.get("human_decision_only")),
|
||||
"manual_record_required": bool(confirmations.get("manual_record_required")),
|
||||
"no_approval_token_payload": bool(
|
||||
confirmations.get("no_approval_token_payload")
|
||||
),
|
||||
"no_api_cli_execution": bool(confirmations.get("no_api_cli_execution")),
|
||||
"no_api_database_write": bool(confirmations.get("no_api_database_write")),
|
||||
"no_api_inventory_query": bool(confirmations.get("no_api_inventory_query")),
|
||||
"no_api_review_state_update": bool(
|
||||
confirmations.get("no_api_review_state_update")
|
||||
),
|
||||
"no_api_queue_insert": bool(confirmations.get("no_api_queue_insert")),
|
||||
"no_api_file_write": bool(confirmations.get("no_api_file_write")),
|
||||
"no_scheduler_attach": bool(confirmations.get("no_scheduler_attach")),
|
||||
"stronger_existing_match_guard_preserved": bool(
|
||||
confirmations.get("stronger_existing_match_guard_preserved")
|
||||
),
|
||||
"false_positive_guard_completed": bool(
|
||||
confirmations.get("false_positive_guard_completed")
|
||||
),
|
||||
"variant_sensitive_review_completed": bool(
|
||||
confirmations.get("variant_sensitive_review_completed")
|
||||
),
|
||||
"hard_veto_respected": bool(confirmations.get("hard_veto_respected")),
|
||||
"api_execution_allowed": bool(operator_decision.get("api_execution_allowed")),
|
||||
"real_write_allowed_by_api": bool(
|
||||
operator_decision.get("real_write_allowed_by_api")
|
||||
),
|
||||
"api_candidate_review_allowed": bool(
|
||||
operator_decision.get("api_candidate_review_allowed")
|
||||
),
|
||||
"api_updates_review_state": bool(
|
||||
operator_decision.get("api_updates_review_state")
|
||||
),
|
||||
"api_writes_match_result": bool(
|
||||
operator_decision.get("api_writes_match_result")
|
||||
),
|
||||
"secret_or_token_submitted_to_api": _contains_forbidden_key(
|
||||
operator_decision,
|
||||
FORBIDDEN_SECRET_KEYS,
|
||||
safe_keys=SAFE_SECRET_METADATA_KEYS,
|
||||
),
|
||||
"blocked_side_effects": _blocked_review_decision_side_effects(
|
||||
operator_decision
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
def build_mcp_fetch_candidate_queue_writer_review_decision_preview(
|
||||
*,
|
||||
writer_review_inventory_package=None,
|
||||
writer_review_inventory_result=None,
|
||||
operator_review_decision=None,
|
||||
phase=None,
|
||||
):
|
||||
"""建立 writer review decision;不執行查詢、寫檔或寫入。"""
|
||||
writer_review_inventory_package = _as_dict(writer_review_inventory_package)
|
||||
inventory_result_received = bool(
|
||||
isinstance(writer_review_inventory_result, dict)
|
||||
and writer_review_inventory_result
|
||||
)
|
||||
decision_valid_object = (
|
||||
isinstance(operator_review_decision, dict)
|
||||
if operator_review_decision is not None
|
||||
else True
|
||||
)
|
||||
decision_payload = _as_dict(operator_review_decision)
|
||||
inventory_received = bool(
|
||||
writer_review_inventory_package or inventory_result_received
|
||||
)
|
||||
inventory_result = (
|
||||
_inventory_from_inputs(
|
||||
writer_review_inventory_package,
|
||||
writer_review_inventory_result,
|
||||
phase,
|
||||
)
|
||||
if inventory_received
|
||||
else {}
|
||||
)
|
||||
payload_received = bool(
|
||||
inventory_received or decision_payload or operator_review_decision is not None
|
||||
)
|
||||
decision_received = bool(decision_payload)
|
||||
inventory = _inventory_summary(inventory_result)
|
||||
decision = _operator_review_decision_summary(decision_payload)
|
||||
gates = build_review_decision_gates(
|
||||
inventory_received=inventory_received,
|
||||
decision_received=decision_received and decision_valid_object,
|
||||
inventory=inventory,
|
||||
decision=decision,
|
||||
)
|
||||
blocked_reasons = [gate["key"] for gate in gates if not gate["passed"]]
|
||||
if not decision_valid_object:
|
||||
blocked_reasons.append("operator_review_decision_payload_valid_object")
|
||||
accepted = bool(payload_received and not blocked_reasons)
|
||||
|
||||
return {
|
||||
"mode": (
|
||||
"mcp_fetch_candidate_queue_writer_review_decision"
|
||||
if payload_received
|
||||
else "mcp_fetch_candidate_queue_writer_review_decision_preview"
|
||||
),
|
||||
"phase": phase,
|
||||
"writer_review_decision_payload_received": payload_received,
|
||||
"review_decision_payload_received": payload_received,
|
||||
"writer_review_inventory_received": inventory_received,
|
||||
"operator_review_decision_received": decision_received,
|
||||
"operator_review_decision_valid_object": decision_valid_object,
|
||||
"writer_review_inventory_accepted": inventory["accepted"],
|
||||
"mcp_fetch_candidate_queue_writer_review_decision_accepted": accepted,
|
||||
"candidate_queue_writer_review_decision_ready": accepted,
|
||||
"ready_for_candidate_queue_human_review": accepted,
|
||||
"ready_for_candidate_queue_review_decision_approval": accepted,
|
||||
"ready_for_candidate_queue_review_state_writer": False,
|
||||
"ready_for_candidate_queue_review_decision_writer_preflight": False,
|
||||
"ready_for_api_database_write": False,
|
||||
"ready_for_real_write": False,
|
||||
"ready_for_scheduler_attach": False,
|
||||
"ready_for_candidate_queue_review_api_update": False,
|
||||
"ready_for_api_review_state_update": False,
|
||||
"ready_for_api_match_write": False,
|
||||
"network_request_allowed": False,
|
||||
"api_executes_cli": False,
|
||||
"api_reads_approval_token": False,
|
||||
"api_opens_database_connection": False,
|
||||
"api_writes_database": False,
|
||||
"api_writes_file": False,
|
||||
"api_uses_external_network": False,
|
||||
"api_inventory_query_executed": False,
|
||||
"api_candidate_review_update_executed": False,
|
||||
"api_updates_review_state": False,
|
||||
"api_writes_decision_record": False,
|
||||
"api_writes_match_result": False,
|
||||
"review_decision_query_executed_by_api": False,
|
||||
"payload_row_count": decision["decision_row_count"],
|
||||
"gate_count": len(gates),
|
||||
"passed_gate_count": sum(1 for gate in gates if gate["passed"]),
|
||||
"blocked_reasons": blocked_reasons,
|
||||
"gates": gates,
|
||||
"writer_review_inventory_summary": inventory,
|
||||
"operator_review_decision_summary": decision,
|
||||
"decision_rows": decision["decision_rows"],
|
||||
"decision_contract": {
|
||||
"expected_current_state": "needs_review",
|
||||
"allowed_next_states": list(ALLOWED_REVIEW_DECISIONS),
|
||||
"manual_decision_required": True,
|
||||
"next_gate": "candidate_queue_review_decision_approval",
|
||||
"decision_scope": "candidate_queue_review_decision",
|
||||
"forbidden_api_actions": [
|
||||
"update_review_state",
|
||||
"write_match_result",
|
||||
"write_decision_record",
|
||||
"insert_missing_queue_row",
|
||||
"attach_scheduler",
|
||||
],
|
||||
},
|
||||
"sample_writer_review_decision_package": (
|
||||
build_sample_writer_review_decision_package()
|
||||
),
|
||||
"next_operator_steps": [
|
||||
"Decision 通過後,只代表可進入人工 approval / writer preflight 設計",
|
||||
"API/UI 仍不得自動寫 review_state、寫 match result、補 queue row 或讀 token",
|
||||
"matched 決策必須保留 exact identity、variant、false-positive 與 overwrite guard",
|
||||
],
|
||||
"payload_persisted": False,
|
||||
"review_decision_persisted": False,
|
||||
"writer_review_decision_persisted": False,
|
||||
"candidate_queue_review_decision_persisted": False,
|
||||
"candidate_queue_review_decision_file_written": False,
|
||||
"review_decision_file_written": False,
|
||||
"decision_record_written": False,
|
||||
"decision_record_file_written": False,
|
||||
"review_queue_file_written": False,
|
||||
"candidate_review_file_written": False,
|
||||
"match_result_written": False,
|
||||
"market_product_match_written": False,
|
||||
"package_artifact_created": False,
|
||||
"database_connection_opened": False,
|
||||
"database_session_created": False,
|
||||
"database_commit_executed": False,
|
||||
"database_write_executed": False,
|
||||
"external_network_executed": False,
|
||||
"cli_executed": False,
|
||||
"file_written": False,
|
||||
"writes_executed": False,
|
||||
"would_write_database": False,
|
||||
"scheduler_attached": False,
|
||||
"candidate_queue_created": False,
|
||||
"candidate_queue_persisted": False,
|
||||
"candidate_review_state_updated": False,
|
||||
}
|
||||
@@ -0,0 +1,241 @@
|
||||
"""Gate checks for market intel candidate queue review decision."""
|
||||
|
||||
from services.market_intel.mcp_fetch_candidate_queue_writer_preflight import TARGET_TABLE
|
||||
|
||||
|
||||
ALLOWED_REVIEW_DECISIONS = (
|
||||
"matched",
|
||||
"low_score",
|
||||
"identity_veto",
|
||||
"unit_comparable",
|
||||
"fresh_search_required",
|
||||
"deferred",
|
||||
)
|
||||
|
||||
|
||||
def _sample_keys_covered_by_expected(inventory, decision):
|
||||
sample_keys = set(inventory["sample_dedupe_keys"])
|
||||
expected_keys = set(decision["expected_dedupe_keys"])
|
||||
return bool(sample_keys and sample_keys.issubset(expected_keys))
|
||||
|
||||
|
||||
def _row_keys_match_expected(decision):
|
||||
row_keys = [row["dedupe_key"] for row in decision["decision_rows"]]
|
||||
expected_keys = decision["expected_dedupe_keys"]
|
||||
return bool(
|
||||
row_keys
|
||||
and len(row_keys) == len(set(row_keys))
|
||||
and set(row_keys) == set(expected_keys)
|
||||
)
|
||||
|
||||
|
||||
def _all_rows_have_allowed_decisions(decision):
|
||||
return bool(
|
||||
decision["decision_rows"]
|
||||
and all(
|
||||
row["proposed_review_decision"] in ALLOWED_REVIEW_DECISIONS
|
||||
for row in decision["decision_rows"]
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def _all_rows_have_manual_evidence(decision):
|
||||
return bool(
|
||||
decision["decision_rows"]
|
||||
and all(
|
||||
row["decision_notes_present"] and row["evidence_ref_safe"]
|
||||
for row in decision["decision_rows"]
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def _matched_rows_keep_identity_guards(decision):
|
||||
matched_rows = [
|
||||
row
|
||||
for row in decision["decision_rows"]
|
||||
if row["proposed_review_decision"] == "matched"
|
||||
]
|
||||
if not matched_rows:
|
||||
return True
|
||||
return all(
|
||||
row["evidence_lane"] == "exact_identity"
|
||||
and row["exact_identity_confirmed"]
|
||||
and not row["hard_veto_present"]
|
||||
and not row["stronger_existing_match_conflict"]
|
||||
and row["false_positive_guard_passed"]
|
||||
and row["variant_conflict_checked"]
|
||||
and row["overwrite_protection_checked"]
|
||||
for row in matched_rows
|
||||
)
|
||||
|
||||
|
||||
def build_review_decision_gates(*, inventory_received, decision_received, inventory, decision):
|
||||
operator_confirmed_boundaries = bool(
|
||||
decision["review_inventory_checked"]
|
||||
and decision["candidate_queue_review_only"]
|
||||
and decision["human_decision_only"]
|
||||
and decision["manual_record_required"]
|
||||
and decision["no_approval_token_payload"]
|
||||
and decision["no_api_cli_execution"]
|
||||
and decision["no_api_database_write"]
|
||||
and decision["no_api_inventory_query"]
|
||||
and decision["no_api_review_state_update"]
|
||||
and decision["no_api_queue_insert"]
|
||||
and decision["no_api_file_write"]
|
||||
and decision["no_scheduler_attach"]
|
||||
and decision["stronger_existing_match_guard_preserved"]
|
||||
and decision["false_positive_guard_completed"]
|
||||
and decision["variant_sensitive_review_completed"]
|
||||
and decision["hard_veto_respected"]
|
||||
)
|
||||
return [
|
||||
{
|
||||
"key": "candidate_queue_review_inventory_payload_or_result_received",
|
||||
"label": "已提供 review inventory package 或已審核結果",
|
||||
"passed": inventory_received,
|
||||
},
|
||||
{
|
||||
"key": "candidate_queue_review_inventory_accepted",
|
||||
"label": "review inventory gate 必須已通過",
|
||||
"passed": inventory["accepted"],
|
||||
},
|
||||
{
|
||||
"key": "candidate_queue_review_inventory_ready_for_decision",
|
||||
"label": "inventory 必須只放行到 candidate queue review decision",
|
||||
"passed": inventory["ready_for_candidate_queue_review_decision"],
|
||||
},
|
||||
{
|
||||
"key": "candidate_queue_review_inventory_side_effect_free",
|
||||
"label": "inventory 未顯示 API 執行、寫 DB、查 DB、更新 review_state 或掛 scheduler",
|
||||
"passed": inventory["side_effects_clear"],
|
||||
},
|
||||
{
|
||||
"key": "candidate_queue_review_decision_payload_received",
|
||||
"label": "已提供 operator candidate queue review decision 摘要",
|
||||
"passed": decision_received,
|
||||
},
|
||||
{
|
||||
"key": "candidate_queue_review_decision_identity_recorded",
|
||||
"label": "decision 必須記錄 review_decision_id 與上游 identity",
|
||||
"passed": bool(
|
||||
decision["review_decision_id"]
|
||||
and decision["review_inventory_id"]
|
||||
and decision["handoff_id"]
|
||||
and decision["inventory_review_id"]
|
||||
and decision["closeout_id"]
|
||||
and decision["run_package_id"]
|
||||
and decision["receipt_id"]
|
||||
),
|
||||
},
|
||||
{
|
||||
"key": "candidate_queue_review_decision_identity_matches_inventory",
|
||||
"label": "decision identity 必須對齊 review inventory",
|
||||
"passed": bool(
|
||||
decision["review_inventory_id"] == inventory["review_inventory_id"]
|
||||
and decision["handoff_id"] == inventory["handoff_id"]
|
||||
and decision["inventory_review_id"] == inventory["inventory_review_id"]
|
||||
and decision["closeout_id"] == inventory["closeout_id"]
|
||||
and decision["run_package_id"] == inventory["run_package_id"]
|
||||
and decision["receipt_id"] == inventory["receipt_id"]
|
||||
),
|
||||
},
|
||||
{
|
||||
"key": "candidate_queue_review_decision_target_table_safe",
|
||||
"label": "target table 必須是 market_alert_review_queue",
|
||||
"passed": decision["target_table"] == TARGET_TABLE,
|
||||
},
|
||||
{
|
||||
"key": "candidate_queue_review_decision_scope_safe",
|
||||
"label": "decision 僅能是 human review only",
|
||||
"passed": bool(
|
||||
decision["decision_scope"] == "candidate_queue_review_decision"
|
||||
and decision["decision_mode"] == "human_review_only"
|
||||
and decision["expected_current_review_state"] == "needs_review"
|
||||
),
|
||||
},
|
||||
{
|
||||
"key": "candidate_queue_review_decision_row_count_matches_inventory",
|
||||
"label": "decision row count 必須對齊 inventory dedupe count",
|
||||
"passed": bool(
|
||||
decision["expected_decision_row_count"]
|
||||
and decision["decision_row_count"]
|
||||
== decision["expected_decision_row_count"]
|
||||
== inventory["dedupe_key_count"]
|
||||
== inventory["found_row_count"]
|
||||
and len(decision["decision_rows"]) == inventory["dedupe_key_count"]
|
||||
),
|
||||
},
|
||||
{
|
||||
"key": "candidate_queue_review_decision_dedupe_keys_match_inventory",
|
||||
"label": "decision dedupe keys 必須覆蓋 inventory keys 且逐列對齊",
|
||||
"passed": bool(
|
||||
_sample_keys_covered_by_expected(inventory, decision)
|
||||
and _row_keys_match_expected(decision)
|
||||
),
|
||||
},
|
||||
{
|
||||
"key": "candidate_queue_review_decision_rows_still_needs_review",
|
||||
"label": "所有 decision rows 的目前狀態必須仍是 needs_review",
|
||||
"passed": bool(
|
||||
decision["decision_rows"]
|
||||
and all(
|
||||
row["current_review_state"] == "needs_review"
|
||||
for row in decision["decision_rows"]
|
||||
)
|
||||
),
|
||||
},
|
||||
{
|
||||
"key": "candidate_queue_review_decision_values_allowed",
|
||||
"label": "人工決策只能使用允許的狀態集合",
|
||||
"passed": _all_rows_have_allowed_decisions(decision),
|
||||
},
|
||||
{
|
||||
"key": "candidate_queue_review_decision_row_evidence_complete",
|
||||
"label": "每列決策必須保留 evidence ref 與 notes",
|
||||
"passed": _all_rows_have_manual_evidence(decision),
|
||||
},
|
||||
{
|
||||
"key": "candidate_queue_review_decision_matched_rows_keep_identity_guards",
|
||||
"label": "matched rows 必須保留 exact identity、false-positive、variant 與 overwrite guard",
|
||||
"passed": _matched_rows_keep_identity_guards(decision),
|
||||
},
|
||||
{
|
||||
"key": "candidate_queue_review_decision_artifact_paths_safe",
|
||||
"label": "review queue、inventory 與 decision artifact paths 必須安全並對齊",
|
||||
"passed": bool(
|
||||
decision["review_queue_artifact_path_safe"]
|
||||
and decision["review_inventory_artifact_path_safe"]
|
||||
and decision["review_decision_artifact_path_safe"]
|
||||
and decision["review_queue_artifact_path"]
|
||||
== inventory["review_queue_artifact_path"]
|
||||
and decision["review_inventory_artifact_path"]
|
||||
== inventory["review_inventory_artifact_path"]
|
||||
),
|
||||
},
|
||||
{
|
||||
"key": "candidate_queue_review_decision_operator_boundaries_confirmed",
|
||||
"label": "操作員確認 API 未執行 CLI/DB/query/file/review_state/scheduler,且 guardrail 已完成",
|
||||
"passed": operator_confirmed_boundaries,
|
||||
},
|
||||
{
|
||||
"key": "candidate_queue_review_decision_no_api_execution_or_real_write",
|
||||
"label": "decision payload 不得允許 API execution、real write、review update 或 match write",
|
||||
"passed": bool(
|
||||
not decision["api_execution_allowed"]
|
||||
and not decision["real_write_allowed_by_api"]
|
||||
and not decision["api_candidate_review_allowed"]
|
||||
and not decision["api_updates_review_state"]
|
||||
and not decision["api_writes_match_result"]
|
||||
),
|
||||
},
|
||||
{
|
||||
"key": "candidate_queue_review_decision_no_secret_or_token_key",
|
||||
"label": "decision payload 不得包含 secret、cookie、password 或 token key",
|
||||
"passed": not decision["secret_or_token_submitted_to_api"],
|
||||
},
|
||||
{
|
||||
"key": "candidate_queue_review_decision_side_effect_free",
|
||||
"label": "decision payload 不得要求 API 寫檔、執行、查 DB、寫 DB、補 queue、寫 match 或掛 scheduler",
|
||||
"passed": not decision["blocked_side_effects"],
|
||||
},
|
||||
]
|
||||
@@ -0,0 +1,118 @@
|
||||
"""Sample payload for the market intel candidate queue review decision gate."""
|
||||
|
||||
from copy import deepcopy
|
||||
|
||||
from services.market_intel.mcp_fetch_candidate_queue_writer_preflight import TARGET_TABLE
|
||||
from services.market_intel.mcp_fetch_candidate_queue_writer_run_readiness import (
|
||||
ARTIFACT_PREFIX,
|
||||
)
|
||||
from services.market_intel.mcp_fetch_candidate_queue_writer_review_inventory import (
|
||||
build_mcp_fetch_candidate_queue_writer_review_inventory_preview,
|
||||
)
|
||||
|
||||
|
||||
_SAMPLE_WRITER_REVIEW_DECISION_PACKAGE = None
|
||||
|
||||
|
||||
def build_sample_writer_review_decision_package():
|
||||
global _SAMPLE_WRITER_REVIEW_DECISION_PACKAGE
|
||||
if _SAMPLE_WRITER_REVIEW_DECISION_PACKAGE is not None:
|
||||
return deepcopy(_SAMPLE_WRITER_REVIEW_DECISION_PACKAGE)
|
||||
|
||||
inventory_preview = (
|
||||
build_mcp_fetch_candidate_queue_writer_review_inventory_preview()
|
||||
)
|
||||
inventory_package = inventory_preview["sample_writer_review_inventory_package"]
|
||||
inventory_result = build_mcp_fetch_candidate_queue_writer_review_inventory_preview(
|
||||
writer_review_handoff_package=inventory_package[
|
||||
"writer_review_handoff_package"
|
||||
],
|
||||
writer_review_handoff_result=inventory_package[
|
||||
"writer_review_handoff_result"
|
||||
],
|
||||
operator_review_inventory=inventory_package["operator_review_inventory"],
|
||||
)
|
||||
inventory = inventory_result["operator_review_inventory_summary"]
|
||||
dedupe_keys = inventory["sample_dedupe_keys"]
|
||||
decision_rows = []
|
||||
for index, dedupe_key in enumerate(dedupe_keys):
|
||||
proposed_decision = "matched" if index == 0 else "low_score"
|
||||
decision_rows.append(
|
||||
{
|
||||
"dedupe_key": dedupe_key,
|
||||
"current_review_state": "needs_review",
|
||||
"proposed_review_decision": proposed_decision,
|
||||
"decision_bucket": "recoverable_low_score",
|
||||
"evidence_lane": "exact_identity",
|
||||
"evidence_ref": (
|
||||
ARTIFACT_PREFIX
|
||||
+ f"candidate-queue-review-decision-evidence-{index + 1}.json"
|
||||
),
|
||||
"decision_notes": (
|
||||
"same candidate lane checked; keep human audit trail"
|
||||
),
|
||||
"hard_veto_present": False,
|
||||
"stronger_existing_match_conflict": False,
|
||||
"false_positive_guard_passed": True,
|
||||
"variant_conflict_checked": True,
|
||||
"overwrite_protection_checked": True,
|
||||
"exact_identity_confirmed": proposed_decision == "matched",
|
||||
}
|
||||
)
|
||||
|
||||
operator_review_decision = {
|
||||
"review_decision_id": (
|
||||
"market-intel-candidate-writer-review-decision-sample"
|
||||
),
|
||||
"review_inventory_id": inventory["review_inventory_id"],
|
||||
"handoff_id": inventory["handoff_id"],
|
||||
"inventory_review_id": inventory["inventory_review_id"],
|
||||
"closeout_id": inventory["closeout_id"],
|
||||
"run_package_id": inventory["run_package_id"],
|
||||
"receipt_id": inventory["receipt_id"],
|
||||
"target_table": TARGET_TABLE,
|
||||
"decision_scope": "candidate_queue_review_decision",
|
||||
"decision_mode": "human_review_only",
|
||||
"expected_current_review_state": "needs_review",
|
||||
"decision_row_count": len(decision_rows),
|
||||
"expected_decision_row_count": inventory["dedupe_key_count"],
|
||||
"expected_dedupe_keys": dedupe_keys,
|
||||
"review_queue_artifact_path": inventory["review_queue_artifact_path"],
|
||||
"review_inventory_artifact_path": (
|
||||
inventory["review_inventory_artifact_path"]
|
||||
),
|
||||
"review_decision_artifact_path": (
|
||||
ARTIFACT_PREFIX
|
||||
+ "candidate-queue-review-decision-sample.json"
|
||||
),
|
||||
"decision_rows": decision_rows,
|
||||
"operator_confirmations": {
|
||||
"review_inventory_checked": True,
|
||||
"candidate_queue_review_only": True,
|
||||
"human_decision_only": True,
|
||||
"manual_record_required": True,
|
||||
"no_approval_token_payload": True,
|
||||
"no_api_cli_execution": True,
|
||||
"no_api_database_write": True,
|
||||
"no_api_inventory_query": True,
|
||||
"no_api_review_state_update": True,
|
||||
"no_api_queue_insert": True,
|
||||
"no_api_file_write": True,
|
||||
"no_scheduler_attach": True,
|
||||
"stronger_existing_match_guard_preserved": True,
|
||||
"false_positive_guard_completed": True,
|
||||
"variant_sensitive_review_completed": True,
|
||||
"hard_veto_respected": True,
|
||||
},
|
||||
"api_execution_allowed": False,
|
||||
"real_write_allowed_by_api": False,
|
||||
"api_candidate_review_allowed": False,
|
||||
"api_updates_review_state": False,
|
||||
"api_writes_match_result": False,
|
||||
}
|
||||
_SAMPLE_WRITER_REVIEW_DECISION_PACKAGE = {
|
||||
"writer_review_inventory_package": inventory_package,
|
||||
"writer_review_inventory_result": inventory_result,
|
||||
"operator_review_decision": operator_review_decision,
|
||||
}
|
||||
return deepcopy(_SAMPLE_WRITER_REVIEW_DECISION_PACKAGE)
|
||||
@@ -1,3 +1,3 @@
|
||||
"""市場情報 rollout phase 單一來源。"""
|
||||
|
||||
MARKET_INTEL_PHASE = "phase_136_market_intel_mcp_fetch_candidate_queue_writer_review_inventory"
|
||||
MARKET_INTEL_PHASE = "phase_137_market_intel_mcp_fetch_candidate_queue_writer_review_decision"
|
||||
|
||||
@@ -1072,6 +1072,32 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="market-intel-panel" data-market-intel-mcp-fetch-candidate-queue-writer-review-decision>
|
||||
<div class="market-intel-preview-head">
|
||||
<div>
|
||||
<p class="market-intel-muted momo-mono mb-1">MCP / REVIEW DECISION</p>
|
||||
<h2 class="market-intel-preview-title">MCP Candidate Queue Writer Review Decision</h2>
|
||||
</div>
|
||||
<button class="market-intel-icon-button" type="button" title="重新整理 MCP Writer Review Decision" data-market-intel-mcp-fetch-candidate-queue-writer-review-decision-refresh>
|
||||
<i class="fas fa-rotate-right" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="market-intel-preview-meta" data-market-intel-mcp-fetch-candidate-queue-writer-review-decision-meta>
|
||||
<span class="market-intel-pill">loading</span>
|
||||
</div>
|
||||
<div data-market-intel-mcp-fetch-candidate-queue-writer-review-decision-body>
|
||||
<div class="market-intel-empty">讀取 MCP Writer Review Decision 中...</div>
|
||||
</div>
|
||||
<div class="market-intel-control-row mt-3">
|
||||
<textarea class="market-intel-json-input" rows="9" spellcheck="false" data-market-intel-mcp-fetch-candidate-queue-writer-review-decision-input placeholder="writer review inventory and operator decision JSON"></textarea>
|
||||
<div class="market-intel-control-actions">
|
||||
<button class="market-intel-icon-button" type="button" title="審核 MCP Writer Review Decision JSON" data-market-intel-mcp-fetch-candidate-queue-writer-review-decision-review>
|
||||
<i class="fas fa-check" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="market-intel-panel" data-market-intel-manual-sample>
|
||||
<div class="market-intel-preview-head">
|
||||
<div>
|
||||
@@ -1596,6 +1622,7 @@
|
||||
const mcpFetchCandidateQueueWriterPostCloseoutInventoryReviewRoot = document.querySelector('[data-market-intel-mcp-fetch-candidate-queue-writer-post-closeout-inventory-review]');
|
||||
const mcpFetchCandidateQueueWriterReviewHandoffRoot = document.querySelector('[data-market-intel-mcp-fetch-candidate-queue-writer-review-handoff]');
|
||||
const mcpFetchCandidateQueueWriterReviewInventoryRoot = document.querySelector('[data-market-intel-mcp-fetch-candidate-queue-writer-review-inventory]');
|
||||
const mcpFetchCandidateQueueWriterReviewDecisionRoot = document.querySelector('[data-market-intel-mcp-fetch-candidate-queue-writer-review-decision]');
|
||||
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]');
|
||||
@@ -1612,7 +1639,7 @@
|
||||
const liveInventoryRoot = document.querySelector('[data-market-intel-live-inventory]');
|
||||
const approvalRoot = document.querySelector('[data-market-intel-approval]');
|
||||
const deployRoot = document.querySelector('[data-market-intel-deploy]');
|
||||
if (!root && !writerRoot && !cliRoot && !dbProbeRoot && !seedDiffRoot && !legacyBridgeRoot && !mcpReadinessRoot && !mcpPreflightRoot && !mcpActivationRoot && !mcpFetchGateRoot && !mcpCompletionRoot && !mcpActivationEvidenceRoot && !mcpRuntimeSmokeRoot && !mcpRuntimePromotionRoot && !mcpManualFetchHandoffRoot && !mcpFetchTargetReviewRoot && !mcpFetchRunPackageRoot && !mcpFetchRunReadinessRoot && !mcpFetchRunReceiptRoot && !mcpFetchResultParserReviewRoot && !mcpFetchCandidateHandoffReviewRoot && !mcpFetchCandidateQueueReviewRoot && !mcpFetchCandidateQueueWriterPreflightRoot && !mcpFetchCandidateQueueWriterCliReviewRoot && !mcpFetchCandidateQueueWriterRunPackageReviewRoot && !mcpFetchCandidateQueueWriterRunReadinessRoot && !mcpFetchCandidateQueueWriterRunReceiptReviewRoot && !mcpFetchCandidateQueueWriterRunCloseoutReviewRoot && !mcpFetchCandidateQueueWriterPostCloseoutInventoryReviewRoot && !mcpFetchCandidateQueueWriterReviewHandoffRoot && !mcpFetchCandidateQueueWriterReviewInventoryRoot && !manualSampleRoot && !sampleAcceptanceRoot && !sampleReviewRoot && !schedulerRoot && !matchReviewRoot && !opportunityRoot && !opportunityScoringRoot && !opportunityEvidenceRoot && !opportunityAlertRoot && !migrationRoot && !migrationDrillRoot && !catalogReviewRoot && !liveSmokeRoot && !liveInventoryRoot && !approvalRoot && !deployRoot) return;
|
||||
if (!root && !writerRoot && !cliRoot && !dbProbeRoot && !seedDiffRoot && !legacyBridgeRoot && !mcpReadinessRoot && !mcpPreflightRoot && !mcpActivationRoot && !mcpFetchGateRoot && !mcpCompletionRoot && !mcpActivationEvidenceRoot && !mcpRuntimeSmokeRoot && !mcpRuntimePromotionRoot && !mcpManualFetchHandoffRoot && !mcpFetchTargetReviewRoot && !mcpFetchRunPackageRoot && !mcpFetchRunReadinessRoot && !mcpFetchRunReceiptRoot && !mcpFetchResultParserReviewRoot && !mcpFetchCandidateHandoffReviewRoot && !mcpFetchCandidateQueueReviewRoot && !mcpFetchCandidateQueueWriterPreflightRoot && !mcpFetchCandidateQueueWriterCliReviewRoot && !mcpFetchCandidateQueueWriterRunPackageReviewRoot && !mcpFetchCandidateQueueWriterRunReadinessRoot && !mcpFetchCandidateQueueWriterRunReceiptReviewRoot && !mcpFetchCandidateQueueWriterRunCloseoutReviewRoot && !mcpFetchCandidateQueueWriterPostCloseoutInventoryReviewRoot && !mcpFetchCandidateQueueWriterReviewHandoffRoot && !mcpFetchCandidateQueueWriterReviewInventoryRoot && !mcpFetchCandidateQueueWriterReviewDecisionRoot && !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;
|
||||
@@ -1779,6 +1806,12 @@
|
||||
const mcpFetchCandidateQueueWriterReviewInventoryReview = mcpFetchCandidateQueueWriterReviewInventoryRoot ? mcpFetchCandidateQueueWriterReviewInventoryRoot.querySelector('[data-market-intel-mcp-fetch-candidate-queue-writer-review-inventory-review]') : null;
|
||||
const mcpFetchCandidateQueueWriterReviewInventoryRefresh = mcpFetchCandidateQueueWriterReviewInventoryRoot ? mcpFetchCandidateQueueWriterReviewInventoryRoot.querySelector('[data-market-intel-mcp-fetch-candidate-queue-writer-review-inventory-refresh]') : null;
|
||||
const mcpFetchCandidateQueueWriterReviewInventoryEndpoint = "{{ url_for('market_intel.market_intel_mcp_fetch_candidate_queue_writer_review_inventory') }}";
|
||||
const mcpFetchCandidateQueueWriterReviewDecisionMeta = mcpFetchCandidateQueueWriterReviewDecisionRoot ? mcpFetchCandidateQueueWriterReviewDecisionRoot.querySelector('[data-market-intel-mcp-fetch-candidate-queue-writer-review-decision-meta]') : null;
|
||||
const mcpFetchCandidateQueueWriterReviewDecisionBody = mcpFetchCandidateQueueWriterReviewDecisionRoot ? mcpFetchCandidateQueueWriterReviewDecisionRoot.querySelector('[data-market-intel-mcp-fetch-candidate-queue-writer-review-decision-body]') : null;
|
||||
const mcpFetchCandidateQueueWriterReviewDecisionInput = mcpFetchCandidateQueueWriterReviewDecisionRoot ? mcpFetchCandidateQueueWriterReviewDecisionRoot.querySelector('[data-market-intel-mcp-fetch-candidate-queue-writer-review-decision-input]') : null;
|
||||
const mcpFetchCandidateQueueWriterReviewDecisionReview = mcpFetchCandidateQueueWriterReviewDecisionRoot ? mcpFetchCandidateQueueWriterReviewDecisionRoot.querySelector('[data-market-intel-mcp-fetch-candidate-queue-writer-review-decision-review]') : null;
|
||||
const mcpFetchCandidateQueueWriterReviewDecisionRefresh = mcpFetchCandidateQueueWriterReviewDecisionRoot ? mcpFetchCandidateQueueWriterReviewDecisionRoot.querySelector('[data-market-intel-mcp-fetch-candidate-queue-writer-review-decision-refresh]') : null;
|
||||
const mcpFetchCandidateQueueWriterReviewDecisionEndpoint = "{{ url_for('market_intel.market_intel_mcp_fetch_candidate_queue_writer_review_decision') }}";
|
||||
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;
|
||||
@@ -5261,6 +5294,143 @@
|
||||
}
|
||||
};
|
||||
|
||||
const renderMcpFetchCandidateQueueWriterReviewDecisionMeta = data => {
|
||||
const decisionRows = data.decision_rows || data.operator_review_decision_summary?.decision_rows || [];
|
||||
mcpFetchCandidateQueueWriterReviewDecisionMeta.innerHTML = [
|
||||
`mode=${data.mode || 'unknown'}`,
|
||||
`accepted=${data.mcp_fetch_candidate_queue_writer_review_decision_accepted ? 'yes' : 'no'}`,
|
||||
`gates=${data.passed_gate_count || 0}/${data.gate_count || 0}`,
|
||||
`rows=${data.payload_row_count || data.operator_review_decision_summary?.decision_row_count || decisionRows.length || 0}`,
|
||||
`decision=${data.ready_for_candidate_queue_review_decision_approval ? 'ready' : 'blocked'}`,
|
||||
`match=${data.api_writes_match_result ? 'api' : 'manual'}`
|
||||
].map(item => `<span class="market-intel-pill">${escapeHtml(item)}</span>`).join('');
|
||||
};
|
||||
|
||||
const renderMcpFetchCandidateQueueWriterReviewDecisionBody = data => {
|
||||
const blockers = (data.blocked_reasons || []).join(' / ');
|
||||
const gates = data.gates || [];
|
||||
const inventory = data.writer_review_inventory_summary || {};
|
||||
const decision = data.operator_review_decision_summary || {};
|
||||
const rows = decision.decision_rows || data.decision_rows || [];
|
||||
const contract = data.decision_contract || {};
|
||||
const allowedDecisions = contract.allowed_next_states || [];
|
||||
const steps = data.next_operator_steps || [];
|
||||
const renderCheck = (key, label, status) => `
|
||||
<div class="market-intel-check">
|
||||
<div>
|
||||
<strong>${escapeHtml(key)}</strong>
|
||||
<small>${escapeHtml(label || '')}</small>
|
||||
</div>
|
||||
<span>${escapeHtml(status)}</span>
|
||||
</div>
|
||||
`;
|
||||
mcpFetchCandidateQueueWriterReviewDecisionBody.innerHTML = `
|
||||
<div class="market-intel-empty mb-3">此 review decision 只審核人工候選隊列決策摘要;API 不更新 review_state、不寫 match result、不補 queue row、不讀 approval token、不執行 CLI、不開 DB、不掛 scheduler。${blockers ? `阻擋:${escapeHtml(blockers)}` : ''}</div>
|
||||
<div class="market-intel-deploy-grid">
|
||||
<div data-market-intel-mcp-fetch-candidate-queue-writer-review-decision-gates>
|
||||
<p class="market-intel-deploy-section-title">DECISION GATES</p>
|
||||
<div class="market-intel-check-list">${
|
||||
gates.length
|
||||
? gates.map(item => renderCheck(item.key, item.label, item.passed ? 'PASS' : 'BLOCK')).join('')
|
||||
: '<div class="market-intel-empty">尚未提供 decision gates。</div>'
|
||||
}</div>
|
||||
</div>
|
||||
<div data-market-intel-mcp-fetch-candidate-queue-writer-review-decision-inventory>
|
||||
<p class="market-intel-deploy-section-title">INVENTORY LINK</p>
|
||||
<div class="market-intel-check-list">
|
||||
${renderCheck('inventory', `${inventory.mode || 'missing'} / ${inventory.accepted ? 'accepted' : 'pending'}`, inventory.accepted ? 'ACCEPTED' : 'BLOCK')}
|
||||
${renderCheck('review_inventory_id', inventory.review_inventory_id || 'missing', inventory.review_inventory_id ? 'LINKED' : 'BLOCK')}
|
||||
${renderCheck('handoff_id', inventory.handoff_id || 'missing', inventory.handoff_id ? 'LINKED' : 'BLOCK')}
|
||||
${renderCheck('target_table', inventory.target_table || 'missing', inventory.target_table === 'market_alert_review_queue' ? 'SAFE' : 'BLOCK')}
|
||||
${renderCheck('rows', `${inventory.found_row_count || 0}/${inventory.dedupe_key_count || 0}`, inventory.found_row_count === inventory.dedupe_key_count && inventory.found_row_count > 0 ? 'MATCH' : 'BLOCK')}
|
||||
${renderCheck('api_boundary', 'no DB / no queue write / no review_state update / no scheduler', inventory.side_effects_clear ? 'CLOSED' : 'BLOCK')}
|
||||
</div>
|
||||
</div>
|
||||
<div data-market-intel-mcp-fetch-candidate-queue-writer-review-decision-decision>
|
||||
<p class="market-intel-deploy-section-title">DECISION SUMMARY</p>
|
||||
<div class="market-intel-check-list">
|
||||
${renderCheck('review_decision_id', decision.review_decision_id || 'missing', decision.review_decision_id ? 'RECORDED' : 'BLOCK')}
|
||||
${renderCheck('scope', `${decision.decision_scope || 'missing'} / ${decision.decision_mode || 'missing'}`, decision.decision_scope === 'candidate_queue_review_decision' && decision.decision_mode === 'human_review_only' ? 'SAFE' : 'BLOCK')}
|
||||
${renderCheck('current_state', decision.expected_current_review_state || 'missing', decision.expected_current_review_state === 'needs_review' ? 'LOCKED' : 'BLOCK')}
|
||||
${renderCheck('artifacts', `${decision.review_decision_artifact_path || 'missing'}`, decision.review_decision_artifact_path_safe ? 'SAFE' : 'BLOCK')}
|
||||
</div>
|
||||
</div>
|
||||
<div data-market-intel-mcp-fetch-candidate-queue-writer-review-decision-rows>
|
||||
<p class="market-intel-deploy-section-title">DECISION ROWS</p>
|
||||
<div class="market-intel-check-list">${
|
||||
rows.length
|
||||
? rows.map((row, index) => renderCheck(
|
||||
row.dedupe_key || `row_${index + 1}`,
|
||||
`${row.current_review_state || 'missing'} -> ${row.proposed_review_decision || 'missing'} / ${row.evidence_lane || 'missing'}`,
|
||||
allowedDecisions.includes(row.proposed_review_decision) && row.current_review_state === 'needs_review' && row.evidence_ref_safe && row.decision_notes_present ? 'READY' : 'BLOCK'
|
||||
)).join('')
|
||||
: '<div class="market-intel-empty">尚未提供 decision rows。</div>'
|
||||
}</div>
|
||||
</div>
|
||||
<div data-market-intel-mcp-fetch-candidate-queue-writer-review-decision-next>
|
||||
<p class="market-intel-deploy-section-title">BOUNDARY / NEXT</p>
|
||||
<div class="market-intel-check-list">
|
||||
${renderCheck('allowed_decisions', allowedDecisions.join(' / ') || 'missing', allowedDecisions.length ? 'LOCKED' : 'BLOCK')}
|
||||
${renderCheck('manual_only', `${contract.next_gate || 'candidate_queue_review_decision_approval'} / ${contract.decision_scope || 'missing'}`, contract.manual_decision_required ? 'CONFIRMED' : 'BLOCK')}
|
||||
${renderCheck('operator_boundaries', 'manual decision / no API DB write / no review_state update / no match write', decision.review_inventory_checked && decision.human_decision_only && decision.no_api_cli_execution && decision.no_api_database_write && decision.no_api_inventory_query && decision.no_api_review_state_update && decision.no_api_queue_insert && decision.no_api_file_write && decision.no_scheduler_attach ? 'CONFIRMED' : 'BLOCK')}
|
||||
${renderCheck('identity_guards', 'stronger existing match / false positive / variant / hard veto', decision.stronger_existing_match_guard_preserved && decision.false_positive_guard_completed && decision.variant_sensitive_review_completed && decision.hard_veto_respected ? 'CONFIRMED' : 'BLOCK')}
|
||||
${renderCheck('api_side_effects', 'no decision record write / no match write / no scheduler', data.api_writes_decision_record || data.api_writes_match_result || data.decision_record_written || data.candidate_review_state_updated || data.scheduler_attached ? 'BLOCK' : 'CLOSED')}
|
||||
${steps.map((item, index) => renderCheck(`step_${index + 1}`, item, 'NEXT')).join('')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
if (mcpFetchCandidateQueueWriterReviewDecisionInput && !mcpFetchCandidateQueueWriterReviewDecisionInput.value.trim() && data.sample_writer_review_decision_package) {
|
||||
mcpFetchCandidateQueueWriterReviewDecisionInput.value = JSON.stringify(data.sample_writer_review_decision_package, null, 2);
|
||||
}
|
||||
};
|
||||
|
||||
const loadMcpFetchCandidateQueueWriterReviewDecision = async () => {
|
||||
if (!mcpFetchCandidateQueueWriterReviewDecisionMeta || !mcpFetchCandidateQueueWriterReviewDecisionBody) return;
|
||||
mcpFetchCandidateQueueWriterReviewDecisionBody.innerHTML = '<div class="market-intel-empty">讀取 MCP Writer Review Decision 中...</div>';
|
||||
try {
|
||||
const response = await fetch(mcpFetchCandidateQueueWriterReviewDecisionEndpoint, { credentials: 'same-origin' });
|
||||
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
||||
const data = await response.json();
|
||||
renderMcpFetchCandidateQueueWriterReviewDecisionMeta(data);
|
||||
renderMcpFetchCandidateQueueWriterReviewDecisionBody(data);
|
||||
} catch (error) {
|
||||
mcpFetchCandidateQueueWriterReviewDecisionMeta.innerHTML = '<span class="market-intel-pill">error</span>';
|
||||
mcpFetchCandidateQueueWriterReviewDecisionBody.innerHTML = `<div class="market-intel-empty">MCP Writer Review Decision 讀取失敗:${escapeHtml(error.message)}</div>`;
|
||||
}
|
||||
};
|
||||
|
||||
const reviewMcpFetchCandidateQueueWriterReviewDecision = async () => {
|
||||
if (!mcpFetchCandidateQueueWriterReviewDecisionMeta || !mcpFetchCandidateQueueWriterReviewDecisionBody || !mcpFetchCandidateQueueWriterReviewDecisionInput) return;
|
||||
let parsed;
|
||||
try {
|
||||
parsed = JSON.parse(mcpFetchCandidateQueueWriterReviewDecisionInput.value || '{}');
|
||||
} catch (error) {
|
||||
mcpFetchCandidateQueueWriterReviewDecisionMeta.innerHTML = '<span class="market-intel-pill">json_error</span>';
|
||||
mcpFetchCandidateQueueWriterReviewDecisionBody.innerHTML = `<div class="market-intel-empty">JSON 格式錯誤:${escapeHtml(error.message)}</div>`;
|
||||
return;
|
||||
}
|
||||
mcpFetchCandidateQueueWriterReviewDecisionBody.innerHTML = '<div class="market-intel-empty">審核 MCP Writer Review Decision 中...</div>';
|
||||
try {
|
||||
const response = await fetch(mcpFetchCandidateQueueWriterReviewDecisionEndpoint, {
|
||||
method: 'POST',
|
||||
credentials: 'same-origin',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRFToken': csrfToken
|
||||
},
|
||||
body: JSON.stringify({ writer_review_decision_package: parsed })
|
||||
});
|
||||
const data = await response.json();
|
||||
if (!response.ok && !data.mode) throw new Error(`HTTP ${response.status}`);
|
||||
renderMcpFetchCandidateQueueWriterReviewDecisionMeta(data);
|
||||
renderMcpFetchCandidateQueueWriterReviewDecisionBody(data);
|
||||
} catch (error) {
|
||||
mcpFetchCandidateQueueWriterReviewDecisionMeta.innerHTML = '<span class="market-intel-pill">error</span>';
|
||||
mcpFetchCandidateQueueWriterReviewDecisionBody.innerHTML = `<div class="market-intel-empty">MCP Writer Review Decision 審核失敗:${escapeHtml(error.message)}</div>`;
|
||||
}
|
||||
};
|
||||
|
||||
const renderManualSampleMeta = data => {
|
||||
manualSampleMeta.innerHTML = [
|
||||
`mode=${data.mode || 'unknown'}`,
|
||||
@@ -14784,6 +14954,12 @@
|
||||
if (mcpFetchCandidateQueueWriterReviewInventoryReview) {
|
||||
mcpFetchCandidateQueueWriterReviewInventoryReview.addEventListener('click', reviewMcpFetchCandidateQueueWriterReviewInventory);
|
||||
}
|
||||
if (mcpFetchCandidateQueueWriterReviewDecisionRefresh) {
|
||||
mcpFetchCandidateQueueWriterReviewDecisionRefresh.addEventListener('click', loadMcpFetchCandidateQueueWriterReviewDecision);
|
||||
}
|
||||
if (mcpFetchCandidateQueueWriterReviewDecisionReview) {
|
||||
mcpFetchCandidateQueueWriterReviewDecisionReview.addEventListener('click', reviewMcpFetchCandidateQueueWriterReviewDecision);
|
||||
}
|
||||
if (manualSampleRefresh) {
|
||||
manualSampleRefresh.addEventListener('click', loadManualSample);
|
||||
}
|
||||
@@ -15058,6 +15234,7 @@
|
||||
loadMcpFetchCandidateQueueWriterPostCloseoutInventoryReview();
|
||||
loadMcpFetchCandidateQueueWriterReviewHandoff();
|
||||
loadMcpFetchCandidateQueueWriterReviewInventory();
|
||||
loadMcpFetchCandidateQueueWriterReviewDecision();
|
||||
loadManualSample();
|
||||
loadSampleAcceptance();
|
||||
loadSampleReview();
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user