feat(market-intel): add alert review queue migration blueprint
All checks were successful
CD Pipeline / deploy (push) Successful in 1m1s

This commit is contained in:
OoO
2026-05-18 19:51:36 +08:00
parent 7e2f1ac671
commit bc900321f8
13 changed files with 188 additions and 30 deletions

1
app.py
View File

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

View File

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

View File

@@ -46,6 +46,7 @@ from .market_intel_models import ( # noqa: F401 - ADR-035 market_* 表
MarketProductPriceHistory,
MarketProductMatch,
MarketCrawlerRun,
MarketAlertReviewQueue,
)
# 🚩 導入優化後的日誌管理模組

View File

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

View File

@@ -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 4Coupang / Shopee Adapter

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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 || {};

View File

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