V10.505 新增市場情報 writer review decision gate
This commit is contained in:
@@ -108,6 +108,9 @@ from services.market_intel.mcp_fetch_candidate_queue_writer_review_handoff impor
|
||||
from services.market_intel.mcp_fetch_candidate_queue_writer_review_inventory import (
|
||||
build_mcp_fetch_candidate_queue_writer_review_inventory_preview,
|
||||
)
|
||||
from services.market_intel.mcp_fetch_candidate_queue_writer_review_decision import (
|
||||
build_mcp_fetch_candidate_queue_writer_review_decision_preview,
|
||||
)
|
||||
from services.market_intel.mcp_manual_fetch_handoff import (
|
||||
build_mcp_manual_fetch_handoff_preview,
|
||||
)
|
||||
@@ -287,8 +290,17 @@ PRODUCTION_SMOKE_TARGETS = (
|
||||
+ ("/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:]
|
||||
)
|
||||
|
||||
|
||||
def _run_review_preview_safe(payload, mode):
|
||||
return bool(payload["mode"] == mode and all(not payload.get(key) for key in BLOCKED_RUN_REVIEW_KEYS))
|
||||
|
||||
|
||||
def build_deployment_readiness_preview(*, service, market_intel_tables, schema_smoke_builder):
|
||||
"""建立市場情報推版準備狀態;不執行 git、部署或遠端操作。"""
|
||||
status = service.get_runtime_status()
|
||||
@@ -381,6 +393,11 @@ def build_deployment_readiness_preview(*, service, market_intel_tables, schema_s
|
||||
phase=service.phase,
|
||||
)
|
||||
)
|
||||
mcp_fetch_candidate_queue_writer_review_decision = (
|
||||
build_mcp_fetch_candidate_queue_writer_review_decision_preview(
|
||||
phase=service.phase,
|
||||
)
|
||||
)
|
||||
scheduler_plan = service.build_scheduler_plan()
|
||||
manual_sample_plan = service.build_manual_sample_plan()
|
||||
manual_sample_acceptance = service.build_manual_sample_acceptance()
|
||||
@@ -1325,6 +1342,76 @@ def build_deployment_readiness_preview(*, service, market_intel_tables, schema_s
|
||||
"candidate_review_state_updated"
|
||||
]
|
||||
),
|
||||
"mcp_fetch_candidate_queue_writer_review_decision_preview_safe": bool(
|
||||
mcp_fetch_candidate_queue_writer_review_decision["mode"]
|
||||
== "mcp_fetch_candidate_queue_writer_review_decision_preview"
|
||||
and not mcp_fetch_candidate_queue_writer_review_decision[
|
||||
"payload_persisted"
|
||||
]
|
||||
and not mcp_fetch_candidate_queue_writer_review_decision[
|
||||
"review_decision_persisted"
|
||||
]
|
||||
and not mcp_fetch_candidate_queue_writer_review_decision[
|
||||
"candidate_queue_review_decision_persisted"
|
||||
]
|
||||
and not mcp_fetch_candidate_queue_writer_review_decision[
|
||||
"candidate_queue_review_decision_file_written"
|
||||
]
|
||||
and not mcp_fetch_candidate_queue_writer_review_decision[
|
||||
"decision_record_written"
|
||||
]
|
||||
and not mcp_fetch_candidate_queue_writer_review_decision[
|
||||
"decision_record_file_written"
|
||||
]
|
||||
and not mcp_fetch_candidate_queue_writer_review_decision[
|
||||
"package_artifact_created"
|
||||
]
|
||||
and not mcp_fetch_candidate_queue_writer_review_decision[
|
||||
"api_writes_file"
|
||||
]
|
||||
and not mcp_fetch_candidate_queue_writer_review_decision[
|
||||
"api_executes_cli"
|
||||
]
|
||||
and not mcp_fetch_candidate_queue_writer_review_decision[
|
||||
"api_reads_approval_token"
|
||||
]
|
||||
and not mcp_fetch_candidate_queue_writer_review_decision[
|
||||
"api_opens_database_connection"
|
||||
]
|
||||
and not mcp_fetch_candidate_queue_writer_review_decision[
|
||||
"api_writes_database"
|
||||
]
|
||||
and not mcp_fetch_candidate_queue_writer_review_decision[
|
||||
"api_updates_review_state"
|
||||
]
|
||||
and not mcp_fetch_candidate_queue_writer_review_decision[
|
||||
"api_writes_decision_record"
|
||||
]
|
||||
and not mcp_fetch_candidate_queue_writer_review_decision[
|
||||
"database_write_executed"
|
||||
]
|
||||
and not mcp_fetch_candidate_queue_writer_review_decision[
|
||||
"database_commit_executed"
|
||||
]
|
||||
and not mcp_fetch_candidate_queue_writer_review_decision[
|
||||
"database_connection_opened"
|
||||
]
|
||||
and not mcp_fetch_candidate_queue_writer_review_decision[
|
||||
"cli_executed"
|
||||
]
|
||||
and not mcp_fetch_candidate_queue_writer_review_decision[
|
||||
"file_written"
|
||||
]
|
||||
and not mcp_fetch_candidate_queue_writer_review_decision[
|
||||
"scheduler_attached"
|
||||
]
|
||||
and not mcp_fetch_candidate_queue_writer_review_decision[
|
||||
"candidate_queue_persisted"
|
||||
]
|
||||
and not mcp_fetch_candidate_queue_writer_review_decision[
|
||||
"candidate_review_state_updated"
|
||||
]
|
||||
),
|
||||
"candidate_queue_writer_postwrite_smoke_planned_safe": bool(
|
||||
candidate_queue_writer_postwrite_smoke["mode"]
|
||||
== "candidate_queue_writer_postwrite_smoke_planned"
|
||||
@@ -1653,6 +1740,7 @@ def build_deployment_readiness_preview(*, service, market_intel_tables, schema_s
|
||||
"mcp_fetch_candidate_queue_writer_post_closeout_inventory_review": mcp_fetch_candidate_queue_writer_post_closeout_inventory_review,
|
||||
"mcp_fetch_candidate_queue_writer_review_handoff": mcp_fetch_candidate_queue_writer_review_handoff,
|
||||
"mcp_fetch_candidate_queue_writer_review_inventory": mcp_fetch_candidate_queue_writer_review_inventory,
|
||||
"mcp_fetch_candidate_queue_writer_review_decision": mcp_fetch_candidate_queue_writer_review_decision,
|
||||
"scheduler_plan": scheduler_plan,
|
||||
"manual_sample_plan": manual_sample_plan,
|
||||
"manual_sample_acceptance": manual_sample_acceptance,
|
||||
|
||||
@@ -0,0 +1,498 @@
|
||||
"""市場情報 MCP fetch candidate queue writer review decision。
|
||||
|
||||
本模組只審核 review inventory 後的人工候選隊列決策摘要;
|
||||
API/UI 不讀 approval token、不執行 CLI、不開 DB、不寫 queue、不做 inventory query、
|
||||
不更新 review_state、不寫 match result、不掛 scheduler。
|
||||
"""
|
||||
|
||||
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,
|
||||
)
|
||||
from services.market_intel.mcp_fetch_candidate_queue_writer_post_closeout_inventory_review import (
|
||||
FORBIDDEN_SECRET_KEYS,
|
||||
SAFE_SECRET_METADATA_KEYS,
|
||||
_as_dict,
|
||||
_as_list,
|
||||
_blocked_side_effects,
|
||||
_contains_forbidden_key,
|
||||
_safe_int,
|
||||
_safe_path,
|
||||
_safe_text,
|
||||
)
|
||||
from services.market_intel.mcp_fetch_candidate_queue_writer_review_inventory import (
|
||||
build_mcp_fetch_candidate_queue_writer_review_inventory_preview,
|
||||
)
|
||||
from services.market_intel.mcp_fetch_candidate_queue_writer_review_decision_gates import (
|
||||
ALLOWED_REVIEW_DECISIONS,
|
||||
build_review_decision_gates,
|
||||
)
|
||||
from services.market_intel.mcp_fetch_candidate_queue_writer_review_decision_sample import (
|
||||
build_sample_writer_review_decision_package,
|
||||
)
|
||||
|
||||
|
||||
_REVIEW_DECISION_BLOCKED_SIDE_EFFECT_KEYS = (
|
||||
"allow_api_candidate_review_update",
|
||||
"allow_api_execution",
|
||||
"allow_api_file_write",
|
||||
"allow_api_inventory_query",
|
||||
"allow_api_match_write",
|
||||
"allow_api_queue_insert",
|
||||
"allow_api_queue_write",
|
||||
"allow_cli_execution",
|
||||
"allow_database_write",
|
||||
"api_candidate_review_update_executed",
|
||||
"api_created_review_decision_file",
|
||||
"api_created_review_queue",
|
||||
"api_executed_candidate_review",
|
||||
"api_inventory_query_executed",
|
||||
"api_review_decision_persisted",
|
||||
"api_review_state_update_executed",
|
||||
"api_updates_review_state",
|
||||
"api_writes_match_result",
|
||||
"api_writes_review_queue",
|
||||
"candidate_queue_review_decision_file_written",
|
||||
"candidate_queue_review_decision_persisted",
|
||||
"candidate_queue_review_started_by_api",
|
||||
"candidate_review_file_written",
|
||||
"candidate_review_queue_written",
|
||||
"decision_record_written",
|
||||
"identity_match_written",
|
||||
"manual_review_record_written",
|
||||
"market_product_match_written",
|
||||
"match_result_written",
|
||||
"queue_review_persisted",
|
||||
"review_decision_file_written",
|
||||
"review_decision_persisted",
|
||||
"review_queue_file_written",
|
||||
"review_queue_row_written",
|
||||
"review_state_update_executed",
|
||||
)
|
||||
|
||||
|
||||
def _blocked_review_decision_side_effects(payload):
|
||||
found = list(_blocked_side_effects(payload))
|
||||
|
||||
def visit(value, path):
|
||||
if isinstance(value, dict):
|
||||
for key, item in value.items():
|
||||
normalized_key = str(key).lower()
|
||||
key_path = f"{path}.{key}" if path else key
|
||||
if (
|
||||
normalized_key in _REVIEW_DECISION_BLOCKED_SIDE_EFFECT_KEYS
|
||||
and bool(item)
|
||||
):
|
||||
found.append(key_path)
|
||||
visit(item, key_path)
|
||||
elif isinstance(value, list):
|
||||
for index, item in enumerate(value):
|
||||
visit(item, f"{path}[{index}]")
|
||||
|
||||
visit(payload, "")
|
||||
return sorted(set(found))
|
||||
|
||||
|
||||
def _inventory_from_inputs(inventory_package, inventory_result, phase):
|
||||
if isinstance(inventory_result, dict) and inventory_result:
|
||||
return inventory_result
|
||||
|
||||
inventory_package = _as_dict(inventory_package)
|
||||
return build_mcp_fetch_candidate_queue_writer_review_inventory_preview(
|
||||
writer_review_handoff_package=(
|
||||
inventory_package.get("writer_review_handoff_package")
|
||||
or inventory_package.get("candidate_queue_writer_review_handoff")
|
||||
or inventory_package.get("writer_review_handoff")
|
||||
or inventory_package.get("review_handoff_package")
|
||||
or inventory_package.get("review_handoff")
|
||||
or {}
|
||||
),
|
||||
writer_review_handoff_result=(
|
||||
inventory_package.get("writer_review_handoff_result")
|
||||
or inventory_package.get(
|
||||
"mcp_fetch_candidate_queue_writer_review_handoff"
|
||||
)
|
||||
),
|
||||
operator_review_inventory=(
|
||||
inventory_package.get("operator_review_inventory")
|
||||
or inventory_package.get("candidate_queue_review_inventory")
|
||||
or inventory_package.get("writer_review_inventory")
|
||||
or inventory_package.get("review_inventory_payload")
|
||||
or inventory_package.get("inventory_payload")
|
||||
or inventory_package.get("inventory")
|
||||
),
|
||||
phase=phase,
|
||||
)
|
||||
|
||||
|
||||
def _inventory_summary(inventory_result):
|
||||
inventory_result = _as_dict(inventory_result)
|
||||
inventory = _as_dict(inventory_result.get("operator_review_inventory_summary"))
|
||||
side_effects_clear = bool(
|
||||
not inventory_result.get("payload_persisted")
|
||||
and not inventory_result.get("review_inventory_persisted")
|
||||
and not inventory_result.get("writer_review_inventory_persisted")
|
||||
and not inventory_result.get("candidate_queue_review_inventory_persisted")
|
||||
and not inventory_result.get("candidate_queue_review_inventory_file_written")
|
||||
and not inventory_result.get("review_inventory_file_written")
|
||||
and not inventory_result.get("inventory_file_written")
|
||||
and not inventory_result.get("review_queue_file_written")
|
||||
and not inventory_result.get("candidate_review_file_written")
|
||||
and not inventory_result.get("package_artifact_created")
|
||||
and not inventory_result.get("network_request_allowed")
|
||||
and not inventory_result.get("api_executes_cli")
|
||||
and not inventory_result.get("api_reads_approval_token")
|
||||
and not inventory_result.get("api_opens_database_connection")
|
||||
and not inventory_result.get("api_writes_database")
|
||||
and not inventory_result.get("api_writes_file")
|
||||
and not inventory_result.get("api_uses_external_network")
|
||||
and not inventory_result.get("api_inventory_query_executed")
|
||||
and not inventory_result.get("api_candidate_review_update_executed")
|
||||
and not inventory_result.get("api_updates_review_state")
|
||||
and not inventory_result.get("review_inventory_query_executed_by_api")
|
||||
and not inventory_result.get("database_write_executed")
|
||||
and not inventory_result.get("database_commit_executed")
|
||||
and not inventory_result.get("cli_executed")
|
||||
and not inventory_result.get("file_written")
|
||||
and not inventory_result.get("scheduler_attached")
|
||||
and not inventory_result.get("candidate_queue_created")
|
||||
and not inventory_result.get("candidate_queue_persisted")
|
||||
and not inventory_result.get("candidate_review_state_updated")
|
||||
)
|
||||
return {
|
||||
"mode": inventory_result.get("mode"),
|
||||
"accepted": bool(
|
||||
inventory_result.get(
|
||||
"mcp_fetch_candidate_queue_writer_review_inventory_accepted"
|
||||
)
|
||||
),
|
||||
"ready_for_candidate_queue_review_decision": bool(
|
||||
inventory_result.get("ready_for_candidate_queue_review_decision")
|
||||
),
|
||||
"review_inventory_id": _safe_text(
|
||||
inventory.get("review_inventory_id"), 160
|
||||
),
|
||||
"handoff_id": _safe_text(inventory.get("handoff_id"), 160),
|
||||
"inventory_review_id": _safe_text(inventory.get("inventory_review_id"), 160),
|
||||
"closeout_id": _safe_text(inventory.get("closeout_id"), 160),
|
||||
"run_package_id": _safe_text(inventory.get("run_package_id"), 160),
|
||||
"receipt_id": _safe_text(inventory.get("receipt_id"), 160),
|
||||
"target_table": _safe_text(inventory.get("target_table"), 160),
|
||||
"payload_row_count": _safe_int(inventory.get("payload_row_count")),
|
||||
"expected_payload_row_count": _safe_int(
|
||||
inventory.get("expected_payload_row_count")
|
||||
or inventory.get("payload_row_count")
|
||||
),
|
||||
"found_row_count": _safe_int(inventory.get("found_row_count")),
|
||||
"dedupe_key_count": _safe_int(inventory.get("dedupe_key_count")),
|
||||
"sample_dedupe_keys": [
|
||||
_safe_text(item, 160)
|
||||
for item in _as_list(inventory.get("sample_dedupe_keys"))
|
||||
if _safe_text(item, 160)
|
||||
],
|
||||
"review_state": _safe_text(inventory.get("review_state"), 80),
|
||||
"review_state_breakdown": _as_dict(inventory.get("review_state_breakdown")),
|
||||
"review_queue_artifact_path": _safe_text(
|
||||
inventory.get("review_queue_artifact_path")
|
||||
),
|
||||
"review_inventory_artifact_path": _safe_text(
|
||||
inventory.get("review_inventory_artifact_path")
|
||||
),
|
||||
"read_only_query_result_path": _safe_text(
|
||||
inventory.get("read_only_query_result_path")
|
||||
),
|
||||
"side_effects_clear": side_effects_clear,
|
||||
"blocked_reasons": inventory_result.get("blocked_reasons", []),
|
||||
}
|
||||
|
||||
|
||||
def _decision_row_summary(row):
|
||||
row = _as_dict(row)
|
||||
return {
|
||||
"dedupe_key": _safe_text(row.get("dedupe_key"), 160),
|
||||
"current_review_state": _safe_text(row.get("current_review_state"), 80),
|
||||
"proposed_review_decision": _safe_text(
|
||||
row.get("proposed_review_decision"), 80
|
||||
),
|
||||
"decision_bucket": _safe_text(row.get("decision_bucket"), 120),
|
||||
"evidence_lane": _safe_text(row.get("evidence_lane"), 120),
|
||||
"evidence_ref": _safe_text(row.get("evidence_ref")),
|
||||
"evidence_ref_safe": _safe_path(
|
||||
row.get("evidence_ref"),
|
||||
prefixes=(ARTIFACT_PREFIX,),
|
||||
suffixes=(".json",),
|
||||
),
|
||||
"decision_notes_present": bool(_safe_text(row.get("decision_notes"))),
|
||||
"hard_veto_present": bool(row.get("hard_veto_present")),
|
||||
"stronger_existing_match_conflict": bool(
|
||||
row.get("stronger_existing_match_conflict")
|
||||
),
|
||||
"false_positive_guard_passed": bool(row.get("false_positive_guard_passed")),
|
||||
"variant_conflict_checked": bool(row.get("variant_conflict_checked")),
|
||||
"overwrite_protection_checked": bool(
|
||||
row.get("overwrite_protection_checked")
|
||||
),
|
||||
"exact_identity_confirmed": bool(row.get("exact_identity_confirmed")),
|
||||
}
|
||||
|
||||
|
||||
def _operator_review_decision_summary(operator_decision):
|
||||
operator_decision = _as_dict(operator_decision)
|
||||
confirmations = _as_dict(operator_decision.get("operator_confirmations"))
|
||||
rows = [_decision_row_summary(row) for row in _as_list(
|
||||
operator_decision.get("decision_rows")
|
||||
)]
|
||||
expected_dedupe_keys = [
|
||||
_safe_text(item, 160)
|
||||
for item in _as_list(operator_decision.get("expected_dedupe_keys"))
|
||||
if _safe_text(item, 160)
|
||||
]
|
||||
return {
|
||||
"provided_keys": sorted(operator_decision.keys()),
|
||||
"review_decision_id": _safe_text(
|
||||
operator_decision.get("review_decision_id"), 160
|
||||
),
|
||||
"review_inventory_id": _safe_text(
|
||||
operator_decision.get("review_inventory_id"), 160
|
||||
),
|
||||
"handoff_id": _safe_text(operator_decision.get("handoff_id"), 160),
|
||||
"inventory_review_id": _safe_text(
|
||||
operator_decision.get("inventory_review_id"), 160
|
||||
),
|
||||
"closeout_id": _safe_text(operator_decision.get("closeout_id"), 160),
|
||||
"run_package_id": _safe_text(operator_decision.get("run_package_id"), 160),
|
||||
"receipt_id": _safe_text(operator_decision.get("receipt_id"), 160),
|
||||
"target_table": _safe_text(operator_decision.get("target_table"), 160),
|
||||
"decision_scope": _safe_text(operator_decision.get("decision_scope"), 120),
|
||||
"decision_mode": _safe_text(operator_decision.get("decision_mode"), 120),
|
||||
"expected_current_review_state": _safe_text(
|
||||
operator_decision.get("expected_current_review_state"), 80
|
||||
),
|
||||
"decision_row_count": _safe_int(operator_decision.get("decision_row_count")),
|
||||
"expected_decision_row_count": _safe_int(
|
||||
operator_decision.get("expected_decision_row_count")
|
||||
or operator_decision.get("decision_row_count")
|
||||
),
|
||||
"expected_dedupe_keys": expected_dedupe_keys,
|
||||
"decision_rows": rows,
|
||||
"review_queue_artifact_path": _safe_text(
|
||||
operator_decision.get("review_queue_artifact_path")
|
||||
),
|
||||
"review_inventory_artifact_path": _safe_text(
|
||||
operator_decision.get("review_inventory_artifact_path")
|
||||
),
|
||||
"review_decision_artifact_path": _safe_text(
|
||||
operator_decision.get("review_decision_artifact_path")
|
||||
),
|
||||
"review_queue_artifact_path_safe": _safe_path(
|
||||
operator_decision.get("review_queue_artifact_path"),
|
||||
prefixes=(ARTIFACT_PREFIX,),
|
||||
suffixes=(".json",),
|
||||
),
|
||||
"review_inventory_artifact_path_safe": _safe_path(
|
||||
operator_decision.get("review_inventory_artifact_path"),
|
||||
prefixes=(ARTIFACT_PREFIX,),
|
||||
suffixes=(".json",),
|
||||
),
|
||||
"review_decision_artifact_path_safe": _safe_path(
|
||||
operator_decision.get("review_decision_artifact_path"),
|
||||
prefixes=(ARTIFACT_PREFIX,),
|
||||
suffixes=(".json",),
|
||||
),
|
||||
"review_inventory_checked": bool(
|
||||
confirmations.get("review_inventory_checked")
|
||||
),
|
||||
"candidate_queue_review_only": bool(
|
||||
confirmations.get("candidate_queue_review_only")
|
||||
),
|
||||
"human_decision_only": bool(confirmations.get("human_decision_only")),
|
||||
"manual_record_required": bool(confirmations.get("manual_record_required")),
|
||||
"no_approval_token_payload": bool(
|
||||
confirmations.get("no_approval_token_payload")
|
||||
),
|
||||
"no_api_cli_execution": bool(confirmations.get("no_api_cli_execution")),
|
||||
"no_api_database_write": bool(confirmations.get("no_api_database_write")),
|
||||
"no_api_inventory_query": bool(confirmations.get("no_api_inventory_query")),
|
||||
"no_api_review_state_update": bool(
|
||||
confirmations.get("no_api_review_state_update")
|
||||
),
|
||||
"no_api_queue_insert": bool(confirmations.get("no_api_queue_insert")),
|
||||
"no_api_file_write": bool(confirmations.get("no_api_file_write")),
|
||||
"no_scheduler_attach": bool(confirmations.get("no_scheduler_attach")),
|
||||
"stronger_existing_match_guard_preserved": bool(
|
||||
confirmations.get("stronger_existing_match_guard_preserved")
|
||||
),
|
||||
"false_positive_guard_completed": bool(
|
||||
confirmations.get("false_positive_guard_completed")
|
||||
),
|
||||
"variant_sensitive_review_completed": bool(
|
||||
confirmations.get("variant_sensitive_review_completed")
|
||||
),
|
||||
"hard_veto_respected": bool(confirmations.get("hard_veto_respected")),
|
||||
"api_execution_allowed": bool(operator_decision.get("api_execution_allowed")),
|
||||
"real_write_allowed_by_api": bool(
|
||||
operator_decision.get("real_write_allowed_by_api")
|
||||
),
|
||||
"api_candidate_review_allowed": bool(
|
||||
operator_decision.get("api_candidate_review_allowed")
|
||||
),
|
||||
"api_updates_review_state": bool(
|
||||
operator_decision.get("api_updates_review_state")
|
||||
),
|
||||
"api_writes_match_result": bool(
|
||||
operator_decision.get("api_writes_match_result")
|
||||
),
|
||||
"secret_or_token_submitted_to_api": _contains_forbidden_key(
|
||||
operator_decision,
|
||||
FORBIDDEN_SECRET_KEYS,
|
||||
safe_keys=SAFE_SECRET_METADATA_KEYS,
|
||||
),
|
||||
"blocked_side_effects": _blocked_review_decision_side_effects(
|
||||
operator_decision
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
def build_mcp_fetch_candidate_queue_writer_review_decision_preview(
|
||||
*,
|
||||
writer_review_inventory_package=None,
|
||||
writer_review_inventory_result=None,
|
||||
operator_review_decision=None,
|
||||
phase=None,
|
||||
):
|
||||
"""建立 writer review decision;不執行查詢、寫檔或寫入。"""
|
||||
writer_review_inventory_package = _as_dict(writer_review_inventory_package)
|
||||
inventory_result_received = bool(
|
||||
isinstance(writer_review_inventory_result, dict)
|
||||
and writer_review_inventory_result
|
||||
)
|
||||
decision_valid_object = (
|
||||
isinstance(operator_review_decision, dict)
|
||||
if operator_review_decision is not None
|
||||
else True
|
||||
)
|
||||
decision_payload = _as_dict(operator_review_decision)
|
||||
inventory_received = bool(
|
||||
writer_review_inventory_package or inventory_result_received
|
||||
)
|
||||
inventory_result = (
|
||||
_inventory_from_inputs(
|
||||
writer_review_inventory_package,
|
||||
writer_review_inventory_result,
|
||||
phase,
|
||||
)
|
||||
if inventory_received
|
||||
else {}
|
||||
)
|
||||
payload_received = bool(
|
||||
inventory_received or decision_payload or operator_review_decision is not None
|
||||
)
|
||||
decision_received = bool(decision_payload)
|
||||
inventory = _inventory_summary(inventory_result)
|
||||
decision = _operator_review_decision_summary(decision_payload)
|
||||
gates = build_review_decision_gates(
|
||||
inventory_received=inventory_received,
|
||||
decision_received=decision_received and decision_valid_object,
|
||||
inventory=inventory,
|
||||
decision=decision,
|
||||
)
|
||||
blocked_reasons = [gate["key"] for gate in gates if not gate["passed"]]
|
||||
if not decision_valid_object:
|
||||
blocked_reasons.append("operator_review_decision_payload_valid_object")
|
||||
accepted = bool(payload_received and not blocked_reasons)
|
||||
|
||||
return {
|
||||
"mode": (
|
||||
"mcp_fetch_candidate_queue_writer_review_decision"
|
||||
if payload_received
|
||||
else "mcp_fetch_candidate_queue_writer_review_decision_preview"
|
||||
),
|
||||
"phase": phase,
|
||||
"writer_review_decision_payload_received": payload_received,
|
||||
"review_decision_payload_received": payload_received,
|
||||
"writer_review_inventory_received": inventory_received,
|
||||
"operator_review_decision_received": decision_received,
|
||||
"operator_review_decision_valid_object": decision_valid_object,
|
||||
"writer_review_inventory_accepted": inventory["accepted"],
|
||||
"mcp_fetch_candidate_queue_writer_review_decision_accepted": accepted,
|
||||
"candidate_queue_writer_review_decision_ready": accepted,
|
||||
"ready_for_candidate_queue_human_review": accepted,
|
||||
"ready_for_candidate_queue_review_decision_approval": accepted,
|
||||
"ready_for_candidate_queue_review_state_writer": False,
|
||||
"ready_for_candidate_queue_review_decision_writer_preflight": False,
|
||||
"ready_for_api_database_write": False,
|
||||
"ready_for_real_write": False,
|
||||
"ready_for_scheduler_attach": False,
|
||||
"ready_for_candidate_queue_review_api_update": False,
|
||||
"ready_for_api_review_state_update": False,
|
||||
"ready_for_api_match_write": False,
|
||||
"network_request_allowed": False,
|
||||
"api_executes_cli": False,
|
||||
"api_reads_approval_token": False,
|
||||
"api_opens_database_connection": False,
|
||||
"api_writes_database": False,
|
||||
"api_writes_file": False,
|
||||
"api_uses_external_network": False,
|
||||
"api_inventory_query_executed": False,
|
||||
"api_candidate_review_update_executed": False,
|
||||
"api_updates_review_state": False,
|
||||
"api_writes_decision_record": False,
|
||||
"api_writes_match_result": False,
|
||||
"review_decision_query_executed_by_api": False,
|
||||
"payload_row_count": decision["decision_row_count"],
|
||||
"gate_count": len(gates),
|
||||
"passed_gate_count": sum(1 for gate in gates if gate["passed"]),
|
||||
"blocked_reasons": blocked_reasons,
|
||||
"gates": gates,
|
||||
"writer_review_inventory_summary": inventory,
|
||||
"operator_review_decision_summary": decision,
|
||||
"decision_rows": decision["decision_rows"],
|
||||
"decision_contract": {
|
||||
"expected_current_state": "needs_review",
|
||||
"allowed_next_states": list(ALLOWED_REVIEW_DECISIONS),
|
||||
"manual_decision_required": True,
|
||||
"next_gate": "candidate_queue_review_decision_approval",
|
||||
"decision_scope": "candidate_queue_review_decision",
|
||||
"forbidden_api_actions": [
|
||||
"update_review_state",
|
||||
"write_match_result",
|
||||
"write_decision_record",
|
||||
"insert_missing_queue_row",
|
||||
"attach_scheduler",
|
||||
],
|
||||
},
|
||||
"sample_writer_review_decision_package": (
|
||||
build_sample_writer_review_decision_package()
|
||||
),
|
||||
"next_operator_steps": [
|
||||
"Decision 通過後,只代表可進入人工 approval / writer preflight 設計",
|
||||
"API/UI 仍不得自動寫 review_state、寫 match result、補 queue row 或讀 token",
|
||||
"matched 決策必須保留 exact identity、variant、false-positive 與 overwrite guard",
|
||||
],
|
||||
"payload_persisted": False,
|
||||
"review_decision_persisted": False,
|
||||
"writer_review_decision_persisted": False,
|
||||
"candidate_queue_review_decision_persisted": False,
|
||||
"candidate_queue_review_decision_file_written": False,
|
||||
"review_decision_file_written": False,
|
||||
"decision_record_written": False,
|
||||
"decision_record_file_written": False,
|
||||
"review_queue_file_written": False,
|
||||
"candidate_review_file_written": False,
|
||||
"match_result_written": False,
|
||||
"market_product_match_written": False,
|
||||
"package_artifact_created": False,
|
||||
"database_connection_opened": False,
|
||||
"database_session_created": False,
|
||||
"database_commit_executed": False,
|
||||
"database_write_executed": False,
|
||||
"external_network_executed": False,
|
||||
"cli_executed": False,
|
||||
"file_written": False,
|
||||
"writes_executed": False,
|
||||
"would_write_database": False,
|
||||
"scheduler_attached": False,
|
||||
"candidate_queue_created": False,
|
||||
"candidate_queue_persisted": False,
|
||||
"candidate_review_state_updated": False,
|
||||
}
|
||||
@@ -0,0 +1,241 @@
|
||||
"""Gate checks for market intel candidate queue review decision."""
|
||||
|
||||
from services.market_intel.mcp_fetch_candidate_queue_writer_preflight import TARGET_TABLE
|
||||
|
||||
|
||||
ALLOWED_REVIEW_DECISIONS = (
|
||||
"matched",
|
||||
"low_score",
|
||||
"identity_veto",
|
||||
"unit_comparable",
|
||||
"fresh_search_required",
|
||||
"deferred",
|
||||
)
|
||||
|
||||
|
||||
def _sample_keys_covered_by_expected(inventory, decision):
|
||||
sample_keys = set(inventory["sample_dedupe_keys"])
|
||||
expected_keys = set(decision["expected_dedupe_keys"])
|
||||
return bool(sample_keys and sample_keys.issubset(expected_keys))
|
||||
|
||||
|
||||
def _row_keys_match_expected(decision):
|
||||
row_keys = [row["dedupe_key"] for row in decision["decision_rows"]]
|
||||
expected_keys = decision["expected_dedupe_keys"]
|
||||
return bool(
|
||||
row_keys
|
||||
and len(row_keys) == len(set(row_keys))
|
||||
and set(row_keys) == set(expected_keys)
|
||||
)
|
||||
|
||||
|
||||
def _all_rows_have_allowed_decisions(decision):
|
||||
return bool(
|
||||
decision["decision_rows"]
|
||||
and all(
|
||||
row["proposed_review_decision"] in ALLOWED_REVIEW_DECISIONS
|
||||
for row in decision["decision_rows"]
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def _all_rows_have_manual_evidence(decision):
|
||||
return bool(
|
||||
decision["decision_rows"]
|
||||
and all(
|
||||
row["decision_notes_present"] and row["evidence_ref_safe"]
|
||||
for row in decision["decision_rows"]
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def _matched_rows_keep_identity_guards(decision):
|
||||
matched_rows = [
|
||||
row
|
||||
for row in decision["decision_rows"]
|
||||
if row["proposed_review_decision"] == "matched"
|
||||
]
|
||||
if not matched_rows:
|
||||
return True
|
||||
return all(
|
||||
row["evidence_lane"] == "exact_identity"
|
||||
and row["exact_identity_confirmed"]
|
||||
and not row["hard_veto_present"]
|
||||
and not row["stronger_existing_match_conflict"]
|
||||
and row["false_positive_guard_passed"]
|
||||
and row["variant_conflict_checked"]
|
||||
and row["overwrite_protection_checked"]
|
||||
for row in matched_rows
|
||||
)
|
||||
|
||||
|
||||
def build_review_decision_gates(*, inventory_received, decision_received, inventory, decision):
|
||||
operator_confirmed_boundaries = bool(
|
||||
decision["review_inventory_checked"]
|
||||
and decision["candidate_queue_review_only"]
|
||||
and decision["human_decision_only"]
|
||||
and decision["manual_record_required"]
|
||||
and decision["no_approval_token_payload"]
|
||||
and decision["no_api_cli_execution"]
|
||||
and decision["no_api_database_write"]
|
||||
and decision["no_api_inventory_query"]
|
||||
and decision["no_api_review_state_update"]
|
||||
and decision["no_api_queue_insert"]
|
||||
and decision["no_api_file_write"]
|
||||
and decision["no_scheduler_attach"]
|
||||
and decision["stronger_existing_match_guard_preserved"]
|
||||
and decision["false_positive_guard_completed"]
|
||||
and decision["variant_sensitive_review_completed"]
|
||||
and decision["hard_veto_respected"]
|
||||
)
|
||||
return [
|
||||
{
|
||||
"key": "candidate_queue_review_inventory_payload_or_result_received",
|
||||
"label": "已提供 review inventory package 或已審核結果",
|
||||
"passed": inventory_received,
|
||||
},
|
||||
{
|
||||
"key": "candidate_queue_review_inventory_accepted",
|
||||
"label": "review inventory gate 必須已通過",
|
||||
"passed": inventory["accepted"],
|
||||
},
|
||||
{
|
||||
"key": "candidate_queue_review_inventory_ready_for_decision",
|
||||
"label": "inventory 必須只放行到 candidate queue review decision",
|
||||
"passed": inventory["ready_for_candidate_queue_review_decision"],
|
||||
},
|
||||
{
|
||||
"key": "candidate_queue_review_inventory_side_effect_free",
|
||||
"label": "inventory 未顯示 API 執行、寫 DB、查 DB、更新 review_state 或掛 scheduler",
|
||||
"passed": inventory["side_effects_clear"],
|
||||
},
|
||||
{
|
||||
"key": "candidate_queue_review_decision_payload_received",
|
||||
"label": "已提供 operator candidate queue review decision 摘要",
|
||||
"passed": decision_received,
|
||||
},
|
||||
{
|
||||
"key": "candidate_queue_review_decision_identity_recorded",
|
||||
"label": "decision 必須記錄 review_decision_id 與上游 identity",
|
||||
"passed": bool(
|
||||
decision["review_decision_id"]
|
||||
and decision["review_inventory_id"]
|
||||
and decision["handoff_id"]
|
||||
and decision["inventory_review_id"]
|
||||
and decision["closeout_id"]
|
||||
and decision["run_package_id"]
|
||||
and decision["receipt_id"]
|
||||
),
|
||||
},
|
||||
{
|
||||
"key": "candidate_queue_review_decision_identity_matches_inventory",
|
||||
"label": "decision identity 必須對齊 review inventory",
|
||||
"passed": bool(
|
||||
decision["review_inventory_id"] == inventory["review_inventory_id"]
|
||||
and decision["handoff_id"] == inventory["handoff_id"]
|
||||
and decision["inventory_review_id"] == inventory["inventory_review_id"]
|
||||
and decision["closeout_id"] == inventory["closeout_id"]
|
||||
and decision["run_package_id"] == inventory["run_package_id"]
|
||||
and decision["receipt_id"] == inventory["receipt_id"]
|
||||
),
|
||||
},
|
||||
{
|
||||
"key": "candidate_queue_review_decision_target_table_safe",
|
||||
"label": "target table 必須是 market_alert_review_queue",
|
||||
"passed": decision["target_table"] == TARGET_TABLE,
|
||||
},
|
||||
{
|
||||
"key": "candidate_queue_review_decision_scope_safe",
|
||||
"label": "decision 僅能是 human review only",
|
||||
"passed": bool(
|
||||
decision["decision_scope"] == "candidate_queue_review_decision"
|
||||
and decision["decision_mode"] == "human_review_only"
|
||||
and decision["expected_current_review_state"] == "needs_review"
|
||||
),
|
||||
},
|
||||
{
|
||||
"key": "candidate_queue_review_decision_row_count_matches_inventory",
|
||||
"label": "decision row count 必須對齊 inventory dedupe count",
|
||||
"passed": bool(
|
||||
decision["expected_decision_row_count"]
|
||||
and decision["decision_row_count"]
|
||||
== decision["expected_decision_row_count"]
|
||||
== inventory["dedupe_key_count"]
|
||||
== inventory["found_row_count"]
|
||||
and len(decision["decision_rows"]) == inventory["dedupe_key_count"]
|
||||
),
|
||||
},
|
||||
{
|
||||
"key": "candidate_queue_review_decision_dedupe_keys_match_inventory",
|
||||
"label": "decision dedupe keys 必須覆蓋 inventory keys 且逐列對齊",
|
||||
"passed": bool(
|
||||
_sample_keys_covered_by_expected(inventory, decision)
|
||||
and _row_keys_match_expected(decision)
|
||||
),
|
||||
},
|
||||
{
|
||||
"key": "candidate_queue_review_decision_rows_still_needs_review",
|
||||
"label": "所有 decision rows 的目前狀態必須仍是 needs_review",
|
||||
"passed": bool(
|
||||
decision["decision_rows"]
|
||||
and all(
|
||||
row["current_review_state"] == "needs_review"
|
||||
for row in decision["decision_rows"]
|
||||
)
|
||||
),
|
||||
},
|
||||
{
|
||||
"key": "candidate_queue_review_decision_values_allowed",
|
||||
"label": "人工決策只能使用允許的狀態集合",
|
||||
"passed": _all_rows_have_allowed_decisions(decision),
|
||||
},
|
||||
{
|
||||
"key": "candidate_queue_review_decision_row_evidence_complete",
|
||||
"label": "每列決策必須保留 evidence ref 與 notes",
|
||||
"passed": _all_rows_have_manual_evidence(decision),
|
||||
},
|
||||
{
|
||||
"key": "candidate_queue_review_decision_matched_rows_keep_identity_guards",
|
||||
"label": "matched rows 必須保留 exact identity、false-positive、variant 與 overwrite guard",
|
||||
"passed": _matched_rows_keep_identity_guards(decision),
|
||||
},
|
||||
{
|
||||
"key": "candidate_queue_review_decision_artifact_paths_safe",
|
||||
"label": "review queue、inventory 與 decision artifact paths 必須安全並對齊",
|
||||
"passed": bool(
|
||||
decision["review_queue_artifact_path_safe"]
|
||||
and decision["review_inventory_artifact_path_safe"]
|
||||
and decision["review_decision_artifact_path_safe"]
|
||||
and decision["review_queue_artifact_path"]
|
||||
== inventory["review_queue_artifact_path"]
|
||||
and decision["review_inventory_artifact_path"]
|
||||
== inventory["review_inventory_artifact_path"]
|
||||
),
|
||||
},
|
||||
{
|
||||
"key": "candidate_queue_review_decision_operator_boundaries_confirmed",
|
||||
"label": "操作員確認 API 未執行 CLI/DB/query/file/review_state/scheduler,且 guardrail 已完成",
|
||||
"passed": operator_confirmed_boundaries,
|
||||
},
|
||||
{
|
||||
"key": "candidate_queue_review_decision_no_api_execution_or_real_write",
|
||||
"label": "decision payload 不得允許 API execution、real write、review update 或 match write",
|
||||
"passed": bool(
|
||||
not decision["api_execution_allowed"]
|
||||
and not decision["real_write_allowed_by_api"]
|
||||
and not decision["api_candidate_review_allowed"]
|
||||
and not decision["api_updates_review_state"]
|
||||
and not decision["api_writes_match_result"]
|
||||
),
|
||||
},
|
||||
{
|
||||
"key": "candidate_queue_review_decision_no_secret_or_token_key",
|
||||
"label": "decision payload 不得包含 secret、cookie、password 或 token key",
|
||||
"passed": not decision["secret_or_token_submitted_to_api"],
|
||||
},
|
||||
{
|
||||
"key": "candidate_queue_review_decision_side_effect_free",
|
||||
"label": "decision payload 不得要求 API 寫檔、執行、查 DB、寫 DB、補 queue、寫 match 或掛 scheduler",
|
||||
"passed": not decision["blocked_side_effects"],
|
||||
},
|
||||
]
|
||||
@@ -0,0 +1,118 @@
|
||||
"""Sample payload for the market intel candidate queue review decision gate."""
|
||||
|
||||
from copy import deepcopy
|
||||
|
||||
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,
|
||||
)
|
||||
from services.market_intel.mcp_fetch_candidate_queue_writer_review_inventory import (
|
||||
build_mcp_fetch_candidate_queue_writer_review_inventory_preview,
|
||||
)
|
||||
|
||||
|
||||
_SAMPLE_WRITER_REVIEW_DECISION_PACKAGE = None
|
||||
|
||||
|
||||
def build_sample_writer_review_decision_package():
|
||||
global _SAMPLE_WRITER_REVIEW_DECISION_PACKAGE
|
||||
if _SAMPLE_WRITER_REVIEW_DECISION_PACKAGE is not None:
|
||||
return deepcopy(_SAMPLE_WRITER_REVIEW_DECISION_PACKAGE)
|
||||
|
||||
inventory_preview = (
|
||||
build_mcp_fetch_candidate_queue_writer_review_inventory_preview()
|
||||
)
|
||||
inventory_package = inventory_preview["sample_writer_review_inventory_package"]
|
||||
inventory_result = build_mcp_fetch_candidate_queue_writer_review_inventory_preview(
|
||||
writer_review_handoff_package=inventory_package[
|
||||
"writer_review_handoff_package"
|
||||
],
|
||||
writer_review_handoff_result=inventory_package[
|
||||
"writer_review_handoff_result"
|
||||
],
|
||||
operator_review_inventory=inventory_package["operator_review_inventory"],
|
||||
)
|
||||
inventory = inventory_result["operator_review_inventory_summary"]
|
||||
dedupe_keys = inventory["sample_dedupe_keys"]
|
||||
decision_rows = []
|
||||
for index, dedupe_key in enumerate(dedupe_keys):
|
||||
proposed_decision = "matched" if index == 0 else "low_score"
|
||||
decision_rows.append(
|
||||
{
|
||||
"dedupe_key": dedupe_key,
|
||||
"current_review_state": "needs_review",
|
||||
"proposed_review_decision": proposed_decision,
|
||||
"decision_bucket": "recoverable_low_score",
|
||||
"evidence_lane": "exact_identity",
|
||||
"evidence_ref": (
|
||||
ARTIFACT_PREFIX
|
||||
+ f"candidate-queue-review-decision-evidence-{index + 1}.json"
|
||||
),
|
||||
"decision_notes": (
|
||||
"same candidate lane checked; keep human audit trail"
|
||||
),
|
||||
"hard_veto_present": False,
|
||||
"stronger_existing_match_conflict": False,
|
||||
"false_positive_guard_passed": True,
|
||||
"variant_conflict_checked": True,
|
||||
"overwrite_protection_checked": True,
|
||||
"exact_identity_confirmed": proposed_decision == "matched",
|
||||
}
|
||||
)
|
||||
|
||||
operator_review_decision = {
|
||||
"review_decision_id": (
|
||||
"market-intel-candidate-writer-review-decision-sample"
|
||||
),
|
||||
"review_inventory_id": inventory["review_inventory_id"],
|
||||
"handoff_id": inventory["handoff_id"],
|
||||
"inventory_review_id": inventory["inventory_review_id"],
|
||||
"closeout_id": inventory["closeout_id"],
|
||||
"run_package_id": inventory["run_package_id"],
|
||||
"receipt_id": inventory["receipt_id"],
|
||||
"target_table": TARGET_TABLE,
|
||||
"decision_scope": "candidate_queue_review_decision",
|
||||
"decision_mode": "human_review_only",
|
||||
"expected_current_review_state": "needs_review",
|
||||
"decision_row_count": len(decision_rows),
|
||||
"expected_decision_row_count": inventory["dedupe_key_count"],
|
||||
"expected_dedupe_keys": dedupe_keys,
|
||||
"review_queue_artifact_path": inventory["review_queue_artifact_path"],
|
||||
"review_inventory_artifact_path": (
|
||||
inventory["review_inventory_artifact_path"]
|
||||
),
|
||||
"review_decision_artifact_path": (
|
||||
ARTIFACT_PREFIX
|
||||
+ "candidate-queue-review-decision-sample.json"
|
||||
),
|
||||
"decision_rows": decision_rows,
|
||||
"operator_confirmations": {
|
||||
"review_inventory_checked": True,
|
||||
"candidate_queue_review_only": True,
|
||||
"human_decision_only": True,
|
||||
"manual_record_required": True,
|
||||
"no_approval_token_payload": True,
|
||||
"no_api_cli_execution": True,
|
||||
"no_api_database_write": True,
|
||||
"no_api_inventory_query": True,
|
||||
"no_api_review_state_update": True,
|
||||
"no_api_queue_insert": True,
|
||||
"no_api_file_write": True,
|
||||
"no_scheduler_attach": True,
|
||||
"stronger_existing_match_guard_preserved": True,
|
||||
"false_positive_guard_completed": True,
|
||||
"variant_sensitive_review_completed": True,
|
||||
"hard_veto_respected": True,
|
||||
},
|
||||
"api_execution_allowed": False,
|
||||
"real_write_allowed_by_api": False,
|
||||
"api_candidate_review_allowed": False,
|
||||
"api_updates_review_state": False,
|
||||
"api_writes_match_result": False,
|
||||
}
|
||||
_SAMPLE_WRITER_REVIEW_DECISION_PACKAGE = {
|
||||
"writer_review_inventory_package": inventory_package,
|
||||
"writer_review_inventory_result": inventory_result,
|
||||
"operator_review_decision": operator_review_decision,
|
||||
}
|
||||
return deepcopy(_SAMPLE_WRITER_REVIEW_DECISION_PACKAGE)
|
||||
@@ -1,3 +1,3 @@
|
||||
"""市場情報 rollout phase 單一來源。"""
|
||||
|
||||
MARKET_INTEL_PHASE = "phase_136_market_intel_mcp_fetch_candidate_queue_writer_review_inventory"
|
||||
MARKET_INTEL_PHASE = "phase_137_market_intel_mcp_fetch_candidate_queue_writer_review_decision"
|
||||
|
||||
Reference in New Issue
Block a user