#!/usr/bin/env python3
"""Torn-verify for 016 - A Name Became Ambiguous Without Being Touched.

Claim under test: the model name GPT-5.6 did not change and yet became ambiguous.
On one day it was a complete name; on the next it was a proper prefix of three
other complete names. The string is innocent -- ambiguity is a property of the
SET a name belongs to, and the set is not under the string's control.

The finding is prefix-code theory. A set of names is a prefix code when no member
is a proper prefix of another; such codes are instantaneously decodable. Drop a
symbol from a prefix code and the result is not a name at all -- the error is
loud. Drop a symbol from a code that is NOT prefix-free and you can land, silently,
on something else that is perfectly legal.

This reads the legal names from models.txt -- the repository's own dictionary, the
one check_lab.py enforces -- so the check cannot drift from the names the site
actually publishes. Standard library only, deterministic, ASCII output. Exit 0 iff
every part of the claim holds.

The first draft claimed the CURRENT OpenAI set is not prefix-free. It is. That
refutation is kept below as a passing check, because it is the same shape as the
finding: a claim that looked complete, was locally well-formed, and needed a set it
did not contain in order to be judged. Only the UNION of retired and current fails.
"""

import pathlib
import sys

FAILS = []


def check(name, condition, detail=""):
    print(("PASS" if condition else "FAIL") + "  " + name +
          (("  [" + detail + "]") if detail else ""))
    if not condition:
        FAILS.append(name)


# --- the dictionary, read from the repo (not hard-coded here) ------------------
ROOT = pathlib.Path(__file__).resolve().parents[2]
MODELS = ROOT / "models.txt"


def load_models(path):
    names = []
    for line in path.read_text(encoding="utf-8").splitlines():
        line = line.split("#", 1)[0].strip()
        if line:
            names.append(line)
    return names


def toks(name):
    return tuple(name.split())


def is_proper_prefix(a, b):
    """token-wise: a is a proper prefix of b."""
    return len(a) < len(b) and b[:len(a)] == a


def prefix_free(nameset):
    seq = [toks(n) if isinstance(n, str) else tuple(n) for n in nameset]
    for i, a in enumerate(seq):
        for j, b in enumerate(seq):
            if i != j and is_proper_prefix(a, b):
                return False
    return True


def well_formed(t):
    t = tuple(t)
    if not t:
        return False
    if t[0] == "Claude":
        return len(t) == 3 and t[1] in {"Opus", "Sonnet", "Haiku", "Fable"}
    if t[0].startswith("GPT-"):
        return len(t) == 1 or (len(t) == 2 and t[1] in {"Sol", "Terra", "Luna"})
    return False


models = load_models(MODELS)
anthropic = [n for n in models if n.startswith("Claude")]
openai = [n for n in models if n.startswith("GPT-")]
# The OpenAI scheme as it stood BEFORE strength suffixes: each current name minus its
# last word. Derived from the list, so a new tier needs no edit here.
retired_openai = sorted({toks(n)[:-1] for n in openai if len(toks(n)) > 1})

check("the dictionary has both makers to compare",
      len(anthropic) >= 2 and len(openai) >= 2,
      "%d Anthropic, %d OpenAI names in models.txt" % (len(anthropic), len(openai)))

# --- 1. each set alone is prefix-free -- which is why nobody saw it coming ------
check("the current Anthropic names are prefix-free on their own",
      prefix_free(anthropic), " | ".join(anthropic))
check("the current OpenAI names are prefix-free on their own (the first draft's claim, REFUTED)",
      prefix_free(openai), " | ".join(openai) + "  -> the current set alone is fine")
check("the retired OpenAI scheme (before suffixes) is prefix-free on its own",
      prefix_free(retired_openai),
      " | ".join(" ".join(t) for t in retired_openai))

# --- 2. the ambiguity lives in the UNION, not in any string --------------------
union = [toks(n) for n in openai] + retired_openai
check("their UNION is NOT prefix-free: a retired name is a prefix of a current one",
      not prefix_free(union),
      "GPT-5.6 is a proper prefix of GPT-5.6 Sol/Terra/Luna in before + after")

# name the exact collisions, so the failure is concrete not asserted
collisions = sorted({" ".join(a) + "  <=  " + " ".join(b)
                     for a in union for b in union if is_proper_prefix(a, b)})
check("the collision is exactly the retired name sitting under the current ones",
      collisions == sorted({"GPT-5.6  <=  GPT-5.6 " + s for s in ("Luna", "Sol", "Terra")}),
      "; ".join(collisions))

# --- 3. counterfactual: position is the sole variable --------------------------
# Grow one base set two ways. APPEND a discriminator to a name (terminal) and the
# base name becomes its prefix -> union breaks. INSERT one in the middle (medial)
# and the base name is no longer a prefix -> union holds. Same discriminator, same
# base, only the position differs.
base_term = [("GPT-5.6",)]
appended = base_term + [("GPT-5.6", "Sol")]                 # discriminator at the END
base_med = [("Claude", "4.8")]
inserted = base_med + [("Claude", "Opus", "4.8")]           # discriminator in the MIDDLE
check("appending a discriminator breaks prefix-freedom of the union",
      not prefix_free(appended), "{GPT-5.6} + {GPT-5.6 Sol}")
check("inserting the same discriminator medially does not",
      prefix_free(inserted), "{Claude 4.8} + {Claude Opus 4.8}")

# --- 4. position test: five deletions, three silent, two loud ------------------
# Delete one word from a name and ask whether the result is still a well-formed name.
# Terminal deletions (OpenAI) stay legal and drop SILENTLY; medial deletions
# (Anthropic) become malformed and are LOUD.
terminal = [toks(n)[:-1] for n in openai[:3]]                       # drop the last word
medial = [toks(n)[:1] + toks(n)[2:] for n in anthropic[:2]]        # drop the middle word
silent = [t for t in terminal if well_formed(t)]
loud = [t for t in medial if not well_formed(t)]
check("three terminal deletions stay well-formed (silent truncation)",
      len(silent) == 3 and len(terminal) == 3,
      " ; ".join(" ".join(t) or "(empty)" for t in terminal))
check("two medial deletions become malformed (loud, a reader stops)",
      len(loud) == 2 and len(medial) == 2,
      " ; ".join(" ".join(t) for t in medial))
check("the asymmetry is position, not carelessness: 3 silent vs 2 loud",
      len(silent) == 3 and len(loud) == 2,
      "terminal drops land on a legal name; medial drops do not")

print()
print("claim holds:", not FAILS)
print("FAILS:", FAILS if FAILS else "none")
raise SystemExit(1 if FAILS else 0)
