新增市場情報 review archive summary gate

This commit is contained in:
OoO
2026-05-19 21:37:47 +08:00
parent eb5558f1e7
commit 6622aa458b
8 changed files with 477 additions and 22 deletions

View File

@@ -153,6 +153,7 @@
- Phase 76 candidate queue review decision writer run closeout新增 `services/market_intel/candidate_queue_review_decision_writer_run_closeout.py`、POST `/api/market_intel/manual_sample_review/candidate_queue_review_decision_writer_run_closeout` 與 UI closeout 按鈕,在 review_state receipt 通過後整理 closeout gate、操作員 closeout artifact、post-closeout read-only inventory 確認與 promotion 摘要API/UI 不回吐 receipt 原文、不讀 token、不執行 CLI、不連 DB、不更新 review_state、不 commit、不掛 scheduler版本同步至 V10.268。
- Phase 77 candidate queue review decision post-closeout inventory新增 `services/market_intel/candidate_queue_review_decision_post_closeout_inventory.py`、`routes/market_intel_review_post_routes.py`、POST `/api/market_intel/manual_sample_review/candidate_queue_review_decision_post_closeout_inventory` 與 UI inventory 按鈕,在 review_state closeout 後整理 post-write smoke、live inventory、dedupe key 與 review_state 結果API/UI 不讀 token、不執行 CLI、不更新 review_state、不寫 DB、不 commit、不掛 scheduler版本同步至 V10.270。
- Phase 78 candidate queue review completion archive新增 `services/market_intel/candidate_queue_review_completion_archive.py`、POST `/api/market_intel/manual_sample_review/candidate_queue_review_completion_archive` 與 UI archive 按鈕,在 post-closeout inventory 通過後整理 receipt、closeout、inventory、dedupe key、review_state row snapshot 與 artifact path manifestAPI/UI 不寫檔、不讀 token、不執行 CLI、不更新 review_state、不寫 DB、不 commit、不掛 scheduler版本同步至 V10.273。
- Phase 79 candidate queue review archive summary新增 `services/market_intel/candidate_queue_review_archive_summary.py`、POST `/api/market_intel/manual_sample_review/candidate_queue_review_archive_summary` 與 UI summary 按鈕,在 review completion archive 後整理可供摘要/報表審核的結構化輸入API/UI 不呼叫 LLM、不派送 Telegram、不寫檔、不讀 token、不執行 CLI、不更新 review_state、不寫 DB、不 commit、不掛 scheduler版本同步至 V10.275。
- V10.248 補市場情報 390px preview panel QAsample review 工具列改為 textarea + 可換行 action rail移除舊的硬編 8 欄 grid`check_responsive_overflow` 新增 `--screenshot-all`,本機 390x844 `/market_intel` 真頁面 QA 通過且 overflow=0。
- V10.250 補 Code Review Gemini 備援遙測護欄Ollama 主路徑失敗時 `fallback_to` 明確指向 `code_review_openclaw_gemini`測試鎖住「Gemini 不得記成 `code_review_openclaw` 主 caller」AI Calls 觀測台會把 legacy `code_review_openclaw + gemini` 顯示成 Gemini 備援,避免誤判 Gemini-first。
- Schema smoke`tests/test_market_intel_skeleton.py` 檢查 `Base.metadata` 內含 ADR-035 八張 `market_*` tables。

View File

@@ -320,7 +320,7 @@ YOUTUBE_API_KEY = os.getenv('YOUTUBE_API_KEY', '')
# ==========================================
# 系統版本與路徑
# ==========================================
SYSTEM_VERSION = "V10.274"
SYSTEM_VERSION = "V10.275"
LOG_FILE_PATH = os.path.join(BASE_DIR, 'logs/system.log')
public_url = PUBLIC_URL # 用於模板顯示

View File

@@ -204,6 +204,7 @@ EwoooC 目前已有 MOMO EDM / 節慶活動資料、`promo_products`、PChome
- 2026-05-19 追加 candidate queue review decision writer run closeout`services.market_intel.candidate_queue_review_decision_writer_run_closeout``/api/market_intel/manual_sample_review/candidate_queue_review_decision_writer_run_closeout` 在 review_state receipt 通過後整理 closeout gate、操作員 closeout artifact、post-closeout read-only inventory 確認與 promotion 摘要。此 closeout 只允許放行到後續人工/只讀檢查API/UI 不回吐 receipt 原文、不讀 approval token、不執行 CLI、不連 DB、不更新 `review_state`、不 commit、不掛 scheduler。
- 2026-05-19 追加 candidate queue review decision post-closeout inventory`services.market_intel.candidate_queue_review_decision_post_closeout_inventory``routes.market_intel_review_post_routes``/api/market_intel/manual_sample_review/candidate_queue_review_decision_post_closeout_inventory` 在 review_state closeout 後整理 post-write smoke、live inventory、dedupe key 與 review_state 結果。此 inventory 只允許只讀驗證API/UI 不讀 approval token、不執行 CLI、不更新 `review_state`、不寫 DB、不 commit、不掛 scheduler。
- 2026-05-19 追加 candidate queue review completion archive`services.market_intel.candidate_queue_review_completion_archive``/api/market_intel/manual_sample_review/candidate_queue_review_completion_archive` 在 post-closeout inventory 後整理 receipt、closeout、inventory、dedupe key、review_state row snapshot 與 artifact path manifest。此 archive gate 只輸出封存預覽API/UI 不寫檔、不讀 approval token、不執行 CLI、不更新 `review_state`、不寫 DB、不 commit、不掛 scheduler。
- 2026-05-19 追加 candidate queue review archive summary`services.market_intel.candidate_queue_review_archive_summary``/api/market_intel/manual_sample_review/candidate_queue_review_archive_summary` 在 review completion archive 後整理可供摘要/報表審核的結構化輸入。此 summary gate 不產生 AI 摘要API/UI 不呼叫 LLM、不派送 Telegram、不寫檔、不讀 approval token、不執行 CLI、不更新 `review_state`、不寫 DB、不 commit、不掛 scheduler。
### Phase 4Coupang / Shopee Adapter

View File

@@ -19,9 +19,9 @@
| `edm_routes.py` | EDM 與節慶儀表板 | `/edm`, `/festival` |
| `monthly_routes.py` | 月結分析 | `/monthly_summary_analysis`, `/api/monthly_summary_data` |
| `daily_sales_routes.py` | 當日業績 | `/daily_sales`, `/daily_sales/export*` |
| `market_intel_routes.py` | 市場情報 Phase 78 candidate queue review completion archive 主路由 | `/market_intel`, `/market_intel/*`, `/api/market_intel/status`, `/api/market_intel/schema`, `/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/manual_sample_review/evaluate`, `/api/market_intel/manual_sample_review/candidate_handoff`, `/api/market_intel/manual_sample_review/candidate_queue_draft`, `/api/market_intel/manual_sample_review/candidate_queue_approval`, `/api/market_intel/manual_sample_review/candidate_queue_transaction`, `/api/market_intel/manual_sample_review/candidate_queue_writer_status`, `/api/market_intel/manual_sample_review/candidate_queue_writer_preflight`, `/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/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/adapters`, `/api/market_intel/dry_run_plan`, `/api/market_intel/discovery_plan`, `/api/market_intel/manual_discovery`, `/api/market_intel/candidate_preview`, `/api/market_intel/platform_seed_plan`, `/api/market_intel/platform_seed_write_guard`, `/api/market_intel/platform_seed_writer_plan`, `/api/market_intel/migration_blueprint`, `/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/seed_writer_cli_status`, `/api/market_intel/write_approval_runbook`, `/api/market_intel/deployment_readiness` |
| `market_intel_routes.py` | 市場情報 Phase 79 candidate queue review archive summary 主路由 | `/market_intel`, `/market_intel/*`, `/api/market_intel/status`, `/api/market_intel/schema`, `/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/manual_sample_review/evaluate`, `/api/market_intel/manual_sample_review/candidate_handoff`, `/api/market_intel/manual_sample_review/candidate_queue_draft`, `/api/market_intel/manual_sample_review/candidate_queue_approval`, `/api/market_intel/manual_sample_review/candidate_queue_transaction`, `/api/market_intel/manual_sample_review/candidate_queue_writer_status`, `/api/market_intel/manual_sample_review/candidate_queue_writer_preflight`, `/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/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/adapters`, `/api/market_intel/dry_run_plan`, `/api/market_intel/discovery_plan`, `/api/market_intel/manual_discovery`, `/api/market_intel/candidate_preview`, `/api/market_intel/platform_seed_plan`, `/api/market_intel/platform_seed_write_guard`, `/api/market_intel/platform_seed_writer_plan`, `/api/market_intel/migration_blueprint`, `/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/seed_writer_cli_status`, `/api/market_intel/write_approval_runbook`, `/api/market_intel/deployment_readiness` |
| `market_intel_review_routes.py` | 市場情報人工 queue review 只讀延伸 API | `/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_status`, `/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` |
| `market_intel_review_post_routes.py` | 市場情報 review_state closeout 後只讀延伸 API掛在 `market_intel_review_bp` | `/api/market_intel/manual_sample_review/candidate_queue_review_decision_post_closeout_inventory`, `/api/market_intel/manual_sample_review/candidate_queue_review_completion_archive` |
| `market_intel_review_post_routes.py` | 市場情報 review_state closeout 後只讀延伸 API掛在 `market_intel_review_bp` | `/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_routes.py` | 通用任務與查詢 API | `/api/run_task`, `/api/history/*` |
| `export_routes.py` | 匯出功能 | `/api/export/*` |
| `import_routes.py` | 匯入功能 | `/api/import_excel`, `/api/import/monthly_summary` |

View File

@@ -11,6 +11,9 @@ from routes.market_intel_review_routes import (
market_intel_review_bp,
)
from services.market_intel import MarketIntelService
from services.market_intel.candidate_queue_review_archive_summary import (
build_candidate_queue_review_archive_summary,
)
from services.market_intel.candidate_queue_review_completion_archive import (
build_candidate_queue_review_completion_archive,
)
@@ -71,6 +74,38 @@ def _build_post_closeout_inventory_stack(
return transaction, receipt, closeout, inventory
def _build_review_completion_archive_stack(
*,
service,
sample_result,
payload_error,
operator_evidence,
writer_output,
smoke_result,
limit,
execute_requested,
):
transaction, receipt, closeout, inventory = _build_post_closeout_inventory_stack(
service=service,
sample_result=sample_result,
payload_error=payload_error,
operator_evidence=operator_evidence,
writer_output=writer_output,
smoke_result=smoke_result,
limit=limit,
execute_requested=execute_requested,
)
archive = build_candidate_queue_review_completion_archive(
transaction_preview=transaction,
run_receipt=receipt,
run_closeout=closeout,
post_closeout_inventory=inventory,
operator_evidence=operator_evidence,
execute_requested=execute_requested,
)
return transaction, receipt, closeout, inventory, archive
@market_intel_review_bp.route(
"/api/market_intel/manual_sample_review/"
"candidate_queue_review_decision_post_closeout_inventory",
@@ -98,6 +133,39 @@ def market_intel_manual_sample_candidate_queue_review_decision_post_closeout_inv
return jsonify(data), 400 if payload_error else 200
@market_intel_review_bp.route(
"/api/market_intel/manual_sample_review/"
"candidate_queue_review_archive_summary",
methods=["POST"],
)
@login_required
def market_intel_manual_sample_candidate_queue_review_archive_summary():
service = MarketIntelService()
execute_requested = request.args.get("execute", "false").lower() == "true"
sample_result, operator_evidence, writer_output, smoke_result, payload_error, limit = (
_extract_run_payload()
)
transaction, receipt, closeout, inventory, archive = (
_build_review_completion_archive_stack(
service=service,
sample_result=sample_result,
payload_error=payload_error,
operator_evidence=operator_evidence,
writer_output=writer_output,
smoke_result=smoke_result,
limit=limit,
execute_requested=execute_requested,
)
)
data = build_candidate_queue_review_archive_summary(
review_completion_archive=archive,
operator_evidence=operator_evidence,
execute_requested=execute_requested,
)
data["phase"] = service.phase
return jsonify(data), 400 if payload_error else 200
@market_intel_review_bp.route(
"/api/market_intel/manual_sample_review/"
"candidate_queue_review_completion_archive",
@@ -110,23 +178,18 @@ def market_intel_manual_sample_candidate_queue_review_completion_archive():
sample_result, operator_evidence, writer_output, smoke_result, payload_error, limit = (
_extract_run_payload()
)
transaction, receipt, closeout, inventory = _build_post_closeout_inventory_stack(
service=service,
sample_result=sample_result,
payload_error=payload_error,
operator_evidence=operator_evidence,
writer_output=writer_output,
smoke_result=smoke_result,
limit=limit,
execute_requested=execute_requested,
)
data = build_candidate_queue_review_completion_archive(
transaction_preview=transaction,
run_receipt=receipt,
run_closeout=closeout,
post_closeout_inventory=inventory,
operator_evidence=operator_evidence,
execute_requested=execute_requested,
transaction, receipt, closeout, inventory, archive = (
_build_review_completion_archive_stack(
service=service,
sample_result=sample_result,
payload_error=payload_error,
operator_evidence=operator_evidence,
writer_output=writer_output,
smoke_result=smoke_result,
limit=limit,
execute_requested=execute_requested,
)
)
data = archive
data["phase"] = service.phase
return jsonify(data), 400 if payload_error else 200

View File

@@ -0,0 +1,381 @@
"""候選審核 queue archive summary 預覽。
本模組只整理 review completion archive 後可供摘要/報表審核的結構化輸入;
不呼叫 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_writes_file",
"api_writes_database",
"api_updates_review_state",
"archive_file_written",
"archive_record_written",
"archive_manifest_written",
"summary_file_written",
"summary_record_written",
"summary_manifest_written",
"database_write_executed",
"database_commit_executed",
"migration_executed",
"external_network_executed",
"scheduler_attached",
"review_state_update_executed",
"llm_call_executed",
"ollama_call_executed",
"gemini_call_executed",
"telegram_dispatched",
"ai_summary_generated",
"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)
return {
"provided_keys": sorted(operator_evidence.keys()),
"operator_confirmed_ai_summary_review": bool(
operator_evidence.get("operator_confirmed_ai_summary_review")
or operator_evidence.get("operator_confirmed_archive_summary_review")
),
"operator_confirmed_summary_is_read_only": bool(
operator_evidence.get("operator_confirmed_summary_is_read_only")
or operator_evidence.get("operator_confirmed_archive_is_read_only")
),
"operator_confirmed_no_llm_call": bool(
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")
),
"archive_summary_artifact_path": _safe_text(
operator_evidence.get("archive_summary_artifact_path")
or operator_evidence.get("ai_summary_input_artifact_path")
),
"approval_token_submitted_to_api": _contains_forbidden_token_key(
operator_evidence
),
}
def _archive_payload_summary(review_completion_archive):
archive = _as_dict(review_completion_archive)
manifest = _as_dict(archive.get("archive_manifest"))
manifest_inventory = _as_dict(manifest.get("inventory"))
artifact_paths = _as_dict(manifest.get("artifact_paths"))
expected_keys = sorted(str(key) for key in _as_list(archive.get("expected_dedupe_keys")))
found_keys = sorted(str(key) for key in _as_list(archive.get("found_dedupe_keys")))
missing_keys = sorted(str(key) for key in _as_list(archive.get("missing_dedupe_keys")))
state_mismatches = _as_list(archive.get("state_mismatches"))
return {
"provided": bool(archive),
"mode": archive.get("mode"),
"review_completion_archive_ready": bool(
archive.get("review_completion_archive_ready")
),
"archive_manifest_ready": bool(archive.get("archive_manifest_ready")),
"ready_for_ai_summary_review": bool(archive.get("ready_for_ai_summary_review")),
"expected_dedupe_keys": expected_keys,
"found_dedupe_keys": found_keys,
"missing_dedupe_keys": missing_keys,
"state_mismatches": state_mismatches,
"row_summary_count": int(archive.get("row_summary_count") or 0),
"evidence_bundle_ids": _as_list(
manifest_inventory.get("evidence_bundle_ids")
),
"artifact_paths": {
"review_state_receipt_artifact_path": artifact_paths.get(
"review_state_receipt_artifact_path"
),
"review_state_closeout_artifact_path": artifact_paths.get(
"review_state_closeout_artifact_path"
),
"post_closeout_inventory_artifact_path": artifact_paths.get(
"post_closeout_inventory_artifact_path"
),
"review_completion_archive_path": artifact_paths.get(
"review_completion_archive_path"
),
},
"read_only_query_executed": bool(archive.get("read_only_query_executed")),
"database_connection_opened": bool(archive.get("database_connection_opened")),
"database_write_executed": bool(archive.get("database_write_executed")),
"database_commit_executed": bool(archive.get("database_commit_executed")),
"review_state_update_executed": bool(
archive.get("review_state_update_executed")
),
"scheduler_attached": bool(archive.get("scheduler_attached")),
"blocked_reasons": _as_list(archive.get("blocked_reasons")),
"safe_boundaries": _as_list(archive.get("safe_boundaries")),
}
def _summary_sections(archive):
return [
{
"key": "review_scope",
"title": "人工 review 範圍",
"facts": [
f"expected_dedupe_key_count={len(archive['expected_dedupe_keys'])}",
f"found_dedupe_key_count={len(archive['found_dedupe_keys'])}",
f"row_summary_count={archive['row_summary_count']}",
],
},
{
"key": "review_state_result",
"title": "review_state 結果",
"facts": [
f"missing_dedupe_key_count={len(archive['missing_dedupe_keys'])}",
f"state_mismatch_count={len(archive['state_mismatches'])}",
],
},
{
"key": "evidence_artifacts",
"title": "封存證據",
"facts": [
f"artifact_paths_recorded={sum(1 for value in archive['artifact_paths'].values() if value)}",
f"evidence_bundle_count={len(archive['evidence_bundle_ids'])}",
],
},
{
"key": "execution_safety",
"title": "執行邊界",
"facts": [
"llm_call_executed=false",
"telegram_dispatched=false",
"database_write_executed=false",
],
},
]
def _summary_gates(*, archive, operator):
artifact_paths = archive["artifact_paths"]
return [
{
"key": "review_completion_archive_preview_provided",
"label": "必須提供 review completion archive preview",
"passed": bool(
archive["provided"]
and archive["mode"]
== "candidate_queue_review_completion_archive_preview"
),
},
{
"key": "review_completion_archive_ready",
"label": "review completion archive 必須已通過",
"passed": bool(
archive["review_completion_archive_ready"]
and archive["archive_manifest_ready"]
and archive["ready_for_ai_summary_review"]
),
},
{
"key": "summary_dedupe_scope_complete",
"label": "摘要輸入必須具備完整 expected / found dedupe key",
"passed": bool(
archive["expected_dedupe_keys"]
and archive["expected_dedupe_keys"] == archive["found_dedupe_keys"]
and not archive["missing_dedupe_keys"]
),
},
{
"key": "summary_review_state_clean",
"label": "摘要前不可存在 review_state mismatch",
"passed": bool(archive["row_summary_count"] > 0 and not archive["state_mismatches"]),
},
{
"key": "summary_artifact_paths_recorded",
"label": "摘要輸入必須保留所有上游 artifact path",
"passed": all(artifact_paths.values()),
},
{
"key": "operator_confirmed_archive_summary_review",
"label": "操作員確認此步只做摘要輸入審核",
"passed": bool(
operator["operator_confirmed_ai_summary_review"]
and operator["operator_confirmed_summary_is_read_only"]
and operator["operator_confirmed_no_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": "summary_artifact_path_recorded",
"label": "摘要輸入 artifact path 必須由操作員提供",
"passed": bool(operator["archive_summary_artifact_path"]),
},
{
"key": "archive_summary_no_approval_token_submitted_to_api",
"label": "payload 不得包含一次性 approval token key",
"passed": not operator["approval_token_submitted_to_api"],
},
{
"key": "archive_summary_preview_has_no_side_effects",
"label": "archive summary gate 不得呼叫 LLM、派送 Telegram、寫檔、寫 DB 或掛 scheduler",
"passed": _side_effects_clear(archive),
},
]
def build_candidate_queue_review_archive_summary(
*,
review_completion_archive,
operator_evidence=None,
execute_requested=False,
):
"""建立 archive summary 預覽;不執行 LLM、檔案、網路或 DB 副作用。"""
archive = _archive_payload_summary(review_completion_archive)
operator = _operator_summary(operator_evidence)
gates = _summary_gates(archive=archive, operator=operator)
blocked_reasons = [gate["key"] for gate in gates if not gate["passed"]]
summary_ready = bool(not blocked_reasons)
sections = _summary_sections(archive)
return {
"mode": "candidate_queue_review_archive_summary_preview",
"target_table": TARGET_TABLE,
"target_operation": "prepare_archive_summary_input",
"execute_requested": bool(execute_requested),
"archive_summary_ready": summary_ready,
"summary_input_ready": summary_ready,
"ready_for_next_manual_phase": summary_ready,
"ready_for_ai_summary_review": summary_ready,
"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_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,
"summary_file_written": False,
"summary_record_written": False,
"summary_manifest_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": archive["read_only_query_executed"],
"database_connection_opened": archive["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": archive["expected_dedupe_keys"],
"found_dedupe_keys": archive["found_dedupe_keys"],
"missing_dedupe_keys": archive["missing_dedupe_keys"],
"state_mismatches": archive["state_mismatches"],
"row_summary_count": archive["row_summary_count"],
"summary_sections": sections,
"artifact_paths": {
**archive["artifact_paths"],
"archive_summary_artifact_path": operator[
"archive_summary_artifact_path"
],
},
"operator_summary_review": operator,
"blocked_reasons": blocked_reasons,
"gates": gates,
"next_operator_steps": [
"確認 archive summary input 只包含摘要事實與 artifact path",
"確認本階段未呼叫 LLM、未派送 Telegram、未寫 DB",
"後續若要產生 AI 摘要,必須另開 Ollama-first summary gate",
"若 summary scope 不完整,回到 review completion archive 修正",
],
"safe_boundaries": [
"do_not_call_llm_from_archive_summary_gate",
"do_not_dispatch_telegram_from_archive_summary_gate",
"do_not_write_summary_file_from_api",
"do_not_insert_summary_record_from_api",
"do_not_update_review_state_from_archive_summary_gate",
"do_not_read_approval_token_from_archive_summary_gate",
"do_not_execute_cli_from_archive_summary_gate",
"do_not_attach_scheduler_from_archive_summary_gate",
"ollama_first_only_for_future_llm_summary",
"gemini_backup_only_for_future_llm_summary",
"archive_summary_preview_only",
"no_remove_orphans",
"no_momo_db_lifecycle_change",
],
}

View File

@@ -26,10 +26,11 @@ from services.market_intel.candidate_queue_review_decision_writer_run_receipt im
from services.market_intel.candidate_queue_review_decision_writer_run_closeout import build_candidate_queue_review_decision_writer_run_closeout
from services.market_intel.candidate_queue_review_decision_post_closeout_inventory import build_candidate_queue_review_decision_post_closeout_inventory
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
BLOCKED_RUN_REVIEW_KEYS = ("ready_for_api_database_write", "ready_for_scheduler_attach", "api_executes_cli", "api_reads_approval_token", "api_writes_file", "api_writes_database", "api_updates_review_state", "approval_record_written", "decision_record_written", "review_state_update_executed", "database_connection_opened", "database_session_created", "explicit_transaction_opened", "transaction_opened", "transaction_committed", "database_write_executed", "database_commit_executed", "database_rollback_executed", "scheduler_attached", "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_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_decision_writer_status")
def _run_review_preview_safe(payload, mode):
@@ -174,6 +175,9 @@ def build_deployment_readiness_preview(
run_closeout=candidate_queue_review_decision_writer_run_closeout,
post_closeout_inventory=candidate_queue_review_decision_post_closeout_inventory,
)
candidate_queue_review_archive_summary = build_candidate_queue_review_archive_summary(
review_completion_archive=candidate_queue_review_completion_archive,
)
checks = {
"schema_smoke_passed": bool(schema_smoke["passed"]),
"feature_flags_default_safe": bool(
@@ -448,6 +452,10 @@ def build_deployment_readiness_preview(
candidate_queue_review_completion_archive,
"candidate_queue_review_completion_archive_preview",
),
"candidate_queue_review_archive_summary_preview_safe": _run_review_preview_safe(
candidate_queue_review_archive_summary,
"candidate_queue_review_archive_summary_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",
@@ -693,6 +701,7 @@ def build_deployment_readiness_preview(
"candidate_queue_review_decision_writer_run_closeout": candidate_queue_review_decision_writer_run_closeout,
"candidate_queue_review_decision_post_closeout_inventory": candidate_queue_review_decision_post_closeout_inventory,
"candidate_queue_review_completion_archive": candidate_queue_review_completion_archive,
"candidate_queue_review_archive_summary": candidate_queue_review_archive_summary,
"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_78_candidate_queue_review_completion_archive"
MARKET_INTEL_PHASE = "phase_79_candidate_queue_review_archive_summary"