From 079832f2a46e14ed87d57bb8807c25d8b722c475 Mon Sep 17 00:00:00 2001 From: ogt Date: Thu, 9 Jul 2026 21:55:45 +0800 Subject: [PATCH] chore(dev): sync ewoooc development workbench state --- .env.example | 37 + docs/memory/README.md | 2 + .../code_modularization_inventory_20260430.md | 74 +- .../memory/pchome_mapping_backlog_20260628.md | 117 +++ .../production_version_truth_20260627.md | 29 + scripts/ops/report_pchome_mapping_backlog.py | 153 ++++ services/ai_automation_smoke_service.py | 699 ++++++++++++++++++ tests/test_ai_automation_smoke_service.py | 123 +++ tests/test_auto_import_failure_boundaries.py | 1 + tests/test_competitor_identity_revalidator.py | 18 +- tests/test_competitor_intel_cache.py | 30 +- tests/test_elephant_alpha_engine.py | 10 +- tests/test_event_router.py | 19 +- tests/test_external_market_offer_service.py | 38 +- tests/test_frontend_v2_assets.py | 76 +- tests/test_market_intel_skeleton.py | 2 +- tests/test_migration_metadata_coverage.py | 15 + tests/test_momo_crawler_targeted_search.py | 2 +- tests/test_nemotron_decision_envelope.py | 3 +- tests/test_openclaw_daily_template.py | 2 +- tests/test_pchome_revenue_growth_service.py | 25 +- tests/test_telegram_triaged_alert_format.py | 38 +- tests/test_webcrumbs_host_data_service.py | 17 +- web/static/css/page-dashboard-v2.css | 208 ++++++ web/static/css/page-growth-bem.css | 18 + 25 files changed, 1649 insertions(+), 107 deletions(-) create mode 100644 docs/memory/pchome_mapping_backlog_20260628.md create mode 100644 docs/memory/production_version_truth_20260627.md create mode 100755 scripts/ops/report_pchome_mapping_backlog.py 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;