diff --git a/.env.example b/.env.example
index 151edbf..ac91b08 100644
--- a/.env.example
+++ b/.env.example
@@ -46,6 +46,10 @@ EMAIL_RECEIVER=receiver_email@gmail.com
# ==========================================
PUBLIC_URL=http://your_server_ip:port
NGROK_AUTH_TOKEN=your_ngrok_auth_token
+FLASK_ENV=production
+KUBERNETES_SERVICE_HOST=
+CICD_PROD_BASE_URL=
+CICD_UAT_BASE_URL=
# ==========================================
# Webcrumbs 共用 UI Runtime
@@ -80,6 +84,9 @@ FRONTEND_VERSION=stable
MARKET_INTEL_ENABLED=false
MARKET_INTEL_CRAWLER_ENABLED=false
MARKET_INTEL_WRITE_ENABLED=false
+MCP_FAST_STATIC_FALLBACK=false
+MCP_OLLAMA_TIMEOUT=30
+MCP_OLLAMA_NUM_PREDICT=512
# ==========================================
# 通訊模組設定(從環境變數讀取)
@@ -156,6 +163,33 @@ REDIS_PORT=6379
# Token 檔案位置:config/google_token.pickle(首次認證後自動產生)
GDRIVE_FOLDER_PATH=業績報表/當日業績
GDRIVE_FILE_PATTERN=即時業績_當日
+GOOGLE_DRIVE_CREDENTIALS_FILE=config/google_credentials.json
+GOOGLE_DRIVE_TOKEN_FILE=config/google_token.json
+GOOGLE_DRIVE_LEGACY_PICKLE_FILE=config/google_token.pickle
+
+# ==========================================
+# 匯入 / PChome / 外部商品資料控制
+# ==========================================
+DAILY_SALES_IMPORT_STALENESS_GUARD_ENABLED=true
+DAILY_SALES_IMPORT_MAX_STALE_DAYS=3
+EXTERNAL_OFFER_SYNC_LIMIT=200
+PCHOME_PUBLIC_API_TIMEOUT_SECONDS=8
+PCHOME_STALE_RECOVERY_ENABLED=false
+PCHOME_SALES_JOIN_BY_MOMO_SKU_ENABLED=false
+PCHOME_GROWTH_MOMO_BACKFILL_LIMIT=20
+PCHOME_BACKFILL_EXPIRED_REFRESH_LIMIT=20
+PCHOME_BACKFILL_RETRYABLE_LIMIT=20
+PCHOME_BACKFILL_REVALIDATION_LIMIT=20
+PCHOME_BACKFILL_UNMATCHED_LIMIT=20
+PCHOME_FEEDER_TTL_HOURS=24
+PCHOME_FEEDER_BACKFILL_TIMEOUT=8
+PCHOME_FEEDER_BACKFILL_SEARCH_MAX_PAGES=1
+PCHOME_FEEDER_BACKFILL_MAX_SEARCH_TERMS=3
+PCHOME_FEEDER_BACKFILL_MAX_SECONDS_PER_SKU=15
+MOMO_TARGETED_SEARCH_LIMIT_PER_TERM=3
+MOMO_TARGETED_SEARCH_MAX_PRODUCTS=10
+MOMO_TARGETED_SEARCH_MAX_TERMS=3
+MOMO_TARGETED_SEARCH_MIN_SCORE=0.5
# ==========================================
# Hermes 3 競價情報分析(Module 2 / ADR-012)
@@ -184,6 +218,8 @@ OLLAMA_EMBED_GCP_FAILURE_NOTICE_SEC=30
# [預設 true] 背景 embedding 會讀 host_health_probes,跳過最近 runtime unhealthy 的 GCP 節點
OLLAMA_EMBED_HOST_HEALTH_SKIP_ENABLED=true
OLLAMA_EMBED_HOST_HEALTH_SKIP_WINDOW_MINUTES=20
+OLLAMA_SECONDARY_MODEL_FALLBACK=gemma3:4b
+OLLAMA_SECONDARY_MODEL_FALLBACK_PATTERNS=
# 111 Mac final fallback guardrail and allowlist proxy
OLLAMA_111_CIRCUIT_BREAKER_ENABLED=true
@@ -236,6 +272,7 @@ ELEPHANT_ALPHA_HERMES_MODEL=hermes3:latest
ELEPHANT_ALPHA_NEMOTRON_NIM_ENDPOINT=https://integrate.api.nvidia.com/v1
ELEPHANT_ALPHA_URL=https://integrate.api.nvidia.com/v1/chat/completions
ELEPHANT_ALPHA_OPENCLAW_GEMINI_ENDPOINT=https://generativelanguage.googleapis.com/v1beta
+PPT_SCHEDULED_FAST_FALLBACK=false
# ── Google Gemini API(僅備援 / 鎖定場景)────────────────────────────────────
# Gemini 只能作為 Ollama 失敗備援或 ADR-028 鎖定場景,不可設為通用預設 provider
diff --git a/docs/memory/README.md b/docs/memory/README.md
index 9d9a54f..a9943c0 100644
--- a/docs/memory/README.md
+++ b/docs/memory/README.md
@@ -25,6 +25,8 @@
| `schema_inventory_baseline.md` | DB 表分類與 drift 基線 | 要收斂 migration / ORM / raw SQL 真相時 |
| `frontend_v3_handoff_20260512.md` | 前端 V3 守門落地、正式部署、UI/UX 響應式缺口與接續順序 | 新 session 接續前端 V3 全站 UI/UX、手機版確認、`/daily_sales` 或 `/edm` 修正與部署時 |
| `claude_inventory_validation_20260513.md` | Claude Code V1/V2 盤點逐項驗證、已修項與不可盲動清單 | 接續盤點整改、判斷某項是否仍是阻擋、避免重複清已修項時 |
+| `production_version_truth_20260627.md` | 正式環境 `/health` 作為最新版本真相,避免 local/Gitea/iCloud drift 誤判 | 開工、部署前、handoff 或任何版本宣稱前 |
+| `pchome_mapping_backlog_20260628.md` | PChome growth API 的商品對應缺口只讀盤點、mapping rate 與下一步順序 | 接續 PChome 商品對應率、正式 API readback、mapping backlog 優化時 |
## 關聯 Guide
diff --git a/docs/memory/code_modularization_inventory_20260430.md b/docs/memory/code_modularization_inventory_20260430.md
index ce423cd..a9a7900 100644
--- a/docs/memory/code_modularization_inventory_20260430.md
+++ b/docs/memory/code_modularization_inventory_20260430.md
@@ -7,7 +7,7 @@
- Python 總量:約 139,476 行(排除 `venv/`、`backups/`、`__pycache__/`、`.claude/worktrees/`)。
- 最大壓力區:`services/` 約 84,159 行、`routes/` 約 36,245 行。
- `app.py` 目前約 1,232 行,功能定位應固定為 bootstrap / Blueprint registration / startup guard,不再承接新 route。
-- 目前工作樹仍有 33 個 Python 檔案達到或超過 800 行;這些不是禁止修 bug,而是禁止繼續塞新功能。
+- 目前工作樹仍有 42 個 Python 檔案達到或超過 800 行;這些不是禁止修 bug,而是禁止繼續塞新功能。
- 2026-05-05 追記:Phase 38→56 觀測台戰役讓 `routes/admin_observability_routes.py` 與 `run_scheduler.py` 進入大檔治理清單;後續觀測台功能應先抽 query/action service,不再把新 SQL 與 L2 mutation 直接塞回 route。
- 2026-05-06 追記:跨平台市場情報模組啟動前,必須先把新增爬蟲、排程、DB schema、UI route 全部隔離在 `market_*` / `services/market_intel/` / `routes/market_intel_routes.py`,不可塞回既有大檔。
- 2026-05-18 追記:Phase 42 市場情報只在 `app.py` 的 `EXPECTED_METADATA_TABLES` 補上 `market_alert_review_queue` 名稱,未新增 route / bootstrap 邏輯;後續仍應把 metadata verification 抽到 app factory 或 startup guard module,避免 `app.py` 繼續承接功能。
@@ -71,46 +71,56 @@
- 2026-05-24 追記:同步 PChome review queue 決策信封合併後的 `services/competitor_intel_repository.py` 行數;此處只更新 inventory,不變更拆分策略。
- 2026-05-25 追記:同步背景 embedding 讀取 `host_health_probes` skip guard 後的 `services/ollama_service.py` 行數;此處只更新 inventory,不變更 Ollama 路由決策。
- 2026-05-29 追記:同步 PChome near-threshold / focused identity 回收系列後的 `services/marketplace_product_matcher.py` 行數;此處只更新 inventory,不變更拆分策略。
+- 2026-06-29 追記:同步 AI 自動化例外決策、PChome mapping backlog 與 market intel alias work 後的 >800 行檔案清單;此處只更新 inventory,不變更拆分策略。
+- 2026-06-29 追記:同步 market intel AI-controlled canonical route aliases 後的 route extension 行數;`routes/market_intel_mcp_run_routes.py` 與 `routes/market_intel_review_post_routes.py` 進入 800 行治理清單,下一步新增 route alias 應優先抽 route registration helper,避免 extension 檔繼續膨脹。
+- 2026-06-29 追記:同步 market intel review/report endpoint compatibility migration 後的 route extension 行數;`routes/market_intel_review_routes.py` 進入 800 行治理清單,下一步應抽 `market_intel_review_route_registration.py` 或 declarative endpoint map,避免 decorator 重複擴張。
## 達到或超過 800 行檔案清單
| 行數 | 檔案 | 分類 | 拆分方向 |
|---:|---|---|---|
-| 9225 | `routes/openclaw_bot_routes.py` | P0 巨型 Blueprint | route / bot command service / report service / scheduler hook;禁止再新增市場情報入口 |
-| 5499 | `services/ppt_generator.py` | P0 報表生成巨型 service | deck orchestration / slide builders / chart builders / report type registry |
-| 3186 | `routes/sales_routes.py` | P0 巨型 Blueprint | page routes / API routes / chart query service / calendar service;分析頁新增功能先抽 `services/sales/` |
-| 2973 | `scheduler.py` | P0 排程總管 | task registry / crawler jobs / report jobs / notification jobs;市場情報只能透過獨立 job module 掛入 |
-| 2731 | `services/openclaw_strategist_service.py` | P0 OpenClaw service | prompt builders / report composer / strategy rules |
-| 3681 | `routes/admin_observability_routes.py` | P0 觀測台巨型 Blueprint | `services/observability_query_service.py` / `services/observability_action_service.py` / route glue |
-| 1796 | `routes/ai_routes.py` | P1 AI Blueprint | route glue / AI orchestration service / prompt builders |
-| 2154 | `services/nemoton_dispatcher_service.py` | P1 NemoTron service | NIM client / tool-call parser / action dispatcher |
-| 2535 | `routes/dashboard_routes.py` | P1 Dashboard Blueprint | competitor decision overview / dashboard query service;首頁資料整併需抽 service |
-| 1485 | `routes/vendor_routes.py` | P1 Vendor Blueprint | route glue / stockout mutation/email;V2 page query、stockout list/batches API query、vendor list/detail query 已抽到 `services/vendor_stockout_query_service.py` |
-| 1390 | `services/telegram_bot_service.py` | P1 Telegram service | command handlers / message formatters / bot client |
-| 1237 | `app.py` | P1 bootstrap | 保持只做 app setup;繼續往 app_factory / extension setup 抽;Phase 42 只做 metadata table name 對齊 |
-| 2149 | `services/elephant_alpha_autonomous_engine.py` | P1 ElephantAlpha engine | HITL / executor / planning policy |
-| 970 | `routes/cicd_routes.py` | P2 CI/CD Blueprint | route glue / CI query service / deployment action service |
-| 1250 | `run_scheduler.py` | P2 scheduler entrypoint | observability jobs / token report jobs / task registration 分離 |
-| 916 | `services/ppt_auto_generation_service.py` | P2 PPT 自動產線 service | schedule resolver / generation queue / missing report planner |
+| 9424 | `routes/openclaw_bot_routes.py` | P0 巨型 Blueprint | route / bot command service / report service / scheduler hook;禁止再新增市場情報入口 |
+| 8689 | `services/pchome_mapping_backlog_service.py` | P0 PChome mapping backlog service | mapping backlog / evidence receipt / controlled apply package 分拆到 `services/pchome_mapping/` |
+| 5865 | `services/marketplace_product_matcher.py` | P2 marketplace matcher | identity parsing / unit-comparable scoring / search term quality / persistence normalization |
+| 5513 | `services/ppt_generator.py` | P0 報表生成巨型 service | deck orchestration / slide builders / chart builders / report type registry |
+| 4398 | `routes/ai_routes.py` | P1 AI Blueprint | route glue / AI orchestration service / prompt builders |
+| 4152 | `routes/admin_observability_routes.py` | P0 觀測台巨型 Blueprint | `services/observability_query_service.py` / `services/observability_action_service.py` / route glue |
+| 3763 | `services/competitor_price_feeder.py` | P2 competitor price feeder | crawler scheduling / price normalization / retryable candidate recovery / cache strategy |
+| 3290 | `routes/sales_routes.py` | P0 巨型 Blueprint | page routes / API routes / chart query service / calendar service;分析頁新增功能先抽 `services/sales/` |
+| 3125 | `routes/dashboard_routes.py` | P1 Dashboard Blueprint | competitor decision overview / dashboard query service;首頁資料整併需抽 service |
+| 3107 | `scheduler.py` | P0 排程總管 | task registry / crawler jobs / report jobs / notification jobs;市場情報只能透過獨立 job module 掛入 |
+| 2961 | `services/openclaw_strategist_service.py` | P0 OpenClaw service | prompt builders / report composer / strategy rules |
+| 2383 | `services/competitor_intel_repository.py` | P2 competitor intel repository | review queue query / cache shaping / formatting helpers |
+| 2332 | `services/elephant_alpha_autonomous_engine.py` | P1 ElephantAlpha engine | AI exception policy / executor / planning policy |
+| 2166 | `services/nemoton_dispatcher_service.py` | P1 NemoTron service | NIM client / tool-call parser / action dispatcher |
+| 2017 | `services/market_intel/deployment_readiness.py` | P2 market intel deployment readiness | preflight gates / readiness payload / route contract helpers |
+| 1956 | `services/external_market_offer_service.py` | P2 external offer service | offer ingestion / freshness / quality normalization 分離 |
+| 1658 | `routes/market_intel_review_report_routes.py` | P2 market intel review report Blueprint | review report route glue / export payload / phase handoff orchestration |
+| 1494 | `routes/vendor_routes.py` | P1 Vendor Blueprint | route glue / stockout mutation/email;V2 page query、stockout list/batches API query、vendor list/detail query 已抽到 `services/vendor_stockout_query_service.py` |
+| 1456 | `services/ollama_service.py` | P2 Ollama client | host health / request client / fallback policy / response parsing |
+| 1417 | `services/telegram_templates.py` | P2 Telegram templates | alert template groups / channel-specific formatting / reusable render helpers |
+| 1394 | `services/telegram_bot_service.py` | P1 Telegram service | command handlers / message formatters / bot client |
+| 1385 | `services/import_service.py` | P2 import service | validators / import writers / report builders |
+| 1306 | `run_scheduler.py` | P2 scheduler entrypoint | observability jobs / token report jobs / task registration 分離 |
+| 1271 | `services/code_review_pipeline_service.py` | P2 Code review pipeline service | scan orchestration / finding normalization / persistence adapter |
+| 1255 | `app.py` | P1 bootstrap | 保持只做 app setup;繼續往 app_factory / extension setup 抽;Phase 42 只做 metadata table name 對齊 |
+| 1188 | `services/ppt_auto_generation_service.py` | P2 PPT 自動產線 service | schedule resolver / generation queue / missing report planner |
+| 1023 | `routes/cicd_routes.py` | P2 CI/CD Blueprint | route glue / CI query service / deployment action service |
+| 1014 | `routes/daily_sales_routes.py` | P2 Daily Sales Blueprint | route glue / export helpers / daily query and formatting service |
+| 1013 | `routes/export_routes.py` | P2 Export flow | export command/router glue / file path / download orchestration |
| 966 | `services/trend_crawler.py` | P2 crawler service | source adapters / parser / persistence |
| 942 | `services/learning_pipeline.py` | P2 RAG learning pipeline | distiller / promotion gate / persistence / telemetry |
-| 940 | `services/import_service.py` | P2 import service | validators / import writers / report builders |
-| 1071 | `services/telegram_templates.py` | P2 Telegram templates | alert template groups / channel-specific formatting / reusable render helpers |
+| 941 | `services/pchome_revenue_growth_service.py` | P2 PChome revenue growth service | growth aggregation / mapping backlog handoff / source adapter 分離 |
+| 931 | `services/pchome_crawler.py` | P2 PChome crawler | search fetch / parsing / fallback source handling / rate limit policy |
+| 920 | `routes/market_intel_routes.py` | P2 market intel Blueprint | page route / API route glue / MCP gate route registration helper |
+| 906 | `services/competitor_match_attempt_rescore_audit.py` | P2 rescore audit service | audit query / matcher replay / summary payload 分離 |
+| 895 | `routes/market_intel_review_routes.py` | P2 market intel review route extension | review decision route glue / canonical alias decorators / legacy endpoint compatibility |
| 867 | `services/token_report_service.py` | P2 token report service | query / aggregation / chart payload / notification formatting |
-| 4865 | `services/marketplace_product_matcher.py` | P2 marketplace matcher | identity parsing / unit-comparable scoring / search term quality / persistence normalization |
-| 865 | `routes/daily_sales_routes.py` | P2 Daily Sales Blueprint | route glue / export helpers / daily query and formatting service |
-| 1266 | `services/ollama_service.py` | P2 Ollama client | host health / request client / fallback policy / response parsing |
-| 849 | `services/pchome_crawler.py` | P2 PChome crawler | search fetch / parsing / fallback source handling / rate limit policy |
-| 1100 | `services/code_review_pipeline_service.py` | P2 Code review pipeline service | scan orchestration / finding normalization / persistence adapter |
-| 953 | `routes/export_routes.py` | P2 Export flow | export command/router glue / file path / download orchestration |
-| 816 | `services/ppt_vision_service.py` | P2 PPT vision QA service | runtime state / queue status / model probe / audit execution 分離 |
-| 2149 | `services/competitor_price_feeder.py` | P2 competitor price feeder | crawler scheduling / price normalization / retryable candidate recovery / cache strategy |
-| 1535 | `services/competitor_intel_repository.py` | P2 competitor intel repository | review queue query / cache shaping / formatting helpers |
-| 805 | `routes/bot_api_routes.py` | P2 Bot API Blueprint | route glue / bot action service |
-| 1319 | `routes/market_intel_review_report_routes.py` | P2 market intel review report Blueprint | review report route glue / export payload / phase handoff orchestration |
-| 917 | `routes/market_intel_routes.py` | P2 market intel Blueprint | page route / API route glue / MCP gate route registration helper |
-| 1965 | `services/market_intel/deployment_readiness.py` | P2 market intel deployment readiness | preflight gates / readiness payload / route contract helpers |
+| 864 | `services/ppt_vision_service.py` | P2 PPT vision QA service | runtime state / queue status / model probe / audit execution 分離 |
| 846 | `services/market_intel/candidate_queue_review_ai_summary_persistence_telegram_dispatch_report_catalog_record_run_receipt.py` | P2 market intel review receipt pipeline | AI summary / persistence / Telegram dispatch / report catalog run receipt orchestration |
+| 826 | `routes/market_intel_mcp_run_routes.py` | P2 market intel MCP run route extension | route glue / canonical alias decorators / MCP run preview handoff |
+| 861 | `routes/market_intel_review_post_routes.py` | P2 market intel review post route extension | review post route glue / canonical alias decorators / AI summary persistence preview handoff |
+| 813 | `services/momo_crawler.py` | P2 MOMO crawler | targeted search / browser fallback / parsing 分離 |
## 市場情報開發前置禁區
diff --git a/docs/memory/pchome_mapping_backlog_20260628.md b/docs/memory/pchome_mapping_backlog_20260628.md
new file mode 100644
index 0000000..d5e04b6
--- /dev/null
+++ b/docs/memory/pchome_mapping_backlog_20260628.md
@@ -0,0 +1,117 @@
+# PChome Mapping Backlog Read-Only Report (2026-06-28)
+
+正式環境目前以 `/api/ai/pchome-growth/opportunities` 作為 PChome 業績成長作戰清單來源。接續商品對應率優化時,先用只讀 reporter 取得 mapping backlog,不直接碰正式 DB。
+
+## 產品優先順序
+
+1. 先守住正式版本真相:production `/health` 必須是最新版本來源,本機只當 source candidate。
+2. AI 自動化營收閉環優先:正式 `needs_mapping_count=16` 先處理 direct mapping,再處理 review candidate,不退回人工逐筆找商品。
+3. AI Agent 決策必須 machine-verifiable:每個候選、證據補齊、單位價判斷、待確認項都要有 confidence、guardrails、trace、資料品質與可回滾下一步。
+4. 主流專業產品網站化必須同步推進:首頁 / Dashboard 要直接呈現缺口、價值、下一步與可執行 package,不得只顯示工程 API 或 raw endpoint。
+5. 外部來源治理要跟上:MOMO 是已接入來源,其他主流平台至少要有來源契約或待接入狀態;所有資料要正規化成 offer evidence。
+6. 再做小批次 no-write operator package / persistence dry-run / verifier,確認可回滾、可重跑、可讀回。
+7. 最後才推 DB apply authorization / signing / closeout 鏈;這些是底層 apply 安全工程,不得搶在產品缺口、AI 自動化與專業網站化前面。
+
+## 技術入口順序(按需使用,不代表產品優先順序)
+
+1. 先跑 `python scripts/ops/check_production_version_truth.py`,確認 production `/health` 仍是版本真相。
+2. 再跑 `python scripts/ops/report_pchome_mapping_backlog.py --json`。
+3. 或使用登入後只讀 API `/api/ai/pchome-growth/mapping-backlog` 讀同一套 service summary。
+4. 需要小批次操作前,先讀只讀 API `/api/ai/pchome-growth/mapping-backlog/operator-preview`。
+5. 需要 P1 direct mapping 自動搜尋候選包時,先讀 `/api/ai/pchome-growth/mapping-backlog/direct-mapping-auto-search-package`;預設只產生搜尋詞、身份錨點與接受門檻,不搜尋、不寫 DB。
+6. 需要 P2 候選機器決策包時,讀 `/api/ai/pchome-growth/mapping-backlog/direct-mapping-candidate-decision-package`;預設不搜尋、不寫 DB,若有候選則輸出 decision envelope、confidence、guardrails、failure reasons 與 exception-only machine review 分流。
+7. 需要看整體 AI 自動化進度時,讀 `/api/ai/pchome-growth/ai-automation-readiness`;它聚合缺口偵測、同款搜尋包、候選決策包、證據收據與受控落地,並回傳 `primary_human_gate_count=0`、`automation_policy.primary_flow=ai_controlled`。
+8. 需要補商品證據前,先讀只讀 API `/api/ai/pchome-growth/mapping-backlog/evidence-enrichment-preview`。
+9. 需要檢查 image / availability / price 要從哪裡補時,先讀只讀 API `/api/ai/pchome-growth/mapping-backlog/evidence-source-preview`。
+10. 需要 controlled GET smoke 時,先讀 `/api/ai/pchome-growth/mapping-backlog/evidence-fetch-gate`;預設不抓外站,只有 `execute_fetch=1` 才執行小批次 GET。
+11. 需要檢查 fetch receipt 能補掉哪些 evidence 缺口時,先讀 `/api/ai/pchome-growth/mapping-backlog/evidence-merge-preview`;預設不抓外站,只有 `execute_fetch=1` 才走 controlled fetch gate。
+12. 需要自動化收據時,讀 `/api/ai/pchome-growth/mapping-backlog/auto-policy-receipt-gate`;預設不抓外站、不 persistence,只有 `execute_fetch=1` 才走 controlled fetch gate 產生 ready receipt。
+13. 需要自動化 persistence dry-run contract 時,讀 `/api/ai/pchome-growth/mapping-backlog/auto-policy-persistence-gate`;只接受 ready receipts,輸出 idempotency key、payload hash、schema migration contract、transaction preview、rollback plan 與 post-write verifier,但不寫 DB。
+14. 需要 schema migration / future apply verifier contract 時,讀 `/api/ai/pchome-growth/mapping-backlog/auto-policy-schema-migration-preview`;只輸出 DDL preview、rollback preview、prewrite snapshot contract 與 post-write verifier,不執行 migration、不寫 DB。
+15. 需要 migration file / future apply endpoint contract 時,讀 `/api/ai/pchome-growth/mapping-backlog/auto-policy-migration-file-preview`;只輸出 `migrations/045_pchome_auto_policy_evidence_receipts.sql` 內容預覽、hash、request contract、abort conditions 與 rollback contract,不寫檔、不執行 endpoint。
+16. 需要真正 migration file 生成前的 readiness closeout 時,讀 `/api/ai/pchome-growth/mapping-backlog/auto-policy-apply-readiness-closeout`;只輸出 readiness checks、future-apply blockers 與 closeout decision,不寫檔、不執行 endpoint、不寫 DB。
+17. 需要 migration file generation request contract 時,讀 `/api/ai/pchome-growth/mapping-backlog/auto-policy-migration-file-generation-request`;只輸出 request id、target file、expected hash、required artifacts 與 file generation steps,不寫檔、不執行 endpoint、不寫 DB。
+18. 需要 migration apply gate preview 時,讀 `/api/ai/pchome-growth/mapping-backlog/auto-policy-migration-apply-gate-preview`;只讀本地 `045` migration 檔、hash、apply endpoint contract、prewrite snapshot requirement 與 post-apply verifier,不執行 SQL、不寫 DB。
+19. 需要 explicit DB apply request gate preview 時,讀 `/api/ai/pchome-growth/mapping-backlog/auto-policy-db-apply-request-gate-preview`;只輸出 command preview、required artifacts、abort conditions、runtime readback 與 rollback gate preview,不讀 secret、不執行 SQL、不寫 DB。
+20. 需要 DB apply execution preflight / snapshot artifact preview 時,讀 `/api/ai/pchome-growth/mapping-backlog/auto-policy-db-apply-execution-preflight`;只輸出 prewrite snapshot plan、post-apply readback plan、rollback artifact plan 與 abort conditions,不讀 secret、不執行 SQL、不寫 DB。
+21. 需要 fresh production apply authorization package 時,讀 `/api/ai/pchome-growth/mapping-backlog/auto-policy-db-apply-authorization-package`;只輸出 authorization checks、freshness requirements、machine apply manifest 與 verifier bundle,不讀 secret、不執行 SQL、不寫 DB。
+22. 需要 fresh-run verifier artifact generator preview 時,讀 `/api/ai/pchome-growth/mapping-backlog/auto-policy-db-apply-verifier-artifact-preview`;只輸出 prewrite snapshot / post-apply readback / rollback artifact schema、generation plan 與 verifier manifest,不寫檔、不讀 secret、不執行 SQL、不寫 DB。
+23. 需要 final explicit DB apply handoff package 時,讀 `/api/ai/pchome-growth/mapping-backlog/auto-policy-db-apply-final-handoff-package`;只輸出 final handoff sections、future runbook、command previews、abort gates 與 source proof manifest,不寫檔、不讀 secret、不執行 SQL、不寫 DB。
+24. 需要 controlled apply dry-run shell preview 時,讀 `/api/ai/pchome-growth/mapping-backlog/auto-policy-db-apply-controlled-dry-run-shell-preview`;只輸出 shell phases、script preview、check-mode contract 與 rollback hooks,不寫 script、不讀 secret、不執行 shell、不執行 SQL、不寫 DB。
+25. 需要 controlled dry-run shell closeout / explicit apply authorization boundary 時,讀 `/api/ai/pchome-growth/mapping-backlog/auto-policy-db-apply-controlled-dry-run-shell-closeout`;只輸出 closeout checks、future apply boundaries 與 explicit authorization boundary,不寫 script、不讀 secret、不執行 shell、不執行 SQL、不寫 DB。
+26. 需要 explicit DB apply authorization request intake 時,讀 `/api/ai/pchome-growth/mapping-backlog/auto-policy-db-apply-authorization-request-intake`;只輸出 authorization request intake、request payload schema、acceptance gates 與 rejection reasons,不讀 secret、不執行 shell、不執行 SQL、不寫 DB,且不簽發 database apply authorization。
+27. 需要 authorization request intake closeout / final exact request package 時,讀 `/api/ai/pchome-growth/mapping-backlog/auto-policy-db-apply-authorization-request-closeout`;只輸出 final exact request package、machine request manifest 與 closeout checks,不讀 secret、不執行 shell、不執行 SQL、不寫 DB,且不簽發 database apply authorization。
+28. 需要 future DB apply authorization lane guard 時,讀 `/api/ai/pchome-growth/mapping-backlog/auto-policy-db-apply-authorization-lane-guard`;只輸出 future authorization lane guard、lane transfer contract 與 lane entry requirements,不讀 secret、不執行 shell、不執行 SQL、不寫 DB,且不簽發 database apply authorization。
+29. 需要 future DB apply authorization decision preflight 時,讀 `/api/ai/pchome-growth/mapping-backlog/auto-policy-db-apply-authorization-decision-preflight`;只輸出 future authorization decision preflight、decision input requirements、decision rejection policy 與 preflight checks,不讀 secret、不執行 shell、不執行 SQL、不寫 DB,且不簽發 database apply authorization。
+30. 需要 future DB apply authorization decision closeout 時,讀 `/api/ai/pchome-growth/mapping-backlog/auto-policy-db-apply-authorization-decision-closeout`;只輸出 future authorization decision closeout、decision package、decision closeout contract 與 closeout checks,不讀 secret、不執行 shell、不執行 SQL、不寫 DB,且不簽發 database apply authorization。
+31. 需要 future DB apply authorization issuer gate / final nonsecret authorization envelope 時,讀 `/api/ai/pchome-growth/mapping-backlog/auto-policy-db-apply-authorization-issuer-gate`;只輸出 future authorization issuer gate、final nonsecret authorization envelope、issuer gate contract 與 issuer gate checks,不讀 secret、不執行 shell、不執行 SQL、不寫 DB,且不簽發 database apply authorization。
+32. 需要 future DB apply authorization signing-decision preflight 時,讀 `/api/ai/pchome-growth/mapping-backlog/auto-policy-db-apply-authorization-signing-decision-preflight`;只輸出 future authorization signing decision preflight、signing decision input requirements、rejection policy 與 preflight checks,不讀 secret、不執行 shell、不執行 SQL、不寫 DB,且不簽發 database apply authorization。
+33. 需要 future DB apply authorization signing-decision closeout / unsigned signing decision package 時,讀 `/api/ai/pchome-growth/mapping-backlog/auto-policy-db-apply-authorization-signing-decision-closeout`;只輸出 future authorization signing decision closeout、unsigned signing decision package、closeout contract 與 closeout checks,不讀 secret、不執行 shell、不執行 SQL、不寫 DB,且不簽發 database apply authorization。
+34. 需要 future DB apply authorization signing-issuer guard / signable request boundary 時,讀 `/api/ai/pchome-growth/mapping-backlog/auto-policy-db-apply-authorization-signing-issuer-guard`;只輸出 future authorization signing issuer guard、signable request boundary、signing issuer guard contract 與 guard checks,不讀 secret、不執行 shell、不執行 SQL、不寫 DB,且不簽發 database apply authorization。
+35. 若 `result=NEEDS_MAPPING`,先處理 `direct_mapping_count`,再處理 `review_candidate_count`。
+
+## 目前讀到的正式缺口
+
+- production `/health`: `V10.725`
+- PChome growth candidate_count: `20`
+- mapped_count: `4`
+- mapping_rate: `20.0%`
+- needs_mapping_count: `16`
+- action_counts: `先補商品對應=14`、`確認候選=2`、`放大價格優勢=4`
+
+## 邊界
+
+- reporter 只做 GET readback。
+- service truth source: `services/pchome_mapping_backlog_service.py`
+- operator preview 只產生 run package 與 write gate,不執行搜尋或寫入。
+- direct mapping auto-search package 已接上 `/api/ai/pchome-growth/mapping-backlog/direct-mapping-auto-search-package`;預設只產生搜尋詞、身份錨點與接受門檻,`execute_search=1` 才執行 read-only candidate search,仍不寫 DB。
+- direct mapping candidate decision package 已接上 `/api/ai/pchome-growth/mapping-backlog/direct-mapping-candidate-decision-package`;把候選轉成 P2 decision envelope,分流到 no-write receipt 或 exception-only machine review,仍不寫 DB、不 persistence。
+- AI automation readiness 已接上 `/api/ai/pchome-growth/ai-automation-readiness` 與 Dashboard status strip;它用單一摘要顯示缺口偵測、同款搜尋包、候選決策包、證據收據與受控落地,且 `primary_human_gate_count=0`、`automation_policy.primary_flow=ai_controlled`,例外也走 AI machine-verifiable auto-resolution。
+- Dashboard command center 已加入 AI 自動化作戰流水線,將 P1 搜尋包、P2 決策包、證據收據與受控落地轉成營運語言與數字,不外露 raw endpoint。
+- 外部來源治理已改成由 source readiness 產生完整 source_scope;除 MOMO 已接入外,Shopee、Lazada、Amazon、Google Merchant / Shopping、TikTok Shop、LINE 購物、Rakuten、Yahoo、露天、品牌官網 / Shopify、Meta Commerce、Coupang 都列為待接入且不進告警,並提供 offer evidence 欄位契約。
+- operator preview 參考外部主流規範:Google Merchant Center product data、Google Product / Merchant structured data、Baymard ecommerce product/search UX。
+- 每個 target 都應帶 `evidence_completeness`,至少列出 product URL、present/missing fields、blocking missing fields、auto_accept_ready 與 human_review_required。
+- deterministic `unit_package_basis` parser preview 已接入 evidence completeness,可從商品標題解析 `30ml`、`40ml x2`、`3.2g 多款任選`、`42張` 等單位/入數訊號;若遇到任選、色號、贈品、即期等風險訊號,仍要求人工覆核。
+- evidence enrichment preview 只產生 image / availability / unit basis 的補證據任務與來源建議,不抓外站、不寫 DB;unit basis 可由本地 parser 補足時,不再列為缺口。
+- evidence source preview 已接上 `/api/ai/pchome-growth/mapping-backlog/evidence-source-preview`,只列出 image / availability 的 future GET fetch gate、payload keys checked,以及 price payload mapping probe;preview 本身不抓外站、不寫 DB、不呼叫 LLM。
+- check-mode product-page evidence parser 已完成,可用 HTML fixture 解析 JSON-LD `Product.image`、`Offer.availability`、`og:image` 與 `product:availability` fallback;parser 本身不抓外站、不寫 DB、不呼叫 LLM。
+- controlled read-only fetch gate 已接上 `/api/ai/pchome-growth/mapping-backlog/evidence-fetch-gate`;預設 `execute_fetch=false` 只產生 planned receipts,`execute_fetch=1` 時才對 allowlist domain `24h.pchome.com.tw` 執行最多 12 筆 GET,含 timeout、512KB HTML cap、no-write receipt。
+- production-derived 1 筆 GET smoke:`DDBH3K-A900K0KWV-002` 回 `HTTP 200`、`content_bytes=215631`,解析到 image URL 與 `Offer.availability=InStock`;receipt 仍是 `writes_database=false`、`dispatches_telegram=false`、`llm_calls=false`。
+- evidence merge preview 已接上 `/api/ai/pchome-growth/mapping-backlog/evidence-merge-preview`;production-derived 1 筆 smoke 讓 `DDBH3K-A900K0KWV-002` 的 `image/availability` 從 missing fields 移除,`merge_ready_count=1`、`writes_database_count=0`。
+- automation-first merge policy 已接上:review candidate 的 `pchome_price` 會自動納入 price evidence;production-derived batch=12 已達 `AUTO_ACCEPT_EVIDENCE_MERGE=10/10`、`manual_review_required_count=0`、`remaining_blocker_count=0`、`writes_database_count=0`。
+- auto-policy receipt gate 已接上 `/api/ai/pchome-growth/mapping-backlog/auto-policy-receipt-gate`;production-derived batch=12 已達 `READY_FOR_AUTO_PERSISTENCE=10/10`、`persists_receipt_count=0`、`writes_database_count=0`、`manual_review_required_count=0`。
+- auto-policy persistence gate 已接上 `/api/ai/pchome-growth/mapping-backlog/auto-policy-persistence-gate`;只接受 ready receipts,輸出 idempotency key、payload hash、future target table、transaction preview、schema contract、rollback plan 與 post-write verifier;production-derived batch=12 smoke 已達 `PERSISTENCE_DRY_RUN_READY`、`dry_run_ready_count=10/10`、`manual_review_required_count=0`、`writes_database_count=0`、`persists_receipt_count=0`,目前仍是 no-write / dry-run-only。
+- auto-policy schema migration preview 已接上 `/api/ai/pchome-growth/mapping-backlog/auto-policy-schema-migration-preview`;輸出 `external_offer_evidence_receipts` DDL preview、rollback preview、prewrite snapshot contract、future apply verifier 與 exception-only failure routing;production-derived batch=12 smoke 已達 `SCHEMA_MIGRATION_PREVIEW_READY`、`dry_run_ready_count=10/10`、`schema_statement_count=13`、`future_verifier_count=5`、`current_preview_apply_allowed=false`、`executes_migration_count=0`、`writes_database_count=0`,目前仍是 no-write / no-SQL-execute preview。
+- auto-policy migration file preview 已接上 `/api/ai/pchome-growth/mapping-backlog/auto-policy-migration-file-preview`;輸出 `migrations/045_pchome_auto_policy_evidence_receipts.sql` forward SQL preview、hash、rollback preview、future apply endpoint request contract、preflight sequence、abort conditions 與 rollback contract;production-derived batch=12 smoke 已達 `MIGRATION_FILE_PREVIEW_READY`、`dry_run_ready_count=10/10`、`migration_file_line_count=31`、`apply_endpoint_contract_ready_count=1`、`forbidden_forward_tokens_absent=true`、`writes_file_count=0`、`executes_endpoint_count=0`、`writes_database_count=0`,目前仍是 no-file-write / no-endpoint-execute / no-SQL-execute preview。
+- auto-policy apply-readiness closeout 已接上 `/api/ai/pchome-growth/mapping-backlog/auto-policy-apply-readiness-closeout`;輸出 readiness checks、future-apply blockers、migration file hash summary 與 apply endpoint summary;production-derived batch=12 smoke 已達 `APPLY_READINESS_CLOSEOUT_READY`、`readiness_pass_count=9/9`、`current_preview_ready_count=1`、`ready_for_migration_file_generation_request=true`、`ready_for_database_apply=false`、`future_apply_blocker_count=4`、`writes_file_count=0`、`executes_endpoint_count=0`、`writes_database_count=0`;closeout ready 只代表可提出 migration file generation request,不代表 DB apply。
+- auto-policy migration file generation request 已接上 `/api/ai/pchome-growth/mapping-backlog/auto-policy-migration-file-generation-request`;輸出 request id、target file、expected SHA256、required artifacts 與 file generation steps;`migrations/045_pchome_auto_policy_evidence_receipts.sql` 已在本地 repo 生成,hash 與 request expected SHA256 對齊;production-derived batch=12 smoke 已達 `FILE_GENERATION_REQUEST_READY`、`request_ready_count=1`、`dry_run_ready_count=10/10`、`required_artifact_count=4`、`file_generation_step_count=3`、`ready_to_generate_file=true`、`ready_for_database_apply=false`、`generated_file_exists=true`、`generated_file_hash_matches_request=true`、`writes_file_count=0`、`executes_endpoint_count=0`、`writes_database_count=0`,request API 本身仍是 no-file-write / no-endpoint-execute / no-DB-write。
+- auto-policy migration apply gate preview 已接上 `/api/ai/pchome-growth/mapping-backlog/auto-policy-migration-apply-gate-preview`;讀本地 `045` migration 檔並比對 request hash、additive-only、future apply endpoint contract、prewrite snapshot requirement 與 post-apply verifier requirement;production-derived batch=12 smoke 已達 `MIGRATION_APPLY_GATE_PREVIEW_READY`、`apply_gate_pass_count=9/9`、`apply_preview_ready_count=1`、`generated_file_hash_matches_count=1`、`ready_for_explicit_db_apply_request=true`、`ready_for_database_apply_now=false`、`future_apply_blocker_count=3`、`executes_migration_count=0`、`writes_database_count=0`,目前仍是 no-SQL-execute / no-DB-write preview。
+- auto-policy explicit DB apply request gate preview 已接上 `/api/ai/pchome-growth/mapping-backlog/auto-policy-db-apply-request-gate-preview`;輸出 DB apply request id、future command preview、required artifacts、apply sequence、abort conditions、runtime readback 與 rollback gate preview;production-derived batch=12 smoke 已達 `DB_APPLY_REQUEST_GATE_READY`、`request_ready_count=1`、`required_artifact_count=5`、`apply_sequence_step_count=5`、`abort_condition_count=6`、`generated_file_hash_matches_count=1`、`ready_for_explicit_db_apply_request=true`、`ready_for_database_apply_now=false`、`reads_secret_in_preview=false`、`executes_migration_count=0`、`writes_database_count=0`,目前仍是 no-secret-read / no-SQL-execute / no-DB-write preview。
+- auto-policy DB apply execution preflight 已接上 `/api/ai/pchome-growth/mapping-backlog/auto-policy-db-apply-execution-preflight`;輸出 prewrite snapshot plan、post-apply readback plan、rollback artifact plan、required artifacts 與 abort conditions;production-derived batch=12 smoke 已達 `DB_APPLY_EXECUTION_PREFLIGHT_READY`、`preflight_ready_count=1`、`request_ready_count=1`、`required_artifact_count=6`、`snapshot_plan_count=5`、`readback_plan_count=6`、`rollback_artifact_count=1`、`generated_file_hash_matches_count=1`、`ready_for_preflight_artifact_generation=true`、`ready_for_database_apply_now=false`、`reads_secret_in_preview=false`、`executes_sql_count=0`、`executes_migration_count=0`、`writes_database_count=0`,目前仍是 no-secret-read / no-SQL-execute / no-DB-write preview。
+- auto-policy DB apply authorization package 已接上 `/api/ai/pchome-growth/mapping-backlog/auto-policy-db-apply-authorization-package`;輸出 authorization checks、freshness requirements、machine apply manifest 與 verifier bundle;production-derived batch=12 smoke 已達 `DB_APPLY_AUTHORIZATION_PACKAGE_READY`、`authorization_check_count=11`、`authorization_pass_count=11`、`authorization_waiting_count=0`、`authorization_package_ready_count=1`、`freshness_requirement_count=5`、`manifest_step_count=6`、`verifier_bundle_count=3`、`ready_for_explicit_apply_authorization_request=true`、`ready_for_database_apply_now=false`、`reads_secret_in_preview=false`、`executes_sql_count=0`、`executes_migration_count=0`、`writes_database_count=0`,目前仍是 no-secret-read / no-SQL-execute / no-DB-write preview。
+- auto-policy DB apply verifier artifact preview 已接上 `/api/ai/pchome-growth/mapping-backlog/auto-policy-db-apply-verifier-artifact-preview`;輸出 prewrite snapshot / post-apply readback / rollback artifact schema、generation plan 與 verifier manifest;production-derived batch=12 smoke 已達 `DB_APPLY_VERIFIER_ARTIFACT_PREVIEW_READY`、`artifact_preview_ready_count=1`、`authorization_package_ready_count=1`、`artifact_schema_count=3`、`artifact_generation_step_count=5`、`verifier_check_count=15`、`writes_artifact_count=0`、`ready_for_future_artifact_generation=true`、`ready_to_write_artifacts_now=false`、`ready_for_database_apply_now=false`、`reads_secret_in_preview=false`、`executes_sql_count=0`、`executes_migration_count=0`、`writes_database_count=0`,目前仍是 no-file-write / no-secret-read / no-SQL-execute / no-DB-write preview。
+- auto-policy DB apply final handoff package 已接上 `/api/ai/pchome-growth/mapping-backlog/auto-policy-db-apply-final-handoff-package`;輸出 final handoff sections、future runbook、command previews、abort gates 與 source proof manifest;production-derived batch=12 smoke 已達 `DB_APPLY_FINAL_HANDOFF_PACKAGE_READY`、`final_handoff_ready_count=1`、`artifact_preview_ready_count=1`、`handoff_section_count=6`、`final_runbook_step_count=7`、`command_preview_count=3`、`abort_gate_count=10`、`source_endpoint_count=4`、`ready_for_explicit_db_apply_handoff=true`、`ready_for_database_apply_now=false`、`writes_artifact_count=0`、`reads_secret_in_preview=false`、`executes_sql_count=0`、`executes_migration_count=0`、`writes_database_count=0`,目前仍是 no-file-write / no-secret-read / no-SQL-execute / no-DB-write preview。
+- auto-policy DB apply controlled dry-run shell preview 已接上 `/api/ai/pchome-growth/mapping-backlog/auto-policy-db-apply-controlled-dry-run-shell-preview`;輸出 shell phases、script preview、check-mode contract 與 rollback hooks;production-derived batch=12 smoke 已達 `DB_APPLY_CONTROLLED_DRY_RUN_SHELL_PREVIEW_READY`、`dry_run_shell_preview_ready_count=1`、`final_handoff_ready_count=1`、`shell_phase_count=9`、`shell_script_line_count=10`、`check_mode_required_check_count=6`、`rollback_hook_count=3`、`writes_script_count=0`、`executes_script_count=0`、`ready_for_future_shell_script_generation=true`、`ready_to_write_script_now=false`、`ready_to_execute_shell_now=false`、`ready_for_database_apply_now=false`、`reads_secret_in_preview=false`、`executes_sql_count=0`、`executes_migration_count=0`、`writes_database_count=0`,目前仍是 no-script-write / no-secret-read / no-shell-execute / no-SQL-execute / no-DB-write preview。
+- auto-policy DB apply controlled dry-run shell closeout 已接上 `/api/ai/pchome-growth/mapping-backlog/auto-policy-db-apply-controlled-dry-run-shell-closeout`;輸出 closeout checks、future apply boundaries 與 explicit authorization boundary;production-derived batch=12 smoke 已達 `DB_APPLY_CONTROLLED_DRY_RUN_SHELL_CLOSEOUT_READY`、`closeout_ready_count=1`、`closeout_check_count=13`、`closeout_pass_count=13`、`closeout_waiting_count=0`、`dry_run_shell_preview_ready_count=1`、`future_apply_boundary_count=6`、`ready_for_explicit_apply_authorization_boundary=true`、`ready_for_database_apply_now=false`、`writes_script_count=0`、`executes_script_count=0`、`reads_secret_in_preview=false`、`executes_sql_count=0`、`executes_migration_count=0`、`writes_database_count=0`,目前仍是 no-script-write / no-secret-read / no-shell-execute / no-SQL-execute / no-DB-write preview。
+- auto-policy DB apply authorization request intake 已接上 `/api/ai/pchome-growth/mapping-backlog/auto-policy-db-apply-authorization-request-intake`;輸出 authorization request intake、request payload schema、acceptance gates、required request evidence 與 rejection reasons;production-derived batch=12 smoke 已達 `DB_APPLY_AUTHORIZATION_REQUEST_INTAKE_READY`、`authorization_request_intake_ready_count=1`、`required_request_evidence_count=7`、`request_payload_required_field_count=10`、`authorization_acceptance_gate_count=11`、`authorization_acceptance_pass_count=11`、`authorization_acceptance_waiting_count=0`、`rejection_reason_count=10`、`closeout_ready_count=1`、`future_apply_boundary_count=6`、`accepts_authorization_request=true`、`issues_database_apply_authorization=false`、`ready_for_database_apply_now=false`、`reads_secret_in_preview=false`、`executes_script_count=0`、`executes_sql_count=0`、`executes_migration_count=0`、`writes_database_count=0`,目前仍是 request-intake-only / no-secret-read / no-shell-execute / no-SQL-execute / no-DB-write preview。
+- auto-policy DB apply authorization request closeout 已接上 `/api/ai/pchome-growth/mapping-backlog/auto-policy-db-apply-authorization-request-closeout`;輸出 final exact request package、machine request manifest 與 closeout checks;production-derived batch=12 smoke 已達 `DB_APPLY_AUTHORIZATION_REQUEST_CLOSEOUT_READY`、`authorization_request_closeout_ready_count=1`、`closeout_check_count=12`、`closeout_pass_count=12`、`closeout_waiting_count=0`、`authorization_request_intake_ready_count=1`、`exact_request_payload_field_count=10`、`machine_request_manifest_step_count=6`、`required_request_evidence_count=7`、`authorization_acceptance_gate_count=11`、`rejection_reason_count=10`、`ready_for_exact_authorization_request_package=true`、`issues_database_apply_authorization=false`、`ready_for_database_apply_now=false`、`reads_secret_in_preview=false`、`executes_script_count=0`、`executes_sql_count=0`、`executes_migration_count=0`、`writes_database_count=0`,目前仍是 final-request-package-only / no-secret-read / no-shell-execute / no-SQL-execute / no-DB-write preview。
+- auto-policy DB apply authorization lane guard 已接上 `/api/ai/pchome-growth/mapping-backlog/auto-policy-db-apply-authorization-lane-guard`;輸出 future authorization lane guard、lane transfer contract、lane entry requirements 與 lane guard checks;production-derived batch=12 smoke 已達 `DB_APPLY_AUTHORIZATION_LANE_GUARD_READY`、`authorization_lane_guard_ready_count=1`、`lane_guard_check_count=12`、`lane_guard_pass_count=12`、`lane_guard_waiting_count=0`、`authorization_request_closeout_ready_count=1`、`exact_request_payload_field_count=10`、`machine_request_manifest_step_count=6`、`lane_entry_requirement_count=6`、`required_request_evidence_count=7`、`authorization_acceptance_gate_count=11`、`rejection_reason_count=10`、`ready_for_future_authorization_lane_entry=true`、`issues_database_apply_authorization=false`、`ready_for_database_apply_now=false`、`reads_secret_in_preview=false`、`executes_script_count=0`、`executes_sql_count=0`、`executes_migration_count=0`、`writes_database_count=0`,目前仍是 lane-guard-only / no-secret-read / no-shell-execute / no-SQL-execute / no-DB-write preview。
+- auto-policy DB apply authorization decision preflight 已接上 `/api/ai/pchome-growth/mapping-backlog/auto-policy-db-apply-authorization-decision-preflight`;輸出 future authorization decision preflight、decision input requirements、decision rejection policy 與 preflight checks;production-derived batch=12 smoke 已達 `DB_APPLY_AUTHORIZATION_DECISION_PREFLIGHT_READY`、`authorization_decision_preflight_ready_count=1`、`decision_preflight_check_count=12`、`decision_preflight_pass_count=12`、`decision_preflight_waiting_count=0`、`authorization_lane_guard_ready_count=1`、`decision_input_requirement_count=8`、`decision_rejection_reason_count=10`、`lane_entry_requirement_count=6`、`exact_request_payload_field_count=10`、`machine_request_manifest_step_count=6`、`allows_authorization_decision_in_future_lane=true`、`issues_database_apply_authorization=false`、`ready_for_database_apply_now=false`、`reads_secret_in_preview=false`、`executes_script_count=0`、`executes_sql_count=0`、`executes_migration_count=0`、`writes_database_count=0`,目前仍是 decision-preflight-only / no-secret-read / no-shell-execute / no-SQL-execute / no-DB-write preview。
+- auto-policy DB apply authorization decision closeout 已接上 `/api/ai/pchome-growth/mapping-backlog/auto-policy-db-apply-authorization-decision-closeout`;輸出 future authorization decision closeout、future authorization decision package、decision closeout contract 與 closeout checks;production-derived batch=12 smoke 已達 `DB_APPLY_AUTHORIZATION_DECISION_CLOSEOUT_READY`、`authorization_decision_closeout_ready_count=1`、`decision_closeout_check_count=12`、`decision_closeout_pass_count=12`、`decision_closeout_waiting_count=0`、`authorization_decision_preflight_ready_count=1`、`decision_input_requirement_count=8`、`decision_rejection_reason_count=10`、`post_apply_verifier_required_count=1`、`same_run_truth_required_count=1`、`permits_future_authorization_decision_lane=true`、`issues_database_apply_authorization=false`、`ready_for_database_apply_now=false`、`reads_secret_in_preview=false`、`executes_script_count=0`、`executes_sql_count=0`、`executes_migration_count=0`、`writes_database_count=0`,目前仍是 decision-closeout-only / no-secret-read / no-shell-execute / no-SQL-execute / no-DB-write preview。
+- auto-policy DB apply authorization issuer gate 已接上 `/api/ai/pchome-growth/mapping-backlog/auto-policy-db-apply-authorization-issuer-gate`;輸出 future authorization issuer gate、final nonsecret authorization envelope、issuer gate contract 與 issuer gate checks;production-derived batch=12 smoke 已達 `DB_APPLY_AUTHORIZATION_ISSUER_GATE_READY`、`authorization_issuer_gate_ready_count=1`、`issuer_gate_check_count=12`、`issuer_gate_pass_count=12`、`issuer_gate_waiting_count=0`、`authorization_decision_closeout_ready_count=1`、`decision_closeout_check_count=12`、`required_issuer_evidence_count=9`、`nonsecret_authorization_claim_count=8`、`post_apply_verifier_required_count=1`、`same_run_truth_required_count=1`、`permits_future_authorization_issuer_lane=true`、`issues_database_apply_authorization=false`、`signs_database_apply_authorization=false`、`ready_for_database_apply_now=false`、`secret_material_included=false`、`reads_secret_in_preview=false`、`executes_script_count=0`、`executes_sql_count=0`、`executes_migration_count=0`、`writes_database_count=0`,目前仍是 issuer-gate-only / nonsecret-envelope-only / no-secret-read / no-shell-execute / no-SQL-execute / no-DB-write preview。
+- auto-policy DB apply authorization signing-decision preflight 已接上 `/api/ai/pchome-growth/mapping-backlog/auto-policy-db-apply-authorization-signing-decision-preflight`;輸出 future authorization signing decision preflight、signing decision preflight envelope、signing decision input requirements、rejection policy 與 preflight checks;production-derived batch=12 smoke 已達 `DB_APPLY_AUTHORIZATION_SIGNING_DECISION_PREFLIGHT_READY`、`authorization_signing_decision_preflight_ready_count=1`、`signing_decision_preflight_check_count=12`、`signing_decision_preflight_pass_count=12`、`signing_decision_preflight_waiting_count=0`、`authorization_issuer_gate_ready_count=1`、`issuer_gate_check_count=12`、`required_issuer_evidence_count=9`、`nonsecret_authorization_claim_count=8`、`signing_decision_input_requirement_count=10`、`signing_decision_rejection_reason_count=11`、`allows_future_authorization_signing_decision_lane=true`、`issues_database_apply_authorization=false`、`signs_database_apply_authorization=false`、`ready_for_database_apply_now=false`、`secret_material_required_in_preview=false`、`reads_secret_in_preview=false`、`executes_script_count=0`、`executes_sql_count=0`、`executes_migration_count=0`、`writes_database_count=0`,目前仍是 signing-decision-preflight-only / no-secret-read / no-signing / no-shell-execute / no-SQL-execute / no-DB-write preview。
+- auto-policy DB apply authorization signing-decision closeout 已接上 `/api/ai/pchome-growth/mapping-backlog/auto-policy-db-apply-authorization-signing-decision-closeout`;輸出 future authorization signing decision closeout、unsigned signing decision package、signing decision closeout contract 與 closeout checks;production-derived batch=12 smoke 已達 `DB_APPLY_AUTHORIZATION_SIGNING_DECISION_CLOSEOUT_READY`、`authorization_signing_decision_closeout_ready_count=1`、`signing_decision_closeout_check_count=12`、`signing_decision_closeout_pass_count=12`、`signing_decision_closeout_waiting_count=0`、`authorization_signing_decision_preflight_ready_count=1`、`signing_decision_input_requirement_count=10`、`signing_decision_rejection_reason_count=11`、`required_issuer_evidence_count=9`、`nonsecret_authorization_claim_count=8`、`permits_future_unsigned_signing_decision_package_lane=true`、`issues_database_apply_authorization=false`、`signs_database_apply_authorization=false`、`ready_for_database_apply_now=false`、`secret_material_included=false`、`secret_material_required_in_preview=false`、`reads_secret_in_preview=false`、`executes_script_count=0`、`executes_sql_count=0`、`executes_migration_count=0`、`writes_database_count=0`,目前仍是 signing-decision-closeout-only / unsigned-package-only / no-secret-read / no-signing / no-shell-execute / no-SQL-execute / no-DB-write preview。
+- auto-policy DB apply authorization signing-issuer guard 已接上 `/api/ai/pchome-growth/mapping-backlog/auto-policy-db-apply-authorization-signing-issuer-guard`;輸出 future authorization signing issuer guard、signable request boundary、signing issuer guard contract 與 guard checks;production-derived batch=12 smoke 已達 `DB_APPLY_AUTHORIZATION_SIGNING_ISSUER_GUARD_READY`、`authorization_signing_issuer_guard_ready_count=1`、`signing_issuer_guard_check_count=12`、`signing_issuer_guard_pass_count=12`、`signing_issuer_guard_waiting_count=0`、`authorization_signing_decision_closeout_ready_count=1`、`signing_decision_closeout_check_count=12`、`signing_decision_input_requirement_count=10`、`signing_decision_rejection_reason_count=11`、`required_issuer_evidence_count=9`、`nonsecret_authorization_claim_count=8`、`permits_future_authorization_signing_issuer_lane=true`、`issues_database_apply_authorization=false`、`signs_database_apply_authorization=false`、`ready_for_database_apply_now=false`、`secret_material_included=false`、`secret_material_required_in_preview=false`、`reads_secret_in_preview=false`、`executes_script_count=0`、`executes_sql_count=0`、`executes_migration_count=0`、`writes_database_count=0`,目前仍是 signing-issuer-guard-only / signable-boundary-only / no-secret-read / no-signing / no-shell-execute / no-SQL-execute / no-DB-write preview。
+- Dashboard no-write UI 已顯示 `auto_receipt_count` 與 `manual_review_required_count`,首頁不觸發外站 GET、不 persistence。
+- production-derived evidence/source preview 目前 `task_count=6`、`tasks_with_blockers=6`、`missing_field_counts=image=6, availability=6, price=1, unit_price_or_package_basis=0`、`fetch_gate_candidate_count=6`;price 缺口樣本是 review candidate `DDABN0-A900IH795-000`。
+- AI automation 只先產生 Ollama-first read-only assist plan;preview 不呼叫 LLM、不允許 Gemini、不寫 DB。
+- 不執行 backfill POST。
+- 不寫 DB、不掛 scheduler、不派 Telegram。
+- 若版本 guard blocked,不應宣稱本地或 Gitea 是最新正式版本。
+
+## 下一步
+
+1. 進入 future DB apply authorization signing-issuer closeout / final signable request package:只收斂 signing issuer guard 的 signable boundary、source chain、same-run production truth、post-apply verifier、secret rejection 與 no-signing/no-write guarantees;仍不讀 secret、不在此步簽發 database apply authorization,也不直接 DB apply。
+2. 以 Google Product / Merchant listing structured data 的 `Product.image`、`Offer.availability` 為解析目標。
+3. 順手追 review candidate price payload 邊界,確認缺口是來源 payload 欄位缺失還是 summary 映射缺失。
+4. image / availability / price payload 邊界都補齊並通過 production readback 後,才進入 operator write approval / backfill POST 小批次。
diff --git a/docs/memory/production_version_truth_20260627.md b/docs/memory/production_version_truth_20260627.md
new file mode 100644
index 0000000..c2b0058
--- /dev/null
+++ b/docs/memory/production_version_truth_20260627.md
@@ -0,0 +1,29 @@
+# Production Version Truth 2026-06-27
+
+## Rule
+
+正式環境 `/health` 是 EwoooC 的最新版本真相。
+
+Local workspace、Gitea `main`、branch、handoff 或文件只能作為 source candidate;除非 production readback 回同一個版本,否則不能宣稱已上線或最新。
+
+## Current Protected Baseline
+
+- Production URL: `https://mo.wooo.work/health`
+- Production readback at setup time: `status=healthy`, `database=postgresql`, `version=V10.725`
+- Clean local workspace: `/Users/ogt/codex-workspaces/ewoooc-dev`
+- Local branch at setup time: `main`
+- Local / origin HEAD at setup time: `f3e412c`
+- Clean local `config.py`: `SYSTEM_VERSION = "V10.725"`
+- Old iCloud checkout had a local-only `config.py` drift to `V10.726`; that drift is not production truth.
+
+## Required Check
+
+Before claiming a version, run:
+
+```bash
+python scripts/ops/check_production_version_truth.py
+```
+
+If the script reports `BLOCKED`, treat local work as a candidate and do not deploy, report completion, or write handoff text that says the local version is latest.
+
+For an explicit release-prep branch only, `--allow-local-version-drift` may be used to inspect a local `config.py` version bump, but production `/health` still remains the latest runtime truth until deploy readback matches it.
diff --git a/scripts/ops/report_pchome_mapping_backlog.py b/scripts/ops/report_pchome_mapping_backlog.py
new file mode 100755
index 0000000..ef39f49
--- /dev/null
+++ b/scripts/ops/report_pchome_mapping_backlog.py
@@ -0,0 +1,153 @@
+#!/usr/bin/env python3
+"""Read-only report for the production PChome growth mapping backlog."""
+
+from __future__ import annotations
+
+import argparse
+import json
+import urllib.error
+import urllib.parse
+import urllib.request
+from typing import Any
+
+from scripts.ops import check_production_version_truth as version_guard
+from services.pchome_mapping_backlog_service import summarize_pchome_mapping_backlog
+
+
+DEFAULT_API_URL = "https://mo.wooo.work/api/ai/pchome-growth/opportunities"
+DEFAULT_LIMIT = 20
+
+
+def with_limit(url: str, limit: int) -> str:
+ parsed = urllib.parse.urlparse(url)
+ query = urllib.parse.parse_qs(parsed.query, keep_blank_values=True)
+ query["limit"] = [str(max(5, min(int(limit or DEFAULT_LIMIT), 50)))]
+ return urllib.parse.urlunparse(parsed._replace(query=urllib.parse.urlencode(query, doseq=True)))
+
+
+def fetch_json(url: str, timeout: float) -> dict[str, Any]:
+ with urllib.request.urlopen(url, timeout=timeout) as response:
+ payload = response.read().decode("utf-8")
+ data = json.loads(payload)
+ if not isinstance(data, dict):
+ raise ValueError("API payload must be a JSON object")
+ return data
+
+
+def summarize_payload(payload: dict[str, Any]) -> dict[str, Any]:
+ return summarize_pchome_mapping_backlog(payload)
+
+
+def build_report(
+ api_url: str,
+ limit: int,
+ timeout: float,
+ health_url: str,
+ skip_version_truth: bool,
+) -> dict[str, Any]:
+ version_report = None
+ version_errors: list[str] = []
+ if not skip_version_truth:
+ version_report = version_guard.build_report(health_url, timeout)
+ version_ok, version_errors = version_guard.evaluate(version_report, allow_local_version_drift=False)
+ if not version_ok:
+ return {
+ "policy": "read_only_production_pchome_mapping_backlog",
+ "result": "BLOCKED",
+ "api_url": with_limit(api_url, limit),
+ "version_truth": version_report,
+ "errors": version_errors,
+ }
+
+ final_url = with_limit(api_url, limit)
+ payload = fetch_json(final_url, timeout)
+ summary = summarize_payload(payload)
+ return {
+ **summary,
+ "policy": "read_only_production_pchome_mapping_backlog",
+ "api_url": final_url,
+ "version_truth": version_report,
+ "errors": version_errors,
+ }
+
+
+def format_text(report: dict[str, Any]) -> str:
+ stats = report.get("stats") or {}
+ backlog = report.get("backlog") or {}
+ version = ((report.get("version_truth") or {}).get("production") or {}).get("version")
+ lines = [
+ "pchome_mapping_backlog:",
+ f"- policy: {report.get('policy')}",
+ f"- result: {report.get('result')}",
+ f"- production_version: {version or 'not_checked'}",
+ f"- api_url: {report.get('api_url')}",
+ f"- generated_at: {report.get('generated_at')}",
+ f"- cache_state: {report.get('cache_state') or 'unknown'}",
+ f"- candidate_count: {stats.get('candidate_count')}",
+ f"- mapped_count: {stats.get('mapped_count')}",
+ f"- mapping_rate: {stats.get('mapping_rate')}%",
+ f"- needs_mapping_count: {stats.get('needs_mapping_count')}",
+ f"- review_candidate_count: {stats.get('review_candidate_count')}",
+ f"- overall_sales_7d: {stats.get('overall_sales_7d')}",
+ f"- action_counts: {json.dumps(stats.get('action_counts') or {}, ensure_ascii=False, sort_keys=True)}",
+ f"- direct_mapping_count: {backlog.get('direct_mapping_count')}",
+ f"- mapped_opportunity_count: {backlog.get('mapped_opportunity_count')}",
+ ]
+ for error in report.get("errors") or []:
+ lines.append(f"- blocker: {error}")
+
+ top_items = list((backlog.get("top_needs_mapping") or [])[:5])
+ if top_items:
+ lines.append("- top_needs_mapping:")
+ for item in top_items:
+ lines.append(
+ " - "
+ f"{item.get('pchome_product_id')} | "
+ f"sales_7d={item.get('sales_7d')} | "
+ f"score={item.get('priority_score')} | "
+ f"action={item.get('action_label')}"
+ )
+ return "\n".join(lines)
+
+
+def main(argv: list[str] | None = None) -> int:
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument("--api-url", default=DEFAULT_API_URL)
+ parser.add_argument("--health-url", default=version_guard.DEFAULT_HEALTH_URL)
+ parser.add_argument("--limit", type=int, default=DEFAULT_LIMIT)
+ parser.add_argument("--timeout", type=float, default=12.0)
+ parser.add_argument("--json", action="store_true", help="Print machine-readable report")
+ parser.add_argument(
+ "--skip-version-truth",
+ action="store_true",
+ help="Skip production version guard; intended only for offline parser tests.",
+ )
+ args = parser.parse_args(argv)
+
+ try:
+ report = build_report(
+ api_url=args.api_url,
+ limit=args.limit,
+ timeout=args.timeout,
+ health_url=args.health_url,
+ skip_version_truth=args.skip_version_truth,
+ )
+ except (OSError, ValueError, urllib.error.URLError) as exc:
+ report = {
+ "policy": "read_only_production_pchome_mapping_backlog",
+ "result": "BLOCKED",
+ "api_url": with_limit(args.api_url, args.limit),
+ "errors": [str(exc)],
+ }
+ print(json.dumps(report, ensure_ascii=False, indent=2) if args.json else format_text(report))
+ return 2
+
+ if args.json:
+ print(json.dumps(report, ensure_ascii=False, indent=2))
+ else:
+ print(format_text(report))
+ return 0 if report.get("result") != "BLOCKED" else 1
+
+
+if __name__ == "__main__":
+ raise SystemExit(main())
diff --git a/services/ai_automation_smoke_service.py b/services/ai_automation_smoke_service.py
index 37551ea..4a3a33f 100644
--- a/services/ai_automation_smoke_service.py
+++ b/services/ai_automation_smoke_service.py
@@ -501,6 +501,23 @@ def build_scheduled_automation_health_summary(
auto_policy_controlled_dry_run_receipt_closeout_details = (
auto_policy_controlled_dry_run_receipt_closeout.get("details") or {}
)
+ auto_policy_controlled_dry_run_runner_readiness = _find_check(
+ source_result,
+ "PChome auto-policy controlled dry-run runner readiness",
+ )
+ auto_policy_controlled_dry_run_runner_readiness_details = (
+ auto_policy_controlled_dry_run_runner_readiness.get("details") or {}
+ )
+ if (
+ not auto_policy_controlled_dry_run_runner_readiness
+ or not auto_policy_controlled_dry_run_runner_readiness_details
+ ):
+ auto_policy_controlled_dry_run_runner_readiness = (
+ _pchome_auto_policy_controlled_dry_run_runner_readiness_check()
+ )
+ auto_policy_controlled_dry_run_runner_readiness_details = (
+ auto_policy_controlled_dry_run_runner_readiness.get("details") or {}
+ )
surface_readback = _find_check(source_result, "AI surface HTML readback")
surface_details = surface_readback.get("details") or {}
sitewide_readback = _find_check(source_result, "Sitewide UI/UX Agent readback")
@@ -4408,6 +4425,36 @@ def build_scheduled_automation_health_summary(
"writes_database": False,
},
},
+ {
+ "key": "pchome_auto_policy_controlled_dry_run_runner_readiness",
+ "label": "PChome auto-policy controlled dry-run runner readiness",
+ "status": (
+ auto_policy_controlled_dry_run_runner_readiness.get("status")
+ or "warning"
+ ),
+ "summary": (
+ auto_policy_controlled_dry_run_runner_readiness.get("summary")
+ or (
+ "PChome auto-policy controlled dry-run runner readiness "
+ "has no latest check."
+ )
+ ),
+ "next_machine_action": (
+ auto_policy_controlled_dry_run_runner_readiness_details.get(
+ "next_machine_action"
+ )
+ or (
+ "continue_pchome_auto_policy_controlled_dry_run_runner_readiness_monitoring"
+ if auto_policy_controlled_dry_run_runner_readiness.get("status")
+ == "ok"
+ else "refresh_pchome_auto_policy_controlled_dry_run_runner_readiness"
+ )
+ ),
+ "details": {
+ **auto_policy_controlled_dry_run_runner_readiness_details,
+ "writes_database": False,
+ },
+ },
{
"key": "ai_surface_html_readback",
"label": "AI surface HTML readback",
@@ -12146,6 +12193,657 @@ def _pchome_auto_policy_controlled_dry_run_receipt_closeout_check() -> Dict[str,
)
+def _pchome_auto_policy_controlled_dry_run_runner_readiness_check() -> Dict[str, Any]:
+ """Read-only monitor for the controlled dry-run runner readiness binding."""
+ try:
+ from services import pchome_mapping_backlog_service as backlog
+ from services.ai_exception_contract import (
+ LEGACY_REVIEW_REQUIRED_COUNT_KEY,
+ PRIMARY_HUMAN_GATE_COUNT_KEY,
+ )
+
+ payload = _pchome_auto_policy_decision_preflight_contract_payload()
+ readiness = (
+ backlog.build_pchome_auto_policy_db_apply_controlled_dry_run_runner_readiness(
+ payload,
+ batch_size=max(
+ 1,
+ min(_PCHOME_AUTO_POLICY_DECISION_PREFLIGHT_BATCH_SIZE, 50),
+ ),
+ execute_fetch=True,
+ timeout_seconds=max(
+ 1,
+ min(_PCHOME_AUTO_POLICY_DECISION_PREFLIGHT_TIMEOUT_SECONDS, 30),
+ ),
+ http_get=_pchome_auto_policy_machine_fetch_evidence_get,
+ )
+ )
+ summary = readiness.get("summary") or {}
+ future_plan = (
+ readiness.get(
+ "future_database_apply_controlled_dry_run_execution_plan_binding"
+ )
+ or {}
+ )
+ runner = readiness.get("controlled_dry_run_runner_readiness") or {}
+ contract = readiness.get("controlled_dry_run_runner_readiness_contract") or {}
+ safety = readiness.get("safety") or {}
+ execution_plan = runner.get("execution_plan_binding") or {}
+ result_parser = runner.get("dry_run_result_parser") or {}
+ validation = runner.get("receipt_validation_report") or {}
+ source_closeout = (
+ readiness.get("source_controlled_dry_run_receipt_closeout") or {}
+ )
+ source_future_verification = (
+ readiness.get(
+ "source_database_apply_controlled_dry_run_result_parser_verification"
+ )
+ or {}
+ )
+ source_closeout_contract = (
+ readiness.get("source_controlled_dry_run_receipt_closeout_contract")
+ or {}
+ )
+ result = str(readiness.get("result") or "UNKNOWN")
+
+ def _count(key: str) -> int:
+ return int(summary.get(key) or 0)
+
+ ready_count = _count("controlled_dry_run_runner_readiness_ready_count")
+ check_count = _count("controlled_dry_run_runner_readiness_check_count")
+ pass_count = _count("controlled_dry_run_runner_readiness_pass_count")
+ waiting_count = _count("controlled_dry_run_runner_readiness_waiting_count")
+ receipt_closeout_ready_count = _count(
+ "controlled_dry_run_receipt_closeout_ready_count"
+ )
+ receipt_closeout_check_count = _count(
+ "controlled_dry_run_receipt_closeout_check_count"
+ )
+ package_ready_count = _count("controlled_dry_run_package_ready_count")
+ package_check_count = _count("controlled_dry_run_package_check_count")
+ controlled_apply_ready_count = _count(
+ "controlled_apply_final_preflight_ready_count"
+ )
+ controlled_apply_check_count = _count(
+ "controlled_apply_final_preflight_check_count"
+ )
+ evidence_closeout_ready_count = _count(
+ "authorization_evidence_execution_closeout_ready_count"
+ )
+ evidence_closeout_check_count = _count(
+ "authorization_evidence_execution_closeout_check_count"
+ )
+ evidence_preflight_ready_count = _count(
+ "authorization_evidence_execution_preflight_ready_count"
+ )
+ evidence_preflight_check_count = _count(
+ "authorization_evidence_execution_preflight_check_count"
+ )
+ final_verifier_gate_count = _count("database_apply_final_verifier_gate_count")
+ final_verifier_gate_ready_count = _count(
+ "database_apply_authorization_final_verifier_gate_ready_count"
+ )
+ runner_count = _count("controlled_dry_run_runner_readiness_count")
+ runner_field_count = _count("controlled_dry_run_runner_readiness_field_count")
+ runner_gate_count = _count(
+ "controlled_dry_run_runner_readiness_acceptance_gate_count"
+ )
+ execution_plan_count = _count("execution_plan_binding_count")
+ execution_plan_field_count = _count("execution_plan_binding_field_count")
+ parser_count = _count("dry_run_result_parser_count")
+ parser_field_count = _count("dry_run_result_parser_field_count")
+ validation_report_count = _count("receipt_validation_report_count")
+ validation_field_count = _count("receipt_validation_field_count")
+ receipt_closeout_count = _count("controlled_dry_run_receipt_closeout_count")
+ receipt_closeout_field_count = _count(
+ "controlled_dry_run_receipt_closeout_field_count"
+ )
+ receipt_closeout_gate_count = _count(
+ "controlled_dry_run_receipt_closeout_acceptance_gate_count"
+ )
+ rollback_binding_count = _count("rollback_binding_count")
+ verifier_binding_count = _count("post_apply_verifier_binding_count")
+ verifier_required_count = _count("post_apply_verifier_required_count")
+ same_run_truth_count = _count("same_run_truth_required_count")
+ writes_script_count = _count("writes_script_count")
+ writes_artifact_count = _count("writes_artifact_count")
+ reads_secret_count = _count("reads_secret_count")
+ executes_script_count = _count("executes_script_count")
+ executes_migration_count = _count("executes_migration_count")
+ executes_endpoint_count = _count("executes_endpoint_count")
+ executes_sql_count = _count("executes_sql_count")
+ writes_database_count = _count("writes_database_count")
+ signs_database_apply_authorization_count = _count(
+ "signs_database_apply_authorization_count"
+ )
+ primary_human_gate_count = _count(PRIMARY_HUMAN_GATE_COUNT_KEY)
+ manual_review_required_count = _count(LEGACY_REVIEW_REQUIRED_COUNT_KEY)
+ side_effect_count = (
+ writes_script_count
+ + writes_artifact_count
+ + reads_secret_count
+ + executes_script_count
+ + executes_migration_count
+ + executes_endpoint_count
+ + executes_sql_count
+ + writes_database_count
+ + signs_database_apply_authorization_count
+ )
+ risk_detected = any(
+ [
+ side_effect_count,
+ primary_human_gate_count,
+ manual_review_required_count,
+ safety.get("reads_secret_in_preview") is not False,
+ safety.get("writes_file") is not False,
+ safety.get("writes_script_in_preview") is not False,
+ safety.get("writes_artifact_in_preview") is not False,
+ safety.get("executes_script") is not False,
+ safety.get("executes_endpoint") is not False,
+ safety.get("executes_migration") is not False,
+ safety.get("executes_sql") is not False,
+ safety.get("writes_database") is not False,
+ safety.get("signs_database_apply_authorization") is not False,
+ safety.get("performs_detached_signature_verification") is not False,
+ safety.get("persists_verifier_receipt") is not False,
+ safety.get("executes_authorization_evidence") is not False,
+ safety.get("executes_database_apply") is not False,
+ future_plan.get("dry_run_execution_performed") is not False,
+ future_plan.get("runner_execution_authorized") is not False,
+ future_plan.get("dry_run_execution_authorized") is not False,
+ future_plan.get("ready_for_database_apply_now") is not False,
+ future_plan.get("database_apply_authorized") is not False,
+ future_plan.get("issues_database_apply_authorization") is not False,
+ future_plan.get("signs_database_apply_authorization") is not False,
+ future_plan.get("executes_authorization_evidence") is not False,
+ future_plan.get("executes_database_apply") is not False,
+ future_plan.get("executes_endpoint") is not False,
+ future_plan.get("executes_sql") is not False,
+ future_plan.get("writes_database") is not False,
+ future_plan.get("reads_secret_in_preview") is not False,
+ runner.get("runner_execution_authorized") is not False,
+ runner.get("dry_run_execution_authorized") is not False,
+ runner.get("ready_for_database_apply_now") is not False,
+ runner.get("database_apply_authorized") is not False,
+ runner.get("issues_database_apply_authorization") is not False,
+ runner.get("signs_database_apply_authorization") is not False,
+ runner.get("accepts_plaintext_secret") is not False,
+ runner.get("reads_secret_in_preview") is not False,
+ runner.get("signature_material_included") is not False,
+ runner.get("secret_material_included") is not False,
+ runner.get("executes_authorization_evidence") is not False,
+ runner.get("executes_database_apply") is not False,
+ runner.get("executes_endpoint_in_preview") is not False,
+ runner.get("executes_sql_in_preview") is not False,
+ runner.get("writes_database_in_preview") is not False,
+ execution_plan.get("execution_authorized") is not False,
+ execution_plan.get("dry_run_execution_authorized") is not False,
+ execution_plan.get("runner_execution_authorized") is not False,
+ execution_plan.get("shell_execution_included") is not False,
+ execution_plan.get("endpoint_execution_included") is not False,
+ execution_plan.get("sql_execution_included") is not False,
+ execution_plan.get("database_write_included") is not False,
+ execution_plan.get("stdout_capture_allowed") is not False,
+ execution_plan.get("stderr_capture_allowed") is not False,
+ execution_plan.get("database_apply_authorized") is not False,
+ execution_plan.get("ready_for_database_apply_now") is not False,
+ validation.get("execution_performed") is not False,
+ validation.get("stdout_included") is not False,
+ validation.get("stderr_included") is not False,
+ validation.get("database_apply_authorized") is not False,
+ validation.get("executes_shell") is not False,
+ validation.get("executes_endpoint") is not False,
+ validation.get("executes_sql") is not False,
+ validation.get("writes_database") is not False,
+ validation.get("reads_secret") is not False,
+ result_parser.get("execution_required") is not False,
+ result_parser.get("stdout_allowed") is not False,
+ result_parser.get("stderr_allowed") is not False,
+ result_parser.get("database_apply_authorized") is not False,
+ source_closeout_contract.get(
+ "permits_future_database_apply_controlled_dry_run_runner_readiness"
+ )
+ is not True,
+ contract.get("accepts_plaintext_secret") is not False,
+ contract.get("performs_detached_signature_verification") is not False,
+ contract.get("persists_verifier_receipt") is not False,
+ contract.get("executes_authorization_evidence") is not False,
+ contract.get("executes_database_apply") is not False,
+ contract.get("executes_endpoint") is not False,
+ contract.get("executes_sql") is not False,
+ contract.get("issues_database_apply_authorization") is not False,
+ contract.get("database_apply_authorized") is not False,
+ contract.get("ready_for_database_apply_now") is not False,
+ contract.get("signs_database_apply_authorization") is not False,
+ contract.get("writes_database") is not False,
+ contract.get("executes_in_preview") is not False,
+ contract.get("secret_material_required_in_preview") is not False,
+ ]
+ )
+ runner_contract_present = (
+ result == "DB_APPLY_CONTROLLED_DRY_RUN_RUNNER_READINESS_READY"
+ and ready_count == 1
+ and check_count >= 12
+ and pass_count == check_count
+ and waiting_count == 0
+ and receipt_closeout_ready_count == 1
+ and receipt_closeout_check_count == 12
+ and package_ready_count == 1
+ and package_check_count == 12
+ and controlled_apply_ready_count == 1
+ and controlled_apply_check_count == 12
+ and evidence_closeout_ready_count == 1
+ and evidence_closeout_check_count == 12
+ and evidence_preflight_ready_count == 1
+ and evidence_preflight_check_count == 12
+ and final_verifier_gate_count == 1
+ and final_verifier_gate_ready_count == 1
+ and runner_count == 1
+ and runner_field_count == 12
+ and runner_gate_count == 10
+ and execution_plan_count == 1
+ and execution_plan_field_count == 12
+ and parser_count == 1
+ and parser_field_count == 10
+ and validation_report_count == 1
+ and validation_field_count == 8
+ and receipt_closeout_count == 1
+ and receipt_closeout_field_count == 12
+ and receipt_closeout_gate_count == 10
+ and rollback_binding_count == 1
+ and verifier_binding_count == 1
+ and verifier_required_count == 1
+ and same_run_truth_count == 1
+ and future_plan.get(
+ "ready_for_future_database_apply_controlled_dry_run_execution_plan_binding"
+ )
+ is True
+ and future_plan.get(
+ "can_enter_future_database_apply_controlled_dry_run_execution_plan_closeout"
+ )
+ is True
+ and future_plan.get("controlled_dry_run_runner_readiness_ready") is True
+ and future_plan.get("execution_plan_bound") is True
+ and future_plan.get("dry_run_execution_performed") is False
+ and future_plan.get("runner_execution_authorized") is False
+ and future_plan.get("dry_run_execution_authorized") is False
+ and source_future_verification.get(
+ "can_enter_future_database_apply_controlled_dry_run_runner_readiness"
+ )
+ is True
+ and source_future_verification.get("dry_run_execution_performed")
+ is False
+ and runner.get("authorization_material_type")
+ == "controlled_dry_run_runner_readiness"
+ and runner.get(
+ "ready_for_future_database_apply_controlled_dry_run_runner_readiness"
+ )
+ is True
+ and runner.get("target_migration_hash_locked") is True
+ and runner.get("dry_run_only") is True
+ and runner.get("check_mode_only") is True
+ and runner.get("runner_readiness_only") is True
+ and runner.get("execution_plan_preview_only") is True
+ and runner.get("requires_fresh_production_truth_in_same_run") is True
+ and runner.get("requires_post_apply_verifier") is True
+ and execution_plan.get("runner_mode")
+ == "future_controlled_dry_run_runner_readiness_only"
+ and execution_plan.get("plan_status")
+ == "plan_binding_preview_not_executable"
+ and execution_plan.get("dry_run_only") is True
+ and execution_plan.get("check_mode_only") is True
+ and execution_plan.get("execution_authorized") is False
+ and execution_plan.get("dry_run_execution_authorized") is False
+ and execution_plan.get("runner_execution_authorized") is False
+ and execution_plan.get("shell_execution_included") is False
+ and execution_plan.get("endpoint_execution_included") is False
+ and execution_plan.get("sql_execution_included") is False
+ and execution_plan.get("database_write_included") is False
+ and execution_plan.get("stdout_capture_allowed") is False
+ and execution_plan.get("stderr_capture_allowed") is False
+ and execution_plan.get("database_apply_authorized") is False
+ and execution_plan.get("ready_for_database_apply_now") is False
+ and validation.get("receipt_validation_status")
+ == "preview_validated_not_executed"
+ and validation.get("execution_performed") is False
+ and validation.get("stdout_included") is False
+ and validation.get("stderr_included") is False
+ and validation.get("database_apply_authorized") is False
+ and validation.get("executes_shell") is False
+ and validation.get("executes_endpoint") is False
+ and validation.get("executes_sql") is False
+ and validation.get("writes_database") is False
+ and validation.get("reads_secret") is False
+ and result_parser.get("expected_receipt_status")
+ == "preview_only_not_executed"
+ and result_parser.get("execution_required") is False
+ and result_parser.get("parser_verification_status")
+ == "schema_preview_ready"
+ and contract.get("machine_verifiable") is True
+ and contract.get(
+ "permits_future_database_apply_controlled_dry_run_execution_plan_binding"
+ )
+ is True
+ and contract.get("manual_review_mode") == "exception_only"
+ and safety.get("manual_review_mode") == "exception_only"
+ )
+
+ if risk_detected:
+ status = "critical"
+ summary_text = (
+ "PChome auto-policy controlled dry-run runner readiness "
+ "偵測到 secret、runner execution、stdout/stderr、SQL、DB write、"
+ "database apply、簽發授權或人工主 gate 風險"
+ )
+ next_machine_action = (
+ "halt_pchome_auto_policy_controlled_dry_run_runner_readiness_and_inspect_risk"
+ )
+ elif runner_contract_present:
+ status = "ok"
+ summary_text = (
+ "PChome auto-policy controlled dry-run runner readiness "
+ f"已自動完成 {pass_count}/{check_count} checks,execution plan 已綁定且不可執行,runner/write/apply=0"
+ )
+ next_machine_action = (
+ "continue_to_pchome_auto_policy_controlled_dry_run_execution_plan_closeout_lane"
+ )
+ else:
+ status = "warning"
+ summary_text = (
+ "PChome auto-policy controlled dry-run runner readiness "
+ "尚未達自動監控契約"
+ )
+ next_machine_action = (
+ "refresh_pchome_auto_policy_controlled_dry_run_runner_readiness"
+ )
+
+ return _check(
+ "PChome auto-policy controlled dry-run runner readiness",
+ status,
+ summary_text,
+ {
+ "policy": readiness.get("policy"),
+ "result": result,
+ "source_policy": readiness.get("source_policy"),
+ "controlled_dry_run_runner_readiness_ready_count": ready_count,
+ "controlled_dry_run_runner_readiness_check_count": check_count,
+ "controlled_dry_run_runner_readiness_pass_count": pass_count,
+ "controlled_dry_run_runner_readiness_waiting_count": waiting_count,
+ "controlled_dry_run_receipt_closeout_ready_count": (
+ receipt_closeout_ready_count
+ ),
+ "controlled_dry_run_receipt_closeout_check_count": (
+ receipt_closeout_check_count
+ ),
+ "controlled_dry_run_package_ready_count": package_ready_count,
+ "controlled_dry_run_package_check_count": package_check_count,
+ "controlled_apply_final_preflight_ready_count": (
+ controlled_apply_ready_count
+ ),
+ "controlled_apply_final_preflight_check_count": (
+ controlled_apply_check_count
+ ),
+ "authorization_evidence_execution_closeout_ready_count": (
+ evidence_closeout_ready_count
+ ),
+ "authorization_evidence_execution_closeout_check_count": (
+ evidence_closeout_check_count
+ ),
+ "authorization_evidence_execution_preflight_ready_count": (
+ evidence_preflight_ready_count
+ ),
+ "authorization_evidence_execution_preflight_check_count": (
+ evidence_preflight_check_count
+ ),
+ "database_apply_final_verifier_gate_count": final_verifier_gate_count,
+ "database_apply_authorization_final_verifier_gate_ready_count": (
+ final_verifier_gate_ready_count
+ ),
+ "controlled_dry_run_runner_readiness_count": runner_count,
+ "controlled_dry_run_runner_readiness_field_count": runner_field_count,
+ "controlled_dry_run_runner_readiness_acceptance_gate_count": (
+ runner_gate_count
+ ),
+ "execution_plan_binding_count": execution_plan_count,
+ "execution_plan_binding_field_count": execution_plan_field_count,
+ "dry_run_result_parser_count": parser_count,
+ "dry_run_result_parser_field_count": parser_field_count,
+ "receipt_validation_report_count": validation_report_count,
+ "receipt_validation_field_count": validation_field_count,
+ "controlled_dry_run_receipt_closeout_count": receipt_closeout_count,
+ "controlled_dry_run_receipt_closeout_field_count": (
+ receipt_closeout_field_count
+ ),
+ "controlled_dry_run_receipt_closeout_acceptance_gate_count": (
+ receipt_closeout_gate_count
+ ),
+ "rollback_binding_count": rollback_binding_count,
+ "post_apply_verifier_binding_count": verifier_binding_count,
+ "post_apply_verifier_required_count": verifier_required_count,
+ "same_run_truth_required_count": same_run_truth_count,
+ "writes_script_count": writes_script_count,
+ "writes_artifact_count": writes_artifact_count,
+ "reads_secret_count": reads_secret_count,
+ "executes_script_count": executes_script_count,
+ "executes_migration_count": executes_migration_count,
+ "executes_endpoint_count": executes_endpoint_count,
+ "executes_sql_count": executes_sql_count,
+ "writes_database_count": writes_database_count,
+ "signs_database_apply_authorization_count": (
+ signs_database_apply_authorization_count
+ ),
+ "primary_human_gate_count": primary_human_gate_count,
+ "manual_review_required_count": manual_review_required_count,
+ "manual_review_mode": safety.get("manual_review_mode"),
+ "payload_source": "local_contract_fixture",
+ "machine_fetch_evidence_source": "local_schema_fixture",
+ "http_get_adapter": "_pchome_auto_policy_machine_fetch_evidence_get",
+ "execute_fetch": True,
+ "outbound_network": False,
+ "business_data_source": False,
+ "ready_for_future_database_apply_controlled_dry_run_execution_plan_binding": bool(
+ future_plan.get(
+ "ready_for_future_database_apply_controlled_dry_run_execution_plan_binding"
+ )
+ ),
+ "can_enter_future_database_apply_controlled_dry_run_execution_plan_closeout": bool(
+ future_plan.get(
+ "can_enter_future_database_apply_controlled_dry_run_execution_plan_closeout"
+ )
+ ),
+ "controlled_dry_run_runner_readiness_ready": bool(
+ future_plan.get("controlled_dry_run_runner_readiness_ready")
+ ),
+ "execution_plan_bound": bool(future_plan.get("execution_plan_bound")),
+ "source_controlled_dry_run_receipt_closeout_ready": bool(
+ source_future_verification.get(
+ "controlled_dry_run_receipt_closeout_ready"
+ )
+ ),
+ "dry_run_execution_performed": bool(
+ future_plan.get("dry_run_execution_performed")
+ or source_future_verification.get("dry_run_execution_performed")
+ ),
+ "ready_for_future_database_apply_controlled_dry_run_runner_readiness": bool(
+ runner.get(
+ "ready_for_future_database_apply_controlled_dry_run_runner_readiness"
+ )
+ ),
+ "target_migration_hash_locked": bool(
+ runner.get("target_migration_hash_locked")
+ or source_closeout.get("target_migration_hash_locked")
+ ),
+ "dry_run_only": bool(runner.get("dry_run_only")),
+ "check_mode_only": bool(runner.get("check_mode_only")),
+ "runner_readiness_only": bool(runner.get("runner_readiness_only")),
+ "execution_plan_preview_only": bool(
+ runner.get("execution_plan_preview_only")
+ ),
+ "requires_fresh_production_truth": bool(
+ runner.get("requires_fresh_production_truth_in_same_run")
+ ),
+ "requires_post_apply_verifier": bool(
+ runner.get("requires_post_apply_verifier")
+ ),
+ "execution_plan_runner_mode": execution_plan.get("runner_mode"),
+ "execution_plan_status": execution_plan.get("plan_status"),
+ "execution_authorized": bool(
+ execution_plan.get("execution_authorized")
+ ),
+ "dry_run_execution_authorized": bool(
+ execution_plan.get("dry_run_execution_authorized")
+ or future_plan.get("dry_run_execution_authorized")
+ or runner.get("dry_run_execution_authorized")
+ ),
+ "runner_execution_authorized": bool(
+ execution_plan.get("runner_execution_authorized")
+ or future_plan.get("runner_execution_authorized")
+ or runner.get("runner_execution_authorized")
+ ),
+ "shell_execution_included": bool(
+ execution_plan.get("shell_execution_included")
+ ),
+ "endpoint_execution_included": bool(
+ execution_plan.get("endpoint_execution_included")
+ ),
+ "sql_execution_included": bool(
+ execution_plan.get("sql_execution_included")
+ ),
+ "database_write_included": bool(
+ execution_plan.get("database_write_included")
+ ),
+ "stdout_capture_allowed": bool(
+ execution_plan.get("stdout_capture_allowed")
+ ),
+ "stderr_capture_allowed": bool(
+ execution_plan.get("stderr_capture_allowed")
+ ),
+ "receipt_validation_status": validation.get(
+ "receipt_validation_status"
+ ),
+ "validation_execution_performed": bool(
+ validation.get("execution_performed")
+ ),
+ "validation_stdout_included": bool(validation.get("stdout_included")),
+ "validation_stderr_included": bool(validation.get("stderr_included")),
+ "validation_database_apply_authorized": bool(
+ validation.get("database_apply_authorized")
+ ),
+ "validation_executes_shell": bool(validation.get("executes_shell")),
+ "validation_executes_endpoint": bool(
+ validation.get("executes_endpoint")
+ ),
+ "validation_executes_sql": bool(validation.get("executes_sql")),
+ "validation_writes_database": bool(validation.get("writes_database")),
+ "validation_reads_secret": bool(validation.get("reads_secret")),
+ "parser_expected_receipt_status": result_parser.get(
+ "expected_receipt_status"
+ ),
+ "parser_execution_required": bool(
+ result_parser.get("execution_required")
+ ),
+ "parser_stdout_allowed": bool(result_parser.get("stdout_allowed")),
+ "parser_stderr_allowed": bool(result_parser.get("stderr_allowed")),
+ "parser_database_apply_authorized": bool(
+ result_parser.get("database_apply_authorized")
+ ),
+ "parser_verification_status": result_parser.get(
+ "parser_verification_status"
+ ),
+ "permits_future_database_apply_controlled_dry_run_execution_plan_binding": bool(
+ contract.get(
+ "permits_future_database_apply_controlled_dry_run_execution_plan_binding"
+ )
+ ),
+ "accepts_plaintext_secret": bool(
+ runner.get("accepts_plaintext_secret")
+ or contract.get("accepts_plaintext_secret")
+ ),
+ "reads_secret_in_preview": bool(
+ runner.get("reads_secret_in_preview")
+ or safety.get("reads_secret_in_preview")
+ ),
+ "signature_material_included": bool(
+ runner.get("signature_material_included")
+ ),
+ "secret_material_included": bool(
+ runner.get("secret_material_included")
+ ),
+ "secret_material_required_in_preview": bool(
+ contract.get("secret_material_required_in_preview")
+ ),
+ "executes_authorization_evidence": bool(
+ future_plan.get("executes_authorization_evidence")
+ or runner.get("executes_authorization_evidence")
+ or contract.get("executes_authorization_evidence")
+ or safety.get("executes_authorization_evidence")
+ ),
+ "executes_database_apply": bool(
+ future_plan.get("executes_database_apply")
+ or runner.get("executes_database_apply")
+ or contract.get("executes_database_apply")
+ or safety.get("executes_database_apply")
+ ),
+ "executes_endpoint": bool(
+ future_plan.get("executes_endpoint")
+ or contract.get("executes_endpoint")
+ or safety.get("executes_endpoint")
+ ),
+ "executes_sql": bool(
+ future_plan.get("executes_sql")
+ or contract.get("executes_sql")
+ or safety.get("executes_sql")
+ ),
+ "ready_for_database_apply_now": bool(
+ future_plan.get("ready_for_database_apply_now")
+ or runner.get("ready_for_database_apply_now")
+ or execution_plan.get("ready_for_database_apply_now")
+ or contract.get("ready_for_database_apply_now")
+ ),
+ "database_apply_authorized": bool(
+ future_plan.get("database_apply_authorized")
+ or runner.get("database_apply_authorized")
+ or execution_plan.get("database_apply_authorized")
+ or contract.get("database_apply_authorized")
+ ),
+ "issues_database_apply_authorization": bool(
+ future_plan.get("issues_database_apply_authorization")
+ or runner.get("issues_database_apply_authorization")
+ or contract.get("issues_database_apply_authorization")
+ ),
+ "signs_database_apply_authorization": bool(
+ future_plan.get("signs_database_apply_authorization")
+ or runner.get("signs_database_apply_authorization")
+ or contract.get("signs_database_apply_authorization")
+ ),
+ "writes_database": False,
+ "next_machine_action": next_machine_action,
+ },
+ )
+ except Exception as exc:
+ return _check(
+ "PChome auto-policy controlled dry-run runner readiness",
+ "warning",
+ (
+ "PChome auto-policy controlled dry-run runner readiness "
+ f"例行監控暫時無法讀取:{exc}"
+ ),
+ {
+ "writes_database": False,
+ "writes_database_count": 0,
+ "signs_database_apply_authorization_count": 0,
+ "primary_human_gate_count": 0,
+ "execute_fetch": True,
+ "outbound_network": False,
+ "business_data_source": False,
+ "payload_source": "local_contract_fixture",
+ "next_machine_action": (
+ "refresh_pchome_auto_policy_controlled_dry_run_runner_readiness"
+ ),
+ },
+ )
+
+
def _ai_surface_html_readback_check() -> Dict[str, Any]:
"""Read-only AI product surface guardrail readback."""
try:
@@ -12393,6 +13091,7 @@ def collect_ai_automation_smoke(*, record_history: bool = True, history_limit: i
_pchome_auto_policy_controlled_apply_final_preflight_check(),
_pchome_auto_policy_controlled_dry_run_package_check(),
_pchome_auto_policy_controlled_dry_run_receipt_closeout_check(),
+ _pchome_auto_policy_controlled_dry_run_runner_readiness_check(),
_ai_surface_html_readback_check(),
_sitewide_ui_ux_agent_check(),
_sitewide_visual_qa_check(),
diff --git a/tests/test_ai_automation_smoke_service.py b/tests/test_ai_automation_smoke_service.py
index d30bf39..3031bc7 100644
--- a/tests/test_ai_automation_smoke_service.py
+++ b/tests/test_ai_automation_smoke_service.py
@@ -1250,6 +1250,129 @@ def _auto_policy_controlled_dry_run_receipt_closeout_history_check(status="ok"):
}
+def _auto_policy_controlled_dry_run_runner_readiness_history_check(status="ok"):
+ return {
+ "name": "PChome auto-policy controlled dry-run runner readiness",
+ "status": status,
+ "summary": (
+ "PChome auto-policy controlled dry-run runner readiness 已自動完成"
+ ),
+ "details": {
+ "policy": (
+ "read_only_pchome_growth_auto_policy_db_apply_controlled_"
+ "dry_run_runner_readiness"
+ ),
+ "result": "DB_APPLY_CONTROLLED_DRY_RUN_RUNNER_READINESS_READY",
+ "controlled_dry_run_runner_readiness_ready_count": 1,
+ "controlled_dry_run_runner_readiness_check_count": 12,
+ "controlled_dry_run_runner_readiness_pass_count": 12,
+ "controlled_dry_run_runner_readiness_waiting_count": 0,
+ "controlled_dry_run_receipt_closeout_ready_count": 1,
+ "controlled_dry_run_receipt_closeout_check_count": 12,
+ "controlled_dry_run_package_ready_count": 1,
+ "controlled_dry_run_package_check_count": 12,
+ "controlled_apply_final_preflight_ready_count": 1,
+ "controlled_apply_final_preflight_check_count": 12,
+ "authorization_evidence_execution_closeout_ready_count": 1,
+ "authorization_evidence_execution_closeout_check_count": 12,
+ "authorization_evidence_execution_preflight_ready_count": 1,
+ "authorization_evidence_execution_preflight_check_count": 12,
+ "database_apply_final_verifier_gate_count": 1,
+ "database_apply_authorization_final_verifier_gate_ready_count": 1,
+ "controlled_dry_run_runner_readiness_count": 1,
+ "controlled_dry_run_runner_readiness_field_count": 12,
+ "controlled_dry_run_runner_readiness_acceptance_gate_count": 10,
+ "execution_plan_binding_count": 1,
+ "execution_plan_binding_field_count": 12,
+ "dry_run_result_parser_count": 1,
+ "dry_run_result_parser_field_count": 10,
+ "receipt_validation_report_count": 1,
+ "receipt_validation_field_count": 8,
+ "controlled_dry_run_receipt_closeout_count": 1,
+ "controlled_dry_run_receipt_closeout_field_count": 12,
+ "controlled_dry_run_receipt_closeout_acceptance_gate_count": 10,
+ "rollback_binding_count": 1,
+ "post_apply_verifier_binding_count": 1,
+ "post_apply_verifier_required_count": 1,
+ "same_run_truth_required_count": 1,
+ "writes_script_count": 0,
+ "writes_artifact_count": 0,
+ "reads_secret_count": 0,
+ "executes_script_count": 0,
+ "executes_migration_count": 0,
+ "executes_endpoint_count": 0,
+ "executes_sql_count": 0,
+ "writes_database_count": 0,
+ "signs_database_apply_authorization_count": 0,
+ "primary_human_gate_count": 0,
+ "manual_review_required_count": 0,
+ "manual_review_mode": "exception_only",
+ "payload_source": "local_contract_fixture",
+ "execute_fetch": True,
+ "outbound_network": False,
+ "business_data_source": False,
+ "ready_for_future_database_apply_controlled_dry_run_execution_plan_binding": True,
+ "can_enter_future_database_apply_controlled_dry_run_execution_plan_closeout": True,
+ "controlled_dry_run_runner_readiness_ready": True,
+ "execution_plan_bound": True,
+ "source_controlled_dry_run_receipt_closeout_ready": True,
+ "dry_run_execution_performed": False,
+ "ready_for_future_database_apply_controlled_dry_run_runner_readiness": True,
+ "target_migration_hash_locked": True,
+ "dry_run_only": True,
+ "check_mode_only": True,
+ "runner_readiness_only": True,
+ "execution_plan_preview_only": True,
+ "requires_fresh_production_truth": True,
+ "requires_post_apply_verifier": True,
+ "execution_plan_runner_mode": "future_controlled_dry_run_runner_readiness_only",
+ "execution_plan_status": "plan_binding_preview_not_executable",
+ "execution_authorized": False,
+ "dry_run_execution_authorized": False,
+ "runner_execution_authorized": False,
+ "shell_execution_included": False,
+ "endpoint_execution_included": False,
+ "sql_execution_included": False,
+ "database_write_included": False,
+ "stdout_capture_allowed": False,
+ "stderr_capture_allowed": False,
+ "receipt_validation_status": "preview_validated_not_executed",
+ "validation_execution_performed": False,
+ "validation_stdout_included": False,
+ "validation_stderr_included": False,
+ "validation_database_apply_authorized": False,
+ "validation_executes_shell": False,
+ "validation_executes_endpoint": False,
+ "validation_executes_sql": False,
+ "validation_writes_database": False,
+ "validation_reads_secret": False,
+ "parser_expected_receipt_status": "preview_only_not_executed",
+ "parser_execution_required": False,
+ "parser_stdout_allowed": False,
+ "parser_stderr_allowed": False,
+ "parser_database_apply_authorized": False,
+ "parser_verification_status": "schema_preview_ready",
+ "permits_future_database_apply_controlled_dry_run_execution_plan_binding": True,
+ "accepts_plaintext_secret": False,
+ "reads_secret_in_preview": False,
+ "signature_material_included": False,
+ "secret_material_included": False,
+ "secret_material_required_in_preview": False,
+ "executes_authorization_evidence": False,
+ "executes_database_apply": False,
+ "executes_endpoint": False,
+ "executes_sql": False,
+ "ready_for_database_apply_now": False,
+ "database_apply_authorized": False,
+ "issues_database_apply_authorization": False,
+ "signs_database_apply_authorization": False,
+ "next_machine_action": (
+ "continue_to_pchome_auto_policy_controlled_dry_run_execution_plan_closeout_lane"
+ ),
+ },
+ }
+
+
def test_event_router_smoke_reports_queued_deliveries(tmp_path, monkeypatch):
from services import ai_automation_metrics as metrics
from services import ai_automation_smoke_service as smoke
diff --git a/tests/test_auto_import_failure_boundaries.py b/tests/test_auto_import_failure_boundaries.py
index fc7b770..44eec9d 100644
--- a/tests/test_auto_import_failure_boundaries.py
+++ b/tests/test_auto_import_failure_boundaries.py
@@ -43,6 +43,7 @@ def _prepare_daily_sales_tables(import_service):
def test_daily_sales_import_fails_when_monthly_sync_fails(monkeypatch, tmp_path):
+ monkeypatch.setenv("DAILY_SALES_IMPORT_MAX_STALE_DAYS", "30")
import_service = _load_import_service(monkeypatch, f"sqlite:///{tmp_path / 'momo.db'}")
_prepare_daily_sales_tables(import_service)
diff --git a/tests/test_competitor_identity_revalidator.py b/tests/test_competitor_identity_revalidator.py
index ce9234a..0949888 100644
--- a/tests/test_competitor_identity_revalidator.py
+++ b/tests/test_competitor_identity_revalidator.py
@@ -117,7 +117,7 @@ def test_dashboard_match_status_explains_unit_comparable_bundle():
assert "已換算單位價" in status["summary"]
-def test_dashboard_match_status_shows_manual_review_closure_states():
+def test_dashboard_match_status_shows_ai_exception_closure_states():
from routes.dashboard_routes import _build_competitor_decision, _build_pchome_match_status
rejected = _build_pchome_match_status({
@@ -133,15 +133,15 @@ def test_dashboard_match_status_shows_manual_review_closure_states():
"best_match_score": 0.68,
})
- assert rejected["label"] == "人工已否決"
+ assert rejected["label"] == "AI 已排除"
assert "跳過正式價差寫入" in rejected["summary"]
- assert unit_price["label"] == "人工標記單位價"
+ assert unit_price["label"] == "AI 標記單位價"
assert "總價不可直接比較" in unit_price["summary"]
- assert needs_research["label"] == "人工要求補搜尋"
+ assert needs_research["label"] == "AI 要求補搜尋"
assert "重新抓取" in needs_research["summary"]
decision = _build_competitor_decision(980, 899, match_status=rejected)
- assert decision["label"] == "人工已否決"
+ assert decision["label"] == "AI 已排除"
assert decision["gap_amount"] is None
@@ -154,9 +154,9 @@ def test_dashboard_match_status_shows_rescore_accepted_review_state():
"error_message": "matcher_rescore=accepted_current; reasons=strong_exact_spec_match",
})
- assert status["label"] == "重算待人工覆核"
+ assert status["label"] == "重算待 AI 決策"
assert status["tone"] == "watch"
- assert "人工確認身份後才可寫入正式 PChome 價差" in status["summary"]
+ assert "AI 決策信封確認身份" in status["summary"]
def test_dashboard_match_status_uses_specific_matcher_reason_labels():
@@ -175,8 +175,8 @@ def test_dashboard_match_status_uses_specific_matcher_reason_labels():
assert finish_gap["label"] == "妝效質地不同"
assert "妝效質地不同" in finish_gap["summary"]
- assert variant_review["label"] == "多款任選待確認"
- assert "需人工確認" in variant_review["summary"]
+ assert variant_review["label"] == "多款任選待決策"
+ assert "需 AI 例外決策" in variant_review["summary"]
def test_dashboard_match_status_uses_focused_marketplace_reason_labels():
diff --git a/tests/test_competitor_intel_cache.py b/tests/test_competitor_intel_cache.py
index 6f2853d..beabbfb 100644
--- a/tests/test_competitor_intel_cache.py
+++ b/tests/test_competitor_intel_cache.py
@@ -184,12 +184,12 @@ def test_competitor_review_queue_is_canonical_unit_price_handoff():
assert "\"action_label\"" in source
assert "build_unit_price_comparison" in source
assert "需單位價覆核" in daily_template
- assert "人工採用" in daily_template
- assert "人工否決" in daily_template
- assert "人工單位價" in daily_template
+ assert "AI 採用" in daily_template
+ assert "AI 排除" in daily_template
+ assert "AI 單位價" in daily_template
assert "competitor_intel.review_queue" in daily_template
- assert "coverage.fresh_matches" in growth_template
- assert "coverage.fresh_match_rate" in growth_template
+ assert "chart_data.competitor_coverage" in growth_template
+ assert "coverage.decision_support_rate" in growth_template
assert "coverage.decision_ready_rate" in growth_template
assert "coverage.decision_support_rate" in growth_template
assert "coverage.catalog_comparable_count" in growth_template
@@ -199,12 +199,12 @@ def test_competitor_review_queue_is_canonical_unit_price_handoff():
assert "未形成有效身份配對" in growth_template
assert "coverage.unit_comparable_count" in growth_template
assert "coverage.rescore_accepted_count" in growth_template
- assert "重算待人工覆核" in growth_template
+ assert "重算待 AI 決策" in growth_template
assert "coverage.manual_accept_count" in growth_template
assert "coverage.manual_reject_count" in growth_template
assert "coverage.manual_unit_price_count" in growth_template
assert "comp_coverage.rescore_accepted_count" in daily_template
- assert "重算待人工覆核" in daily_template
+ assert "重算待 AI 決策" in daily_template
assert "comp_coverage.decision_support_rate" in daily_template
assert "comp_coverage.catalog_comparable_count" in daily_template
assert "精準可告警覆蓋" in daily_template
@@ -364,7 +364,9 @@ def test_competitor_review_reasons_prefer_json_payload_labels():
assert envelope["identity_evidence"]["brand"]["shared"] == ["mac"]
assert envelope["offer_evidence"]["gap_pct"] == 12.5
assert envelope["difference_highlights"][0]["dimension"] == "妝效/質地不同"
- assert envelope["recommended_action"]["requires_hitl"] is True
+ assert envelope["recommended_action"]["requires_hitl"] is False
+ assert envelope["recommended_action"]["owner"] == "AI Agent"
+ assert envelope["recommended_action"]["ai_exception_mode"] == "machine_verifiable_auto_resolution"
assert envelope["recommended_action"]["action"] == "verify_or_reject_identity"
assert any(evidence["metric"] == "reasons" for evidence in envelope["evidence"])
assert any(evidence["metric"] == "identity_evidence" for evidence in envelope["evidence"])
@@ -502,12 +504,12 @@ def test_review_decision_brief_is_shared_by_openclaw_and_ppt():
brief = summarize_review_decision_envelopes([item], limit=5)
- assert brief["hitl_count"] == 1
+ assert brief["hitl_count"] == 0
assert brief["auto_execute_blocked_count"] == 1
assert brief["severity_counts"]
assert brief["data_quality_counts"] == {"complete": 1}
- assert "人工確認身份後採用同款" in brief["text"]
- assert "HITL" in brief["text"]
+ assert "AI 確認身份後採用同款" in brief["text"]
+ assert "HITL" not in brief["text"]
assert "DDAO4C-A79050612" in brief["text"]
repo_source = (ROOT / "services" / "competitor_intel_repository.py").read_text(encoding="utf-8")
@@ -518,11 +520,11 @@ def test_review_decision_brief_is_shared_by_openclaw_and_ppt():
assert "\"review_decision_brief\": summarize_review_decision_envelopes" in repo_source
assert "summarize_review_decision_envelopes" in openclaw_source
assert "review_decision_text" in openclaw_source
- assert "PChome 覆核決策信封(HITL,不可自動寫正式價差)" in openclaw_source
+ assert "PChome AI 決策信封(no-write receipt 前不寫正式價差)" in openclaw_source
assert "SUM(CASE WHEN attempt_status IN ('unit_comparable'" not in openclaw_source
assert "review_decision_brief" in bot_source
- assert "覆核決策信封(HITL,不可自動寫正式價差)" in bot_source
- assert "覆核決策信封(HITL)" in ppt_source
+ assert "AI 決策信封(no-write receipt 前不寫正式價差)" in bot_source
+ assert "AI 決策信封" in ppt_source
def test_competitor_ppt_prompt_uses_neutral_ewooc_viewpoint():
diff --git a/tests/test_elephant_alpha_engine.py b/tests/test_elephant_alpha_engine.py
index f55327f..4d5bdde 100644
--- a/tests/test_elephant_alpha_engine.py
+++ b/tests/test_elephant_alpha_engine.py
@@ -146,7 +146,7 @@ def test_competitor_db_evidence_actions_are_concrete():
)
assert actions == [
- "[SKU-1] 測試商品|MOMO $1,200 vs PChome $990 (+17.5%)|每件價差 NT$ 210|證據:高信心同款 / 總價可比 / 可直接價格告警 / score 0.84|建議人工確認 PChome identity_v2 後評估跟價或促銷|PChome D123456"
+ "[SKU-1] 測試商品|MOMO $1,200 vs PChome $990 (+17.5%)|每件價差 NT$ 210|證據:高信心同款 / 總價可比 / 可直接價格告警 / score 0.84|建議 AI 自動驗證確認 PChome identity_v2 後評估跟價或促銷|PChome D123456"
]
@@ -279,9 +279,10 @@ def test_price_review_decision_envelope_blocks_auto_execution():
assert envelope["decision_type"] == "price_decision_review"
assert envelope["source_agent"] == "elephant_alpha"
- assert envelope["recommended_action"]["requires_hitl"] is True
+ assert envelope["recommended_action"]["requires_hitl"] is False
+ assert envelope["recommended_action"]["requires_ai_exception"] is True
assert envelope["guardrails"]["can_auto_execute"] is False
- assert envelope["guardrails"]["blocked_reason"] == "price decisions require HITL; execution_plan skipped"
+ assert envelope["guardrails"]["blocked_reason"] == "price decisions require AI 例外決策; execution_plan skipped"
assert envelope["guardrails"]["data_quality"] == "complete"
@@ -701,7 +702,8 @@ def test_human_escalation_decision_envelope_blocks_auto_execution():
assert envelope["decision_type"] == "ea_escalation"
assert envelope["guardrails"]["can_auto_execute"] is False
assert envelope["guardrails"]["data_quality"] == "partial"
- assert envelope["recommended_action"]["requires_hitl"] is True
+ assert envelope["recommended_action"]["requires_hitl"] is False
+ assert envelope["recommended_action"]["requires_ai_exception"] is True
assert envelope["trace"]["provider"] == "elephant_alpha"
diff --git a/tests/test_event_router.py b/tests/test_event_router.py
index e24d4e6..8a43dcf 100644
--- a/tests/test_event_router.py
+++ b/tests/test_event_router.py
@@ -163,11 +163,16 @@ def test_dispatch_decision_envelope_skips_ai_handler(monkeypatch):
"confidence": 0.93,
}
],
- "recommended_action": {"action": "price_follow_review", "owner": "營運", "requires_hitl": True},
+ "recommended_action": {
+ "action": "price_follow_review",
+ "owner": "營運",
+ "requires_hitl": False,
+ "requires_ai_exception": True,
+ },
"guardrails": {
"can_auto_execute": False,
"data_quality": "complete",
- "blocked_reason": "價格調整需人工覆核",
+ "blocked_reason": "價格調整需 AI 例外決策",
},
"expected_impact": {
"gap_amount": 20,
@@ -193,7 +198,8 @@ def test_dispatch_decision_envelope_skips_ai_handler(monkeypatch):
assert "🎯 標的" in message
assert "📊 價格證據" in message
assert "🧩 比對證據" in message
- assert "✅ 人工下一步" in message
+ assert "✅ AI 例外下一步" in message
+ assert "AI 例外:需要" in message
assert "🚦 通知分級" in message
assert "直接價格威脅" in message
assert "高信心同款 / 總價可比 / 可直接價格告警" in message
@@ -231,7 +237,12 @@ def test_price_decision_template_marks_unit_price_review_as_non_direct_alert():
"basis": "same_product_different_pack/unit_price/unit_price_review",
}
],
- "recommended_action": {"action": "unit_price_required", "owner": "營運", "requires_hitl": True},
+ "recommended_action": {
+ "action": "unit_price_required",
+ "owner": "營運",
+ "requires_hitl": False,
+ "requires_ai_exception": True,
+ },
"guardrails": {
"can_auto_execute": False,
"data_quality": "partial",
diff --git a/tests/test_external_market_offer_service.py b/tests/test_external_market_offer_service.py
index 9677e3c..bcf3763 100644
--- a/tests/test_external_market_offer_service.py
+++ b/tests/test_external_market_offer_service.py
@@ -38,6 +38,23 @@ def test_connector_contract_lists_mainstream_sources_without_auto_alerts():
assert sources["tiktok_shop"]["plain_note"].startswith("保留短影音電商")
assert sources["ruten"]["display_name"] == "露天"
assert "price" in payload["manual_csv"]["required_headers"]
+ assert "product_url" in payload["manual_csv"]["optional_headers"]
+ assert "image_url" in payload["manual_csv"]["optional_headers"]
+ assert "stock_status" in payload["manual_csv"]["optional_headers"]
+ evidence_fields = {
+ field["name"]
+ for field in payload["offer_evidence_contract"]["professional_evidence_fields"]
+ }
+ assert {
+ "source_product_id",
+ "title",
+ "product_url",
+ "image_url",
+ "price",
+ "stock_status",
+ "observed_at",
+ "quality_score",
+ }.issubset(evidence_fields)
def test_normalized_offer_payload_validates_plain_required_fields():
@@ -50,6 +67,9 @@ def test_normalized_offer_payload_validates_plain_required_fields():
"price": "899",
"observed_at": "2026-06-15T10:00:00",
"ingestion_method": "manual_csv",
+ "product_url": "https://momo.test/MOMO-1",
+ "image_url": "https://img.test/MOMO-1.jpg",
+ "stock_status": "in_stock",
"quality_score": 82,
"quality_note": "人工確認同款",
})
@@ -57,6 +77,9 @@ def test_normalized_offer_payload_validates_plain_required_fields():
assert errors == []
assert record is not None
assert record.to_record()["source_offer_key"] == "momo_reference:MOMO-1"
+ assert record.to_record()["product_url"] == "https://momo.test/MOMO-1"
+ assert record.to_record()["image_url"] == "https://img.test/MOMO-1.jpg"
+ assert record.to_record()["stock_status"] == "in_stock"
assert record.to_record()["data_quality_status"] == "needs_review"
missing_record, missing_errors = normalize_external_offer_payload({
@@ -91,7 +114,7 @@ def test_external_offer_csv_dry_run_accepts_chinese_headers_and_classifies_rows(
}
rows = payload["rows"]
assert rows[0]["status_label"] == "可使用"
- assert rows[1]["status_label"] == "需人工確認"
+ assert rows[1]["status_label"] == "需 AI 自動驗證確認"
assert "這個來源目前先暫停,不進告警" in rows[1]["reasons"]
assert rows[2]["status_label"] == "不能使用"
assert "缺少售價" in rows[2]["reasons"]
@@ -117,7 +140,7 @@ def test_external_offer_csv_dry_run_accepts_plain_business_headers_and_values():
assert rows[0]["source_code"] == "momo_reference"
assert rows[0]["status_label"] == "可使用"
assert rows[1]["source_code"] == "momo_reference"
- assert rows[1]["status_label"] == "需人工確認"
+ assert rows[1]["status_label"] == "需 AI 自動驗證確認"
assert "尚未確認同款" in rows[1]["reasons"]
assert "缺少 PChome 商品 ID,無法連到業績" in rows[1]["reasons"]
@@ -377,7 +400,7 @@ def test_sync_targeted_momo_candidates_skips_total_price_identity_review(monkeyp
assert payload["success"] is True
assert payload["candidate_count"] == 1
assert payload["written_count"] == 0
- assert payload["skipped_reasons"] == {"候選仍需人工確認": 1}
+ assert payload["skipped_reasons"] == {"候選仍需 AI 自動驗證確認": 1}
assert stale_marks == []
with engine.connect() as conn:
count = conn.execute(text("SELECT COUNT(*) FROM external_offers")).scalar()
@@ -424,7 +447,7 @@ def test_sync_targeted_momo_review_candidates_writes_needs_review_offer(monkeypa
assert payload["status"] == "synced"
assert payload["candidate_count"] == 2
assert payload["written_count"] == 1
- assert payload["skipped_reasons"] == {"人工確認候選分數過低": 1}
+ assert payload["skipped_reasons"] == {"AI 自動驗證確認候選分數過低": 1}
with engine.connect() as conn:
row = conn.execute(text("""
@@ -608,6 +631,13 @@ def test_external_source_readiness_uses_legacy_momo_reference_cache():
assert sources["tiktok_shop"]["plain_state"] == "先保留接口,不進告警"
assert sources["line_shopping"]["plain_state"] == "先保留接口,不進告警"
assert sources["shopify_brand_store"]["plain_state"] == "先保留接口,不進告警"
+ evidence_fields = {
+ field["name"]
+ for field in payload["offer_evidence_contract"]["professional_evidence_fields"]
+ }
+ assert "image_url" in evidence_fields
+ assert "stock_status" in evidence_fields
+ assert payload["offer_evidence_contract"]["quality_gate"]["unverified_data_route"] == "待確認候選,不進告警"
assert payload["plain_summary"] == "MOMO 先用;其他主流平台已列管,未接合法穩定來源前不進告警。"
diff --git a/tests/test_frontend_v2_assets.py b/tests/test_frontend_v2_assets.py
index 8b22400..4aad45d 100644
--- a/tests/test_frontend_v2_assets.py
+++ b/tests/test_frontend_v2_assets.py
@@ -338,12 +338,16 @@ def test_dashboard_v2_is_production_default_and_uses_real_dashboard_data():
assert "def _load_competitor_decision_overview(session, latest_items=None)" in route_source
assert "def _load_pchome_growth_command_center(session)" in route_source
assert "build_pchome_growth_opportunities(engine, limit=16)" in route_source
+ assert "build_pchome_direct_mapping_candidate_decision_package(" in route_source
assert "build_pchome_direct_mapping_retry_candidate_exception_controlled_apply_receipt_replay_package(" in route_source
assert "build_pchome_direct_mapping_retry_candidate_exception_controlled_apply_compact_readback_package(" in route_source
assert "build_pchome_direct_mapping_retry_candidate_exception_controlled_apply_artifact_retention_package(" in route_source
+ assert "build_pchome_growth_ai_automation_readiness(" in route_source
+ assert "'ai_automation_readiness': ai_automation_readiness" in route_source
assert "'controlled_apply_compact_readback': controlled_apply_compact_readback" in route_source
assert "'artifact_retention_policy': artifact_retention_policy" in route_source
assert "'ai_automation_first_viewport': {" in route_source
+ assert "'automation_pipeline': automation_pipeline" in route_source
assert "pchome_growth_command_center=pchome_growth_command_center" in route_source
assert "fetch_competitor_review_queue" in route_source
assert "fetch_competitor_review_queue_page" in route_source
@@ -393,7 +397,7 @@ def test_dashboard_v2_is_production_default_and_uses_real_dashboard_data():
assert "clear_competitor_intel_cache()" in route_source
assert "_extract_match_diagnostic_reasons" in route_source
assert "妝效質地不同" in route_source
- assert "多款任選待確認" in route_source
+ assert "多款任選待決策" in route_source
assert "MockRecord" not in route_source
assert "{% for item in items %}" in dashboard
assert "PChome 業績成長作戰台" in dashboard
@@ -403,10 +407,21 @@ def test_dashboard_v2_is_production_default_and_uses_real_dashboard_data():
assert "高業績商品作戰清單" in dashboard
assert "業績 × MOMO 價格 × 下一步" in dashboard
assert "growth.mapping_rate" in dashboard
+ assert "growth.mapping_backlog" in dashboard
+ assert "商品對應缺口" in dashboard
+ assert "候選待確認" in dashboard
+ assert "自動收據" in dashboard
+ assert "mapping_backlog.auto_receipt_count" in dashboard
+ assert "AI 自動化作戰流水線" in dashboard
+ assert "growth.automation_pipeline" in dashboard
assert "找同款 · 決策 · 證據 · 落地 · 回讀 · 留存" in dashboard
+ assert "AI 主流程" in dashboard
assert "growth.ai_automation_first_viewport" in dashboard
assert "ai_first_viewport.title" in dashboard
assert "ai_first_viewport.metric" in dashboard
+ assert "primary_human_gate_count" in dashboard
+ assert "ai_exception_count" in dashboard
+ assert "主流程人工" not in dashboard
assert "growth.top_opportunities" in dashboard
assert "比價監控總覽" in dashboard
assert "決策支援覆蓋率" in dashboard
@@ -427,6 +442,12 @@ def test_dashboard_v2_is_production_default_and_uses_real_dashboard_data():
assert "身份採用待核" in dashboard
assert "grid-template-columns: repeat(5, minmax(0, 1fr))" in dashboard_css
assert ".growth-command-center" in dashboard_css
+ assert ".growth-mapping-backlog" in dashboard_css
+ assert ".growth-mapping-metric.is-needs" in dashboard_css
+ assert ".growth-mapping-metric.is-auto" in dashboard_css
+ assert ".growth-automation-panel" in dashboard_css
+ assert ".growth-automation-status" in dashboard_css
+ assert ".growth-automation-step.is-warning" in dashboard_css
assert ".growth-strategy-grid" in dashboard_css
assert ".growth-opportunity-table" in dashboard_css
assert "{% if review_total_is_estimated %}約 {% endif %}" in dashboard
@@ -440,14 +461,14 @@ def test_dashboard_v2_is_production_default_and_uses_real_dashboard_data():
assert "'legacy_low_score'" in route_source
assert "dashboard-review-segments" in dashboard
assert "data-pchome-review-action" in dashboard
- assert "採用同款" in dashboard
- assert "否決候選" in dashboard
- assert "標記單位價" in dashboard
- assert "補搜尋" in dashboard
- assert "覆核建議:" in dashboard
+ assert "AI 採用同款" in dashboard
+ assert "AI 排除候選" in dashboard
+ assert "AI 標記單位價" in dashboard
+ assert "AI 補搜尋" in dashboard
+ assert "AI 建議:" in dashboard
assert "review.catalog_review_guidance.action_hint" in dashboard
assert 'data-review-action="needs_research"' in dashboard
- assert "人工閉環" in route_source
+ assert "AI 例外閉環" in route_source
assert "AI 挑品清單" in dashboard
assert "比價覆核隊列" in dashboard
assert "下一步" in dashboard
@@ -534,7 +555,7 @@ def test_pchome_review_export_and_diagnostics_use_real_queue_data():
assert "商品 / MOMO" in dashboard
assert "PChome 待確認商品" in dashboard
assert "開 PChome 待確認商品" in dashboard
- assert "確認採用這筆 PChome 商品為正式同款配對?" in dashboard
+ assert "執行 AI 決策:採用這筆 PChome 商品為正式同款配對?" in dashboard
assert "PChome 候選" not in dashboard
assert "覆核判讀" in dashboard
assert "dashboard-review-workbench-row" in dashboard
@@ -551,7 +572,9 @@ def test_pchome_review_export_and_diagnostics_use_real_queue_data():
assert 'title="決策追蹤"' in dashboard
assert "優先 {{ envelope.severity" in dashboard
assert "證據完整" in dashboard
- assert "需人工" in dashboard
+ assert "AI 待決策" in dashboard
+ assert "需人工" not in dashboard
+ assert "人工覆核" not in dashboard
assert "review.decision_envelope" in dashboard
assert "dashboard-review-envelope" in dashboard
assert "review.diagnostic_reasons" in dashboard
@@ -627,6 +650,41 @@ def test_ai_intelligence_uses_v2_shell_and_real_runtime_apis():
assert "price_basis_label" in template
assert "商品總價" in template
assert "/api/ai/pchome-growth/opportunities" in template
+ assert "@ai_bp.route('/api/ai/pchome-growth/mapping-backlog')" in route_source
+ assert "@ai_bp.route('/api/ai/pchome-growth/mapping-backlog/operator-preview')" in route_source
+ assert "@ai_bp.route('/api/ai/pchome-growth/mapping-backlog/direct-mapping-auto-search-package')" in route_source
+ assert "@ai_bp.route('/api/ai/pchome-growth/mapping-backlog/direct-mapping-candidate-decision-package')" in route_source
+ assert "@ai_bp.route('/api/ai/pchome-growth/ai-automation-readiness')" in route_source
+ assert "@ai_bp.route('/api/ai/pchome-growth/mapping-backlog/evidence-enrichment-preview')" in route_source
+ assert "@ai_bp.route('/api/ai/pchome-growth/mapping-backlog/evidence-source-preview')" in route_source
+ assert "@ai_bp.route('/api/ai/pchome-growth/mapping-backlog/evidence-fetch-gate')" in route_source
+ assert "@ai_bp.route('/api/ai/pchome-growth/mapping-backlog/evidence-merge-preview')" in route_source
+ assert "@ai_bp.route('/api/ai/pchome-growth/mapping-backlog/auto-policy-receipt-gate')" in route_source
+ assert "@ai_bp.route('/api/ai/pchome-growth/mapping-backlog/auto-policy-persistence-gate')" in route_source
+ assert "@ai_bp.route('/api/ai/pchome-growth/mapping-backlog/auto-policy-schema-migration-preview')" in route_source
+ assert "@ai_bp.route('/api/ai/pchome-growth/mapping-backlog/auto-policy-migration-file-preview')" in route_source
+ assert "@ai_bp.route('/api/ai/pchome-growth/mapping-backlog/auto-policy-apply-readiness-closeout')" in route_source
+ assert "@ai_bp.route('/api/ai/pchome-growth/mapping-backlog/auto-policy-migration-file-generation-request')" in route_source
+ assert "@ai_bp.route('/api/ai/pchome-growth/mapping-backlog/auto-policy-migration-apply-gate-preview')" in route_source
+ assert "@ai_bp.route('/api/ai/pchome-growth/mapping-backlog/auto-policy-db-apply-request-gate-preview')" in route_source
+ assert "@ai_bp.route('/api/ai/pchome-growth/mapping-backlog/auto-policy-db-apply-execution-preflight')" in route_source
+ assert "@ai_bp.route('/api/ai/pchome-growth/mapping-backlog/auto-policy-db-apply-authorization-package')" in route_source
+ assert "@ai_bp.route('/api/ai/pchome-growth/mapping-backlog/auto-policy-db-apply-verifier-artifact-preview')" in route_source
+ assert "@ai_bp.route('/api/ai/pchome-growth/mapping-backlog/auto-policy-db-apply-final-handoff-package')" in route_source
+ assert "@ai_bp.route('/api/ai/pchome-growth/mapping-backlog/auto-policy-db-apply-controlled-dry-run-shell-preview')" in route_source
+ assert "@ai_bp.route('/api/ai/pchome-growth/mapping-backlog/auto-policy-db-apply-controlled-dry-run-shell-closeout')" in route_source
+ assert "@ai_bp.route('/api/ai/pchome-growth/mapping-backlog/auto-policy-db-apply-authorization-request-intake')" in route_source
+ assert "@ai_bp.route('/api/ai/pchome-growth/mapping-backlog/auto-policy-db-apply-authorization-request-closeout')" in route_source
+ assert "@ai_bp.route('/api/ai/pchome-growth/mapping-backlog/auto-policy-db-apply-authorization-lane-guard')" in route_source
+ assert "@ai_bp.route('/api/ai/pchome-growth/mapping-backlog/auto-policy-db-apply-authorization-decision-preflight')" in route_source
+ assert "@ai_bp.route('/api/ai/pchome-growth/mapping-backlog/auto-policy-db-apply-authorization-decision-closeout')" in route_source
+ assert "@ai_bp.route('/api/ai/pchome-growth/mapping-backlog/auto-policy-db-apply-authorization-issuer-gate')" in route_source
+ assert "@ai_bp.route('/api/ai/pchome-growth/mapping-backlog/auto-policy-db-apply-authorization-signing-decision-preflight')" in route_source
+ assert "@ai_bp.route('/api/ai/pchome-growth/mapping-backlog/auto-policy-db-apply-authorization-signing-decision-closeout')" in route_source
+ assert "@ai_bp.route('/api/ai/pchome-growth/mapping-backlog/auto-policy-db-apply-authorization-signing-issuer-guard')" in route_source
+ assert "summarize_pchome_mapping_backlog(payload)" in route_source
+ assert "build_pchome_mapping_operator_preview(payload, batch_size=batch_size)" in route_source
+ assert "build_pchome_evidence_enrichment_preview(payload, batch_size=batch_size)" in route_source
assert "最近處理紀錄" in template
assert "處理紀錄" in template
assert "作戰建議紀錄" not in template
diff --git a/tests/test_market_intel_skeleton.py b/tests/test_market_intel_skeleton.py
index d2b9db9..c019fc9 100644
--- a/tests/test_market_intel_skeleton.py
+++ b/tests/test_market_intel_skeleton.py
@@ -882,7 +882,7 @@ def test_market_intel_preview_template_uses_safe_fetch_false_endpoint():
assert "市場情報尚未進入正式決策" in template
assert "資料狀態" in template
assert "來源規格" in template
- assert "手動整理" in template
+ assert "AI 受控整理" in template
assert "data-market-intel-preview" not in template
assert "data-market-intel-writer" not in template
assert "/api/market_intel/" not in template
diff --git a/tests/test_migration_metadata_coverage.py b/tests/test_migration_metadata_coverage.py
index c9e2a95..64cbb6e 100644
--- a/tests/test_migration_metadata_coverage.py
+++ b/tests/test_migration_metadata_coverage.py
@@ -74,6 +74,21 @@ def test_ai_calls_provider_check_accepts_ollama_other_telemetry_bucket():
assert "NOT VALID" in migration
+def test_pchome_auto_policy_receipt_migration_is_additive_only():
+ path = ROOT / "migrations" / "045_pchome_auto_policy_evidence_receipts.sql"
+
+ assert path.exists()
+ migration = path.read_text(encoding="utf-8")
+ assert "Migration 045: PChome auto-policy evidence receipts" in migration
+ assert "CREATE TABLE IF NOT EXISTS external_offer_evidence_receipts" in migration
+ assert "evidence_delta_json JSONB NOT NULL DEFAULT '{}'::jsonb" in migration
+ assert "GRANT ALL PRIVILEGES ON external_offer_evidence_receipts TO momo" in migration
+ assert "DROP " not in migration.upper()
+ assert "TRUNCATE " not in migration.upper()
+ assert "DELETE " not in migration.upper()
+ assert "ALTER TABLE EXTERNAL_OFFERS" not in migration.upper()
+
+
def test_rag_embedding_signature_migration_covers_query_and_learning_tables():
migration = (ROOT / "migrations" / "034_add_embedding_signature_to_rag_tables.sql").read_text(encoding="utf-8")
diff --git a/tests/test_momo_crawler_targeted_search.py b/tests/test_momo_crawler_targeted_search.py
index 5056095..5425086 100644
--- a/tests/test_momo_crawler_targeted_search.py
+++ b/tests/test_momo_crawler_targeted_search.py
@@ -178,7 +178,7 @@ def test_targeted_momo_search_keeps_identity_review_candidates_manual(monkeypatc
)
assert success is True
- assert "需人工確認 1 筆" in message
+ assert "需 AI 自動驗證確認 1 筆" in message
assert products[0]["auto_compare_type"] == "manual_review"
assert products[0]["can_auto_compare"] is False
assert products[0]["target_price_basis"] == "none"
diff --git a/tests/test_nemotron_decision_envelope.py b/tests/test_nemotron_decision_envelope.py
index 90f59a8..f4103de 100644
--- a/tests/test_nemotron_decision_envelope.py
+++ b/tests/test_nemotron_decision_envelope.py
@@ -37,7 +37,8 @@ def test_price_decision_envelope_has_hilt_guardrails():
assert envelope["guardrails"]["match_type"] == "exact"
assert envelope["guardrails"]["price_basis"] == "total_price"
assert envelope["guardrails"]["alert_tier"] == "price_alert_exact"
- assert envelope["recommended_action"]["requires_hitl"] is True
+ assert envelope["recommended_action"]["requires_hitl"] is False
+ assert envelope["recommended_action"]["requires_ai_exception"] is True
assert envelope["expected_impact"]["revenue_loss_7d"] == 65000
assert envelope["expected_impact"]["gap_amount"] == 201
assert envelope["expected_impact"]["momo_price"] == 1200
diff --git a/tests/test_openclaw_daily_template.py b/tests/test_openclaw_daily_template.py
index 8e2122b..bdad619 100644
--- a/tests/test_openclaw_daily_template.py
+++ b/tests/test_openclaw_daily_template.py
@@ -158,7 +158,7 @@ class TestKPIComputation:
assert "fetch_competitor_coverage" in source
assert "summarize_review_decision_envelopes" in source
assert "review_decision_text" in source
- assert "重算待人工覆核" in source
+ assert "重算待 AI 決策" in source
# ═══════════════════════════════════════════════════════════════════════════
diff --git a/tests/test_pchome_revenue_growth_service.py b/tests/test_pchome_revenue_growth_service.py
index 59ad78b..67fcc57 100644
--- a/tests/test_pchome_revenue_growth_service.py
+++ b/tests/test_pchome_revenue_growth_service.py
@@ -185,7 +185,26 @@ def test_pchome_growth_opportunities_use_plain_language_and_pause_shopee_coupang
assert payload["success"] is True
assert payload["system_name"] == "PChome 業績成長自動化作戰系統"
assert payload["source_scope"]["active_external_sources"] == ["MOMO 外部價格參考"]
- assert payload["source_scope"]["paused_external_sources"] == ["蝦皮", "酷澎"]
+ assert {
+ "蝦皮",
+ "Lazada",
+ "Amazon",
+ "Google Merchant / Shopping",
+ "TikTok Shop",
+ "LINE 購物",
+ "Rakuten",
+ "Yahoo 購物",
+ "露天",
+ "品牌官網 / Shopify",
+ "Meta Commerce",
+ "酷澎",
+ }.issubset(set(payload["source_scope"]["paused_external_sources"]))
+ assert payload["source_scope"]["plain_note"] == "MOMO 先用;其他主流平台待接入,不進作戰清單,也不發告警。"
+ evidence_fields = {
+ field["name"]
+ for field in payload["source_scope"]["offer_evidence_contract"]["professional_evidence_fields"]
+ }
+ assert {"source_product_id", "product_url", "image_url", "stock_status", "quality_score"}.issubset(evidence_fields)
readiness = payload["source_scope"]["source_readiness"]
sources = {source["code"]: source for source in readiness["sources"]}
assert sources["momo_reference"]["status_label"] == "正在使用"
@@ -542,10 +561,12 @@ def test_growth_analysis_uses_actionable_price_command_panel():
assert "ga-chart-card__body--command" in template
assert "ga-competitor-command" in template
assert "可直接決策" in template
- assert "待補 / 待確認" in template
+ assert "待補 / AI 例外" in template
+ assert "ga-competitor-evidence" in template
assert "下一步" in template
assert "前往今日作戰" in template
assert "ga-competitor-signal" in css
+ assert "ga-competitor-evidence" in css
assert "ga-competitor-next" in css
assert ".growth-analysis-page .ga-chart-card__body--command" in css
assert "比價資料品質" not in template
diff --git a/tests/test_telegram_triaged_alert_format.py b/tests/test_telegram_triaged_alert_format.py
index 73a50cc..4486cc0 100644
--- a/tests/test_telegram_triaged_alert_format.py
+++ b/tests/test_telegram_triaged_alert_format.py
@@ -77,10 +77,10 @@ def test_ea_escalation_uses_structured_incident_brief():
base_event={
"event_type": "ea_escalation",
"title": "🐘 EA 升級審核 · 價格下滑警報",
- "summary": "自主決策信心度 0.82 低於門檻,需人工批准",
+ "summary": "自主決策信心度 0.82 低於門檻,需 AI 例外決策",
"id": "ea_review_test",
},
- tier_label="🐘 Elephant Alpha · L3 HITL",
+ tier_label="🐘 Elephant Alpha · L3 AI 例外決策",
ai_summary=(
"分析顯示 5 個代表性 SKU 的價格差異分別為 16.7%~38.3%,"
"且每件價差至多 370 元。"
@@ -91,13 +91,13 @@ def test_ea_escalation_uses_structured_incident_brief():
"MOMO $300 vs PChome $250 (+16.7%)|"
"每件價差 NT$ 50|"
"證據:高信心同款 / 總價可比 / 可直接價格告警 / score 0.86|"
- "建議人工確認 PChome identity_v2 後評估跟價或促銷|"
+ "建議 AI 自動驗證確認 PChome identity_v2 後評估跟價或促銷|"
"PChome DABC53-A9009OEF",
"[3518670] L'Occitane 歐舒丹 官方直營 乳油木|"
"MOMO $1,220 vs PChome $850 (+30.3%)|"
"每件價差 NT$ 370|"
"證據:高信心同款 / 總價可比 / 可直接價格告警 / score 0.91|"
- "建議人工確認 PChome identity_v2 後評估跟價或促銷|"
+ "建議 AI 自動驗證確認 PChome identity_v2 後評估跟價或促銷|"
"PChome DDADKS-A900HIG5Y",
],
)
@@ -125,7 +125,7 @@ def test_ea_escalation_generic_actions_do_not_render_as_sku_cards():
"summary": "低信心且缺少可格式化的具體行動",
"id": "ea_review_generic",
},
- tier_label="🐘 Elephant Alpha · L3 HITL",
+ tier_label="🐘 Elephant Alpha · L3 AI 例外決策",
ai_summary="已隱藏 LLM plan 文字,避免把推測當成事實。",
ai_cause="觸發類型:程式碼異常偵測 | 信心度:0.62 | 缺少可直接審核的實證資料",
ai_actions=[
@@ -145,7 +145,7 @@ def test_ea_escalation_renders_decision_envelope_and_uses_decision_id_callback()
base_event={
"event_type": "ea_escalation",
"title": "🐘 EA 升級審核 · 資源壓力治理",
- "summary": "自主決策信心度 0.82 低於門檻,需人工批准",
+ "summary": "自主決策信心度 0.82 低於門檻,需 AI 例外決策",
"decision_envelope": {
"decision_id": "ea_resource_pressure_1942",
"decision_type": "resource_optimization",
@@ -166,18 +166,19 @@ def test_ea_escalation_renders_decision_envelope_and_uses_decision_id_callback()
},
],
"recommended_action": {
- "action": "human_review_backlog_triage",
+ "action": "ai_exception_backlog_triage",
"owner": "ops",
- "requires_hitl": True,
+ "requires_hitl": False,
+ "requires_ai_exception": True,
},
"guardrails": {
"can_auto_execute": False,
- "blocked_reason": "resource_optimization requires HITL",
+ "blocked_reason": "resource_optimization requires AI exception decision",
"data_quality": "complete",
},
},
},
- tier_label="🐘 Elephant Alpha · L3 HITL",
+ tier_label="🐘 Elephant Alpha · L3 AI 例外決策",
ai_summary="只採用 action_plans 與 CPU 實測值。",
ai_cause="觸發類型:資源壓力治理 | 信心度:0.86",
ai_actions=["檢查 priority <= 2 的 action_plans"],
@@ -187,7 +188,7 @@ def test_ea_escalation_renders_decision_envelope_and_uses_decision_id_callback()
assert "類型:resource_optimization" in msg
assert "資料品質:complete 自動執行:不允許" in msg
assert "SKU:action_plans" in msg
- assert "human_review_backlog_triage" in msg
+ assert "ai_exception_backlog_triage" in msg
assert keyboard["inline_keyboard"][0][0]["callback_data"] == "momo:eig:ea_resource_pressure_1942"
@@ -225,10 +226,11 @@ def test_triaged_alert_renders_decision_envelope_contract():
},
],
"recommended_action": {
- "action": "human_review",
+ "action": "ai_exception_decision",
"owner": "ops",
"deadline": "2026-05-24T18:00:00+08:00",
- "requires_hitl": True,
+ "requires_hitl": False,
+ "requires_ai_exception": True,
},
"expected_impact": {
"revenue_loss_7d": 42000,
@@ -237,7 +239,7 @@ def test_triaged_alert_renders_decision_envelope_contract():
},
"guardrails": {
"can_auto_execute": False,
- "blocked_reason": "price adjustment requires HITL",
+ "blocked_reason": "price adjustment requires AI exception decision",
"data_quality": "complete",
},
"trace": {
@@ -249,7 +251,7 @@ def test_triaged_alert_renders_decision_envelope_contract():
},
},
tier_label="Hermes · P1",
- ai_summary="建議進人工價格審核。",
+ ai_summary="建議進 AI 例外價格決策。",
)
assert "🧭 決策信封" in msg
@@ -258,7 +260,7 @@ def test_triaged_alert_renders_decision_envelope_contract():
assert "信心度:86%" in msg
assert "資料品質:complete" in msg
assert "自動執行:不允許" in msg
- assert "邊界:price adjustment requires HITL" in msg
+ assert "邊界:price adjustment requires AI exception decision" in msg
assert "標的" in msg
assert "PChome" in msg
assert "📊 價格證據" in msg
@@ -266,8 +268,8 @@ def test_triaged_alert_renders_decision_envelope_contract():
assert "🧩 比對證據" in msg
assert "Match:0.91" in msg
assert "identity_v2 + price_alert_exact" in msg
- assert "✅ 人工下一步" in msg
- assert "動作:human_review" in msg
+ assert "✅ AI 例外下一步" in msg
+ assert "動作:ai_exception_decision" in msg
assert "ai_call_id=123" in msg
assert keyboard["inline_keyboard"][0][0]["callback_data"] == "momo:eig:decision_env_001"
diff --git a/tests/test_webcrumbs_host_data_service.py b/tests/test_webcrumbs_host_data_service.py
index ff29f57..8640cd3 100644
--- a/tests/test_webcrumbs_host_data_service.py
+++ b/tests/test_webcrumbs_host_data_service.py
@@ -10,7 +10,7 @@ def test_webcrumbs_host_data_maps_price_alert_exact_rows(monkeypatch):
lambda passed_engine, limit: [
{
"sku": "SKU-REVIEW",
- "name": "需人工覆核的非直接告警候選",
+ "name": "需 AI 例外決策的非直接告警候選",
"momo_price": 999,
"pchome_price": 500,
"gap_pct": 99.8,
@@ -70,20 +70,21 @@ def test_webcrumbs_host_data_maps_price_alert_exact_rows(monkeypatch):
lambda passed_engine, limit: [
{
"sku": "SKU-REVIEW",
- "name": "人工覆核精華液",
+ "name": "AI 例外決策精華液",
"decision_envelope": {
"decision_id": "review_queue:SKU-REVIEW",
"severity": "P2",
"subject": {
"sku": "SKU-REVIEW",
- "name": "人工覆核精華液",
+ "name": "AI 例外決策精華液",
"momo_price": 399,
"competitor_price": 329,
"competitor_product_id": "PCH-REVIEW",
},
"recommended_action": {
"action": "review_accept_identity",
- "requires_hitl": True,
+ "requires_hitl": False,
+ "owner": "AI Agent",
},
"expected_impact": {"candidate_gap_pct": 21.3},
"guardrails": {
@@ -136,7 +137,9 @@ def test_webcrumbs_host_data_maps_price_alert_exact_rows(monkeypatch):
assert payload["metadata"]["stale_match_count"] == 18
assert payload["metadata"]["pending_match_count"] == 612
assert payload["metadata"]["review_queue_count"] == 1
- assert payload["metadata"]["hitl_count"] == 1
+ assert payload["metadata"]["hitl_count"] == 0
+ assert payload["metadata"]["primary_human_gate_count"] == 0
+ assert payload["metadata"]["ai_exception_count"] == 1
assert payload["metadata"]["auto_execute_blocked_count"] == 1
assert payload["metadata"]["decision_envelope_source"] == "competitor_intel_repository"
assert payload["reviewDecisionBrief"]["items"][0]["sku"] == "SKU-REVIEW"
@@ -188,8 +191,8 @@ def test_webcrumbs_host_data_uses_empty_state_without_risks(monkeypatch):
assert payload["metadata"]["stale_match_count"] == 2
assert payload["metadata"]["pending_match_count"] == 9
assert payload["metadata"]["review_queue_count"] == 0
- assert payload["reviewDecisionBrief"]["text"] == "(目前沒有待覆核決策信封)"
- assert "非同款、單位價或變體候選" in payload["aiCandidate"]["thesis"]
+ assert payload["reviewDecisionBrief"]["text"] == "(目前沒有待 AI 決策信封)"
+ assert "非同款、單位價或變體候選會進入 AI 例外決策隊列" in payload["aiCandidate"]["thesis"]
def test_webcrumbs_seed_data_fallback_does_not_expose_demo_values(monkeypatch):
diff --git a/web/static/css/page-dashboard-v2.css b/web/static/css/page-dashboard-v2.css
index a9aef71..cdb655c 100644
--- a/web/static/css/page-dashboard-v2.css
+++ b/web/static/css/page-dashboard-v2.css
@@ -84,6 +84,7 @@
.growth-command-kpi,
.growth-priority-panel,
.growth-strategy-panel,
+ .growth-automation-panel,
.growth-opportunity-panel {
min-width: 0;
background: var(--momo-bg-surface);
@@ -170,12 +171,193 @@
min-width: 0;
}
+ .growth-mapping-backlog {
+ display: grid;
+ grid-template-columns: minmax(180px, 0.62fr) minmax(0, 1fr);
+ gap: 10px;
+ min-width: 0;
+ padding: 12px;
+ background: var(--momo-bg-surface);
+ border: 1px solid var(--momo-border-light);
+ border-radius: 8px;
+ box-shadow: var(--momo-shadow-soft);
+ }
+
+ .growth-mapping-backlog-head {
+ display: grid;
+ align-content: center;
+ gap: 6px;
+ min-width: 0;
+ padding-right: 10px;
+ border-right: 1px solid var(--momo-border-light);
+ }
+
+ .growth-mapping-backlog-head strong {
+ color: var(--momo-text-primary);
+ font-size: 2rem;
+ font-weight: 900;
+ line-height: 1;
+ }
+
+ .growth-mapping-backlog-head em,
+ .growth-mapping-metric em {
+ color: var(--momo-text-secondary);
+ font-size: 11px;
+ font-style: normal;
+ font-weight: 800;
+ line-height: 1.35;
+ }
+
+ .growth-mapping-backlog-grid {
+ display: grid;
+ grid-template-columns: repeat(4, minmax(0, 1fr));
+ gap: 8px;
+ min-width: 0;
+ }
+
+ .growth-mapping-metric {
+ display: grid;
+ align-content: center;
+ gap: 5px;
+ min-width: 0;
+ min-height: 82px;
+ padding: 10px 12px;
+ background: color-mix(in srgb, var(--momo-bg-paper) 78%, transparent);
+ border: 1px solid var(--momo-border-light);
+ border-radius: 8px;
+ }
+
+ .growth-mapping-metric span {
+ color: var(--momo-text-secondary);
+ font-size: 12px;
+ font-weight: 900;
+ }
+
+ .growth-mapping-metric strong {
+ color: var(--momo-text-primary);
+ font-size: 1.7rem;
+ font-weight: 900;
+ line-height: 1;
+ }
+
+ .growth-mapping-metric.is-needs {
+ border-color: rgba(188, 75, 49, 0.28);
+ background: rgba(255, 244, 239, 0.72);
+ }
+
+ .growth-mapping-metric.is-review {
+ border-color: rgba(210, 158, 58, 0.34);
+ background: rgba(255, 248, 231, 0.72);
+ }
+
+ .growth-mapping-metric.is-ready {
+ border-color: rgba(48, 133, 94, 0.24);
+ background: rgba(235, 248, 241, 0.72);
+ }
+
+ .growth-mapping-metric.is-auto {
+ border-color: rgba(62, 112, 174, 0.26);
+ background: rgba(236, 244, 255, 0.78);
+ }
+
.growth-priority-panel,
.growth-strategy-panel,
+ .growth-automation-panel,
.growth-opportunity-panel {
padding: 14px;
}
+ .growth-automation-steps {
+ display: grid;
+ grid-template-columns: repeat(4, minmax(0, 1fr));
+ gap: 8px;
+ min-width: 0;
+ }
+
+ .growth-automation-status {
+ display: grid;
+ grid-template-columns: auto minmax(0, 1fr) auto;
+ gap: 10px;
+ align-items: center;
+ min-width: 0;
+ margin-bottom: 8px;
+ padding: 10px 12px;
+ background: rgba(235, 248, 241, 0.72);
+ border: 1px solid rgba(48, 133, 94, 0.24);
+ border-radius: 8px;
+ }
+
+ .growth-automation-status span {
+ color: var(--momo-text-tertiary);
+ font-size: 10px;
+ font-weight: 900;
+ letter-spacing: 0.06em;
+ }
+
+ .growth-automation-status strong {
+ overflow-wrap: anywhere;
+ color: var(--momo-success);
+ font-size: 14px;
+ font-weight: 900;
+ }
+
+ .growth-automation-status em {
+ color: var(--momo-text-secondary);
+ font-size: 11px;
+ font-style: normal;
+ font-weight: 800;
+ text-align: right;
+ }
+
+ .growth-automation-step {
+ display: grid;
+ align-content: start;
+ gap: 6px;
+ min-width: 0;
+ min-height: 96px;
+ padding: 12px;
+ background: color-mix(in srgb, var(--momo-bg-paper) 78%, transparent);
+ border: 1px solid var(--momo-border-light);
+ border-radius: 8px;
+ }
+
+ .growth-automation-step span {
+ color: var(--momo-text-secondary);
+ font-size: 12px;
+ font-weight: 900;
+ }
+
+ .growth-automation-step strong {
+ overflow-wrap: anywhere;
+ color: var(--momo-text-primary);
+ font-size: 1.55rem;
+ font-weight: 900;
+ line-height: 1;
+ }
+
+ .growth-automation-step em {
+ color: var(--momo-text-secondary);
+ font-size: 11px;
+ font-style: normal;
+ font-weight: 800;
+ line-height: 1.35;
+ }
+
+ .growth-automation-step.is-danger {
+ border-color: rgba(188, 75, 49, 0.28);
+ background: rgba(255, 244, 239, 0.72);
+ }
+
+ .growth-automation-step.is-warning {
+ border-color: rgba(210, 158, 58, 0.34);
+ background: rgba(255, 248, 231, 0.72);
+ }
+
+ .growth-automation-step.is-success {
+ border-color: rgba(48, 133, 94, 0.24);
+ background: rgba(235, 248, 241, 0.72);
+ }
+
.growth-panel-head {
display: flex;
justify-content: space-between;
@@ -2114,10 +2296,25 @@
grid-template-columns: 1fr;
}
+ .growth-mapping-backlog {
+ grid-template-columns: 1fr;
+ }
+
+ .growth-mapping-backlog-head {
+ padding-right: 0;
+ padding-bottom: 10px;
+ border-right: 0;
+ border-bottom: 1px solid var(--momo-border-light);
+ }
+
.growth-strategy-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
+ .growth-automation-steps {
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+ }
+
.dashboard-kpi-grid {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
@@ -2150,6 +2347,8 @@
@media (max-width: 640px) {
.growth-command-kpis,
+ .growth-mapping-backlog-grid,
+ .growth-automation-steps,
.growth-strategy-grid,
.dashboard-kpi-grid,
.dashboard-ai-summary-grid {
@@ -2159,6 +2358,7 @@
.growth-command-head,
.growth-priority-panel,
.growth-strategy-panel,
+ .growth-automation-panel,
.growth-opportunity-panel {
padding: 12px;
}
@@ -2181,6 +2381,14 @@
white-space: normal;
}
+ .growth-automation-status {
+ grid-template-columns: 1fr;
+ }
+
+ .growth-automation-status em {
+ text-align: left;
+ }
+
.dashboard-focus-grid {
grid-template-columns: 1fr;
}
diff --git a/web/static/css/page-growth-bem.css b/web/static/css/page-growth-bem.css
index 5b6168c..1e3fa9e 100644
--- a/web/static/css/page-growth-bem.css
+++ b/web/static/css/page-growth-bem.css
@@ -420,6 +420,24 @@
overflow-wrap: anywhere;
}
+.growth-analysis-page .ga-competitor-evidence {
+ grid-column: 1 / -1;
+ display: flex;
+ flex-wrap: wrap;
+ gap: 6px;
+ min-width: 0;
+}
+
+.growth-analysis-page .ga-competitor-evidence span {
+ border: 1px solid var(--momo-border-muted, rgba(31, 41, 55, 0.12));
+ border-radius: 999px;
+ padding: 4px 8px;
+ background: var(--momo-surface-soft, rgba(255, 255, 255, 0.72));
+ color: var(--momo-text-secondary);
+ font-size: var(--momo-text-caption, 12px);
+ font-weight: 800;
+}
+
.growth-analysis-page .ga-competitor-next {
grid-column: 1 / -1;
min-width: 0;