"""발행된 팩을 손대면 **여기서 걸린다** (2026-08-27, 감사 3-A).

## 무엇을 막는가 — 하루에 두 번 같은 결함을 냈다

    #94  cine_stage_direction  팩 v24 에 스템을 더했는데 지문이 안 움직였다
    1-E  prev_usage_clause     팩 v33 에 스템을 더했는데 지문이 안 움직였다

둘 다 **이미 발행된 팩에 스템만 추가**한 모양이다. selector 도 팩 디렉토리
해시도 그대로라 완주한 `scene_image` 가 **whole-step clean skip** 하고,
고친 문안이 나가는 프롬프트까지 못 내려간다 — 고친 것이 아무 데도 안 닿는다.

★두 번 다 **Codex 가 잡았다.** 코드 리뷰에 기대는 대신 시험으로 세운다.

## 왜 「지문에 접혔나」를 자동 판정하지 않는가

그쪽을 먼저 시도했다(dataflow 로 「어느 스템이 접히나」를 따라가는 도구).
**고칠 때마다 답이 바뀌었다:**

    0개  → import 별칭(`... as _pu_stem_content`)을 안 따라감
    5개  → **조건부** fold 를 「덮였다」로 셈
    ?    → 팩 해시는 (모듈, **팩버전**) 쌍인데 모듈로만 봄

세 번째를 고치려면 `version=` 상수를 따라가야 하고 그 다음엔 호출부 조건
까지 따라가야 한다. **도구가 결함보다 커진다.**

★그 도구는 **지웠다** (Codex 판정, 동의). 「세 번 틀렸다」는 경고문을
 달아 두더라도 남아 있으면 다음 사람이 경고문보다 **출력 숫자를**
 믿는다. 정확한 dataflow 분석이 필요해지면 그때 별도 판으로 만든다.

★그래서 판정을 사람에게 남긴다. 이 시험이 하는 일은 하나다 —
 **「발행된 팩이 바뀌었다」는 사실을 그 자리에서 보이게 하는 것.**
 그때 「이 바이트가 지문에 접히나」를 확인하고 기준선을 굳힌다.

## 빨강이 떴을 때

    1. 바꾼 팩이 **이미 나간 판**이면 → 그 바이트를 지문에 접었는지 본다
       (`image_steps.py:_config_hash_base` 의 `recipe_*_stem_content` 꼴)
    2. **새 팩 디렉토리**를 만든 것이면 → 정상이다
    3. 확인했으면 굳힌다:
       `.venv/bin/python tools/prompt_measure/rebuild_pack_baseline.py`

★**그냥 굳히지 마라.** 굳히기 전에 1번을 했는지 스스로 묻는다.
"""
from __future__ import annotations

import json
import pathlib

import pytest

_HERE = pathlib.Path(__file__).resolve()
ROOT = _HERE.parents[3]
BASELINE = ROOT / "backend/tests/_baselines/published_packs.json"
_REBUILD = ("  .venv/bin/python "
            "tools/prompt_measure/rebuild_pack_baseline.py")


def _snapshot() -> dict:
    import sys
    sys.path.insert(0, str(ROOT / "backend/tools/prompt_measure"))
    from rebuild_pack_baseline import snapshot
    return snapshot()


@pytest.fixture(scope="module")
def _pair():
    if not BASELINE.exists():
        pytest.fail(f"기준선이 없다 — 만들 것:\n{_REBUILD}")
    return json.loads(BASELINE.read_text(encoding="utf-8")), _snapshot()


def test_a_published_pack_was_not_edited_in_place(_pair):
    """★이 판의 핵심 — **이미 나간 팩의 스템 바이트가 바뀌었나.**

    바뀌었다면 그 팩을 쓰는 완주 스텝이 지금 상태로는 옛 문안을 그대로
    재사용할 수 있다. 바이트가 지문에 접히는지 확인하고 굳혀야 한다.
    """
    base, now = _pair
    changed = []
    for pack, stems in base.items():
        cur = now.get(pack)
        if cur is None:
            # 지운 팩은 `test_the_pack_set_matches_the_baseline_exactly`
            # 가 본다. ★앞 판은 여기 「다른 시험이 본다」고 적어 놓고
            #  **그 시험을 안 만들어** 전체 삭제가 초록이었다.
            continue
        for stem, sha in stems.items():
            got = cur.get(stem)
            if got is None:
                changed.append(f"{pack}/{stem}  삭제됨")
            elif got != sha:
                changed.append(f"{pack}/{stem}  {sha} → {got}")
    assert not changed, (
        "발행된 팩의 스템이 바뀌었다 — 그 바이트가 **지문에 접히는지** "
        "확인한 뒤 기준선을 굳힐 것:\n  "
        + "\n  ".join(changed[:12])
        + (f"\n  … 그 외 {len(changed) - 12}건" if len(changed) > 12 else "")
        + f"\n\n{_REBUILD}")


def test_no_stem_was_added_to_a_published_pack(_pair):
    """★#94 · 1-E 가 정확히 이 모양이었다.

    발행된 팩에 **형제 스템**을 더하면 팩 버전도 팩 디렉토리 해시도
    안 움직인다. 새 절이 나가는데 지문은 옛 값 그대로다.
    """
    base, now = _pair
    added = []
    for pack, stems in now.items():
        known = base.get(pack)
        if known is None:
            continue                       # 새 팩 디렉토리는 정상
        for stem in stems:
            if stem not in known:
                added.append(f"{pack}/{stem}")
    assert not added, (
        "**이미 발행된 팩에 스템을 더했다** — 팩 버전도 팩 디렉토리 해시도 "
        "안 움직이므로, 그 바이트를 지문에 직접 접지 않으면 완주한 스텝이 "
        "옛 문안 그대로 clean skip 한다 (#94 · 감사 1-E 가 이 모양):\n  "
        + "\n  ".join(added[:12])
        + (f"\n  … 그 외 {len(added) - 12}건" if len(added) > 12 else "")
        + f"\n\n확인했으면:\n{_REBUILD}")


def test_the_pack_set_matches_the_baseline_exactly(_pair):
    """★기준선의 팩 목록이 디스크와 **정확히 같아야** 한다.

    앞 판은 「새 팩은 `max(20, 10%)` 까지 허용」이었다. 세 구멍이 났다
    (2026-08-27 Codex BLOCK):

      ① **팩 디렉토리 전체 삭제가 초록**이었다. 위 시험이 `cur is None`
         이면 `continue` 하면서 「지운 팩은 다른 시험이 본다」고 적었는데,
         **그 시험을 안 만들었다.** ★만들어 놓고 안 부른 관문이 없는
         것과 같듯이, 미룬다고 적어 놓고 안 만든 것도 없는 것이다.
      ② 새 팩을 기준선에 **안 넣은 채 병합**할 수 있었다. 그 뒤 그 팩을
         제자리에서 고쳐도 계속 기준선 밖이라 래칫이 못 잡는다 —
         449팩이면 수십 개가 무보호로 남는다.
      ③ `>= 400` 과 `10%` 는 **자의적이면서 핵심 보장을 깬다.**

    정확한 동일성으로 바꾸면 수치 문턱 자체가 필요 없다.

    ★**새 팩을 막는 것이 아니다.** 새 디렉토리를 만들고 `rebuild` 로 그
     키를 기준선에 함께 넣으면 통과하고, **병합 시점부터 바로 보호된다.**
    """
    base, now = _pair
    gone = sorted(set(base) - set(now))
    fresh = sorted(set(now) - set(base))
    msgs = []
    if gone:
        msgs.append("기준선에 있는데 **디스크에 없다**(삭제됨): "
                    + ", ".join(gone[:10]))
    if fresh:
        msgs.append("디스크에 있는데 **기준선에 없다**(안 굳힘): "
                    + ", ".join(fresh[:10]))
    assert not msgs, (
        "팩 목록이 기준선과 어긋난다 — 어긋난 채로는 그 팩이 "
        "**래칫 밖**이라 제자리 수정도 못 잡는다:\n  "
        + "\n  ".join(msgs)
        + f"\n\n확인했으면:\n{_REBUILD}")


def test_every_pack_has_at_least_one_stem(_pair):
    """빈 팩 디렉토리는 곧 「스템을 못 읽는 판」이다.

    ★앞 판은 **공허했다** (Codex BLOCK ③). `snapshot()` 이 `if stems:`
     로 걸러서 빈 팩이 결과에 아예 안 나타났고, 그러니 이 시험이 볼
     대상이 없었다 — **재는 것이 못 보는 것을 「없다」로 읽는** 자리를
     시험 자신이 만들었다. 이제 빈 팩도 빈 dict 로 들어온다.
    """
    _, now = _pair
    empty = [p for p, s in now.items() if not s]
    assert not empty, f"스템이 하나도 없는 팩: {empty[:8]}"
