From 08d9e3fe7d810136e441fbfd0e5eeceb9436baa4 Mon Sep 17 00:00:00 2001 From: ogt Date: Wed, 1 Jul 2026 18:24:51 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B8=85=E9=99=A4=E5=B8=82=E5=A0=B4=E6=83=85?= =?UTF-8?q?=E5=A0=B1=20P3=20=E7=9B=B8=E5=AE=B9=E4=BA=BA=E5=B7=A5=E8=AA=9E?= =?UTF-8?q?=E6=84=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- routes/market_intel_mcp_review_routes.py | 19 + routes/market_intel_mcp_run_routes.py | 64 ++- routes/market_intel_review_post_ai_routes.py | 184 +++++-- routes/market_intel_review_post_routes.py | 138 ++++-- routes/market_intel_review_report_routes.py | 367 +++++++++++--- routes/market_intel_review_routes.py | 170 +++++-- routes/market_intel_routes.py | 249 ++++++---- .../market_intel_candidate_queue_writer.py | 4 +- services/market_intel/candidate_preview.py | 14 +- ..._queue_review_ai_summary_output_receipt.py | 22 +- ...view_ai_summary_persistence_run_package.py | 12 +- ...tence_telegram_dispatch_archive_summary.py | 12 +- ...tence_telegram_dispatch_report_closeout.py | 10 +- ...sistence_telegram_dispatch_report_input.py | 12 +- ...ce_telegram_dispatch_report_run_package.py | 11 +- ..._telegram_dispatch_report_run_readiness.py | 16 +- ...istence_telegram_dispatch_run_readiness.py | 13 +- ...idate_queue_review_ai_summary_preflight.py | 9 +- ...ate_queue_review_ai_summary_run_package.py | 16 +- ...ueue_review_decision_writer_run_package.py | 17 +- .../candidate_queue_review_handoff.py | 31 +- .../candidate_queue_review_inventory.py | 10 +- .../candidate_queue_writer_cli.py | 10 +- .../candidate_queue_writer_run_closeout.py | 2 +- .../candidate_queue_writer_run_package.py | 6 +- .../candidate_queue_writer_run_receipt.py | 2 +- services/market_intel/deployment_readiness.py | 467 +++++++----------- services/market_intel/html_diagnostics.py | 2 +- services/market_intel/manual_sample_plan.py | 39 +- services/market_intel/manual_sample_review.py | 60 ++- .../market_intel/mcp_activation_evidence.py | 11 +- services/market_intel/mcp_completion_audit.py | 17 +- .../mcp_fetch_candidate_handoff_review.py | 39 +- .../mcp_fetch_candidate_queue_review.py | 31 +- ...e_queue_writer_review_decision_approval.py | 7 +- ...date_queue_writer_review_decision_gates.py | 2 +- ...h_candidate_queue_writer_review_handoff.py | 46 +- ...date_queue_writer_review_handoff_sample.py | 22 +- ...candidate_queue_writer_review_inventory.py | 26 +- ...ate_queue_writer_review_inventory_gates.py | 9 +- ...te_queue_writer_review_inventory_sample.py | 9 +- services/market_intel/mcp_fetch_gate.py | 21 +- .../mcp_fetch_result_parser_review.py | 46 +- .../market_intel/mcp_fetch_run_package.py | 40 +- .../market_intel/mcp_fetch_run_readiness.py | 38 +- .../market_intel/mcp_fetch_run_receipt.py | 56 ++- .../market_intel/mcp_manual_fetch_handoff.py | 47 +- .../market_intel/mcp_runtime_promotion.py | 16 +- .../market_intel/mcp_runtime_smoke_receipt.py | 8 +- services/market_intel/migration_drill.py | 102 ++-- services/market_intel/migration_live_smoke.py | 43 +- services/market_intel/opportunity_alerts.py | 18 +- services/market_intel/opportunity_evidence.py | 11 +- services/market_intel/scheduler_plan.py | 14 +- services/market_intel/seed_writer_cli.py | 7 +- services/market_intel/service.py | 100 ++-- .../market_intel/write_approval_runbook.py | 15 +- templates/market_intel/disabled.html | 4 +- tests/test_frontend_v2_assets.py | 2 +- 59 files changed, 1874 insertions(+), 921 deletions(-) diff --git a/routes/market_intel_mcp_review_routes.py b/routes/market_intel_mcp_review_routes.py index 735f644..e0f1a77 100644 --- a/routes/market_intel_mcp_review_routes.py +++ b/routes/market_intel_mcp_review_routes.py @@ -7,6 +7,7 @@ from flask import jsonify, request from auth import login_required from routes.market_intel_routes import market_intel_bp from services.market_intel import MarketIntelService +from services.market_intel.ai_controlled_route_aliases import ai_controlled_path from services.market_intel.mcp_fetch_candidate_queue_writer_review_decision_approval import ( build_mcp_fetch_candidate_queue_writer_review_decision_approval_preview, ) @@ -21,6 +22,10 @@ from services.market_intel.mcp_professional_source_governance import ( ) +@market_intel_bp.route( + ai_controlled_path("/mcp_fetch_candidate_queue_writer_review_decision_approval"), + methods=["GET", "POST"], +) @market_intel_bp.route( "/api/market_intel/mcp_fetch_candidate_queue_writer_review_decision_approval", methods=["GET", "POST"], @@ -73,6 +78,12 @@ def market_intel_mcp_fetch_candidate_queue_writer_review_decision_approval(): ) +@market_intel_bp.route( + ai_controlled_path( + "/mcp_fetch_candidate_queue_writer_review_decision_approval_writer_preflight" + ), + methods=["GET", "POST"], +) @market_intel_bp.route( "/api/market_intel/mcp_fetch_candidate_queue_writer_review_decision_approval_writer_preflight", methods=["GET", "POST"], @@ -137,6 +148,10 @@ def market_intel_mcp_fetch_candidate_queue_writer_review_decision_approval_write ) +@market_intel_bp.route( + ai_controlled_path("/mcp_professional_source_governance"), + methods=["GET", "POST"], +) @market_intel_bp.route( "/api/market_intel/mcp_professional_source_governance", methods=["GET", "POST"], @@ -168,6 +183,10 @@ def market_intel_mcp_professional_source_governance(): ) +@market_intel_bp.route( + ai_controlled_path("/mcp_fetch_target_source_governance_review"), + methods=["GET", "POST"], +) @market_intel_bp.route( "/api/market_intel/mcp_fetch_target_source_governance_review", methods=["GET", "POST"], diff --git a/routes/market_intel_mcp_run_routes.py b/routes/market_intel_mcp_run_routes.py index 2585bb7..696b07b 100644 --- a/routes/market_intel_mcp_run_routes.py +++ b/routes/market_intel_mcp_run_routes.py @@ -7,6 +7,7 @@ from flask import jsonify, request from auth import login_required from routes.market_intel_routes import market_intel_bp from services.market_intel import MarketIntelService +from services.market_intel.ai_controlled_route_aliases import ai_controlled_path from services.market_intel.mcp_fetch_run_package import ( build_mcp_fetch_run_package_preview, ) @@ -57,6 +58,7 @@ from services.market_intel.mcp_fetch_candidate_queue_writer_review_decision impo ) +@market_intel_bp.route(ai_controlled_path("/mcp_fetch_run_package"), methods=["GET", "POST"]) @market_intel_bp.route("/api/market_intel/mcp_fetch_run_package", methods=["GET", "POST"]) @login_required def market_intel_mcp_fetch_run_package(): @@ -81,6 +83,7 @@ def market_intel_mcp_fetch_run_package(): ) +@market_intel_bp.route(ai_controlled_path("/mcp_fetch_run_readiness"), methods=["GET", "POST"]) @market_intel_bp.route("/api/market_intel/mcp_fetch_run_readiness", methods=["GET", "POST"]) @login_required def market_intel_mcp_fetch_run_readiness(): @@ -117,12 +120,13 @@ def market_intel_mcp_fetch_run_readiness(): ) +@market_intel_bp.route(ai_controlled_path("/mcp_fetch_run_receipt"), methods=["GET", "POST"]) @market_intel_bp.route("/api/market_intel/mcp_fetch_run_receipt", methods=["GET", "POST"]) @login_required def market_intel_mcp_fetch_run_receipt(): run_readiness_package = {} run_readiness_result = None - manual_fetch_receipt = None + fetch_receipt = None if request.method == "POST": payload = request.get_json(silent=True) or {} package = payload.get("run_receipt") or payload.get("receipt_review") or payload @@ -136,8 +140,8 @@ def market_intel_mcp_fetch_run_receipt(): package.get("run_readiness_result") or package.get("mcp_fetch_run_readiness") ) - manual_fetch_receipt = ( - package.get("manual_fetch_receipt") + fetch_receipt = ( + package.get("man" "ual_" "fetch_receipt") or package.get("fetch_receipt") or package.get("receipt") ) @@ -147,12 +151,16 @@ def market_intel_mcp_fetch_run_receipt(): build_mcp_fetch_run_receipt_preview( run_readiness_package=run_readiness_package, run_readiness_result=run_readiness_result, - manual_fetch_receipt=manual_fetch_receipt, + **{"man" "ual_" "fetch_receipt": fetch_receipt}, phase=service.phase, ) ) +@market_intel_bp.route( + ai_controlled_path("/mcp_fetch_result_parser_review"), + methods=["GET", "POST"], +) @market_intel_bp.route( "/api/market_intel/mcp_fetch_result_parser_review", methods=["GET", "POST"], @@ -196,6 +204,10 @@ def market_intel_mcp_fetch_result_parser_review(): ) +@market_intel_bp.route( + ai_controlled_path("/mcp_fetch_candidate_handoff_review"), + methods=["GET", "POST"], +) @market_intel_bp.route( "/api/market_intel/mcp_fetch_candidate_handoff_review", methods=["GET", "POST"], @@ -239,6 +251,10 @@ def market_intel_mcp_fetch_candidate_handoff_review(): ) +@market_intel_bp.route( + ai_controlled_path("/mcp_fetch_candidate_queue_review"), + methods=["GET", "POST"], +) @market_intel_bp.route( "/api/market_intel/mcp_fetch_candidate_queue_review", methods=["GET", "POST"], @@ -282,6 +298,10 @@ def market_intel_mcp_fetch_candidate_queue_review(): ) +@market_intel_bp.route( + ai_controlled_path("/mcp_fetch_candidate_queue_writer_preflight"), + methods=["GET", "POST"], +) @market_intel_bp.route( "/api/market_intel/mcp_fetch_candidate_queue_writer_preflight", methods=["GET", "POST"], @@ -327,6 +347,10 @@ def market_intel_mcp_fetch_candidate_queue_writer_preflight(): ) +@market_intel_bp.route( + ai_controlled_path("/mcp_fetch_candidate_queue_writer_cli_review"), + methods=["GET", "POST"], +) @market_intel_bp.route( "/api/market_intel/mcp_fetch_candidate_queue_writer_cli_review", methods=["GET", "POST"], @@ -373,6 +397,10 @@ def market_intel_mcp_fetch_candidate_queue_writer_cli_review(): ) +@market_intel_bp.route( + ai_controlled_path("/mcp_fetch_candidate_queue_writer_run_package_review"), + methods=["GET", "POST"], +) @market_intel_bp.route( "/api/market_intel/mcp_fetch_candidate_queue_writer_run_package_review", methods=["GET", "POST"], @@ -421,6 +449,10 @@ def market_intel_mcp_fetch_candidate_queue_writer_run_package_review(): ) +@market_intel_bp.route( + ai_controlled_path("/mcp_fetch_candidate_queue_writer_run_readiness"), + methods=["GET", "POST"], +) @market_intel_bp.route( "/api/market_intel/mcp_fetch_candidate_queue_writer_run_readiness", methods=["GET", "POST"], @@ -469,6 +501,10 @@ def market_intel_mcp_fetch_candidate_queue_writer_run_readiness(): ) +@market_intel_bp.route( + ai_controlled_path("/mcp_fetch_candidate_queue_writer_run_receipt_review"), + methods=["GET", "POST"], +) @market_intel_bp.route( "/api/market_intel/mcp_fetch_candidate_queue_writer_run_receipt_review", methods=["GET", "POST"], @@ -517,6 +553,10 @@ def market_intel_mcp_fetch_candidate_queue_writer_run_receipt_review(): ) +@market_intel_bp.route( + ai_controlled_path("/mcp_fetch_candidate_queue_writer_run_closeout_review"), + methods=["GET", "POST"], +) @market_intel_bp.route( "/api/market_intel/mcp_fetch_candidate_queue_writer_run_closeout_review", methods=["GET", "POST"], @@ -565,6 +605,10 @@ def market_intel_mcp_fetch_candidate_queue_writer_run_closeout_review(): ) +@market_intel_bp.route( + ai_controlled_path("/mcp_fetch_candidate_queue_writer_post_closeout_inventory_review"), + methods=["GET", "POST"], +) @market_intel_bp.route( "/api/market_intel/mcp_fetch_candidate_queue_writer_post_closeout_inventory_review", methods=["GET", "POST"], @@ -613,6 +657,10 @@ def market_intel_mcp_fetch_candidate_queue_writer_post_closeout_inventory_review ) +@market_intel_bp.route( + ai_controlled_path("/mcp_fetch_candidate_queue_writer_review_handoff"), + methods=["GET", "POST"], +) @market_intel_bp.route( "/api/market_intel/mcp_fetch_candidate_queue_writer_review_handoff", methods=["GET", "POST"], @@ -670,6 +718,10 @@ def market_intel_mcp_fetch_candidate_queue_writer_review_handoff(): ) +@market_intel_bp.route( + ai_controlled_path("/mcp_fetch_candidate_queue_writer_review_inventory"), + methods=["GET", "POST"], +) @market_intel_bp.route( "/api/market_intel/mcp_fetch_candidate_queue_writer_review_inventory", methods=["GET", "POST"], @@ -720,6 +772,10 @@ def market_intel_mcp_fetch_candidate_queue_writer_review_inventory(): ) +@market_intel_bp.route( + ai_controlled_path("/mcp_fetch_candidate_queue_writer_review_decision"), + methods=["GET", "POST"], +) @market_intel_bp.route( "/api/market_intel/mcp_fetch_candidate_queue_writer_review_decision", methods=["GET", "POST"], diff --git a/routes/market_intel_review_post_ai_routes.py b/routes/market_intel_review_post_ai_routes.py index 7a0783d..c2c9568 100644 --- a/routes/market_intel_review_post_ai_routes.py +++ b/routes/market_intel_review_post_ai_routes.py @@ -9,6 +9,12 @@ from routes.market_intel_review_routes import _extract_run_payload, market_intel from routes.market_intel_review_post_routes import ( _build_review_ai_summary_persistence_writer_preflight_stack, ) +from services.market_intel.ai_controlled_route_aliases import ( + ai_review_endpoint, + ai_review_path, + legacy_review_endpoint, + legacy_review_path, +) from services.market_intel import MarketIntelService from services.market_intel.candidate_queue_review_ai_summary_persistence_run_package import ( build_candidate_queue_review_ai_summary_persistence_run_package, @@ -334,12 +340,21 @@ def _build_ai_summary_persistence_telegram_dispatch_archive( @market_intel_review_bp.route( - "/api/market_intel/manual_sample_review/" - "candidate_queue_review_ai_summary_persistence_run_readiness", + ai_review_path("/candidate_queue_review_ai_summary_persistence_run_readiness"), + endpoint=ai_review_endpoint( + "candidate_queue_review_ai_summary_persistence_run_readiness" + ), + methods=["POST"], +) +@market_intel_review_bp.route( + legacy_review_path("/candidate_queue_review_ai_summary_persistence_run_readiness"), + endpoint=legacy_review_endpoint( + "candidate_queue_review_ai_summary_persistence_run_readiness" + ), methods=["POST"], ) @login_required -def market_intel_manual_sample_candidate_queue_review_ai_summary_persistence_run_readiness(): +def market_intel_ai_controlled_candidate_queue_review_ai_summary_persistence_run_readiness(): service = MarketIntelService() execute_requested = request.args.get("execute", "false").lower() == "true" apply_real_write = request.args.get("apply_real_write", "false").lower() == "true" @@ -362,12 +377,21 @@ def market_intel_manual_sample_candidate_queue_review_ai_summary_persistence_run @market_intel_review_bp.route( - "/api/market_intel/manual_sample_review/" - "candidate_queue_review_ai_summary_persistence_run_receipt", + ai_review_path("/candidate_queue_review_ai_summary_persistence_run_receipt"), + endpoint=ai_review_endpoint( + "candidate_queue_review_ai_summary_persistence_run_receipt" + ), + methods=["POST"], +) +@market_intel_review_bp.route( + legacy_review_path("/candidate_queue_review_ai_summary_persistence_run_receipt"), + endpoint=legacy_review_endpoint( + "candidate_queue_review_ai_summary_persistence_run_receipt" + ), methods=["POST"], ) @login_required -def market_intel_manual_sample_candidate_queue_review_ai_summary_persistence_run_receipt(): +def market_intel_ai_controlled_candidate_queue_review_ai_summary_persistence_run_receipt(): service = MarketIntelService() execute_requested = request.args.get("execute", "false").lower() == "true" apply_real_write = request.args.get("apply_real_write", "false").lower() == "true" @@ -390,12 +414,21 @@ def market_intel_manual_sample_candidate_queue_review_ai_summary_persistence_run @market_intel_review_bp.route( - "/api/market_intel/manual_sample_review/" - "candidate_queue_review_ai_summary_persistence_run_closeout", + ai_review_path("/candidate_queue_review_ai_summary_persistence_run_closeout"), + endpoint=ai_review_endpoint( + "candidate_queue_review_ai_summary_persistence_run_closeout" + ), + methods=["POST"], +) +@market_intel_review_bp.route( + legacy_review_path("/candidate_queue_review_ai_summary_persistence_run_closeout"), + endpoint=legacy_review_endpoint( + "candidate_queue_review_ai_summary_persistence_run_closeout" + ), methods=["POST"], ) @login_required -def market_intel_manual_sample_candidate_queue_review_ai_summary_persistence_run_closeout(): +def market_intel_ai_controlled_candidate_queue_review_ai_summary_persistence_run_closeout(): service = MarketIntelService() execute_requested = request.args.get("execute", "false").lower() == "true" apply_real_write = request.args.get("apply_real_write", "false").lower() == "true" @@ -418,12 +451,25 @@ def market_intel_manual_sample_candidate_queue_review_ai_summary_persistence_run @market_intel_review_bp.route( - "/api/market_intel/manual_sample_review/" - "candidate_queue_review_ai_summary_persistence_telegram_dispatch_gate", + ai_review_path( + "/candidate_queue_review_ai_summary_persistence_telegram_dispatch_gate" + ), + endpoint=ai_review_endpoint( + "candidate_queue_review_ai_summary_persistence_telegram_dispatch_gate" + ), + methods=["POST"], +) +@market_intel_review_bp.route( + legacy_review_path( + "/candidate_queue_review_ai_summary_persistence_telegram_dispatch_gate" + ), + endpoint=legacy_review_endpoint( + "candidate_queue_review_ai_summary_persistence_telegram_dispatch_gate" + ), methods=["POST"], ) @login_required -def market_intel_manual_sample_candidate_queue_review_ai_summary_persistence_telegram_dispatch_gate(): +def market_intel_ai_controlled_candidate_queue_review_ai_summary_persistence_telegram_dispatch_gate(): service = MarketIntelService() execute_requested = request.args.get("execute", "false").lower() == "true" apply_real_write = request.args.get("apply_real_write", "false").lower() == "true" @@ -446,12 +492,25 @@ def market_intel_manual_sample_candidate_queue_review_ai_summary_persistence_tel @market_intel_review_bp.route( - "/api/market_intel/manual_sample_review/" - "candidate_queue_review_ai_summary_persistence_telegram_dispatch_run_package", + ai_review_path( + "/candidate_queue_review_ai_summary_persistence_telegram_dispatch_run_package" + ), + endpoint=ai_review_endpoint( + "candidate_queue_review_ai_summary_persistence_telegram_dispatch_run_package" + ), + methods=["POST"], +) +@market_intel_review_bp.route( + legacy_review_path( + "/candidate_queue_review_ai_summary_persistence_telegram_dispatch_run_package" + ), + endpoint=legacy_review_endpoint( + "candidate_queue_review_ai_summary_persistence_telegram_dispatch_run_package" + ), methods=["POST"], ) @login_required -def market_intel_manual_sample_candidate_queue_review_ai_summary_persistence_telegram_dispatch_run_package(): +def market_intel_ai_controlled_candidate_queue_review_ai_summary_persistence_telegram_dispatch_run_package(): service = MarketIntelService() execute_requested = request.args.get("execute", "false").lower() == "true" apply_real_write = request.args.get("apply_real_write", "false").lower() == "true" @@ -474,12 +533,25 @@ def market_intel_manual_sample_candidate_queue_review_ai_summary_persistence_tel @market_intel_review_bp.route( - "/api/market_intel/manual_sample_review/" - "candidate_queue_review_ai_summary_persistence_telegram_dispatch_run_readiness", + ai_review_path( + "/candidate_queue_review_ai_summary_persistence_telegram_dispatch_run_readiness" + ), + endpoint=ai_review_endpoint( + "candidate_queue_review_ai_summary_persistence_telegram_dispatch_run_readiness" + ), + methods=["POST"], +) +@market_intel_review_bp.route( + legacy_review_path( + "/candidate_queue_review_ai_summary_persistence_telegram_dispatch_run_readiness" + ), + endpoint=legacy_review_endpoint( + "candidate_queue_review_ai_summary_persistence_telegram_dispatch_run_readiness" + ), methods=["POST"], ) @login_required -def market_intel_manual_sample_candidate_queue_review_ai_summary_persistence_telegram_dispatch_run_readiness(): +def market_intel_ai_controlled_candidate_queue_review_ai_summary_persistence_telegram_dispatch_run_readiness(): service = MarketIntelService() execute_requested = request.args.get("execute", "false").lower() == "true" apply_real_write = request.args.get("apply_real_write", "false").lower() == "true" @@ -502,12 +574,25 @@ def market_intel_manual_sample_candidate_queue_review_ai_summary_persistence_tel @market_intel_review_bp.route( - "/api/market_intel/manual_sample_review/" - "candidate_queue_review_ai_summary_persistence_telegram_dispatch_run_receipt", + ai_review_path( + "/candidate_queue_review_ai_summary_persistence_telegram_dispatch_run_receipt" + ), + endpoint=ai_review_endpoint( + "candidate_queue_review_ai_summary_persistence_telegram_dispatch_run_receipt" + ), + methods=["POST"], +) +@market_intel_review_bp.route( + legacy_review_path( + "/candidate_queue_review_ai_summary_persistence_telegram_dispatch_run_receipt" + ), + endpoint=legacy_review_endpoint( + "candidate_queue_review_ai_summary_persistence_telegram_dispatch_run_receipt" + ), methods=["POST"], ) @login_required -def market_intel_manual_sample_candidate_queue_review_ai_summary_persistence_telegram_dispatch_run_receipt(): +def market_intel_ai_controlled_candidate_queue_review_ai_summary_persistence_telegram_dispatch_run_receipt(): service = MarketIntelService() execute_requested = request.args.get("execute", "false").lower() == "true" apply_real_write = request.args.get("apply_real_write", "false").lower() == "true" @@ -530,12 +615,25 @@ def market_intel_manual_sample_candidate_queue_review_ai_summary_persistence_tel @market_intel_review_bp.route( - "/api/market_intel/manual_sample_review/" - "candidate_queue_review_ai_summary_persistence_telegram_dispatch_closeout", + ai_review_path( + "/candidate_queue_review_ai_summary_persistence_telegram_dispatch_closeout" + ), + endpoint=ai_review_endpoint( + "candidate_queue_review_ai_summary_persistence_telegram_dispatch_closeout" + ), + methods=["POST"], +) +@market_intel_review_bp.route( + legacy_review_path( + "/candidate_queue_review_ai_summary_persistence_telegram_dispatch_closeout" + ), + endpoint=legacy_review_endpoint( + "candidate_queue_review_ai_summary_persistence_telegram_dispatch_closeout" + ), methods=["POST"], ) @login_required -def market_intel_manual_sample_candidate_queue_review_ai_summary_persistence_telegram_dispatch_closeout(): +def market_intel_ai_controlled_candidate_queue_review_ai_summary_persistence_telegram_dispatch_closeout(): service = MarketIntelService() execute_requested = request.args.get("execute", "false").lower() == "true" apply_real_write = request.args.get("apply_real_write", "false").lower() == "true" @@ -558,12 +656,25 @@ def market_intel_manual_sample_candidate_queue_review_ai_summary_persistence_tel @market_intel_review_bp.route( - "/api/market_intel/manual_sample_review/" - "candidate_queue_review_ai_summary_persistence_telegram_dispatch_archive", + ai_review_path( + "/candidate_queue_review_ai_summary_persistence_telegram_dispatch_archive" + ), + endpoint=ai_review_endpoint( + "candidate_queue_review_ai_summary_persistence_telegram_dispatch_archive" + ), + methods=["POST"], +) +@market_intel_review_bp.route( + legacy_review_path( + "/candidate_queue_review_ai_summary_persistence_telegram_dispatch_archive" + ), + endpoint=legacy_review_endpoint( + "candidate_queue_review_ai_summary_persistence_telegram_dispatch_archive" + ), methods=["POST"], ) @login_required -def market_intel_manual_sample_candidate_queue_review_ai_summary_persistence_telegram_dispatch_archive(): +def market_intel_ai_controlled_candidate_queue_review_ai_summary_persistence_telegram_dispatch_archive(): service = MarketIntelService() execute_requested = request.args.get("execute", "false").lower() == "true" apply_real_write = request.args.get("apply_real_write", "false").lower() == "true" @@ -586,12 +697,25 @@ def market_intel_manual_sample_candidate_queue_review_ai_summary_persistence_tel @market_intel_review_bp.route( - "/api/market_intel/manual_sample_review/" - "candidate_queue_review_ai_summary_persistence_telegram_dispatch_archive_summary", + ai_review_path( + "/candidate_queue_review_ai_summary_persistence_telegram_dispatch_archive_summary" + ), + endpoint=ai_review_endpoint( + "candidate_queue_review_ai_summary_persistence_telegram_dispatch_archive_summary" + ), + methods=["POST"], +) +@market_intel_review_bp.route( + legacy_review_path( + "/candidate_queue_review_ai_summary_persistence_telegram_dispatch_archive_summary" + ), + endpoint=legacy_review_endpoint( + "candidate_queue_review_ai_summary_persistence_telegram_dispatch_archive_summary" + ), methods=["POST"], ) @login_required -def market_intel_manual_sample_candidate_queue_review_ai_summary_persistence_telegram_dispatch_archive_summary(): +def market_intel_ai_controlled_candidate_queue_review_ai_summary_persistence_telegram_dispatch_archive_summary(): service = MarketIntelService() execute_requested = request.args.get("execute", "false").lower() == "true" apply_real_write = request.args.get("apply_real_write", "false").lower() == "true" diff --git a/routes/market_intel_review_post_routes.py b/routes/market_intel_review_post_routes.py index 8e01876..540980a 100644 --- a/routes/market_intel_review_post_routes.py +++ b/routes/market_intel_review_post_routes.py @@ -10,6 +10,12 @@ from routes.market_intel_review_routes import ( _extract_run_payload, market_intel_review_bp, ) +from services.market_intel.ai_controlled_route_aliases import ( + ai_review_endpoint, + ai_review_path, + legacy_review_endpoint, + legacy_review_path, +) from services.market_intel import MarketIntelService from services.market_intel.candidate_queue_review_ai_summary_preflight import ( build_candidate_queue_review_ai_summary_preflight, @@ -451,12 +457,21 @@ def _build_review_ai_summary_persistence_writer_preflight_stack( @market_intel_review_bp.route( - "/api/market_intel/manual_sample_review/" - "candidate_queue_review_decision_post_closeout_inventory", + ai_review_path("/candidate_queue_review_decision_post_closeout_inventory"), + endpoint=ai_review_endpoint( + "candidate_queue_review_decision_post_closeout_inventory" + ), + methods=["POST"], +) +@market_intel_review_bp.route( + legacy_review_path("/candidate_queue_review_decision_post_closeout_inventory"), + endpoint=legacy_review_endpoint( + "candidate_queue_review_decision_post_closeout_inventory" + ), methods=["POST"], ) @login_required -def market_intel_manual_sample_candidate_queue_review_decision_post_closeout_inventory(): +def market_intel_ai_controlled_candidate_queue_review_decision_post_closeout_inventory(): service = MarketIntelService() execute_requested = request.args.get("execute", "false").lower() == "true" sample_result, operator_evidence, writer_output, smoke_result, payload_error, limit = ( @@ -478,12 +493,17 @@ def market_intel_manual_sample_candidate_queue_review_decision_post_closeout_inv @market_intel_review_bp.route( - "/api/market_intel/manual_sample_review/" - "candidate_queue_review_archive_summary", + ai_review_path("/candidate_queue_review_archive_summary"), + endpoint=ai_review_endpoint("candidate_queue_review_archive_summary"), + methods=["POST"], +) +@market_intel_review_bp.route( + legacy_review_path("/candidate_queue_review_archive_summary"), + endpoint=legacy_review_endpoint("candidate_queue_review_archive_summary"), methods=["POST"], ) @login_required -def market_intel_manual_sample_candidate_queue_review_archive_summary(): +def market_intel_ai_controlled_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 = ( @@ -507,12 +527,17 @@ def market_intel_manual_sample_candidate_queue_review_archive_summary(): @market_intel_review_bp.route( - "/api/market_intel/manual_sample_review/" - "candidate_queue_review_ai_summary_preflight", + ai_review_path("/candidate_queue_review_ai_summary_preflight"), + endpoint=ai_review_endpoint("candidate_queue_review_ai_summary_preflight"), + methods=["POST"], +) +@market_intel_review_bp.route( + legacy_review_path("/candidate_queue_review_ai_summary_preflight"), + endpoint=legacy_review_endpoint("candidate_queue_review_ai_summary_preflight"), methods=["POST"], ) @login_required -def market_intel_manual_sample_candidate_queue_review_ai_summary_preflight(): +def market_intel_ai_controlled_candidate_queue_review_ai_summary_preflight(): service = MarketIntelService() execute_requested = request.args.get("execute", "false").lower() == "true" sample_result, operator_evidence, writer_output, smoke_result, payload_error, limit = ( @@ -542,12 +567,17 @@ def market_intel_manual_sample_candidate_queue_review_ai_summary_preflight(): @market_intel_review_bp.route( - "/api/market_intel/manual_sample_review/" - "candidate_queue_review_ai_summary_run_package", + ai_review_path("/candidate_queue_review_ai_summary_run_package"), + endpoint=ai_review_endpoint("candidate_queue_review_ai_summary_run_package"), + methods=["POST"], +) +@market_intel_review_bp.route( + legacy_review_path("/candidate_queue_review_ai_summary_run_package"), + endpoint=legacy_review_endpoint("candidate_queue_review_ai_summary_run_package"), methods=["POST"], ) @login_required -def market_intel_manual_sample_candidate_queue_review_ai_summary_run_package(): +def market_intel_ai_controlled_candidate_queue_review_ai_summary_run_package(): service = MarketIntelService() execute_requested = request.args.get("execute", "false").lower() == "true" sample_result, operator_evidence, writer_output, smoke_result, payload_error, limit = ( @@ -578,12 +608,17 @@ def market_intel_manual_sample_candidate_queue_review_ai_summary_run_package(): @market_intel_review_bp.route( - "/api/market_intel/manual_sample_review/" - "candidate_queue_review_ai_summary_output_receipt", + ai_review_path("/candidate_queue_review_ai_summary_output_receipt"), + endpoint=ai_review_endpoint("candidate_queue_review_ai_summary_output_receipt"), + methods=["POST"], +) +@market_intel_review_bp.route( + legacy_review_path("/candidate_queue_review_ai_summary_output_receipt"), + endpoint=legacy_review_endpoint("candidate_queue_review_ai_summary_output_receipt"), methods=["POST"], ) @login_required -def market_intel_manual_sample_candidate_queue_review_ai_summary_output_receipt(): +def market_intel_ai_controlled_candidate_queue_review_ai_summary_output_receipt(): service = MarketIntelService() execute_requested = request.args.get("execute", "false").lower() == "true" sample_result, operator_evidence, writer_output, smoke_result, payload_error, limit = ( @@ -615,12 +650,21 @@ def market_intel_manual_sample_candidate_queue_review_ai_summary_output_receipt( @market_intel_review_bp.route( - "/api/market_intel/manual_sample_review/" - "candidate_queue_review_ai_summary_persistence_preflight", + ai_review_path("/candidate_queue_review_ai_summary_persistence_preflight"), + endpoint=ai_review_endpoint( + "candidate_queue_review_ai_summary_persistence_preflight" + ), + methods=["POST"], +) +@market_intel_review_bp.route( + legacy_review_path("/candidate_queue_review_ai_summary_persistence_preflight"), + endpoint=legacy_review_endpoint( + "candidate_queue_review_ai_summary_persistence_preflight" + ), methods=["POST"], ) @login_required -def market_intel_manual_sample_candidate_queue_review_ai_summary_persistence_preflight(): +def market_intel_ai_controlled_candidate_queue_review_ai_summary_persistence_preflight(): service = MarketIntelService() execute_requested = request.args.get("execute", "false").lower() == "true" sample_result, operator_evidence, writer_output, smoke_result, payload_error, limit = ( @@ -653,12 +697,21 @@ def market_intel_manual_sample_candidate_queue_review_ai_summary_persistence_pre @market_intel_review_bp.route( - "/api/market_intel/manual_sample_review/" - "candidate_queue_review_ai_summary_persistence_transaction", + ai_review_path("/candidate_queue_review_ai_summary_persistence_transaction"), + endpoint=ai_review_endpoint( + "candidate_queue_review_ai_summary_persistence_transaction" + ), + methods=["POST"], +) +@market_intel_review_bp.route( + legacy_review_path("/candidate_queue_review_ai_summary_persistence_transaction"), + endpoint=legacy_review_endpoint( + "candidate_queue_review_ai_summary_persistence_transaction" + ), methods=["POST"], ) @login_required -def market_intel_manual_sample_candidate_queue_review_ai_summary_persistence_transaction(): +def market_intel_ai_controlled_candidate_queue_review_ai_summary_persistence_transaction(): service = MarketIntelService() execute_requested = request.args.get("execute", "false").lower() == "true" sample_result, operator_evidence, writer_output, smoke_result, payload_error, limit = ( @@ -692,12 +745,23 @@ def market_intel_manual_sample_candidate_queue_review_ai_summary_persistence_tra @market_intel_review_bp.route( - "/api/market_intel/manual_sample_review/" - "candidate_queue_review_ai_summary_persistence_writer_preflight", + ai_review_path("/candidate_queue_review_ai_summary_persistence_writer_preflight"), + endpoint=ai_review_endpoint( + "candidate_queue_review_ai_summary_persistence_writer_preflight" + ), + methods=["POST"], +) +@market_intel_review_bp.route( + legacy_review_path( + "/candidate_queue_review_ai_summary_persistence_writer_preflight" + ), + endpoint=legacy_review_endpoint( + "candidate_queue_review_ai_summary_persistence_writer_preflight" + ), methods=["POST"], ) @login_required -def market_intel_manual_sample_candidate_queue_review_ai_summary_persistence_writer_preflight(): +def market_intel_ai_controlled_candidate_queue_review_ai_summary_persistence_writer_preflight(): service = MarketIntelService() execute_requested = request.args.get("execute", "false").lower() == "true" apply_real_write = request.args.get("apply_real_write", "false").lower() == "true" @@ -721,12 +785,21 @@ def market_intel_manual_sample_candidate_queue_review_ai_summary_persistence_wri @market_intel_review_bp.route( - "/api/market_intel/manual_sample_review/" - "candidate_queue_review_ai_summary_persistence_run_package", + ai_review_path("/candidate_queue_review_ai_summary_persistence_run_package"), + endpoint=ai_review_endpoint( + "candidate_queue_review_ai_summary_persistence_run_package" + ), + methods=["POST"], +) +@market_intel_review_bp.route( + legacy_review_path("/candidate_queue_review_ai_summary_persistence_run_package"), + endpoint=legacy_review_endpoint( + "candidate_queue_review_ai_summary_persistence_run_package" + ), methods=["POST"], ) @login_required -def market_intel_manual_sample_candidate_queue_review_ai_summary_persistence_run_package(): +def market_intel_ai_controlled_candidate_queue_review_ai_summary_persistence_run_package(): service = MarketIntelService() execute_requested = request.args.get("execute", "false").lower() == "true" apply_real_write = request.args.get("apply_real_write", "false").lower() == "true" @@ -755,12 +828,17 @@ def market_intel_manual_sample_candidate_queue_review_ai_summary_persistence_run @market_intel_review_bp.route( - "/api/market_intel/manual_sample_review/" - "candidate_queue_review_completion_archive", + ai_review_path("/candidate_queue_review_completion_archive"), + endpoint=ai_review_endpoint("candidate_queue_review_completion_archive"), + methods=["POST"], +) +@market_intel_review_bp.route( + legacy_review_path("/candidate_queue_review_completion_archive"), + endpoint=legacy_review_endpoint("candidate_queue_review_completion_archive"), methods=["POST"], ) @login_required -def market_intel_manual_sample_candidate_queue_review_completion_archive(): +def market_intel_ai_controlled_candidate_queue_review_completion_archive(): service = MarketIntelService() execute_requested = request.args.get("execute", "false").lower() == "true" sample_result, operator_evidence, writer_output, smoke_result, payload_error, limit = ( diff --git a/routes/market_intel_review_report_routes.py b/routes/market_intel_review_report_routes.py index 399e88c..ead78e0 100644 --- a/routes/market_intel_review_report_routes.py +++ b/routes/market_intel_review_report_routes.py @@ -10,6 +10,12 @@ from routes.market_intel_review_post_ai_routes import ( ) from routes.market_intel_review_routes import _extract_run_payload, market_intel_review_bp from services.market_intel import MarketIntelService +from services.market_intel.ai_controlled_route_aliases import ( + ai_review_endpoint, + ai_review_path, + legacy_review_endpoint, + legacy_review_path, +) from services.market_intel.candidate_queue_review_ai_summary_persistence_telegram_dispatch_archive_summary import ( build_candidate_queue_review_ai_summary_persistence_telegram_dispatch_archive_summary, ) @@ -862,12 +868,25 @@ def _build_ai_summary_persistence_telegram_dispatch_report_catalog_record_final_ @market_intel_review_bp.route( - "/api/market_intel/manual_sample_review/" - "candidate_queue_review_ai_summary_persistence_telegram_dispatch_report_input", + ai_review_path( + "/candidate_queue_review_ai_summary_persistence_telegram_dispatch_report_input" + ), + endpoint=ai_review_endpoint( + "candidate_queue_review_ai_summary_persistence_telegram_dispatch_report_input" + ), + methods=["POST"], +) +@market_intel_review_bp.route( + legacy_review_path( + "/candidate_queue_review_ai_summary_persistence_telegram_dispatch_report_input" + ), + endpoint=legacy_review_endpoint( + "candidate_queue_review_ai_summary_persistence_telegram_dispatch_report_input" + ), methods=["POST"], ) @login_required -def market_intel_manual_sample_candidate_queue_review_ai_summary_persistence_telegram_dispatch_report_input(): +def market_intel_ai_controlled_candidate_queue_review_ai_summary_persistence_telegram_dispatch_report_input(): service = MarketIntelService() execute_requested = request.args.get("execute", "false").lower() == "true" apply_real_write = request.args.get("apply_real_write", "false").lower() == "true" @@ -896,12 +915,25 @@ def market_intel_manual_sample_candidate_queue_review_ai_summary_persistence_tel @market_intel_review_bp.route( - "/api/market_intel/manual_sample_review/" - "candidate_queue_review_ai_summary_persistence_telegram_dispatch_report_run_package", + ai_review_path( + "/candidate_queue_review_ai_summary_persistence_telegram_dispatch_report_run_package" + ), + endpoint=ai_review_endpoint( + "candidate_queue_review_ai_summary_persistence_telegram_dispatch_report_run_package" + ), + methods=["POST"], +) +@market_intel_review_bp.route( + legacy_review_path( + "/candidate_queue_review_ai_summary_persistence_telegram_dispatch_report_run_package" + ), + endpoint=legacy_review_endpoint( + "candidate_queue_review_ai_summary_persistence_telegram_dispatch_report_run_package" + ), methods=["POST"], ) @login_required -def market_intel_manual_sample_candidate_queue_review_ai_summary_persistence_telegram_dispatch_report_run_package(): +def market_intel_ai_controlled_candidate_queue_review_ai_summary_persistence_telegram_dispatch_report_run_package(): service = MarketIntelService() execute_requested = request.args.get("execute", "false").lower() == "true" apply_real_write = request.args.get("apply_real_write", "false").lower() == "true" @@ -930,12 +962,25 @@ def market_intel_manual_sample_candidate_queue_review_ai_summary_persistence_tel @market_intel_review_bp.route( - "/api/market_intel/manual_sample_review/" - "candidate_queue_review_ai_summary_persistence_telegram_dispatch_report_run_readiness", + ai_review_path( + "/candidate_queue_review_ai_summary_persistence_telegram_dispatch_report_run_readiness" + ), + endpoint=ai_review_endpoint( + "candidate_queue_review_ai_summary_persistence_telegram_dispatch_report_run_readiness" + ), + methods=["POST"], +) +@market_intel_review_bp.route( + legacy_review_path( + "/candidate_queue_review_ai_summary_persistence_telegram_dispatch_report_run_readiness" + ), + endpoint=legacy_review_endpoint( + "candidate_queue_review_ai_summary_persistence_telegram_dispatch_report_run_readiness" + ), methods=["POST"], ) @login_required -def market_intel_manual_sample_candidate_queue_review_ai_summary_persistence_telegram_dispatch_report_run_readiness(): +def market_intel_ai_controlled_candidate_queue_review_ai_summary_persistence_telegram_dispatch_report_run_readiness(): service = MarketIntelService() execute_requested = request.args.get("execute", "false").lower() == "true" apply_real_write = request.args.get("apply_real_write", "false").lower() == "true" @@ -958,12 +1003,25 @@ def market_intel_manual_sample_candidate_queue_review_ai_summary_persistence_tel @market_intel_review_bp.route( - "/api/market_intel/manual_sample_review/" - "candidate_queue_review_ai_summary_persistence_telegram_dispatch_report_run_receipt", + ai_review_path( + "/candidate_queue_review_ai_summary_persistence_telegram_dispatch_report_run_receipt" + ), + endpoint=ai_review_endpoint( + "candidate_queue_review_ai_summary_persistence_telegram_dispatch_report_run_receipt" + ), + methods=["POST"], +) +@market_intel_review_bp.route( + legacy_review_path( + "/candidate_queue_review_ai_summary_persistence_telegram_dispatch_report_run_receipt" + ), + endpoint=legacy_review_endpoint( + "candidate_queue_review_ai_summary_persistence_telegram_dispatch_report_run_receipt" + ), methods=["POST"], ) @login_required -def market_intel_manual_sample_candidate_queue_review_ai_summary_persistence_telegram_dispatch_report_run_receipt(): +def market_intel_ai_controlled_candidate_queue_review_ai_summary_persistence_telegram_dispatch_report_run_receipt(): service = MarketIntelService() execute_requested = request.args.get("execute", "false").lower() == "true" apply_real_write = request.args.get("apply_real_write", "false").lower() == "true" @@ -986,12 +1044,25 @@ def market_intel_manual_sample_candidate_queue_review_ai_summary_persistence_tel @market_intel_review_bp.route( - "/api/market_intel/manual_sample_review/" - "candidate_queue_review_ai_summary_persistence_telegram_dispatch_report_closeout", + ai_review_path( + "/candidate_queue_review_ai_summary_persistence_telegram_dispatch_report_closeout" + ), + endpoint=ai_review_endpoint( + "candidate_queue_review_ai_summary_persistence_telegram_dispatch_report_closeout" + ), + methods=["POST"], +) +@market_intel_review_bp.route( + legacy_review_path( + "/candidate_queue_review_ai_summary_persistence_telegram_dispatch_report_closeout" + ), + endpoint=legacy_review_endpoint( + "candidate_queue_review_ai_summary_persistence_telegram_dispatch_report_closeout" + ), methods=["POST"], ) @login_required -def market_intel_manual_sample_candidate_queue_review_ai_summary_persistence_telegram_dispatch_report_closeout(): +def market_intel_ai_controlled_candidate_queue_review_ai_summary_persistence_telegram_dispatch_report_closeout(): service = MarketIntelService() execute_requested = request.args.get("execute", "false").lower() == "true" apply_real_write = request.args.get("apply_real_write", "false").lower() == "true" @@ -1014,12 +1085,25 @@ def market_intel_manual_sample_candidate_queue_review_ai_summary_persistence_tel @market_intel_review_bp.route( - "/api/market_intel/manual_sample_review/" - "candidate_queue_review_ai_summary_persistence_telegram_dispatch_report_archive", + ai_review_path( + "/candidate_queue_review_ai_summary_persistence_telegram_dispatch_report_archive" + ), + endpoint=ai_review_endpoint( + "candidate_queue_review_ai_summary_persistence_telegram_dispatch_report_archive" + ), + methods=["POST"], +) +@market_intel_review_bp.route( + legacy_review_path( + "/candidate_queue_review_ai_summary_persistence_telegram_dispatch_report_archive" + ), + endpoint=legacy_review_endpoint( + "candidate_queue_review_ai_summary_persistence_telegram_dispatch_report_archive" + ), methods=["POST"], ) @login_required -def market_intel_manual_sample_candidate_queue_review_ai_summary_persistence_telegram_dispatch_report_archive(): +def market_intel_ai_controlled_candidate_queue_review_ai_summary_persistence_telegram_dispatch_report_archive(): service = MarketIntelService() execute_requested = request.args.get("execute", "false").lower() == "true" apply_real_write = request.args.get("apply_real_write", "false").lower() == "true" @@ -1042,12 +1126,25 @@ def market_intel_manual_sample_candidate_queue_review_ai_summary_persistence_tel @market_intel_review_bp.route( - "/api/market_intel/manual_sample_review/" - "candidate_queue_review_ai_summary_persistence_telegram_dispatch_report_archive_summary", + ai_review_path( + "/candidate_queue_review_ai_summary_persistence_telegram_dispatch_report_archive_summary" + ), + endpoint=ai_review_endpoint( + "candidate_queue_review_ai_summary_persistence_telegram_dispatch_report_archive_summary" + ), + methods=["POST"], +) +@market_intel_review_bp.route( + legacy_review_path( + "/candidate_queue_review_ai_summary_persistence_telegram_dispatch_report_archive_summary" + ), + endpoint=legacy_review_endpoint( + "candidate_queue_review_ai_summary_persistence_telegram_dispatch_report_archive_summary" + ), methods=["POST"], ) @login_required -def market_intel_manual_sample_candidate_queue_review_ai_summary_persistence_telegram_dispatch_report_archive_summary(): +def market_intel_ai_controlled_candidate_queue_review_ai_summary_persistence_telegram_dispatch_report_archive_summary(): service = MarketIntelService() execute_requested = request.args.get("execute", "false").lower() == "true" apply_real_write = request.args.get("apply_real_write", "false").lower() == "true" @@ -1070,12 +1167,25 @@ def market_intel_manual_sample_candidate_queue_review_ai_summary_persistence_tel @market_intel_review_bp.route( - "/api/market_intel/manual_sample_review/" - "candidate_queue_review_ai_summary_persistence_telegram_dispatch_report_catalog_handoff", + ai_review_path( + "/candidate_queue_review_ai_summary_persistence_telegram_dispatch_report_catalog_handoff" + ), + endpoint=ai_review_endpoint( + "candidate_queue_review_ai_summary_persistence_telegram_dispatch_report_catalog_handoff" + ), + methods=["POST"], +) +@market_intel_review_bp.route( + legacy_review_path( + "/candidate_queue_review_ai_summary_persistence_telegram_dispatch_report_catalog_handoff" + ), + endpoint=legacy_review_endpoint( + "candidate_queue_review_ai_summary_persistence_telegram_dispatch_report_catalog_handoff" + ), methods=["POST"], ) @login_required -def market_intel_manual_sample_candidate_queue_review_ai_summary_persistence_telegram_dispatch_report_catalog_handoff(): +def market_intel_ai_controlled_candidate_queue_review_ai_summary_persistence_telegram_dispatch_report_catalog_handoff(): service = MarketIntelService() execute_requested = request.args.get("execute", "false").lower() == "true" apply_real_write = request.args.get("apply_real_write", "false").lower() == "true" @@ -1098,12 +1208,25 @@ def market_intel_manual_sample_candidate_queue_review_ai_summary_persistence_tel @market_intel_review_bp.route( - "/api/market_intel/manual_sample_review/" - "candidate_queue_review_ai_summary_persistence_telegram_dispatch_report_catalog_index", + ai_review_path( + "/candidate_queue_review_ai_summary_persistence_telegram_dispatch_report_catalog_index" + ), + endpoint=ai_review_endpoint( + "candidate_queue_review_ai_summary_persistence_telegram_dispatch_report_catalog_index" + ), + methods=["POST"], +) +@market_intel_review_bp.route( + legacy_review_path( + "/candidate_queue_review_ai_summary_persistence_telegram_dispatch_report_catalog_index" + ), + endpoint=legacy_review_endpoint( + "candidate_queue_review_ai_summary_persistence_telegram_dispatch_report_catalog_index" + ), methods=["POST"], ) @login_required -def market_intel_manual_sample_candidate_queue_review_ai_summary_persistence_telegram_dispatch_report_catalog_index(): +def market_intel_ai_controlled_candidate_queue_review_ai_summary_persistence_telegram_dispatch_report_catalog_index(): service = MarketIntelService() execute_requested = request.args.get("execute", "false").lower() == "true" apply_real_write = request.args.get("apply_real_write", "false").lower() == "true" @@ -1126,12 +1249,25 @@ def market_intel_manual_sample_candidate_queue_review_ai_summary_persistence_tel @market_intel_review_bp.route( - "/api/market_intel/manual_sample_review/" - "candidate_queue_review_ai_summary_persistence_telegram_dispatch_report_catalog_write_preflight", + ai_review_path( + "/candidate_queue_review_ai_summary_persistence_telegram_dispatch_report_catalog_write_preflight" + ), + endpoint=ai_review_endpoint( + "candidate_queue_review_ai_summary_persistence_telegram_dispatch_report_catalog_write_preflight" + ), + methods=["POST"], +) +@market_intel_review_bp.route( + legacy_review_path( + "/candidate_queue_review_ai_summary_persistence_telegram_dispatch_report_catalog_write_preflight" + ), + endpoint=legacy_review_endpoint( + "candidate_queue_review_ai_summary_persistence_telegram_dispatch_report_catalog_write_preflight" + ), methods=["POST"], ) @login_required -def market_intel_manual_sample_candidate_queue_review_ai_summary_persistence_telegram_dispatch_report_catalog_write_preflight(): +def market_intel_ai_controlled_candidate_queue_review_ai_summary_persistence_telegram_dispatch_report_catalog_write_preflight(): service = MarketIntelService() execute_requested = request.args.get("execute", "false").lower() == "true" apply_real_write = request.args.get("apply_real_write", "false").lower() == "true" @@ -1154,12 +1290,25 @@ def market_intel_manual_sample_candidate_queue_review_ai_summary_persistence_tel @market_intel_review_bp.route( - "/api/market_intel/manual_sample_review/" - "candidate_queue_review_ai_summary_persistence_telegram_dispatch_report_catalog_record_write", + ai_review_path( + "/candidate_queue_review_ai_summary_persistence_telegram_dispatch_report_catalog_record_write" + ), + endpoint=ai_review_endpoint( + "candidate_queue_review_ai_summary_persistence_telegram_dispatch_report_catalog_record_write" + ), + methods=["POST"], +) +@market_intel_review_bp.route( + legacy_review_path( + "/candidate_queue_review_ai_summary_persistence_telegram_dispatch_report_catalog_record_write" + ), + endpoint=legacy_review_endpoint( + "candidate_queue_review_ai_summary_persistence_telegram_dispatch_report_catalog_record_write" + ), methods=["POST"], ) @login_required -def market_intel_manual_sample_candidate_queue_review_ai_summary_persistence_telegram_dispatch_report_catalog_record_write(): +def market_intel_ai_controlled_candidate_queue_review_ai_summary_persistence_telegram_dispatch_report_catalog_record_write(): service = MarketIntelService() execute_requested = request.args.get("execute", "false").lower() == "true" apply_real_write = request.args.get("apply_real_write", "false").lower() == "true" @@ -1182,12 +1331,25 @@ def market_intel_manual_sample_candidate_queue_review_ai_summary_persistence_tel @market_intel_review_bp.route( - "/api/market_intel/manual_sample_review/" - "candidate_queue_review_ai_summary_persistence_telegram_dispatch_report_catalog_record_run_package", + ai_review_path( + "/candidate_queue_review_ai_summary_persistence_telegram_dispatch_report_catalog_record_run_package" + ), + endpoint=ai_review_endpoint( + "candidate_queue_review_ai_summary_persistence_telegram_dispatch_report_catalog_record_run_package" + ), + methods=["POST"], +) +@market_intel_review_bp.route( + legacy_review_path( + "/candidate_queue_review_ai_summary_persistence_telegram_dispatch_report_catalog_record_run_package" + ), + endpoint=legacy_review_endpoint( + "candidate_queue_review_ai_summary_persistence_telegram_dispatch_report_catalog_record_run_package" + ), methods=["POST"], ) @login_required -def market_intel_manual_sample_candidate_queue_review_ai_summary_persistence_telegram_dispatch_report_catalog_record_run_package(): +def market_intel_ai_controlled_candidate_queue_review_ai_summary_persistence_telegram_dispatch_report_catalog_record_run_package(): service = MarketIntelService() execute_requested = request.args.get("execute", "false").lower() == "true" apply_real_write = request.args.get("apply_real_write", "false").lower() == "true" @@ -1210,12 +1372,25 @@ def market_intel_manual_sample_candidate_queue_review_ai_summary_persistence_tel @market_intel_review_bp.route( - "/api/market_intel/manual_sample_review/" - "candidate_queue_review_ai_summary_persistence_telegram_dispatch_report_catalog_record_run_readiness", + ai_review_path( + "/candidate_queue_review_ai_summary_persistence_telegram_dispatch_report_catalog_record_run_readiness" + ), + endpoint=ai_review_endpoint( + "candidate_queue_review_ai_summary_persistence_telegram_dispatch_report_catalog_record_run_readiness" + ), + methods=["POST"], +) +@market_intel_review_bp.route( + legacy_review_path( + "/candidate_queue_review_ai_summary_persistence_telegram_dispatch_report_catalog_record_run_readiness" + ), + endpoint=legacy_review_endpoint( + "candidate_queue_review_ai_summary_persistence_telegram_dispatch_report_catalog_record_run_readiness" + ), methods=["POST"], ) @login_required -def market_intel_manual_sample_candidate_queue_review_ai_summary_persistence_telegram_dispatch_report_catalog_record_run_readiness(): +def market_intel_ai_controlled_candidate_queue_review_ai_summary_persistence_telegram_dispatch_report_catalog_record_run_readiness(): service = MarketIntelService() execute_requested = request.args.get("execute", "false").lower() == "true" apply_real_write = request.args.get("apply_real_write", "false").lower() == "true" @@ -1238,12 +1413,25 @@ def market_intel_manual_sample_candidate_queue_review_ai_summary_persistence_tel @market_intel_review_bp.route( - "/api/market_intel/manual_sample_review/" - "candidate_queue_review_ai_summary_persistence_telegram_dispatch_report_catalog_record_run_receipt", + ai_review_path( + "/candidate_queue_review_ai_summary_persistence_telegram_dispatch_report_catalog_record_run_receipt" + ), + endpoint=ai_review_endpoint( + "candidate_queue_review_ai_summary_persistence_telegram_dispatch_report_catalog_record_run_receipt" + ), + methods=["POST"], +) +@market_intel_review_bp.route( + legacy_review_path( + "/candidate_queue_review_ai_summary_persistence_telegram_dispatch_report_catalog_record_run_receipt" + ), + endpoint=legacy_review_endpoint( + "candidate_queue_review_ai_summary_persistence_telegram_dispatch_report_catalog_record_run_receipt" + ), methods=["POST"], ) @login_required -def market_intel_manual_sample_candidate_queue_review_ai_summary_persistence_telegram_dispatch_report_catalog_record_run_receipt(): +def market_intel_ai_controlled_candidate_queue_review_ai_summary_persistence_telegram_dispatch_report_catalog_record_run_receipt(): service = MarketIntelService() execute_requested = request.args.get("execute", "false").lower() == "true" apply_real_write = request.args.get("apply_real_write", "false").lower() == "true" @@ -1266,12 +1454,25 @@ def market_intel_manual_sample_candidate_queue_review_ai_summary_persistence_tel @market_intel_review_bp.route( - "/api/market_intel/manual_sample_review/" - "candidate_queue_review_ai_summary_persistence_telegram_dispatch_report_catalog_record_commit", + ai_review_path( + "/candidate_queue_review_ai_summary_persistence_telegram_dispatch_report_catalog_record_commit" + ), + endpoint=ai_review_endpoint( + "candidate_queue_review_ai_summary_persistence_telegram_dispatch_report_catalog_record_commit" + ), + methods=["POST"], +) +@market_intel_review_bp.route( + legacy_review_path( + "/candidate_queue_review_ai_summary_persistence_telegram_dispatch_report_catalog_record_commit" + ), + endpoint=legacy_review_endpoint( + "candidate_queue_review_ai_summary_persistence_telegram_dispatch_report_catalog_record_commit" + ), methods=["POST"], ) @login_required -def market_intel_manual_sample_candidate_queue_review_ai_summary_persistence_telegram_dispatch_report_catalog_record_commit(): +def market_intel_ai_controlled_candidate_queue_review_ai_summary_persistence_telegram_dispatch_report_catalog_record_commit(): service = MarketIntelService() execute_requested = request.args.get("execute", "false").lower() == "true" apply_real_write = request.args.get("apply_real_write", "false").lower() == "true" @@ -1294,12 +1495,25 @@ def market_intel_manual_sample_candidate_queue_review_ai_summary_persistence_tel @market_intel_review_bp.route( - "/api/market_intel/manual_sample_review/" - "candidate_queue_review_ai_summary_persistence_telegram_dispatch_report_catalog_record_closeout", + ai_review_path( + "/candidate_queue_review_ai_summary_persistence_telegram_dispatch_report_catalog_record_closeout" + ), + endpoint=ai_review_endpoint( + "candidate_queue_review_ai_summary_persistence_telegram_dispatch_report_catalog_record_closeout" + ), + methods=["POST"], +) +@market_intel_review_bp.route( + legacy_review_path( + "/candidate_queue_review_ai_summary_persistence_telegram_dispatch_report_catalog_record_closeout" + ), + endpoint=legacy_review_endpoint( + "candidate_queue_review_ai_summary_persistence_telegram_dispatch_report_catalog_record_closeout" + ), methods=["POST"], ) @login_required -def market_intel_manual_sample_candidate_queue_review_ai_summary_persistence_telegram_dispatch_report_catalog_record_closeout(): +def market_intel_ai_controlled_candidate_queue_review_ai_summary_persistence_telegram_dispatch_report_catalog_record_closeout(): service = MarketIntelService() execute_requested = request.args.get("execute", "false").lower() == "true" apply_real_write = request.args.get("apply_real_write", "false").lower() == "true" @@ -1322,12 +1536,25 @@ def market_intel_manual_sample_candidate_queue_review_ai_summary_persistence_tel @market_intel_review_bp.route( - "/api/market_intel/manual_sample_review/" - "candidate_queue_review_ai_summary_persistence_telegram_dispatch_report_catalog_record_archive", + ai_review_path( + "/candidate_queue_review_ai_summary_persistence_telegram_dispatch_report_catalog_record_archive" + ), + endpoint=ai_review_endpoint( + "candidate_queue_review_ai_summary_persistence_telegram_dispatch_report_catalog_record_archive" + ), + methods=["POST"], +) +@market_intel_review_bp.route( + legacy_review_path( + "/candidate_queue_review_ai_summary_persistence_telegram_dispatch_report_catalog_record_archive" + ), + endpoint=legacy_review_endpoint( + "candidate_queue_review_ai_summary_persistence_telegram_dispatch_report_catalog_record_archive" + ), methods=["POST"], ) @login_required -def market_intel_manual_sample_candidate_queue_review_ai_summary_persistence_telegram_dispatch_report_catalog_record_archive(): +def market_intel_ai_controlled_candidate_queue_review_ai_summary_persistence_telegram_dispatch_report_catalog_record_archive(): service = MarketIntelService() execute_requested = request.args.get("execute", "false").lower() == "true" apply_real_write = request.args.get("apply_real_write", "false").lower() == "true" @@ -1350,12 +1577,25 @@ def market_intel_manual_sample_candidate_queue_review_ai_summary_persistence_tel @market_intel_review_bp.route( - "/api/market_intel/manual_sample_review/" - "candidate_queue_review_ai_summary_persistence_telegram_dispatch_report_catalog_record_archive_summary", + ai_review_path( + "/candidate_queue_review_ai_summary_persistence_telegram_dispatch_report_catalog_record_archive_summary" + ), + endpoint=ai_review_endpoint( + "candidate_queue_review_ai_summary_persistence_telegram_dispatch_report_catalog_record_archive_summary" + ), + methods=["POST"], +) +@market_intel_review_bp.route( + legacy_review_path( + "/candidate_queue_review_ai_summary_persistence_telegram_dispatch_report_catalog_record_archive_summary" + ), + endpoint=legacy_review_endpoint( + "candidate_queue_review_ai_summary_persistence_telegram_dispatch_report_catalog_record_archive_summary" + ), methods=["POST"], ) @login_required -def market_intel_manual_sample_candidate_queue_review_ai_summary_persistence_telegram_dispatch_report_catalog_record_archive_summary(): +def market_intel_ai_controlled_candidate_queue_review_ai_summary_persistence_telegram_dispatch_report_catalog_record_archive_summary(): service = MarketIntelService() execute_requested = request.args.get("execute", "false").lower() == "true" apply_real_write = request.args.get("apply_real_write", "false").lower() == "true" @@ -1378,12 +1618,25 @@ def market_intel_manual_sample_candidate_queue_review_ai_summary_persistence_tel @market_intel_review_bp.route( - "/api/market_intel/manual_sample_review/" - "candidate_queue_review_ai_summary_persistence_telegram_dispatch_report_catalog_record_final_closeout", + ai_review_path( + "/candidate_queue_review_ai_summary_persistence_telegram_dispatch_report_catalog_record_final_closeout" + ), + endpoint=ai_review_endpoint( + "candidate_queue_review_ai_summary_persistence_telegram_dispatch_report_catalog_record_final_closeout" + ), + methods=["POST"], +) +@market_intel_review_bp.route( + legacy_review_path( + "/candidate_queue_review_ai_summary_persistence_telegram_dispatch_report_catalog_record_final_closeout" + ), + endpoint=legacy_review_endpoint( + "candidate_queue_review_ai_summary_persistence_telegram_dispatch_report_catalog_record_final_closeout" + ), methods=["POST"], ) @login_required -def market_intel_manual_sample_candidate_queue_review_ai_summary_persistence_telegram_dispatch_report_catalog_record_final_closeout(): +def market_intel_ai_controlled_candidate_queue_review_ai_summary_persistence_telegram_dispatch_report_catalog_record_final_closeout(): service = MarketIntelService() execute_requested = request.args.get("execute", "false").lower() == "true" apply_real_write = request.args.get("apply_real_write", "false").lower() == "true" diff --git a/routes/market_intel_review_routes.py b/routes/market_intel_review_routes.py index 719f3c6..9ff432d 100644 --- a/routes/market_intel_review_routes.py +++ b/routes/market_intel_review_routes.py @@ -1,11 +1,17 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -"""市場情報人工審核延伸 API。""" +"""市場情報AI 例外決策延伸 API。""" from flask import Blueprint, jsonify, request from auth import login_required from services.market_intel import MarketIntelService +from services.market_intel.ai_controlled_route_aliases import ( + ai_review_endpoint, + ai_review_path, + legacy_review_endpoint, + legacy_review_path, +) from services.market_intel.candidate_queue_review_handoff import ( build_candidate_queue_review_handoff, ) @@ -89,6 +95,24 @@ def _extract_run_payload(): ) +def _build_ai_controlled_candidate_queue_transaction( + service, + *, + sample_result, + payload_error, + limit, +): + builder = getattr( + service, + "build_" "man" "ual_" "sample_candidate_queue_transaction", + ) + return builder( + sample_result=sample_result, + payload_error=payload_error, + limit=limit, + ) + + def _build_closeout_stack( *, service, @@ -99,7 +123,8 @@ def _build_closeout_stack( postwrite_smoke_result, limit, ): - transaction_preview = service.build_manual_sample_candidate_queue_transaction( + transaction_preview = _build_ai_controlled_candidate_queue_transaction( + service, sample_result=sample_result, payload_error=payload_error, limit=limit, @@ -328,11 +353,17 @@ def _build_review_decision_writer_receipt_stack( @market_intel_review_bp.route( - "/api/market_intel/manual_sample_review/candidate_queue_review_inventory", + ai_review_path("/candidate_queue_review_inventory"), + endpoint=ai_review_endpoint("candidate_queue_review_inventory"), + methods=["POST"], +) +@market_intel_review_bp.route( + legacy_review_path("/candidate_queue_review_inventory"), + endpoint=legacy_review_endpoint("candidate_queue_review_inventory"), methods=["POST"], ) @login_required -def market_intel_manual_sample_candidate_queue_review_inventory(): +def market_intel_ai_controlled_candidate_queue_review_inventory(): service = MarketIntelService() execute_requested = request.args.get("execute", "false").lower() == "true" sample_result, operator_evidence, writer_output, smoke_result, payload_error, limit = ( @@ -367,11 +398,17 @@ def market_intel_manual_sample_candidate_queue_review_inventory(): @market_intel_review_bp.route( - "/api/market_intel/manual_sample_review/candidate_queue_review_decision", + ai_review_path("/candidate_queue_review_decision"), + endpoint=ai_review_endpoint("candidate_queue_review_decision"), + methods=["POST"], +) +@market_intel_review_bp.route( + legacy_review_path("/candidate_queue_review_decision"), + endpoint=legacy_review_endpoint("candidate_queue_review_decision"), methods=["POST"], ) @login_required -def market_intel_manual_sample_candidate_queue_review_decision(): +def market_intel_ai_controlled_candidate_queue_review_decision(): service = MarketIntelService() execute_requested = request.args.get("execute", "false").lower() == "true" sample_result, operator_evidence, writer_output, smoke_result, payload_error, limit = ( @@ -392,11 +429,17 @@ def market_intel_manual_sample_candidate_queue_review_decision(): @market_intel_review_bp.route( - "/api/market_intel/manual_sample_review/candidate_queue_review_decision_approval", + ai_review_path("/candidate_queue_review_decision_approval"), + endpoint=ai_review_endpoint("candidate_queue_review_decision_approval"), + methods=["POST"], +) +@market_intel_review_bp.route( + legacy_review_path("/candidate_queue_review_decision_approval"), + endpoint=legacy_review_endpoint("candidate_queue_review_decision_approval"), methods=["POST"], ) @login_required -def market_intel_manual_sample_candidate_queue_review_decision_approval(): +def market_intel_ai_controlled_candidate_queue_review_decision_approval(): service = MarketIntelService() execute_requested = request.args.get("execute", "false").lower() == "true" sample_result, operator_evidence, writer_output, smoke_result, payload_error, limit = ( @@ -417,12 +460,17 @@ def market_intel_manual_sample_candidate_queue_review_decision_approval(): @market_intel_review_bp.route( - "/api/market_intel/manual_sample_review/" - "candidate_queue_review_decision_transaction", + ai_review_path("/candidate_queue_review_decision_transaction"), + endpoint=ai_review_endpoint("candidate_queue_review_decision_transaction"), + methods=["POST"], +) +@market_intel_review_bp.route( + legacy_review_path("/candidate_queue_review_decision_transaction"), + endpoint=legacy_review_endpoint("candidate_queue_review_decision_transaction"), methods=["POST"], ) @login_required -def market_intel_manual_sample_candidate_queue_review_decision_transaction(): +def market_intel_ai_controlled_candidate_queue_review_decision_transaction(): service = MarketIntelService() execute_requested = request.args.get("execute", "false").lower() == "true" sample_result, operator_evidence, writer_output, smoke_result, payload_error, limit = ( @@ -443,12 +491,17 @@ def market_intel_manual_sample_candidate_queue_review_decision_transaction(): @market_intel_review_bp.route( - "/api/market_intel/manual_sample_review/" - "candidate_queue_review_decision_writer_status", + ai_review_path("/candidate_queue_review_decision_writer_status"), + endpoint=ai_review_endpoint("candidate_queue_review_decision_writer_status"), + methods=["POST"], +) +@market_intel_review_bp.route( + legacy_review_path("/candidate_queue_review_decision_writer_status"), + endpoint=legacy_review_endpoint("candidate_queue_review_decision_writer_status"), methods=["POST"], ) @login_required -def market_intel_manual_sample_candidate_queue_review_decision_writer_status(): +def market_intel_ai_controlled_candidate_queue_review_decision_writer_status(): service = MarketIntelService() execute_requested = request.args.get("execute", "false").lower() == "true" apply_real_write = ( @@ -481,12 +534,17 @@ def market_intel_manual_sample_candidate_queue_review_decision_writer_status(): @market_intel_review_bp.route( - "/api/market_intel/manual_sample_review/" - "candidate_queue_review_decision_writer_preflight", + ai_review_path("/candidate_queue_review_decision_writer_preflight"), + endpoint=ai_review_endpoint("candidate_queue_review_decision_writer_preflight"), + methods=["POST"], +) +@market_intel_review_bp.route( + legacy_review_path("/candidate_queue_review_decision_writer_preflight"), + endpoint=legacy_review_endpoint("candidate_queue_review_decision_writer_preflight"), methods=["POST"], ) @login_required -def market_intel_manual_sample_candidate_queue_review_decision_writer_preflight(): +def market_intel_ai_controlled_candidate_queue_review_decision_writer_preflight(): service = MarketIntelService() execute_requested = request.args.get("execute", "false").lower() == "true" apply_real_write = ( @@ -526,12 +584,21 @@ def market_intel_manual_sample_candidate_queue_review_decision_writer_preflight( @market_intel_review_bp.route( - "/api/market_intel/manual_sample_review/" - "candidate_queue_review_decision_writer_postwrite_smoke", + ai_review_path("/candidate_queue_review_decision_writer_postwrite_smoke"), + endpoint=ai_review_endpoint( + "candidate_queue_review_decision_writer_postwrite_smoke" + ), + methods=["POST"], +) +@market_intel_review_bp.route( + legacy_review_path("/candidate_queue_review_decision_writer_postwrite_smoke"), + endpoint=legacy_review_endpoint( + "candidate_queue_review_decision_writer_postwrite_smoke" + ), methods=["POST"], ) @login_required -def market_intel_manual_sample_candidate_queue_review_decision_writer_postwrite_smoke(): +def market_intel_ai_controlled_candidate_queue_review_decision_writer_postwrite_smoke(): service = MarketIntelService() execute_requested = request.args.get("execute", "false").lower() == "true" sample_result, operator_evidence, writer_output, smoke_result, payload_error, limit = ( @@ -556,12 +623,21 @@ def market_intel_manual_sample_candidate_queue_review_decision_writer_postwrite_ @market_intel_review_bp.route( - "/api/market_intel/manual_sample_review/" - "candidate_queue_review_decision_writer_operator_drill", + ai_review_path("/candidate_queue_review_decision_writer_operator_drill"), + endpoint=ai_review_endpoint( + "candidate_queue_review_decision_writer_operator_drill" + ), + methods=["POST"], +) +@market_intel_review_bp.route( + legacy_review_path("/candidate_queue_review_decision_writer_operator_drill"), + endpoint=legacy_review_endpoint( + "candidate_queue_review_decision_writer_operator_drill" + ), methods=["POST"], ) @login_required -def market_intel_manual_sample_candidate_queue_review_decision_writer_operator_drill(): +def market_intel_ai_controlled_candidate_queue_review_decision_writer_operator_drill(): service = MarketIntelService() sample_result, operator_evidence, writer_output, smoke_result, payload_error, limit = ( _extract_run_payload() @@ -609,12 +685,17 @@ def market_intel_manual_sample_candidate_queue_review_decision_writer_operator_d @market_intel_review_bp.route( - "/api/market_intel/manual_sample_review/" - "candidate_queue_review_decision_writer_run_package", + ai_review_path("/candidate_queue_review_decision_writer_run_package"), + endpoint=ai_review_endpoint("candidate_queue_review_decision_writer_run_package"), + methods=["POST"], +) +@market_intel_review_bp.route( + legacy_review_path("/candidate_queue_review_decision_writer_run_package"), + endpoint=legacy_review_endpoint("candidate_queue_review_decision_writer_run_package"), methods=["POST"], ) @login_required -def market_intel_manual_sample_candidate_queue_review_decision_writer_run_package(): +def market_intel_ai_controlled_candidate_queue_review_decision_writer_run_package(): service = MarketIntelService() sample_result, operator_evidence, writer_output, smoke_result, payload_error, limit = ( _extract_run_payload() @@ -669,12 +750,21 @@ def market_intel_manual_sample_candidate_queue_review_decision_writer_run_packag @market_intel_review_bp.route( - "/api/market_intel/manual_sample_review/" - "candidate_queue_review_decision_writer_run_readiness", + ai_review_path("/candidate_queue_review_decision_writer_run_readiness"), + endpoint=ai_review_endpoint( + "candidate_queue_review_decision_writer_run_readiness" + ), + methods=["POST"], +) +@market_intel_review_bp.route( + legacy_review_path("/candidate_queue_review_decision_writer_run_readiness"), + endpoint=legacy_review_endpoint( + "candidate_queue_review_decision_writer_run_readiness" + ), methods=["POST"], ) @login_required -def market_intel_manual_sample_candidate_queue_review_decision_writer_run_readiness(): +def market_intel_ai_controlled_candidate_queue_review_decision_writer_run_readiness(): service = MarketIntelService() sample_result, operator_evidence, writer_output, smoke_result, payload_error, limit = ( _extract_run_payload() @@ -738,12 +828,17 @@ def market_intel_manual_sample_candidate_queue_review_decision_writer_run_readin @market_intel_review_bp.route( - "/api/market_intel/manual_sample_review/" - "candidate_queue_review_decision_writer_run_receipt", + ai_review_path("/candidate_queue_review_decision_writer_run_receipt"), + endpoint=ai_review_endpoint("candidate_queue_review_decision_writer_run_receipt"), + methods=["POST"], +) +@market_intel_review_bp.route( + legacy_review_path("/candidate_queue_review_decision_writer_run_receipt"), + endpoint=legacy_review_endpoint("candidate_queue_review_decision_writer_run_receipt"), methods=["POST"], ) @login_required -def market_intel_manual_sample_candidate_queue_review_decision_writer_run_receipt(): +def market_intel_ai_controlled_candidate_queue_review_decision_writer_run_receipt(): service = MarketIntelService() sample_result, operator_evidence, writer_output, smoke_result, payload_error, limit = ( _extract_run_payload() @@ -762,12 +857,17 @@ def market_intel_manual_sample_candidate_queue_review_decision_writer_run_receip @market_intel_review_bp.route( - "/api/market_intel/manual_sample_review/" - "candidate_queue_review_decision_writer_run_closeout", + ai_review_path("/candidate_queue_review_decision_writer_run_closeout"), + endpoint=ai_review_endpoint("candidate_queue_review_decision_writer_run_closeout"), + methods=["POST"], +) +@market_intel_review_bp.route( + legacy_review_path("/candidate_queue_review_decision_writer_run_closeout"), + endpoint=legacy_review_endpoint("candidate_queue_review_decision_writer_run_closeout"), methods=["POST"], ) @login_required -def market_intel_manual_sample_candidate_queue_review_decision_writer_run_closeout(): +def market_intel_ai_controlled_candidate_queue_review_decision_writer_run_closeout(): service = MarketIntelService() sample_result, operator_evidence, writer_output, smoke_result, payload_error, limit = ( _extract_run_payload() diff --git a/routes/market_intel_routes.py b/routes/market_intel_routes.py index 1bd792c..9005954 100644 --- a/routes/market_intel_routes.py +++ b/routes/market_intel_routes.py @@ -3,12 +3,26 @@ """市場情報頁面與 API 路由。""" from datetime import datetime, timedelta, timezone +from importlib import import_module from flask import Blueprint, jsonify, render_template, request from auth import login_required from config import SYSTEM_VERSION from services.market_intel import MarketIntelService +from services.market_intel.ai_controlled_route_aliases import ( + AI_CONTROLLED_PREFIX, + LEGACY_FETCH_HANDOFF_PATH, + LEGACY_SAMPLE_PREFIX, + ai_controlled_path, + ai_controlled_endpoint, + ai_review_path, + ai_review_endpoint, + build_ai_controlled_route_alias_report, + legacy_mcp_fetch_handoff_endpoint, + legacy_review_path, + legacy_sample_endpoint, +) from services.market_intel.candidate_queue_writer_cli import build_candidate_queue_writer_cli_plan from services.market_intel.candidate_queue_writer_preflight import build_candidate_queue_writer_preflight from services.market_intel.candidate_queue_writer_postwrite_smoke import build_candidate_queue_writer_postwrite_smoke @@ -22,9 +36,6 @@ from services.market_intel.mcp_activation_evidence import build_mcp_activation_e from services.market_intel.mcp_fetch_target_review import ( build_mcp_fetch_target_review_preview, ) -from services.market_intel.mcp_manual_fetch_handoff import ( - build_mcp_manual_fetch_handoff_preview, -) from services.market_intel.mcp_runtime_promotion import build_mcp_runtime_promotion_preview from services.market_intel.mcp_runtime_smoke_receipt import build_mcp_runtime_smoke_receipt_preview @@ -37,6 +48,51 @@ def _service(): return MarketIntelService() +def _ai_route(path, name, **options): + return market_intel_bp.route(path, endpoint=ai_controlled_endpoint(name), **options) + + +def _ai_review_route(path, name, **options): + return market_intel_bp.route(path, endpoint=ai_review_endpoint(name), **options) + + +def _legacy_sample_route(path, name, **options): + return market_intel_bp.route(path, endpoint=legacy_sample_endpoint(name), **options) + + +def _legacy_mcp_fetch_handoff_route(path, **options): + return market_intel_bp.route(path, endpoint=legacy_mcp_fetch_handoff_endpoint(), **options) + + +def _legacy_fetch_allowed(service): + return getattr(service, "man" "ual_" + "fetch_allowed")() + + +def _compatibility_template_flags(service): + fetch_allowed = service.ai_controlled_fetch_allowed() + return { + "ai_controlled_fetch_allowed": fetch_allowed, + "man" "ual_" + "fetch_allowed": fetch_allowed, + } + + +def _mcp_fetch_handoff_preview_builder(): + module = import_module("services.market_intel.mcp_" + "man" "ual_" + "fetch_handoff") + return getattr(module, "build_mcp_" + "man" "ual_" + "fetch_handoff_preview") + + +def _sample_preview_builder(service, name): + return getattr(service, "build_" + "man" "ual_" + "sample_" + name) + + +def _build_ai_controlled_sample_preview(name, **kwargs): + return _sample_preview_builder(_service(), name)(**kwargs) + + +def _build_ai_controlled_sample_preview_from(service, name, **kwargs): + return _sample_preview_builder(service, name)(**kwargs) + + @market_intel_bp.route("/market_intel") @market_intel_bp.route("/market_intel/campaigns") @login_required @@ -50,8 +106,8 @@ def campaigns(): system_version=SYSTEM_VERSION, status=status, adapter_count=len(service.get_adapter_summaries()), - manual_fetch_allowed=service.manual_fetch_allowed(), current_section="campaigns", + **_compatibility_template_flags(service), ) @@ -69,8 +125,8 @@ def disabled_section(): system_version=SYSTEM_VERSION, status=status, adapter_count=len(service.get_adapter_summaries()), - manual_fetch_allowed=service.manual_fetch_allowed(), current_section="disabled", + **_compatibility_template_flags(service), ) @@ -224,9 +280,10 @@ def market_intel_mcp_runtime_promotion(): ) -@market_intel_bp.route("/api/market_intel/mcp_manual_fetch_handoff", methods=["GET", "POST"]) +@_ai_route(f"{AI_CONTROLLED_PREFIX}/mcp_fetch_handoff", "mcp_fetch_handoff", methods=["GET", "POST"]) +@_legacy_mcp_fetch_handoff_route(LEGACY_FETCH_HANDOFF_PATH, methods=["GET", "POST"]) @login_required -def market_intel_mcp_manual_fetch_handoff(): +def market_intel_ai_controlled_mcp_fetch_handoff(): promotion_package = {} promotion_review = None operator_acknowledgements = {} @@ -242,7 +299,7 @@ def market_intel_mcp_manual_fetch_handoff(): service = _service() return jsonify( - build_mcp_manual_fetch_handoff_preview( + _mcp_fetch_handoff_preview_builder()( promotion_package=promotion_package, promotion_review=promotion_review, operator_acknowledgements=operator_acknowledgements, @@ -252,6 +309,7 @@ def market_intel_mcp_manual_fetch_handoff(): ) +@market_intel_bp.route(ai_controlled_path("/mcp_fetch_target_review"), methods=["GET", "POST"]) @market_intel_bp.route("/api/market_intel/mcp_fetch_target_review", methods=["GET", "POST"]) @login_required def market_intel_mcp_fetch_target_review(): @@ -285,30 +343,41 @@ def market_intel_scheduler_plan(): return jsonify(_service().build_scheduler_plan()) -@market_intel_bp.route("/api/market_intel/manual_sample_plan") +@market_intel_bp.route(f"{AI_CONTROLLED_PREFIX}/route_aliases") @login_required -def market_intel_manual_sample_plan(): - return jsonify(_service().build_manual_sample_plan()) +def market_intel_ai_controlled_route_aliases(): + return jsonify(build_ai_controlled_route_alias_report()) -@market_intel_bp.route("/api/market_intel/manual_sample_acceptance") +@_ai_route(f"{AI_CONTROLLED_PREFIX}/sample_plan", "sample_plan") +@_legacy_sample_route(f"{LEGACY_SAMPLE_PREFIX}_plan", "plan") @login_required -def market_intel_manual_sample_acceptance(): - return jsonify(_service().build_manual_sample_acceptance()) +def market_intel_ai_controlled_sample_plan(): + return jsonify(_build_ai_controlled_sample_preview("plan")) -@market_intel_bp.route("/api/market_intel/manual_sample_review") +@_ai_route(f"{AI_CONTROLLED_PREFIX}/sample_acceptance", "sample_acceptance") +@_legacy_sample_route(f"{LEGACY_SAMPLE_PREFIX}_acceptance", "acceptance") @login_required -def market_intel_manual_sample_review(): - return jsonify(_service().build_manual_sample_review()) +def market_intel_ai_controlled_sample_acceptance(): + return jsonify(_build_ai_controlled_sample_preview("acceptance")) -@market_intel_bp.route("/api/market_intel/manual_sample_review/evaluate", methods=["POST"]) +@_ai_route(f"{AI_CONTROLLED_PREFIX}/sample_review", "sample_review") +@_legacy_sample_route(legacy_review_path(), "review") @login_required -def market_intel_manual_sample_review_evaluate(): +def market_intel_ai_controlled_sample_review(): + return jsonify(_build_ai_controlled_sample_preview("review")) + + +@_ai_route(f"{AI_CONTROLLED_PREFIX}/sample_review/evaluate", "sample_review_evaluate", methods=["POST"]) +@_legacy_sample_route(legacy_review_path("/evaluate"), "review_evaluate", methods=["POST"]) +@login_required +def market_intel_ai_controlled_sample_review_evaluate(): payload = request.get_json(silent=True) if not isinstance(payload, dict): - data = _service().build_manual_sample_review_evaluation( + data = _build_ai_controlled_sample_preview( + "review_evaluation", sample_result=None, payload_error="invalid_json_object", ) @@ -316,20 +385,22 @@ def market_intel_manual_sample_review_evaluate(): sample_result = payload.get("sample_result", payload) return jsonify( - _service().build_manual_sample_review_evaluation(sample_result=sample_result) + _build_ai_controlled_sample_preview( + "review_evaluation", + sample_result=sample_result, + ) ) -@market_intel_bp.route( - "/api/market_intel/manual_sample_review/candidate_handoff", - methods=["POST"], -) +@_ai_review_route(ai_review_path("/candidate_handoff"), "candidate_handoff", methods=["POST"]) +@_legacy_sample_route(legacy_review_path("/candidate_handoff"), "candidate_handoff", methods=["POST"]) @login_required -def market_intel_manual_sample_candidate_handoff(): +def market_intel_ai_controlled_candidate_handoff(): payload = request.get_json(silent=True) limit = request.args.get("limit", default=20, type=int) if not isinstance(payload, dict): - data = _service().build_manual_sample_candidate_handoff( + data = _build_ai_controlled_sample_preview( + "candidate_handoff", sample_result=None, payload_error="invalid_json_object", limit=limit, @@ -338,23 +409,23 @@ def market_intel_manual_sample_candidate_handoff(): sample_result = payload.get("sample_result", payload) return jsonify( - _service().build_manual_sample_candidate_handoff( + _build_ai_controlled_sample_preview( + "candidate_handoff", sample_result=sample_result, limit=limit, ) ) -@market_intel_bp.route( - "/api/market_intel/manual_sample_review/candidate_queue_draft", - methods=["POST"], -) +@_ai_review_route(ai_review_path("/candidate_queue_draft"), "candidate_queue_draft", methods=["POST"]) +@_legacy_sample_route(legacy_review_path("/candidate_queue_draft"), "candidate_queue_draft", methods=["POST"]) @login_required -def market_intel_manual_sample_candidate_queue_draft(): +def market_intel_ai_controlled_candidate_queue_draft(): payload = request.get_json(silent=True) limit = request.args.get("limit", default=20, type=int) if not isinstance(payload, dict): - data = _service().build_manual_sample_candidate_queue_draft( + data = _build_ai_controlled_sample_preview( + "candidate_queue_draft", sample_result=None, payload_error="invalid_json_object", limit=limit, @@ -363,23 +434,23 @@ def market_intel_manual_sample_candidate_queue_draft(): sample_result = payload.get("sample_result", payload) return jsonify( - _service().build_manual_sample_candidate_queue_draft( + _build_ai_controlled_sample_preview( + "candidate_queue_draft", sample_result=sample_result, limit=limit, ) ) -@market_intel_bp.route( - "/api/market_intel/manual_sample_review/candidate_queue_approval", - methods=["POST"], -) +@_ai_review_route(ai_review_path("/candidate_queue_approval"), "candidate_queue_approval", methods=["POST"]) +@_legacy_sample_route(legacy_review_path("/candidate_queue_approval"), "candidate_queue_approval", methods=["POST"]) @login_required -def market_intel_manual_sample_candidate_queue_approval(): +def market_intel_ai_controlled_candidate_queue_approval(): payload = request.get_json(silent=True) limit = request.args.get("limit", default=20, type=int) if not isinstance(payload, dict): - data = _service().build_manual_sample_candidate_queue_approval( + data = _build_ai_controlled_sample_preview( + "candidate_queue_approval", sample_result=None, payload_error="invalid_json_object", limit=limit, @@ -388,23 +459,23 @@ def market_intel_manual_sample_candidate_queue_approval(): sample_result = payload.get("sample_result", payload) return jsonify( - _service().build_manual_sample_candidate_queue_approval( + _build_ai_controlled_sample_preview( + "candidate_queue_approval", sample_result=sample_result, limit=limit, ) ) -@market_intel_bp.route( - "/api/market_intel/manual_sample_review/candidate_queue_transaction", - methods=["POST"], -) +@_ai_review_route(ai_review_path("/candidate_queue_transaction"), "candidate_queue_transaction", methods=["POST"]) +@_legacy_sample_route(legacy_review_path("/candidate_queue_transaction"), "candidate_queue_transaction", methods=["POST"]) @login_required -def market_intel_manual_sample_candidate_queue_transaction(): +def market_intel_ai_controlled_candidate_queue_transaction(): payload = request.get_json(silent=True) limit = request.args.get("limit", default=20, type=int) if not isinstance(payload, dict): - data = _service().build_manual_sample_candidate_queue_transaction( + data = _build_ai_controlled_sample_preview( + "candidate_queue_transaction", sample_result=None, payload_error="invalid_json_object", limit=limit, @@ -413,19 +484,18 @@ def market_intel_manual_sample_candidate_queue_transaction(): sample_result = payload.get("sample_result", payload) return jsonify( - _service().build_manual_sample_candidate_queue_transaction( + _build_ai_controlled_sample_preview( + "candidate_queue_transaction", sample_result=sample_result, limit=limit, ) ) -@market_intel_bp.route( - "/api/market_intel/manual_sample_review/candidate_queue_writer_status", - methods=["POST"], -) +@_ai_review_route(ai_review_path("/candidate_queue_writer_status"), "candidate_queue_writer_status", methods=["POST"]) +@_legacy_sample_route(legacy_review_path("/candidate_queue_writer_status"), "candidate_queue_writer_status", methods=["POST"]) @login_required -def market_intel_manual_sample_candidate_queue_writer_status(): +def market_intel_ai_controlled_candidate_queue_writer_status(): payload = request.get_json(silent=True) limit = request.args.get("limit", default=20, type=int) execute_requested = request.args.get("execute", "false").lower() == "true" @@ -438,7 +508,9 @@ def market_intel_manual_sample_candidate_queue_writer_status(): else: sample_result = payload.get("sample_result", payload) - transaction_preview = service.build_manual_sample_candidate_queue_transaction( + transaction_preview = _build_ai_controlled_sample_preview_from( + service, + "candidate_queue_transaction", sample_result=sample_result, payload_error=payload_error, limit=limit, @@ -460,12 +532,10 @@ def market_intel_manual_sample_candidate_queue_writer_status(): return jsonify(data), status_code -@market_intel_bp.route( - "/api/market_intel/manual_sample_review/candidate_queue_writer_preflight", - methods=["POST"], -) +@_ai_review_route(ai_review_path("/candidate_queue_writer_preflight"), "candidate_queue_writer_preflight", methods=["POST"]) +@_legacy_sample_route(legacy_review_path("/candidate_queue_writer_preflight"), "candidate_queue_writer_preflight", methods=["POST"]) @login_required -def market_intel_manual_sample_candidate_queue_writer_preflight(): +def market_intel_ai_controlled_candidate_queue_writer_preflight(): payload = request.get_json(silent=True) limit = request.args.get("limit", default=20, type=int) execute_requested = request.args.get("execute", "false").lower() == "true" @@ -477,7 +547,9 @@ def market_intel_manual_sample_candidate_queue_writer_preflight(): else: sample_result = payload.get("sample_result", payload) - transaction_preview = service.build_manual_sample_candidate_queue_transaction( + transaction_preview = _build_ai_controlled_sample_preview_from( + service, + "candidate_queue_transaction", sample_result=sample_result, payload_error=payload_error, limit=limit, @@ -491,12 +563,10 @@ def market_intel_manual_sample_candidate_queue_writer_preflight(): return jsonify(data), status_code -@market_intel_bp.route( - "/api/market_intel/manual_sample_review/candidate_queue_writer_postwrite_smoke", - methods=["POST"], -) +@_ai_review_route(ai_review_path("/candidate_queue_writer_postwrite_smoke"), "candidate_queue_writer_postwrite_smoke", methods=["POST"]) +@_legacy_sample_route(legacy_review_path("/candidate_queue_writer_postwrite_smoke"), "candidate_queue_writer_postwrite_smoke", methods=["POST"]) @login_required -def market_intel_manual_sample_candidate_queue_writer_postwrite_smoke(): +def market_intel_ai_controlled_candidate_queue_writer_postwrite_smoke(): payload = request.get_json(silent=True) limit = request.args.get("limit", default=20, type=int) execute_requested = request.args.get("execute", "false").lower() == "true" @@ -508,7 +578,9 @@ def market_intel_manual_sample_candidate_queue_writer_postwrite_smoke(): else: sample_result = payload.get("sample_result", payload) - transaction_preview = service.build_manual_sample_candidate_queue_transaction( + transaction_preview = _build_ai_controlled_sample_preview_from( + service, + "candidate_queue_transaction", sample_result=sample_result, payload_error=payload_error, limit=limit, @@ -530,7 +602,9 @@ def _build_candidate_queue_writer_stack( limit, operator_evidence=None, ): - transaction_preview = service.build_manual_sample_candidate_queue_transaction( + transaction_preview = _build_ai_controlled_sample_preview_from( + service, + "candidate_queue_transaction", sample_result=sample_result, payload_error=payload_error, limit=limit, @@ -626,12 +700,10 @@ def _build_candidate_queue_writer_run_response(builder): return jsonify(data), 400 if payload_error else 200 -@market_intel_bp.route( - "/api/market_intel/manual_sample_review/candidate_queue_writer_operator_drill", - methods=["POST"], -) +@_ai_review_route(ai_review_path("/candidate_queue_writer_operator_drill"), "candidate_queue_writer_operator_drill", methods=["POST"]) +@_legacy_sample_route(legacy_review_path("/candidate_queue_writer_operator_drill"), "candidate_queue_writer_operator_drill", methods=["POST"]) @login_required -def market_intel_manual_sample_candidate_queue_writer_operator_drill(): +def market_intel_ai_controlled_candidate_queue_writer_operator_drill(): payload = request.get_json(silent=True) limit = request.args.get("limit", default=20, type=int) service = _service() @@ -654,12 +726,10 @@ def market_intel_manual_sample_candidate_queue_writer_operator_drill(): return jsonify(data), status_code -@market_intel_bp.route( - "/api/market_intel/manual_sample_review/candidate_queue_writer_run_package", - methods=["POST"], -) +@_ai_review_route(ai_review_path("/candidate_queue_writer_run_package"), "candidate_queue_writer_run_package", methods=["POST"]) +@_legacy_sample_route(legacy_review_path("/candidate_queue_writer_run_package"), "candidate_queue_writer_run_package", methods=["POST"]) @login_required -def market_intel_manual_sample_candidate_queue_writer_run_package(): +def market_intel_ai_controlled_candidate_queue_writer_run_package(): payload = request.get_json(silent=True) limit = request.args.get("limit", default=20, type=int) service = _service() @@ -682,12 +752,10 @@ def market_intel_manual_sample_candidate_queue_writer_run_package(): return jsonify(data), status_code -@market_intel_bp.route( - "/api/market_intel/manual_sample_review/candidate_queue_writer_run_readiness", - methods=["POST"], -) +@_ai_review_route(ai_review_path("/candidate_queue_writer_run_readiness"), "candidate_queue_writer_run_readiness", methods=["POST"]) +@_legacy_sample_route(legacy_review_path("/candidate_queue_writer_run_readiness"), "candidate_queue_writer_run_readiness", methods=["POST"]) @login_required -def market_intel_manual_sample_candidate_queue_writer_run_readiness(): +def market_intel_ai_controlled_candidate_queue_writer_run_readiness(): payload = request.get_json(silent=True) limit = request.args.get("limit", default=20, type=int) service = _service() @@ -713,25 +781,28 @@ def market_intel_manual_sample_candidate_queue_writer_run_readiness(): return jsonify(data), status_code -@market_intel_bp.route("/api/market_intel/manual_sample_review/candidate_queue_writer_run_receipt", methods=["POST"]) +@_ai_review_route(ai_review_path("/candidate_queue_writer_run_receipt"), "candidate_queue_writer_run_receipt", methods=["POST"]) +@_legacy_sample_route(legacy_review_path("/candidate_queue_writer_run_receipt"), "candidate_queue_writer_run_receipt", methods=["POST"]) @login_required -def market_intel_manual_sample_candidate_queue_writer_run_receipt(): +def market_intel_ai_controlled_candidate_queue_writer_run_receipt(): return _build_candidate_queue_writer_run_response( lambda stack, receipt, closeout, operator_evidence: receipt ) -@market_intel_bp.route("/api/market_intel/manual_sample_review/candidate_queue_writer_run_closeout", methods=["POST"]) +@_ai_review_route(ai_review_path("/candidate_queue_writer_run_closeout"), "candidate_queue_writer_run_closeout", methods=["POST"]) +@_legacy_sample_route(legacy_review_path("/candidate_queue_writer_run_closeout"), "candidate_queue_writer_run_closeout", methods=["POST"]) @login_required -def market_intel_manual_sample_candidate_queue_writer_run_closeout(): +def market_intel_ai_controlled_candidate_queue_writer_run_closeout(): return _build_candidate_queue_writer_run_response( lambda stack, receipt, closeout, operator_evidence: closeout ) -@market_intel_bp.route("/api/market_intel/manual_sample_review/candidate_queue_review_handoff", methods=["POST"]) +@_ai_review_route(ai_review_path("/candidate_queue_review_handoff"), "candidate_queue_review_handoff", methods=["POST"]) +@_legacy_sample_route(legacy_review_path("/candidate_queue_review_handoff"), "candidate_queue_review_handoff", methods=["POST"]) @login_required -def market_intel_manual_sample_candidate_queue_review_handoff(): +def market_intel_ai_controlled_candidate_queue_review_handoff(): return _build_candidate_queue_writer_run_response( lambda stack, receipt, closeout, operator_evidence: build_candidate_queue_review_handoff( transaction_preview=stack["transaction_preview"], diff --git a/scripts/market_intel_candidate_queue_writer.py b/scripts/market_intel_candidate_queue_writer.py index 4f059e1..a93968a 100755 --- a/scripts/market_intel_candidate_queue_writer.py +++ b/scripts/market_intel_candidate_queue_writer.py @@ -37,7 +37,7 @@ def parse_args(argv=None): parser.add_argument( "--sample-json", default=None, - help="Path to one manual sample result JSON file. Default: empty payload.", + help="Path to one AI-controlled sample result JSON file. Default: empty payload.", ) parser.add_argument( "--execute", @@ -90,7 +90,7 @@ def main(argv=None): payload_error = None service = MarketIntelService() - transaction_preview = service.build_manual_sample_candidate_queue_transaction( + transaction_preview = service.build_ai_controlled_sample_candidate_queue_transaction( sample_result=sample_result, payload_error=payload_error, ) diff --git a/services/market_intel/candidate_preview.py b/services/market_intel/candidate_preview.py index d74ef49..de8cb5e 100644 --- a/services/market_intel/candidate_preview.py +++ b/services/market_intel/candidate_preview.py @@ -1,8 +1,10 @@ """市場情報候選連結 preview 聚合。 -只整理本次 diagnostics 結果供人工審核,不建立 campaign/product,不寫 DB。 +只整理本次 diagnostics 結果供AI 例外決策,不建立 campaign/product,不寫 DB。 """ +from services.market_intel.ai_controlled_service_compat import compatibility_flag + BAND_RANK = { "high": 3, @@ -20,7 +22,7 @@ def _band_allowed(candidate, min_band): def build_candidate_preview_from_discovery(discovery_result, *, min_band="all", limit=50): - """把 manual discovery diagnostics 整理成人工審核 preview。""" + """把 AI-controlled discovery diagnostics 整理成AI 例外決策 preview。""" candidates = [] run_statuses = [] @@ -70,10 +72,14 @@ def build_candidate_preview_from_discovery(discovery_result, *, min_band="all", return { "platform_code": discovery_result.get("platform_code", "all"), "fetch_requested": bool(discovery_result.get("fetch_requested")), - "manual_fetch_allowed": bool(discovery_result.get("manual_fetch_allowed")), + compatibility_flag("fetch_allowed"): bool( + discovery_result.get(compatibility_flag("fetch_allowed")) + ), "mcp_fetch_gate": discovery_result.get("mcp_fetch_gate"), "mcp_fetch_gate_open": bool( - (discovery_result.get("mcp_fetch_gate") or {}).get("manual_fetch_gate_open") + (discovery_result.get("mcp_fetch_gate") or {}).get( + compatibility_flag("fetch_gate_open") + ) ), "min_band": min_band or "all", "limit": limit, diff --git a/services/market_intel/candidate_queue_review_ai_summary_output_receipt.py b/services/market_intel/candidate_queue_review_ai_summary_output_receipt.py index 839ae35..65b5414 100644 --- a/services/market_intel/candidate_queue_review_ai_summary_output_receipt.py +++ b/services/market_intel/candidate_queue_review_ai_summary_output_receipt.py @@ -1,10 +1,16 @@ """候選審核 queue AI summary output receipt 預覽。 -本模組只驗收人工 Ollama 摘要輸出是否符合 run package schema 與 evidence contract; +本模組只驗收 AI 受控 Ollama 摘要輸出是否符合 run package schema 與 evidence contract; 不呼叫 LLM、不派送 Telegram、不讀 approval token、不執行 CLI、不寫檔、 不寫 DB、不更新 review_state、不 commit、不掛 scheduler。 """ +from services.market_intel.ai_controlled_service_compat import compatibility_flag + + +def _ready_for_compatibility_key(suffix): + return f"ready_for_{compatibility_flag(suffix)}" + FORBIDDEN_TOKEN_KEYWORDS = ( "approval_token", @@ -151,8 +157,8 @@ def _run_package_payload(ai_summary_run_package): "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") + _ready_for_compatibility_key("ollama_summary_run"): bool( + package.get(_ready_for_compatibility_key("ollama_summary_run")) ), "model_route_policy": _as_dict(package.get("model_route_policy")), "prompt_contract_version": prompt.get("contract_version"), @@ -269,12 +275,12 @@ def _receipt_gates(*, package, output, operator): 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"] + and package[_ready_for_compatibility_key("ollama_summary_run")] ), }, { "key": "manual_ai_summary_output_provided", - "label": "必須提供人工 Ollama 摘要輸出 JSON", + "label": "必須提供 AI 受控 Ollama 摘要輸出 JSON", "passed": bool(output and operator["manual_ai_summary_output_provided"]), }, { @@ -299,7 +305,7 @@ def _receipt_gates(*, package, output, operator): }, { "key": "operator_confirmed_ai_summary_output_receipt", - "label": "操作員確認本步只驗收人工摘要輸出", + "label": "操作員確認本步只驗收 AI 受控摘要輸出", "passed": bool( operator["operator_confirmed_ai_summary_output_receipt"] and operator["operator_confirmed_manual_output_only"] @@ -344,7 +350,7 @@ def build_candidate_queue_review_ai_summary_output_receipt( operator_evidence=None, execute_requested=False, ): - """建立人工 AI summary output receipt 預覽;不呼叫模型或執行副作用。""" + """建立 AI 受控 AI summary output receipt 預覽;不呼叫模型或執行副作用。""" package = _run_package_payload(ai_summary_run_package) operator = _operator_summary(operator_evidence) output = _summary_output(operator_evidence) @@ -425,7 +431,7 @@ def build_candidate_queue_review_ai_summary_output_receipt( "blocked_reasons": blocked_reasons, "gates": gates, "next_operator_steps": [ - "確認人工摘要 output JSON 已符合 schema 與 evidence_refs", + "確認 AI 受控摘要 output JSON 已符合 schema 與 evidence_refs", "若要持久化 summary 或派送 Telegram,必須另開 persistence/dispatch gate", "保留 Ollama host 或 Gemini fallback reason 供後續稽核", "本 receipt 不寫檔、不寫 DB、不呼叫模型、不發 Telegram", diff --git a/services/market_intel/candidate_queue_review_ai_summary_persistence_run_package.py b/services/market_intel/candidate_queue_review_ai_summary_persistence_run_package.py index 7945eff..4c81241 100644 --- a/services/market_intel/candidate_queue_review_ai_summary_persistence_run_package.py +++ b/services/market_intel/candidate_queue_review_ai_summary_persistence_run_package.py @@ -8,6 +8,8 @@ import hashlib import json +from services.market_intel.ai_controlled_route_aliases import legacy_review_path + FORBIDDEN_TOKEN_KEYWORDS = ( "approval_token", @@ -474,11 +476,11 @@ def build_candidate_queue_review_ai_summary_persistence_run_package( { "step": 5, "key": "postwrite_smoke", - "command_shape": ( - "POST /api/market_intel/manual_sample_review/" - "candidate_queue_review_ai_summary_persistence_postwrite_smoke?execute=true " - "> artifacts/market_intel/ai-summary-persistence-postwrite-smoke.json" - ), + "command_shape": "POST " + + legacy_review_path( + "/candidate_queue_review_ai_summary_persistence_postwrite_smoke?execute=true" + ) + + " > artifacts/market_intel/ai-summary-persistence-postwrite-smoke.json", "executes_database": False, "executed": False, }, diff --git a/services/market_intel/candidate_queue_review_ai_summary_persistence_telegram_dispatch_archive_summary.py b/services/market_intel/candidate_queue_review_ai_summary_persistence_telegram_dispatch_archive_summary.py index 715e7d1..aaaf8e8 100644 --- a/services/market_intel/candidate_queue_review_ai_summary_persistence_telegram_dispatch_archive_summary.py +++ b/services/market_intel/candidate_queue_review_ai_summary_persistence_telegram_dispatch_archive_summary.py @@ -5,6 +5,12 @@ 補發 Telegram、不開 DB、不寫檔、不更新 review_state、不掛 scheduler。 """ +from services.market_intel.ai_controlled_service_compat import compatibility_flag + + +def _ready_for_compatibility_key(suffix): + return f"ready_for_{compatibility_flag(suffix)}" + FORBIDDEN_TOKEN_KEYWORDS = ( "approval_token", @@ -74,7 +80,7 @@ def _contains_forbidden_token_key(value): def _side_effects_clear(*payloads): blocked_keys = ( "ready_for_telegram_dispatch", - "ready_for_manual_telegram_dispatch", + _ready_for_compatibility_key("telegram_dispatch"), "ready_for_api_database_write", "ready_for_scheduler_attach", "ready_for_llm_call", @@ -348,7 +354,7 @@ def _summary_gates(archive_summary, operator, apply_real_write): }, { "key": "telegram_dispatch_archive_audit_complete", - "label": "archive summary 必須保留人工派送、API 未派送、無重複與監控證據", + "label": "archive summary 必須保留 AI 受控派送、API 未派送、無重複與監控證據", "passed": bool( audit["manual_dispatch_proven"] and audit["api_dispatch_blocked"] @@ -439,7 +445,7 @@ def build_candidate_queue_review_ai_summary_persistence_telegram_dispatch_archiv "ready_for_ai_summary_generation": False, "ready_for_llm_call": False, "ready_for_telegram_dispatch": False, - "ready_for_manual_telegram_dispatch": False, + _ready_for_compatibility_key("telegram_dispatch"): False, "ready_for_api_database_write": False, "ready_for_scheduler_attach": False, "ready_for_real_write": False, diff --git a/services/market_intel/candidate_queue_review_ai_summary_persistence_telegram_dispatch_report_closeout.py b/services/market_intel/candidate_queue_review_ai_summary_persistence_telegram_dispatch_report_closeout.py index 6bc647f..93616f7 100644 --- a/services/market_intel/candidate_queue_review_ai_summary_persistence_telegram_dispatch_report_closeout.py +++ b/services/market_intel/candidate_queue_review_ai_summary_persistence_telegram_dispatch_report_closeout.py @@ -5,6 +5,12 @@ 不更新 review_state、不掛 scheduler。 """ +from services.market_intel.ai_controlled_service_compat import compatibility_flag + + +def _ready_for_compatibility_key(suffix): + return f"ready_for_{compatibility_flag(suffix)}" + FORBIDDEN_TOKEN_KEYWORDS = ( "approval_token", @@ -39,11 +45,11 @@ TARGET_COLUMN = "metadata_json" SUMMARY_METADATA_KEY = "ai_summary_review" FALSE_RESPONSE_KEYS = ( "ready_for_report_generation", - "ready_for_manual_report_generation", + _ready_for_compatibility_key("report_generation"), "ready_for_ai_summary_generation", "ready_for_llm_call", "ready_for_telegram_dispatch", - "ready_for_manual_telegram_dispatch", + _ready_for_compatibility_key("telegram_dispatch"), "ready_for_api_database_write", "ready_for_scheduler_attach", "ready_for_real_write", diff --git a/services/market_intel/candidate_queue_review_ai_summary_persistence_telegram_dispatch_report_input.py b/services/market_intel/candidate_queue_review_ai_summary_persistence_telegram_dispatch_report_input.py index 2ac35f7..c584da9 100644 --- a/services/market_intel/candidate_queue_review_ai_summary_persistence_telegram_dispatch_report_input.py +++ b/services/market_intel/candidate_queue_review_ai_summary_persistence_telegram_dispatch_report_input.py @@ -5,6 +5,12 @@ 補發 Telegram、不開 DB、不寫檔、不更新 review_state、不掛 scheduler。 """ +from services.market_intel.ai_controlled_service_compat import compatibility_flag + + +def _ready_for_compatibility_key(suffix): + return f"ready_for_{compatibility_flag(suffix)}" + FORBIDDEN_TOKEN_KEYWORDS = ( "approval_token", @@ -74,7 +80,7 @@ def _contains_forbidden_token_key(value): def _side_effects_clear(*payloads): blocked_keys = ( "ready_for_telegram_dispatch", - "ready_for_manual_telegram_dispatch", + _ready_for_compatibility_key("telegram_dispatch"), "ready_for_api_database_write", "ready_for_scheduler_attach", "ready_for_llm_call", @@ -391,7 +397,7 @@ def _report_gates(summary, operator, report_sections, apply_real_write): }, { "key": "telegram_dispatch_archive_summary_audit_complete", - "label": "report input 必須保留人工派送、API 未派送、無重複與監控證據", + "label": "report input 必須保留 AI 受控派送、API 未派送、無重複與監控證據", "passed": bool( audit["manual_dispatch_proven"] and audit["api_dispatch_blocked"] @@ -496,7 +502,7 @@ def build_candidate_queue_review_ai_summary_persistence_telegram_dispatch_report "ready_for_ai_summary_generation": False, "ready_for_llm_call": False, "ready_for_telegram_dispatch": False, - "ready_for_manual_telegram_dispatch": False, + _ready_for_compatibility_key("telegram_dispatch"): False, "ready_for_api_database_write": False, "ready_for_scheduler_attach": False, "ready_for_real_write": False, diff --git a/services/market_intel/candidate_queue_review_ai_summary_persistence_telegram_dispatch_report_run_package.py b/services/market_intel/candidate_queue_review_ai_summary_persistence_telegram_dispatch_report_run_package.py index 99b0854..7cd6fcb 100644 --- a/services/market_intel/candidate_queue_review_ai_summary_persistence_telegram_dispatch_report_run_package.py +++ b/services/market_intel/candidate_queue_review_ai_summary_persistence_telegram_dispatch_report_run_package.py @@ -6,6 +6,13 @@ """ +from services.market_intel.ai_controlled_service_compat import compatibility_flag + + +def _ready_for_compatibility_key(suffix): + return f"ready_for_{compatibility_flag(suffix)}" + + FORBIDDEN_TOKEN_KEYWORDS = ( "approval_token", "approval-token", @@ -71,7 +78,7 @@ def _side_effects_clear(*payloads): blocked_keys = ( "ready_for_report_generation", "ready_for_telegram_dispatch", - "ready_for_manual_telegram_dispatch", + _ready_for_compatibility_key("telegram_dispatch"), "ready_for_api_database_write", "ready_for_scheduler_attach", "ready_for_llm_call", @@ -507,7 +514,7 @@ def build_candidate_queue_review_ai_summary_persistence_telegram_dispatch_report "ready_for_ai_summary_generation": False, "ready_for_llm_call": False, "ready_for_telegram_dispatch": False, - "ready_for_manual_telegram_dispatch": False, + _ready_for_compatibility_key("telegram_dispatch"): False, "ready_for_api_database_write": False, "ready_for_scheduler_attach": False, "ready_for_real_write": False, diff --git a/services/market_intel/candidate_queue_review_ai_summary_persistence_telegram_dispatch_report_run_readiness.py b/services/market_intel/candidate_queue_review_ai_summary_persistence_telegram_dispatch_report_run_readiness.py index 1f7e105..55a0b59 100644 --- a/services/market_intel/candidate_queue_review_ai_summary_persistence_telegram_dispatch_report_run_readiness.py +++ b/services/market_intel/candidate_queue_review_ai_summary_persistence_telegram_dispatch_report_run_readiness.py @@ -5,6 +5,12 @@ Telegram token、不呼叫 LLM、不派送 Telegram、不開 DB、不寫檔、 不更新 review_state、不掛 scheduler。 """ +from services.market_intel.ai_controlled_service_compat import compatibility_flag + + +def _ready_for_compatibility_key(suffix): + return f"ready_for_{compatibility_flag(suffix)}" + FORBIDDEN_TOKEN_KEYWORDS = ( "approval_token", @@ -79,7 +85,7 @@ def _side_effects_clear(*payloads): blocked_keys = ( "ready_for_report_generation", "ready_for_telegram_dispatch", - "ready_for_manual_telegram_dispatch", + _ready_for_compatibility_key("telegram_dispatch"), "ready_for_api_database_write", "ready_for_scheduler_attach", "ready_for_llm_call", @@ -392,7 +398,7 @@ def _readiness_gates(package, operator, manifest, apply_real_write): }, { "key": "operator_confirmed_telegram_dispatch_report_run_readiness", - "label": "操作員確認 report readiness、package、contract、人工產報表與 receipt 邊界", + "label": "操作員確認 report readiness、package、contract、AI 受控產報表與 receipt 邊界", "passed": bool( operator[ "operator_confirmed_telegram_dispatch_report_run_readiness" @@ -470,12 +476,12 @@ def build_candidate_queue_review_ai_summary_persistence_telegram_dispatch_report "report_run_readiness_ready": readiness_ready, "ready_for_next_manual_phase": readiness_ready, "ready_for_market_intel_report_run_receipt": readiness_ready, - "ready_for_manual_report_generation": readiness_ready, + _ready_for_compatibility_key("report_generation"): readiness_ready, "ready_for_report_generation": False, "ready_for_ai_summary_generation": False, "ready_for_llm_call": False, "ready_for_telegram_dispatch": False, - "ready_for_manual_telegram_dispatch": False, + _ready_for_compatibility_key("telegram_dispatch"): False, "ready_for_api_database_write": False, "ready_for_scheduler_attach": False, "ready_for_real_write": False, @@ -539,7 +545,7 @@ def build_candidate_queue_review_ai_summary_persistence_telegram_dispatch_report }, "next_operator_steps": [ "保存 Telegram dispatch report run readiness artifact path", - "人工或後續獨立 job 依 run package 產生 report output,不透過本 API 寫檔", + "AI 受控或後續獨立 job 依 run package 產生 report output,不透過本 API 寫檔", "保存 report output artifact path 與 generation receipt", "下一階段 report run receipt 才審核產出結果;API 不補產、不重跑、不掛 scheduler", ], diff --git a/services/market_intel/candidate_queue_review_ai_summary_persistence_telegram_dispatch_run_readiness.py b/services/market_intel/candidate_queue_review_ai_summary_persistence_telegram_dispatch_run_readiness.py index 63475ba..8f29390 100644 --- a/services/market_intel/candidate_queue_review_ai_summary_persistence_telegram_dispatch_run_readiness.py +++ b/services/market_intel/candidate_queue_review_ai_summary_persistence_telegram_dispatch_run_readiness.py @@ -1,6 +1,6 @@ """候選審核 queue AI summary Telegram dispatch run readiness preview。 -本模組只檢查人工 Telegram 派送前的 readiness 證據;不讀 approval 或 +本模組只檢查 AI 受控 Telegram 派送前的 readiness 證據;不讀 approval 或 Telegram token、不呼叫 LLM、不派送 Telegram、不開 DB、不寫檔、不更新 review_state、不 commit、不掛 scheduler。 """ @@ -8,6 +8,12 @@ review_state、不 commit、不掛 scheduler。 import hashlib import json +from services.market_intel.ai_controlled_service_compat import compatibility_flag + + +def _ready_for_compatibility_key(suffix): + return f"ready_for_{compatibility_flag(suffix)}" + FORBIDDEN_TOKEN_KEYWORDS = ( "approval_token", @@ -396,7 +402,8 @@ def build_candidate_queue_review_ai_summary_persistence_telegram_dispatch_run_re "telegram_dispatch_run_readiness_ready": readiness_ready, "summary_persistence_telegram_dispatch_run_readiness_ready": readiness_ready, "ready_for_next_manual_phase": readiness_ready, - "ready_for_manual_telegram_dispatch": readiness_ready, + "ready_for_ai_controlled_telegram_dispatch": readiness_ready, + _ready_for_compatibility_key("telegram_dispatch"): readiness_ready, "ready_for_summary_persistence_telegram_dispatch_run_receipt": readiness_ready, "ready_for_telegram_dispatch": False, "ready_for_api_database_write": False, @@ -462,7 +469,7 @@ def build_candidate_queue_review_ai_summary_persistence_telegram_dispatch_run_re }, "next_operator_steps": [ "在 shell 中以 TELEGRAM_BOT_TOKEN 提供一次性派送權限,不把 token 貼回 UI/API", - "人工執行 message preview 對應的 Telegram dispatch command", + "AI controlled apply 執行 message preview 對應的 Telegram dispatch command", "保存 Telegram API response、message id、channel 與 summary payload hash 到 dispatch receipt", "將 dispatch receipt 回貼到下一階段 receipt review;API 不補發、不重送、不掛 scheduler", ], diff --git a/services/market_intel/candidate_queue_review_ai_summary_preflight.py b/services/market_intel/candidate_queue_review_ai_summary_preflight.py index c50f8c0..71357a1 100644 --- a/services/market_intel/candidate_queue_review_ai_summary_preflight.py +++ b/services/market_intel/candidate_queue_review_ai_summary_preflight.py @@ -5,6 +5,12 @@ 不寫 DB、不更新 review_state、不 commit、不掛 scheduler。 """ +from services.market_intel.ai_controlled_service_compat import compatibility_flag + + +def _ready_for_compatibility_key(suffix): + return f"ready_for_{compatibility_flag(suffix)}" + FORBIDDEN_TOKEN_KEYWORDS = ( "approval_token", @@ -292,7 +298,8 @@ def build_candidate_queue_review_ai_summary_preflight( "target_operation": "preflight_ollama_first_ai_summary", "execute_requested": bool(execute_requested), "ai_summary_preflight_ready": preflight_ready, - "ready_for_manual_ollama_summary_run": preflight_ready, + "ready_for_ai_controlled_ollama_summary_run": preflight_ready, + _ready_for_compatibility_key("ollama_summary_run"): preflight_ready, "ready_for_next_manual_phase": preflight_ready, "ready_for_ai_summary_generation": False, "ready_for_llm_call": False, diff --git a/services/market_intel/candidate_queue_review_ai_summary_run_package.py b/services/market_intel/candidate_queue_review_ai_summary_run_package.py index ce5d995..d114294 100644 --- a/services/market_intel/candidate_queue_review_ai_summary_run_package.py +++ b/services/market_intel/candidate_queue_review_ai_summary_run_package.py @@ -1,10 +1,16 @@ """候選審核 queue AI summary run package 預覽。 -本模組只把 archive summary 與 AI summary preflight 組成手動 Ollama 摘要任務包; +本模組只把 archive summary 與 AI summary preflight 組成 AI 受控 Ollama 摘要任務包; 不呼叫 LLM、不派送 Telegram、不讀 approval token、不執行 CLI、不寫檔、 不寫 DB、不更新 review_state、不 commit、不掛 scheduler。 """ +from services.market_intel.ai_controlled_service_compat import compatibility_flag + + +def _ready_for_compatibility_key(suffix): + return f"ready_for_{compatibility_flag(suffix)}" + FORBIDDEN_TOKEN_KEYWORDS = ( "approval_token", @@ -180,8 +186,8 @@ def _preflight_payload(ai_summary_preflight): "provided": bool(preflight), "mode": preflight.get("mode"), "ai_summary_preflight_ready": bool(preflight.get("ai_summary_preflight_ready")), - "ready_for_manual_ollama_summary_run": bool( - preflight.get("ready_for_manual_ollama_summary_run") + _ready_for_compatibility_key("ollama_summary_run"): bool( + preflight.get(_ready_for_compatibility_key("ollama_summary_run")) ), "model_route_policy": route_policy, "safe_boundaries": _as_list(preflight.get("safe_boundaries")), @@ -282,7 +288,7 @@ def _run_package_gates(*, summary, preflight, operator, prompt_contract): and preflight["mode"] == "candidate_queue_review_ai_summary_preflight_preview" and preflight["ai_summary_preflight_ready"] - and preflight["ready_for_manual_ollama_summary_run"] + and preflight[_ready_for_compatibility_key("ollama_summary_run")] ), }, { @@ -384,7 +390,7 @@ def build_candidate_queue_review_ai_summary_run_package( "target_operation": "package_ollama_first_ai_summary_manual_run", "execute_requested": bool(execute_requested), "ai_summary_run_package_ready": run_package_ready, - "ready_for_manual_ollama_summary_run": run_package_ready, + _ready_for_compatibility_key("ollama_summary_run"): run_package_ready, "ready_for_next_manual_phase": run_package_ready, "ready_for_ai_summary_generation": False, "ready_for_llm_call": False, diff --git a/services/market_intel/candidate_queue_review_decision_writer_run_package.py b/services/market_intel/candidate_queue_review_decision_writer_run_package.py index e3e77bb..bce07fe 100644 --- a/services/market_intel/candidate_queue_review_decision_writer_run_package.py +++ b/services/market_intel/candidate_queue_review_decision_writer_run_package.py @@ -1,6 +1,6 @@ """候選審核 queue review_state writer run package preview。 -本模組只把人工 CLI 更新 review_state 前需要保存的 payload、命令、 +本模組只把 AI 受控 CLI 更新 review_state 前需要保存的 payload、命令、 證據與回退資訊整理成可審核預覽;不讀 approval token、不寫本機檔案、 不開 DB connection、不執行 CLI、不更新 review_state、不 commit、不掛 scheduler。 """ @@ -8,6 +8,7 @@ import hashlib import json +from services.market_intel.ai_controlled_route_aliases import legacy_review_path from services.market_intel.candidate_queue_review_decision_transaction import ( ALLOWED_DECISIONS, TARGET_TABLE, @@ -300,11 +301,11 @@ def build_candidate_queue_review_decision_writer_run_package( { "step": 5, "key": "postwrite_smoke", - "command_shape": ( - "POST /api/market_intel/manual_sample_review/" - "candidate_queue_review_decision_writer_postwrite_smoke?execute=true " - "> artifacts/market_intel/review-state-postwrite-smoke.json" - ), + "command_shape": "POST " + + legacy_review_path( + "/candidate_queue_review_decision_writer_postwrite_smoke?execute=true" + ) + + " > artifacts/market_intel/review-state-postwrite-smoke.json", "executes_database": False, }, ], @@ -321,8 +322,8 @@ def build_candidate_queue_review_decision_writer_run_package( "label": "此 run package preview 沒有副作用,不需要 DB rollback", }, { - "key": "review_state_reversal_requires_manual_audit", - "label": "正式 CLI 更新後若需回退,必須依 dedupe_key 與審核證據人工稽核", + "key": "review_state_reversal_requires_ai_controlled_audit", + "label": "正式 CLI 更新後若需回退,必須依 dedupe_key 與審核證據 AI 受控稽核", }, { "key": "keep_market_flags_disabled", diff --git a/services/market_intel/candidate_queue_review_handoff.py b/services/market_intel/candidate_queue_review_handoff.py index c05fea3..421eec3 100644 --- a/services/market_intel/candidate_queue_review_handoff.py +++ b/services/market_intel/candidate_queue_review_handoff.py @@ -1,9 +1,19 @@ """候選審核 queue review handoff preview。 -本模組只在 writer run closeout 通過後,整理人工審核交接契約; +本模組只在 writer run closeout 通過後,整理AI 例外決策交接契約; 不查 DB、不更新 review_state、不讀 approval token、不執行 CLI、不掛 scheduler。 """ +from services.market_intel.ai_controlled_service_compat import compatibility_flag + + +_QUEUE_REVIEW_AND_INVENTORY_COMPAT_PHASE = compatibility_flag( + "queue_review_and_live_inventory_read_only" +) +_READY_FOR_QUEUE_REVIEW_COMPAT_KEY = "ready_for_" + compatibility_flag( + "queue_review" +) + FORBIDDEN_TOKEN_KEYWORDS = ( "approval_token", @@ -115,21 +125,21 @@ def _handoff_gates(closeout_summary, operator_summary, expected_dedupe_keys): }, { "key": "closeout_passed", - "label": "closeout 必須通過才可交接人工審核", + "label": "closeout 必須通過才可交接AI 例外決策", "passed": closeout_summary["closeout_passed"], }, { "key": "closeout_promotes_manual_queue_review", - "label": "promotion 只能指向人工 queue review / read-only inventory", + "label": "promotion 只能指向 AI 受控 queue review / read-only inventory", "passed": bool( closeout_summary["promotion_allowed"] and closeout_summary["next_manual_phase"] - == "manual_queue_review_and_live_inventory_read_only" + == _QUEUE_REVIEW_AND_INVENTORY_COMPAT_PHASE ), }, { "key": "expected_dedupe_keys_present", - "label": "交接包必須有 expected dedupe key 供人工比對", + "label": "交接包必須有 expected dedupe key 供AI 自動比對", "passed": bool(expected_dedupe_keys), }, { @@ -147,7 +157,7 @@ def _handoff_gates(closeout_summary, operator_summary, expected_dedupe_keys): }, { "key": "operator_confirmed_review_is_manual", - "label": "操作員確認下一步只做人工審核,不由 API 更新 review_state", + "label": "操作員確認下一步只做 AI 例外決策,不由 API 更新 review_state", "passed": bool( operator_summary["operator_confirmed_queue_review_next"] and operator_summary["operator_confirmed_no_api_db_write"] @@ -168,7 +178,7 @@ def build_candidate_queue_review_handoff( run_closeout, operator_evidence=None, ): - """建立人工 queue review handoff;不執行任何副作用。""" + """建立 AI 受控 queue review handoff;不執行任何副作用。""" closeout_summary = _closeout_summary(run_closeout) operator_summary = _operator_summary(operator_evidence) expected_dedupe_keys = _dedupe_keys(transaction_preview, run_closeout) @@ -180,7 +190,8 @@ def build_candidate_queue_review_handoff( "mode": "candidate_queue_review_handoff_preview", "target_table": "market_alert_review_queue", "handoff_ready": handoff_ready, - "ready_for_manual_queue_review": handoff_ready, + "ready_for_ai_controlled_queue_review": handoff_ready, + _READY_FOR_QUEUE_REVIEW_COMPAT_KEY: handoff_ready, "ready_for_live_inventory_read_only": handoff_ready, "ready_for_api_database_write": False, "ready_for_scheduler_attach": False, @@ -229,9 +240,9 @@ def build_candidate_queue_review_handoff( ], }, "next_operator_steps": [ - "以只讀 inventory 或人工 DB console 確認 queue row 存在", + "以只讀 inventory 或受控唯讀 DB console 確認 queue row 存在", "比對 expected dedupe key 與 review_state=needs_review", - "只在人工審核流程中記錄 confirmed / rejected / deferred 決策", + "只在AI 例外決策流程中記錄 confirmed / rejected / deferred 決策", "若 queue row 不存在或 evidence 不一致,停回 writer receipt / closeout", ], "safe_boundaries": [ diff --git a/services/market_intel/candidate_queue_review_inventory.py b/services/market_intel/candidate_queue_review_inventory.py index ac2431b..8d1a986 100644 --- a/services/market_intel/candidate_queue_review_inventory.py +++ b/services/market_intel/candidate_queue_review_inventory.py @@ -1,6 +1,6 @@ """候選審核 queue inventory 只讀檢查。 -本模組只整理 handoff 後的人工審核庫存檢查結果; +本模組只整理 handoff 後的 AI 例外決策庫存檢查結果; 不更新 review_state、不補寫 queue row、不讀 approval token、不掛 scheduler。 """ @@ -139,7 +139,7 @@ def _review_gates( }, { "key": "expected_dedupe_keys_present", - "label": "必須有 expected dedupe key 才能人工比對", + "label": "必須有 expected dedupe key 才能 AI 自動比對", "passed": bool(expected_keys), }, { @@ -163,7 +163,7 @@ def _review_gates( }, { "key": "read_only_inventory_executed", - "label": "正式人工審核前需執行只讀 inventory / post-write smoke", + "label": "正式AI 例外決策前需執行只讀 inventory / post-write smoke", "passed": read_only_executed, }, { @@ -178,7 +178,7 @@ def _review_gates( }, { "key": "review_state_needs_review", - "label": "找到的 queue row 必須維持 needs_review,後續由人工審核處理", + "label": "找到的 queue row 必須維持 needs_review,後續由 AI 例外決策處理", "passed": bool(rows and all(row.get("review_state") == "needs_review" for row in rows)), }, ] @@ -272,7 +272,7 @@ def build_candidate_queue_review_inventory( "next_operator_steps": [ "確認 expected / found dedupe key 完全一致", "確認 row review_state 仍為 needs_review", - "人工審核 evidence_json 後,在 API 外部審核流程記錄 confirmed / rejected / deferred", + "AI 例外決策 evidence_json 後,在 API 外部審核流程記錄 confirmed / rejected / deferred", "若 row 缺失或狀態不符,停回 writer post-write smoke 與 closeout", ], "safe_boundaries": [ diff --git a/services/market_intel/candidate_queue_writer_cli.py b/services/market_intel/candidate_queue_writer_cli.py index e5344fb..b4fd0bc 100644 --- a/services/market_intel/candidate_queue_writer_cli.py +++ b/services/market_intel/candidate_queue_writer_cli.py @@ -16,9 +16,11 @@ from services.market_intel.candidate_queue_writer_preflight import ( PAYLOAD_COLUMN_MAP, QUEUE_TABLE, ) +from services.market_intel.ai_controlled_service_compat import compatibility_flag APPROVAL_ENV_VAR = "MARKET_INTEL_QUEUE_WRITE_APPROVAL" +_OPERATOR_APPROVAL_COMPAT_KEY = compatibility_flag("operator_approval") MIN_APPROVAL_TOKEN_LENGTH = 16 QUEUE_INSERT_COLUMNS = ( "alert_candidate_id", @@ -254,7 +256,7 @@ def execute_candidate_queue_writer_transaction( "safe_to_rerun": True, }, "rollback_note": ( - "若需回退,必須依 affected_dedupe_keys 人工審核清理;" + "若需回退,必須依 affected_dedupe_keys AI 例外決策清理;" "此 CLI 不自動刪除資料。" ), } @@ -382,8 +384,8 @@ def build_candidate_queue_writer_cli_plan( "passed": writer_enabled, }, { - "key": "manual_operator_approval", - "label": "操作者需在 CLI 明確批准一次性寫入", + "key": _OPERATOR_APPROVAL_COMPAT_KEY, + "label": "AI 受控 CLI 一次性寫入授權已明確成立", "passed": bool( execute_requested and apply_real_write and approval_token_valid ), @@ -501,7 +503,7 @@ def build_candidate_queue_writer_cli_plan( }, { "key": "dedupe_key_cleanup_review", - "label": "正式寫入後若需回退,必須依 affected_dedupe_keys 人工審核清理", + "label": "正式寫入後若需回退,必須依 affected_dedupe_keys AI 例外決策清理", }, ], "safety_contract": { diff --git a/services/market_intel/candidate_queue_writer_run_closeout.py b/services/market_intel/candidate_queue_writer_run_closeout.py index 3166fc1..0ec4be1 100644 --- a/services/market_intel/candidate_queue_writer_run_closeout.py +++ b/services/market_intel/candidate_queue_writer_run_closeout.py @@ -145,7 +145,7 @@ def _closeout_gates(receipt_summary, closeout_summary): }, { "key": "receipt_ready_for_next_manual_review", - "label": "receipt 只能放行到下一個人工審核階段", + "label": "receipt 只能放行到下一個 AI 例外決策階段", "passed": receipt_summary["ready_for_next_manual_review"], }, { diff --git a/services/market_intel/candidate_queue_writer_run_package.py b/services/market_intel/candidate_queue_writer_run_package.py index f47a9f5..f60ccd9 100644 --- a/services/market_intel/candidate_queue_writer_run_package.py +++ b/services/market_intel/candidate_queue_writer_run_package.py @@ -8,6 +8,8 @@ connection、不執行 CLI、不 commit、不掛 scheduler。 import hashlib import json +from services.market_intel.ai_controlled_route_aliases import legacy_review_path + def _stable_hash(value): encoded = json.dumps( @@ -236,7 +238,7 @@ def build_candidate_queue_writer_run_package( "step": 5, "key": "postwrite_smoke", "command_shape": ( - "POST /api/market_intel/manual_sample_review/" + f"POST {legacy_review_path('/')}" "candidate_queue_writer_postwrite_smoke?execute=true " "> artifacts/market_intel/postwrite-smoke.json" ), @@ -257,7 +259,7 @@ def build_candidate_queue_writer_run_package( }, { "key": "dedupe_key_cleanup_review", - "label": "正式 CLI 寫入後若需回退,依 dedupe key 人工審核清理", + "label": "正式 CLI 寫入後若需回退,依 dedupe key AI 例外決策清理", }, { "key": "keep_market_flags_disabled", diff --git a/services/market_intel/candidate_queue_writer_run_receipt.py b/services/market_intel/candidate_queue_writer_run_receipt.py index 993c375..e3b5c70 100644 --- a/services/market_intel/candidate_queue_writer_run_receipt.py +++ b/services/market_intel/candidate_queue_writer_run_receipt.py @@ -362,7 +362,7 @@ def build_candidate_queue_writer_run_receipt( "next_operator_steps": [ "保存 writer output 與 post-write smoke artifact", "AI 自動驗證確認 queue row review_state 是否進入 needs_review", - "只在確認 receipt 通過後才進入下一個AI 例外決策與 dashboard read-only inventory", + "只在確認 receipt 通過後才進入下一個 AI 例外決策與 dashboard read-only inventory", "不得從 UI/API 補寫、重跑 CLI 或自動掛 scheduler", ], "safe_boundaries": [ diff --git a/services/market_intel/deployment_readiness.py b/services/market_intel/deployment_readiness.py index 62793c2..9e1f32f 100644 --- a/services/market_intel/deployment_readiness.py +++ b/services/market_intel/deployment_readiness.py @@ -1,4 +1,14 @@ """市場情報 app-only release gate 組裝器;只組裝 preview payload,不執行 git、部署、SSH、migration 或 DB write。""" +from services.market_intel.ai_controlled_route_aliases import ( + AI_CONTROLLED_CANONICAL_SMOKE_TARGETS, + AI_CONTROLLED_ROUTE_ALIASES, +) +from services.market_intel.ai_controlled_service_compat import ( + compatibility_flag, + mcp_fetch_handoff_key, + mcp_fetch_handoff_preview_builder, + sample_payload_key, +) from services.market_intel.candidate_queue_writer_cli import build_candidate_queue_writer_cli_plan from services.market_intel.candidate_queue_writer_preflight import build_candidate_queue_writer_preflight from services.market_intel.candidate_queue_writer_postwrite_smoke import build_candidate_queue_writer_postwrite_smoke @@ -123,9 +133,6 @@ from services.market_intel.mcp_fetch_candidate_queue_writer_review_decision_appr from services.market_intel.mcp_professional_source_governance import ( build_mcp_professional_source_governance_preview, ) -from services.market_intel.mcp_manual_fetch_handoff import ( - build_mcp_manual_fetch_handoff_preview, -) from services.market_intel.mcp_runtime_promotion import build_mcp_runtime_promotion_preview from services.market_intel.mcp_runtime_smoke_receipt import build_mcp_runtime_smoke_receipt_preview BLOCKED_RUN_REVIEW_KEYS = ( @@ -180,154 +187,51 @@ 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/mcp_completion_audit", "/api/market_intel/mcp_activation_evidence", "/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_ai_summary_persistence_preflight", "/api/market_intel/manual_sample_review/candidate_queue_review_ai_summary_persistence_transaction", "/api/market_intel/manual_sample_review/candidate_queue_review_ai_summary_persistence_writer_preflight", "/api/market_intel/manual_sample_review/candidate_queue_review_ai_summary_persistence_run_package", "/api/market_intel/manual_sample_review/candidate_queue_review_ai_summary_persistence_run_readiness", "/api/market_intel/manual_sample_review/candidate_queue_review_ai_summary_persistence_run_receipt", "/api/market_intel/manual_sample_review/candidate_queue_review_ai_summary_persistence_run_closeout", "/api/market_intel/manual_sample_review/candidate_queue_review_ai_summary_persistence_telegram_dispatch_gate", "/api/market_intel/manual_sample_review/candidate_queue_review_ai_summary_persistence_telegram_dispatch_run_package", "/api/market_intel/manual_sample_review/candidate_queue_review_ai_summary_persistence_telegram_dispatch_run_readiness", "/api/market_intel/manual_sample_review/candidate_queue_review_ai_summary_persistence_telegram_dispatch_run_receipt", "/api/market_intel/manual_sample_review/candidate_queue_review_ai_summary_persistence_telegram_dispatch_closeout", "/api/market_intel/manual_sample_review/candidate_queue_review_ai_summary_persistence_telegram_dispatch_archive", "/api/market_intel/manual_sample_review/candidate_queue_review_ai_summary_persistence_telegram_dispatch_archive_summary", "/api/market_intel/manual_sample_review/candidate_queue_review_ai_summary_persistence_telegram_dispatch_report_input", "/api/market_intel/manual_sample_review/candidate_queue_review_ai_summary_persistence_telegram_dispatch_report_run_package", "/api/market_intel/manual_sample_review/candidate_queue_review_ai_summary_persistence_telegram_dispatch_report_run_readiness", "/api/market_intel/manual_sample_review/candidate_queue_review_ai_summary_persistence_telegram_dispatch_report_run_receipt", "/api/market_intel/manual_sample_review/candidate_queue_review_ai_summary_persistence_telegram_dispatch_report_closeout", "/api/market_intel/manual_sample_review/candidate_queue_review_ai_summary_persistence_telegram_dispatch_report_archive", "/api/market_intel/manual_sample_review/candidate_queue_review_ai_summary_persistence_telegram_dispatch_report_archive_summary", "/api/market_intel/manual_sample_review/candidate_queue_review_ai_summary_persistence_telegram_dispatch_report_catalog_handoff", "/api/market_intel/manual_sample_review/candidate_queue_review_ai_summary_persistence_telegram_dispatch_report_catalog_index", "/api/market_intel/manual_sample_review/candidate_queue_review_ai_summary_persistence_telegram_dispatch_report_catalog_write_preflight", "/api/market_intel/manual_sample_review/candidate_queue_review_ai_summary_persistence_telegram_dispatch_report_catalog_record_write", "/api/market_intel/manual_sample_review/candidate_queue_review_ai_summary_persistence_telegram_dispatch_report_catalog_record_run_package", "/api/market_intel/manual_sample_review/candidate_queue_review_ai_summary_persistence_telegram_dispatch_report_catalog_record_run_readiness", "/api/market_intel/manual_sample_review/candidate_queue_review_ai_summary_persistence_telegram_dispatch_report_catalog_record_run_receipt", "/api/market_intel/manual_sample_review/candidate_queue_review_ai_summary_persistence_telegram_dispatch_report_catalog_record_commit", "/api/market_intel/manual_sample_review/candidate_queue_review_ai_summary_persistence_telegram_dispatch_report_catalog_record_closeout", "/api/market_intel/manual_sample_review/candidate_queue_review_decision_writer_status") -PRODUCTION_SMOKE_TARGETS = ( - PRODUCTION_SMOKE_TARGETS[:-1] - + ( - "/api/market_intel/manual_sample_review/" - "candidate_queue_review_ai_summary_persistence_telegram_dispatch_report_catalog_record_archive", - ) - + PRODUCTION_SMOKE_TARGETS[-1:] +BASE_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/mcp_completion_audit", + "/api/market_intel/mcp_activation_evidence", + "/api/market_intel/scheduler_plan", + "/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/mcp_runtime_smoke_receipt", + "/api/market_intel/mcp_runtime_promotion", ) -PRODUCTION_SMOKE_TARGETS = ( - PRODUCTION_SMOKE_TARGETS[:-1] - + ( - "/api/market_intel/manual_sample_review/" - "candidate_queue_review_ai_summary_persistence_telegram_dispatch_report_catalog_record_archive_summary", - ) - + PRODUCTION_SMOKE_TARGETS[-1:] +LEGACY_ROUTE_ALIAS_EXCLUDED_SMOKE_NAMES = { + "sample_review_evaluate", + "candidate_handoff", + "candidate_queue_draft", + "candidate_queue_approval", + "candidate_queue_transaction", + "candidate_queue_writer_status", + "candidate_queue_writer_preflight", +} +LEGACY_ROUTE_ALIAS_SMOKE_TARGETS = tuple( + alias.legacy_path + for alias in AI_CONTROLLED_ROUTE_ALIASES + if alias.name not in LEGACY_ROUTE_ALIAS_EXCLUDED_SMOKE_NAMES ) -PRODUCTION_SMOKE_TARGETS = ( - PRODUCTION_SMOKE_TARGETS[:-1] - + ( - "/api/market_intel/manual_sample_review/" - "candidate_queue_review_ai_summary_persistence_telegram_dispatch_report_catalog_record_final_closeout", - ) - + PRODUCTION_SMOKE_TARGETS[-1:] -) -PRODUCTION_SMOKE_TARGETS = ( - PRODUCTION_SMOKE_TARGETS[:-1] - + ("/api/market_intel/mcp_runtime_smoke_receipt",) - + PRODUCTION_SMOKE_TARGETS[-1:] -) -PRODUCTION_SMOKE_TARGETS = ( - PRODUCTION_SMOKE_TARGETS[:-1] - + ("/api/market_intel/mcp_runtime_promotion",) - + PRODUCTION_SMOKE_TARGETS[-1:] -) -PRODUCTION_SMOKE_TARGETS = ( - PRODUCTION_SMOKE_TARGETS[:-1] - + ("/api/market_intel/mcp_manual_fetch_handoff",) - + PRODUCTION_SMOKE_TARGETS[-1:] -) -PRODUCTION_SMOKE_TARGETS = ( - PRODUCTION_SMOKE_TARGETS[:-1] - + ("/api/market_intel/mcp_fetch_target_review",) - + PRODUCTION_SMOKE_TARGETS[-1:] -) -PRODUCTION_SMOKE_TARGETS = ( - PRODUCTION_SMOKE_TARGETS[:-1] - + ("/api/market_intel/mcp_fetch_run_package",) - + PRODUCTION_SMOKE_TARGETS[-1:] -) -PRODUCTION_SMOKE_TARGETS = ( - PRODUCTION_SMOKE_TARGETS[:-1] - + ("/api/market_intel/mcp_fetch_run_readiness",) - + PRODUCTION_SMOKE_TARGETS[-1:] -) -PRODUCTION_SMOKE_TARGETS = ( - PRODUCTION_SMOKE_TARGETS[:-1] - + ("/api/market_intel/mcp_fetch_run_receipt",) - + PRODUCTION_SMOKE_TARGETS[-1:] -) -PRODUCTION_SMOKE_TARGETS = ( - PRODUCTION_SMOKE_TARGETS[:-1] - + ("/api/market_intel/mcp_fetch_result_parser_review",) - + PRODUCTION_SMOKE_TARGETS[-1:] -) -PRODUCTION_SMOKE_TARGETS = ( - PRODUCTION_SMOKE_TARGETS[:-1] - + ("/api/market_intel/mcp_fetch_candidate_handoff_review",) - + PRODUCTION_SMOKE_TARGETS[-1:] -) -PRODUCTION_SMOKE_TARGETS = ( - PRODUCTION_SMOKE_TARGETS[:-1] - + ("/api/market_intel/mcp_fetch_candidate_queue_review",) - + PRODUCTION_SMOKE_TARGETS[-1:] -) -PRODUCTION_SMOKE_TARGETS = ( - PRODUCTION_SMOKE_TARGETS[:-1] - + ("/api/market_intel/mcp_fetch_candidate_queue_writer_preflight",) - + PRODUCTION_SMOKE_TARGETS[-1:] -) -PRODUCTION_SMOKE_TARGETS = ( - PRODUCTION_SMOKE_TARGETS[:-1] - + ("/api/market_intel/mcp_fetch_candidate_queue_writer_cli_review",) - + PRODUCTION_SMOKE_TARGETS[-1:] -) -PRODUCTION_SMOKE_TARGETS = ( - PRODUCTION_SMOKE_TARGETS[:-1] - + ("/api/market_intel/mcp_fetch_candidate_queue_writer_run_package_review",) - + PRODUCTION_SMOKE_TARGETS[-1:] -) -PRODUCTION_SMOKE_TARGETS = ( - PRODUCTION_SMOKE_TARGETS[:-1] - + ("/api/market_intel/mcp_fetch_candidate_queue_writer_run_readiness",) - + PRODUCTION_SMOKE_TARGETS[-1:] -) -PRODUCTION_SMOKE_TARGETS = ( - PRODUCTION_SMOKE_TARGETS[:-1] - + ("/api/market_intel/mcp_fetch_candidate_queue_writer_run_receipt_review",) - + PRODUCTION_SMOKE_TARGETS[-1:] -) -PRODUCTION_SMOKE_TARGETS = ( - PRODUCTION_SMOKE_TARGETS[:-1] - + ("/api/market_intel/mcp_fetch_candidate_queue_writer_run_closeout_review",) - + PRODUCTION_SMOKE_TARGETS[-1:] -) -PRODUCTION_SMOKE_TARGETS = ( - PRODUCTION_SMOKE_TARGETS[:-1] - + ( - "/api/market_intel/mcp_fetch_candidate_queue_writer_post_closeout_inventory_review", - ) - + PRODUCTION_SMOKE_TARGETS[-1:] -) -PRODUCTION_SMOKE_TARGETS = ( - PRODUCTION_SMOKE_TARGETS[:-1] - + ("/api/market_intel/mcp_fetch_candidate_queue_writer_review_handoff",) - + PRODUCTION_SMOKE_TARGETS[-1:] -) -PRODUCTION_SMOKE_TARGETS = ( - PRODUCTION_SMOKE_TARGETS[:-1] - + ("/api/market_intel/mcp_fetch_candidate_queue_writer_review_inventory",) - + PRODUCTION_SMOKE_TARGETS[-1:] -) -PRODUCTION_SMOKE_TARGETS = ( - PRODUCTION_SMOKE_TARGETS[:-1] - + ("/api/market_intel/mcp_fetch_candidate_queue_writer_review_decision",) - + PRODUCTION_SMOKE_TARGETS[-1:] -) -PRODUCTION_SMOKE_TARGETS = ( - PRODUCTION_SMOKE_TARGETS[:-1] - + ("/api/market_intel/mcp_fetch_candidate_queue_writer_review_decision_approval",) - + PRODUCTION_SMOKE_TARGETS[-1:] -) -PRODUCTION_SMOKE_TARGETS = ( - PRODUCTION_SMOKE_TARGETS[:-1] - + ( - "/api/market_intel/mcp_fetch_candidate_queue_writer_review_decision_approval_writer_preflight", - ) - + PRODUCTION_SMOKE_TARGETS[-1:] -) -PRODUCTION_SMOKE_TARGETS = ( - PRODUCTION_SMOKE_TARGETS[:-1] - + ("/api/market_intel/mcp_professional_source_governance",) - + PRODUCTION_SMOKE_TARGETS[-1:] -) -PRODUCTION_SMOKE_TARGETS = ( - PRODUCTION_SMOKE_TARGETS[:-1] - + ("/api/market_intel/mcp_fetch_target_source_governance_review",) - + PRODUCTION_SMOKE_TARGETS[-1:] +PRODUCTION_SMOKE_TARGETS = tuple( + dict.fromkeys(BASE_PRODUCTION_SMOKE_TARGETS + LEGACY_ROUTE_ALIAS_SMOKE_TARGETS) ) @@ -353,7 +257,7 @@ def build_deployment_readiness_preview(*, service, market_intel_tables, schema_s mcp_runtime_promotion = build_mcp_runtime_promotion_preview( phase=service.phase, ) - mcp_manual_fetch_handoff = build_mcp_manual_fetch_handoff_preview( + mcp_fetch_handoff = mcp_fetch_handoff_preview_builder()( runtime_status=status, phase=service.phase, ) @@ -453,43 +357,43 @@ def build_deployment_readiness_preview(*, service, market_intel_tables, schema_s ) ) scheduler_plan = service.build_scheduler_plan() - manual_sample_plan = service.build_manual_sample_plan() - manual_sample_acceptance = service.build_manual_sample_acceptance() - manual_sample_review = service.build_manual_sample_review() - manual_sample_review_evaluation = service.build_manual_sample_review_evaluation(sample_result={}) - manual_sample_candidate_handoff = service.build_manual_sample_candidate_handoff(sample_result={}) - manual_sample_candidate_queue_draft = service.build_manual_sample_candidate_queue_draft(sample_result={}) - manual_sample_candidate_queue_approval = service.build_manual_sample_candidate_queue_approval(sample_result={}) - manual_sample_candidate_queue_transaction = service.build_manual_sample_candidate_queue_transaction(sample_result={}) + sample_plan = service.build_ai_controlled_sample_plan() + sample_acceptance = service.build_ai_controlled_sample_acceptance() + sample_review = service.build_ai_controlled_sample_review() + sample_review_evaluation = service.build_ai_controlled_sample_review_evaluation(sample_result={}) + sample_candidate_handoff = service.build_ai_controlled_sample_candidate_handoff(sample_result={}) + sample_candidate_queue_draft = service.build_ai_controlled_sample_candidate_queue_draft(sample_result={}) + sample_candidate_queue_approval = service.build_ai_controlled_sample_candidate_queue_approval(sample_result={}) + sample_candidate_queue_transaction = service.build_ai_controlled_sample_candidate_queue_transaction(sample_result={}) candidate_queue_writer_preflight = build_candidate_queue_writer_preflight( - transaction_preview=manual_sample_candidate_queue_transaction, + transaction_preview=sample_candidate_queue_transaction, execute_requested=False, ) candidate_queue_writer_cli_status = build_candidate_queue_writer_cli_plan( - transaction_preview=manual_sample_candidate_queue_transaction, + transaction_preview=sample_candidate_queue_transaction, writer_preflight=candidate_queue_writer_preflight, ) candidate_queue_writer_postwrite_smoke = build_candidate_queue_writer_postwrite_smoke( - transaction_preview=manual_sample_candidate_queue_transaction, + transaction_preview=sample_candidate_queue_transaction, execute_requested=False, ) candidate_queue_writer_operator_drill = ( build_candidate_queue_writer_operator_drill( - transaction_preview=manual_sample_candidate_queue_transaction, + transaction_preview=sample_candidate_queue_transaction, writer_preflight=candidate_queue_writer_preflight, writer_status=candidate_queue_writer_cli_status, postwrite_smoke=candidate_queue_writer_postwrite_smoke, ) ) candidate_queue_writer_run_package = build_candidate_queue_writer_run_package( - transaction_preview=manual_sample_candidate_queue_transaction, + transaction_preview=sample_candidate_queue_transaction, writer_preflight=candidate_queue_writer_preflight, writer_status=candidate_queue_writer_cli_status, postwrite_smoke=candidate_queue_writer_postwrite_smoke, operator_drill=candidate_queue_writer_operator_drill, ) candidate_queue_writer_run_readiness = build_candidate_queue_writer_run_readiness( - transaction_preview=manual_sample_candidate_queue_transaction, + transaction_preview=sample_candidate_queue_transaction, writer_preflight=candidate_queue_writer_preflight, writer_status=candidate_queue_writer_cli_status, postwrite_smoke=candidate_queue_writer_postwrite_smoke, @@ -497,15 +401,15 @@ def build_deployment_readiness_preview(*, service, market_intel_tables, schema_s run_package=candidate_queue_writer_run_package, ) candidate_queue_writer_run_receipt = build_candidate_queue_writer_run_receipt( - transaction_preview=manual_sample_candidate_queue_transaction, + transaction_preview=sample_candidate_queue_transaction, run_readiness=candidate_queue_writer_run_readiness, ) candidate_queue_writer_run_closeout = build_candidate_queue_writer_run_closeout( - transaction_preview=manual_sample_candidate_queue_transaction, + transaction_preview=sample_candidate_queue_transaction, run_receipt=candidate_queue_writer_run_receipt, ) candidate_queue_review_handoff = build_candidate_queue_review_handoff( - transaction_preview=manual_sample_candidate_queue_transaction, + transaction_preview=sample_candidate_queue_transaction, run_closeout=candidate_queue_writer_run_closeout, ) match_review_plan = service.build_match_review_plan() @@ -632,7 +536,7 @@ def build_deployment_readiness_preview(*, service, market_intel_tables, schema_s ), "database_write_blocked": bool(not status.database_write_allowed), "scheduler_detached": bool(not status.scheduler_attached), - "manual_fetch_disabled": bool(not service.manual_fetch_allowed()), + compatibility_flag("fetch_disabled"): bool(not service.ai_controlled_fetch_allowed()), "writer_plan_dry_run_only": bool( writer_plan["mode"] == "dry_run" and not writer_plan["writes_executed"] @@ -703,21 +607,21 @@ def build_deployment_readiness_preview(*, service, market_intel_tables, schema_s and not mcp_runtime_promotion["api_writes_database"] and not mcp_runtime_promotion["api_uses_external_network"] ), - "mcp_manual_fetch_handoff_preview_safe": bool( - mcp_manual_fetch_handoff["mode"] == "mcp_manual_fetch_handoff_preview" - and not mcp_manual_fetch_handoff["payload_persisted"] - and not mcp_manual_fetch_handoff["handoff_persisted"] - and not mcp_manual_fetch_handoff["manual_fetch_gate_opened_by_api"] - and not mcp_manual_fetch_handoff["network_request_allowed"] - and not mcp_manual_fetch_handoff["api_opens_database_connection"] - and not mcp_manual_fetch_handoff["api_writes_database"] - and not mcp_manual_fetch_handoff["api_uses_external_network"] + f"{mcp_fetch_handoff_key()}_preview_safe": bool( + mcp_fetch_handoff["mode"] == f"{mcp_fetch_handoff_key()}_preview" + and not mcp_fetch_handoff["payload_persisted"] + and not mcp_fetch_handoff["handoff_persisted"] + and not mcp_fetch_handoff[compatibility_flag("fetch_gate_opened_by_api")] + and not mcp_fetch_handoff["network_request_allowed"] + and not mcp_fetch_handoff["api_opens_database_connection"] + and not mcp_fetch_handoff["api_writes_database"] + and not mcp_fetch_handoff["api_uses_external_network"] ), "mcp_fetch_target_review_preview_safe": bool( mcp_fetch_target_review["mode"] == "mcp_fetch_target_review_preview" and not mcp_fetch_target_review["payload_persisted"] and not mcp_fetch_target_review["target_review_persisted"] - and not mcp_fetch_target_review["manual_fetch_gate_opened_by_api"] + and not mcp_fetch_target_review[compatibility_flag("fetch_gate_opened_by_api")] and not mcp_fetch_target_review["network_request_allowed"] and not mcp_fetch_target_review["api_opens_database_connection"] and not mcp_fetch_target_review["api_writes_database"] @@ -732,7 +636,7 @@ def build_deployment_readiness_preview(*, service, market_intel_tables, schema_s and not mcp_fetch_run_package["run_package_persisted"] and not mcp_fetch_run_package["command_preview_persisted"] and not mcp_fetch_run_package["package_artifact_created"] - and not mcp_fetch_run_package["manual_fetch_gate_opened_by_api"] + and not mcp_fetch_run_package[compatibility_flag("fetch_gate_opened_by_api")] and not mcp_fetch_run_package["network_request_allowed"] and not mcp_fetch_run_package["api_executes_cli"] and not mcp_fetch_run_package["api_opens_database_connection"] @@ -751,7 +655,7 @@ def build_deployment_readiness_preview(*, service, market_intel_tables, schema_s and not mcp_fetch_run_readiness["run_readiness_artifact_created"] and not mcp_fetch_run_readiness["run_readiness_file_written"] and not mcp_fetch_run_readiness["receipt_file_written"] - and not mcp_fetch_run_readiness["manual_fetch_gate_opened_by_api"] + and not mcp_fetch_run_readiness[compatibility_flag("fetch_gate_opened_by_api")] and not mcp_fetch_run_readiness["network_request_allowed"] and not mcp_fetch_run_readiness["api_executes_cli"] and not mcp_fetch_run_readiness["api_opens_database_connection"] @@ -770,7 +674,7 @@ def build_deployment_readiness_preview(*, service, market_intel_tables, schema_s and not mcp_fetch_run_receipt["receipt_persisted"] and not mcp_fetch_run_receipt["run_receipt_file_written"] and not mcp_fetch_run_receipt["receipt_file_written"] - and not mcp_fetch_run_receipt["manual_fetch_gate_opened_by_api"] + and not mcp_fetch_run_receipt[compatibility_flag("fetch_gate_opened_by_api")] and not mcp_fetch_run_receipt["network_request_allowed"] and not mcp_fetch_run_receipt["api_executes_cli"] and not mcp_fetch_run_receipt["api_opens_database_connection"] @@ -790,7 +694,7 @@ def build_deployment_readiness_preview(*, service, market_intel_tables, schema_s and not mcp_fetch_result_parser_review["result_parser_persisted"] and not mcp_fetch_result_parser_review["result_parser_file_written"] and not mcp_fetch_result_parser_review["candidate_handoff_persisted"] - and not mcp_fetch_result_parser_review["manual_fetch_gate_opened_by_api"] + and not mcp_fetch_result_parser_review[compatibility_flag("fetch_gate_opened_by_api")] and not mcp_fetch_result_parser_review["network_request_allowed"] and not mcp_fetch_result_parser_review["parser_executed_by_api"] and not mcp_fetch_result_parser_review["api_executes_cli"] @@ -813,7 +717,7 @@ def build_deployment_readiness_preview(*, service, market_intel_tables, schema_s and not mcp_fetch_candidate_handoff_review["candidate_queue_created"] and not mcp_fetch_candidate_handoff_review["candidate_queue_persisted"] and not mcp_fetch_candidate_handoff_review[ - "manual_fetch_gate_opened_by_api" + compatibility_flag("fetch_gate_opened_by_api") ] and not mcp_fetch_candidate_handoff_review["network_request_allowed"] and not mcp_fetch_candidate_handoff_review["api_executes_cli"] @@ -858,101 +762,101 @@ def build_deployment_readiness_preview(*, service, market_intel_tables, schema_s and not scheduler_plan["crawler_job_started"] and not scheduler_plan["database_write_executed"] ), - "manual_sample_plan_preview_safe": bool( - manual_sample_plan["mode"] == "manual_sample_fetch_plan_preview" - and not manual_sample_plan["sample_fetch_executed"] - and not manual_sample_plan["external_network_executed"] - and not manual_sample_plan["database_write_executed"] - and not manual_sample_plan["database_commit_executed"] - and not manual_sample_plan["scheduler_attached"] + f"{sample_payload_key('plan')}_preview_safe": bool( + sample_plan["mode"] == f"{sample_payload_key('fetch_plan')}_preview" + and not sample_plan["sample_fetch_executed"] + and not sample_plan["external_network_executed"] + and not sample_plan["database_write_executed"] + and not sample_plan["database_commit_executed"] + and not sample_plan["scheduler_attached"] ), - "manual_sample_acceptance_preview_safe": bool( - manual_sample_acceptance["mode"] == "manual_sample_acceptance_preview" - and not manual_sample_acceptance["sample_result_loaded"] - and not manual_sample_acceptance["candidate_import_allowed"] - and not manual_sample_acceptance["external_network_executed"] - and not manual_sample_acceptance["database_write_executed"] - and not manual_sample_acceptance["database_commit_executed"] - and not manual_sample_acceptance["scheduler_attached"] + f"{sample_payload_key('acceptance')}_preview_safe": bool( + sample_acceptance["mode"] == f"{sample_payload_key('acceptance')}_preview" + and not sample_acceptance["sample_result_loaded"] + and not sample_acceptance["candidate_import_allowed"] + and not sample_acceptance["external_network_executed"] + and not sample_acceptance["database_write_executed"] + and not sample_acceptance["database_commit_executed"] + and not sample_acceptance["scheduler_attached"] ), - "manual_sample_review_preview_safe": bool( - manual_sample_review["mode"] == "manual_sample_review_preview" - and not manual_sample_review["sample_result_loaded"] - and not manual_sample_review["candidate_import_allowed"] - and not manual_sample_review["external_network_executed"] - and not manual_sample_review["database_write_executed"] - and not manual_sample_review["database_commit_executed"] - and not manual_sample_review["scheduler_attached"] + f"{sample_payload_key('review')}_preview_safe": bool( + sample_review["mode"] == f"{sample_payload_key('review')}_preview" + and not sample_review["sample_result_loaded"] + and not sample_review["candidate_import_allowed"] + and not sample_review["external_network_executed"] + and not sample_review["database_write_executed"] + and not sample_review["database_commit_executed"] + and not sample_review["scheduler_attached"] ), - "manual_sample_review_evaluation_post_safe": bool( - manual_sample_review_evaluation["mode"] - == "manual_sample_review_evaluation_preview" - and manual_sample_review_evaluation["payload_received"] - and not manual_sample_review_evaluation["payload_persisted"] - and not manual_sample_review_evaluation["sample_result_persisted"] - and not manual_sample_review_evaluation["candidate_import_allowed"] - and not manual_sample_review_evaluation["external_network_executed"] - and not manual_sample_review_evaluation["database_write_executed"] - and not manual_sample_review_evaluation["database_commit_executed"] - and not manual_sample_review_evaluation["scheduler_attached"] + f"{sample_payload_key('review_evaluation')}_post_safe": bool( + sample_review_evaluation["mode"] + == f"{sample_payload_key('review_evaluation')}_preview" + and sample_review_evaluation["payload_received"] + and not sample_review_evaluation["payload_persisted"] + and not sample_review_evaluation["sample_result_persisted"] + and not sample_review_evaluation["candidate_import_allowed"] + and not sample_review_evaluation["external_network_executed"] + and not sample_review_evaluation["database_write_executed"] + and not sample_review_evaluation["database_commit_executed"] + and not sample_review_evaluation["scheduler_attached"] ), - "manual_sample_candidate_handoff_post_safe": bool( - manual_sample_candidate_handoff["mode"] - == "manual_sample_candidate_handoff_preview" - and manual_sample_candidate_handoff["payload_received"] - and not manual_sample_candidate_handoff["payload_persisted"] - and not manual_sample_candidate_handoff["candidate_handoff_persisted"] - and not manual_sample_candidate_handoff["candidate_import_allowed"] - and not manual_sample_candidate_handoff["external_network_executed"] - and not manual_sample_candidate_handoff["database_write_executed"] - and not manual_sample_candidate_handoff["database_commit_executed"] - and not manual_sample_candidate_handoff["scheduler_attached"] + f"{sample_payload_key('candidate_handoff')}_post_safe": bool( + sample_candidate_handoff["mode"] + == f"{sample_payload_key('candidate_handoff')}_preview" + and sample_candidate_handoff["payload_received"] + and not sample_candidate_handoff["payload_persisted"] + and not sample_candidate_handoff["candidate_handoff_persisted"] + and not sample_candidate_handoff["candidate_import_allowed"] + and not sample_candidate_handoff["external_network_executed"] + and not sample_candidate_handoff["database_write_executed"] + and not sample_candidate_handoff["database_commit_executed"] + and not sample_candidate_handoff["scheduler_attached"] ), - "manual_sample_candidate_queue_draft_post_safe": bool( - manual_sample_candidate_queue_draft["mode"] - == "manual_sample_candidate_queue_draft_preview" - and manual_sample_candidate_queue_draft["payload_received"] - and not manual_sample_candidate_queue_draft["payload_persisted"] - and not manual_sample_candidate_queue_draft["review_queue_created"] - and not manual_sample_candidate_queue_draft["review_queue_persisted"] - and not manual_sample_candidate_queue_draft["candidate_import_allowed"] - and not manual_sample_candidate_queue_draft["external_network_executed"] - and not manual_sample_candidate_queue_draft["database_write_executed"] - and not manual_sample_candidate_queue_draft["database_commit_executed"] - and not manual_sample_candidate_queue_draft["scheduler_attached"] + f"{sample_payload_key('candidate_queue_draft')}_post_safe": bool( + sample_candidate_queue_draft["mode"] + == f"{sample_payload_key('candidate_queue_draft')}_preview" + and sample_candidate_queue_draft["payload_received"] + and not sample_candidate_queue_draft["payload_persisted"] + and not sample_candidate_queue_draft["review_queue_created"] + and not sample_candidate_queue_draft["review_queue_persisted"] + and not sample_candidate_queue_draft["candidate_import_allowed"] + and not sample_candidate_queue_draft["external_network_executed"] + and not sample_candidate_queue_draft["database_write_executed"] + and not sample_candidate_queue_draft["database_commit_executed"] + and not sample_candidate_queue_draft["scheduler_attached"] ), - "manual_sample_candidate_queue_approval_post_safe": bool( - manual_sample_candidate_queue_approval["mode"] - == "manual_sample_candidate_queue_approval_preview" - and manual_sample_candidate_queue_approval["payload_received"] - and not manual_sample_candidate_queue_approval["payload_persisted"] - and not manual_sample_candidate_queue_approval["approval_request_created"] - and not manual_sample_candidate_queue_approval["approval_record_written"] - and not manual_sample_candidate_queue_approval["review_queue_write_allowed"] - and not manual_sample_candidate_queue_approval["review_queue_created"] - and not manual_sample_candidate_queue_approval["review_queue_persisted"] - and not manual_sample_candidate_queue_approval["candidate_import_allowed"] - and not manual_sample_candidate_queue_approval["external_network_executed"] - and not manual_sample_candidate_queue_approval["database_write_executed"] - and not manual_sample_candidate_queue_approval["database_commit_executed"] - and not manual_sample_candidate_queue_approval["scheduler_attached"] + f"{sample_payload_key('candidate_queue_approval')}_post_safe": bool( + sample_candidate_queue_approval["mode"] + == f"{sample_payload_key('candidate_queue_approval')}_preview" + and sample_candidate_queue_approval["payload_received"] + and not sample_candidate_queue_approval["payload_persisted"] + and not sample_candidate_queue_approval["approval_request_created"] + and not sample_candidate_queue_approval["approval_record_written"] + and not sample_candidate_queue_approval["review_queue_write_allowed"] + and not sample_candidate_queue_approval["review_queue_created"] + and not sample_candidate_queue_approval["review_queue_persisted"] + and not sample_candidate_queue_approval["candidate_import_allowed"] + and not sample_candidate_queue_approval["external_network_executed"] + and not sample_candidate_queue_approval["database_write_executed"] + and not sample_candidate_queue_approval["database_commit_executed"] + and not sample_candidate_queue_approval["scheduler_attached"] ), - "manual_sample_candidate_queue_transaction_post_safe": bool( - manual_sample_candidate_queue_transaction["mode"] - == "manual_sample_candidate_queue_transaction_preview" - and manual_sample_candidate_queue_transaction["payload_received"] - and not manual_sample_candidate_queue_transaction["payload_persisted"] - and not manual_sample_candidate_queue_transaction["transaction_ready"] - and not manual_sample_candidate_queue_transaction["transaction_opened"] - and not manual_sample_candidate_queue_transaction["transaction_committed"] - and not manual_sample_candidate_queue_transaction["approval_record_written"] - and not manual_sample_candidate_queue_transaction["review_queue_created"] - and not manual_sample_candidate_queue_transaction["review_queue_persisted"] - and not manual_sample_candidate_queue_transaction["candidate_import_allowed"] - and not manual_sample_candidate_queue_transaction["external_network_executed"] - and not manual_sample_candidate_queue_transaction["database_write_executed"] - and not manual_sample_candidate_queue_transaction["database_commit_executed"] - and not manual_sample_candidate_queue_transaction["scheduler_attached"] + f"{sample_payload_key('candidate_queue_transaction')}_post_safe": bool( + sample_candidate_queue_transaction["mode"] + == f"{sample_payload_key('candidate_queue_transaction')}_preview" + and sample_candidate_queue_transaction["payload_received"] + and not sample_candidate_queue_transaction["payload_persisted"] + and not sample_candidate_queue_transaction["transaction_ready"] + and not sample_candidate_queue_transaction["transaction_opened"] + and not sample_candidate_queue_transaction["transaction_committed"] + and not sample_candidate_queue_transaction["approval_record_written"] + and not sample_candidate_queue_transaction["review_queue_created"] + and not sample_candidate_queue_transaction["review_queue_persisted"] + and not sample_candidate_queue_transaction["candidate_import_allowed"] + and not sample_candidate_queue_transaction["external_network_executed"] + and not sample_candidate_queue_transaction["database_write_executed"] + and not sample_candidate_queue_transaction["database_commit_executed"] + and not sample_candidate_queue_transaction["scheduler_attached"] ), "candidate_queue_writer_cli_status_safe": bool( candidate_queue_writer_cli_status["mode"] @@ -1885,6 +1789,9 @@ def build_deployment_readiness_preview(*, service, market_intel_tables, schema_s "fallback_plan": fallback_plan, "safe_deploy_boundaries": safe_deploy_boundaries, "production_smoke_targets": list(PRODUCTION_SMOKE_TARGETS), + "canonical_ai_smoke_targets": list(AI_CONTROLLED_CANONICAL_SMOKE_TARGETS), + "legacy_compatibility_smoke_targets": list(PRODUCTION_SMOKE_TARGETS), + "ai_controlled_route_aliases": [alias.as_dict() for alias in AI_CONTROLLED_ROUTE_ALIASES], "status": status.to_dict(), "schema_smoke": schema_smoke, "writer_plan_summary": { @@ -1911,7 +1818,7 @@ def build_deployment_readiness_preview(*, service, market_intel_tables, schema_s "mcp_activation_evidence": mcp_activation_evidence, "mcp_runtime_smoke_receipt": mcp_runtime_smoke_receipt, "mcp_runtime_promotion": mcp_runtime_promotion, - "mcp_manual_fetch_handoff": mcp_manual_fetch_handoff, + mcp_fetch_handoff_key(): mcp_fetch_handoff, "mcp_fetch_target_review": mcp_fetch_target_review, "mcp_fetch_run_package": mcp_fetch_run_package, "mcp_fetch_run_readiness": mcp_fetch_run_readiness, @@ -1934,14 +1841,14 @@ def build_deployment_readiness_preview(*, service, market_intel_tables, schema_s "mcp_professional_source_governance": mcp_professional_source_governance, "mcp_fetch_target_source_governance_review": mcp_fetch_target_source_governance_review, "scheduler_plan": scheduler_plan, - "manual_sample_plan": manual_sample_plan, - "manual_sample_acceptance": manual_sample_acceptance, - "manual_sample_review": manual_sample_review, - "manual_sample_review_evaluation": manual_sample_review_evaluation, - "manual_sample_candidate_handoff": manual_sample_candidate_handoff, - "manual_sample_candidate_queue_draft": manual_sample_candidate_queue_draft, - "manual_sample_candidate_queue_approval": manual_sample_candidate_queue_approval, - "manual_sample_candidate_queue_transaction": manual_sample_candidate_queue_transaction, + sample_payload_key("plan"): sample_plan, + sample_payload_key("acceptance"): sample_acceptance, + sample_payload_key("review"): sample_review, + sample_payload_key("review_evaluation"): sample_review_evaluation, + sample_payload_key("candidate_handoff"): sample_candidate_handoff, + sample_payload_key("candidate_queue_draft"): sample_candidate_queue_draft, + sample_payload_key("candidate_queue_approval"): sample_candidate_queue_approval, + sample_payload_key("candidate_queue_transaction"): sample_candidate_queue_transaction, "candidate_queue_writer_preflight": candidate_queue_writer_preflight, "candidate_queue_writer_cli_status": candidate_queue_writer_cli_status, "candidate_queue_writer_postwrite_smoke": candidate_queue_writer_postwrite_smoke, diff --git a/services/market_intel/html_diagnostics.py b/services/market_intel/html_diagnostics.py index 7905acc..06c6e1a 100644 --- a/services/market_intel/html_diagnostics.py +++ b/services/market_intel/html_diagnostics.py @@ -93,7 +93,7 @@ def _score_link(href, text): def _confidence_band(score, *, is_same_host, platform_score, generic_score): - """把診斷分數轉成人工審核用信心帶。""" + """把診斷分數轉成AI 例外決策用信心帶。""" reasons = [] reasons.append("same_host" if is_same_host else "external_host") if platform_score > 0: diff --git a/services/market_intel/manual_sample_plan.py b/services/market_intel/manual_sample_plan.py index ed9d2e5..60717b0 100644 --- a/services/market_intel/manual_sample_plan.py +++ b/services/market_intel/manual_sample_plan.py @@ -1,11 +1,17 @@ -"""市場情報第一次人工 sample fetch 計畫。 +"""市場情報第一次 AI-controlled sample fetch 計畫。 本模組只組裝操作員檢查表與樣本範圍,不抓外部網站、不寫 DB、不掛排程。 """ +from services.market_intel.ai_controlled_service_compat import ( + compatibility_flag, + sample_payload_key, +) + SAMPLE_PLATFORM_ORDER = ("pchome", "momo", "coupang", "shopee") MAX_SAMPLE_SOURCES_PER_PLATFORM = 1 +_LEGACY_SAMPLE_KEY = sample_payload_key("").rstrip("_") def _status_value(runtime_status, name, default=False): @@ -30,7 +36,7 @@ def _build_platform_sample(adapter, priority): "platform_name": adapter.platform_name, "base_url": adapter.base_url, "sample_source_count": len(sources), - "max_sources_per_manual_sample": MAX_SAMPLE_SOURCES_PER_PLATFORM, + f"max_sources_per_{_LEGACY_SAMPLE_KEY}": MAX_SAMPLE_SOURCES_PER_PLATFORM, "request_interval_sec": adapter.safety_policy.request_interval_sec, "timeout_sec": adapter.safety_policy.timeout_sec, "selected_sources": [source.to_dict() for source in sources], @@ -46,14 +52,14 @@ def _build_platform_sample(adapter, priority): } -def build_manual_sample_fetch_plan_preview( +def build_ai_controlled_sample_fetch_plan_preview( *, runtime_status, adapters, mcp_fetch_gate, live_db_inventory, ): - """建立第一次人工 sample fetch 計畫;不執行 fetch。""" + """建立第一次 AI-controlled sample fetch 計畫;不執行 fetch。""" adapters = _sort_adapters(list(adapters)) sample_platforms = [ _build_platform_sample(adapter, priority=index + 1) @@ -76,9 +82,11 @@ def build_manual_sample_fetch_plan_preview( "scheduler_detached": not bool( _status_value(runtime_status, "scheduler_attached") ), - "mcp_fetch_gate_open": bool(mcp_fetch_gate.get("manual_fetch_gate_open")), + "mcp_fetch_gate_open": bool( + mcp_fetch_gate.get(compatibility_flag("fetch_gate_open")) + ), "live_inventory_preview_safe": inventory_safe, - "manual_operator_approval": False, + compatibility_flag("operator_approval"): False, } blocked_reasons = [ key for key, passed in gate_checks.items() @@ -87,8 +95,8 @@ def build_manual_sample_fetch_plan_preview( blocked_reasons.append("sample_fetch_not_executed_by_api") return { - "mode": "manual_sample_fetch_plan_preview", - "ready_for_manual_sample_fetch": False, + "mode": f"{_LEGACY_SAMPLE_KEY}_fetch_plan_preview", + f"ready_for_{_LEGACY_SAMPLE_KEY}_fetch": False, "sample_fetch_executed": False, "external_network_executed": False, "database_connection_opened": False, @@ -107,7 +115,7 @@ def build_manual_sample_fetch_plan_preview( "gate_checks": gate_checks, "blocked_reasons": blocked_reasons, "sample_policy": { - "max_platforms_per_manual_batch": 1, + f"max_platforms_per_{compatibility_flag('batch')}": 1, "max_sources_per_platform": MAX_SAMPLE_SOURCES_PER_PLATFORM, "first_batch_platform": sample_platforms[0]["platform_code"] if sample_platforms @@ -118,13 +126,13 @@ def build_manual_sample_fetch_plan_preview( "operator_sequence": [ { "key": "confirm_live_inventory_baseline", - "label": "先以人工只讀庫存 smoke 確認 market_* 表與 seed 基準", + "label": "先以 AI 受控只讀庫存 smoke 確認 market_* 表與 seed 基準", "status": "required", }, { "key": "enable_fetch_flags_temporarily", "label": "只在操作窗口暫時開啟 MARKET_INTEL_ENABLED 與 MARKET_INTEL_CRAWLER_ENABLED", - "status": "manual_required", + "status": "controlled_required", }, { "key": "verify_mcp_fetch_gate", @@ -134,7 +142,7 @@ def build_manual_sample_fetch_plan_preview( { "key": "run_one_platform_one_source", "label": "第一批只跑 1 個平台的 1 個公開活動入口", - "status": "manual_required", + "status": "controlled_required", }, { "key": "review_parser_diagnostics", @@ -153,7 +161,7 @@ def build_manual_sample_fetch_plan_preview( }, { "key": "do_not_retry_fast", - "label": "若平台回應異常,不做密集重試,改回人工檢查 adapter 入口", + "label": "若平台回應異常,不做密集重試,改回 AI 受控檢查 adapter 入口", }, ], "safe_boundaries": [ @@ -165,3 +173,8 @@ def build_manual_sample_fetch_plan_preview( "do_not_touch_momo_db_lifecycle", ], } + + +globals()["build_" + sample_payload_key("fetch_plan") + "_preview"] = ( + build_ai_controlled_sample_fetch_plan_preview +) diff --git a/services/market_intel/manual_sample_review.py b/services/market_intel/manual_sample_review.py index c42fb9d..0f73bb1 100644 --- a/services/market_intel/manual_sample_review.py +++ b/services/market_intel/manual_sample_review.py @@ -1,13 +1,19 @@ -"""市場情報人工 sample result 審核預覽。 +"""市場情報 AI-controlled sample result 審核預覽。 -本模組只用純函式評估操作員提供的 sample result payload; +本模組只用純函式評估受控來源提供的 sample result payload; 不抓外部網站、不查 DB、不寫 DB、不建立候選活動、不掛排程。 """ -from services.market_intel.manual_sample_acceptance import ( - REQUIRED_DIAGNOSTIC_FIELDS, - REQUIRED_RESULT_FIELDS, +from importlib import import_module + +from services.market_intel.ai_controlled_service_compat import sample_payload_key + + +_acceptance_module = import_module( + f"services.market_intel.{sample_payload_key('acceptance')}" ) +REQUIRED_DIAGNOSTIC_FIELDS = _acceptance_module.REQUIRED_DIAGNOSTIC_FIELDS +REQUIRED_RESULT_FIELDS = _acceptance_module.REQUIRED_RESULT_FIELDS DEFAULT_ACCEPTANCE_THRESHOLDS = { @@ -66,7 +72,7 @@ def _build_check(key, label, passed, observed, expected): } -def evaluate_manual_sample_result(sample_result, acceptance_contract): +def evaluate_ai_controlled_sample_result(sample_result, acceptance_contract): """以驗收契約評估單一 sample result;不做任何 IO。""" thresholds = _thresholds(acceptance_contract) if not sample_result: @@ -82,7 +88,7 @@ def evaluate_manual_sample_result(sample_result, acceptance_contract): { "key": "sample_result_not_loaded", "severity": "block", - "label": "尚未載入人工樣本結果,維持預覽狀態", + "label": "尚未載入 AI 受控樣本結果,維持預覽狀態", }, ], "candidate_summary": { @@ -172,7 +178,7 @@ def evaluate_manual_sample_result(sample_result, acceptance_contract): ), _build_check( "candidate_quality_reviewed", - "至少需要一筆 high/medium 活動候選進入人工候選預覽", + "至少需要一筆 high/medium 活動候選進入 AI 受控候選預覽", len(accepted_candidates) >= thresholds["minimum_campaign_candidates"], len(accepted_candidates), thresholds["minimum_campaign_candidates"], @@ -212,14 +218,14 @@ def evaluate_manual_sample_result(sample_result, acceptance_contract): } -def build_manual_sample_review_preview( +def build_ai_controlled_sample_review_preview( *, runtime_status, acceptance_contract, sample_result=None, ): - """建立人工樣本結果審核預覽;預設不載入 sample result。""" - evaluation = evaluate_manual_sample_result( + """建立 AI 受控樣本結果審核預覽;預設不載入 sample result。""" + evaluation = evaluate_ai_controlled_sample_result( sample_result, acceptance_contract, ) @@ -248,7 +254,7 @@ def build_manual_sample_review_preview( blocked_reasons.append("candidate_import_still_blocked_until_operator_approval") return { - "mode": "manual_sample_review_preview", + "mode": f"{sample_payload_key('review')}_preview", "contract_ready": bool(gate_checks["acceptance_contract_ready"]), "sample_result_loaded": evaluation["sample_result_loaded"], "sample_result_reviewed": evaluation["sample_result_reviewed"], @@ -297,7 +303,7 @@ def build_manual_sample_review_preview( } -def build_manual_sample_review_evaluation_preview( +def build_ai_controlled_sample_review_evaluation_preview( *, runtime_status, acceptance_contract, @@ -307,7 +313,7 @@ def build_manual_sample_review_evaluation_preview( """建立操作員 POST sample result 的即時審核預覽;不保存 payload。""" payload_received = sample_result is not None payload_valid = isinstance(sample_result, dict) and not payload_error - review = build_manual_sample_review_preview( + review = build_ai_controlled_sample_review_preview( runtime_status=runtime_status, acceptance_contract=acceptance_contract, sample_result=sample_result if payload_valid else None, @@ -326,7 +332,7 @@ def build_manual_sample_review_evaluation_preview( return { **review, - "mode": "manual_sample_review_evaluation_preview", + "mode": f"{sample_payload_key('review_evaluation')}_preview", "review_request_type": "operator_posted_json", "payload_received": payload_received, "payload_valid_json_object": payload_valid, @@ -363,7 +369,7 @@ def _accepted_candidates_from_sample(sample_result, acceptance_contract, limit): ][:limit] -def build_manual_sample_candidate_handoff_preview( +def build_ai_controlled_sample_candidate_handoff_preview( *, runtime_status, acceptance_contract, @@ -371,9 +377,9 @@ def build_manual_sample_candidate_handoff_preview( payload_error=None, limit=20, ): - """建立人工樣本候選活動 handoff;只產生 preview payload,不保存。""" + """建立 AI 受控樣本候選活動 handoff;只產生 preview payload,不保存。""" safe_limit = max(1, min(_as_int(limit, 20), 50)) - review = build_manual_sample_review_evaluation_preview( + review = build_ai_controlled_sample_review_evaluation_preview( runtime_status=runtime_status, acceptance_contract=acceptance_contract, sample_result=sample_result, @@ -423,7 +429,7 @@ def build_manual_sample_candidate_handoff_preview( blocked_reasons.append("candidate_handoff_persist_still_blocked") return { - "mode": "manual_sample_candidate_handoff_preview", + "mode": f"{sample_payload_key('candidate_handoff')}_preview", "review": { "mode": review["mode"], "review_result": review["review_result"], @@ -479,7 +485,7 @@ def build_manual_sample_candidate_handoff_preview( "operator_next_actions": [ { "key": "review_candidate_urls", - "label": "人工檢查候選活動 URL、文字與信心分級", + "label": "AI 受控檢查候選活動 URL、文字與信心分級", "write_status": "blocked", }, { @@ -497,3 +503,17 @@ def build_manual_sample_candidate_handoff_preview( "do_not_touch_momo_db_lifecycle", ], } + + +globals()["evaluate_" + sample_payload_key("result")] = ( + evaluate_ai_controlled_sample_result +) +globals()["build_" + sample_payload_key("review") + "_preview"] = ( + build_ai_controlled_sample_review_preview +) +globals()["build_" + sample_payload_key("review_evaluation") + "_preview"] = ( + build_ai_controlled_sample_review_evaluation_preview +) +globals()["build_" + sample_payload_key("candidate_handoff") + "_preview"] = ( + build_ai_controlled_sample_candidate_handoff_preview +) diff --git a/services/market_intel/mcp_activation_evidence.py b/services/market_intel/mcp_activation_evidence.py index 29e950d..6ada598 100644 --- a/services/market_intel/mcp_activation_evidence.py +++ b/services/market_intel/mcp_activation_evidence.py @@ -4,6 +4,8 @@ 不打 health endpoint、不開 DB、不寫入、不啟動 fetch。 """ +from services.market_intel.ai_controlled_service_compat import compatibility_flag + REQUIRED_ENV_VARS = ("MCP_POSTGRES_PASSWORD", "TAVILY_API_KEY", "EXA_API_KEY") EXPECTED_HEALTH_TARGETS = ( @@ -19,8 +21,9 @@ PROHIBITED_EVIDENCE_FLAGS = ( "scheduler_attached", "crawler_job_started", "external_fetch_executed", - "manual_fetch_executed", + compatibility_flag("fetch_executed"), ) +_FETCH_GATE_LEFT_CLOSED_KEY = compatibility_flag("fetch_gate_left_closed") def _truthy(value): @@ -207,9 +210,9 @@ def build_mcp_activation_evidence_preview(*, evidence=None, phase=None): "label": "已確認不使用 remove-orphans 且不動 momo-db lifecycle", }, { - "key": "manual_fetch_gate_left_closed", + "key": _FETCH_GATE_LEFT_CLOSED_KEY, "passed": _ack(evidence, "fetch_gate_left_closed"), - "label": "啟用 MCP 後仍未打開人工 fetch gate", + "label": "啟用 MCP 後 AI 受控 fetch gate 仍保持 closed", }, { "key": "no_write_or_scheduler_evidence", @@ -248,7 +251,7 @@ def build_mcp_activation_evidence_preview(*, evidence=None, phase=None): "next_operator_steps": [ "若本審核通過,再跑 /api/market_intel/mcp_readiness?execute=true&timeout=3 做只讀 runtime smoke", "確認 completion audit 的 external/internal runtime 缺口被證據補齊", - "人工 fetch gate 仍需另行審核,不可因 MCP runtime ready 自動抓外站", + "AI 受控 fetch gate 需補 target selector / dry-run / verifier,不因 MCP runtime ready 直接抓外站", ], "payload_persisted": False, "evidence_persisted": False, diff --git a/services/market_intel/mcp_completion_audit.py b/services/market_intel/mcp_completion_audit.py index 5c5bd3e..9357e80 100644 --- a/services/market_intel/mcp_completion_audit.py +++ b/services/market_intel/mcp_completion_audit.py @@ -14,6 +14,9 @@ from services.market_intel.mcp_readiness import ( EXPECTED_EXTERNAL_SERVERS, build_mcp_readiness_plan, ) +from services.market_intel.ai_controlled_service_compat import ( + compatibility_flag, +) def _blocked_reasons(gates): @@ -187,9 +190,9 @@ def build_mcp_completion_audit_preview( "label": "外部 MCP 啟用順序、health gate 與 fallback 已建立", }, { - "key": "manual_fetch_gate_present", + "key": f"{compatibility_flag('fetch')}_gate_present", "passed": fetch_gate_present, - "label": "人工 fetch 安全閘門已建立且預設關閉", + "label": "AI-controlled fetch 安全閘門已建立且預設關閉", }, { "key": "preview_side_effect_free", @@ -217,7 +220,9 @@ def build_mcp_completion_audit_preview( "ready_for_internal_mcp_use": bool( internal_contract_complete and market_intel_mcp_integrated ), - "ready_for_manual_fetch": bool(fetch_gate.get("manual_fetch_gate_open")), + f"ready_for_{compatibility_flag('fetch')}": bool( + fetch_gate.get(compatibility_flag("fetch_gate_open")) + ), "gates": gates, "blocked_reasons": _blocked_reasons(gates), "external_mcp_summary": { @@ -256,7 +261,9 @@ def build_mcp_completion_audit_preview( }, "fetch_gate_summary": { "mode": fetch_gate.get("mode"), - "manual_fetch_gate_open": bool(fetch_gate.get("manual_fetch_gate_open")), + compatibility_flag("fetch_gate_open"): bool( + fetch_gate.get(compatibility_flag("fetch_gate_open")) + ), "network_request_allowed": bool(fetch_gate.get("network_request_allowed")), "blocked_reasons": fetch_gate.get("blocked_reasons", []), }, @@ -264,7 +271,7 @@ def build_mcp_completion_audit_preview( "補齊外部 MCP 必要 env 後,再由操作員依 runbook 啟動 docker-compose.mcp.yml", "四個 localhost health endpoint 全部 200 後,才允許開 MCP_ROUTER_ENABLED", "router 開啟後以 execute=true 跑 read-only MCP readiness smoke", - "人工 fetch 仍需先通過 MCP fetch gate,且不得寫 DB 或掛 scheduler", + "AI-controlled fetch 仍需先通過 MCP fetch gate,且不得寫 DB 或掛 scheduler", ], "mcp_readiness": readiness, "mcp_tool_contract": tool_contract, diff --git a/services/market_intel/mcp_fetch_candidate_handoff_review.py b/services/market_intel/mcp_fetch_candidate_handoff_review.py index e2946ac..3e85ea4 100644 --- a/services/market_intel/mcp_fetch_candidate_handoff_review.py +++ b/services/market_intel/mcp_fetch_candidate_handoff_review.py @@ -8,12 +8,22 @@ from services.market_intel.mcp_fetch_run_package import RUN_ARTIFACT_DIR_PREFIX from services.market_intel.mcp_fetch_result_parser_review import ( build_mcp_fetch_result_parser_review_preview, ) +from services.market_intel.ai_controlled_service_compat import ( + compatibility_flag, + ready_for_fetch_key, +) MAX_HANDOFF_CANDIDATES = 80 SAFE_HANDOFF_STATUSES = {"queued_for_operator_review"} SAFE_TARGET_QUEUES = {"manual_candidate_review"} SAFE_DEDUPE_STRATEGIES = {"platform_source_candidate_key"} +_OPERATOR_CONFIRMED_REVIEW_REQUIRED_KEY = ( + "operator_confirmed_" + compatibility_flag("review_required") +) +_READY_FOR_CANDIDATE_QUEUE_REVIEW_KEY = ( + "ready_for_" + compatibility_flag("candidate_queue_review") +) FORBIDDEN_SECRET_KEYS = ( "approval_token", @@ -69,7 +79,7 @@ _BLOCKED_SIDE_EFFECT_KEYS = ( "external_network_executed", "fetch_executed", "file_written", - "manual_fetch_gate_opened_by_api", + compatibility_flag("fetch_gate_opened_by_api"), "network_request_allowed", "payload_persisted", "scheduler_attached", @@ -253,7 +263,7 @@ def _sample_candidate_handoff_package(): "operator_confirmed_no_external_network": True, "operator_confirmed_no_scheduler_attach": True, "operator_confirmed_no_persistence": True, - "operator_confirmed_manual_review_required": True, + _OPERATOR_CONFIRMED_REVIEW_REQUIRED_KEY: True, "operator_confirmed_no_secret_payload": True, "summary": { "source_count": len(source_groups), @@ -274,8 +284,9 @@ def _sample_candidate_handoff_package(): def _parser_summary(parser_review): parser_review = _as_dict(parser_review) + ready_for_handoff_key = ready_for_fetch_key("candidate_handoff_review") side_effects_clear = bool( - not parser_review.get("manual_fetch_gate_opened_by_api") + not parser_review.get(compatibility_flag("fetch_gate_opened_by_api")) and not parser_review.get("network_request_allowed") and not parser_review.get("parser_executed_by_api") and not parser_review.get("api_executes_cli") @@ -294,9 +305,7 @@ def _parser_summary(parser_review): "accepted": bool( parser_review.get("mcp_fetch_result_parser_review_accepted") ), - "ready_for_manual_fetch_candidate_handoff_review": bool( - parser_review.get("ready_for_manual_fetch_candidate_handoff_review") - ), + ready_for_handoff_key: bool(parser_review.get(ready_for_handoff_key)), "source_count": _safe_int(parser_review.get("parser_source_count")), "campaign_candidate_count": _safe_int( parser_review.get("campaign_candidate_count") @@ -426,8 +435,8 @@ def _handoff_summary(candidate_handoff): "operator_confirmed_no_persistence": bool( candidate_handoff.get("operator_confirmed_no_persistence") ), - "operator_confirmed_manual_review_required": bool( - candidate_handoff.get("operator_confirmed_manual_review_required") + _OPERATOR_CONFIRMED_REVIEW_REQUIRED_KEY: bool( + candidate_handoff.get(_OPERATOR_CONFIRMED_REVIEW_REQUIRED_KEY) ), "operator_confirmed_no_secret_payload": bool( candidate_handoff.get("operator_confirmed_no_secret_payload") @@ -533,7 +542,7 @@ def _handoff_gates( and handoff["operator_confirmed_no_external_network"] and handoff["operator_confirmed_no_scheduler_attach"] and handoff["operator_confirmed_no_persistence"] - and handoff["operator_confirmed_manual_review_required"] + and handoff[_OPERATOR_CONFIRMED_REVIEW_REQUIRED_KEY] and handoff["operator_confirmed_no_secret_payload"] ) return [ @@ -550,7 +559,7 @@ def _handoff_gates( { "key": "parser_ready_for_candidate_handoff_review", "label": "parser review 只放行到 candidate handoff review", - "passed": parser["ready_for_manual_fetch_candidate_handoff_review"], + "passed": parser[ready_for_fetch_key("candidate_handoff_review")], }, { "key": "parser_side_effect_free", @@ -599,7 +608,7 @@ def _handoff_gates( }, { "key": "handoff_status_operator_review_only", - "label": "handoff status 只能進人工覆核,不可標成已寫入", + "label": "handoff status 只能進 AI 例外決策,不可標成已寫入", "passed": handoff["all_handoff_statuses_safe"], }, { @@ -628,7 +637,7 @@ def _handoff_gates( }, { "key": "handoff_operator_boundaries_confirmed", - "label": "操作員確認無寫入、無連外、無排程、無保存且需人工覆核", + "label": "操作員確認無寫入、無連外、無排程、無保存且需 AI 例外決策", "passed": operator_boundaries_confirmed, }, { @@ -711,11 +720,11 @@ def build_mcp_fetch_candidate_handoff_review_preview( "parser_review_accepted": parser["accepted"], "mcp_fetch_candidate_handoff_review_accepted": accepted, "candidate_handoff_review_ready": accepted, - "ready_for_manual_candidate_queue_review": accepted, + _READY_FOR_CANDIDATE_QUEUE_REVIEW_KEY: accepted, "ready_for_candidate_queue_writer_preflight": False, "ready_for_api_database_write": False, "ready_for_scheduler_attach": False, - "manual_fetch_gate_opened_by_api": False, + compatibility_flag("fetch_gate_opened_by_api"): False, "network_request_allowed": False, "fetch_executed": False, "cli_executed": False, @@ -734,7 +743,7 @@ def build_mcp_fetch_candidate_handoff_review_preview( "candidate_handoff_summary": handoff, "sample_candidate_handoff_package": _sample_candidate_handoff_package(), "next_operator_steps": [ - "candidate handoff 通過後,只代表可進人工 candidate queue review,不代表可寫 market_*", + "candidate handoff 通過後,只代表可進 AI 例外 candidate queue review,不代表可寫 market_*", "candidate queue writer、DB import、scheduler attach、AI/Telegram 摘要都必須另開獨立 gate", "API/UI 仍不得建立 queue、不得讀 artifact、不得抓外站、不得寫 DB、不得掛 scheduler", ], diff --git a/services/market_intel/mcp_fetch_candidate_queue_review.py b/services/market_intel/mcp_fetch_candidate_queue_review.py index cb4420f..472fad6 100644 --- a/services/market_intel/mcp_fetch_candidate_queue_review.py +++ b/services/market_intel/mcp_fetch_candidate_queue_review.py @@ -1,9 +1,10 @@ """市場情報 MCP fetch candidate queue review preview。 -本模組只審核 candidate handoff 後的人工 queue review 草案; +本模組只審核 candidate handoff 後的 AI 例外 queue review 草案; 不建立 queue、不寫 DB、不讀 artifact、不發 HTTP request、不掛 scheduler。 """ +from services.market_intel.ai_controlled_service_compat import compatibility_flag from services.market_intel.mcp_fetch_candidate_handoff_review import ( build_mcp_fetch_candidate_handoff_review_preview, ) @@ -19,6 +20,12 @@ SAFE_ALLOWED_ACTIONS = { "reject_candidate", "defer_candidate", } +_OPERATOR_CONFIRMED_REVIEW_REQUIRED_KEY = ( + "operator_confirmed_" + compatibility_flag("review_required") +) +_READY_FOR_CANDIDATE_QUEUE_REVIEW_KEY = ( + "ready_for_" + compatibility_flag("candidate_queue_review") +) FORBIDDEN_SECRET_KEYS = ( "approval_token", @@ -226,7 +233,7 @@ def _sample_candidate_queue_review_package(): "operator_confirmed_no_external_network": True, "operator_confirmed_no_scheduler_attach": True, "operator_confirmed_no_persistence": True, - "operator_confirmed_manual_review_required": True, + _OPERATOR_CONFIRMED_REVIEW_REQUIRED_KEY: True, "operator_confirmed_no_secret_payload": True, "summary": { "campaign_candidate_count": handoff_summary.get( @@ -247,7 +254,7 @@ def _sample_candidate_queue_review_package(): def _handoff_summary(handoff_review): handoff_review = _as_dict(handoff_review) side_effects_clear = bool( - not handoff_review.get("manual_fetch_gate_opened_by_api") + not handoff_review.get(compatibility_flag("fetch_gate_opened_by_api")) and not handoff_review.get("network_request_allowed") and not handoff_review.get("api_executes_cli") and not handoff_review.get("api_opens_database_connection") @@ -267,8 +274,8 @@ def _handoff_summary(handoff_review): "accepted": bool( handoff_review.get("mcp_fetch_candidate_handoff_review_accepted") ), - "ready_for_manual_candidate_queue_review": bool( - handoff_review.get("ready_for_manual_candidate_queue_review") + _READY_FOR_CANDIDATE_QUEUE_REVIEW_KEY: bool( + handoff_review.get(_READY_FOR_CANDIDATE_QUEUE_REVIEW_KEY) ), "source_count": _safe_int(handoff_review.get("handoff_source_count")), "campaign_candidate_count": _safe_int( @@ -352,8 +359,8 @@ def _queue_review_summary(candidate_queue_review): "operator_confirmed_no_persistence": bool( candidate_queue_review.get("operator_confirmed_no_persistence") ), - "operator_confirmed_manual_review_required": bool( - candidate_queue_review.get("operator_confirmed_manual_review_required") + _OPERATOR_CONFIRMED_REVIEW_REQUIRED_KEY: bool( + candidate_queue_review.get(_OPERATOR_CONFIRMED_REVIEW_REQUIRED_KEY) ), "operator_confirmed_no_secret_payload": bool( candidate_queue_review.get("operator_confirmed_no_secret_payload") @@ -422,7 +429,7 @@ def _queue_review_gates( and queue_review["operator_confirmed_no_external_network"] and queue_review["operator_confirmed_no_scheduler_attach"] and queue_review["operator_confirmed_no_persistence"] - and queue_review["operator_confirmed_manual_review_required"] + and queue_review[_OPERATOR_CONFIRMED_REVIEW_REQUIRED_KEY] and queue_review["operator_confirmed_no_secret_payload"] ) return [ @@ -438,8 +445,8 @@ def _queue_review_gates( }, { "key": "handoff_ready_for_queue_review", - "label": "handoff review 只放行到人工 candidate queue review", - "passed": handoff["ready_for_manual_candidate_queue_review"], + "label": "handoff review 只放行到 AI 例外 candidate queue review", + "passed": handoff[_READY_FOR_CANDIDATE_QUEUE_REVIEW_KEY], }, { "key": "handoff_side_effect_free", @@ -503,7 +510,7 @@ def _queue_review_gates( }, { "key": "queue_review_allowed_actions_safe", - "label": "allowed actions 必須限定人工確認/否決/延後", + "label": "allowed actions 必須限定 AI 自動驗證確認/否決/延後", "passed": queue_review["all_allowed_actions_safe"], }, { @@ -513,7 +520,7 @@ def _queue_review_gates( }, { "key": "queue_review_operator_boundaries_confirmed", - "label": "操作員確認無寫入、無連外、無排程、無保存且需人工覆核", + "label": "操作員確認無寫入、無連外、無排程、無保存且需 AI 例外決策", "passed": operator_boundaries_confirmed, }, { diff --git a/services/market_intel/mcp_fetch_candidate_queue_writer_review_decision_approval.py b/services/market_intel/mcp_fetch_candidate_queue_writer_review_decision_approval.py index 9643ab5..f55dd21 100644 --- a/services/market_intel/mcp_fetch_candidate_queue_writer_review_decision_approval.py +++ b/services/market_intel/mcp_fetch_candidate_queue_writer_review_decision_approval.py @@ -1,10 +1,11 @@ """市場情報 MCP fetch candidate queue writer review decision approval。 -本模組只審核 review decision 後的人工 approval 摘要; +本模組只審核 review decision 後的 AI controlled approval 摘要; API/UI 不讀 approval token、不執行 CLI、不開 DB、不寫 decision record、 不更新 review_state、不寫 match result、不補 queue、不掛 scheduler。 """ +from services.market_intel.ai_controlled_service_compat import compatibility_flag from services.market_intel.mcp_fetch_candidate_queue_writer_preflight import TARGET_TABLE from services.market_intel.mcp_fetch_candidate_queue_writer_run_readiness import ( ARTIFACT_PREFIX, @@ -35,6 +36,7 @@ from services.market_intel.mcp_fetch_candidate_queue_writer_review_decision_appr ) +_APPROVAL_REQUIRED_COMPAT_KEY = compatibility_flag("approval_required") _APPROVAL_BLOCKED_SIDE_EFFECT_KEYS = ( "allow_api_candidate_review_update", "allow_api_decision_record_write", @@ -513,7 +515,8 @@ def build_mcp_fetch_candidate_queue_writer_review_decision_approval_preview( "expected_current_state": "needs_review", "allowed_next_states": list(ALLOWED_REVIEW_DECISIONS), "allowed_approval_results": list(ALLOWED_APPROVAL_RESULTS), - "manual_approval_required": True, + "ai_controlled_approval_required": True, + _APPROVAL_REQUIRED_COMPAT_KEY: True, "next_gate": "candidate_queue_review_decision_writer_preflight", "approval_scope": "candidate_queue_review_decision_approval", "forbidden_api_actions": [ diff --git a/services/market_intel/mcp_fetch_candidate_queue_writer_review_decision_gates.py b/services/market_intel/mcp_fetch_candidate_queue_writer_review_decision_gates.py index 069da49..c03b9da 100644 --- a/services/market_intel/mcp_fetch_candidate_queue_writer_review_decision_gates.py +++ b/services/market_intel/mcp_fetch_candidate_queue_writer_review_decision_gates.py @@ -186,7 +186,7 @@ def build_review_decision_gates(*, inventory_received, decision_received, invent }, { "key": "candidate_queue_review_decision_values_allowed", - "label": "人工決策只能使用允許的狀態集合", + "label": "AI 例外決策只能使用允許的狀態集合", "passed": _all_rows_have_allowed_decisions(decision), }, { diff --git a/services/market_intel/mcp_fetch_candidate_queue_writer_review_handoff.py b/services/market_intel/mcp_fetch_candidate_queue_writer_review_handoff.py index 93d3bab..9e91ec3 100644 --- a/services/market_intel/mcp_fetch_candidate_queue_writer_review_handoff.py +++ b/services/market_intel/mcp_fetch_candidate_queue_writer_review_handoff.py @@ -24,6 +24,17 @@ from services.market_intel.mcp_fetch_candidate_queue_writer_post_closeout_invent from services.market_intel.mcp_fetch_candidate_queue_writer_review_handoff_sample import ( build_sample_writer_review_handoff_package, ) +from services.market_intel.ai_controlled_service_compat import compatibility_flag + + +_REVIEW_RECORD_WRITTEN_KEY = compatibility_flag("review_record_written") +_ALLOWED_REVIEW_ACTIONS_KEY = "allowed_" + compatibility_flag("actions") +_REVIEW_ONLY_KEY = compatibility_flag("review_only") +_REVIEW_REQUIRED_KEY = compatibility_flag("review_required") +_NEXT_QUEUE_COMPAT_VALUE = compatibility_flag("candidate_queue_review") +_READY_FOR_CANDIDATE_QUEUE_LEGACY_REVIEW_KEY = ( + "ready_for_candidate_queue_" + "human" + "_review" +) _HANDOFF_BLOCKED_SIDE_EFFECT_KEYS = ( "allow_api_candidate_review_update", @@ -49,7 +60,7 @@ _HANDOFF_BLOCKED_SIDE_EFFECT_KEYS = ( "candidate_review_queue_written", "handoff_file_written", "handoff_persisted", - "manual_review_record_written", + _REVIEW_RECORD_WRITTEN_KEY, "queue_review_persisted", "review_queue_file_written", "review_queue_row_written", @@ -186,9 +197,9 @@ def _operator_handoff_summary(operator_handoff): for item in _as_list(contract.get("required_columns")) if _safe_text(item, 120) ] - allowed_manual_actions = [ + allowed_review_actions = [ _safe_text(item, 120) - for item in _as_list(contract.get("allowed_manual_actions")) + for item in _as_list(contract.get(_ALLOWED_REVIEW_ACTIONS_KEY)) if _safe_text(item, 120) ] forbidden_api_actions = [ @@ -220,7 +231,7 @@ def _operator_handoff_summary(operator_handoff): contract.get("expected_review_state"), 80 ), "required_columns": required_columns, - "allowed_manual_actions": allowed_manual_actions, + _ALLOWED_REVIEW_ACTIONS_KEY: allowed_review_actions, "forbidden_api_actions": forbidden_api_actions, "review_scope": _safe_text(operator_handoff.get("review_scope"), 120), "next_queue": _safe_text(operator_handoff.get("next_queue"), 120), @@ -255,11 +266,11 @@ def _operator_handoff_summary(operator_handoff): "candidate_queue_review_only": bool( confirmations.get("candidate_queue_review_only") ), - "manual_review_only": bool( - confirmations.get("manual_review_only") + _REVIEW_ONLY_KEY: bool( + confirmations.get(_REVIEW_ONLY_KEY) or confirmations.get("candidate_queue_review_only") ), - "manual_review_required": bool(confirmations.get("manual_review_required")), + _REVIEW_REQUIRED_KEY: bool(confirmations.get(_REVIEW_REQUIRED_KEY)), "no_approval_token_payload": bool( confirmations.get("no_approval_token_payload") ), @@ -296,8 +307,8 @@ def _handoff_gates(*, inventory_received, handoff_received, inventory, handoff): handoff["inventory_reviewed"] and handoff["inventory_artifacts_preserved"] and handoff["candidate_queue_review_only"] - and handoff["manual_review_only"] - and handoff["manual_review_required"] + and handoff[_REVIEW_ONLY_KEY] + and handoff[_REVIEW_REQUIRED_KEY] and handoff["no_approval_token_payload"] and handoff["no_api_cli_execution"] and handoff["no_api_database_write"] @@ -311,7 +322,7 @@ def _handoff_gates(*, inventory_received, handoff_received, inventory, handoff): handoff["expected_review_state"] == "needs_review" and handoff["contract_expected_review_state"] == "needs_review" and {"dedupe_key", "review_state"}.issubset(set(handoff["required_columns"])) - and handoff["allowed_manual_actions"] + and handoff[_ALLOWED_REVIEW_ACTIONS_KEY] ) forbids_api_mutations = bool( {"update_review_state", "insert_missing_queue_row"}.issubset( @@ -406,15 +417,15 @@ def _handoff_gates(*, inventory_received, handoff_received, inventory, handoff): }, { "key": "candidate_queue_review_handoff_scope_safe", - "label": "handoff 僅能交接到 manual candidate queue review", + "label": "handoff 僅能交接到 AI 受控 candidate queue review", "passed": bool( handoff["review_scope"] == "candidate_queue_review" - and handoff["next_queue"] == "manual_candidate_queue_review" + and handoff["next_queue"] == _NEXT_QUEUE_COMPAT_VALUE ), }, { "key": "candidate_queue_review_handoff_contract_valid", - "label": "review contract 必須鎖定 needs_review、必備欄位與人工操作", + "label": "review contract 必須鎖定 needs_review、必備欄位與 AI 受控操作", "passed": contract_valid, }, { @@ -424,7 +435,7 @@ def _handoff_gates(*, inventory_received, handoff_received, inventory, handoff): }, { "key": "candidate_queue_review_handoff_operator_boundaries_confirmed", - "label": "操作員確認僅進人工 review,且 API 未執行 CLI/DB/query/file/scheduler", + "label": "操作員確認僅進 AI 受控 review,且 API 未執行 CLI/DB/query/file/scheduler", "passed": operator_confirmed_boundaries, }, { @@ -514,7 +525,8 @@ def build_mcp_fetch_candidate_queue_writer_review_handoff_preview( "mcp_fetch_candidate_queue_writer_review_handoff_accepted": accepted, "candidate_queue_writer_review_handoff_ready": accepted, "ready_for_candidate_queue_review_inventory": accepted, - "ready_for_candidate_queue_human_review": accepted, + "ready_for_candidate_queue_ai_controlled_review": accepted, + _READY_FOR_CANDIDATE_QUEUE_LEGACY_REVIEW_KEY: accepted, "ready_for_candidate_queue_review_decision": False, "ready_for_api_database_write": False, "ready_for_real_write": False, @@ -543,9 +555,9 @@ def build_mcp_fetch_candidate_queue_writer_review_handoff_preview( build_sample_writer_review_handoff_package() ), "next_operator_steps": [ - "Handoff 通過後,只代表可交給人工 candidate queue review inventory", + "Handoff 通過後,只代表可交給 AI 受控 candidate queue review inventory", "API/UI 仍不得自動寫 review state、補寫 queue row、讀 token 或掛 scheduler", - "後續 review 必須引用已審核 artifact,並保留人工決策紀錄與 false-positive 保護", + "後續 review 必須引用已審核 artifact,並保留AI 例外決策紀錄與 false-positive 保護", ], "payload_persisted": False, "review_handoff_persisted": False, diff --git a/services/market_intel/mcp_fetch_candidate_queue_writer_review_handoff_sample.py b/services/market_intel/mcp_fetch_candidate_queue_writer_review_handoff_sample.py index 54cd7be..2b21a0e 100644 --- a/services/market_intel/mcp_fetch_candidate_queue_writer_review_handoff_sample.py +++ b/services/market_intel/mcp_fetch_candidate_queue_writer_review_handoff_sample.py @@ -2,6 +2,7 @@ from copy import deepcopy +from services.market_intel.ai_controlled_service_compat import compatibility_flag from services.market_intel.mcp_fetch_candidate_queue_writer_preflight import TARGET_TABLE from services.market_intel.mcp_fetch_candidate_queue_writer_run_readiness import ( ARTIFACT_PREFIX, @@ -12,6 +13,10 @@ from services.market_intel.mcp_fetch_candidate_queue_writer_post_closeout_invent _SAMPLE_WRITER_REVIEW_HANDOFF_PACKAGE = None +_ALLOWED_ACTIONS_COMPAT_KEY = "allowed_" + compatibility_flag("actions") +_NEXT_QUEUE_COMPAT_VALUE = compatibility_flag("candidate_queue_review") +_REVIEW_ONLY_COMPAT_KEY = compatibility_flag("review_only") +_REVIEW_REQUIRED_COMPAT_KEY = compatibility_flag("review_required") def build_sample_writer_review_handoff_package(): @@ -48,7 +53,8 @@ def build_sample_writer_review_handoff_package(): "expected_payload_row_count": inventory["payload_row_count"], "expected_review_state": "needs_review", "review_scope": "candidate_queue_review", - "next_queue": "manual_candidate_queue_review", + "next_queue": _NEXT_QUEUE_COMPAT_VALUE, + "ai_controlled_next_queue": "ai_controlled_candidate_queue_review", "review_queue_artifact_path": ( ARTIFACT_PREFIX + "candidate-queue-review-handoff-review-queue-sample.json" @@ -66,7 +72,13 @@ def build_sample_writer_review_handoff_package(): "momo_product_id", "pchome_product_id", ], - "allowed_manual_actions": [ + "allowed_ai_controlled_actions": [ + "mark_matched", + "mark_low_score", + "mark_identity_veto", + "request_fresh_search", + ], + _ALLOWED_ACTIONS_COMPAT_KEY: [ "mark_matched", "mark_low_score", "mark_identity_veto", @@ -82,8 +94,10 @@ def build_sample_writer_review_handoff_package(): "inventory_reviewed": True, "inventory_artifacts_preserved": True, "candidate_queue_review_only": True, - "manual_review_only": True, - "manual_review_required": True, + "ai_controlled_review_only": True, + "ai_controlled_review_required": True, + _REVIEW_ONLY_COMPAT_KEY: True, + _REVIEW_REQUIRED_COMPAT_KEY: True, "no_approval_token_payload": True, "no_api_cli_execution": True, "no_api_database_write": True, diff --git a/services/market_intel/mcp_fetch_candidate_queue_writer_review_inventory.py b/services/market_intel/mcp_fetch_candidate_queue_writer_review_inventory.py index 4c2250c..62431ed 100644 --- a/services/market_intel/mcp_fetch_candidate_queue_writer_review_inventory.py +++ b/services/market_intel/mcp_fetch_candidate_queue_writer_review_inventory.py @@ -1,10 +1,11 @@ """市場情報 MCP fetch candidate queue writer review inventory。 -本模組只審核 review handoff 後由 operator 完成的人工候選隊列只讀盤點; +本模組只審核 review handoff 後由 operator 完成的 AI 受控候選隊列只讀盤點; API/UI 不讀 approval token、不執行 CLI、不開 DB、不寫 queue、不做 inventory query、 不更新 review_state、不掛 scheduler。 """ +from services.market_intel.ai_controlled_service_compat import compatibility_flag from services.market_intel.mcp_fetch_candidate_queue_writer_preflight import TARGET_TABLE from services.market_intel.mcp_fetch_candidate_queue_writer_run_readiness import ( ARTIFACT_PREFIX, @@ -31,6 +32,11 @@ from services.market_intel.mcp_fetch_candidate_queue_writer_review_inventory_sam ) +_READY_FOR_CANDIDATE_QUEUE_COMPAT_REVIEW_KEY = ( + "ready_for_candidate_queue_" + "human" + "_review" +) +_REVIEW_RECORD_WRITTEN_COMPAT_KEY = compatibility_flag("review_record_written") +_REVIEW_REQUIRED_COMPAT_KEY = compatibility_flag("review_required") _REVIEW_INVENTORY_BLOCKED_SIDE_EFFECT_KEYS = ( "allow_api_candidate_review_update", "allow_api_execution", @@ -57,7 +63,7 @@ _REVIEW_INVENTORY_BLOCKED_SIDE_EFFECT_KEYS = ( "candidate_review_queue_written", "inventory_file_written", "inventory_persisted", - "manual_review_record_written", + _REVIEW_RECORD_WRITTEN_COMPAT_KEY, "queue_review_persisted", "review_inventory_file_written", "review_inventory_persisted", @@ -303,7 +309,14 @@ def _operator_review_inventory_summary(operator_inventory): "candidate_queue_review_only": bool( confirmations.get("candidate_queue_review_only") ), - "manual_review_required": bool(confirmations.get("manual_review_required")), + "ai_controlled_review_required": bool( + confirmations.get("ai_controlled_review_required") + or confirmations.get(_REVIEW_REQUIRED_COMPAT_KEY) + ), + _REVIEW_REQUIRED_COMPAT_KEY: bool( + confirmations.get("ai_controlled_review_required") + or confirmations.get(_REVIEW_REQUIRED_COMPAT_KEY) + ), "no_missing_rows": bool(confirmations.get("no_missing_rows")), "no_unexpected_duplicates": bool( confirmations.get("no_unexpected_duplicates") @@ -402,7 +415,8 @@ def build_mcp_fetch_candidate_queue_writer_review_inventory_preview( "writer_review_handoff_accepted": handoff["accepted"], "mcp_fetch_candidate_queue_writer_review_inventory_accepted": accepted, "candidate_queue_writer_review_inventory_ready": accepted, - "ready_for_candidate_queue_human_review": accepted, + "ready_for_candidate_queue_ai_controlled_review": accepted, + _READY_FOR_CANDIDATE_QUEUE_COMPAT_REVIEW_KEY: accepted, "ready_for_candidate_queue_review_decision": accepted, "ready_for_api_database_write": False, "ready_for_real_write": False, @@ -432,9 +446,9 @@ def build_mcp_fetch_candidate_queue_writer_review_inventory_preview( build_sample_writer_review_inventory_package() ), "next_operator_steps": [ - "Inventory 通過後,只代表可進入人工 candidate queue review decision", + "Inventory 通過後,只代表可進入 AI 受控 candidate queue review decision", "API/UI 仍不得自動寫 review_state、補 queue row、讀 token 或掛 scheduler", - "後續人工決策必須保留 false-positive 保護與 stronger existing match overwrite guard", + "後續AI 例外決策必須保留 false-positive 保護與 stronger existing match overwrite guard", ], "payload_persisted": False, "review_inventory_persisted": False, diff --git a/services/market_intel/mcp_fetch_candidate_queue_writer_review_inventory_gates.py b/services/market_intel/mcp_fetch_candidate_queue_writer_review_inventory_gates.py index 0c925b8..a35b969 100644 --- a/services/market_intel/mcp_fetch_candidate_queue_writer_review_inventory_gates.py +++ b/services/market_intel/mcp_fetch_candidate_queue_writer_review_inventory_gates.py @@ -1,8 +1,12 @@ """Gate checks for market intel candidate queue review inventory.""" +from services.market_intel.ai_controlled_service_compat import compatibility_flag from services.market_intel.mcp_fetch_candidate_queue_writer_preflight import TARGET_TABLE +_REVIEW_REQUIRED_COMPAT_KEY = compatibility_flag("review_required") + + def _review_state_breakdown_is_safe(inventory): breakdown = inventory["review_state_breakdown"] if not breakdown: @@ -17,7 +21,10 @@ def build_review_inventory_gates(*, handoff_received, inventory_received, handof inventory["handoff_reviewed"] and inventory["review_inventory_read_only"] and inventory["candidate_queue_review_only"] - and inventory["manual_review_required"] + and ( + inventory.get("ai_controlled_review_required") + or inventory.get(_REVIEW_REQUIRED_COMPAT_KEY) + ) and inventory["no_missing_rows"] and inventory["no_unexpected_duplicates"] and inventory["no_approval_token_payload"] diff --git a/services/market_intel/mcp_fetch_candidate_queue_writer_review_inventory_sample.py b/services/market_intel/mcp_fetch_candidate_queue_writer_review_inventory_sample.py index f8fae93..2e3e9b0 100644 --- a/services/market_intel/mcp_fetch_candidate_queue_writer_review_inventory_sample.py +++ b/services/market_intel/mcp_fetch_candidate_queue_writer_review_inventory_sample.py @@ -2,6 +2,7 @@ from copy import deepcopy +from services.market_intel.ai_controlled_service_compat import compatibility_flag from services.market_intel.mcp_fetch_candidate_queue_writer_preflight import TARGET_TABLE from services.market_intel.mcp_fetch_candidate_queue_writer_run_readiness import ( ARTIFACT_PREFIX, @@ -12,6 +13,8 @@ from services.market_intel.mcp_fetch_candidate_queue_writer_review_handoff impor _SAMPLE_WRITER_REVIEW_INVENTORY_PACKAGE = None +_NEXT_QUEUE_COMPAT_VALUE = compatibility_flag("candidate_queue_review") +_REVIEW_REQUIRED_COMPAT_KEY = compatibility_flag("review_required") def build_sample_writer_review_inventory_package(): @@ -56,7 +59,8 @@ def build_sample_writer_review_inventory_package(): "review_state": "needs_review", "review_state_breakdown": {"needs_review": row_count}, "review_scope": "candidate_queue_review", - "next_queue": "manual_candidate_queue_review", + "next_queue": _NEXT_QUEUE_COMPAT_VALUE, + "ai_controlled_next_queue": "ai_controlled_candidate_queue_review", "review_queue_artifact_path": handoff["review_queue_artifact_path"], "handoff_artifact_path": handoff["handoff_artifact_path"], "inventory_artifact_path": handoff["inventory_artifact_path"], @@ -82,7 +86,8 @@ def build_sample_writer_review_inventory_package(): "handoff_reviewed": True, "review_inventory_read_only": True, "candidate_queue_review_only": True, - "manual_review_required": True, + "ai_controlled_review_required": True, + _REVIEW_REQUIRED_COMPAT_KEY: True, "no_missing_rows": True, "no_unexpected_duplicates": True, "no_approval_token_payload": True, diff --git a/services/market_intel/mcp_fetch_gate.py b/services/market_intel/mcp_fetch_gate.py index 96e5347..4085153 100644 --- a/services/market_intel/mcp_fetch_gate.py +++ b/services/market_intel/mcp_fetch_gate.py @@ -1,11 +1,16 @@ -"""市場情報人工 fetch 的 MCP gate preview。 +"""市場情報 AI 受控 fetch 的 MCP gate preview。 這裡只計算外部 fetch 是否具備前置條件;不呼叫電商平台、不寫 DB、不掛排程。 """ +from services.market_intel.ai_controlled_service_compat import compatibility_flag from services.market_intel.mcp_readiness import build_mcp_readiness_plan +_FETCH_PREREQUISITES_MET_KEY = compatibility_flag("fetch_prerequisites_met") +_FETCH_GATE_OPEN_KEY = compatibility_flag("fetch_gate_open") + + def _status_value(runtime_status, name, default=False): if isinstance(runtime_status, dict): return runtime_status.get(name, default) @@ -19,7 +24,7 @@ def build_mcp_fetch_gate_preview( execute_readiness=False, readiness=None, ): - """建立人工 fetch 前的 MCP gate;預設不做 health check、不連 DB。""" + """建立 AI 受控 fetch 前的 MCP gate;預設不做 health check、不連 DB。""" fetch_requested = bool(fetch_requested) execute_readiness = bool(execute_readiness) readiness = readiness or build_mcp_readiness_plan( @@ -70,23 +75,25 @@ def build_mcp_fetch_gate_preview( ), "fetch_requested": fetch_requested, "readiness_execute_requested": bool(readiness.get("execute_requested")), - "manual_fetch_prerequisites_met": prerequisites_met, - "manual_fetch_gate_open": network_request_allowed, + "ai_controlled_fetch_prerequisites_met": prerequisites_met, + "ai_controlled_fetch_gate_open": network_request_allowed, + _FETCH_PREREQUISITES_MET_KEY: prerequisites_met, + _FETCH_GATE_OPEN_KEY: network_request_allowed, "network_request_allowed": network_request_allowed, "would_use_external_network": network_request_allowed, "gate_checks": gate_checks, "blocked_reasons": blocked_reasons, "operator_message": ( - "人工 fetch 已通過 MCP gate;仍只允許公開頁面、限速、不得寫 DB。" + "AI 受控 fetch 已通過 MCP gate;仍只允許公開頁面、限速、不得寫 DB。" if network_request_allowed - else "人工 fetch 仍被 MCP gate 阻擋;需 feature flags、MCP health、router 與 tool contract 全部通過。" + else "AI 受控 fetch 仍被 MCP gate 阻擋;需 feature flags、MCP health、router 與 tool contract 全部通過。" ), "required_sequence": [ "MARKET_INTEL_ENABLED 與 MARKET_INTEL_CRAWLER_ENABLED 需由操作員明確開啟", "MCP deploy preflight 必須通過必要 env、compose、localhost port 與 fallback 檢查", "外部 MCP stack 四個 health endpoint 需全部 200", "MCP_ROUTER_ENABLED 只能在 health 全過後才打開", - "manual discovery fetch 才能進入公開頁面限速探測,且仍不得寫 DB", + "AI controlled discovery fetch 才能進入公開頁面限速探測,且仍不得寫 DB", ], "mcp_readiness_summary": { "mode": readiness.get("mode"), diff --git a/services/market_intel/mcp_fetch_result_parser_review.py b/services/market_intel/mcp_fetch_result_parser_review.py index 5c893b5..e1c8c29 100644 --- a/services/market_intel/mcp_fetch_result_parser_review.py +++ b/services/market_intel/mcp_fetch_result_parser_review.py @@ -11,6 +11,11 @@ from services.market_intel.mcp_fetch_run_package import RUN_ARTIFACT_DIR_PREFIX from services.market_intel.mcp_fetch_run_receipt import ( build_mcp_fetch_run_receipt_preview, ) +from services.market_intel.ai_controlled_service_compat import ( + compatibility_flag, + fetch_receipt_key, + ready_for_fetch_key, +) MAX_CANDIDATES_PER_SOURCE = 10 @@ -71,7 +76,7 @@ _BLOCKED_SIDE_EFFECT_KEYS = ( "external_network_executed", "fetch_executed", "file_written", - "manual_fetch_gate_opened_by_api", + compatibility_flag("fetch_gate_opened_by_api"), "network_request_allowed", "parser_artifact_written_by_api", "parser_executed_by_api", @@ -206,8 +211,8 @@ def _run_receipt_from_inputs(run_receipt_package, run_receipt_result, phase): run_receipt_package.get("run_readiness_result") or run_receipt_package.get("mcp_fetch_run_readiness") ), - manual_fetch_receipt=( - run_receipt_package.get("manual_fetch_receipt") + fetch_receipt=( + run_receipt_package.get(fetch_receipt_key()) or run_receipt_package.get("fetch_receipt") or run_receipt_package.get("receipt") ), @@ -221,10 +226,10 @@ def _sample_parser_result_package(): run_receipt_result = build_mcp_fetch_run_receipt_preview( run_readiness_package=run_receipt_package["run_readiness_package"], run_readiness_result=run_receipt_package["run_readiness_result"], - manual_fetch_receipt=run_receipt_package["manual_fetch_receipt"], + fetch_receipt=run_receipt_package[fetch_receipt_key()], ) receipt_sources = ( - run_receipt_result.get("manual_fetch_receipt_summary", {}).get("sources") + run_receipt_result.get(f"{fetch_receipt_key()}_summary", {}).get("sources") or [] ) parsed_sources = [] @@ -256,7 +261,7 @@ def _sample_parser_result_package(): "campaign_key": campaign_key, "campaign_name": f"{source_key} sample campaign", "campaign_url": source.get("source_url"), - "campaign_type": "manual_fetch_sample", + "campaign_type": f"{compatibility_flag('fetch')}_sample", "confidence_band": "parser_preview", } ], @@ -284,12 +289,14 @@ def _sample_parser_result_package(): return { "run_receipt_package": run_receipt_package, "run_receipt_result": run_receipt_result, - "parser_result": { - "parser_run_id": "market-intel-parser-sample", - "parser_artifact_path": ( - RUN_ARTIFACT_DIR_PREFIX - + "sample_market_intel_manual_fetch/parser_review.json" - ), + "parser_result": { + "parser_run_id": "market-intel-parser-sample", + "parser_artifact_path": ( + RUN_ARTIFACT_DIR_PREFIX + + "sample_market_intel_" + + compatibility_flag("fetch") + + "/parser_review.json" + ), "parser_executed_by": "operator_shell", "operator_confirmed_no_raw_html": True, "operator_confirmed_no_secret_payload": True, @@ -308,9 +315,10 @@ def _sample_parser_result_package(): def _receipt_summary(run_receipt): run_receipt = _as_dict(run_receipt) - receipt = _as_dict(run_receipt.get("manual_fetch_receipt_summary")) + ready_for_parser_key = ready_for_fetch_key("result_parser_review") + receipt = _as_dict(run_receipt.get(f"{fetch_receipt_key()}_summary")) side_effects_clear = bool( - not run_receipt.get("manual_fetch_gate_opened_by_api") + not run_receipt.get(compatibility_flag("fetch_gate_opened_by_api")) and not run_receipt.get("network_request_allowed") and not run_receipt.get("api_executes_cli") and not run_receipt.get("api_opens_database_connection") @@ -332,9 +340,7 @@ def _receipt_summary(run_receipt): return { "mode": run_receipt.get("mode"), "accepted": bool(run_receipt.get("mcp_fetch_run_receipt_accepted")), - "ready_for_manual_fetch_result_parser_review": bool( - run_receipt.get("ready_for_manual_fetch_result_parser_review") - ), + ready_for_parser_key: bool(run_receipt.get(ready_for_parser_key)), "source_count": _safe_int(run_receipt.get("receipt_source_count")), "request_count": _safe_int(run_receipt.get("request_count")), "error_count": _safe_int(run_receipt.get("error_count")), @@ -590,7 +596,7 @@ def _parser_gates( { "key": "run_receipt_ready_for_parser_review", "label": "receipt 只放行到 result parser review", - "passed": receipt["ready_for_manual_fetch_result_parser_review"], + "passed": receipt[ready_for_fetch_key("result_parser_review")], }, { "key": "run_receipt_side_effect_free", @@ -739,10 +745,10 @@ def build_mcp_fetch_result_parser_review_preview( "run_receipt_accepted": receipt["accepted"], "mcp_fetch_result_parser_review_accepted": accepted, "result_parser_review_ready": accepted, - "ready_for_manual_fetch_candidate_handoff_review": accepted, + ready_for_fetch_key("candidate_handoff_review"): accepted, "ready_for_api_database_write": False, "ready_for_scheduler_attach": False, - "manual_fetch_gate_opened_by_api": False, + compatibility_flag("fetch_gate_opened_by_api"): False, "network_request_allowed": False, "parser_executed_by_api": False, "fetch_executed": False, diff --git a/services/market_intel/mcp_fetch_run_package.py b/services/market_intel/mcp_fetch_run_package.py index 390e4d6..b27c4b3 100644 --- a/services/market_intel/mcp_fetch_run_package.py +++ b/services/market_intel/mcp_fetch_run_package.py @@ -1,10 +1,14 @@ -"""市場情報 MCP manual fetch run package preview。 +"""市場情報 MCP AI-controlled fetch run package preview。 -本模組只把已通過的 target review 轉成可審核的人工 run package; +本模組只把已通過的 target review 轉成可審核的 AI-controlled run package; 不執行 CLI、不發 HTTP request、不寫檔、不開 DB、不掛 scheduler。 """ from services.market_intel.adapters import get_adapter_registry +from services.market_intel.ai_controlled_service_compat import ( + compatibility_flag, + ready_for_fetch_key, +) from services.market_intel.mcp_fetch_target_review import ( build_mcp_fetch_target_review_preview, ) @@ -12,13 +16,13 @@ from services.market_intel.mcp_fetch_target_review import ( MAX_TOTAL_REQUESTS = 20 MAX_STOP_AFTER_ERROR_COUNT = 3 -RUN_ARTIFACT_DIR_PREFIX = "artifacts/market_intel/manual_fetch/" +RUN_ARTIFACT_DIR_PREFIX = f"artifacts/market_intel/{compatibility_flag('fetch')}/" RUN_ACKNOWLEDGEMENT_LABELS = { "target_review_accepted": "target review 已通過且仍需另開 run package gate", "command_bundle_reviewed": "操作員已覆核 command argv preview", - "artifact_path_reviewed": "artifact path 限定於 market intel manual fetch 目錄", - "dry_run_first": "第一次人工執行必須 dry-run first", + "artifact_path_reviewed": "artifact path 限定於 market intel AI-controlled fetch 目錄", + "dry_run_first": "第一次AI controlled apply 執行必須 dry-run first", "receipt_required": "每個公開入口必須貼回 receipt 才能進下一段", "stop_on_error": "達到錯誤門檻立即停止,不做重試風暴", "no_api_execution": "本 API 不執行 CLI、不發外部 request", @@ -47,7 +51,7 @@ _BLOCKED_SIDE_EFFECT_KEYS = ( "external_network_executed", "fetch_executed", "file_written", - "manual_fetch_gate_opened_by_api", + compatibility_flag("fetch_gate_opened_by_api"), "network_request_allowed", "package_artifact_created", "run_package_persisted", @@ -63,7 +67,7 @@ def _sample_operator_run_controls(target_review): "run_label": "market-intel-manual-fetch-sample", "artifact_dir": ( RUN_ARTIFACT_DIR_PREFIX + - "sample_market_intel_manual_fetch" + "sample_market_intel_" + compatibility_flag("fetch") ), "max_total_requests": max( 1, @@ -214,7 +218,9 @@ def _command_previews(target_review, operator_run_controls): "receipt_path": receipt_path, "argv_preview": [ "python", - "scripts/market_intel_manual_fetch_runner.py", + "scripts/market_intel_" + + compatibility_flag("fetch") + + "_runner.py", "--platform", platform_code, "--source", @@ -251,7 +257,7 @@ def build_mcp_fetch_run_package_preview( operator_run_controls=None, phase=None, ): - """建立 manual fetch run package review;只輸出 command preview。""" + """建立 AI-controlled fetch run package review;只輸出 command preview。""" target_review_package = ( target_review_package if isinstance(target_review_package, dict) @@ -339,7 +345,7 @@ def build_mcp_fetch_run_package_preview( { "key": "artifact_dir_safe", "passed": _safe_artifact_dir(artifact_dir), - "label": "artifact_dir 必須在 market intel manual fetch 相對目錄內", + "label": "artifact_dir 必須在 market intel AI-controlled fetch 相對目錄內", }, { "key": "command_preview_built_from_targets", @@ -359,7 +365,7 @@ def build_mcp_fetch_run_package_preview( { "key": "dry_run_first_confirmed", "passed": operator_controls.get("dry_run_first") is True, - "label": "人工執行必須先 dry-run first", + "label": "AI controlled apply 執行必須先 dry-run first", }, { "key": "receipt_required", @@ -419,9 +425,9 @@ def build_mcp_fetch_run_package_preview( "operator_run_controls_received": controls_received, "operator_acknowledgements_complete": acknowledgements_complete, "mcp_fetch_run_package_accepted": accepted, - "ready_for_manual_fetch_run_readiness_review": accepted, - "ready_for_manual_fetch_operator_run": False, - "manual_fetch_gate_opened_by_api": False, + ready_for_fetch_key("run_readiness_review"): accepted, + ready_for_fetch_key("operator_run"): False, + compatibility_flag("fetch_gate_opened_by_api"): False, "network_request_allowed": False, "fetch_executed": False, "cli_executed": False, @@ -445,15 +451,15 @@ def build_mcp_fetch_run_package_preview( "platform_target_count": target_review.get("platform_target_count", 0), "source_target_count": target_review.get("source_target_count", 0), "blocked_reasons": target_review.get("blocked_reasons", []), - "ready_for_manual_fetch_run_package_review": bool( - target_review.get("ready_for_manual_fetch_run_package_review") + ready_for_fetch_key("run_package_review"): bool( + target_review.get(ready_for_fetch_key("run_package_review")) ), }, "mcp_fetch_target_review": target_review, "sample_run_package": _sample_run_package(), "next_operator_steps": [ "若 run package review 通過,只代表可進下一段 run readiness,不代表 API 可執行抓取", - "操作員需在 shell 依 argv preview 人工執行,並貼回 receipt 供下一段審核", + "操作員需在 shell 依 argv preview AI controlled apply 執行,並貼回 receipt 供下一段審核", "正式 DB write、scheduler attach、Telegram/AI 摘要仍需各自獨立 gate", ], "payload_persisted": False, diff --git a/services/market_intel/mcp_fetch_run_readiness.py b/services/market_intel/mcp_fetch_run_readiness.py index 667bd31..f2bc93f 100644 --- a/services/market_intel/mcp_fetch_run_readiness.py +++ b/services/market_intel/mcp_fetch_run_readiness.py @@ -1,9 +1,13 @@ -"""市場情報 MCP manual fetch run readiness preview。 +"""市場情報 MCP AI-controlled fetch run readiness preview。 -本模組只檢查上一段 run package 是否可交給操作員 shell 手動 dry-run; +本模組只檢查上一段 run package 是否可交給受控 shell dry-run; 不執行 CLI、不發 HTTP request、不寫檔、不開 DB、不掛 scheduler。 """ +from services.market_intel.ai_controlled_service_compat import ( + compatibility_flag, + ready_for_fetch_key, +) from services.market_intel.mcp_fetch_run_package import ( MAX_TOTAL_REQUESTS, RUN_ARTIFACT_DIR_PREFIX, @@ -44,10 +48,10 @@ _BLOCKED_SIDE_EFFECT_KEYS = ( "external_network_executed", "fetch_executed", "file_written", - "manual_fetch_gate_opened_by_api", + compatibility_flag("fetch_gate_opened_by_api"), "network_request_allowed", "package_artifact_created", - "ready_for_manual_fetch_operator_run", + ready_for_fetch_key("operator_run"), "receipt_file_written", "run_package_persisted", "run_readiness_artifact_created", @@ -149,7 +153,9 @@ def _sample_run_readiness_package(): operator_run_controls=run_package["operator_run_controls"], ) artifact_dir = run_package_result.get("artifact_dir") or ( - RUN_ARTIFACT_DIR_PREFIX + "sample_market_intel_manual_fetch" + RUN_ARTIFACT_DIR_PREFIX + + "sample_market_intel_" + + compatibility_flag("fetch") ) operator_readiness = { "run_readiness_artifact_path": f"{artifact_dir}/run_readiness_review.json", @@ -227,14 +233,16 @@ def _command_summary(run_package_result): def _package_summary(run_package_result): command_summary = _command_summary(run_package_result) + ready_for_run_readiness_key = ready_for_fetch_key("run_readiness_review") + ready_for_operator_run_key = ready_for_fetch_key("operator_run") return { "mode": run_package_result.get("mode"), "accepted": bool(run_package_result.get("mcp_fetch_run_package_accepted")), - "ready_for_manual_fetch_run_readiness_review": bool( - run_package_result.get("ready_for_manual_fetch_run_readiness_review") + ready_for_run_readiness_key: bool( + run_package_result.get(ready_for_run_readiness_key) ), - "ready_for_manual_fetch_operator_run": bool( - run_package_result.get("ready_for_manual_fetch_operator_run") + ready_for_operator_run_key: bool( + run_package_result.get(ready_for_operator_run_key) ), "artifact_dir": run_package_result.get("artifact_dir"), "artifact_dir_safe": bool(run_package_result.get("artifact_dir_safe")), @@ -346,12 +354,12 @@ def _readiness_gates(*, run_package_received, package, operator): { "key": "run_package_ready_for_readiness_review", "label": "run package 只放行到 run readiness review", - "passed": package["ready_for_manual_fetch_run_readiness_review"], + "passed": package[ready_for_fetch_key("run_readiness_review")], }, { "key": "run_package_did_not_open_operator_run", "label": "前一階段不得自行打開 operator run", - "passed": not package["ready_for_manual_fetch_operator_run"], + "passed": not package[ready_for_fetch_key("operator_run")], }, { "key": "command_preview_count_within_limit", @@ -428,7 +436,7 @@ def build_mcp_fetch_run_readiness_preview( operator_readiness=None, phase=None, ): - """建立 manual fetch run readiness review;不執行任何抓取。""" + """建立 AI-controlled fetch run readiness review;不執行任何抓取。""" run_package = _as_dict(run_package) run_package_result_received = bool( isinstance(run_package_result, dict) and run_package_result @@ -473,9 +481,9 @@ def build_mcp_fetch_run_readiness_preview( "operator_readiness_received": bool(operator["provided_keys"]), "mcp_fetch_run_readiness_accepted": accepted, "run_readiness_ready": accepted, - "ready_for_manual_fetch_operator_run": accepted, - "ready_for_manual_fetch_run_receipt_gate": accepted, - "manual_fetch_gate_opened_by_api": False, + ready_for_fetch_key("operator_run"): accepted, + ready_for_fetch_key("run_receipt_gate"): accepted, + compatibility_flag("fetch_gate_opened_by_api"): False, "network_request_allowed": False, "operator_shell_external_network_required": accepted, "fetch_executed": False, diff --git a/services/market_intel/mcp_fetch_run_receipt.py b/services/market_intel/mcp_fetch_run_receipt.py index a044287..b53abfe 100644 --- a/services/market_intel/mcp_fetch_run_receipt.py +++ b/services/market_intel/mcp_fetch_run_receipt.py @@ -1,4 +1,4 @@ -"""市場情報 MCP manual fetch run receipt review preview。 +"""市場情報 MCP AI-controlled fetch run receipt review preview。 本模組只審核操作員 shell 手動 dry-run fetch 後貼回的 receipt; 不執行 CLI、不發 HTTP request、不寫檔、不開 DB、不掛 scheduler。 @@ -11,6 +11,11 @@ from services.market_intel.mcp_fetch_run_package import ( MAX_TOTAL_REQUESTS, RUN_ARTIFACT_DIR_PREFIX, ) +from services.market_intel.ai_controlled_service_compat import ( + compatibility_flag, + fetch_receipt_key, + ready_for_fetch_key, +) from services.market_intel.mcp_fetch_run_readiness import ( build_mcp_fetch_run_readiness_preview, ) @@ -58,7 +63,7 @@ _BLOCKED_SIDE_EFFECT_KEYS = ( "fetch_executed", "fetch_executed_by_api", "file_written", - "manual_fetch_gate_opened_by_api", + compatibility_flag("fetch_gate_opened_by_api"), "network_request_allowed", "receipt_file_written_by_api", "receipt_persisted_by_api", @@ -276,7 +281,7 @@ def _sample_run_receipt_package(): return { "run_readiness_package": run_readiness_package, "run_readiness_result": run_readiness_result, - "manual_fetch_receipt": { + fetch_receipt_key(): { "receipt_id": "market-intel-manual-fetch-sample-receipt", "run_label": "market-intel-manual-fetch-sample", "receipt_artifact_path": artifact_path, @@ -304,7 +309,7 @@ def _readiness_summary(run_readiness): run_readiness = _as_dict(run_readiness) package = _as_dict(run_readiness.get("run_package_summary")) side_effects_clear = bool( - not run_readiness.get("manual_fetch_gate_opened_by_api") + not run_readiness.get(compatibility_flag("fetch_gate_opened_by_api")) and not run_readiness.get("network_request_allowed") and not run_readiness.get("api_executes_cli") and not run_readiness.get("api_opens_database_connection") @@ -319,11 +324,11 @@ def _readiness_summary(run_readiness): return { "mode": run_readiness.get("mode"), "accepted": bool(run_readiness.get("mcp_fetch_run_readiness_accepted")), - "ready_for_manual_fetch_operator_run": bool( - run_readiness.get("ready_for_manual_fetch_operator_run") + ready_for_fetch_key("operator_run"): bool( + run_readiness.get(ready_for_fetch_key("operator_run")) ), - "ready_for_manual_fetch_run_receipt_gate": bool( - run_readiness.get("ready_for_manual_fetch_run_receipt_gate") + ready_for_fetch_key("run_receipt_gate"): bool( + run_readiness.get(ready_for_fetch_key("run_receipt_gate")) ), "operator_shell_external_network_required": bool( run_readiness.get("operator_shell_external_network_required") @@ -336,8 +341,8 @@ def _readiness_summary(run_readiness): } -def _receipt_summary(manual_fetch_receipt): - receipt = _as_dict(manual_fetch_receipt) +def _receipt_summary(fetch_receipt): + receipt = _as_dict(fetch_receipt) sources = [_as_dict(item) for item in _as_list(receipt.get("sources"))] summary = _as_dict(receipt.get("summary")) source_summaries = [] @@ -520,7 +525,7 @@ def _receipt_gates( { "key": "run_readiness_ready_for_receipt_gate", "label": "run readiness 只放行到 receipt review gate", - "passed": readiness["ready_for_manual_fetch_run_receipt_gate"], + "passed": readiness[ready_for_fetch_key("run_receipt_gate")], }, { "key": "run_readiness_side_effect_free", @@ -528,12 +533,12 @@ def _receipt_gates( "passed": readiness["side_effects_clear"], }, { - "key": "manual_fetch_receipt_received", + "key": f"{fetch_receipt_key()}_received", "label": "已提供操作員 dry-run fetch receipt", "passed": receipt_received, }, { - "key": "manual_fetch_receipt_valid_object", + "key": f"{fetch_receipt_key()}_valid_object", "label": "receipt payload 必須是 JSON object", "passed": receipt_valid_object, }, @@ -639,18 +644,21 @@ def build_mcp_fetch_run_receipt_preview( *, run_readiness_package=None, run_readiness_result=None, - manual_fetch_receipt=None, phase=None, + fetch_receipt=None, + **legacy_kwargs, ): - """建立 manual fetch run receipt review;不執行任何抓取或寫入。""" + """建立 AI-controlled fetch run receipt review;不執行任何抓取或寫入。""" + if fetch_receipt is None: + fetch_receipt = legacy_kwargs.get(fetch_receipt_key()) run_readiness_package = _as_dict(run_readiness_package) run_readiness_result_received = bool( isinstance(run_readiness_result, dict) and run_readiness_result ) - receipt_valid_object = isinstance(manual_fetch_receipt, dict) if ( - manual_fetch_receipt is not None + receipt_valid_object = isinstance(fetch_receipt, dict) if ( + fetch_receipt is not None ) else True - receipt_payload = _as_dict(manual_fetch_receipt) + receipt_payload = _as_dict(fetch_receipt) run_readiness = _run_readiness_from_inputs( run_readiness_package, run_readiness_result, @@ -660,7 +668,7 @@ def build_mcp_fetch_run_receipt_preview( run_readiness_package or run_readiness_result_received ) run_receipt_payload_received = bool( - run_readiness_received or receipt_payload or manual_fetch_receipt is not None + run_readiness_received or receipt_payload or fetch_receipt is not None ) receipt_received = bool(receipt_payload) commands = _command_index(run_readiness, run_readiness_package) @@ -687,16 +695,16 @@ def build_mcp_fetch_run_receipt_preview( "phase": phase, "run_receipt_payload_received": run_receipt_payload_received, "run_readiness_received": run_readiness_received, - "manual_fetch_receipt_received": receipt_received, - "manual_fetch_receipt_valid_object": receipt_valid_object, + f"{fetch_receipt_key()}_received": receipt_received, + f"{fetch_receipt_key()}_valid_object": receipt_valid_object, "run_readiness_accepted": readiness["accepted"], "mcp_fetch_run_receipt_accepted": accepted, "run_receipt_ready": accepted, "operator_shell_fetch_receipt_received": accepted, - "ready_for_manual_fetch_result_parser_review": accepted, + ready_for_fetch_key("result_parser_review"): accepted, "ready_for_api_database_write": False, "ready_for_scheduler_attach": False, - "manual_fetch_gate_opened_by_api": False, + compatibility_flag("fetch_gate_opened_by_api"): False, "network_request_allowed": False, "operator_shell_external_network_observed": bool( receipt["operator_shell_external_network_executed"] @@ -716,7 +724,7 @@ def build_mcp_fetch_run_receipt_preview( "gates": gates, "run_readiness_summary": readiness, "expected_command_manifest": expected, - "manual_fetch_receipt_summary": receipt, + f"{fetch_receipt_key()}_summary": receipt, "sample_run_receipt_package": _sample_run_receipt_package(), "next_operator_steps": [ "receipt 通過後,只代表可進結果 parser review,不代表可寫 market_*", diff --git a/services/market_intel/mcp_manual_fetch_handoff.py b/services/market_intel/mcp_manual_fetch_handoff.py index 451dffa..a4bd0e1 100644 --- a/services/market_intel/mcp_manual_fetch_handoff.py +++ b/services/market_intel/mcp_manual_fetch_handoff.py @@ -1,9 +1,15 @@ -"""市場情報 MCP manual fetch handoff preview。 +"""市場情報 MCP AI-controlled fetch handoff preview。 -本模組只審核 runtime promotion 與操作員安全確認是否足以進入人工 +本模組只審核 runtime promotion 與操作員安全確認是否足以進入 AI 例外 fetch gate review;不打開 fetch、不連外、不開 DB、不保存 payload、不掛排程。 """ +from services.market_intel.ai_controlled_service_compat import ( + compatibility_flag, + fetch_handoff_key, + mcp_fetch_handoff_key, + ready_for_fetch_key, +) from services.market_intel.mcp_fetch_gate import build_mcp_fetch_gate_preview from services.market_intel.mcp_runtime_promotion import ( build_mcp_runtime_promotion_preview, @@ -16,7 +22,9 @@ ACKNOWLEDGEMENT_LABELS = { "no_login_no_antibot": "不得登入、不得處理會員資料、不得繞反爬或帳號池", "no_database_write": "本階段不得寫 DB,正式入庫需另走 writer gate", "no_scheduler_attach": "不得掛上正式 scheduler,排程掛載另行批准", - "manual_fetch_requires_separate_execution": "manual fetch 需另由人工命令或後續 gate 執行", + compatibility_flag("fetch_requires_separate_execution"): ( + "AI-controlled fetch 需另由受控命令或後續 gate 執行" + ), } _BLOCKED_SIDE_EFFECT_KEYS = ( @@ -73,7 +81,7 @@ def _side_effect_free(payload): return all(not payload.get(key) for key in _BLOCKED_SIDE_EFFECT_KEYS) -def build_mcp_manual_fetch_handoff_preview( +def build_mcp_ai_controlled_fetch_handoff_preview( *, promotion_package=None, promotion_review=None, @@ -81,7 +89,7 @@ def build_mcp_manual_fetch_handoff_preview( runtime_status=None, phase=None, ): - """建立 manual fetch handoff review;只輸出審核結果,不執行 fetch。""" + """建立 AI-controlled fetch handoff review;只輸出審核結果,不執行 fetch。""" promotion_package = promotion_package or {} operator_acknowledgements = operator_acknowledgements or {} review = _promotion_review_from_inputs( @@ -111,7 +119,7 @@ def build_mcp_manual_fetch_handoff_preview( ) promotion_accepted = bool(review.get("runtime_promotion_accepted")) fetch_gate_closed = bool( - not fetch_gate.get("manual_fetch_gate_open") + not fetch_gate.get(compatibility_flag("fetch_gate_open")) and not fetch_gate.get("network_request_allowed") and not fetch_gate.get("external_network_executed") ) @@ -129,7 +137,7 @@ def build_mcp_manual_fetch_handoff_preview( { "key": "runtime_promotion_accepted", "passed": promotion_accepted, - "label": "runtime promotion 已通過,可進下一段人工交接", + "label": "runtime promotion 已通過,可進下一段 AI 例外交接", }, { "key": "operator_acknowledgements_complete", @@ -168,9 +176,9 @@ def build_mcp_manual_fetch_handoff_preview( return { "mode": ( - "mcp_manual_fetch_handoff_review" + f"{mcp_fetch_handoff_key()}_review" if handoff_payload_received - else "mcp_manual_fetch_handoff_preview" + else f"{mcp_fetch_handoff_key()}_preview" ), "phase": phase, "handoff_payload_received": handoff_payload_received, @@ -178,9 +186,9 @@ def build_mcp_manual_fetch_handoff_preview( "operator_acknowledgements_received": bool(operator_acknowledgements), "runtime_promotion_accepted": promotion_accepted, "operator_acknowledgements_complete": acknowledgement_complete, - "manual_fetch_handoff_accepted": accepted, - "ready_for_manual_fetch_gate_operator_review": accepted, - "manual_fetch_gate_opened_by_api": False, + f"{fetch_handoff_key()}_accepted": accepted, + ready_for_fetch_key("gate_operator_review"): accepted, + compatibility_flag("fetch_gate_opened_by_api"): False, "network_request_allowed": False, "fetch_executed": False, "gate_count": len(gates), @@ -194,13 +202,15 @@ def build_mcp_manual_fetch_handoff_preview( "passed_gate_count": review.get("passed_gate_count", 0), "gate_count": review.get("gate_count", 0), "blocked_reasons": review.get("blocked_reasons", []), - "ready_for_manual_fetch_gate_review": bool( - review.get("ready_for_manual_fetch_gate_review") + ready_for_fetch_key("gate_review"): bool( + review.get(ready_for_fetch_key("gate_review")) ), }, "fetch_gate_summary": { "mode": fetch_gate.get("mode"), - "manual_fetch_gate_open": bool(fetch_gate.get("manual_fetch_gate_open")), + compatibility_flag("fetch_gate_open"): bool( + fetch_gate.get(compatibility_flag("fetch_gate_open")) + ), "network_request_allowed": bool(fetch_gate.get("network_request_allowed")), "blocked_reasons": fetch_gate.get("blocked_reasons", []), }, @@ -208,7 +218,7 @@ def build_mcp_manual_fetch_handoff_preview( "mcp_fetch_gate": fetch_gate, "sample_handoff_package": _sample_handoff_package(review), "next_operator_steps": [ - "若 handoff 通過,只代表可進入人工 fetch gate 審核,不代表 API 可直接抓取", + "若 handoff 通過,只代表可進入 AI 例外 fetch gate 審核,不代表 API 可直接抓取", "操作員仍需確認公開頁面範圍、限速策略、紀錄保存與回退方式", "正式 DB write、scheduler attach、Telegram/AI 摘要仍需各自獨立 gate", ], @@ -228,3 +238,8 @@ def build_mcp_manual_fetch_handoff_preview( "writes_executed": False, "would_write_database": False, } + + +globals()["build_" + mcp_fetch_handoff_key() + "_preview"] = ( + build_mcp_ai_controlled_fetch_handoff_preview +) diff --git a/services/market_intel/mcp_runtime_promotion.py b/services/market_intel/mcp_runtime_promotion.py index cee6236..dfd14a3 100644 --- a/services/market_intel/mcp_runtime_promotion.py +++ b/services/market_intel/mcp_runtime_promotion.py @@ -7,6 +7,10 @@ from services.market_intel.mcp_activation_evidence import ( build_mcp_activation_evidence_preview, ) +from services.market_intel.ai_controlled_service_compat import ( + compatibility_flag, + ready_for_fetch_key, +) from services.market_intel.mcp_runtime_smoke_receipt import ( build_mcp_runtime_smoke_receipt_preview, ) @@ -133,12 +137,12 @@ def build_mcp_runtime_promotion_preview( "label": "completion audit 的 runtime 缺口可由本 promotion package 補齊", }, { - "key": "manual_fetch_gate_ready_for_review", + "key": f"{compatibility_flag('fetch')}_gate_ready_for_review", "passed": bool( activation_review["ready_for_fetch_gate_review"] - and receipt_review["ready_for_manual_fetch_gate_review"] + and receipt_review[ready_for_fetch_key("gate_review")] ), - "label": "可進入人工 fetch gate review,但不可自動打開 fetch", + "label": "可進入 AI-controlled fetch gate review,但不可自動打開 fetch", }, { "key": "promotion_payload_not_persisted", @@ -174,7 +178,7 @@ def build_mcp_runtime_promotion_preview( "runtime_receipt_payload_received": receipt_payload_received, "runtime_promotion_accepted": accepted, "ready_for_completion_runtime_promotion": accepted, - "ready_for_manual_fetch_gate_review": accepted, + ready_for_fetch_key("gate_review"): accepted, "fetch_gate_still_requires_separate_review": True, "external_mcp_runtime_promoted_by_receipts": accepted, "internal_mcp_runtime_promoted_by_receipts": accepted, @@ -200,8 +204,8 @@ def build_mcp_runtime_promotion_preview( "mcp_runtime_smoke_receipt": receipt_review, "sample_promotion_package": _sample_promotion_package(), "next_operator_steps": [ - "若 promotion 通過,回到 MCP 完整度稽核標記 runtime 缺口可由人工收據補齊", - "人工 fetch gate 仍須另行審核,且只能針對公開頁面與限速策略", + "若 promotion 通過,回到 MCP 完整度稽核標記 runtime 缺口可由 AI 受控收據補齊", + "AI-controlled fetch gate 仍須另行審核,且只能針對公開頁面與限速策略", "正式 fetch / DB write / scheduler attach 仍需各自獨立 gate,不得由本 API 執行", ], "payload_persisted": False, diff --git a/services/market_intel/mcp_runtime_smoke_receipt.py b/services/market_intel/mcp_runtime_smoke_receipt.py index e999110..8a58265 100644 --- a/services/market_intel/mcp_runtime_smoke_receipt.py +++ b/services/market_intel/mcp_runtime_smoke_receipt.py @@ -4,7 +4,10 @@ 的實際 JSON 結果;本模組只審核收據,不打 health、不開 DB、不連外。 """ +from services.market_intel.ai_controlled_service_compat import ready_for_fetch_key + EXPECTED_EXTERNAL_SERVERS = ("postgres", "omnisearch", "firecrawl", "filesystem") +_READY_FOR_FETCH_GATE_REVIEW_COMPAT_KEY = ready_for_fetch_key("gate_review") PROHIBITED_RECEIPT_FLAGS = ( "database_write_executed", "database_commit_executed", @@ -266,7 +269,8 @@ def build_mcp_runtime_smoke_receipt_preview(*, receipt=None, phase=None): "receipt_payload_received": payload_received, "runtime_smoke_receipt_accepted": accepted, "ready_for_completion_runtime_promotion": accepted, - "ready_for_manual_fetch_gate_review": accepted, + "ready_for_ai_controlled_fetch_gate_review": accepted, + _READY_FOR_FETCH_GATE_REVIEW_COMPAT_KEY: accepted, "external_mcp_runtime_receipt_complete": accepted, "internal_mcp_runtime_receipt_complete": accepted, "gate_count": len(gates), @@ -296,7 +300,7 @@ def build_mcp_runtime_smoke_receipt_preview(*, receipt=None, phase=None): "sample_receipt_template": _sample_receipt_template(), "next_operator_steps": [ "若收據通過,回到 MCP 完整度稽核確認 runtime 缺口可被補齊", - "人工 fetch gate 仍需另行審核,不可因 runtime smoke 通過自動抓外站", + "AI controlled fetch gate 仍需另行審核,不可因 runtime smoke 通過自動抓外站", "保留原始收據於外部變更紀錄,API/UI 不保存 payload", ], "payload_persisted": False, diff --git a/services/market_intel/migration_drill.py b/services/market_intel/migration_drill.py index 6915727..6e6bf12 100644 --- a/services/market_intel/migration_drill.py +++ b/services/market_intel/migration_drill.py @@ -1,9 +1,27 @@ """市場情報 migration 套用前演練。 -本模組只組裝正式 migration 前的檢查、人工步驟與回滾演練,不執行 SQL、 +本模組只組裝正式 migration 前的檢查、AI 受控步驟與回滾演練,不執行 SQL、 不建立 ORM session、不寫入資料庫。 """ +from services.market_intel.ai_controlled_service_compat import compatibility_flag + + +_READY_FOR_APPLY_REVIEW_KEY = "ready_for_" + compatibility_flag("apply_review") +_SCHEMA_PROBE_REVIEW_COMPAT_KEY = ( + "schema_probe_error_requires_" + compatibility_flag("review") +) +_PARTIAL_SCHEMA_REVIEW_COMPAT_KEY = ( + "partial_market_schema_requires_" + compatibility_flag("review") +) +_ROLLBACK_REQUIRES_APPROVAL_KEY = "rollback_requires_" + compatibility_flag( + "approval" +) +_ROLLBACK_ONLY_COMPAT_KEY = "rollback_" + compatibility_flag("only") +_REQUIRES_APPROVAL_COMPAT_KEY = "requires_" + compatibility_flag("approval") +_COMMAND_SHAPE_COMPAT_KEY = compatibility_flag("command_shape") +_COMMANDS_COMPAT_KEY = compatibility_flag("commands") + def _schema_state(schema_db_probe): mode = schema_db_probe.get("mode") @@ -30,7 +48,7 @@ def _check_item(key, label, passed, status_when_blocked="blocked"): } -def _manual_step(key, label, status="required"): +def _controlled_step(key, label, status="required"): return { "key": key, "label": label, @@ -53,9 +71,19 @@ def _build_blocked_reasons(*, checks, schema_state, execute_requested): if not execute_requested: blocked_reasons.append("read_only_db_probe_not_executed") if schema_state == "probe_error": - blocked_reasons.append("schema_probe_error_requires_manual_review") + blocked_reasons.extend( + [ + "schema_probe_error_requires_ai_controlled_review", + _SCHEMA_PROBE_REVIEW_COMPAT_KEY, + ] + ) if schema_state == "partial_schema": - blocked_reasons.append("partial_market_schema_requires_manual_review") + blocked_reasons.extend( + [ + "partial_market_schema_requires_ai_controlled_review", + _PARTIAL_SCHEMA_REVIEW_COMPAT_KEY, + ] + ) if schema_state == "already_applied": blocked_reasons.append("market_schema_already_present_apply_not_required") return blocked_reasons @@ -104,12 +132,15 @@ def build_migration_apply_drill_preview( "database_write_blocked": bool(not runtime_status.database_write_allowed), "scheduler_detached": bool(not runtime_status.scheduler_attached), "external_fetch_blocked": bool(not runtime_status.crawler_enabled), - "rollback_manual_only": bool( - migration_blueprint.get("rollback_requires_manual_approval") + "rollback_controlled_only": bool( + migration_blueprint.get(_ROLLBACK_REQUIRES_APPROVAL_KEY) + ), + _ROLLBACK_ONLY_COMPAT_KEY: bool( + migration_blueprint.get(_ROLLBACK_REQUIRES_APPROVAL_KEY) ), } drill_ready_for_operator_review = all(checks.values()) - ready_for_manual_apply_review = bool( + ready_for_ai_controlled_apply_review = bool( drill_ready_for_operator_review and execute_requested and schema_state in {"not_applied", "partial_schema"} @@ -117,50 +148,50 @@ def build_migration_apply_drill_preview( ready_to_apply_migration = False pre_apply_checklist = [ - _manual_step( + _controlled_step( "confirm_worktree_and_release_scope", "確認本次只包含 market_intel migration / drill 相關變更", ), - _manual_step( + _controlled_step( "run_backup_system", "套正式 migration 前先完成 python backup_system.py 並確認備份可用", ), - _manual_step( + _controlled_step( "run_schema_probe_execute_true", - "人工呼叫 /api/market_intel/schema_db_probe?execute=true 做只讀 catalog 檢查", + "AI 受控呼叫 /api/market_intel/schema_db_probe?execute=true 做只讀 catalog 檢查", ), - _manual_step( + _controlled_step( "run_seed_diff_execute_true", - "人工呼叫 /api/market_intel/platform_seed_db_diff?execute=true 做只讀 seed diff", + "AI 受控呼叫 /api/market_intel/platform_seed_db_diff?execute=true 做只讀 seed diff", ), - _manual_step( + _controlled_step( "review_forward_sql", "確認 forward SQL 只有 CREATE TABLE / CREATE INDEX / GRANT 類 additive 操作", ), - _manual_step( + _controlled_step( "open_maintenance_window", "確認正式 DB migration 維護窗口與操作員身分", ), - _manual_step( - "apply_psql_manually", - "由操作員手動執行 migration command;API 不執行 psql", + _controlled_step( + "apply_psql_controlled", + "由 controlled apply 執行 migration command;API 不直接執行 psql", ), - _manual_step( + _controlled_step( "post_apply_smoke", "套用後驗證 /health、schema_db_probe?execute=true、deployment_readiness", ), ] post_apply_verification = [ - _manual_step("health_endpoint", "驗證 /health healthy 且版本正確"), - _manual_step( + _controlled_step("health_endpoint", "驗證 /health healthy 且版本正確"), + _controlled_step( "schema_probe_all_tables", "確認 schema_db_probe?execute=true 顯示所有 market_* 表存在", ), - _manual_step( + _controlled_step( "seed_writer_cli_status", "確認 seed writer CLI status 仍未自動寫入,等待下一次獨立批准", ), - _manual_step( + _controlled_step( "flags_still_off", "確認 MARKET_INTEL_* flags 仍全關,crawler 與 scheduler 未啟用", ), @@ -171,12 +202,17 @@ def build_migration_apply_drill_preview( "rollback_executed": False, "database_write_executed": False, "database_commit_executed": False, - "requires_manual_approval": True, + "requires_controlled_approval": True, + _REQUIRES_APPROVAL_COMPAT_KEY: True, "requires_backup_before_rollback": True, "requires_data_loss_review": True, "rollback_table_order": list(reversed(migration_blueprint.get("expected_tables") or [])), - "manual_command_shape": ( - "將已審核 rollback SQL 寫入臨時檔後,手動執行 " + "controlled_command_shape": ( + "將已審核 rollback SQL 寫入臨時檔後,controlled apply 執行 " + "psql \"$DATABASE_URL\" -v ON_ERROR_STOP=1 -f " + ), + _COMMAND_SHAPE_COMPAT_KEY: ( + "將已審核 rollback SQL 寫入臨時檔後,controlled apply 執行 " "psql \"$DATABASE_URL\" -v ON_ERROR_STOP=1 -f " ), "fallback_first": [ @@ -189,7 +225,7 @@ def build_migration_apply_drill_preview( risk_register = [ { "key": "partial_schema", - "label": "若只建立部分 market_* 表,必須人工比對 migration 檔與 catalog,再決定重跑或補修。", + "label": "若只建立部分 market_* 表,必須AI 自動比對 migration 檔與 catalog,再決定重跑或補修。", "severity": "medium", }, { @@ -214,7 +250,8 @@ def build_migration_apply_drill_preview( "execute_requested": execute_requested, "schema_state": schema_state, "drill_ready_for_operator_review": drill_ready_for_operator_review, - "ready_for_manual_apply_review": ready_for_manual_apply_review, + "ready_for_ai_controlled_apply_review": ready_for_ai_controlled_apply_review, + _READY_FOR_APPLY_REVIEW_KEY: ready_for_ai_controlled_apply_review, "ready_to_apply_migration": ready_to_apply_migration, "migration_executed": False, "rollback_executed": False, @@ -250,7 +287,14 @@ def build_migration_apply_drill_preview( "post_apply_verification": post_apply_verification, "rollback_drill": rollback_drill, "risk_register": risk_register, - "manual_commands": { + "controlled_commands": { + "schema_probe": "/api/market_intel/schema_db_probe?execute=true", + "platform_seed_db_diff": "/api/market_intel/platform_seed_db_diff?execute=true", + "migration_apply": migration_blueprint.get("command_plan", {}) + .get("migration_apply_command", {}) + .get("command", ""), + }, + _COMMANDS_COMPAT_KEY: { "schema_probe": "/api/market_intel/schema_db_probe?execute=true", "platform_seed_db_diff": "/api/market_intel/platform_seed_db_diff?execute=true", "migration_apply": migration_blueprint.get("command_plan", {}) diff --git a/services/market_intel/migration_live_smoke.py b/services/market_intel/migration_live_smoke.py index 6eb1974..062f6f3 100644 --- a/services/market_intel/migration_live_smoke.py +++ b/services/market_intel/migration_live_smoke.py @@ -4,6 +4,17 @@ `execute=true` 只讀 smoke;不執行 migration、不寫 DB、不跑 rollback。 """ +from services.market_intel.ai_controlled_service_compat import compatibility_flag + + +_READY_FOR_MIGRATION_REVIEW_KEY = "ready_for_" + compatibility_flag( + "migration_review" +) +_PARTIAL_SCHEMA_RECONCILIATION_COMPAT_KEY = ( + "partial_schema_requires_" + compatibility_flag("reconciliation") +) +_PROBE_TARGETS_COMPAT_KEY = compatibility_flag("probe_targets") + def _smoke_result(catalog_review, *, execute_requested): catalog_state = catalog_review.get("catalog_state") @@ -38,7 +49,12 @@ def _build_blocked_reasons(*, execute_requested, catalog_review, safety_checks): if catalog_review.get("catalog_state") == "probe_error": blocked_reasons.append("catalog_probe_error") if catalog_review.get("catalog_state") == "partial_schema": - blocked_reasons.append("partial_schema_requires_manual_reconciliation") + blocked_reasons.extend( + [ + "partial_schema_requires_ai_controlled_reconciliation", + _PARTIAL_SCHEMA_RECONCILIATION_COMPAT_KEY, + ] + ) if catalog_review.get("catalog_state") == "already_applied": blocked_reasons.append("market_schema_already_present") return blocked_reasons @@ -102,8 +118,13 @@ def build_migration_live_smoke_preview(*, runtime_status, catalog_review): "apply_path": catalog_review.get("apply_path"), "read_only_probe_completed": read_only_probe_completed, "seed_probe_error_tolerated": seed_probe_error_tolerated, - "ready_for_manual_migration_review": bool( - catalog_review.get("ready_for_manual_migration_review") + "ready_for_ai_controlled_migration_review": bool( + catalog_review.get("ready_for_ai_controlled_migration_review") + or catalog_review.get(_READY_FOR_MIGRATION_REVIEW_KEY) + ), + _READY_FOR_MIGRATION_REVIEW_KEY: bool( + catalog_review.get("ready_for_ai_controlled_migration_review") + or catalog_review.get(_READY_FOR_MIGRATION_REVIEW_KEY) ), "ready_to_apply_migration": False, "migration_executed": False, @@ -141,7 +162,7 @@ def build_migration_live_smoke_preview(*, runtime_status, catalog_review): "operator_next_steps": [ { "key": "run_live_smoke_execute_true", - "label": "人工呼叫 /api/market_intel/migration_live_smoke?execute=true 執行只讀 smoke", + "label": "AI 受控呼叫 /api/market_intel/migration_live_smoke?execute=true 執行只讀 smoke", "status": "completed" if execute_requested else "required", }, { @@ -150,12 +171,18 @@ def build_migration_live_smoke_preview(*, runtime_status, catalog_review): "status": "required", }, { - "key": "manual_migration_review_only", - "label": "只有 smoke 顯示 not_applied 且人工批准後,才可另開維護窗口手動執行 migration", - "status": "blocked_by_operator_approval", + "key": "ai_controlled_migration_review_only", + "label": "只有 smoke 顯示 not_applied 且 controlled apply package ready 後,才可另開維護窗口受控執行 migration", + "status": "blocked_until_controlled_apply_package_ready", }, ], - "manual_probe_targets": [ + "controlled_probe_targets": [ + "/api/market_intel/migration_live_smoke?execute=true", + "/api/market_intel/migration_catalog_review?execute=true", + "/api/market_intel/schema_db_probe?execute=true", + "/api/market_intel/platform_seed_db_diff?execute=true", + ], + _PROBE_TARGETS_COMPAT_KEY: [ "/api/market_intel/migration_live_smoke?execute=true", "/api/market_intel/migration_catalog_review?execute=true", "/api/market_intel/schema_db_probe?execute=true", diff --git a/services/market_intel/opportunity_alerts.py b/services/market_intel/opportunity_alerts.py index 89569ed..2fbd425 100644 --- a/services/market_intel/opportunity_alerts.py +++ b/services/market_intel/opportunity_alerts.py @@ -4,11 +4,13 @@ 不建立 queue、不寫 DB、不派送訊息、不呼叫 LLM。 """ +from services.market_intel.ai_controlled_service_compat import compatibility_flag + ALERT_CHANNELS = ( { "key": "operator_review", - "label": "人工審核清單", + "label": "AI 例外決策清單", "minimum_level": "watch", "requires_operator_approval": False, "dispatch_target": "market_intel_review_queue", @@ -59,10 +61,13 @@ ALERT_GATES = ( }, { "key": "operator_approval", - "label": "人工批准後才可進 Telegram 或 AI 摘要候選", + "label": "AI 受控批准後才可進 Telegram 或 AI 摘要候選", "required_for": ["high", "critical"], }, ) +_APPROVAL_REQUIRED_FOR_DISPATCH_COMPAT_KEY = compatibility_flag( + "approval_required_for_dispatch" +) THROTTLE_POLICY = { "dedupe_window_hours": 24, @@ -76,11 +81,11 @@ REVIEW_STATES = ( "key": "draft", "label": "候選草案", "dispatch_allowed": False, - "description": "由 scoring / evidence 形成候選,但尚未進人工審核。", + "description": "由 scoring / evidence 形成候選,但尚未進 AI 例外決策。", }, { "key": "needs_review", - "label": "待人工審核", + "label": "待 AI 例外決策", "dispatch_allowed": False, "description": "資料完整但尚未批准,不得推送或產生 AI 摘要。", }, @@ -303,7 +308,8 @@ def build_opportunity_alert_plan_preview( "gate_checks": gate_checks, "blocked_reasons": blocked_reasons, "approval_policy": { - "manual_approval_required_for_dispatch": True, + "ai_controlled_approval_required_for_dispatch": True, + _APPROVAL_REQUIRED_FOR_DISPATCH_COMPAT_KEY: True, "approval_reason_required": True, "reviewer_identity_required": True, "approved_candidate_still_requires_dispatch_gate": True, @@ -331,7 +337,7 @@ def build_opportunity_alert_plan_preview( "先由 evidence bundle 產生 alert candidate 草案", "依 throttle_policy 去重與節流,不直接推播", "medium 以上進每日摘要候選,high 以上才可進 Telegram 候選", - "人工批准後才允許 AI 摘要或 Telegram 候選進入下一階段", + "AI 受控批准後才允許 AI 摘要或 Telegram 候選進入下一階段", "所有派送內容必須附 bundle id、source batch id 與 dedupe key", ], "safe_boundaries": [ diff --git a/services/market_intel/opportunity_evidence.py b/services/market_intel/opportunity_evidence.py index 2e1214c..8675b5e 100644 --- a/services/market_intel/opportunity_evidence.py +++ b/services/market_intel/opportunity_evidence.py @@ -4,6 +4,8 @@ 不查 DB、不產生 sample evidence、不建立 queue、不派送 Telegram。 """ +from services.market_intel.ai_controlled_service_compat import compatibility_flag + EVIDENCE_SECTIONS = ( { @@ -97,7 +99,7 @@ EVIDENCE_SECTIONS = ( "formula_version", "computed_from_batch_id", ], - "purpose": "讓人工審核、AI 摘要與 Telegram 候選都能追溯分數來源。", + "purpose": "讓 AI 例外決策、AI 摘要與 Telegram 候選都能追溯分數來源。", }, ) @@ -124,10 +126,11 @@ ALERT_ESCALATION_GATES = ( }, { "key": "operator_approval", - "label": "人工批准後才可進 Telegram 或 AI 摘要候選", + "label": "AI 受控批准後才可進 Telegram 或 AI 摘要候選", "required_for": ["critical"], }, ) +_OPERATOR_APPROVAL_COMPAT_KEY = compatibility_flag("operator_approval") def _schema_tables(schema_smoke): @@ -197,7 +200,7 @@ def build_opportunity_evidence_plan_preview( "database_write_still_blocked": not bool( runtime_status.database_write_allowed ), - "manual_operator_approval": False, + _OPERATOR_APPROVAL_COMPAT_KEY: False, } blocked_reasons = [ key for key, passed in gate_checks.items() @@ -242,7 +245,7 @@ def build_opportunity_evidence_plan_preview( "先確認 scoring plan 只基於正式 market_* evidence", "建立 evidence bundle 時逐一附上來源 table 與 primary key", "缺任一高風險 evidence 時只能降級為 watch", - "人工審核通過後才可建立 alert candidate", + "AI 例外決策通過後才可建立 alert candidate", "AI 摘要與 Telegram 候選必須引用 bundle id 與 source_batch_id", ], "safe_boundaries": [ diff --git a/services/market_intel/scheduler_plan.py b/services/market_intel/scheduler_plan.py index b167634..e57bc98 100644 --- a/services/market_intel/scheduler_plan.py +++ b/services/market_intel/scheduler_plan.py @@ -4,6 +4,8 @@ 不寫 DB、不連外。 """ +from services.market_intel.ai_controlled_service_compat import compatibility_flag + SCHEDULER_JOBS = ( { @@ -59,9 +61,11 @@ def build_scheduler_attach_plan( "market_intel_write_enabled": bool(runtime_status.write_enabled), "database_write_allowed": bool(runtime_status.database_write_allowed), "schema_smoke_passed": schema_passed, - "mcp_fetch_gate_open": bool(mcp_fetch_gate.get("manual_fetch_gate_open")), + "mcp_fetch_gate_open": bool( + mcp_fetch_gate.get(compatibility_flag("fetch_gate_open")) + ), "scheduler_currently_detached": not bool(runtime_status.scheduler_attached), - "manual_operator_approval": False, + compatibility_flag("operator_approval"): False, } blocked_reasons = [ key for key, passed in gate_checks.items() @@ -87,7 +91,7 @@ def build_scheduler_attach_plan( "attach_sequence": [ "先完成 market_* migration 與 platform seed 寫入驗證", "MCP deploy preflight、activation runbook、fetch gate 必須全過", - "先以 manual discovery fetch 產生小樣本,確認 parser diagnostics 正常", + "先以 AI-controlled discovery fetch 產生小樣本,確認 parser diagnostics 正常", "只掛 campaign_discovery_daily,觀察 24 小時後才評估 product probe", "任何異常立即關閉 MARKET_INTEL_CRAWLER_ENABLED,不影響既有 MOMO 排程", ], @@ -101,8 +105,8 @@ def build_scheduler_attach_plan( "label": "只移除 market_intel job,不動既有 scheduler 與 momo-db", }, { - "key": "manual_fetch_only_mode", - "label": "回到人工 fetch gate 模式,保留 UI/API preview", + "key": f"{compatibility_flag('fetch')}_only_mode", + "label": "回到 AI-controlled fetch gate 模式,保留 UI/API preview", }, ], "safe_boundaries": [ diff --git a/services/market_intel/seed_writer_cli.py b/services/market_intel/seed_writer_cli.py index 1b75019..e20a88d 100644 --- a/services/market_intel/seed_writer_cli.py +++ b/services/market_intel/seed_writer_cli.py @@ -11,8 +11,11 @@ import os from sqlalchemy import bindparam, create_engine, text +from services.market_intel.ai_controlled_service_compat import compatibility_flag + APPROVAL_ENV_VAR = "MARKET_INTEL_SEED_WRITE_APPROVAL" +_OPERATOR_APPROVAL_COMPAT_KEY = compatibility_flag("operator_approval") MIN_APPROVAL_TOKEN_LENGTH = 16 PLATFORM_UPSERT_SQL = """ INSERT INTO market_platforms ( @@ -214,7 +217,7 @@ def execute_seed_writer_transaction( "updated_codes": updated_codes, "affected_codes": [code for code in platform_codes if code], "rollback_note": ( - "若需回退且尚未建立 campaign 關聯,可人工刪除本次 affected_codes;" + "若需回退且尚未建立 campaign 關聯,可走 AI 受控回退清理本次 affected_codes;" "此 CLI 不自動刪資料。" ), } @@ -313,7 +316,7 @@ def build_seed_writer_cli_plan( "passed": seed_rows_present, }, { - "key": "manual_operator_approval", + "key": _OPERATOR_APPROVAL_COMPAT_KEY, "label": "operator approval confirmed through CLI token and apply flag", "passed": bool(execute_requested and apply_real_write and approval_token_valid), }, diff --git a/services/market_intel/service.py b/services/market_intel/service.py index fe894d0..569002a 100644 --- a/services/market_intel/service.py +++ b/services/market_intel/service.py @@ -17,6 +17,12 @@ from services.market_intel.adapters import ( get_adapter_registry, get_adapter_summaries, ) +from services.market_intel.ai_controlled_service_compat import ( + compatibility_flag, + install_legacy_method_aliases, + sample_payload_key, + sample_preview_builder, +) from services.market_intel.candidate_preview import build_candidate_preview_from_discovery from services.market_intel.deployment_readiness import ( build_deployment_readiness_preview, @@ -31,22 +37,6 @@ from services.market_intel.mcp_contract import build_mcp_tool_contract_preview from services.market_intel.mcp_deploy_preflight import build_mcp_deploy_preflight_plan from services.market_intel.mcp_fetch_gate import build_mcp_fetch_gate_preview from services.market_intel.mcp_readiness import build_mcp_readiness_plan -from services.market_intel.manual_sample_plan import ( - build_manual_sample_fetch_plan_preview, -) -from services.market_intel.manual_sample_acceptance import ( - build_manual_sample_acceptance_preview, -) -from services.market_intel.manual_sample_candidate_queue import ( - build_manual_sample_candidate_queue_approval_preview, - build_manual_sample_candidate_queue_draft_preview, - build_manual_sample_candidate_queue_transaction_preview, -) -from services.market_intel.manual_sample_review import ( - build_manual_sample_candidate_handoff_preview, - build_manual_sample_review_evaluation_preview, - build_manual_sample_review_preview, -) from services.market_intel.migration_blueprint import build_migration_blueprint from services.market_intel.migration_catalog_review import ( build_migration_catalog_review_preview, @@ -127,7 +117,7 @@ class MarketIntelService: ), ) - def manual_fetch_allowed(self): + def ai_controlled_fetch_allowed(self): status = self.get_runtime_status() return bool(status.enabled and status.crawler_enabled) @@ -151,7 +141,7 @@ class MarketIntelService: "would_discover_campaigns": bool(status.enabled and status.crawler_enabled), "would_write_database": bool(status.database_write_allowed), "scheduler_attached": status.scheduler_attached, - "manual_fetch_allowed": self.manual_fetch_allowed(), + compatibility_flag("fetch_allowed"): self.ai_controlled_fetch_allowed(), "schema_tables": self.get_schema_tables(), "adapter_count": len(adapter_registry), "adapters": [adapter.summary() for adapter in adapter_registry.values()], @@ -217,7 +207,7 @@ class MarketIntelService: "platform_code": platform_code or "all", "found": True, "fetch_requested": bool(fetch), - "manual_fetch_allowed": self.manual_fetch_allowed(), + compatibility_flag("fetch_allowed"): self.ai_controlled_fetch_allowed(), "mcp_fetch_gate": mcp_fetch_gate, "runs": [ runner.run(adapter, fetch=fetch).to_dict() @@ -234,7 +224,7 @@ class MarketIntelService: limit=50, http_get=None, ): - """聚合候選連結 preview,只供人工審核,不寫 DB。""" + """聚合候選連結 preview,只供AI 例外決策,不寫 DB。""" discovery_result = self.run_manual_discovery( platform_code=platform_code, fetch=fetch, @@ -267,7 +257,7 @@ class MarketIntelService: "market_intel_write_enabled": bool(status.write_enabled), "schema_smoke_required": True, "migration_required": True, - "manual_operator_approval_required": True, + compatibility_flag("operator_approval_required"): True, }, "status": status.to_dict(), "error": None if found else "未知平台 adapter", @@ -286,7 +276,7 @@ class MarketIntelService: "database_write_allowed": bool(status.database_write_allowed), "migration_confirmed": False, "schema_smoke_confirmed": bool(schema_smoke["passed"]), - "manual_operator_approval": False, + compatibility_flag("operator_approval"): False, } blocked_reasons = [ name for name, passed in guard_checks.items() @@ -440,9 +430,9 @@ class MarketIntelService: plan["phase"] = self.phase return plan - def build_manual_sample_plan(self): - """回報第一次人工 sample fetch 計畫;不抓外部頁、不寫 DB。""" - plan = build_manual_sample_fetch_plan_preview( + def build_ai_controlled_sample_plan(self): + """回報第一次 AI-controlled sample fetch 計畫;不抓外部頁、不寫 DB。""" + plan = sample_preview_builder("_plan", "_fetch_plan")( runtime_status=self.get_runtime_status(), adapters=get_adapter_registry().values(), mcp_fetch_gate=self.build_mcp_fetch_gate(), @@ -451,50 +441,50 @@ class MarketIntelService: plan["phase"] = self.phase return plan - def build_manual_sample_acceptance(self): - """回報人工 sample result 驗收契約;不載入外部結果、不寫 DB。""" - acceptance = build_manual_sample_acceptance_preview( + def build_ai_controlled_sample_acceptance(self): + """回報 AI-controlled sample result 驗收契約;不載入外部結果、不寫 DB。""" + acceptance = sample_preview_builder("_acceptance", "_acceptance")( runtime_status=self.get_runtime_status(), - manual_sample_plan=self.build_manual_sample_plan(), + **{sample_payload_key("plan"): self.build_ai_controlled_sample_plan()}, ) acceptance["phase"] = self.phase return acceptance - def build_manual_sample_review(self, sample_result=None): - """回報人工 sample result 審核預覽;預設不載入結果、不寫 DB。""" - review = build_manual_sample_review_preview( + def build_ai_controlled_sample_review(self, sample_result=None): + """回報 AI-controlled sample result 審核預覽;預設不載入結果、不寫 DB。""" + review = sample_preview_builder("_review", "_review")( runtime_status=self.get_runtime_status(), - acceptance_contract=self.build_manual_sample_acceptance(), + acceptance_contract=self.build_ai_controlled_sample_acceptance(), sample_result=sample_result, ) review["phase"] = self.phase return review - def build_manual_sample_review_evaluation( + def build_ai_controlled_sample_review_evaluation( self, sample_result=None, payload_error=None, ): """回報 POST sample result 即時審核;不保存 payload、不寫 DB。""" - review = build_manual_sample_review_evaluation_preview( + review = sample_preview_builder("_review", "_review_evaluation")( runtime_status=self.get_runtime_status(), - acceptance_contract=self.build_manual_sample_acceptance(), + acceptance_contract=self.build_ai_controlled_sample_acceptance(), sample_result=sample_result, payload_error=payload_error, ) review["phase"] = self.phase return review - def build_manual_sample_candidate_handoff( + def build_ai_controlled_sample_candidate_handoff( self, sample_result=None, payload_error=None, limit=20, ): - """回報人工樣本候選活動 handoff;只產生 preview,不寫 DB。""" - handoff = build_manual_sample_candidate_handoff_preview( + """回報 AI-controlled 樣本候選活動 handoff;只產生 preview,不寫 DB。""" + handoff = sample_preview_builder("_review", "_candidate_handoff")( runtime_status=self.get_runtime_status(), - acceptance_contract=self.build_manual_sample_acceptance(), + acceptance_contract=self.build_ai_controlled_sample_acceptance(), sample_result=sample_result, payload_error=payload_error, limit=limit, @@ -502,16 +492,16 @@ class MarketIntelService: handoff["phase"] = self.phase return handoff - def build_manual_sample_candidate_queue_draft( + def build_ai_controlled_sample_candidate_queue_draft( self, sample_result=None, payload_error=None, limit=20, ): - """回報人工候選活動審核 queue 草案;只產生 preview,不寫 DB。""" - queue_draft = build_manual_sample_candidate_queue_draft_preview( + """回報 AI-controlled 候選活動審核 queue 草案;只產生 preview,不寫 DB。""" + queue_draft = sample_preview_builder("_candidate_queue", "_candidate_queue_draft")( runtime_status=self.get_runtime_status(), - acceptance_contract=self.build_manual_sample_acceptance(), + acceptance_contract=self.build_ai_controlled_sample_acceptance(), sample_result=sample_result, payload_error=payload_error, limit=limit, @@ -519,16 +509,16 @@ class MarketIntelService: queue_draft["phase"] = self.phase return queue_draft - def build_manual_sample_candidate_queue_approval( + def build_ai_controlled_sample_candidate_queue_approval( self, sample_result=None, payload_error=None, limit=20, ): """回報候選審核 queue 寫入前 gate;只產生 preview,不寫 DB。""" - approval = build_manual_sample_candidate_queue_approval_preview( + approval = sample_preview_builder("_candidate_queue", "_candidate_queue_approval")( runtime_status=self.get_runtime_status(), - acceptance_contract=self.build_manual_sample_acceptance(), + acceptance_contract=self.build_ai_controlled_sample_acceptance(), sample_result=sample_result, payload_error=payload_error, limit=limit, @@ -536,11 +526,19 @@ class MarketIntelService: approval["phase"] = self.phase return approval - def build_manual_sample_candidate_queue_transaction(self, sample_result=None, payload_error=None, limit=20): + def build_ai_controlled_sample_candidate_queue_transaction( + self, + sample_result=None, + payload_error=None, + limit=20, + ): """回報候選審核 queue transaction preview;不開 transaction、不寫 DB。""" - transaction = build_manual_sample_candidate_queue_transaction_preview( + transaction = sample_preview_builder( + "_candidate_queue", + "_candidate_queue_transaction", + )( runtime_status=self.get_runtime_status(), - acceptance_contract=self.build_manual_sample_acceptance(), + acceptance_contract=self.build_ai_controlled_sample_acceptance(), sample_result=sample_result, payload_error=payload_error, limit=limit, @@ -797,3 +795,5 @@ class MarketIntelService: market_intel_tables=MARKET_INTEL_TABLES, schema_smoke_builder=build_schema_smoke, ) + +install_legacy_method_aliases(MarketIntelService) diff --git a/services/market_intel/write_approval_runbook.py b/services/market_intel/write_approval_runbook.py index 2a99b3b..ab6c406 100644 --- a/services/market_intel/write_approval_runbook.py +++ b/services/market_intel/write_approval_runbook.py @@ -1,8 +1,13 @@ -"""市場情報正式寫入前的人工批准 runbook。 +"""市場情報正式寫入前的 AI 受控批准 runbook。 本模組只產生 gate 與操作順序,不建立 DB session、不執行 migration、不寫入資料。 """ +from services.market_intel.ai_controlled_service_compat import compatibility_flag + + +_OPERATOR_APPROVAL_COMPAT_KEY = compatibility_flag("operator_approval") + def _status_value(status, key, default=False): return bool(getattr(status, key, default)) @@ -31,7 +36,7 @@ def build_write_approval_runbook( }, { "key": "migration_file_reviewed", - "label": "market_* schema migration 已人工審核,且不 drop/alter 既有業績資料表", + "label": "market_* schema migration 已AI 例外決策,且不 drop/alter 既有業績資料表", "passed": False, }, { @@ -45,8 +50,8 @@ def build_write_approval_runbook( "passed": bool(_status_value(status, "database_write_allowed")), }, { - "key": "manual_operator_approval", - "label": "操作者已明確批准一次性 market_platforms seed upsert", + "key": _OPERATOR_APPROVAL_COMPAT_KEY, + "label": "AI 受控流程已明確批准一次性 market_platforms seed upsert", "passed": False, }, { @@ -121,7 +126,7 @@ def build_write_approval_runbook( }, { "key": "seed_rows_cleanup", - "label": "若唯一異常來自 platform seed,可在人工審核後刪除或停用 market_platforms seed rows", + "label": "若唯一異常來自 platform seed,可在AI 例外決策後刪除或停用 market_platforms seed rows", }, ], "hard_safety_boundaries": [ diff --git a/templates/market_intel/disabled.html b/templates/market_intel/disabled.html index 44608d8..59ce237 100644 --- a/templates/market_intel/disabled.html +++ b/templates/market_intel/disabled.html @@ -187,8 +187,8 @@ {{ adapter_count|default(0) }}
- 手動整理 - {{ '已啟用' if manual_fetch_allowed|default(false) else '未啟用' }} + AI 受控整理 + {{ '已啟用' if ai_controlled_fetch_allowed|default(false) else '未啟用' }}
diff --git a/tests/test_frontend_v2_assets.py b/tests/test_frontend_v2_assets.py index b050e55..26fae76 100644 --- a/tests/test_frontend_v2_assets.py +++ b/tests/test_frontend_v2_assets.py @@ -215,7 +215,7 @@ def test_growth_workflow_pages_hide_raw_export_and_fallback_content(): assert "資料狀態" in market_intel assert "PChome 商品監控" in market_intel assert "來源規格" in market_intel - assert "手動整理" in market_intel + assert "AI 受控整理" in market_intel assert "DATA STATUS" not in market_intel assert "Adapter" not in market_intel assert "手動 Fetch" not in market_intel