"""C(c) **B-only** 진단 주행 preflight — 사기 전에 한 장으로. ★유료 0 · 외부 0.

두 번째 판이다. 첫 판에서 Codex 가 일곱을 냈고 여섯이 내 것이었다 —

    ① arm 의미 범위가 달랐다   B 는 두 축·질의를 내는데 A 8콜에는 그게 없다
    ② arm A 의 8 payload 가    미리 깔아 둔 체크포인트로 지은 **대역**이었다.
       실제 주행 신원이 아니다  실제 A 는 앞 응답에서 뒤 payload 가 생긴다
    ③ 물리 상한 13 이 틀렸다   `num_retries=0` 은 **키 슬롯 loop 를 안 닫는다**
    ④ 「13 전부」가 거짓        merge 신원은 비어 있었고(실제 12), 게다가
                               모델을 `gpt` 로 **박아** 찍고 있었다
    ⑤ 표적 장부가 채점 불가     산문 dict 였다
    ⑥ merge 가 근거를 버렸다    같은 말 다른 실물을 가를 정보가 payload 에 없다
    ⑦ 의무 규칙이 어긋났다      한쪽 flag 만으로 질의를 채우게 했다

그래서 **A 를 안 산다.** B 가 구조적으로 되는지부터 보고, 구조 gate 가
떨어지면 A 를 안 사고 끝낸다 (Codex 최소 실험).

    python tools/grounding_audit/preflight_cc_experiment.py
"""
from __future__ import annotations

import json
import sys
from pathlib import Path
from typing import Any, Dict, List

ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(ROOT))
sys.path.insert(0, str(ROOT / "tests"))

from tools.grounding_audit import cc_runner as rr  # noqa: E402
from tools.grounding_audit.call_payload_table import seal_outbound  # noqa: E402

#: ★못박는 값은 **주행기가 정본**이다 — 여기 다시 적으면 두 벌이 되고,
#:  「preflight 에 적힌 값」과 「실제로 넘어가는 값」이 갈린다.
PINNED = rr.PINNED
_TIERS = 3


def arm_b_calls(world: str) -> List[Dict[str, Any]]:
    """구간마다 1콜 + merge 1. ★**실제 팩과 실제 조립**으로 짓는다."""
    from app.modules.pipeline import grounding_chunk as gc

    phys = rr.physical_model()
    out: List[Dict[str, Any]] = []
    for c in rr.plan_calls(world):
        p = c["payload"]
        out.append({
            "name": f"{c['chunk_id']} {','.join(c['segment_ids'])}",
            "kind": "chunk", "model": rr.MODEL_ALIAS, "physical": phys,
            "bytes": len(p["parts"][0]["text"].encode("utf-8")),
            "identity": rr.identity(p["system"], p["parts"][0]["text"],
                                    p["schema"], rr.MODEL_ALIAS, phys),
            "parallel": True,
        })
    ms = gc.load_text("merge_system.md")
    out.append({
        "name": "merge (구간 산출을 받아서)", "kind": "merge",
        "model": rr.MODEL_ALIAS, "physical": phys,
        "bytes": len(ms.encode("utf-8")),
        "identity": "",
        "parallel": False,
        "note": ("★신원과 bytes 는 **구간이 답해야** 정해진다. 지어내지 않는다 "
                 "— 주행기가 보내기 직전에 매기고 장부에 적는다"),
    })
    return out


def main() -> int:
    seal_outbound()
    from app.core import openai_keys
    from tests.grounding.fixtures import synthetic_episode as ep

    world = ep.WORLD_FACTS          # ★fixture 가 갖는다 — 두 벌로 안 둔다
    calls = arm_b_calls(world)
    n = len(calls)
    slots = int(openai_keys.slot_count())
    P = print

    P(f"■ C(c) **B-only** 진단 preflight — 합성 원고 {len(ep.manuscript()):,}자 · "
      f"씬 {len(ep.segments())} · 구간 {len(ep.bundles())}")
    P("  ★이 도구는 아무것도 사지 않는다. 최외곽 전송 문을 잠갔다.")
    P()

    P("── §1 무엇을 사고 무엇을 안 사나")
    P(f"  산다     구간 판독 {len(ep.bundles())} + 동일성 판정 1 = **논리 {n}**")
    P("  ★안 산다  arm A · classifier · 검색 · 이미지 생성 · 이미지 검색")
    P("  ★arm A 를 왜 안 사나 — ①두 arm 의 의미 범위가 다르다(B 는 두 축과")
    P("    질의를 내는데 A 8콜에는 그게 없다) ②A 의 후단 payload 는 앞 응답에서")
    P("    동적으로 생기는데, 지금 잰 8개는 미리 깔아 둔 체크포인트로 지은")
    P("    **대역**이라 실제 주행 신원이 아니다. 그것으로 사면 「현행 구조」")
    P("    비교가 아니다. A 비교는 production 후보가 된 뒤 canary 로 미룬다.")
    P()

    P("── §1.5 앞 판이 못 잰 것 — ★언급과 근거를 갈랐다")
    P("  유료 1회가 드러냈다: `source_quote` 한 칸이 「부른 자리」와 「겉모습")
    P("  근거」를 겸해서, 한 번만 나온 어려운 대상이 **출현 2회**로 세어졌다.")
    P("  그래서 `grounding_exception` 이 아니라 반복 출현 축으로 살았고,")
    P("  **이 실험의 핵심 규칙을 전혀 못 쟀다.**")
    P("  → schema 를 `mentions`(횟수를 세는 이름 덩어리)와 `evidence_quotes`")
    P("    (근거, 횟수에 안 셈)로 갈랐다. 근거도 원문 대조하되 신원엔 안 쓴다.")
    P()

    P("── §2 호출 그래프와 신원")
    P(f"{'호출':30} {'bytes':>8}  {'alias':6} {'물리 모델':14} {'신원':>26}")
    P("─" * 92)
    for c in calls:
        P(f"{c['name'][:30]:30} {c['bytes']:>8,}  {c['model']:6} "
          f"{c['physical'][:14]:14} {(c['identity'] or '★주행 때'):>26}")
        if c.get("note"):
            P(f"  ↳ {c['note']}")
    P("─" * 92)
    ids = [c["identity"] for c in calls if c["identity"]]
    P(f"  지금 신원이 나는 것 **{len(ids)}/{n}** · 중복 {len(ids) - len(set(ids))}건")
    P("  ★신원 = payload + **모델 alias + 물리 모델**. alias 만 넣으면 설정의")
    P("    실제 모델만 바뀌었을 때 옛 산출이 조용히 재사용된다.")
    P()

    P("── §3 채점 — 무엇을 자동으로 보고 무엇을 사람이 보나")
    P("  자동 (`tools/grounding_audit/cc_scorer.py`)")
    P(f"    · 표적 {len(ep.EXPECTED_TARGETS)}개가 **빠졌나** — ★**주소**로 본다:")
    P("      언급이 표적 좌표와 **정확히 같을 때만** 주장. 품기만 하면")
    P("      **모호**로 세운다 — 조용히 귀속하지 않고 사람에게 넘긴다.")
    P("      한 행이 **두 표적**을 주장하면 wrong(서로 다른 실물을 합친 것).")
    P("      자리가 여럿인 표적은 자리↔행이 **일대일**이어야 한다.")
    P("    · **근거 없이 더했나** — 목록 밖인가가 **아니라** 원문에 인용이 있나")
    P("    · owner 갈래 · 관계 종류 · 출현 **수**(★언급만. 근거는 안 셈)")
    P("    · span 무결성 · 한 번 나온 hard+notice 가 **예외 축으로** 등록됐나")
    P("    · 판정 칸은 넷 — ok/missing/wrong/unresolved. ★**셋 다 0 일 때만** 통과")
    P("  사람만 — `visual_brief` 의 쓸모 · 두 축 판단 · **어느 쪽이 나은가**")
    P("  ★표적 장부는 **원고의 모든 합법 엔티티 목록이 아니다.** 표적 회귀")
    P("    장부다. 목록 밖을 결함으로 세면 내가 안 적은 정당한 것이 다 결함이 된다.")
    P()

    P("── §4 상한 — ★슬롯을 빠뜨렸던 자리")
    P(f"  논리                {n}")
    P(f"  못박는 값           {json.dumps(PINNED, ensure_ascii=False)}")
    P(f"  ★키 슬롯           지금 **{slots}개** (`llm_client.py:427-439` 의")
    P("                      loop 는 Router **밖**이라 `num_retries=0` 이 안 닫는다)")
    P(f"  dispatch 예산       {n} (= `call_structured` 를 부르는 횟수)")
    P(f"  물리 상한(못박음)   dispatch {n} × 슬롯 {slots} = **{n * slots}**")
    P("  ★우리가 셀 수 있는 것은 dispatch 뿐이다 — 키 슬롯 loop 는 그 안에서")
    P("    돈다. 물리 실제치는 Opik+provider 로그로 본다.")
    try:
        from app.core.config import settings

        r = int(getattr(settings, "llm_max_retries", 3))
    except Exception:
        r = 3
    P(f"  물리 상한(안 박음)  {n} × tier {_TIERS} × 시도 {1 + r} × 슬롯 {slots} "
      f"= **{n * _TIERS * (1 + r) * slots}**")
    P(f"  ★주행기는 보내기 **전에** 슬롯 수를 세고, 승인 때 센 "
      f"{rr.APPROVED_SLOTS}개와 다르면")
    P("    provider 를 **한 번도 안 부르고** 선다.")
    P("  검색 0 · 이미지 생성 0 · 이미지 검색 0")
    P()

    P("  ★기록 기능(`opik_trace_v2`)이 꺼져 있으면 **사기 전에** 선다 —")
    P("    결속 키 없이 사면 무엇을 샀는지 못 되짚는다.")
    P()

    P("── §5 장부와 재개 — ★말이 아니라 **문**으로 있다")
    P("  `tools/grounding_audit/cc_runner.py`")
    P("    · 상한은 **보내기 전에** 선다 (끝점 시험: 상한 3이면 send 3번)")
    P("    · 호출마다 **바로** 적는다 (끊기면 앞서 산 것이 장부에 남는다)")
    P("    · 재개는 신원으로 찾는다 (끊긴 뒤 다시 돌리면 산 것 3 · 재사용 2)")
    P("    · `bought`/`reused` 를 따로 센다")
    P("    · `send` 를 안 주면 **아무것도 안 나간다** — provider 를 안 박았다")
    P()

    P("── §6 이 주행 결과의 지위")
    P("  ★**C(c) 가능성 진단**이다. 「현행보다 낫다」가 아니다.")
    P("  ★벽시계 우열의 근거로 안 쓴다 — 한 번씩으로는 provider 대기·429·")
    P("    동시성에 흔들린다. 병렬 가능성은 **구조적 장점**으로만 적는다.")
    P("  ★production 활성화·PASS 의 근거가 아니다.")
    P("  ★구조 gate 가 떨어지면 **A 를 안 사고 끝낸다.**")
    P(f"  ★종료코드가 acceptance 와 갈린다 — 기계 실패 "
      f"{rr.EXIT_MACHINE_FAIL} · **사람 검토 대기 "
      f"{rr.EXIT_HUMAN_REVIEW_REQUIRED}** · {rr.EXIT_PASS} 은 사람 판정이")
    P("    적힌 뒤에만. **자동은 최종 판정을 못 만든다.**")
    P("  ★사람이 볼 두 축 판정은 기계가 「맞다」고 한 것까지 **전부** 올린다 —")
    P("    모양이 우연히 맞아도 **뜻**은 기계가 못 본다.")
    return 0


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