#!/usr/bin/env python3 """Classify MOMO daily-sales source arrival from a read-only preflight log. This parser never connects to MOMO, never imports files, never moves Drive artifacts, and never authorizes DB / host / Drive writes. It turns the existing `momo-drive-token-source-recovery-preflight.sh` evidence into a compact gate so operators can tell whether they should keep waiting for a legitimate source or start a separate safe-import preflight. """ from __future__ import annotations import argparse import json import re import sys from pathlib import Path from typing import Any EXPECTED_IMPORT_CONFIG = "當日業績匯入|即時業績_當日" SUMMARY_RE = re.compile( r"^MOMO_DRIVE_TOKEN_SOURCE_PREFLIGHT " r"PASS=(?P\d+) WARN=(?P\d+) BLOCKED=(?P\d+) " r"HOST=(?P\S+) FRESHNESS_MAX_DAYS=(?P\d+)" ) def parse_args() -> argparse.Namespace: parser = argparse.ArgumentParser( description="Classify MOMO source-arrival readiness from preflight output.", ) parser.add_argument( "--preflight-log", required=True, help="Path to momo-drive-token-source-recovery-preflight output, or '-' for stdin.", ) parser.add_argument("--json", action="store_true", help="Print JSON result.") return parser.parse_args() def load_text(source: str) -> str: if source == "-": return sys.stdin.read() return Path(source).read_text(encoding="utf-8") def parse_int(value: Any, default: int | None = None) -> int | None: try: return int(str(value).strip()) except (TypeError, ValueError): return default def parse_pipe(value: str, expected_parts: int) -> list[str]: parts = str(value or "").split("|") if len(parts) < expected_parts: parts.extend([""] * (expected_parts - len(parts))) return parts[:expected_parts] def date_prefix(value: str) -> str: candidate = str(value or "")[:10] if re.match(r"^\d{4}-\d{2}-\d{2}$", candidate): return candidate return "" def parse_preflight(text: str) -> dict[str, Any]: values: dict[str, str] = {} messages = {"ok": [], "warn": [], "blocked": []} summary: dict[str, Any] = {} for raw_line in text.splitlines(): line = raw_line.strip() if not line: continue summary_match = SUMMARY_RE.match(line) if summary_match: summary = { key: parse_int(value) if key != "host" else value for key, value in summary_match.groupdict().items() } continue if line.startswith("OK: "): messages["ok"].append(line[4:]) continue if line.startswith("WARN: "): messages["warn"].append(line[6:]) continue if line.startswith("BLOCKED: "): messages["blocked"].append(line[9:]) continue if re.match(r"^[A-Z][A-Z0-9_]+(?:\s|$)", line): key, _, value = line.partition(" ") values[key] = value.strip() return {"values": values, "messages": messages, "summary": summary} def monthly_sync_ok(value: str) -> bool: snapshot_count, monthly_count, dmin, dmax, mmin, mmax = parse_pipe(value, 6) snapshot_n = parse_int(snapshot_count, 0) or 0 return ( snapshot_n > 0 and snapshot_count == monthly_count and bool(dmin) and bool(dmax) and dmin == mmin and dmax == mmax ) def latest_import_clean(value: str) -> bool: job_id, status, _file_name, _created, _completed, total, success, errors = parse_pipe( value, 8 ) return ( parse_int(job_id) is not None and status == "completed" and parse_int(total, -1) == parse_int(success, -2) and parse_int(errors, -1) == 0 ) def no_newer_drive_source(values: dict[str, str], clean_import: bool) -> bool: if not clean_import: return False drive_intake_count = parse_int(values.get("DRIVE_INTAKE_COUNT"), 0) or 0 drive_failed_count = parse_int(values.get("DRIVE_FAILED_COUNT"), 0) or 0 if drive_intake_count > 0 or drive_failed_count > 0: return False global_latest = date_prefix(values.get("DRIVE_GLOBAL_LATEST_MODIFIED", "")) _job_id, _status, _file_name, _created, completed, *_rest = parse_pipe( values.get("DB_LATEST_DAILY_IMPORT_JOB", ""), 8, ) completed_date = date_prefix(completed) return bool(global_latest and completed_date and global_latest <= completed_date) def monthly_sync_status(value: str, source_absent_clean: bool) -> str: if monthly_sync_ok(value): return "in_sync" snapshot_count, monthly_count, dmin, dmax, mmin, mmax = parse_pipe(value, 6) if ( source_absent_clean and snapshot_count == "0" and monthly_count == "0" and dmin in {"", "-"} and dmax in {"", "-"} and mmin in {"", "-"} and mmax in {"", "-"} ): return "not_applicable_no_current_month_source" return "not_proven" def classify(parsed: dict[str, Any]) -> dict[str, Any]: values = parsed["values"] summary = parsed["summary"] messages = parsed["messages"] freshness_days_text, latest_daily_date = parse_pipe(values.get("DB_DAILY_FRESHNESS", ""), 2) freshness_days = parse_int(freshness_days_text) freshness_max_days = parse_int(summary.get("freshness_max_days"), 2) or 2 drive_intake_count = parse_int(values.get("DRIVE_INTAKE_COUNT"), 0) or 0 drive_failed_count = parse_int(values.get("DRIVE_FAILED_COUNT"), 0) or 0 drive_archive_latest = values.get("DRIVE_ARCHIVE_LATEST_MODIFIED", "none") or "none" drive_global_latest = values.get("DRIVE_GLOBAL_LATEST_MODIFIED", "none") or "none" service_ready = ( values.get("MOMO_PUBLIC_HEALTH_CODE") == "200" and values.get("MOMO_HEALTH_CODE") == "200" and values.get("MOMO_APP_HEALTH") == "healthy" and values.get("SCHEDULER_RUNNING") == "true" and values.get("SCHEDULER_HEALTH") == "healthy" ) import_config_ok = EXPECTED_IMPORT_CONFIG in values.get("IMPORT_CONFIG", "") clean_import = latest_import_clean(values.get("DB_LATEST_DAILY_IMPORT_JOB", "")) source_absent_clean = no_newer_drive_source(values, clean_import) sync_status = monthly_sync_status( values.get("DB_MONTHLY_SYNC", ""), source_absent_clean, ) sync_ok = sync_status != "not_proven" freshness_green = ( freshness_days is not None and 0 <= freshness_days <= freshness_max_days ) freshness_stale = freshness_days is not None and freshness_days > freshness_max_days blockers: list[str] = [] warnings: list[str] = [] status = "blocked_preflight_evidence_incomplete" next_step = "rerun_momo_drive_token_source_recovery_preflight" safe_import_preflight_allowed = False exit_code = 2 if not summary: blockers.append("preflight_summary_missing") if not service_ready: blockers.append("momo_service_or_scheduler_not_ready") if not import_config_ok: blockers.append("drive_import_config_not_expected_intake") if sync_status == "not_proven": blockers.append("current_month_snapshot_realtime_sync_not_proven") elif sync_status == "not_applicable_no_current_month_source": warnings.append("current_month_sync_not_applicable_no_current_month_source") if drive_failed_count > 0: warnings.append("drive_failed_folder_has_matching_candidates") if blockers: status = "blocked_service_or_evidence_not_ready" next_step = "repair_readonly_preflight_evidence_before_source_or_import_decision" elif freshness_green: status = "freshness_already_green_recheck_cold_start" next_step = "rerun_post_reboot_readiness_summary_with_same_evidence_chain" exit_code = 0 elif drive_intake_count > 0 and freshness_stale: status = "source_arrived_ready_for_safe_import_preflight" next_step = "run_owner_approved_safe_import_preflight_no_db_or_drive_write_yet" safe_import_preflight_allowed = True exit_code = 0 elif drive_intake_count > 0: status = "source_arrived_freshness_unknown_recheck_before_import" next_step = "rerun_momo_preflight_and_validate_freshness_before_import" safe_import_preflight_allowed = True exit_code = 1 elif freshness_stale and source_absent_clean: status = "source_absent_no_newer_drive_waiting" next_step = "wait_for_legitimate_daily_sales_source_then_rerun_gate" exit_code = 1 elif freshness_stale: status = "blocked_source_absent_fail_closed" next_step = "wait_for_legitimate_daily_sales_source_then_rerun_gate" else: status = "blocked_freshness_unknown_fail_closed" next_step = "rerun_preflight_or_repair_readonly_freshness_readback" if not clean_import: warnings.append("latest_daily_import_job_not_clean_completed") return { "schema_version": "momo_source_arrival_gate_v1", "status": status, "exit_code": exit_code, "next_step": next_step, "safe_import_preflight_allowed": safe_import_preflight_allowed, "runtime_write_authorized": False, "db_write_authorized": False, "drive_move_authorized": False, "manual_import_authorized": False, "secret_value_collection_allowed": False, "service_ready": service_ready, "import_config_ok": import_config_ok, "current_month_sync_ok": sync_ok, "current_month_sync_status": sync_status, "latest_import_clean": clean_import, "source_absent_without_newer_drive": source_absent_clean, "freshness_days": freshness_days, "freshness_latest_date": latest_daily_date or "unknown", "freshness_max_days": freshness_max_days, "drive_intake_count": drive_intake_count, "drive_archive_latest_modified": drive_archive_latest, "drive_global_latest_modified": drive_global_latest, "drive_failed_count": drive_failed_count, "preflight_pass": summary.get("pass", 0), "preflight_warn": summary.get("warn", len(messages["warn"])), "preflight_blocked": summary.get("blocked", len(messages["blocked"])), "blockers": blockers, "warnings": warnings, "no_false_green_rules": [ "source_arrived_does_not_authorize_import", "safe_import_preflight_allowed_does_not_authorize_db_write", "freshness_green_requires_post_reboot_summary_recheck", "archive_or_local_old_file_does_not_count_as_new_source", ], } def print_human(result: dict[str, Any]) -> None: print( "MOMO_SOURCE_ARRIVAL_GATE " f"status={result['status']} " f"source_intake={result['drive_intake_count']} " f"freshness={result['freshness_days']}|{result['freshness_latest_date']} " f"safe_import_preflight_allowed={int(result['safe_import_preflight_allowed'])} " "runtime_write_authorized=0 " "db_write_authorized=0 " "drive_move_authorized=0 " f"next_step={result['next_step']}" ) def main() -> int: args = parse_args() result = classify(parse_preflight(load_text(args.preflight_log))) if args.json: print(json.dumps(result, ensure_ascii=False, indent=2, sort_keys=True)) else: print_human(result) return int(result["exit_code"]) if __name__ == "__main__": raise SystemExit(main())