#!/usr/bin/env python3
"""S2-Ff shape diff — frozen fixture ↔ live response 의 *모양*(키/타입)만 비교.

스칼라 *값*은 무시한다(LLM 응답 본문·id·토큰수는 호출마다 다름). 비교 대상 = 키 존재 + 타입.
argv[1] = frozen fixture 경로, argv[2] = live 응답 JSON 경로.
exit 1 = breaking 드리프트(frozen 키 삭제 / 타입 변경), exit 0 = clean(동일 or live 신규키만).
양쪽 top-level `_meta`(우리 주석)는 비교에서 제외.
"""
import json
import sys


def strip_meta(o):
    return {k: v for k, v in o.items() if k != "_meta"} if isinstance(o, dict) else o


def tag(v):
    if v is None:
        return "null"
    if isinstance(v, bool):
        return "bool"
    if isinstance(v, (int, float)):
        return "number"
    if isinstance(v, str):
        return "string"
    if isinstance(v, list):
        return "array"
    if isinstance(v, dict):
        return "object"
    return "unknown"


breaking = []
info = []


def diff(live, frozen, path):
    lt, ft = tag(live), tag(frozen)
    # nullability 는 드리프트로 보지 않음(fixture 의 null = nullable 표식).
    if lt == "null" or ft == "null":
        if lt != ft:
            info.append(f"{path or '<root>'}: null↔{ft if lt == 'null' else lt} (nullable, 무시 가능)")
        return
    if lt != ft:
        breaking.append(f"{path or '<root>'}: frozen={ft} live={lt} (타입 변경)")
        return
    if ft == "object":
        for k, fv in frozen.items():
            p = f"{path}.{k}" if path else k
            if k in live:
                diff(live[k], fv, p)
            else:
                breaking.append(f"{p}: frozen 에 있으나 live 에 없음 (키 삭제)")
        for k in live:
            if k not in frozen:
                p = f"{path}.{k}" if path else k
                info.append(f"{p}: live 신규 키 (비파괴)")
    elif ft == "array":
        if frozen:
            if live:
                diff(live[0], frozen[0], f"{path}[0]")
            else:
                info.append(f"{path}[]: live 빈 배열 — element shape 검증 불가")


def main():
    if len(sys.argv) != 3:
        print("usage: shape_diff.py <frozen_fixture> <live_json>", file=sys.stderr)
        sys.exit(2)
    frozen = strip_meta(json.load(open(sys.argv[1], encoding="utf-8")))
    live = strip_meta(json.load(open(sys.argv[2], encoding="utf-8")))
    diff(live, frozen, "")
    for m in breaking:
        print(f"  DRIFT  {m}")
    for m in info:
        print(f"  info   {m}")
    if not breaking and not info:
        print("  (shape identical)")
    sys.exit(1 if breaking else 0)


if __name__ == "__main__":
    main()
