新增市場情報 queue writer run package
All checks were successful
CD Pipeline / deploy (push) Successful in 1m4s

This commit is contained in:
OoO
2026-05-19 11:01:02 +08:00
parent ac5d1a2d81
commit 98a0ca7af9
10 changed files with 714 additions and 52 deletions

View File

@@ -0,0 +1,277 @@
"""候選審核 queue writer operator run package preview。
本模組只把正式 CLI 小流量寫入前需要保存的 payload、命令、證據與
回退資訊整理成可審核預覽;不讀 approval token、不寫本機檔案、不開 DB
connection、不執行 CLI、不 commit、不掛 scheduler。
"""
import hashlib
import json
def _stable_hash(value):
encoded = json.dumps(
value,
ensure_ascii=False,
sort_keys=True,
separators=(",", ":"),
).encode("utf-8")
return hashlib.sha256(encoded).hexdigest()
def _statement_payloads(transaction_preview):
payloads = []
for statement in transaction_preview.get("statements", []):
payload = statement.get("parameter_preview")
if not isinstance(payload, dict):
continue
payloads.append(
{
"index": statement.get("index"),
"idempotency_key": statement.get("idempotency_key"),
"dedupe_key": payload.get("dedupe_key"),
"alert_candidate_id": payload.get("alert_candidate_id"),
"review_state": payload.get("review_state"),
"priority_lane": payload.get("priority_lane"),
"threshold_level": payload.get("threshold_level"),
"source_batch_id": payload.get("source_batch_id"),
"payload_hash": _stable_hash(payload),
}
)
return payloads
def _package_gates(
*,
transaction_preview,
writer_preflight,
writer_status,
postwrite_smoke,
operator_drill,
):
transaction_summary = transaction_preview.get("transaction_summary", {})
statement_count = int(transaction_summary.get("statement_count") or 0)
return [
{
"key": "transaction_preview_created",
"label": "已產生 candidate queue transaction preview",
"passed": bool(transaction_preview.get("transaction_preview_created")),
},
{
"key": "transaction_has_one_small_batch",
"label": "首次 operator run 必須是小流量,且至少有一筆 statement",
"passed": 0 < statement_count <= 5,
},
{
"key": "writer_preflight_planned",
"label": "已建立 writer preflight 計畫,正式操作時需另跑只讀 smoke",
"passed": bool(writer_preflight),
},
{
"key": "writer_cli_gate_available",
"label": "CLI writer gate 已可用,正式寫入只能走 CLI",
"passed": bool(writer_status),
},
{
"key": "postwrite_smoke_available",
"label": "post-write smoke 已可用,寫入後可只讀驗證 dedupe key",
"passed": bool(postwrite_smoke),
},
{
"key": "operator_drill_ready",
"label": "operator drill 已產生正式操作順序",
"passed": bool(operator_drill.get("operator_drill_ready")),
},
{
"key": "real_sample_payload_saved_by_operator",
"label": "真實 sample result 必須由操作員存成 reviewed JSON 檔",
"passed": False,
},
{
"key": "backup_artifact_path_recorded",
"label": "正式寫入前必須記錄最新備份檔路徑",
"passed": False,
},
{
"key": "one_time_token_supplied_in_shell_only",
"label": "一次性 approval token 只能在 CLI shell 提供",
"passed": False,
},
{
"key": "postwrite_evidence_saved",
"label": "正式寫入後必須保存 writer output 與 post-write smoke 結果",
"passed": False,
},
]
def build_candidate_queue_writer_run_package(
*,
transaction_preview,
writer_preflight,
writer_status,
postwrite_smoke,
operator_drill,
):
"""建立正式 operator run package preview不執行任何副作用。"""
payloads = _statement_payloads(transaction_preview)
payload_manifest_hash = _stable_hash(payloads)
gates = _package_gates(
transaction_preview=transaction_preview,
writer_preflight=writer_preflight,
writer_status=writer_status,
postwrite_smoke=postwrite_smoke,
operator_drill=operator_drill,
)
blocked_reasons = [gate["key"] for gate in gates if not gate["passed"]]
package_ready = bool(payloads and operator_drill.get("operator_drill_ready"))
return {
"mode": "candidate_queue_writer_run_package_preview",
"target_table": "market_alert_review_queue",
"package_ready": package_ready,
"package_artifact_created": False,
"api_writes_file": False,
"api_executes_cli": False,
"api_reads_approval_token": False,
"api_writes_database": False,
"database_connection_opened": False,
"database_session_created": False,
"explicit_transaction_opened": False,
"database_write_executed": False,
"database_commit_executed": False,
"database_rollback_executed": False,
"external_network_executed": False,
"scheduler_attached": False,
"writes_executed": False,
"would_write_database": False,
"blocked_reasons": blocked_reasons,
"payload_manifest": {
"payload_count": len(payloads),
"manifest_hash": payload_manifest_hash,
"dedupe_keys": [
item["dedupe_key"] for item in payloads if item.get("dedupe_key")
],
"payloads": payloads,
"redaction_policy": [
"do_not_include_approval_token",
"do_not_include_raw_html",
"do_not_include_login_or_member_data",
],
},
"required_artifacts": [
{
"key": "reviewed_sample_json",
"label": "操作員保存的真實 reviewed sample JSON",
"path_shape": "artifacts/market_intel/reviewed-sample-YYYYMMDD-HHMMSS.json",
"created_by_api": False,
},
{
"key": "project_backup_zip",
"label": "正式寫入前備份檔",
"path_shape": "scripts/tools/backups/momo_pro_system_backup_*.zip",
"created_by_api": False,
},
{
"key": "read_only_preflight_json",
"label": "CLI read-only preflight 輸出",
"path_shape": "artifacts/market_intel/preflight-*.json",
"created_by_api": False,
},
{
"key": "writer_output_json",
"label": "CLI writer 正式執行輸出",
"path_shape": "artifacts/market_intel/writer-output-*.json",
"created_by_api": False,
},
{
"key": "postwrite_smoke_json",
"label": "post-write smoke 只讀驗證輸出",
"path_shape": "artifacts/market_intel/postwrite-smoke-*.json",
"created_by_api": False,
},
],
"command_bundle": [
{
"step": 1,
"key": "save_reviewed_sample",
"command_shape": (
"mkdir -p artifacts/market_intel && "
"write reviewed sample JSON to artifacts/market_intel/"
"reviewed-sample-YYYYMMDD-HHMMSS.json"
),
"executes_database": False,
},
{
"step": 2,
"key": "backup",
"command_shape": "python3 scripts/tools/backup_system.py",
"executes_database": False,
},
{
"step": 3,
"key": "preflight",
"command_shape": (
"python3 scripts/market_intel_candidate_queue_writer.py "
"--sample-json artifacts/market_intel/reviewed-sample.json "
"--read-only-preflight > artifacts/market_intel/preflight.json"
),
"executes_database": False,
},
{
"step": 4,
"key": "writer",
"command_shape": (
"MARKET_INTEL_QUEUE_WRITE_APPROVAL=... "
"python3 scripts/market_intel_candidate_queue_writer.py "
"--sample-json artifacts/market_intel/reviewed-sample.json "
"--read-only-preflight --backup-verified "
"--migration-live-smoke-passed --execute --apply-real-write "
"--approval-token \"$MARKET_INTEL_QUEUE_WRITE_APPROVAL\" "
"> artifacts/market_intel/writer-output.json"
),
"executes_database": True,
},
{
"step": 5,
"key": "postwrite_smoke",
"command_shape": (
"POST /api/market_intel/manual_sample_review/"
"candidate_queue_writer_postwrite_smoke?execute=true "
"> artifacts/market_intel/postwrite-smoke.json"
),
"executes_database": False,
},
],
"operator_signoff": [
"確認 reviewed sample JSON 來自真實公開頁抓取結果",
"確認備份檔路徑與時間",
"確認 migration live smoke 結果為通過或可接受狀態",
"確認 writer output 的 inserted_count / skipped_count",
"確認 post-write smoke 找到預期 dedupe key",
],
"rollback_plan": [
{
"key": "no_preview_side_effect",
"label": "此 run package preview 沒有副作用,不需要 DB rollback",
},
{
"key": "dedupe_key_cleanup_review",
"label": "正式 CLI 寫入後若需回退,依 dedupe key 人工審核清理",
},
{
"key": "keep_market_flags_disabled",
"label": "異常時維持 MARKET_INTEL_* flags 關閉,不掛 scheduler",
},
],
"safe_boundaries": [
"do_not_create_run_artifacts_from_api",
"do_not_execute_cli_from_api",
"do_not_read_approval_token_from_api",
"do_not_open_database_connection_from_run_package_preview",
"do_not_write_queue_from_run_package_preview",
"do_not_attach_scheduler_from_run_package",
"no_remove_orphans",
"no_momo_db_lifecycle_change",
],
}

View File

@@ -15,6 +15,9 @@ from services.market_intel.candidate_queue_writer_postwrite_smoke import (
from services.market_intel.candidate_queue_writer_operator_drill import (
build_candidate_queue_writer_operator_drill,
)
from services.market_intel.candidate_queue_writer_run_package import (
build_candidate_queue_writer_run_package,
)
def build_deployment_readiness_preview(
@@ -71,6 +74,13 @@ def build_deployment_readiness_preview(
postwrite_smoke=candidate_queue_writer_postwrite_smoke,
)
)
candidate_queue_writer_run_package = build_candidate_queue_writer_run_package(
transaction_preview=manual_sample_candidate_queue_transaction,
writer_preflight=candidate_queue_writer_preflight,
writer_status=candidate_queue_writer_cli_status,
postwrite_smoke=candidate_queue_writer_postwrite_smoke,
operator_drill=candidate_queue_writer_operator_drill,
)
match_review_plan = service.build_match_review_plan()
opportunity_plan = service.build_opportunity_plan()
opportunity_scoring_plan = service.build_opportunity_scoring_plan()
@@ -266,6 +276,18 @@ def build_deployment_readiness_preview(
and not candidate_queue_writer_operator_drill["database_commit_executed"]
and not candidate_queue_writer_operator_drill["scheduler_attached"]
),
"candidate_queue_writer_run_package_preview_safe": bool(
candidate_queue_writer_run_package["mode"]
== "candidate_queue_writer_run_package_preview"
and not candidate_queue_writer_run_package["package_artifact_created"]
and not candidate_queue_writer_run_package["api_writes_file"]
and not candidate_queue_writer_run_package["api_executes_cli"]
and not candidate_queue_writer_run_package["api_reads_approval_token"]
and not candidate_queue_writer_run_package["database_connection_opened"]
and not candidate_queue_writer_run_package["database_write_executed"]
and not candidate_queue_writer_run_package["database_commit_executed"]
and not candidate_queue_writer_run_package["scheduler_attached"]
),
"match_review_plan_preview_safe": bool(
match_review_plan["mode"] == "match_review_plan_preview"
and not match_review_plan["review_queue_created"]
@@ -482,6 +504,7 @@ def build_deployment_readiness_preview(
"/api/market_intel/live_db_inventory",
"/api/market_intel/manual_sample_review/candidate_queue_writer_postwrite_smoke",
"/api/market_intel/manual_sample_review/candidate_queue_writer_operator_drill",
"/api/market_intel/manual_sample_review/candidate_queue_writer_run_package",
],
"status": status.to_dict(),
"schema_smoke": schema_smoke,
@@ -526,6 +549,7 @@ def build_deployment_readiness_preview(
"candidate_queue_writer_operator_drill": (
candidate_queue_writer_operator_drill
),
"candidate_queue_writer_run_package": candidate_queue_writer_run_package,
"match_review_plan": match_review_plan,
"opportunity_plan": opportunity_plan,
"opportunity_scoring_plan": opportunity_scoring_plan,

View File

@@ -108,7 +108,7 @@ class MarketIntelRuntimeStatus:
class MarketIntelService:
"""市場情報入口服務,先集中 feature gate 與安全狀態。"""
phase = "phase_59_candidate_queue_writer_operator_drill"
phase = "phase_60_candidate_queue_writer_run_package"
def get_runtime_status(self) -> MarketIntelRuntimeStatus:
return MarketIntelRuntimeStatus(