V10.505 新增市場情報 writer review decision gate

This commit is contained in:
OoO
2026-05-31 18:19:28 +08:00
parent 58ac16781c
commit b347aa44b9
14 changed files with 1703 additions and 228 deletions

View File

@@ -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 actionsAPI 不讀 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 actionsAPI 不讀 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 confirmationAPI 不讀 token、不執行 CLI、不開 DB、不寫 queue、不更新 review_state、不掛 scheduler。
- V10.502 修正 AiderHeal 自動修復診斷鏈:先做 ADR-020 檔案白名單再打 110 preflight`tests/` finding 會明確略過而不誤報 repo preflightCode Review 完成通知會把全數不在白名單的 finding 標成需人工處理,不再宣稱已觸發 AiderHeal白名單放行 `services/routes/database` 子目錄 Python 檔preflight 通知帶 stderr/stdout 細節,健康檢查同時接受 `/health` 回 `ok` 與 `healthy`。

View File

@@ -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 # 用於模板顯示

View File

@@ -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 confirmationsAPI/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 confirmationsAPI/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 confirmationsAPI/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 actionsAPI/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 才能升級。

View File

@@ -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不變更模組化決策。

View File

@@ -100,6 +100,7 @@
- 2026-05-31 起,`V10.502` 修正 AiderHeal 自動修復診斷鏈:先檢查 ADR-020 檔案白名單再執行 110 preflight`tests/` finding 會明確略過而不誤報 repo preflightCode 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 決策信封整合

View File

@@ -13,6 +13,7 @@
## 📅 詳細更新日誌 (考古存檔)
### 2026-05-24PChome 近門檻身份回收第二輪
- **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`

View File

@@ -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,
)
)

View File

@@ -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,

View File

@@ -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,
}

View File

@@ -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"],
},
]

View File

@@ -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)

View File

@@ -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"

View File

@@ -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