"""도면 읽기 산출이 **무엇을 담고 있나** (2026-08-27, #92 6단계 준비).

Codex 판정: geometry 이중화의 합의는 「좌표 평균 금지, 구조화 ID 합의」다.
`marker ID 집합·kind·missing/extra` 가 같아야 하고 `row/col` 은 평균 대신
`exact / adjacent / conflict` 로 기록한다. 「인접 셀 허용은 **양성 표본으로
경계를 잰 뒤에만**」.

★그래서 **구현 전에 재야 한다.** `exact` 만 통과시키는 계약을 두 모델의
 일치율도 모르고 넣으면, 두 모델이 격자를 다르게 읽을 때 **도면 읽기가
 통째로 막혀 파이프라인이 선다.**

이 도구는 **무료**다 — 이미 완주한 체크포인트를 읽어 다음을 센다:

    · 한 도면에 마커가 몇 개인가 (합의해야 할 항목 수)
    · 격자가 얼마나 큰가 (「인접」이 몇 칸인지의 뜻이 여기서 정해진다)
    · `missing`/`extra` 가 실제로 나오는가
    · `confidence` 분포

★이것만으로는 **두 모델의 일치율을 못 잰다** — 그건 유료 호출이 필요하고
 별도 판이다. 이 도구가 정하는 것은 「합의 대상이 몇 개짜리 문제인가」다.

    .venv/bin/python tools/prompt_measure/audit_floor_readback_shape.py
"""
from __future__ import annotations

import json
import pathlib
import sys
from collections import Counter

ROOT = pathlib.Path(__file__).resolve().parents[3]


def _readbacks():
    for cp in sorted(ROOT.glob(
            "projects/*/checkpoints/episodes/*/floor_plan_geometry_readback/"
            "manifest*.json")):
        try:
            yield cp, json.loads(cp.read_text(encoding="utf-8"))
        except Exception:
            continue


def _walk(o):
    """`observed_markers` 를 가진 딕셔너리를 넓게 훑는다."""
    if isinstance(o, dict):
        if "observed_markers" in o and isinstance(o["observed_markers"], list):
            yield o
        for v in o.values():
            yield from _walk(v)
    elif isinstance(o, list):
        for v in o:
            yield from _walk(v)


def main() -> None:
    files = 0
    plans = 0
    marker_counts = []
    grids = Counter()
    kinds = Counter()
    n_missing = n_extra = 0
    confs = []
    cells_used = []

    for cp, data in _readbacks():
        files += 1
        for rb in _walk(data):
            plans += 1
            obs = rb.get("observed_markers") or []
            marker_counts.append(len(obs))
            g = rb.get("grid_size")
            if isinstance(g, list) and len(g) == 2:
                grids[f"{g[0]}x{g[1]}"] += 1
            for m in obs:
                if isinstance(m, dict):
                    kinds[str(m.get("kind"))] += 1
            if rb.get("missing_markers"):
                n_missing += 1
            if rb.get("extra_markers"):
                n_extra += 1
            c = rb.get("confidence")
            if isinstance(c, (int, float)):
                confs.append(float(c))
            # 격자에서 실제로 쓰인 칸 — 「인접 허용」이 얼마나 느슨한지
            cells = {(m.get("row"), m.get("col")) for m in obs
                     if isinstance(m, dict)}
            if isinstance(g, list) and len(g) == 2 and g[0] and g[1]:
                cells_used.append(len(cells) / (int(g[0]) * int(g[1])))

    if not plans:
        print("  도면 읽기 체크포인트를 못 찾았다 — 경로/모양을 볼 것")
        return

    print(f"■ 도면 읽기 체크포인트 {files}개 · 도면 {plans}장\n")
    mc = sorted(marker_counts)
    print(f"  마커 수     평균 {sum(mc)//len(mc)} · 최소 {mc[0]} · "
          f"최대 {mc[-1]} · 중앙 {mc[len(mc)//2]}")
    print(f"  격자        {dict(grids.most_common(5))}")
    print(f"  kind        {dict(kinds.most_common(6))}")
    print(f"  missing 있음 {n_missing}/{plans}  ·  extra 있음 {n_extra}/{plans}")
    if confs:
        cs = sorted(confs)
        print(f"  confidence  평균 {sum(cs)/len(cs):.2f} · 최소 {cs[0]:.2f}")
    if cells_used:
        u = sorted(cells_used)
        print(f"  격자 점유율 평균 {sum(u)/len(u)*100:.1f}% "
              f"(최대 {u[-1]*100:.1f}%)")

    print("\n★합의가 몇 개짜리 문제인가")
    print(f"   한 도면마다 **{sum(mc)//len(mc)}개 마커**의 "
          f"number·kind·row·col 이 두 모델에서 같아야 한다.")
    print("   ★마커가 많을수록 `exact` 전부 일치는 어려워진다 —")
    print("    그래서 두 모델 일치율을 **재기 전에** 계약을 못 정한다.")
    print("\n★이 도구가 **못 재는 것**: 두 모델의 실제 일치율. 유료 호출이")
    print(" 필요하고 별도 판이다. 여기서 나온 것은 「문제의 크기」뿐이다.")


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