feat(market-intel): add alert review queue migration blueprint
All checks were successful
CD Pipeline / deploy (push) Successful in 1m1s
All checks were successful
CD Pipeline / deploy (push) Successful in 1m1s
This commit is contained in:
1
app.py
1
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',
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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 # 用於模板顯示
|
||||
|
||||
|
||||
@@ -46,6 +46,7 @@ from .market_intel_models import ( # noqa: F401 - ADR-035 market_* 表
|
||||
MarketProductPriceHistory,
|
||||
MarketProductMatch,
|
||||
MarketCrawlerRun,
|
||||
MarketAlertReviewQueue,
|
||||
)
|
||||
|
||||
# 🚩 導入優化後的日誌管理模組
|
||||
|
||||
@@ -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",
|
||||
),
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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 |
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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` |
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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")),
|
||||
},
|
||||
{
|
||||
|
||||
@@ -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 || {};
|
||||
|
||||
@@ -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():
|
||||
|
||||
Reference in New Issue
Block a user