#!/usr/bin/env python3
"""scripts/validate_fallback_acceptance_2606_coherence_ext.py

task-2606 (TRACK C) — TASK_2553_PLUS58_MICRO_FIX, additive-only.

Spec: memory/tasks/task-2606.md
(sha256 842758659aa026280c76d27ba6b20e647a239d2e6b0374ebac33f247a8d59037).
Preflight: memory/events/task-2604-multitrack-preflight-decision_260519.json
(TrackC).

회장 §2 / Codex **MEDIUM#1** (additive-only — 기존 +58 산출물 byte-0):

  +58 ``check_criteria_schema_coherence`` 의 12-check 는
  ``marked_by_collector_role`` / ``bound_after_normal_durable_success`` /
  ``expected_on_fire`` 에 대한 schema ↔ criteria ↔ fixture **cross-assert
  를 누락**한다. 세 필드는 §2/§5/§7 의 핵심 안전 의미(ANU-only 마킹·
  durable-success 선행 한정·발화 시 비차단 무동작)를 담지만, 정합 검사가
  그 const/enum 과 criteria·fixture 의 일치를 강제하지 않는다.

본 모듈은 그 공백을 **신규 additive extension entrypoint** 로 메운다:

  * 기존 ``scripts/validate_fallback_acceptance_2553plus58.py`` 를
    **read-only import** (importlib, byte-0 — 어떤 +58 파일도 수정/write
    하지 않음).
  * 기존 ``check_criteria_schema_coherence`` 12-check 를 **그대로 재사용**
    하고, 그 위에 누락된 cross-assert 만 **additive** 로 보강한다.
  * ``evaluate_coherence_ext`` — 실 entrypoint (mock 아님). base coherence
    + extended cross-assert 결과를 dict 로 반환.
  * ``main`` — 실 산출물(criteria·schema·+58 fixture)에 대해 검사 후
    stdout JSON 출력. **파일 write 0** (read-only — §4 allowlist 외 write
    0).

순수/오프라인: network·git mutation·cron·dispatch·cokacdir·subprocess·
파일 write 0. fallback/dead-man/fixed-time 진행 트리거 0.
"""
from __future__ import annotations

import argparse
import importlib.util
import json
import sys
from pathlib import Path
from typing import Any

WORKSPACE = Path(__file__).resolve().parent.parent
PLUS58_VALIDATOR = WORKSPACE / "scripts/validate_fallback_acceptance_2553plus58.py"
CRITERIA_PATH = WORKSPACE / "memory/events/fallback_acceptance_criteria.json"
SCHEMA_PATH = WORKSPACE / "schemas/non_blocking_fallback_schema.json"
FIX58_PATH = WORKSPACE / "memory/fixtures/task-2553plus58.cases.json"

ANU_KEY = "c119085addb0f8b7"
ANU_CHAT_ID = 6937032012

# §2/§5/§7 안전 의미를 담지만 +58 12-check 가 cross-assert 하지 않는 필드.
EXPECTED_ON_FIRE_ENUM = ["DUPLICATE_CALLBACK_IGNORED", "NO-ACTION"]


def _load_plus58_validator():
    """READ-ONLY import of the frozen +58 validator (byte-0).

    The module is loaded straight from disk; no attribute is mutated and
    no +58 file is written. This module only *consumes* the +58 API.
    """
    spec = importlib.util.spec_from_file_location(
        "validate_fallback_acceptance_2553plus58_readonly", PLUS58_VALIDATOR
    )
    if not (spec and spec.loader):  # pragma: no cover - import wiring
        raise RuntimeError("cannot locate frozen +58 validator")
    mod = importlib.util.module_from_spec(spec)
    spec.loader.exec_module(mod)
    return mod


def _load_json(path: Path) -> Any:
    return json.loads(path.read_text(encoding="utf-8"))


def evaluate_coherence_ext(
    criteria: dict | None = None,
    schema: dict | None = None,
    fixture: dict | None = None,
) -> dict:
    """REAL entrypoint — base +58 coherence + additive cross-asserts.

    Returns a dict with ``base`` (verbatim +58 result), ``ext_checks``
    (the additive cross-asserts MEDIUM#1 flagged as missing), and an
    overall ``coherent`` that is True only when *both* hold.
    """
    base_mod = _load_plus58_validator()
    criteria = dict(criteria if criteria is not None else _load_json(CRITERIA_PATH))
    schema = dict(schema if schema is not None else _load_json(SCHEMA_PATH))
    fixture = dict(fixture if fixture is not None else _load_json(FIX58_PATH))

    # 1. base 12-check — 기존 +58 entrypoint 를 그대로 호출 (재구현 아님).
    base = base_mod.check_criteria_schema_coherence(criteria, schema)

    props = schema.get("properties", {})
    required = set(schema.get("required", []))
    cb = criteria.get("criterion_b", {})
    anti = criteria.get("anti_pattern", {})
    decision_table = criteria.get("decision_table", [])

    ext: dict[str, bool] = {}

    # ── marked_by_collector_role: schema const ANU ↔ criteria ↔ fixture ──
    mbcr = props.get("marked_by_collector_role", {})
    ext["mbcr_schema_const_anu"] = mbcr.get("const") == "ANU"
    ext["mbcr_in_schema_required"] = "marked_by_collector_role" in required
    # criteria 가 ANU-only 마킹/ self-* 무효를 의미적으로 명시.
    ext["mbcr_criteria_anu_only"] = (
        "ANU" in cb.get("definition", "") + cb.get("safety", "")
        or "ANU" in json.dumps(criteria.get("references", {}), ensure_ascii=False)
    )
    # fixture 의 valid/legacy 마크는 ANU 로, malformed 케이스는 binding 으로만
    # 무효(즉 collector_role 자체는 ANU const 를 위반하지 않아야 정합).
    valid_marks = [
        fixture.get("valid_non_blocking_mark", {}),
        fixture.get("legacy_pending_mark", {}),
    ]
    ext["mbcr_fixture_valid_marks_anu"] = all(
        m.get("marked_by_collector_role") == "ANU" for m in valid_marks
    )

    # ── bound_after_normal_durable_success: schema const true ↔ criteria ──
    bands = props.get("bound_after_normal_durable_success", {})
    ext["bands_schema_const_true"] = bands.get("const") is True
    ext["bands_in_schema_required"] = (
        "bound_after_normal_durable_success" in required
    )
    # criteria.applicability 가 durable-success 선행 한정·미선행 NOT_APPLICABLE.
    appl = criteria.get("applicability", {})
    ext["bands_criteria_durable_precedence"] = (
        "durable-success" in appl.get("scope", "")
        and "NOT_APPLICABLE" in appl.get("out_of_scope", "")
    )
    ext["bands_fixture_valid_marks_true"] = all(
        m.get("bound_after_normal_durable_success") is True
        for m in valid_marks
    )

    # ── expected_on_fire: schema enum ↔ criteria anti_pattern handling set ──
    eof = props.get("expected_on_fire", {})
    ext["eof_schema_enum_matches"] = sorted(eof.get("enum", [])) == sorted(
        EXPECTED_ON_FIRE_ENUM
    )
    ext["eof_in_schema_required"] = "expected_on_fire" in required
    # criteria.anti_pattern 정의가 동일 handling set {DUPLICATE…, NO-ACTION}.
    anti_def = anti.get("definition", "")
    ext["eof_criteria_anti_pattern_handling_set"] = (
        "DUPLICATE_CALLBACK_IGNORED" in anti_def and "NO-ACTION" in anti_def
    )
    # decision_table 의 idle handling 집합이 schema enum 과 정합.
    dt_blob = json.dumps(decision_table, ensure_ascii=False)
    ext["eof_decision_table_handling_set"] = (
        "DUPLICATE_CALLBACK_IGNORED" in dt_blob and "NO-ACTION" in dt_blob
    )
    ext["eof_fixture_valid_marks_in_enum"] = all(
        m.get("expected_on_fire") in EXPECTED_ON_FIRE_ENUM
        for m in valid_marks
    )

    ext_coherent = all(ext.values())
    return {
        "schema": "task-2606.coherence_ext.v1",
        "coherent": bool(base.get("coherent")) and ext_coherent,
        "base_coherent": bool(base.get("coherent")),
        "ext_coherent": ext_coherent,
        "base": base,
        "ext_checks": ext,
        "plus58_validator": "scripts/validate_fallback_acceptance_2553plus58.py",
        "plus58_modified": False,
        "writes_performed": 0,
        "anu_key": ANU_KEY,
        "anu_chat_id": ANU_CHAT_ID,
    }


def main(argv: list[str] | None = None) -> int:
    ap = argparse.ArgumentParser(
        description="task-2606 coherence extension (read-only; additive "
        "cross-asserts over the frozen +58 12-check; no file writes)."
    )
    ap.add_argument(
        "--json",
        action="store_true",
        help="결과 JSON 을 stdout 으로 출력 (default on).",
    )
    ap.parse_args(argv)

    result = evaluate_coherence_ext()
    print(json.dumps(result, ensure_ascii=False, indent=2))
    return 0 if result["coherent"] else 1


if __name__ == "__main__":
    sys.exit(main())
