新增市場情報 report archive gate
All checks were successful
CD Pipeline / deploy (push) Successful in 1m3s
All checks were successful
CD Pipeline / deploy (push) Successful in 1m3s
This commit is contained in:
@@ -0,0 +1,456 @@
|
||||
"""候選審核 queue AI summary Telegram dispatch report archive preview。
|
||||
|
||||
本模組只在 report closeout 通過後整理 report archive gate;不讀 approval
|
||||
或 Telegram token、不呼叫 LLM、不產報表、不派送 Telegram、不開 DB、不寫檔、
|
||||
不更新 review_state、不掛 scheduler。
|
||||
"""
|
||||
|
||||
from services.market_intel.candidate_queue_review_ai_summary_persistence_telegram_dispatch_report_closeout import (
|
||||
FALSE_RESPONSE_KEYS as CLOSEOUT_FALSE_RESPONSE_KEYS,
|
||||
SAFE_APPROVAL_ENV_VAR,
|
||||
SAFE_TOKEN_METADATA_KEYS,
|
||||
TARGET_COLUMN,
|
||||
TARGET_TABLE,
|
||||
_as_dict,
|
||||
_as_list,
|
||||
_contains_forbidden_token_key,
|
||||
_has_text,
|
||||
_safe_int,
|
||||
_safe_text,
|
||||
)
|
||||
|
||||
|
||||
REQUIRED_CLOSEOUT_BOUNDARIES = {
|
||||
"do_not_read_approval_token_from_report_closeout_api",
|
||||
"do_not_read_telegram_token_from_report_closeout_api",
|
||||
"do_not_call_llm_from_report_closeout",
|
||||
"do_not_generate_report_from_report_closeout_api",
|
||||
"do_not_write_report_artifact_from_report_closeout_api",
|
||||
"do_not_dispatch_telegram_from_report_closeout_api",
|
||||
"do_not_open_database_connection_from_report_closeout",
|
||||
"do_not_update_review_state_from_report_closeout",
|
||||
"do_not_attach_scheduler_from_report_closeout",
|
||||
"future_market_intel_report_archive_must_use_separate_gate",
|
||||
}
|
||||
SUMMARY_METADATA_KEY = "ai_summary_review"
|
||||
SAFE_TOKEN_BOOLEAN_KEYS = {
|
||||
"api_reads_approval_token",
|
||||
"approval_or_telegram_token_key_detected",
|
||||
"operator_confirmed_no_token_in_report_closeout",
|
||||
"operator_confirmed_no_token_in_report_archive",
|
||||
"operator_confirmed_no_token_in_artifacts",
|
||||
}
|
||||
FALSE_RESPONSE_KEYS = tuple(
|
||||
dict.fromkeys(
|
||||
CLOSEOUT_FALSE_RESPONSE_KEYS
|
||||
+ (
|
||||
"telegram_dispatch_report_archive_file_written",
|
||||
"report_archive_file_written",
|
||||
"report_archive_record_written",
|
||||
"report_archive_manifest_written",
|
||||
"report_archive_summary_file_written",
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def _strip_safe_token_boolean_keys(value):
|
||||
if isinstance(value, dict):
|
||||
return {
|
||||
key: _strip_safe_token_boolean_keys(nested)
|
||||
for key, nested in value.items()
|
||||
if not (
|
||||
str(key).lower() in SAFE_TOKEN_BOOLEAN_KEYS
|
||||
and isinstance(nested, bool)
|
||||
)
|
||||
}
|
||||
if isinstance(value, list):
|
||||
return [_strip_safe_token_boolean_keys(item) for item in value]
|
||||
return value
|
||||
|
||||
|
||||
def _closeout_summary(report_closeout):
|
||||
closeout_payload = _as_dict(report_closeout)
|
||||
receipt = _as_dict(
|
||||
closeout_payload.get("telegram_dispatch_report_run_receipt_summary")
|
||||
)
|
||||
operator_closeout = _as_dict(
|
||||
closeout_payload.get("operator_telegram_dispatch_report_closeout")
|
||||
)
|
||||
promotion = _as_dict(closeout_payload.get("promotion_gate"))
|
||||
safe_boundaries = set(
|
||||
str(item) for item in _as_list(closeout_payload.get("safe_boundaries"))
|
||||
)
|
||||
missing_boundaries = sorted(REQUIRED_CLOSEOUT_BOUNDARIES - safe_boundaries)
|
||||
|
||||
return {
|
||||
"provided": bool(closeout_payload),
|
||||
"mode": closeout_payload.get("mode"),
|
||||
"target_operation": closeout_payload.get("target_operation"),
|
||||
"closeout_passed": bool(
|
||||
closeout_payload.get("telegram_dispatch_report_closeout_passed")
|
||||
or closeout_payload.get(
|
||||
"summary_persistence_telegram_dispatch_report_closeout_passed"
|
||||
)
|
||||
or closeout_payload.get("report_closeout_passed")
|
||||
),
|
||||
"ready_for_market_intel_report_archive": bool(
|
||||
closeout_payload.get("ready_for_market_intel_report_archive")
|
||||
),
|
||||
"ready_for_next_manual_phase": bool(
|
||||
closeout_payload.get("ready_for_next_manual_phase")
|
||||
),
|
||||
"statement_count": _safe_int(closeout_payload.get("statement_count")),
|
||||
"expected_summary_payload_hash": _safe_text(
|
||||
closeout_payload.get("expected_summary_payload_hash")
|
||||
or receipt.get("expected_summary_payload_hash"),
|
||||
80,
|
||||
),
|
||||
"report_receipt_mode": receipt.get("report_receipt_mode"),
|
||||
"target_report_family": receipt.get("target_report_family"),
|
||||
"language": receipt.get("language"),
|
||||
"report_output_artifact_path": receipt.get("report_output_artifact_path"),
|
||||
"report_output_hash": receipt.get("report_output_hash"),
|
||||
"summary_payload_hash": receipt.get("summary_payload_hash"),
|
||||
"summary_payload_hash_matches_expected": bool(
|
||||
receipt.get("summary_payload_hash_matches_expected")
|
||||
),
|
||||
"required_report_sections_present": bool(
|
||||
receipt.get("required_report_sections_present")
|
||||
),
|
||||
"section_keys": _as_list(receipt.get("section_keys")),
|
||||
"report_closeout_artifact_path_recorded": bool(
|
||||
operator_closeout.get("report_closeout_artifact_path_recorded")
|
||||
),
|
||||
"report_run_receipt_artifact_path_recorded": bool(
|
||||
operator_closeout.get("report_run_receipt_artifact_path_recorded")
|
||||
),
|
||||
"report_output_artifact_path_recorded": bool(
|
||||
operator_closeout.get("report_output_artifact_path_recorded")
|
||||
),
|
||||
"operator_confirmed_report_closeout": bool(
|
||||
operator_closeout.get("operator_confirmed_report_closeout")
|
||||
),
|
||||
"operator_confirmed_report_receipt_archived": bool(
|
||||
operator_closeout.get("operator_confirmed_report_receipt_archived")
|
||||
),
|
||||
"operator_confirmed_report_output_hash_matched": bool(
|
||||
operator_closeout.get("operator_confirmed_report_output_hash_matched")
|
||||
),
|
||||
"operator_confirmed_report_sections_reviewed": bool(
|
||||
operator_closeout.get("operator_confirmed_report_sections_reviewed")
|
||||
),
|
||||
"operator_confirmed_report_archive_requires_separate_gate": bool(
|
||||
operator_closeout.get(
|
||||
"operator_confirmed_report_archive_requires_separate_gate"
|
||||
)
|
||||
),
|
||||
"promotion_allowed": bool(promotion.get("allowed")),
|
||||
"promotion_next_manual_phase": promotion.get("next_manual_phase"),
|
||||
"promotion_archive_separate_gate": bool(
|
||||
promotion.get("report_archive_requires_separate_gate")
|
||||
),
|
||||
"safe_boundaries_complete": not missing_boundaries,
|
||||
"missing_safe_boundaries": missing_boundaries,
|
||||
"forbidden_token_key_detected": _contains_forbidden_token_key(
|
||||
_strip_safe_token_boolean_keys(closeout_payload)
|
||||
),
|
||||
"blocked_count": len(_as_list(closeout_payload.get("blocked_reasons"))),
|
||||
**{key: bool(closeout_payload.get(key)) for key in FALSE_RESPONSE_KEYS},
|
||||
}
|
||||
|
||||
|
||||
def _operator_archive_summary(operator_evidence):
|
||||
operator_evidence = _as_dict(operator_evidence)
|
||||
return {
|
||||
"provided_keys": sorted(operator_evidence.keys()),
|
||||
"report_archive_artifact_path_recorded": _has_text(
|
||||
operator_evidence.get("report_archive_artifact_path")
|
||||
or operator_evidence.get("telegram_dispatch_report_archive_artifact_path")
|
||||
or operator_evidence.get("market_intel_report_archive_artifact_path")
|
||||
),
|
||||
"report_closeout_artifact_path_recorded": _has_text(
|
||||
operator_evidence.get("report_closeout_artifact_path")
|
||||
or operator_evidence.get("telegram_dispatch_report_closeout_artifact_path")
|
||||
),
|
||||
"report_run_receipt_artifact_path_recorded": _has_text(
|
||||
operator_evidence.get("report_run_receipt_artifact_path")
|
||||
or operator_evidence.get(
|
||||
"telegram_dispatch_report_run_receipt_artifact_path"
|
||||
)
|
||||
),
|
||||
"report_output_artifact_path_recorded": _has_text(
|
||||
operator_evidence.get("report_output_artifact_path")
|
||||
or operator_evidence.get("telegram_dispatch_report_output_artifact_path")
|
||||
),
|
||||
"operator_confirmed_report_archive": bool(
|
||||
operator_evidence.get("operator_confirmed_report_archive")
|
||||
or operator_evidence.get("operator_confirmed_market_intel_report_archive")
|
||||
),
|
||||
"operator_confirmed_report_closeout_archived": bool(
|
||||
operator_evidence.get("operator_confirmed_report_closeout_archived")
|
||||
or operator_evidence.get("operator_confirmed_closeout_archived")
|
||||
),
|
||||
"operator_confirmed_report_receipt_archived": bool(
|
||||
operator_evidence.get("operator_confirmed_report_receipt_archived")
|
||||
or operator_evidence.get("operator_confirmed_receipt_archived")
|
||||
),
|
||||
"operator_confirmed_report_output_archived": bool(
|
||||
operator_evidence.get("operator_confirmed_report_output_archived")
|
||||
or operator_evidence.get("operator_confirmed_output_archived")
|
||||
),
|
||||
"operator_confirmed_report_output_hash_matched": bool(
|
||||
operator_evidence.get("operator_confirmed_report_output_hash_matched")
|
||||
or operator_evidence.get("operator_confirmed_summary_hash_matched")
|
||||
),
|
||||
"operator_confirmed_report_sections_reviewed": bool(
|
||||
operator_evidence.get("operator_confirmed_report_sections_reviewed")
|
||||
),
|
||||
"operator_confirmed_archive_manifest_reviewed": bool(
|
||||
operator_evidence.get("operator_confirmed_archive_manifest_reviewed")
|
||||
),
|
||||
"operator_confirmed_archive_retention_policy_recorded": bool(
|
||||
operator_evidence.get(
|
||||
"operator_confirmed_archive_retention_policy_recorded"
|
||||
)
|
||||
),
|
||||
"operator_confirmed_report_archive_summary_requires_separate_gate": bool(
|
||||
operator_evidence.get(
|
||||
"operator_confirmed_report_archive_summary_requires_separate_gate"
|
||||
)
|
||||
),
|
||||
"operator_confirmed_no_token_in_report_archive": bool(
|
||||
operator_evidence.get("operator_confirmed_no_token_in_report_archive")
|
||||
or operator_evidence.get("operator_confirmed_no_token_in_artifacts")
|
||||
),
|
||||
"operator_confirmed_no_api_report_generation": bool(
|
||||
operator_evidence.get("operator_confirmed_no_api_report_generation")
|
||||
or operator_evidence.get("operator_confirmed_no_api_report_write")
|
||||
),
|
||||
"operator_confirmed_no_api_file_write": bool(
|
||||
operator_evidence.get("operator_confirmed_no_api_file_write")
|
||||
or operator_evidence.get("operator_confirmed_archive_written_outside_api")
|
||||
),
|
||||
"operator_confirmed_no_api_telegram_dispatch": bool(
|
||||
operator_evidence.get("operator_confirmed_no_api_telegram_dispatch")
|
||||
),
|
||||
"operator_confirmed_no_api_db_write": bool(
|
||||
operator_evidence.get("operator_confirmed_no_api_db_write")
|
||||
),
|
||||
"operator_confirmed_no_llm_call": bool(
|
||||
operator_evidence.get("operator_confirmed_no_llm_call")
|
||||
),
|
||||
"operator_confirmed_no_scheduler_attach": bool(
|
||||
operator_evidence.get("operator_confirmed_no_scheduler_attach")
|
||||
),
|
||||
"archive_notes_recorded": _has_text(
|
||||
operator_evidence.get("report_archive_notes")
|
||||
or operator_evidence.get("archive_notes")
|
||||
),
|
||||
"safe_token_metadata_only": all(
|
||||
key in SAFE_TOKEN_METADATA_KEYS or key == "approval_env_var"
|
||||
for key in operator_evidence
|
||||
if "token" in str(key).lower() or str(key).lower() == "approval_env_var"
|
||||
)
|
||||
and operator_evidence.get("approval_env_var", SAFE_APPROVAL_ENV_VAR)
|
||||
== SAFE_APPROVAL_ENV_VAR,
|
||||
"forbidden_token_submitted_to_api": _contains_forbidden_token_key(
|
||||
operator_evidence
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
def _archive_gates(closeout, operator, apply_real_write):
|
||||
return [
|
||||
{
|
||||
"key": "report_closeout_preview_provided",
|
||||
"label": "必須提供上一階段 report closeout preview",
|
||||
"passed": bool(
|
||||
closeout["provided"]
|
||||
and closeout["mode"]
|
||||
== "candidate_queue_review_ai_summary_persistence_telegram_dispatch_report_closeout_preview"
|
||||
),
|
||||
},
|
||||
{
|
||||
"key": "report_closeout_passed",
|
||||
"label": "report closeout 必須已通過",
|
||||
"passed": closeout["closeout_passed"],
|
||||
},
|
||||
{
|
||||
"key": "report_closeout_ready_for_archive",
|
||||
"label": "report closeout 必須只放行到 report archive",
|
||||
"passed": bool(
|
||||
closeout["ready_for_market_intel_report_archive"]
|
||||
and closeout["ready_for_next_manual_phase"]
|
||||
and closeout["promotion_allowed"]
|
||||
and closeout["promotion_next_manual_phase"]
|
||||
== "market_intel_report_archive"
|
||||
and closeout["promotion_archive_separate_gate"]
|
||||
),
|
||||
},
|
||||
{
|
||||
"key": "report_closeout_artifacts_verified",
|
||||
"label": "closeout 必須保留 report output、receipt 與 closeout artifact trace",
|
||||
"passed": bool(
|
||||
closeout["report_output_artifact_path"]
|
||||
and closeout["report_output_hash"]
|
||||
and closeout["summary_payload_hash_matches_expected"]
|
||||
and closeout["required_report_sections_present"]
|
||||
and closeout["report_closeout_artifact_path_recorded"]
|
||||
and closeout["report_run_receipt_artifact_path_recorded"]
|
||||
and closeout["report_output_artifact_path_recorded"]
|
||||
and closeout["operator_confirmed_report_closeout"]
|
||||
and closeout["operator_confirmed_report_receipt_archived"]
|
||||
and closeout["operator_confirmed_report_output_hash_matched"]
|
||||
and closeout["operator_confirmed_report_sections_reviewed"]
|
||||
),
|
||||
},
|
||||
{
|
||||
"key": "report_closeout_runtime_boundaries_clear",
|
||||
"label": "closeout 不得顯示 API 產報表、寫檔、DB、Telegram、LLM 或 scheduler 副作用",
|
||||
"passed": all(not closeout[key] for key in FALSE_RESPONSE_KEYS),
|
||||
},
|
||||
{
|
||||
"key": "report_closeout_safe_boundaries_complete",
|
||||
"label": "closeout 必須保留 archive separate gate 與 runtime 安全邊界",
|
||||
"passed": closeout["safe_boundaries_complete"],
|
||||
},
|
||||
{
|
||||
"key": "report_closeout_no_token_key",
|
||||
"label": "closeout payload 不得包含 approval 或 Telegram token key",
|
||||
"passed": not closeout["forbidden_token_key_detected"],
|
||||
},
|
||||
{
|
||||
"key": "report_archive_artifacts_recorded",
|
||||
"label": "操作員需記錄 archive、closeout、receipt 與 report output artifact path",
|
||||
"passed": bool(
|
||||
operator["report_archive_artifact_path_recorded"]
|
||||
and operator["report_closeout_artifact_path_recorded"]
|
||||
and operator["report_run_receipt_artifact_path_recorded"]
|
||||
and operator["report_output_artifact_path_recorded"]
|
||||
),
|
||||
},
|
||||
{
|
||||
"key": "operator_confirmed_report_archive",
|
||||
"label": "操作員確認 report archive、closeout/receipt/output 都已封存",
|
||||
"passed": bool(
|
||||
operator["operator_confirmed_report_archive"]
|
||||
and operator["operator_confirmed_report_closeout_archived"]
|
||||
and operator["operator_confirmed_report_receipt_archived"]
|
||||
and operator["operator_confirmed_report_output_archived"]
|
||||
),
|
||||
},
|
||||
{
|
||||
"key": "operator_confirmed_report_archive_integrity",
|
||||
"label": "操作員確認 hash、章節、archive manifest 與 retention policy",
|
||||
"passed": bool(
|
||||
operator["operator_confirmed_report_output_hash_matched"]
|
||||
and operator["operator_confirmed_report_sections_reviewed"]
|
||||
and operator["operator_confirmed_archive_manifest_reviewed"]
|
||||
and operator["operator_confirmed_archive_retention_policy_recorded"]
|
||||
),
|
||||
},
|
||||
{
|
||||
"key": "operator_confirmed_report_archive_summary_requires_separate_gate",
|
||||
"label": "操作員確認 archive summary 必須另開 gate",
|
||||
"passed": operator[
|
||||
"operator_confirmed_report_archive_summary_requires_separate_gate"
|
||||
],
|
||||
},
|
||||
{
|
||||
"key": "operator_confirmed_report_archive_runtime_boundaries",
|
||||
"label": "操作員確認本 API 不產報表、不寫檔、不派送 Telegram、不寫 DB、不呼叫 LLM、不掛 scheduler",
|
||||
"passed": bool(
|
||||
operator["operator_confirmed_no_token_in_report_archive"]
|
||||
and operator["operator_confirmed_no_api_report_generation"]
|
||||
and operator["operator_confirmed_no_api_file_write"]
|
||||
and operator["operator_confirmed_no_api_telegram_dispatch"]
|
||||
and operator["operator_confirmed_no_api_db_write"]
|
||||
and operator["operator_confirmed_no_llm_call"]
|
||||
and operator["operator_confirmed_no_scheduler_attach"]
|
||||
),
|
||||
},
|
||||
{
|
||||
"key": "report_archive_no_token_submitted_to_api",
|
||||
"label": "archive payload 不得包含 approval 或 Telegram token key",
|
||||
"passed": not operator["forbidden_token_submitted_to_api"],
|
||||
},
|
||||
{
|
||||
"key": "report_archive_apply_real_write_not_requested_from_api",
|
||||
"label": "API/UI report archive 不接受 apply_real_write",
|
||||
"passed": not apply_real_write,
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
def build_candidate_queue_review_ai_summary_persistence_telegram_dispatch_report_archive(
|
||||
*,
|
||||
telegram_dispatch_report_closeout,
|
||||
operator_evidence=None,
|
||||
execute_requested=False,
|
||||
apply_real_write=False,
|
||||
):
|
||||
"""建立 Telegram dispatch report archive gate;不執行任何副作用。"""
|
||||
closeout = _closeout_summary(telegram_dispatch_report_closeout)
|
||||
operator = _operator_archive_summary(operator_evidence)
|
||||
gates = _archive_gates(closeout, operator, bool(apply_real_write))
|
||||
blocked_reasons = [gate["key"] for gate in gates if not gate["passed"]]
|
||||
archive_passed = bool(not blocked_reasons)
|
||||
|
||||
return {
|
||||
"mode": "candidate_queue_review_ai_summary_persistence_telegram_dispatch_report_archive_preview",
|
||||
"target_table": TARGET_TABLE,
|
||||
"target_column": TARGET_COLUMN,
|
||||
"target_json_path": [
|
||||
SUMMARY_METADATA_KEY,
|
||||
"telegram_dispatch_report_archive",
|
||||
],
|
||||
"target_operation": "archive_manual_market_intel_report",
|
||||
"execute_requested": bool(execute_requested),
|
||||
"apply_real_write_requested": bool(apply_real_write),
|
||||
"report_archive_reviewed": True,
|
||||
"telegram_dispatch_report_archive_passed": archive_passed,
|
||||
"summary_persistence_telegram_dispatch_report_archive_passed": archive_passed,
|
||||
"report_archive_passed": archive_passed,
|
||||
"ready_for_next_manual_phase": archive_passed,
|
||||
"ready_for_market_intel_report_archive_summary": archive_passed,
|
||||
**{key: False for key in FALSE_RESPONSE_KEYS},
|
||||
"statement_count": closeout["statement_count"],
|
||||
"expected_summary_payload_hash": closeout["expected_summary_payload_hash"],
|
||||
"blocked_reasons": blocked_reasons,
|
||||
"gates": gates,
|
||||
"telegram_dispatch_report_closeout_summary": closeout,
|
||||
"operator_telegram_dispatch_report_archive": operator,
|
||||
"promotion_gate": {
|
||||
"allowed": archive_passed,
|
||||
"next_manual_phase": "market_intel_report_archive_summary",
|
||||
"requires_real_db_write": False,
|
||||
"requires_scheduler_attach": False,
|
||||
"requires_operator_approval": True,
|
||||
"api_must_not_generate_report": True,
|
||||
"api_must_not_write_archive_file": True,
|
||||
"api_must_not_dispatch_telegram": True,
|
||||
"report_archive_summary_requires_separate_gate": True,
|
||||
},
|
||||
"next_operator_steps": [
|
||||
"保存 report archive artifact path 與 retention policy 註記",
|
||||
"確認 closeout、receipt、report output artifact 與 hash 都已封存",
|
||||
"下一階段 report archive summary 才整理長期查閱摘要;本 API 不寫檔、不補產報表",
|
||||
"若任何 archive gate 阻擋,停在 closeout 階段並保留 feature flags 關閉",
|
||||
],
|
||||
"safe_boundaries": [
|
||||
"do_not_read_approval_token_from_report_archive_api",
|
||||
"do_not_read_telegram_token_from_report_archive_api",
|
||||
"do_not_call_llm_from_report_archive",
|
||||
"do_not_generate_report_from_report_archive_api",
|
||||
"do_not_write_archive_artifact_from_report_archive_api",
|
||||
"do_not_dispatch_telegram_from_report_archive_api",
|
||||
"do_not_open_database_connection_from_report_archive",
|
||||
"do_not_update_review_state_from_report_archive",
|
||||
"do_not_attach_scheduler_from_report_archive",
|
||||
"future_market_intel_report_archive_summary_must_use_separate_gate",
|
||||
"report_archive_preview_only",
|
||||
"no_remove_orphans",
|
||||
"no_momo_db_lifecycle_change",
|
||||
],
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
@@ -1,3 +1,3 @@
|
||||
"""市場情報 rollout phase 單一來源。"""
|
||||
|
||||
MARKET_INTEL_PHASE = "phase_101_candidate_queue_review_ai_summary_persistence_telegram_dispatch_report_closeout"
|
||||
MARKET_INTEL_PHASE = "phase_102_candidate_queue_review_ai_summary_persistence_telegram_dispatch_report_archive"
|
||||
|
||||
Reference in New Issue
Block a user