新增市場情報 report archive gate
All checks were successful
CD Pipeline / deploy (push) Successful in 1m3s

This commit is contained in:
OoO
2026-05-20 14:24:12 +08:00
parent 396a51dd7f
commit 9c45a87723
10 changed files with 1152 additions and 105 deletions

View File

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

View File

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