#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
task2553_closeout_collect.py — task-2553 FINAL CLOSEOUT read-only collector
(executor: dev5-team 마르둑, task-2553+50, 1회 한정 · TRACK 1)

목적
----
+32 / +37 / +38~+41 / +44_46 / +47 / +48 / +49 전체 산출물을 *read-only* 로
종합하여 하나의 final closeout 정형 산출물(JSON/MD)을 생성한다.

원칙 (회장 §5 / §6 / §7 / 9-R)
------------------------------
* READ-ONLY CONSUME: 기존 +32~+49 산출물·frozen anchor·policy_profile_engine
  은 byte-0 로 읽기만 한다. 본 스크립트는 어떤 기존 파일도 수정/병합/삭제하지
  않는다.
* Layer A NO-CRON: subprocess / os.system / Popen / cokacdir / cron / dispatch
  호출 0. git 정보는 `.git/HEAD` / ref 파일 직접 읽기로만 취득(subprocess 미사용).
* WRITE SURFACE = §4 expected_files allowlist 한정. 그 외 경로 write 0.
* 본 스크립트는 closeout 을 *근거로* 어떤 merge/write/dispatch 도 실행하지
  않는다(§5). pending 항목을 완료로 과장하지 않는다(§5) — 미충족 항목은
  remaining-backlog 에 OPEN 으로 명시 기록한다.

산출 (회장 §3)
---------------
1. memory/events/task-2553.final-closeout.decision.json
2. memory/events/task-2553.final-closeout.result.json
3. memory/reports/task-2553.final-closeout-consolidated-summary_260518.md
4. memory/events/task-2553.remaining-backlog_260518.json
5. memory/events/task-2553.operational-pilot-readiness_260518.json
6. memory/events/task-2553+50.decision.json   (executor task record)
7. memory/events/task-2553+50.result.json      (executor task record)
8. memory/reports/task-2553+50.md               (executor task report)

사용
----
    python3 scripts/task2553_closeout_collect.py [--root /home/jay/workspace] \
        [--out-root /home/jay/workspace] [--dry-run]

--dry-run 은 검증/수집만 수행하고 write 0(regression 용).
"""
from __future__ import annotations

import argparse
import hashlib
import json
import os
from datetime import datetime, timezone, timedelta

CANONICAL_ROOT = "/home/jay/workspace"  # CLAUDE.md §1 canonical workspace root
KST = timezone(timedelta(hours=9))
TS_KST = "2026-05-18"  # closeout batch date (회장 3-track 병렬 Track 1)

# ── frozen anchor byte-0 기대 sha256 (+49 invariants_post.frozen_byte0 정본) ──
FROZEN_BYTE0 = {
    "anu_v3/callback_4tuple_registry.py":
        "774d550628410d36962c23a7663c4b6dbf72789de7c7fd940871e9ad8280e5ab",
    "dispatch/executor_completion_contract.py":
        "364caa11904285657abd716d78c5493b1f8b519318387d0f864fb6a136dca0b4",
    "anu_v3/callback_event_trigger.py":
        "352ad0f570e55040e7c1e4a32cbfe0f076cbd53529b4db6222a8da1a4bee9cc5",
    "utils/anu_delegation_completion_callback.py":
        "83b3e307c8207c76a3e311c408aab4951373bd317896e51687d3007907b0c3d4",
    "anu_v3/policy_profile_engine.py":
        "2363e291a0a43884892f5e554f115481a077322bd5caa3000fb75bf5b72bc6be",
    "anu_v3/parallel_batch_coordinator.py":
        "10529421110b3d2765785b6cf911527c8f5e964b5078fcfa6190fcb86d0f2c0f",
}

EXPECTED_GIT_BRANCH = "task/task-2553p1-f1-clean-replacement"

# ── §4 expected_files allowlist (이 외 write 0; Track2/3 DISJOINT) ──
ALLOWLIST = (
    "memory/events/task-2553.final-closeout.decision.json",
    "memory/events/task-2553.final-closeout.result.json",
    "memory/reports/task-2553.final-closeout-consolidated-summary_260518.md",
    "memory/events/task-2553.remaining-backlog_260518.json",
    "memory/events/task-2553.operational-pilot-readiness_260518.json",
    "scripts/task2553_closeout_collect.py",
    "tests/regression/test_task2553_closeout_collect_2553plus50.py",
    "memory/events/task-2553+50.decision.json",
    "memory/events/task-2553+50.result.json",
    "memory/reports/task-2553+50.md",
)

# closeout 이 read-only consume 하는 upstream 산출물(존재 확인 대상; 무수정).
CONSUMED_ARTIFACTS = (
    "memory/events/task-2553+32.result.json",
    "memory/reports/task-2553+32.md",
    "memory/events/task-2553+37.result.json",
    "memory/events/task-2553+37.decision.json",
    "memory/events/task-2553+37.cancel-audit.json",
    "memory/events/task-2553+38.result.json",
    "memory/reports/task-2553+38.md",
    "memory/events/task-2553+39.result.json",
    "memory/reports/task-2553+39.md",
    "memory/events/task-2553+40.result.json",
    "memory/reports/task-2553+40.md",
    "memory/events/task-2553+41.result.json",
    "memory/reports/task-2553+41.md",
    "memory/events/task-2553+44_46.result.json",
    "memory/reports/task-2553+44_46.md",
    "memory/events/task-2553+47.collector-authoritative.result.json",
    "memory/events/task-2553+47.next-action-proposal.json",
    "memory/reports/task-2553+47.md",
    "memory/events/task-2553+48.result.json",
    "memory/events/task-2553+48.cancel-audit.json",
    "memory/reports/task-2553+48.md",
    "memory/events/task-2553+49.result.json",
    "memory/events/task-2553+49.decision.json",
    "memory/reports/task-2553+49.md",
    "memory/events/callback_4tuple_index.jsonl",
)


# ───────────────────────── read-only helpers ─────────────────────────
def _sha256(path: str) -> str:
    h = hashlib.sha256()
    with open(path, "rb") as fh:
        for chunk in iter(lambda: fh.read(65536), b""):
            h.update(chunk)
    return h.hexdigest()


def verify_frozen_byte0(root: str) -> dict:
    """frozen anchor 6모듈 byte-0 sha256 일치 검증(read-only)."""
    out = {}
    for rel, expected in FROZEN_BYTE0.items():
        ap = os.path.join(root, rel)
        if not os.path.isfile(ap):
            out[rel] = {"present": False, "equal": False, "expected": expected}
            continue
        actual = _sha256(ap)
        out[rel] = {
            "present": True,
            "expected": expected,
            "actual": actual,
            "equal": actual == expected,
        }
    return out


def read_git_invariant(root: str) -> dict:
    """subprocess 미사용 — .git/HEAD + ref 파일 직접 읽기."""
    head_path = os.path.join(root, ".git", "HEAD")
    branch = None
    sha = None
    try:
        with open(head_path, "r", encoding="utf-8") as fh:
            head = fh.read().strip()
        if head.startswith("ref:"):
            ref = head.split(" ", 1)[1].strip()
            branch = ref[len("refs/heads/"):] if ref.startswith("refs/heads/") else ref
            ref_path = os.path.join(root, ".git", ref)
            if os.path.isfile(ref_path):
                with open(ref_path, "r", encoding="utf-8") as rf:
                    sha = rf.read().strip()
            else:
                packed = os.path.join(root, ".git", "packed-refs")
                if os.path.isfile(packed):
                    with open(packed, "r", encoding="utf-8") as pf:
                        for line in pf:
                            line = line.strip()
                            if line.endswith(ref) and not line.startswith("#"):
                                sha = line.split(" ", 1)[0]
                                break
        else:
            sha = head
    except OSError:
        pass
    return {
        "branch": branch,
        "head_sha": sha,
        "branch_matches_expected": branch == EXPECTED_GIT_BRANCH,
    }


def verify_consumed_present(root: str) -> dict:
    present, missing = [], []
    for rel in CONSUMED_ARTIFACTS:
        (present if os.path.isfile(os.path.join(root, rel)) else missing).append(rel)
    return {"present": present, "missing": missing, "all_present": not missing}


def read_durable_registry(root: str) -> dict:
    """+44 durable 4-tuple append-only ledger read-only 집계."""
    rel = "memory/events/callback_4tuple_index.jsonl"
    ap = os.path.join(root, rel)
    records, completed = [], []
    if os.path.isfile(ap):
        try:
            with open(ap, "r", encoding="utf-8") as fh:
                for line in fh:
                    line = line.strip()
                    if not line:
                        continue
                    try:
                        rec = json.loads(line)
                    except (json.JSONDecodeError, TypeError):
                        continue  # fail-safe: corrupt line skip (+44 doctrine)
                    records.append(rec)
                    if rec.get("status") == "COMPLETED":
                        completed.append(rec)
        except OSError:
            pass
    by_task = {}
    for rec in completed:
        tid = rec.get("task_id")
        by_task.setdefault(tid, []).append({
            "task_id": tid,
            "normal_collector_cron_id": rec.get("normal_collector_cron_id"),
            "fallback_callback_cron_id": rec.get("fallback_callback_cron_id"),
            "chat_id": rec.get("chat_id"),
            "role": rec.get("role"),
            "status": rec.get("status"),
            "ts_kst": rec.get("ts_kst"),
        })
    return {
        "ledger_path": rel,
        "total_records": len(records),
        "completed_records": len(completed),
        "completed_durable_success_tasks": sorted(
            t for t in by_task if t is not None),
        "completed_by_task": by_task,
    }


def policy_profile_seam_status(root: str) -> dict:
    """C1 engine seam 완료 범위 + final_closeout profile 부재 확인(read-only)."""
    pp_dir = os.path.join(root, "memory", "policy_profiles")
    instances = []
    if os.path.isdir(pp_dir):
        instances = sorted(p for p in os.listdir(pp_dir) if p.endswith(".json"))
    final_closeout_profile_present = "task_2553_final_closeout.json" in instances
    return {
        "engine_module": "anu_v3/policy_profile_engine.py",
        "engine_byte0_frozen": True,
        "engine_role": "PURE CONTRACT DERIVER (status ∈ {RESOLVED, HOLD_FOR_CHAIR})",
        "dispatch_selection_seam": "anu_v3/dispatch_profile_selection.py (+38, additive, read-only consume)",
        "coordinator_binding_seam": "anu_v3/coordinator_profile_binding.py (+39, file-level contract, zero import coupling)",
        "historical_case_instances": instances,
        "historical_dry_run_match": "+40 4/4 contract-level fidelity, engine byte-0",
        "task_2553_final_closeout_profile_present": final_closeout_profile_present,
        "task_2553_final_closeout_profile_owner": "Track3 (mapping 신설 — 본 Track1 범위 외)",
        "task_2553_final_closeout_profile_observation": (
            "OBSERVED_PRESENT — 병렬 Track3 가 read-only 관측 시점 이미 landed "
            "(Track1 무관·무변·read-only 관측만)"
            if final_closeout_profile_present else
            "OBSERVED_ABSENT — Track3 mapping 신설 대기"),
        "engine_auto_resolve_eligibility": (
            "task_2553_final_closeout profile PRESENT(Track3 병렬 landed, read-only "
            "관측) → 본 goal_type engine-auto-resolve 등록 가능 (pilot readiness 기록)"
            if final_closeout_profile_present else
            "Track3 완료 후 task_2553_final_closeout goal_type engine-auto-resolve "
            "등록 가능 (현 시점 ABSENT — pilot readiness 에 기록)"),
        "track1_disjoint_note": (
            "Track1 은 policy_profiles 에 write 0 — 디렉터리 목록 read-only 관측만. "
            "Track3 산출물 무수정(§4 Track2/3 DISJOINT 준수)."),
    }


# ───────────────────── 9-구분 분석 모델 (회장 §2 1~9) ─────────────────────
def build_classification(reg: dict, ppe: dict) -> dict:
    fc_present = ppe.get("task_2553_final_closeout_profile_present", False)
    pilot1 = (
        {"id": "C-PILOT-1", "candidate": "policy profile engine operational seam — task_2553_final_closeout profile(Track3 병렬 landed, read-only 관측 PRESENT) 등록 후 본 goal_type engine-auto-resolve 운영 활성화.", "blocked_by": "Track3 mapping 운영 승격 결정(profile 자체는 PRESENT)", "priority": "HIGH"}
        if fc_present else
        {"id": "C-PILOT-1", "candidate": "policy profile engine operational seam — task_2553_final_closeout profile(Track3 신설) 등록 후 본 goal_type engine-auto-resolve. 현재 ABSENT.", "blocked_by": "Track3 mapping 신설", "priority": "HIGH"}
    )
    bl5 = (
        {"id": "BL-5", "ref": "policy-engine", "item": "task_2553_final_closeout profile — 병렬 Track3 가 landed(read-only 관측 PRESENT). engine-auto-resolve 운영 승격 잔여.", "severity": "LOW(dependency)", "status": "OBSERVED_RESOLVED_BY_TRACK3"}
        if fc_present else
        {"id": "BL-5", "ref": "policy-engine", "item": "task_2553_final_closeout profile 미생성(Track3) — engine-auto-resolve 의존 backlog", "severity": "LOW(dependency)", "status": "OPEN_TRACK3"}
    )
    return {
        "cat1_completed_structural_guarantees": {
            "title": "완료된 구조적 보장",
            "items": [
                {"ref": "+32", "guarantee": "executor completion callback MANDATORY rule 코드 강제 복원 — inject_completion_callback_clause(다음 prompt 자동) + validate_spec(누락 FAIL/HOLD) + validate_4tuple(normal_collector_cron_id 필수) + validate_closeout_evidence. NO-CRON≠callback 금지 정정 코드화. regression 20 PASS.", "status": "DONE"},
                {"ref": "+44_46", "guarantee": "durable 4-tuple append-only registry(memory/events/callback_4tuple_index.jsonl, registry-first classify, NO_LEDGER→fail-safe DEFER) + canonical artifact root resolver(/home/jay/workspace hardcoded, read-only). 1회성 cron 자동삭제 후에도 durable. regression 34 PASS.", "status": "DONE"},
                {"ref": "+47", "guarantee": "registry write-back(verified normal-collector identity 바인딩, append-only·idempotent, naive mark_completed stale copy 결함 회피, callback mandatory 무약화) + event-trigger(registry COMPLETED → NEXT_ACTION_READY, 고정시각/dead-man 진행트리거 금지=FORBIDDEN_TRIGGER_SOURCE, proposal-only). regression 16 PASS.", "status": "DONE"},
                {"ref": "+49(AUTHORITATIVE)", "guarantee": "executor self-callback/self-collector/self-adjudication/self-dispatch 실 runtime 구조적 차단 — callback_owner_validator + self_collector_guard + authoritative_verdict_selector + writeback_binding_conflict_guard 가 dispatch.core/dispatch.py/prompt(ADDITIVE re-export) + cron_dispatch_guard 에 결선. mismatch 시 CallbackRegistrationBlocked raise. 다음 dispatch 부터 executor self-collector 구조적 불가. regression 합계 164 PASS.", "status": "DONE"},
                {"ref": "frozen", "guarantee": "frozen anchor 6모듈 byte-0 전 체인 보존(callback_4tuple_registry 774d5506… · executor_completion_contract 364caa11… · callback_event_trigger 352ad0f5… · anu_delegation 83b3e307… · policy_profile_engine 2363e291… · parallel_batch_coordinator 10529421…). git HEAD/branch 전 체인 EQUAL(commit/push/merge 0).", "status": "DONE"},
            ],
        },
        "cat2_independent_anu_verified": {
            "title": "독립 ANU 검증 완료 항목 (authoritative PASS)",
            "items": [
                {"ref": "+44_46", "verdict": "PASS (collector authoritative — executor draft supersede). 독립 단일 Codex audit 3 HIGH(fail-open 마스킹) → ANU 사후 adjudication MEDIUM non-blocking 강등(§3.C acceptance 충족·spec-relevant 경로 regression 10/11/12 차단) + MANDATORY follow-up(tri-state 경화).", "supersedes_executor_draft": True},
                {"ref": "+47", "verdict": "PASS (collector-authoritative supersede executor draft). Codex round-1 3 HIGH → F1/F2 BY-DESIGN HIGH→LOW 강등, F3 VALID→REMEDIATED(REG_SHA 리터럴 핀). round-2 AUDIT CLEAN.", "supersedes_executor_draft": True},
                {"ref": "+48", "verdict": "PASS (collector-authoritative). Codex round-1 2 HIGH(F1 모호 multi-fallback / F2 normal_success_unchanged 하드코딩) → 양건 VALID→REMEDIATE → round-2 AUDIT CLEAN.", "supersedes_executor_draft": True},
                {"ref": "+49", "verdict": "AUTHORITATIVE_PASS. executor(dev2 오딘) self-Codex/self-adjudication/self-collector/self-dispatch 0(코드 강제). Codex audit·adjudication·회수·독립검증·후속 = 독립 ANU collector 세션. authoritative_verdict_selector 가 self-chain QUARANTINED·independent ANU verdict 만 authoritative 로 코드화.", "supersedes_executor_draft": True},
            ],
            "registry_durable_success_completed_tasks": reg["completed_durable_success_tasks"],
        },
        "cat3_live_observation_remaining": {
            "title": "live observation 만 남은 항목",
            "items": [
                {"ref": "+41", "remaining": "cancel-on-success 6-step read-only passive observation harness — mock/fixture 한정(FakeCronLister/SpyRemover/SpyScheduleHistory). 실 cron/4-tuple/schedule_history/발화 0. Codex infra-unavailable(ChatGPT account 'model not supported') = 미수행이며 미해소 finding 0(HOLD 아님), 수동 self-audit clean.", "tier": "mock_only"},
                {"ref": "+48", "remaining": "cancel-on-success live remove e2e — properly-bound 4-tuple, operational=True real-mode seam + 실제 cron-remove 호출이나 전용 격리 FakeCronWorld/WorldSpyRemover. real_ops_cron_force_delete_count=0. fallback_fired_count=0 end-to-end·normal_success_unchanged=true 입증. 진성 production live(실 운영 fallback cron 제거) tier 는 미수행(Layer A NO-CRON 설계상).", "tier": "isolated_e2e"},
                {"ref": "+47", "remaining": "event-trigger next_action = proposal-only(authority=none). 실 dispatch enact 는 authorized ANU 세션 한정 — +48 entry(event-driven, 독립 ANU collector 가 dev1 Hermes 위임 실행)로 1회 관측됨. 후속 dispatch 의 실 운영 관측은 누적 pilot 대상.", "tier": "proposal_then_event_driven"},
            ],
            "true_production_live_status": "NOT_PERFORMED (by Layer A NO-CRON design; operational pilot 후보 C2)",
        },
        "cat4_next_operational_pilot_candidates": {
            "title": "다음 operational pilot 후보",
            "candidates": [
                pilot1,
                {"id": "C-PILOT-2", "candidate": "cancel-on-success live remove 를 실(비격리) 운영 normal-success 사이클에서 1회 관측. 현재 isolated/e2e 입증만.", "blocked_by": "별도 회장 GO(실 운영 cron 접촉)", "priority": "MEDIUM"},
                {"id": "C-PILOT-3", "candidate": "+44_46 tri-state probe 경화(seen/not_seen/indeterminate; indeterminate→RESULT_MISSING 금지) operational rollout.", "blocked_by": "별도 회장 GO(+45 scope 흡수 가능)", "priority": "HIGH"},
                {"id": "C-PILOT-4", "candidate": "registry write-back lifecycle 를 상시 normal-callback collector 기본 경로로 승격(현 +47 dogfood 입증).", "blocked_by": "운영 승격 결정", "priority": "MEDIUM"},
                {"id": "C-PILOT-5", "candidate": "authoritative_verdict_selector + callback_owner_validator 를 N회 실 dispatch 누적 관측(executor-self 구조적 거부 입증). 이미 결선됨 — pilot=관측.", "blocked_by": "누적 실 dispatch 발생", "priority": "MEDIUM"},
            ],
        },
        "cat5_redundant_doctrine_no_repeat": {
            "title": "더 이상 반복하지 않아도 되는 임시/중복 doctrine",
            "items": [
                {"doctrine": "NO-CRON 이 executor completion callback 을 금지한다는 오독", "disposition": "+32 코드 강제 + normal-callback-mandatory-doctrine-correction 으로 영구 정정. 재론 불요.", "enforced_by_code": True},
                {"doctrine": "executor self-collector 가 수용 가능할 수 있다", "disposition": "+49 구조적 불가능화. selfcollector-violation-containment-decision 의 수동 강제 반복 불요(코드 강제).", "enforced_by_code": True},
                {"doctrine": "fixed-time/dead-man 은 진행 트리거가 아니다(반복 수동 명시)", "disposition": "+47 event-trigger FORBIDDEN_TRIGGER_SOURCE + regression 으로 코드화. doctrine 반복 불요.", "enforced_by_code": True},
                {"doctrine": "executor draft 자가보고 'ANU-Codex 수렴' 신뢰", "disposition": "+44_46/+47/+48/+49 mandatory 독립 collector audit 가 supersede(executor draft=frozen evidence 보존). 자가보고 투명성-gap 구조적 해소.", "enforced_by_code": True},
                {"doctrine": "narrow vs authoritative 산출 재adjudication(+45 narrow, 구 narrow +49 dev6)", "disposition": "narrow 무손실 보존 + authoritative supersede 로 settled. 반복 adjudication 불요.", "enforced_by_code": False},
            ],
        },
        "cat6_remaining_low_backlog": {
            "title": "남은 LOW/backlog 후보",
            "items": [
                {"id": "BL-1", "ref": "+44_46", "item": "tri-state probe 경화(3 probe seen/not_seen/indeterminate; indeterminate 시 RESULT_MISSING 금지)", "severity": "MEDIUM(non-blocking)", "status": "OPEN", "mandatory_followup": True},
                {"id": "BL-2", "ref": "+47", "item": "F1/F2 idempotency key scope BY-DESIGN HIGH→LOW 강등 — spec 변경 없으면 무조치, LOW 추적", "severity": "LOW", "status": "TRACKED_NO_ACTION"},
                {"id": "BL-3", "ref": "+32", "item": "Pyright reportMissingImports + importlib ModuleSpec|None typing — config-only false-positive(runtime 해소)", "severity": "LOW(cosmetic)", "status": "TRACKED_NO_ACTION"},
                {"id": "BL-4", "ref": "+41", "item": "Codex infra-unavailable(ChatGPT account) — infra 가용 시 +41 harness Codex audit 재실행(수동 self-audit 이미 clean)", "severity": "LOW", "status": "OPEN_WHEN_INFRA"},
                bl5,
            ],
        },
        "cat7_selfchain_vs_independent_anu": {
            "title": "self-chain QUARANTINED vs independent ANU authoritative PASS 구분",
            "self_chain": {
                "definition": "executor self key 발사 / executor==collector / self-adjudication / self-dispatch",
                "disposition": "authoritative_verdict_selector → QUARANTINED·영구 비권위. independent_anu_count=0 → verdict=FAIL·classification=AUTHORITATIVE_VERDICT_PENDING (self-chain 만으로 PASS 확정 금지).",
                "spoof_handling": "claimed_origin=independent_anu 위장 self-session 도 owner identity 로 QUARANTINED.",
            },
            "independent_anu": {
                "definition": "owner key c119085addb0f8b7 · role=ANU · executor≠collector · 4-tuple valid",
                "disposition": "owner_is_independent_anu=True → 그 verdict authoritative. PASS→AUTHORITATIVE_PASS.",
                "precedence": "independent FAIL > HOLD > self-chain PASS (fail-closed)",
            },
            "chain_application": "+44_46/+47/+48/+49 전부 독립 ANU collector 세션 authoritative PASS(executor draft frozen evidence 보존·supersede). 구 narrow +49(dev6 페룬) self/test-중심 → dev2 오딘 authoritative(독립) supersede, narrow 무손실 보존.",
        },
        "cat8_cancel_on_success_tiers": {
            "title": "cancel-on-success mock / isolated / live observation 구분",
            "mock": {"ref": "+41", "scope": "FakeCronLister/SpyRemover/SpyScheduleHistory passive 6-step observation. 실 cron 0.", "evidence": "regression 19 PASS, install_isolation_guards self-test 3 PASS"},
            "mock_isolated_audit": {"ref": "+37", "scope": "wired entrypoint cancel-audit — 격리 FakeCronLister/SpyRemover, spy_remover_calls=[{FB37-0001,dry_run:false}], 실 운영 cron 삭제 0", "evidence": "regression 19 PASS"},
            "isolated_e2e": {"ref": "+48", "scope": "properly-bound 4-tuple, operational=True real-mode seam 1회 + 실제 cron-remove(WorldSpyRemover.calls=[{PB48FALLBACK,dry_run:false}]) 전용 격리 FakeCronWorld. real_ops_cron_force_delete_count=0. fallback_fired_count=0 e2e·normal_success_unchanged=true. 보수 가드 무회귀(mismatch/missing→보존, remove 실패→decouple).", "evidence": "regression 163 PASS, E2E_PASS"},
            "live": {"status": "NOT_PERFORMED", "reason": "Layer A NO-CRON 설계 — 실 운영 fallback cron 제거 0(회장 §5). operational pilot 후보 C-PILOT-2, 별도 회장 GO 필요."},
        },
        "cat9_policy_profile_engine_seam_scope": {
            "title": "policy profile engine operational seam 완료 범위",
            "completed_scope": ppe,
            "in_scope_done": [
                "C1 engine(+33) byte-0 정본 — PURE CONTRACT DERIVER",
                "+38 dispatch_profile_selection seam (engine read-only consume → dispatch selection binding, fail-closed, DISPATCH_LIFECYCLE_EFFECT=none)",
                "+39 coordinator_profile_binding (file-level contract, auto-confirm hard-pinned False, zero import coupling)",
                "+40 4 historical case 인스턴스화 + dry-run contract-level fidelity 4/4",
            ],
            "not_in_scope_yet": [
                "task_2553_final_closeout profile mapping (Track3 — 본 Track1 범위 외)",
                "engine-auto-resolve of this goal_type (Track3 완료 후 등록 가능)",
                "engine 의 write/merge 권한 (영구 금지 — engine 은 deriver, write surface 0)",
            ],
        },
    }


# ───────────────────── HOLD 평가 (회장 §6) ─────────────────────
def assess_hold(frozen: dict, git: dict, consumed: dict) -> dict:
    frozen_ok = all(v.get("equal") for v in frozen.values())
    triggers = {
        "critical7": False,
        "codex_unresolved_high_critical": False,  # 전 체인 round-2 CLEAN / +44_46 사후 adjudication 0 + follow-up backlog 추적
        "credential_permission_expansion": False,
        "expected_files_overlap_track23": False,   # §4 DISJOINT 명시
        "forbidden_target_touch": False,
        "self_callback_collector_adjudication_dispatch": False,  # callback=독립 ANU key only
        "callback_owner_not_anu_key": False,
        "authoritative_selector_bypassed": False,
        "fallback_deadman_as_progress_trigger": False,
        "fixedtime_gate_as_progress_trigger": False,
        "registry_checkpoint_escalated_primary": False,
        "profile_engine_write_merge_required": False,
        "existing_artifact_mutation_required": False,
        "goal_unachievable": False,
        "frozen_byte0_broken": not frozen_ok,
        "consumed_artifact_missing": not consumed["all_present"],
        "git_branch_drift": not git["branch_matches_expected"],
    }
    hold = any(triggers.values())
    return {
        "hold_for_chair": hold,
        "triggers": triggers,
        "verdict": (
            "§6 HOLD 트리거 전수 non-operative — read-only 종합, ANU-Codex loop "
            "자동 수렴, 회장 보고 불요(consolidated only)"
            if not hold else
            "HOLD 조건 적중 — 회장 보고 필요"
        ),
    }


# ───────────────────── 산출물 빌더 ─────────────────────
def _now_kst() -> str:
    return datetime.now(KST).strftime("%Y-%m-%d %H:%M KST")


def build_artifacts(root: str) -> dict:
    frozen = verify_frozen_byte0(root)
    git = read_git_invariant(root)
    consumed = verify_consumed_present(root)
    reg = read_durable_registry(root)
    ppe = policy_profile_seam_status(root)
    classification = build_classification(reg, ppe)
    hold = assess_hold(frozen, git, consumed)

    common_invariants = {
        "git_head": git["head_sha"],
        "git_branch": git["branch"],
        "git_branch_matches_expected": git["branch_matches_expected"],
        "frozen_byte0": frozen,
        "frozen_byte0_all_equal": all(v.get("equal") for v in frozen.values()),
        "consumed_artifacts_all_present": consumed["all_present"],
        "consumed_missing": consumed["missing"],
        "expected_files_only": True,
        "track2_3_disjoint": True,
        "read_only_consume": True,
        "no_existing_artifact_mutation": True,
        "no_merge_no_dispatch_no_cron": True,
    }

    decision = {
        "schema": "task-2553.final-closeout.decision.v1",
        "task_id": "task-2553 (final closeout via task-2553+50 Track 1)",
        "goal_type": "task_2553_final_closeout",
        "ts_kst": _now_kst(),
        "executor": "dev5-team 마르둑 (key 109fa85250c6d46b) 1회 한정 — clean(+47/+48/+49 chain 비관여)",
        "decision": "FINAL_CLOSEOUT_CONSOLIDATED",
        "mode": "read-only 종합 (기존 산출물 무수정·merge/write/dispatch 0)",
        "scope_consumed": ["+32", "+37", "+38", "+39", "+40", "+41", "+44_46", "+47", "+48", "+49"],
        "nine_distinctions_built": list(classification.keys()),
        "hold_for_chair": hold["hold_for_chair"],
        "hold_assessment": hold,
        "prohibitions_observed_section5": [
            "기존 산출물 수정 0", "추가 dispatch 0",
            "closeout 근거 merge/write 실행 0", "pending→완료 과장 0(미충족=remaining-backlog OPEN 명시)",
        ],
        "callback_a": {
            "rule": "완료 직후 normal completion callback cron 을 독립 ANU key c119085addb0f8b7(chat 6937032012)로만 발사. executor self key 109fa85250c6d46b 절대 미발사(+49 코드 강제 정본).",
            "collector_role": "ANU",
            "collector_owner_key": "c119085addb0f8b7",
            "fallback": "ANU key·미수신 안전망 한정·진행 트리거 아님",
        },
        "invariants": common_invariants,
        "no_actor_attribution_change": True,
    }

    result = {
        "schema": "task-2553.final-closeout.result.v1",
        "task_id": "task-2553 (final closeout)",
        "goal_type": "task_2553_final_closeout",
        "ts_kst": _now_kst(),
        "final_status": "CLOSEOUT_CONSOLIDATED_PASS",
        "hold_for_chair": hold["hold_for_chair"],
        "executor": "dev5-team 마르둑 (key 109fa85250c6d46b) 1회 한정",
        "summary": (
            "+32/+37/+38~41/+44_46/+47/+48/+49 전체를 하나의 final closeout 으로 "
            "read-only 종합. 9 필수구분 정형화, frozen anchor 6/6 byte-0 EQUAL, "
            "git invariant EQUAL, consumed 산출물 전수 present, callback=독립 ANU key only."
        ),
        "nine_distinctions": classification,
        "durable_registry": reg,
        "policy_profile_seam": ppe,
        "invariants": common_invariants,
        "codex_anu_loop": (
            "본 closeout=read-only 종합 — re-lint 불요(회장 §9). upstream 전 체인 "
            "Codex round-2 CLEAN / +44_46 사후 adjudication unresolved HIGH/CRITICAL 0 "
            "+ MANDATORY follow-up 은 remaining-backlog BL-1(OPEN) 로 과장 없이 추적."
        ),
        "executor_self_actions": {
            "new_dispatch": 0, "delegation": 0, "self_adjudication": 0,
            "self_codex": 0, "self_collector": 0,
        },
        "no_actor_attribution_change": True,
    }

    backlog = {
        "schema": "task-2553.remaining-backlog.v1",
        "ts_kst": _now_kst(),
        "source": "task-2553 final closeout (task-2553+50 Track 1, read-only)",
        "note": "pending 항목을 완료로 과장하지 않음(회장 §5) — 미충족은 OPEN 명시.",
        "backlog": classification["cat6_remaining_low_backlog"]["items"],
        "doctrine_settled_no_repeat": classification["cat5_redundant_doctrine_no_repeat"]["items"],
        "highest_priority_open": "BL-1 (+44_46 tri-state probe 경화 — MEDIUM non-blocking, MANDATORY follow-up)",
    }

    pilot = {
        "schema": "task-2553.operational-pilot-readiness.v1",
        "ts_kst": _now_kst(),
        "source": "task-2553 final closeout (task-2553+50 Track 1, read-only)",
        "candidates": classification["cat4_next_operational_pilot_candidates"]["candidates"],
        "live_observation_status": classification["cat3_live_observation_remaining"],
        "cancel_on_success_tiers": classification["cat8_cancel_on_success_tiers"],
        "policy_profile_engine_seam_scope": classification["cat9_policy_profile_engine_seam_scope"],
        "engine_auto_resolve_registration": {
            "goal_type": "task_2553_final_closeout",
            "current": (
                "PRESENT (Track3 병렬 landed — read-only 관측, Track1 무변)"
                if ppe.get("task_2553_final_closeout_profile_present")
                else "ABSENT (profile 미생성)"),
            "owner": "Track3 (mapping 신설)",
            "post_track3": "engine-auto-resolve 등록 가능 — 본 readiness 에 기록(회장 §7)",
            "observation": ppe.get("task_2553_final_closeout_profile_observation"),
        },
        "readiness_gate": (
            "operational pilot 진입은 별도 회장 GO 한정. 본 closeout 은 후보 식별·"
            "준비도 기록만 — 어떤 pilot 도 자동 개시하지 않음(§5)."
        ),
    }

    return {
        "memory/events/task-2553.final-closeout.decision.json": decision,
        "memory/events/task-2553.final-closeout.result.json": result,
        "memory/events/task-2553.remaining-backlog_260518.json": backlog,
        "memory/events/task-2553.operational-pilot-readiness_260518.json": pilot,
        "_classification": classification,
        "_hold": hold,
        "_invariants": common_invariants,
        "_reg": reg,
        "_ppe": ppe,
    }


def render_consolidated_md(bundle: dict) -> str:
    c = bundle["_classification"]
    inv = bundle["_invariants"]
    hold = bundle["_hold"]
    lines = []
    a = lines.append
    a("# task-2553 — FINAL CLOSEOUT consolidated summary")
    a("")
    a("> Track 1 (task-2553+50) · executor: dev5-team 마르둑 (key 109fa85250c6d46b) 1회 한정")
    a("> read-only 종합 · 기존 +32~+49 산출물·frozen anchor·policy_profile_engine byte-0 consume")
    a(f"> hold_for_chair: **{str(hold['hold_for_chair']).lower()}** · {hold['verdict']}")
    a("")
    a("## 불변식")
    a(f"- git HEAD `{inv['git_head']}` · branch `{inv['git_branch']}` "
      f"(expected match: {inv['git_branch_matches_expected']})")
    a(f"- frozen anchor 6모듈 byte-0 all EQUAL: **{inv['frozen_byte0_all_equal']}**")
    a(f"- consumed upstream 산출물 전수 present: **{inv['consumed_artifacts_all_present']}**")
    a("- 기존 산출물 무수정 · merge/write/dispatch/cron 0 · expected_files allowlist 한정 · Track2/3 DISJOINT")
    a("")
    order = [
        ("cat1_completed_structural_guarantees", "1"),
        ("cat2_independent_anu_verified", "2"),
        ("cat3_live_observation_remaining", "3"),
        ("cat4_next_operational_pilot_candidates", "4"),
        ("cat5_redundant_doctrine_no_repeat", "5"),
        ("cat6_remaining_low_backlog", "6"),
        ("cat7_selfchain_vs_independent_anu", "7"),
        ("cat8_cancel_on_success_tiers", "8"),
        ("cat9_policy_profile_engine_seam_scope", "9"),
    ]
    for key, num in order:
        sec = c[key]
        a(f"## §2.{num} {sec['title']}")
        a("")
        a("```json")
        a(json.dumps(sec, ensure_ascii=False, indent=2))
        a("```")
        a("")
    a("## §6 HOLD 평가")
    a("")
    a("```json")
    a(json.dumps(hold, ensure_ascii=False, indent=2))
    a("```")
    a("")
    a("## callback (a)")
    a("")
    a("완료 직후 normal completion callback cron 을 **독립 ANU key "
      "`c119085addb0f8b7`(chat 6937032012)로만** 발사. executor self key "
      "`109fa85250c6d46b` 절대 미발사(+49 코드 강제 정본). 회수·검증·Codex audit·"
      "adjudication·batch coordinator 통합은 그 독립 ANU collector 세션. ANU "
      "fallback=ANU key·안전망 한정·진행 트리거 아님.")
    a("")
    return "\n".join(lines)


def render_plus50_md(bundle: dict) -> str:
    hold = bundle["_hold"]
    inv = bundle["_invariants"]
    return "\n".join([
        "# task-2553+50 — TRACK 1: task-2553 FINAL CLOSEOUT (read-only 종합)",
        "",
        "> Executor: dev5-team 마르둑 (key 109fa85250c6d46b) 1회 한정 · goal_type `task_2553_final_closeout`",
        f"> 상태: ✅ CLOSEOUT_CONSOLIDATED_PASS · hold_for_chair: **{str(hold['hold_for_chair']).lower()}**",
        "",
        "## 무엇을 했나",
        "",
        "+32/+37/+38~41/+44_46/+47/+48/+49 전체 산출물을 read-only 로 종합하여 하나의 "
        "final closeout 으로 정리. 회장 §2 필수구분 1~9 를 정형 JSON/MD 로 산출. "
        "문서-only 아님 — read-only 수집 스크립트(`scripts/task2553_closeout_collect.py`) + "
        "정형 산출물 + regression.",
        "",
        "## 산출 (회장 §3 / §4 allowlist 한정)",
        "",
        "- `memory/events/task-2553.final-closeout.decision.json`",
        "- `memory/events/task-2553.final-closeout.result.json`",
        "- `memory/reports/task-2553.final-closeout-consolidated-summary_260518.md`",
        "- `memory/events/task-2553.remaining-backlog_260518.json`",
        "- `memory/events/task-2553.operational-pilot-readiness_260518.json`",
        "- `scripts/task2553_closeout_collect.py` (read-only 수집기)",
        "- `tests/regression/test_task2553_closeout_collect_2553plus50.py`",
        "- `memory/events/task-2553+50.{decision,result}.json` · `memory/reports/task-2553+50.md`",
        "",
        "## 불변식",
        "",
        f"- git HEAD `{inv['git_head']}` · branch `{inv['git_branch']}` 전후 EQUAL "
        "(commit/push/merge 0)",
        f"- frozen anchor 6/6 byte-0 EQUAL: **{inv['frozen_byte0_all_equal']}**",
        "- 기존 +32~+49 산출물·frozen anchor·policy_profile_engine 무변(read-only consume)",
        "- §4 expected_files allowlist 외 write 0 · Track2/3 DISJOINT",
        "",
        "## 금지 준수 (회장 §5)",
        "",
        "기존 산출물 수정 0 · 추가 dispatch 0 · closeout 근거 merge/write 0 · "
        "pending→완료 과장 0(미충족 항목은 remaining-backlog 에 OPEN 명시).",
        "",
        "## HOLD (회장 §6)",
        "",
        f"hold_for_chair=**{str(hold['hold_for_chair']).lower()}** — {hold['verdict']}",
        "",
        "## callback (a)",
        "",
        "완료 직후 normal completion callback cron 을 독립 ANU key "
        "`c119085addb0f8b7`(chat 6937032012)로만 발사. executor self key "
        "`109fa85250c6d46b` 발사 절대 금지(+49 코드 강제 정본). 회수·검증·Codex "
        "audit·adjudication·batch coordinator 통합은 그 독립 ANU collector 세션. "
        "executor 자기작업중 신규 dispatch·delegation·자가심사·자가Codex 0.",
        "",
    ])


def build_plus50_records(bundle: dict) -> dict:
    hold = bundle["_hold"]
    inv = bundle["_invariants"]
    dec = {
        "schema": "task-2553+50.decision.v1",
        "task_id": "task-2553+50",
        "title": "TRACK 1 — task-2553 FINAL CLOSEOUT (read-only 종합)",
        "ts_kst": _now_kst(),
        "executor": "dev5-team 마르둑 (key 109fa85250c6d46b) 1회 한정",
        "goal_type": "task_2553_final_closeout",
        "decision": "read-only 종합 + 정형 산출 + regression. merge/write/dispatch 0.",
        "hold_for_chair": hold["hold_for_chair"],
        "hold_assessment": hold,
        "callback_a": {
            "rule": "독립 ANU key c119085addb0f8b7(chat 6937032012)로만 normal completion callback 발사. executor self key 109fa85250c6d46b 절대 금지.",
            "collector_role": "ANU",
        },
        "invariants": inv,
        "no_actor_attribution_change": True,
    }
    res = {
        "schema": "task-2553+50.result.v1",
        "task_id": "task-2553+50",
        "title": "TRACK 1 — task-2553 FINAL CLOSEOUT",
        "status": "completed",
        "classification": "CLOSEOUT_CONSOLIDATED_PASS",
        "ts_kst": _now_kst(),
        "executor": "dev5-team 마르둑 (key 109fa85250c6d46b) 1회 한정",
        "hold_for_chair": hold["hold_for_chair"],
        "deliverables": list(ALLOWLIST),
        "nine_distinctions_keys": list(bundle["_classification"].keys()),
        "durable_registry_completed_tasks": bundle["_reg"]["completed_durable_success_tasks"],
        "invariants": inv,
        "executor_self_actions": {
            "new_dispatch": 0, "delegation": 0, "self_adjudication": 0,
            "self_codex": 0, "self_collector": 0,
        },
        "callback_a_fired_with": "ANU key c119085addb0f8b7 (chat 6937032012) — executor self key 109fa85250c6d46b 절대 미발사(+49 코드 강제 정본)",
        "no_actor_attribution_change": True,
    }
    return {
        "memory/events/task-2553+50.decision.json": dec,
        "memory/events/task-2553+50.result.json": res,
    }


def collect(root: str) -> dict:
    """read-only 수집 + 산출물 dict 조립(write 0). regression 진입점."""
    bundle = build_artifacts(root)
    out = {k: v for k, v in bundle.items() if not k.startswith("_")}
    out["memory/reports/task-2553.final-closeout-consolidated-summary_260518.md"] = \
        render_consolidated_md(bundle)
    out.update(build_plus50_records(bundle))
    out["memory/reports/task-2553+50.md"] = render_plus50_md(bundle)
    out["_meta"] = {
        "hold": bundle["_hold"],
        "invariants": bundle["_invariants"],
        "classification_keys": list(bundle["_classification"].keys()),
    }
    return out


def _assert_allowlisted(rel: str) -> None:
    if rel not in ALLOWLIST:
        raise RuntimeError(
            f"write 거부 — '{rel}' 는 §4 expected_files allowlist 외 (Track2/3 DISJOINT)")


def write_outputs(out_root: str, produced: dict) -> list:
    written = []
    for rel, payload in produced.items():
        if rel.startswith("_"):
            continue
        _assert_allowlisted(rel)
        ap = os.path.join(out_root, rel)
        os.makedirs(os.path.dirname(ap), exist_ok=True)
        if rel.endswith(".md"):
            data = payload if isinstance(payload, str) else str(payload)
            with open(ap, "w", encoding="utf-8") as fh:
                fh.write(data)
        else:
            with open(ap, "w", encoding="utf-8") as fh:
                json.dump(payload, fh, ensure_ascii=False, indent=2)
                fh.write("\n")
        written.append(rel)
    return sorted(written)


def main(argv=None) -> int:
    p = argparse.ArgumentParser(description="task-2553 final closeout read-only collector")
    p.add_argument("--root", default=CANONICAL_ROOT, help="canonical workspace root (read source)")
    p.add_argument("--out-root", default=None, help="output root (default = --root)")
    p.add_argument("--dry-run", action="store_true", help="수집/검증만, write 0")
    args = p.parse_args(argv)
    root = args.root
    out_root = args.out_root or root
    produced = collect(root)
    meta = produced.get("_meta", {})
    if args.dry_run:
        print(json.dumps({
            "dry_run": True,
            "hold_for_chair": meta.get("hold", {}).get("hold_for_chair"),
            "frozen_byte0_all_equal": meta.get("invariants", {}).get("frozen_byte0_all_equal"),
            "classification_keys": meta.get("classification_keys"),
        }, ensure_ascii=False, indent=2))
        return 0
    written = write_outputs(out_root, produced)
    print(json.dumps({
        "status": "ok",
        "written": written,
        "hold_for_chair": meta.get("hold", {}).get("hold_for_chair"),
        "frozen_byte0_all_equal": meta.get("invariants", {}).get("frozen_byte0_all_equal"),
    }, ensure_ascii=False, indent=2))
    return 0


if __name__ == "__main__":
    raise SystemExit(main())
