198 lines
7.0 KiB
Python
198 lines
7.0 KiB
Python
"""市場情報 seed writer CLI skeleton。
|
||
|
||
本階段只回報 CLI 執行計畫,不建立 DB session、不寫入、不 commit。
|
||
"""
|
||
|
||
import hashlib
|
||
import json
|
||
|
||
|
||
APPROVAL_ENV_VAR = "MARKET_INTEL_SEED_WRITE_APPROVAL"
|
||
PLATFORM_UPSERT_SQL = """
|
||
INSERT INTO market_platforms (
|
||
code,
|
||
name,
|
||
base_url,
|
||
enabled,
|
||
crawl_policy_json
|
||
) VALUES (
|
||
:code,
|
||
:name,
|
||
:base_url,
|
||
:enabled,
|
||
:crawl_policy_json
|
||
)
|
||
ON CONFLICT (code) DO UPDATE SET
|
||
name = EXCLUDED.name,
|
||
base_url = EXCLUDED.base_url,
|
||
enabled = EXCLUDED.enabled,
|
||
crawl_policy_json = EXCLUDED.crawl_policy_json,
|
||
updated_at = NOW()
|
||
""".strip()
|
||
|
||
|
||
def _payload_hash(payload):
|
||
encoded = json.dumps(payload, ensure_ascii=False, sort_keys=True).encode("utf-8")
|
||
return hashlib.sha256(encoded).hexdigest()[:16]
|
||
|
||
|
||
def build_seed_transaction_preview(writer_plan, migration_blueprint):
|
||
"""建立 seed writer transaction preview;不建立 DB session。"""
|
||
statements = []
|
||
for index, operation in enumerate(writer_plan.get("operations", []), start=1):
|
||
values = operation.get("values", {})
|
||
lookup_code = operation.get("lookup", {}).get("code") or values.get("code")
|
||
statements.append(
|
||
{
|
||
"index": index,
|
||
"operation": operation.get("operation", "upsert"),
|
||
"table": operation.get("table", "market_platforms"),
|
||
"lookup": {"code": lookup_code},
|
||
"sql_template": PLATFORM_UPSERT_SQL,
|
||
"parameter_keys": sorted(values),
|
||
"parameter_payload_hash": _payload_hash(values),
|
||
"idempotency_key": f"market_platforms:{lookup_code}",
|
||
"diff_status": "not_loaded_no_db_session",
|
||
"write_status": "blocked_transaction_preview_only",
|
||
}
|
||
)
|
||
|
||
migration_ready = bool(
|
||
migration_blueprint.get("file_created")
|
||
and migration_blueprint.get("file_matches_blueprint")
|
||
and not migration_blueprint.get("migration_executed")
|
||
)
|
||
return {
|
||
"mode": "seed_transaction_preview_no_session",
|
||
"target_table": "market_platforms",
|
||
"statement_count": len(statements),
|
||
"statements": statements,
|
||
"migration_draft_ready": migration_ready,
|
||
"database_snapshot_loaded": False,
|
||
"existing_rows_seen": 0,
|
||
"database_session_created": False,
|
||
"transaction_opened": False,
|
||
"writes_executed": False,
|
||
"would_write_database": False,
|
||
"database_commit_executed": False,
|
||
"database_rollback_executed": False,
|
||
"external_network_executed": False,
|
||
"scheduler_attached": False,
|
||
"required_runtime_order": [
|
||
"backup_verified",
|
||
"migration_applied_by_operator",
|
||
"schema_smoke_passed",
|
||
"feature_flags_reviewed",
|
||
"one_time_approval_token_verified",
|
||
"real_write_implementation_enabled",
|
||
],
|
||
"safety_contract": {
|
||
"idempotent_upsert_preview_only": True,
|
||
"does_not_load_existing_rows": True,
|
||
"does_not_open_transaction": True,
|
||
"does_not_commit": True,
|
||
},
|
||
}
|
||
|
||
|
||
def build_seed_writer_cli_plan(
|
||
*,
|
||
platform_code,
|
||
execute_requested,
|
||
approval_token,
|
||
seed_plan,
|
||
write_guard,
|
||
writer_plan,
|
||
migration_blueprint,
|
||
):
|
||
"""建立 seed writer CLI blocked plan。"""
|
||
approval_token_present = bool(approval_token)
|
||
migration_ready = bool(
|
||
migration_blueprint.get("file_created")
|
||
and migration_blueprint.get("file_matches_blueprint")
|
||
and not migration_blueprint.get("migration_executed")
|
||
)
|
||
gates = [
|
||
{
|
||
"key": "script_created",
|
||
"label": "scripts/market_intel_seed_writer.py exists",
|
||
"passed": True,
|
||
},
|
||
{
|
||
"key": "migration_file_matches_blueprint",
|
||
"label": "migration draft exists and matches the reviewed blueprint",
|
||
"passed": migration_ready,
|
||
},
|
||
{
|
||
"key": "execute_requested",
|
||
"label": "--execute flag was explicitly provided",
|
||
"passed": bool(execute_requested),
|
||
},
|
||
{
|
||
"key": "approval_token_present",
|
||
"label": f"{APPROVAL_ENV_VAR} or --approval-token was provided",
|
||
"passed": approval_token_present,
|
||
},
|
||
{
|
||
"key": "database_write_allowed",
|
||
"label": "runtime database_write_allowed gate is true",
|
||
"passed": bool(write_guard.get("database_write_allowed")),
|
||
},
|
||
{
|
||
"key": "manual_operator_approval",
|
||
"label": "operator approval has been verified out-of-band",
|
||
"passed": False,
|
||
},
|
||
{
|
||
"key": "real_write_implementation_enabled",
|
||
"label": "CLI real write implementation has been enabled",
|
||
"passed": False,
|
||
},
|
||
]
|
||
blocked_reasons = [gate["key"] for gate in gates if not gate["passed"]]
|
||
if execute_requested:
|
||
blocked_reasons.insert(0, "execute_request_blocked_by_skeleton")
|
||
transaction_preview = build_seed_transaction_preview(
|
||
writer_plan=writer_plan,
|
||
migration_blueprint=migration_blueprint,
|
||
)
|
||
|
||
return {
|
||
"mode": "seed_writer_cli_blocked_skeleton",
|
||
"platform_code": platform_code or "all",
|
||
"execute_requested": bool(execute_requested),
|
||
"approval_token_present": approval_token_present,
|
||
"approval_env_var": APPROVAL_ENV_VAR,
|
||
"ready_for_real_write": False,
|
||
"writes_executed": False,
|
||
"would_write_database": False,
|
||
"database_session_created": False,
|
||
"database_commit_executed": False,
|
||
"external_network_executed": False,
|
||
"scheduler_attached": False,
|
||
"exit_code": 2 if execute_requested else 0,
|
||
"blocked_reasons": blocked_reasons,
|
||
"approval_gates": gates,
|
||
"seed_count": int(seed_plan.get("seed_count") or 0),
|
||
"writer_operation_count": int(writer_plan.get("operation_count") or 0),
|
||
"transaction_preview": transaction_preview,
|
||
"write_guard_summary": {
|
||
"ready_to_write": bool(write_guard.get("ready_to_write")),
|
||
"would_write_database": bool(write_guard.get("would_write_database")),
|
||
"database_write_allowed": bool(write_guard.get("database_write_allowed")),
|
||
"blocked_reasons": write_guard.get("blocked_reasons", []),
|
||
},
|
||
"migration_file_summary": {
|
||
"suggested_filename": migration_blueprint.get("suggested_filename"),
|
||
"file_created": bool(migration_blueprint.get("file_created")),
|
||
"file_matches_blueprint": bool(migration_blueprint.get("file_matches_blueprint")),
|
||
"migration_executed": bool(migration_blueprint.get("migration_executed")),
|
||
},
|
||
"safety_contract": {
|
||
"refuses_execute_in_this_phase": True,
|
||
"requires_independent_approval_token": True,
|
||
"keeps_crawler_disabled_for_seed_write": True,
|
||
"no_db_session_in_skeleton": True,
|
||
},
|
||
}
|