diff --git a/app.py b/app.py index 48a9af3..abb0234 100644 --- a/app.py +++ b/app.py @@ -409,6 +409,7 @@ EXPECTED_METADATA_TABLES = { 'market_platforms', 'market_campaigns', 'market_campaign_snapshots', 'market_campaign_products', 'market_product_price_history', 'market_product_matches', 'market_crawler_runs', + 'market_alert_review_queue', } diff --git a/config.py b/config.py index 3e6a0f7..c44a906 100644 --- a/config.py +++ b/config.py @@ -320,7 +320,7 @@ YOUTUBE_API_KEY = os.getenv('YOUTUBE_API_KEY', '') # ========================================== # 系統版本與路徑 # ========================================== -SYSTEM_VERSION = "V10.201" +SYSTEM_VERSION = "V10.202" LOG_FILE_PATH = os.path.join(BASE_DIR, 'logs/system.log') public_url = PUBLIC_URL # 用於模板顯示 diff --git a/database/manager.py b/database/manager.py index 51dce5b..694dc4c 100644 --- a/database/manager.py +++ b/database/manager.py @@ -46,6 +46,7 @@ from .market_intel_models import ( # noqa: F401 - ADR-035 market_* 表 MarketProductPriceHistory, MarketProductMatch, MarketCrawlerRun, + MarketAlertReviewQueue, ) # 🚩 導入優化後的日誌管理模組 diff --git a/database/market_intel_models.py b/database/market_intel_models.py index d13ad9d..2dd56f5 100644 --- a/database/market_intel_models.py +++ b/database/market_intel_models.py @@ -222,3 +222,46 @@ class MarketCrawlerRun(Base): Index("idx_market_crawler_run_platform_time", "platform_code", "started_at"), Index("idx_market_crawler_run_status_time", "status", "started_at"), ) + + +class MarketAlertReviewQueue(Base): + """市場機會與威脅告警的人工審核佇列。""" + + __tablename__ = "market_alert_review_queue" + + id = Column(Integer, primary_key=True, autoincrement=True) + alert_candidate_id = Column(String(120), nullable=False, unique=True, index=True) + review_state = Column(String(40), default="draft", nullable=False, index=True) + priority_lane = Column(String(40), default="watch", nullable=False, index=True) + threshold_level = Column(String(40), nullable=False, index=True) + total_score = Column(Float, default=0.0, nullable=False) + evidence_bundle_id = Column(String(120), nullable=False, index=True) + dedupe_key = Column(String(240), nullable=False) + source_batch_id = Column(String(80), nullable=False, index=True) + campaign_id = Column(Integer, ForeignKey("market_campaigns.id"), index=True) + market_product_id = Column(Integer, ForeignKey("market_campaign_products.id"), index=True) + momo_i_code = Column(String(50), index=True) + reviewer_identity = Column(String(120)) + review_action = Column(String(60)) + review_reason = Column(Text) + reviewed_at = Column(DateTime) + previous_state = Column(String(40)) + next_state = Column(String(40)) + created_at = Column(DateTime, default=taipei_now, nullable=False) + updated_at = Column(DateTime, default=taipei_now, onupdate=taipei_now, nullable=False) + metadata_json = Column(Text) + + __table_args__ = ( + Index( + "idx_market_alert_review_queue_state_priority", + "review_state", + "priority_lane", + "created_at", + ), + Index("ux_market_alert_review_queue_dedupe", "dedupe_key", unique=True), + Index( + "idx_market_alert_review_queue_bundle", + "evidence_bundle_id", + "source_batch_id", + ), + ) diff --git a/docs/adr/ADR-035-cross-platform-market-campaign-intelligence.md b/docs/adr/ADR-035-cross-platform-market-campaign-intelligence.md index 9b965c7..81135be 100644 --- a/docs/adr/ADR-035-cross-platform-market-campaign-intelligence.md +++ b/docs/adr/ADR-035-cross-platform-market-campaign-intelligence.md @@ -43,6 +43,7 @@ EwoooC 目前已有 MOMO EDM / 節慶活動資料、`promo_products`、PChome - `market_product_price_history` - `market_product_matches` - `market_crawler_runs` +- `market_alert_review_queue` `promo_products` 只作為既有 MOMO 活動資料的相容來源或雙寫過渡,不再承接跨平台唯一真相。 @@ -166,6 +167,7 @@ EwoooC 目前已有 MOMO EDM / 節慶活動資料、`promo_products`、PChome - 2026-05-18 追加 opportunity alert review preview:`/api/market_intel/opportunity_alert_plan` 擴充人工審核狀態與操作,定義 `draft → needs_review → approved_for_digest / approved_for_telegram / rejected / deferred` 流程、審核理由、審核者身分與派送前二次 gate。此階段不建立 review queue、不執行審核動作、不寫 approval record、不派送 Telegram、不呼叫 LLM。 - 2026-05-18 追加 deployment readiness modularization:將 `/api/market_intel/deployment_readiness` 的大型 app-only release gate 組裝邏輯由 `services.market_intel.service` 拆至 `services.market_intel.deployment_readiness`,主服務保留薄入口,避免後續 crawler / MCP / 審核功能推進時超過 800 行治理線;行為仍維持 preview-only,不執行 git、部署、SSH、migration 或 DB write。 - 2026-05-18 追加 alert review queue contract:`/api/market_intel/opportunity_alert_plan` 補上 `market_alert_review_queue` 的 preview contract、required / audit / forbidden fields、priority lanes 與索引規劃。此階段只定義資料契約,不建立 review table、不寫 queue contract、不執行審核、不派送 Telegram、不呼叫 LLM。 +- 2026-05-18 追加 alert review queue migration blueprint:`market_alert_review_queue` 納入 `database/market_intel_models.py`、`migrations/032_market_intel_core_schema.sql` 與 migration blueprint,補齊 additive CREATE TABLE / index / grant / rollback draft。此階段仍不執行 migration、不連 DB、不建立 review queue、不寫入審核資料。 ### Phase 4:Coupang / Shopee Adapter diff --git a/docs/memory/code_modularization_inventory_20260430.md b/docs/memory/code_modularization_inventory_20260430.md index 10bf5ac..739de96 100644 --- a/docs/memory/code_modularization_inventory_20260430.md +++ b/docs/memory/code_modularization_inventory_20260430.md @@ -7,9 +7,11 @@ - Python 總量:約 90,293 行(排除 `venv/`、`backups/`、`__pycache__/`、`.claude/worktrees/`)。 - 最大壓力區:`services/` 約 42,364 行、`routes/` 約 29,511 行。 - `app.py` 目前約 1,232 行,功能定位應固定為 bootstrap / Blueprint registration / startup guard,不再承接新 route。 -- 目前工作樹仍有 24 個 Python 檔案達到或超過 800 行;這些不是禁止修 bug,而是禁止繼續塞新功能。 +- 目前工作樹仍有 26 個 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` 繼續承接功能。 +- 2026-05-18 追記:同步治理測試盤點,新增 `services/code_review_pipeline_service.py`、`services/ppt_auto_generation_service.py`,並校正 `routes/admin_observability_routes.py`、`services/import_service.py` 行數。 ## 達到或超過 800 行檔案清單 @@ -20,22 +22,24 @@ | 3186 | `routes/sales_routes.py` | P0 巨型 Blueprint | page routes / API routes / chart query service / calendar service;分析頁新增功能先抽 `services/sales/` | | 2821 | `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 | -| 2657 | `routes/admin_observability_routes.py` | P0 觀測台巨型 Blueprint | `services/observability_query_service.py` / `services/observability_action_service.py` / route glue | +| 3283 | `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 | | 1721 | `services/nemoton_dispatcher_service.py` | P1 NemoTron service | NIM client / tool-call parser / action dispatcher | | 1507 | `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 | -| 1232 | `app.py` | P1 bootstrap | 保持只做 app setup;繼續往 app_factory / extension setup 抽 | +| 1237 | `app.py` | P1 bootstrap | 保持只做 app setup;繼續往 app_factory / extension setup 抽;Phase 42 只做 metadata table name 對齊 | | 1144 | `services/elephant_alpha_autonomous_engine.py` | P1 ElephantAlpha engine | HITL / executor / planning policy | -| 1090 | `routes/cicd_routes.py` | P2 CI/CD Blueprint | route glue / CI query service / deployment action service | +| 970 | `routes/cicd_routes.py` | P2 CI/CD Blueprint | route glue / CI query service / deployment action service | | 1017 | `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 | | 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 | -| 868 | `services/import_service.py` | P2 import service | validators / import writers / report builders | +| 940 | `services/import_service.py` | P2 import service | validators / import writers / report builders | | 867 | `services/token_report_service.py` | P2 token report service | query / aggregation / chart payload / notification formatting | | 865 | `routes/daily_sales_routes.py` | P2 Daily Sales Blueprint | route glue / export helpers / daily query and formatting service | | 844 | `services/ollama_service.py` | P2 Ollama client | host health / request client / fallback policy / response parsing | +| 837 | `services/code_review_pipeline_service.py` | P2 Code review pipeline service | scan orchestration / finding normalization / persistence adapter | | 832 | `routes/export_routes.py` | P2 Export flow | export command/router glue / file path / download orchestration | | 809 | `services/competitor_price_feeder.py` | P2 competitor price feeder | crawler scheduling / price normalization / cache strategy | | 805 | `routes/bot_api_routes.py` | P2 Bot API Blueprint | route glue / bot action service | diff --git a/migrations/032_market_intel_core_schema.sql b/migrations/032_market_intel_core_schema.sql index d6e8156..41dd6b5 100644 --- a/migrations/032_market_intel_core_schema.sql +++ b/migrations/032_market_intel_core_schema.sql @@ -220,6 +220,55 @@ CREATE INDEX IF NOT EXISTS idx_market_crawler_run_platform_time CREATE INDEX IF NOT EXISTS idx_market_crawler_run_status_time ON market_crawler_runs (status, started_at); +CREATE TABLE IF NOT EXISTS market_alert_review_queue ( + id BIGSERIAL PRIMARY KEY, + alert_candidate_id VARCHAR(120) NOT NULL UNIQUE, + review_state VARCHAR(40) NOT NULL DEFAULT 'draft', + priority_lane VARCHAR(40) NOT NULL DEFAULT 'watch', + threshold_level VARCHAR(40) NOT NULL, + total_score DOUBLE PRECISION NOT NULL DEFAULT 0.0, + evidence_bundle_id VARCHAR(120) NOT NULL, + dedupe_key VARCHAR(240) NOT NULL, + source_batch_id VARCHAR(80) NOT NULL, + campaign_id BIGINT REFERENCES market_campaigns(id), + market_product_id BIGINT REFERENCES market_campaign_products(id), + momo_i_code VARCHAR(50), + reviewer_identity VARCHAR(120), + review_action VARCHAR(60), + review_reason TEXT, + reviewed_at TIMESTAMP, + previous_state VARCHAR(40), + next_state VARCHAR(40), + created_at TIMESTAMP NOT NULL DEFAULT NOW(), + updated_at TIMESTAMP NOT NULL DEFAULT NOW(), + metadata_json TEXT +); + +CREATE INDEX IF NOT EXISTS idx_market_alert_review_queue_alert_candidate_id + ON market_alert_review_queue (alert_candidate_id); +CREATE INDEX IF NOT EXISTS idx_market_alert_review_queue_state + ON market_alert_review_queue (review_state); +CREATE INDEX IF NOT EXISTS idx_market_alert_review_queue_priority_lane + ON market_alert_review_queue (priority_lane); +CREATE INDEX IF NOT EXISTS idx_market_alert_review_queue_threshold_level + ON market_alert_review_queue (threshold_level); +CREATE INDEX IF NOT EXISTS idx_market_alert_review_queue_evidence_bundle_id + ON market_alert_review_queue (evidence_bundle_id); +CREATE INDEX IF NOT EXISTS idx_market_alert_review_queue_source_batch_id + ON market_alert_review_queue (source_batch_id); +CREATE INDEX IF NOT EXISTS idx_market_alert_review_queue_campaign_id + ON market_alert_review_queue (campaign_id); +CREATE INDEX IF NOT EXISTS idx_market_alert_review_queue_market_product_id + ON market_alert_review_queue (market_product_id); +CREATE INDEX IF NOT EXISTS idx_market_alert_review_queue_momo_i_code + ON market_alert_review_queue (momo_i_code); +CREATE INDEX IF NOT EXISTS idx_market_alert_review_queue_state_priority + ON market_alert_review_queue (review_state, priority_lane, created_at); +CREATE UNIQUE INDEX IF NOT EXISTS ux_market_alert_review_queue_dedupe + ON market_alert_review_queue (dedupe_key); +CREATE INDEX IF NOT EXISTS idx_market_alert_review_queue_bundle + ON market_alert_review_queue (evidence_bundle_id, source_batch_id); + GRANT ALL PRIVILEGES ON market_platforms TO momo; GRANT ALL PRIVILEGES ON market_campaigns TO momo; GRANT ALL PRIVILEGES ON market_campaign_snapshots TO momo; @@ -227,6 +276,7 @@ GRANT ALL PRIVILEGES ON market_campaign_products TO momo; GRANT ALL PRIVILEGES ON market_product_price_history TO momo; GRANT ALL PRIVILEGES ON market_product_matches TO momo; GRANT ALL PRIVILEGES ON market_crawler_runs TO momo; +GRANT ALL PRIVILEGES ON market_alert_review_queue TO momo; GRANT USAGE, SELECT ON SEQUENCE market_platforms_id_seq TO momo; GRANT USAGE, SELECT ON SEQUENCE market_campaigns_id_seq TO momo; @@ -235,3 +285,4 @@ GRANT USAGE, SELECT ON SEQUENCE market_campaign_products_id_seq TO momo; GRANT USAGE, SELECT ON SEQUENCE market_product_price_history_id_seq TO momo; GRANT USAGE, SELECT ON SEQUENCE market_product_matches_id_seq TO momo; GRANT USAGE, SELECT ON SEQUENCE market_crawler_runs_id_seq TO momo; +GRANT USAGE, SELECT ON SEQUENCE market_alert_review_queue_id_seq TO momo; diff --git a/routes/README.md b/routes/README.md index e5dbed5..d7c8826 100644 --- a/routes/README.md +++ b/routes/README.md @@ -19,7 +19,7 @@ | `edm_routes.py` | EDM 與節慶儀表板 | `/edm`, `/festival` | | `monthly_routes.py` | 月結分析 | `/monthly_summary_analysis`, `/api/monthly_summary_data` | | `daily_sales_routes.py` | 當日業績 | `/daily_sales`, `/daily_sales/export*` | -| `market_intel_routes.py` | 市場情報 Phase 41 alert review queue contract | `/market_intel`, `/market_intel/*`, `/api/market_intel/status`, `/api/market_intel/schema`, `/api/market_intel/schema_smoke`, `/api/market_intel/schema_db_probe`, `/api/market_intel/platform_seed_db_diff`, `/api/market_intel/legacy_source_bridge`, `/api/market_intel/mcp_readiness`, `/api/market_intel/mcp_tool_contract`, `/api/market_intel/mcp_deploy_preflight`, `/api/market_intel/mcp_activation_runbook`, `/api/market_intel/mcp_fetch_gate`, `/api/market_intel/scheduler_plan`, `/api/market_intel/match_review_plan`, `/api/market_intel/opportunity_plan`, `/api/market_intel/opportunity_scoring_plan`, `/api/market_intel/opportunity_evidence_plan`, `/api/market_intel/opportunity_alert_plan`, `/api/market_intel/adapters`, `/api/market_intel/dry_run_plan`, `/api/market_intel/discovery_plan`, `/api/market_intel/manual_discovery`, `/api/market_intel/candidate_preview`, `/api/market_intel/platform_seed_plan`, `/api/market_intel/platform_seed_write_guard`, `/api/market_intel/platform_seed_writer_plan`, `/api/market_intel/migration_blueprint`, `/api/market_intel/seed_writer_cli_status`, `/api/market_intel/write_approval_runbook`, `/api/market_intel/deployment_readiness` | +| `market_intel_routes.py` | 市場情報 Phase 42 alert review queue migration blueprint | `/market_intel`, `/market_intel/*`, `/api/market_intel/status`, `/api/market_intel/schema`, `/api/market_intel/schema_smoke`, `/api/market_intel/schema_db_probe`, `/api/market_intel/platform_seed_db_diff`, `/api/market_intel/legacy_source_bridge`, `/api/market_intel/mcp_readiness`, `/api/market_intel/mcp_tool_contract`, `/api/market_intel/mcp_deploy_preflight`, `/api/market_intel/mcp_activation_runbook`, `/api/market_intel/mcp_fetch_gate`, `/api/market_intel/scheduler_plan`, `/api/market_intel/match_review_plan`, `/api/market_intel/opportunity_plan`, `/api/market_intel/opportunity_scoring_plan`, `/api/market_intel/opportunity_evidence_plan`, `/api/market_intel/opportunity_alert_plan`, `/api/market_intel/adapters`, `/api/market_intel/dry_run_plan`, `/api/market_intel/discovery_plan`, `/api/market_intel/manual_discovery`, `/api/market_intel/candidate_preview`, `/api/market_intel/platform_seed_plan`, `/api/market_intel/platform_seed_write_guard`, `/api/market_intel/platform_seed_writer_plan`, `/api/market_intel/migration_blueprint`, `/api/market_intel/seed_writer_cli_status`, `/api/market_intel/write_approval_runbook`, `/api/market_intel/deployment_readiness` | | `api_routes.py` | 通用任務與查詢 API | `/api/run_task`, `/api/history/*` | | `export_routes.py` | 匯出功能 | `/api/export/*` | | `import_routes.py` | 匯入功能 | `/api/import_excel`, `/api/import/monthly_summary` | diff --git a/services/market_intel/migration_blueprint.py b/services/market_intel/migration_blueprint.py index ba7e2e7..c68738d 100644 --- a/services/market_intel/migration_blueprint.py +++ b/services/market_intel/migration_blueprint.py @@ -234,6 +234,55 @@ CREATE INDEX IF NOT EXISTS idx_market_crawler_run_platform_time CREATE INDEX IF NOT EXISTS idx_market_crawler_run_status_time ON market_crawler_runs (status, started_at); +CREATE TABLE IF NOT EXISTS market_alert_review_queue ( + id BIGSERIAL PRIMARY KEY, + alert_candidate_id VARCHAR(120) NOT NULL UNIQUE, + review_state VARCHAR(40) NOT NULL DEFAULT 'draft', + priority_lane VARCHAR(40) NOT NULL DEFAULT 'watch', + threshold_level VARCHAR(40) NOT NULL, + total_score DOUBLE PRECISION NOT NULL DEFAULT 0.0, + evidence_bundle_id VARCHAR(120) NOT NULL, + dedupe_key VARCHAR(240) NOT NULL, + source_batch_id VARCHAR(80) NOT NULL, + campaign_id BIGINT REFERENCES market_campaigns(id), + market_product_id BIGINT REFERENCES market_campaign_products(id), + momo_i_code VARCHAR(50), + reviewer_identity VARCHAR(120), + review_action VARCHAR(60), + review_reason TEXT, + reviewed_at TIMESTAMP, + previous_state VARCHAR(40), + next_state VARCHAR(40), + created_at TIMESTAMP NOT NULL DEFAULT NOW(), + updated_at TIMESTAMP NOT NULL DEFAULT NOW(), + metadata_json TEXT +); + +CREATE INDEX IF NOT EXISTS idx_market_alert_review_queue_alert_candidate_id + ON market_alert_review_queue (alert_candidate_id); +CREATE INDEX IF NOT EXISTS idx_market_alert_review_queue_state + ON market_alert_review_queue (review_state); +CREATE INDEX IF NOT EXISTS idx_market_alert_review_queue_priority_lane + ON market_alert_review_queue (priority_lane); +CREATE INDEX IF NOT EXISTS idx_market_alert_review_queue_threshold_level + ON market_alert_review_queue (threshold_level); +CREATE INDEX IF NOT EXISTS idx_market_alert_review_queue_evidence_bundle_id + ON market_alert_review_queue (evidence_bundle_id); +CREATE INDEX IF NOT EXISTS idx_market_alert_review_queue_source_batch_id + ON market_alert_review_queue (source_batch_id); +CREATE INDEX IF NOT EXISTS idx_market_alert_review_queue_campaign_id + ON market_alert_review_queue (campaign_id); +CREATE INDEX IF NOT EXISTS idx_market_alert_review_queue_market_product_id + ON market_alert_review_queue (market_product_id); +CREATE INDEX IF NOT EXISTS idx_market_alert_review_queue_momo_i_code + ON market_alert_review_queue (momo_i_code); +CREATE INDEX IF NOT EXISTS idx_market_alert_review_queue_state_priority + ON market_alert_review_queue (review_state, priority_lane, created_at); +CREATE UNIQUE INDEX IF NOT EXISTS ux_market_alert_review_queue_dedupe + ON market_alert_review_queue (dedupe_key); +CREATE INDEX IF NOT EXISTS idx_market_alert_review_queue_bundle + ON market_alert_review_queue (evidence_bundle_id, source_batch_id); + GRANT ALL PRIVILEGES ON market_platforms TO momo; GRANT ALL PRIVILEGES ON market_campaigns TO momo; GRANT ALL PRIVILEGES ON market_campaign_snapshots TO momo; @@ -241,6 +290,7 @@ GRANT ALL PRIVILEGES ON market_campaign_products TO momo; GRANT ALL PRIVILEGES ON market_product_price_history TO momo; GRANT ALL PRIVILEGES ON market_product_matches TO momo; GRANT ALL PRIVILEGES ON market_crawler_runs TO momo; +GRANT ALL PRIVILEGES ON market_alert_review_queue TO momo; GRANT USAGE, SELECT ON SEQUENCE market_platforms_id_seq TO momo; GRANT USAGE, SELECT ON SEQUENCE market_campaigns_id_seq TO momo; @@ -249,11 +299,13 @@ GRANT USAGE, SELECT ON SEQUENCE market_campaign_products_id_seq TO momo; GRANT USAGE, SELECT ON SEQUENCE market_product_price_history_id_seq TO momo; GRANT USAGE, SELECT ON SEQUENCE market_product_matches_id_seq TO momo; GRANT USAGE, SELECT ON SEQUENCE market_crawler_runs_id_seq TO momo; +GRANT USAGE, SELECT ON SEQUENCE market_alert_review_queue_id_seq TO momo; """.strip() ROLLBACK_SQL = """ -- Manual rollback draft only. Do not run without operator approval and backup verification. +DROP TABLE IF EXISTS market_alert_review_queue; DROP TABLE IF EXISTS market_crawler_runs; DROP TABLE IF EXISTS market_product_matches; DROP TABLE IF EXISTS market_product_price_history; diff --git a/services/market_intel/service.py b/services/market_intel/service.py index c235dc2..5155f34 100644 --- a/services/market_intel/service.py +++ b/services/market_intel/service.py @@ -61,6 +61,7 @@ MARKET_INTEL_TABLES = ( "market_product_price_history", "market_product_matches", "market_crawler_runs", + "market_alert_review_queue", ) @@ -83,7 +84,7 @@ class MarketIntelRuntimeStatus: class MarketIntelService: """市場情報入口服務,先集中 feature gate 與安全狀態。""" - phase = "phase_41_alert_review_queue_contract" + phase = "phase_42_alert_review_queue_migration_blueprint" def get_runtime_status(self) -> MarketIntelRuntimeStatus: return MarketIntelRuntimeStatus( diff --git a/services/market_intel/write_approval_runbook.py b/services/market_intel/write_approval_runbook.py index ec63f51..2a99b3b 100644 --- a/services/market_intel/write_approval_runbook.py +++ b/services/market_intel/write_approval_runbook.py @@ -21,7 +21,7 @@ def build_write_approval_runbook( gates = [ { "key": "schema_smoke_passed", - "label": "ORM metadata smoke 已通過,七張 market_* table 與 market_platforms 欄位完整", + "label": "ORM metadata smoke 已通過,八張 market_* table 與 market_platforms 欄位完整", "passed": bool(schema_smoke.get("passed")), }, { diff --git a/templates/market_intel/disabled.html b/templates/market_intel/disabled.html index c6e510a..460fa0a 100644 --- a/templates/market_intel/disabled.html +++ b/templates/market_intel/disabled.html @@ -2230,7 +2230,7 @@ }; const renderMigrationBody = data => { - const operations = (data.table_operations || []).slice(0, 7); + const operations = (data.table_operations || []).slice(0, 8); const blockers = (data.blocked_reasons || []).join(' / '); const commands = data.command_plan || {}; const migrationCommand = commands.migration_apply_command || {}; diff --git a/tests/test_market_intel_skeleton.py b/tests/test_market_intel_skeleton.py index 64f5c57..57d7409 100644 --- a/tests/test_market_intel_skeleton.py +++ b/tests/test_market_intel_skeleton.py @@ -530,7 +530,7 @@ def test_legacy_source_bridge_default_is_planned_only(): bridge = MarketIntelService().build_legacy_source_bridge() assert bridge["mode"] == "legacy_source_bridge_planned" - assert bridge["phase"] == "phase_41_alert_review_queue_contract" + assert bridge["phase"] == "phase_42_alert_review_queue_migration_blueprint" assert bridge["execute_requested"] is False assert bridge["read_only_query_executed"] is False assert bridge["database_connection_opened"] is False @@ -688,7 +688,7 @@ def test_mcp_tool_contract_preview_is_read_only_and_whitelisted(): contract = MarketIntelService().build_mcp_tool_contract() assert contract["mode"] == "mcp_tool_contract_preview" - assert contract["phase"] == "phase_41_alert_review_queue_contract" + assert contract["phase"] == "phase_42_alert_review_queue_migration_blueprint" assert contract["caller"] == "market_intel" assert contract["contract_ready"] is True assert contract["blocked_reasons"] == [] @@ -821,7 +821,7 @@ def test_mcp_activation_runbook_route_is_preview_only(): assert response.status_code == 200 assert data["mode"] == "mcp_activation_runbook_preview" - assert data["phase"] == "phase_41_alert_review_queue_contract" + assert data["phase"] == "phase_42_alert_review_queue_migration_blueprint" assert data["deployment_actions_executed"] is False assert data["docker_command_executed"] is False assert data["ssh_command_executed"] is False @@ -834,7 +834,7 @@ def test_mcp_fetch_gate_default_blocks_external_fetch(): gate = MarketIntelService().build_mcp_fetch_gate(fetch_requested=True) assert gate["mode"] == "mcp_fetch_gate_planned" - assert gate["phase"] == "phase_41_alert_review_queue_contract" + assert gate["phase"] == "phase_42_alert_review_queue_migration_blueprint" assert gate["fetch_requested"] is True assert gate["manual_fetch_gate_open"] is False assert gate["network_request_allowed"] is False @@ -904,7 +904,7 @@ def test_mcp_fetch_gate_route_is_preview_only(): assert response.status_code == 200 assert data["mode"] == "mcp_fetch_gate_planned" - assert data["phase"] == "phase_41_alert_review_queue_contract" + assert data["phase"] == "phase_42_alert_review_queue_migration_blueprint" assert data["fetch_requested"] is False assert data["network_request_allowed"] is False assert data["external_network_executed"] is False @@ -916,7 +916,7 @@ def test_scheduler_plan_preview_blocks_job_attachment(): plan = MarketIntelService().build_scheduler_plan() assert plan["mode"] == "scheduler_attach_plan_preview" - assert plan["phase"] == "phase_41_alert_review_queue_contract" + assert plan["phase"] == "phase_42_alert_review_queue_migration_blueprint" assert plan["ready_to_attach_scheduler"] is False assert plan["scheduler_attached"] is False assert plan["scheduler_registration_executed"] is False @@ -954,7 +954,7 @@ def test_scheduler_plan_route_is_preview_only(): assert response.status_code == 200 assert data["mode"] == "scheduler_attach_plan_preview" - assert data["phase"] == "phase_41_alert_review_queue_contract" + assert data["phase"] == "phase_42_alert_review_queue_migration_blueprint" assert data["scheduler_registration_executed"] is False assert data["crawler_job_started"] is False assert data["external_network_executed"] is False @@ -965,7 +965,7 @@ def test_match_review_plan_preview_blocks_auto_confirm(): plan = MarketIntelService().build_match_review_plan() assert plan["mode"] == "match_review_plan_preview" - assert plan["phase"] == "phase_41_alert_review_queue_contract" + assert plan["phase"] == "phase_42_alert_review_queue_migration_blueprint" assert plan["ready_for_review_queue"] is False assert plan["review_queue_created"] is False assert plan["auto_match_executed"] is False @@ -1001,7 +1001,7 @@ def test_match_review_plan_route_is_preview_only(): assert response.status_code == 200 assert data["mode"] == "match_review_plan_preview" - assert data["phase"] == "phase_41_alert_review_queue_contract" + assert data["phase"] == "phase_42_alert_review_queue_migration_blueprint" assert data["review_queue_created"] is False assert data["auto_confirm_executed"] is False assert data["external_network_executed"] is False @@ -1012,7 +1012,7 @@ def test_opportunity_plan_preview_blocks_alerts_and_ai_summary(): plan = MarketIntelService().build_opportunity_plan() assert plan["mode"] == "opportunity_plan_preview" - assert plan["phase"] == "phase_41_alert_review_queue_contract" + assert plan["phase"] == "phase_42_alert_review_queue_migration_blueprint" assert plan["ready_for_opportunity_queue"] is False assert plan["opportunity_queue_created"] is False assert plan["threat_alert_dispatched"] is False @@ -1053,7 +1053,7 @@ def test_opportunity_plan_route_is_preview_only(): assert response.status_code == 200 assert data["mode"] == "opportunity_plan_preview" - assert data["phase"] == "phase_41_alert_review_queue_contract" + assert data["phase"] == "phase_42_alert_review_queue_migration_blueprint" assert data["opportunity_queue_created"] is False assert data["threat_alert_dispatched"] is False assert data["ai_summary_generated"] is False @@ -1064,7 +1064,7 @@ def test_opportunity_scoring_plan_preview_blocks_scoring_and_alerts(): plan = MarketIntelService().build_opportunity_scoring_plan() assert plan["mode"] == "opportunity_scoring_plan_preview" - assert plan["phase"] == "phase_41_alert_review_queue_contract" + assert plan["phase"] == "phase_42_alert_review_queue_migration_blueprint" assert plan["ready_for_scoring_job"] is False assert plan["scoring_job_created"] is False assert plan["score_calculation_executed"] is False @@ -1112,7 +1112,7 @@ def test_opportunity_scoring_plan_route_is_preview_only(): assert response.status_code == 200 assert data["mode"] == "opportunity_scoring_plan_preview" - assert data["phase"] == "phase_41_alert_review_queue_contract" + assert data["phase"] == "phase_42_alert_review_queue_migration_blueprint" assert data["scoring_job_created"] is False assert data["score_calculation_executed"] is False assert data["sample_scores_generated"] is False @@ -1124,7 +1124,7 @@ def test_opportunity_evidence_plan_preview_blocks_queries_and_alerts(): plan = MarketIntelService().build_opportunity_evidence_plan() assert plan["mode"] == "opportunity_evidence_plan_preview" - assert plan["phase"] == "phase_41_alert_review_queue_contract" + assert plan["phase"] == "phase_42_alert_review_queue_migration_blueprint" assert plan["ready_for_evidence_bundle"] is False assert plan["evidence_bundle_created"] is False assert plan["evidence_query_executed"] is False @@ -1170,7 +1170,7 @@ def test_opportunity_evidence_plan_route_is_preview_only(): assert response.status_code == 200 assert data["mode"] == "opportunity_evidence_plan_preview" - assert data["phase"] == "phase_41_alert_review_queue_contract" + assert data["phase"] == "phase_42_alert_review_queue_migration_blueprint" assert data["evidence_bundle_created"] is False assert data["evidence_query_executed"] is False assert data["sample_evidence_generated"] is False @@ -1183,7 +1183,7 @@ def test_opportunity_alert_plan_preview_blocks_dispatch_and_llm_calls(): plan = MarketIntelService().build_opportunity_alert_plan() assert plan["mode"] == "opportunity_alert_plan_preview" - assert plan["phase"] == "phase_41_alert_review_queue_contract" + assert plan["phase"] == "phase_42_alert_review_queue_migration_blueprint" assert plan["ready_for_alert_candidates"] is False assert plan["alert_candidate_created"] is False assert plan["alert_queue_created"] is False @@ -1268,7 +1268,7 @@ def test_opportunity_alert_plan_route_is_preview_only(): assert response.status_code == 200 assert data["mode"] == "opportunity_alert_plan_preview" - assert data["phase"] == "phase_41_alert_review_queue_contract" + assert data["phase"] == "phase_42_alert_review_queue_migration_blueprint" assert data["alert_candidate_created"] is False assert data["alert_queue_created"] is False assert data["review_queue_created"] is False @@ -1346,7 +1346,7 @@ def test_mcp_deploy_preflight_route_is_preview_only(): assert response.status_code == 200 assert data["mode"] == "mcp_external_deploy_preflight_preview" - assert data["phase"] == "phase_41_alert_review_queue_contract" + assert data["phase"] == "phase_42_alert_review_queue_migration_blueprint" assert data["deployment_actions_executed"] is False assert data["docker_command_executed"] is False assert data["ssh_command_executed"] is False @@ -1361,7 +1361,7 @@ def test_mcp_readiness_default_is_planned_only(monkeypatch): readiness = MarketIntelService().build_mcp_readiness() assert readiness["mode"] == "mcp_readiness_planned" - assert readiness["phase"] == "phase_41_alert_review_queue_contract" + assert readiness["phase"] == "phase_42_alert_review_queue_migration_blueprint" assert readiness["execute_requested"] is False assert readiness["router_enabled"] is False assert readiness["external_mcp_complete"] is False @@ -1875,7 +1875,7 @@ def test_migration_blueprint_is_additive_preview_only(): assert blueprint["database_commit_executed"] is False assert blueprint["external_network_executed"] is False assert blueprint["scheduler_attached"] is False - assert blueprint["table_count"] == 7 + assert blueprint["table_count"] == 8 assert blueprint["forward_has_destructive_sql"] is False assert blueprint["safety_checks"]["forward_sql_additive_only"] is True assert blueprint["safety_checks"]["writes_seed_rows_only_with_cli_apply_flag"] is True @@ -1886,6 +1886,8 @@ def test_migration_blueprint_is_additive_preview_only(): assert migration_file.read_text(encoding="utf-8").strip() == blueprint["forward_sql"] assert "CREATE TABLE IF NOT EXISTS market_platforms".lower() in forward_sql_lower assert "CREATE TABLE IF NOT EXISTS market_crawler_runs".lower() in forward_sql_lower + assert "CREATE TABLE IF NOT EXISTS market_alert_review_queue".lower() in forward_sql_lower + assert "ux_market_alert_review_queue_dedupe".lower() in forward_sql_lower assert "drop table" not in forward_sql_lower assert "truncate " not in forward_sql_lower assert "delete from" not in forward_sql_lower @@ -1893,6 +1895,7 @@ def test_migration_blueprint_is_additive_preview_only(): assert blueprint["command_plan"]["seed_writer_command"]["script_created"] is True assert blueprint["command_plan"]["seed_writer_command"]["script_path"] == "scripts/market_intel_seed_writer.py" assert blueprint["rollback_requires_manual_approval"] is True + assert "DROP TABLE IF EXISTS market_alert_review_queue" in blueprint["rollback_sql"] def test_seed_writer_cli_status_blocks_real_write():