新增市場情報 queue writer run package
All checks were successful
CD Pipeline / deploy (push) Successful in 1m4s
All checks were successful
CD Pipeline / deploy (push) Successful in 1m4s
This commit is contained in:
277
services/market_intel/candidate_queue_writer_run_package.py
Normal file
277
services/market_intel/candidate_queue_writer_run_package.py
Normal 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",
|
||||
],
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user