新增市場情報 AI summary output receipt gate
All checks were successful
CD Pipeline / deploy (push) Successful in 1m3s

This commit is contained in:
OoO
2026-05-19 22:41:13 +08:00
parent 2e5ff091fa
commit cc3be7fd4f
11 changed files with 1135 additions and 84 deletions

View File

@@ -0,0 +1,451 @@
"""候選審核 queue AI summary output receipt 預覽。
本模組只驗收人工 Ollama 摘要輸出是否符合 run package schema 與 evidence contract
不呼叫 LLM、不派送 Telegram、不讀 approval token、不執行 CLI、不寫檔、
不寫 DB、不更新 review_state、不 commit、不掛 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"
TARGET_TABLE = "market_alert_review_queue"
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 _safe_text(value, limit=300):
if value is None:
return None
text = str(value).strip()
return text[:limit] if text else None
def _side_effects_clear(*payloads):
blocked_keys = (
"api_executes_cli",
"api_executes_llm",
"api_reads_approval_token",
"api_writes_file",
"api_writes_database",
"api_updates_review_state",
"run_package_file_written",
"summary_file_written",
"summary_record_written",
"summary_manifest_written",
"summary_receipt_file_written",
"database_write_executed",
"database_commit_executed",
"review_state_update_executed",
"llm_call_executed",
"ollama_call_executed",
"gemini_call_executed",
"telegram_dispatched",
"ai_summary_generated",
"external_network_executed",
"scheduler_attached",
"writes_executed",
"would_write_database",
)
return all(
not _as_dict(payload).get(key)
for payload in payloads
for key in blocked_keys
)
def _operator_summary(operator_evidence):
operator_evidence = _as_dict(operator_evidence)
output = operator_evidence.get("ai_summary_output") or operator_evidence.get(
"summary_output"
)
return {
"provided_keys": sorted(operator_evidence.keys()),
"operator_confirmed_ai_summary_output_receipt": bool(
operator_evidence.get("operator_confirmed_ai_summary_output_receipt")
or operator_evidence.get("operator_confirmed_summary_output_receipt")
),
"operator_confirmed_manual_output_only": bool(
operator_evidence.get("operator_confirmed_manual_output_only")
or operator_evidence.get("operator_confirmed_manual_ollama_summary_run")
),
"operator_confirmed_ollama_first": bool(
operator_evidence.get("operator_confirmed_ollama_first")
or operator_evidence.get("operator_confirmed_ollama_cascade")
),
"operator_confirmed_gemini_backup_only": bool(
operator_evidence.get("operator_confirmed_gemini_backup_only")
or operator_evidence.get("operator_confirmed_gemini_fallback_only")
),
"operator_confirmed_no_api_llm_call": bool(
operator_evidence.get("operator_confirmed_no_api_llm_call")
or operator_evidence.get("operator_confirmed_no_llm_call")
),
"operator_confirmed_no_telegram_dispatch": bool(
operator_evidence.get("operator_confirmed_no_telegram_dispatch")
),
"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")
),
"ai_summary_output_artifact_path": _safe_text(
operator_evidence.get("ai_summary_output_artifact_path")
or operator_evidence.get("summary_output_artifact_path")
),
"ai_summary_receipt_artifact_path": _safe_text(
operator_evidence.get("ai_summary_receipt_artifact_path")
or operator_evidence.get("summary_receipt_artifact_path")
),
"manual_ai_summary_output_provided": isinstance(output, dict),
"approval_token_submitted_to_api": _contains_forbidden_token_key(
operator_evidence
),
}
def _run_package_payload(ai_summary_run_package):
package = _as_dict(ai_summary_run_package)
prompt = _as_dict(package.get("prompt_contract"))
input_contract = _as_dict(prompt.get("input_contract"))
output_schema = _as_dict(package.get("summary_output_schema"))
return {
"provided": bool(package),
"mode": package.get("mode"),
"ai_summary_run_package_ready": bool(
package.get("ai_summary_run_package_ready")
),
"ready_for_manual_ollama_summary_run": bool(
package.get("ready_for_manual_ollama_summary_run")
),
"model_route_policy": _as_dict(package.get("model_route_policy")),
"prompt_contract_version": prompt.get("contract_version"),
"summary_output_schema": output_schema,
"required_fields": _as_list(output_schema.get("required_fields")),
"expected_dedupe_keys": _as_list(package.get("expected_dedupe_keys")),
"summary_section_keys": _as_list(package.get("summary_section_keys")),
"artifact_paths": _as_dict(input_contract.get("artifact_paths")),
"safe_boundaries": _as_list(package.get("safe_boundaries")),
"blocked_reasons": _as_list(package.get("blocked_reasons")),
"read_only_query_executed": bool(package.get("read_only_query_executed")),
"database_connection_opened": bool(package.get("database_connection_opened")),
"database_write_executed": bool(package.get("database_write_executed")),
"database_commit_executed": bool(package.get("database_commit_executed")),
"review_state_update_executed": bool(
package.get("review_state_update_executed")
),
"llm_call_executed": bool(package.get("llm_call_executed")),
"ollama_call_executed": bool(package.get("ollama_call_executed")),
"gemini_call_executed": bool(package.get("gemini_call_executed")),
"telegram_dispatched": bool(package.get("telegram_dispatched")),
"ai_summary_generated": bool(package.get("ai_summary_generated")),
"scheduler_attached": bool(package.get("scheduler_attached")),
}
def _summary_output(operator_evidence):
output = _as_dict(operator_evidence).get("ai_summary_output") or _as_dict(
operator_evidence
).get("summary_output")
return _as_dict(output)
def _required_field_status(*, output, required_fields):
missing = [field for field in required_fields if field not in output]
empty_allowed = {"risk_flags"}
empty = [
field
for field in required_fields
if field in output
and field not in empty_allowed
and output.get(field) in (None, "", [], {})
]
return missing, empty
def _output_shape_valid(output):
if not output:
return False
if not isinstance(output.get("headline"), str):
return False
if not isinstance(output.get("executive_summary"), str):
return False
for key in ("key_findings", "risk_flags", "recommended_actions", "evidence_refs"):
if not isinstance(output.get(key), list):
return False
return isinstance(output.get("model_route"), dict)
def _allowed_evidence_refs(package):
artifact_paths = package["artifact_paths"]
return sorted(
set(str(item) for item in package["expected_dedupe_keys"])
| set(str(item) for item in package["summary_section_keys"])
| set(str(key) for key in artifact_paths.keys())
| set(str(value) for value in artifact_paths.values() if value)
)
def _evidence_refs_grounded(*, output, package):
refs = [str(item) for item in _as_list(output.get("evidence_refs")) if item]
allowed = set(_allowed_evidence_refs(package))
return bool(refs) and all(ref in allowed for ref in refs), refs
def _model_route_accepted(*, output, package):
model_route = _as_dict(output.get("model_route"))
route_policy = package["model_route_policy"]
cascade = _as_list(route_policy.get("primary_cascade"))
allowed_hosts = {str(_as_dict(node).get("host")) for node in cascade}
provider = model_route.get("provider")
host = model_route.get("host")
fallback_reason = _safe_text(model_route.get("fallback_reason"))
failed_hosts = set(str(host) for host in _as_list(model_route.get("ollama_failed_hosts")))
if route_policy.get("primary_policy") != "ollama_first":
return False
if route_policy.get("app_host_forbidden_as_ollama_node") == host:
return False
if provider == "ollama":
return bool(host in allowed_hosts)
if provider == "gemini_fallback":
return bool(
route_policy.get("fallback_policy")
== "gemini_backup_only_after_ollama_cascade_failure"
and fallback_reason
and len(failed_hosts & allowed_hosts) == len(allowed_hosts)
)
return False
def _receipt_gates(*, package, output, operator):
missing_fields, empty_fields = _required_field_status(
output=output,
required_fields=package["required_fields"],
)
grounded, refs = _evidence_refs_grounded(output=output, package=package)
route_accepted = _model_route_accepted(output=output, package=package)
return [
{
"key": "ai_summary_run_package_ready",
"label": "AI summary run package 必須已通過",
"passed": bool(
package["provided"]
and package["mode"]
== "candidate_queue_review_ai_summary_run_package_preview"
and package["ai_summary_run_package_ready"]
and package["ready_for_manual_ollama_summary_run"]
),
},
{
"key": "manual_ai_summary_output_provided",
"label": "必須提供人工 Ollama 摘要輸出 JSON",
"passed": bool(output and operator["manual_ai_summary_output_provided"]),
},
{
"key": "summary_output_required_fields_present",
"label": "摘要輸出必須包含 schema 必要欄位",
"passed": bool(package["required_fields"] and not missing_fields and not empty_fields),
},
{
"key": "summary_output_shape_valid",
"label": "摘要輸出欄位型別必須符合 contract",
"passed": _output_shape_valid(output),
},
{
"key": "summary_output_evidence_refs_grounded",
"label": "摘要 evidence_refs 必須對應 dedupe key、section 或 artifact",
"passed": grounded,
},
{
"key": "summary_output_model_route_accepted",
"label": "摘要 model_route 必須符合 Ollama-first / Gemini fallback-only",
"passed": route_accepted,
},
{
"key": "operator_confirmed_ai_summary_output_receipt",
"label": "操作員確認本步只驗收人工摘要輸出",
"passed": bool(
operator["operator_confirmed_ai_summary_output_receipt"]
and operator["operator_confirmed_manual_output_only"]
and operator["operator_confirmed_ollama_first"]
and operator["operator_confirmed_gemini_backup_only"]
and operator["operator_confirmed_no_api_llm_call"]
and operator["operator_confirmed_no_telegram_dispatch"]
and operator["operator_confirmed_no_api_db_write"]
and operator["operator_confirmed_no_scheduler_attach"]
),
},
{
"key": "ai_summary_output_receipt_artifacts_recorded",
"label": "summary output 與 receipt artifact path 必須由操作員提供",
"passed": bool(
operator["ai_summary_output_artifact_path"]
and operator["ai_summary_receipt_artifact_path"]
),
},
{
"key": "ai_summary_output_receipt_no_approval_token_submitted_to_api",
"label": "payload 不得包含一次性 approval token key",
"passed": not operator["approval_token_submitted_to_api"],
},
{
"key": "ai_summary_output_receipt_has_no_side_effects",
"label": "receipt 不得呼叫 LLM、派送 Telegram、寫檔、寫 DB 或掛 scheduler",
"passed": _side_effects_clear(package),
},
], {
"missing_fields": missing_fields,
"empty_fields": empty_fields,
"evidence_refs": refs,
"allowed_evidence_refs": _allowed_evidence_refs(package),
"model_route_accepted": route_accepted,
}
def build_candidate_queue_review_ai_summary_output_receipt(
*,
ai_summary_run_package,
operator_evidence=None,
execute_requested=False,
):
"""建立人工 AI summary output receipt 預覽;不呼叫模型或執行副作用。"""
package = _run_package_payload(ai_summary_run_package)
operator = _operator_summary(operator_evidence)
output = _summary_output(operator_evidence)
gates, validation = _receipt_gates(
package=package,
output=output,
operator=operator,
)
blocked_reasons = [gate["key"] for gate in gates if not gate["passed"]]
receipt_ready = bool(not blocked_reasons)
return {
"mode": "candidate_queue_review_ai_summary_output_receipt_preview",
"target_table": TARGET_TABLE,
"target_operation": "review_manual_ollama_ai_summary_output",
"execute_requested": bool(execute_requested),
"ai_summary_output_receipt_ready": receipt_ready,
"ready_for_summary_persistence_review": receipt_ready,
"ready_for_next_manual_phase": receipt_ready,
"manual_ai_summary_output_provided": operator[
"manual_ai_summary_output_provided"
],
"summary_output_schema_valid": bool(
output and not validation["missing_fields"] and not validation["empty_fields"]
),
"summary_output_evidence_refs_grounded": bool(
validation["evidence_refs"]
and all(
ref in set(validation["allowed_evidence_refs"])
for ref in validation["evidence_refs"]
)
),
"summary_output_model_route_accepted": validation["model_route_accepted"],
"ready_for_ai_summary_generation": False,
"ready_for_llm_call": False,
"ready_for_telegram_dispatch": False,
"ready_for_api_review_state_update": False,
"ready_for_api_database_write": False,
"ready_for_scheduler_attach": False,
"api_executes_cli": False,
"api_executes_llm": False,
"api_reads_approval_token": False,
"api_writes_file": False,
"api_writes_database": False,
"api_updates_review_state": False,
"approval_record_written": False,
"decision_record_written": False,
"run_package_file_written": False,
"summary_file_written": False,
"summary_record_written": False,
"summary_manifest_written": False,
"summary_receipt_file_written": False,
"ai_summary_generated": False,
"llm_call_executed": False,
"ollama_call_executed": False,
"gemini_call_executed": False,
"telegram_dispatched": False,
"review_state_update_executed": False,
"read_only_query_executed": package["read_only_query_executed"],
"database_connection_opened": package["database_connection_opened"],
"database_session_created": False,
"explicit_transaction_opened": False,
"transaction_opened": False,
"transaction_committed": 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": package["expected_dedupe_keys"],
"summary_section_keys": package["summary_section_keys"],
"summary_output_required_fields": package["required_fields"],
"summary_output_validation": validation,
"model_route_policy": package["model_route_policy"],
"operator_ai_summary_output_receipt": operator,
"blocked_reasons": blocked_reasons,
"gates": gates,
"next_operator_steps": [
"確認人工摘要 output JSON 已符合 schema 與 evidence_refs",
"若要持久化 summary 或派送 Telegram必須另開 persistence/dispatch gate",
"保留 Ollama host 或 Gemini fallback reason 供後續稽核",
"本 receipt 不寫檔、不寫 DB、不呼叫模型、不發 Telegram",
],
"safe_boundaries": [
"do_not_call_llm_from_ai_summary_output_receipt",
"do_not_dispatch_telegram_from_ai_summary_output_receipt",
"do_not_write_summary_receipt_file_from_api",
"do_not_write_summary_file_from_api",
"do_not_insert_summary_record_from_api",
"do_not_update_review_state_from_ai_summary_output_receipt",
"do_not_read_approval_token_from_ai_summary_output_receipt",
"do_not_execute_cli_from_ai_summary_output_receipt",
"do_not_attach_scheduler_from_ai_summary_output_receipt",
"manual_ai_summary_output_only",
"ollama_cascade_must_use_gcp_a_then_gcp_b_then_111",
"gemini_backup_only_after_ollama_cascade_failure",
"host_188_forbidden_as_ollama_node",
"ai_summary_output_receipt_preview_only",
"no_remove_orphans",
"no_momo_db_lifecycle_change",
],
}

View File

@@ -28,6 +28,7 @@ from services.market_intel.candidate_queue_review_decision_post_closeout_invento
from services.market_intel.candidate_queue_review_completion_archive import build_candidate_queue_review_completion_archive
from services.market_intel.candidate_queue_review_archive_summary import build_candidate_queue_review_archive_summary
from services.market_intel.candidate_queue_review_ai_summary_preflight import build_candidate_queue_review_ai_summary_preflight
from services.market_intel.candidate_queue_review_ai_summary_output_receipt import build_candidate_queue_review_ai_summary_output_receipt
from services.market_intel.candidate_queue_review_ai_summary_run_package import build_candidate_queue_review_ai_summary_run_package
@@ -49,6 +50,7 @@ BLOCKED_RUN_REVIEW_KEYS = (
"summary_file_written",
"summary_record_written",
"summary_manifest_written",
"summary_receipt_file_written",
"ai_summary_generated",
"llm_call_executed",
"ollama_call_executed",
@@ -67,7 +69,7 @@ BLOCKED_RUN_REVIEW_KEYS = (
"writes_executed",
"would_write_database",
)
PRODUCTION_SMOKE_TARGETS = ("/health", "/market_intel", "/api/market_intel/status", "/api/market_intel/deployment_readiness", "/api/market_intel/schema_smoke", "/api/market_intel/schema_db_probe", "/api/market_intel/platform_seed_db_diff", "/api/market_intel/legacy_source_bridge", "/api/market_intel/mcp_readiness", "/api/market_intel/mcp_tool_contract", "/api/market_intel/mcp_deploy_preflight", "/api/market_intel/mcp_activation_runbook", "/api/market_intel/mcp_fetch_gate", "/api/market_intel/scheduler_plan", "/api/market_intel/manual_sample_plan", "/api/market_intel/manual_sample_acceptance", "/api/market_intel/manual_sample_review", "/api/market_intel/match_review_plan", "/api/market_intel/opportunity_plan", "/api/market_intel/opportunity_scoring_plan", "/api/market_intel/opportunity_evidence_plan", "/api/market_intel/opportunity_alert_plan", "/api/market_intel/migration_apply_drill", "/api/market_intel/migration_catalog_review", "/api/market_intel/migration_live_smoke", "/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", "/api/market_intel/manual_sample_review/candidate_queue_writer_run_readiness", "/api/market_intel/manual_sample_review/candidate_queue_writer_run_receipt", "/api/market_intel/manual_sample_review/candidate_queue_writer_run_closeout", "/api/market_intel/manual_sample_review/candidate_queue_review_handoff", "/api/market_intel/manual_sample_review/candidate_queue_review_inventory", "/api/market_intel/manual_sample_review/candidate_queue_review_decision", "/api/market_intel/manual_sample_review/candidate_queue_review_decision_approval", "/api/market_intel/manual_sample_review/candidate_queue_review_decision_transaction", "/api/market_intel/manual_sample_review/candidate_queue_review_decision_writer_preflight", "/api/market_intel/manual_sample_review/candidate_queue_review_decision_writer_postwrite_smoke", "/api/market_intel/manual_sample_review/candidate_queue_review_decision_writer_operator_drill", "/api/market_intel/manual_sample_review/candidate_queue_review_decision_writer_run_package", "/api/market_intel/manual_sample_review/candidate_queue_review_decision_writer_run_readiness", "/api/market_intel/manual_sample_review/candidate_queue_review_decision_writer_run_receipt", "/api/market_intel/manual_sample_review/candidate_queue_review_decision_writer_run_closeout", "/api/market_intel/manual_sample_review/candidate_queue_review_decision_post_closeout_inventory", "/api/market_intel/manual_sample_review/candidate_queue_review_completion_archive", "/api/market_intel/manual_sample_review/candidate_queue_review_archive_summary", "/api/market_intel/manual_sample_review/candidate_queue_review_ai_summary_preflight", "/api/market_intel/manual_sample_review/candidate_queue_review_ai_summary_run_package", "/api/market_intel/manual_sample_review/candidate_queue_review_decision_writer_status")
PRODUCTION_SMOKE_TARGETS = ("/health", "/market_intel", "/api/market_intel/status", "/api/market_intel/deployment_readiness", "/api/market_intel/schema_smoke", "/api/market_intel/schema_db_probe", "/api/market_intel/platform_seed_db_diff", "/api/market_intel/legacy_source_bridge", "/api/market_intel/mcp_readiness", "/api/market_intel/mcp_tool_contract", "/api/market_intel/mcp_deploy_preflight", "/api/market_intel/mcp_activation_runbook", "/api/market_intel/mcp_fetch_gate", "/api/market_intel/scheduler_plan", "/api/market_intel/manual_sample_plan", "/api/market_intel/manual_sample_acceptance", "/api/market_intel/manual_sample_review", "/api/market_intel/match_review_plan", "/api/market_intel/opportunity_plan", "/api/market_intel/opportunity_scoring_plan", "/api/market_intel/opportunity_evidence_plan", "/api/market_intel/opportunity_alert_plan", "/api/market_intel/migration_apply_drill", "/api/market_intel/migration_catalog_review", "/api/market_intel/migration_live_smoke", "/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", "/api/market_intel/manual_sample_review/candidate_queue_writer_run_readiness", "/api/market_intel/manual_sample_review/candidate_queue_writer_run_receipt", "/api/market_intel/manual_sample_review/candidate_queue_writer_run_closeout", "/api/market_intel/manual_sample_review/candidate_queue_review_handoff", "/api/market_intel/manual_sample_review/candidate_queue_review_inventory", "/api/market_intel/manual_sample_review/candidate_queue_review_decision", "/api/market_intel/manual_sample_review/candidate_queue_review_decision_approval", "/api/market_intel/manual_sample_review/candidate_queue_review_decision_transaction", "/api/market_intel/manual_sample_review/candidate_queue_review_decision_writer_preflight", "/api/market_intel/manual_sample_review/candidate_queue_review_decision_writer_postwrite_smoke", "/api/market_intel/manual_sample_review/candidate_queue_review_decision_writer_operator_drill", "/api/market_intel/manual_sample_review/candidate_queue_review_decision_writer_run_package", "/api/market_intel/manual_sample_review/candidate_queue_review_decision_writer_run_readiness", "/api/market_intel/manual_sample_review/candidate_queue_review_decision_writer_run_receipt", "/api/market_intel/manual_sample_review/candidate_queue_review_decision_writer_run_closeout", "/api/market_intel/manual_sample_review/candidate_queue_review_decision_post_closeout_inventory", "/api/market_intel/manual_sample_review/candidate_queue_review_completion_archive", "/api/market_intel/manual_sample_review/candidate_queue_review_archive_summary", "/api/market_intel/manual_sample_review/candidate_queue_review_ai_summary_preflight", "/api/market_intel/manual_sample_review/candidate_queue_review_ai_summary_run_package", "/api/market_intel/manual_sample_review/candidate_queue_review_ai_summary_output_receipt", "/api/market_intel/manual_sample_review/candidate_queue_review_decision_writer_status")
def _run_review_preview_safe(payload, mode):
@@ -222,6 +224,9 @@ def build_deployment_readiness_preview(
archive_summary=candidate_queue_review_archive_summary,
ai_summary_preflight=candidate_queue_review_ai_summary_preflight,
)
candidate_queue_review_ai_summary_output_receipt = build_candidate_queue_review_ai_summary_output_receipt(
ai_summary_run_package=candidate_queue_review_ai_summary_run_package,
)
checks = {
"schema_smoke_passed": bool(schema_smoke["passed"]),
"feature_flags_default_safe": bool(
@@ -508,6 +513,10 @@ def build_deployment_readiness_preview(
candidate_queue_review_ai_summary_run_package,
"candidate_queue_review_ai_summary_run_package_preview",
),
"candidate_queue_review_ai_summary_output_receipt_preview_safe": _run_review_preview_safe(
candidate_queue_review_ai_summary_output_receipt,
"candidate_queue_review_ai_summary_output_receipt_preview",
),
"candidate_queue_review_decision_writer_cli_status_safe": _run_review_preview_safe(
candidate_queue_review_decision_writer_status,
"candidate_queue_review_decision_writer_cli_blocked",
@@ -756,6 +765,7 @@ def build_deployment_readiness_preview(
"candidate_queue_review_archive_summary": candidate_queue_review_archive_summary,
"candidate_queue_review_ai_summary_preflight": candidate_queue_review_ai_summary_preflight,
"candidate_queue_review_ai_summary_run_package": candidate_queue_review_ai_summary_run_package,
"candidate_queue_review_ai_summary_output_receipt": candidate_queue_review_ai_summary_output_receipt,
"candidate_queue_review_decision_writer_status": candidate_queue_review_decision_writer_status,
"match_review_plan": match_review_plan,
"opportunity_plan": opportunity_plan,

View File

@@ -1,3 +1,3 @@
"""市場情報 rollout phase 單一來源。"""
MARKET_INTEL_PHASE = "phase_81_candidate_queue_review_ai_summary_run_package"
MARKET_INTEL_PHASE = "phase_82_candidate_queue_review_ai_summary_output_receipt"