248 lines
9.9 KiB
Python
248 lines
9.9 KiB
Python
"""候選審核 queue review handoff preview。
|
||
|
||
本模組只在 writer run closeout 通過後,整理人工審核交接契約;
|
||
不查 DB、不更新 review_state、不讀 approval token、不執行 CLI、不掛 scheduler。
|
||
"""
|
||
|
||
|
||
FORBIDDEN_TOKEN_KEYWORDS = (
|
||
"approval_token",
|
||
"approval-token",
|
||
"market_intel_queue_write_approval",
|
||
)
|
||
SAFE_TOKEN_METADATA_KEYS = {
|
||
"approval_token_present",
|
||
"approval_token_valid",
|
||
"approval_token_secret_configured",
|
||
}
|
||
SAFE_APPROVAL_ENV_VAR = "MARKET_INTEL_QUEUE_WRITE_APPROVAL"
|
||
|
||
|
||
def _as_dict(value):
|
||
return value if isinstance(value, dict) else {}
|
||
|
||
|
||
def _as_list(value):
|
||
if value is None:
|
||
return []
|
||
if isinstance(value, (list, tuple, set)):
|
||
return list(value)
|
||
return [value]
|
||
|
||
|
||
def _contains_forbidden_token_key(value):
|
||
if isinstance(value, dict):
|
||
for key, nested in value.items():
|
||
normalized_key = str(key).lower()
|
||
if normalized_key in SAFE_TOKEN_METADATA_KEYS and isinstance(nested, bool):
|
||
continue
|
||
if normalized_key == "approval_env_var" and nested == SAFE_APPROVAL_ENV_VAR:
|
||
continue
|
||
if any(token_key in normalized_key for token_key in FORBIDDEN_TOKEN_KEYWORDS):
|
||
return True
|
||
if _contains_forbidden_token_key(nested):
|
||
return True
|
||
elif isinstance(value, list):
|
||
return any(_contains_forbidden_token_key(item) for item in value)
|
||
return False
|
||
|
||
|
||
def _dedupe_keys(transaction_preview, closeout):
|
||
keys = []
|
||
for statement in _as_list(_as_dict(transaction_preview).get("statements")):
|
||
lookup = _as_dict(statement.get("lookup"))
|
||
parameter_preview = _as_dict(statement.get("parameter_preview"))
|
||
dedupe_key = lookup.get("dedupe_key") or parameter_preview.get("dedupe_key")
|
||
if dedupe_key:
|
||
keys.append(str(dedupe_key))
|
||
if not keys:
|
||
keys = _as_list(_as_dict(_as_dict(closeout).get("receipt_summary")).get("expected_dedupe_keys"))
|
||
return sorted(set(str(key) for key in keys if key))
|
||
|
||
|
||
def _closeout_summary(closeout):
|
||
closeout = _as_dict(closeout)
|
||
promotion_gate = _as_dict(closeout.get("promotion_gate"))
|
||
return {
|
||
"provided": bool(closeout),
|
||
"mode": closeout.get("mode"),
|
||
"closeout_passed": bool(closeout.get("closeout_passed")),
|
||
"ready_for_next_manual_phase": bool(closeout.get("ready_for_next_manual_phase")),
|
||
"promotion_allowed": bool(promotion_gate.get("allowed")),
|
||
"next_manual_phase": promotion_gate.get("next_manual_phase"),
|
||
"ready_for_api_database_write": bool(closeout.get("ready_for_api_database_write")),
|
||
"ready_for_scheduler_attach": bool(closeout.get("ready_for_scheduler_attach")),
|
||
"api_executes_cli": bool(closeout.get("api_executes_cli")),
|
||
"api_reads_approval_token": bool(closeout.get("api_reads_approval_token")),
|
||
"api_writes_file": bool(closeout.get("api_writes_file")),
|
||
"api_writes_database": bool(closeout.get("api_writes_database")),
|
||
"database_connection_opened": bool(closeout.get("database_connection_opened")),
|
||
"database_write_executed": bool(closeout.get("database_write_executed")),
|
||
"database_commit_executed": bool(closeout.get("database_commit_executed")),
|
||
"scheduler_attached": bool(closeout.get("scheduler_attached")),
|
||
}
|
||
|
||
|
||
def _operator_summary(operator_evidence):
|
||
operator_evidence = _as_dict(operator_evidence)
|
||
return {
|
||
"provided_keys": sorted(operator_evidence.keys()),
|
||
"operator_confirmed_queue_review_next": bool(
|
||
operator_evidence.get("operator_confirmed_queue_review_next")
|
||
),
|
||
"operator_confirmed_no_scheduler_attach": bool(
|
||
operator_evidence.get("operator_confirmed_no_scheduler_attach")
|
||
),
|
||
"operator_confirmed_no_api_db_write": bool(
|
||
operator_evidence.get("operator_confirmed_no_api_db_write")
|
||
),
|
||
"approval_token_submitted_to_api": _contains_forbidden_token_key(
|
||
operator_evidence
|
||
),
|
||
}
|
||
|
||
|
||
def _handoff_gates(closeout_summary, operator_summary, expected_dedupe_keys):
|
||
return [
|
||
{
|
||
"key": "closeout_preview_provided",
|
||
"label": "必須提供上一階段 closeout preview",
|
||
"passed": bool(
|
||
closeout_summary["provided"]
|
||
and closeout_summary["mode"]
|
||
== "candidate_queue_writer_run_closeout_preview"
|
||
),
|
||
},
|
||
{
|
||
"key": "closeout_passed",
|
||
"label": "closeout 必須通過才可交接人工審核",
|
||
"passed": closeout_summary["closeout_passed"],
|
||
},
|
||
{
|
||
"key": "closeout_promotes_manual_queue_review",
|
||
"label": "promotion 只能指向人工 queue review / read-only inventory",
|
||
"passed": bool(
|
||
closeout_summary["promotion_allowed"]
|
||
and closeout_summary["next_manual_phase"]
|
||
== "manual_queue_review_and_live_inventory_read_only"
|
||
),
|
||
},
|
||
{
|
||
"key": "expected_dedupe_keys_present",
|
||
"label": "交接包必須有 expected dedupe key 供人工比對",
|
||
"passed": bool(expected_dedupe_keys),
|
||
},
|
||
{
|
||
"key": "closeout_no_api_write_or_scheduler",
|
||
"label": "closeout 不得允許 API/UI DB 寫入、CLI 或 scheduler",
|
||
"passed": bool(
|
||
not closeout_summary["ready_for_api_database_write"]
|
||
and not closeout_summary["ready_for_scheduler_attach"]
|
||
and not closeout_summary["api_executes_cli"]
|
||
and not closeout_summary["api_writes_database"]
|
||
and not closeout_summary["database_write_executed"]
|
||
and not closeout_summary["database_commit_executed"]
|
||
and not closeout_summary["scheduler_attached"]
|
||
),
|
||
},
|
||
{
|
||
"key": "operator_confirmed_review_is_manual",
|
||
"label": "操作員確認下一步只做人工審核,不由 API 更新 review_state",
|
||
"passed": bool(
|
||
operator_summary["operator_confirmed_queue_review_next"]
|
||
and operator_summary["operator_confirmed_no_api_db_write"]
|
||
and operator_summary["operator_confirmed_no_scheduler_attach"]
|
||
),
|
||
},
|
||
{
|
||
"key": "handoff_no_approval_token_submitted_to_api",
|
||
"label": "handoff payload 不得包含一次性 approval token key",
|
||
"passed": not operator_summary["approval_token_submitted_to_api"],
|
||
},
|
||
]
|
||
|
||
|
||
def build_candidate_queue_review_handoff(
|
||
*,
|
||
transaction_preview,
|
||
run_closeout,
|
||
operator_evidence=None,
|
||
):
|
||
"""建立人工 queue review handoff;不執行任何副作用。"""
|
||
closeout_summary = _closeout_summary(run_closeout)
|
||
operator_summary = _operator_summary(operator_evidence)
|
||
expected_dedupe_keys = _dedupe_keys(transaction_preview, run_closeout)
|
||
gates = _handoff_gates(closeout_summary, operator_summary, expected_dedupe_keys)
|
||
blocked_reasons = [gate["key"] for gate in gates if not gate["passed"]]
|
||
handoff_ready = bool(not blocked_reasons)
|
||
|
||
return {
|
||
"mode": "candidate_queue_review_handoff_preview",
|
||
"target_table": "market_alert_review_queue",
|
||
"handoff_ready": handoff_ready,
|
||
"ready_for_manual_queue_review": handoff_ready,
|
||
"ready_for_live_inventory_read_only": handoff_ready,
|
||
"ready_for_api_database_write": False,
|
||
"ready_for_scheduler_attach": False,
|
||
"api_executes_cli": False,
|
||
"api_reads_approval_token": False,
|
||
"api_writes_file": False,
|
||
"api_writes_database": False,
|
||
"api_updates_review_state": False,
|
||
"read_only_query_executed": 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,
|
||
"expected_dedupe_keys": expected_dedupe_keys,
|
||
"blocked_reasons": blocked_reasons,
|
||
"gates": gates,
|
||
"closeout_summary": closeout_summary,
|
||
"operator_handoff_summary": operator_summary,
|
||
"review_contract": {
|
||
"expected_review_state": "needs_review",
|
||
"allowed_manual_actions": [
|
||
"confirm_queue_row_exists",
|
||
"inspect_evidence_json",
|
||
"compare_dedupe_key",
|
||
"record_human_decision_outside_api",
|
||
],
|
||
"forbidden_api_actions": [
|
||
"update_review_state",
|
||
"insert_missing_queue_row",
|
||
"dispatch_alert",
|
||
"attach_scheduler",
|
||
],
|
||
"required_columns": [
|
||
"dedupe_key",
|
||
"review_state",
|
||
"priority_lane",
|
||
"source_url",
|
||
"evidence_json",
|
||
"created_at",
|
||
],
|
||
},
|
||
"next_operator_steps": [
|
||
"以只讀 inventory 或人工 DB console 確認 queue row 存在",
|
||
"比對 expected dedupe key 與 review_state=needs_review",
|
||
"只在人工審核流程中記錄 confirmed / rejected / deferred 決策",
|
||
"若 queue row 不存在或 evidence 不一致,停回 writer receipt / closeout",
|
||
],
|
||
"safe_boundaries": [
|
||
"do_not_query_database_from_handoff_preview",
|
||
"do_not_update_review_state_from_api",
|
||
"do_not_insert_missing_queue_row_from_api",
|
||
"do_not_read_approval_token_from_api",
|
||
"do_not_execute_cli_from_handoff_preview",
|
||
"do_not_attach_scheduler_from_handoff_preview",
|
||
"no_remove_orphans",
|
||
"no_momo_db_lifecycle_change",
|
||
],
|
||
}
|