#!/usr/bin/env python3
"""build_casting.py \u2014 model temperament profiles for "Cast Your Model" (P17 front-piece).

Parses the committed per-model fingerprints (research/the-map/results/model_notes.md) into a
served casting.json: each of ~67 families with its measured behavioural traits (basin/depth,
imagery, verbosity, list-proneness, deflection/refusal), normalised 0–1 for ranking, plus the
character notes and verbatim signature generations. Derived from a committed artifact \u2014 no raw
data. Re-run:  python3 research/cast-your-model/build_casting.py
"""
import json, re
from pathlib import Path

HERE = Path(__file__).resolve().parent
ROOT = HERE.parents[1]
SRC = (ROOT / "research/the-map/results/model_notes.md").read_text(encoding="utf-8").replace("&#8212;", "\u2014")

# --- the trait table ---
ROW = re.compile(r"^\|\s*([\w.-]+)\s*\|\s*(\d+)\s*\|\s*([\d.]+)\s*\|\s*([\d.]+)\s*\|\s*(\d+)\s*\|\s*(\d+)\s*\|\s*(\d+)\s*\|\s*(\d+)\s*\|\s*(\d+)\s*\|")
models = {}
for line in SRC.splitlines():
    m = ROW.match(line)
    if not m:
        continue
    fam = m.group(1)
    models[fam] = {
        "family": fam, "n": int(m.group(2)),
        "basin": float(m.group(3)), "vivid": float(m.group(4)),
        "defl": int(m.group(5)), "refu": int(m.group(6)), "list": int(m.group(7)),
        "words": int(m.group(8)), "null": int(m.group(9)),
    }

# --- character notes ---
for m in re.finditer(r"^- \*\*([\w.-]+)\*\* \u2014 (.+)$", SRC, re.M):
    fam, note = m.group(1), m.group(2).strip()
    if fam in models:
        if fam == "claude":
            # Assay X7, 2026-10-14: the table gives Claude 198 words and Gemini-pro 77.
            # Correct the imported editorial note locally; signature quotations stay verbatim.
            note = note.replace("in the FEWEST words", "in relatively few words")
            note = note.replace("basin \u2014 writes", "basin; writes")
            note = note.replace("highest imagery density", "highest vividness score")
        if fam == "mimo":
            # The editorial number must agree with the imported table, outside the quotation.
            note = re.sub(r"(most vivid \()[\d.]+(\))", rf"\g<1>{models[fam]['vivid']:.2f}\2", note)
        if fam == "claude-opus":
            note = note.replace("open-ended creative-freedom probes", "the mixed probe sample")
        models[fam]["note"] = note

# --- signature generations (verbatim) ---
for m in re.finditer(r"^\*\*([\w.-]+)\*\* \u2014 _(.+?)_\s*$", SRC, re.M):
    fam, sig = m.group(1), m.group(2).strip()
    if fam in models:
        models[fam]["sig"] = sig

# --- normalise traits 0..1 across the roster (for ranking + bars) ---
def mm(key):
    vals = [v[key] for v in models.values()]
    lo, hi = min(vals), max(vals)
    return lo, (hi - lo) or 1
nb, nv, nw, nl = mm("basin"), mm("vivid"), mm("words"), mm("list")
def comp(v):  # compliance = willing (low deflect+refuse)
    return v["defl"] + v["refu"]
cc = (min(comp(v) for v in models.values()), (max(comp(v) for v in models.values()) - min(comp(v) for v in models.values())) or 1)
nn = mm("null")
for v in models.values():
    v["norm"] = {
        "depth":      round((v["basin"] - nb[0]) / nb[1], 3),
        "vivid":      round((v["vivid"] - nv[0]) / nv[1], 3),
        "brevity":    round(1 - (v["words"] - nw[0]) / nw[1], 3),
        "structure":  round((v["list"] - nl[0]) / nl[1], 3),
        "compliance": round(1 - (comp(v) - cc[0]) / cc[1], 3),
        "reliability":round(1 - (v["null"] - nn[0]) / nn[1], 3),
    }

ordered = sorted(models.values(), key=lambda v: -v["basin"])
out = {
    "source": "The Map's per-model character notes (research/the-map/results/model_notes.md)",
    "n_models": len(ordered),
    "traits": {"depth": "creative-attractor affinity (water/silence/void)",
               "vivid": "proxy combining imagery-marker density and lexical diversity; ineligible outputs score zero",
               "brevity": "fewer words", "structure": "list-proneness", "compliance": "rarely deflects/refuses",
               "reliability": "rarely returns empty"},
    "note": "Measured on a mix of instructed and spontaneous probes: a model's TEMPERAMENT, not its "
            "capability; it may behave differently on a constrained task. Derived from a committed artifact.",
    "models": ordered,
}
# sanity vs the committed character notes
byf = {v["family"]: v for v in ordered}
assert byf["gemini"]["basin"] == max(v["basin"] for v in ordered), "gemini should be deepest basin"
assert byf["claude"]["vivid"] == max(v["vivid"] for v in ordered), "claude should be most vivid"
assert byf["claude"]["norm"]["brevity"] > 0.7, "claude should be terse"
assert byf["claude"]["words"] > min(v["words"] for v in ordered), "claude is not the shortest"
assert "fewest" not in byf["claude"]["note"].lower(), "claude note must not claim the fewest words"
assert f"most vivid ({byf['mimo']['vivid']:.2f})" in byf["mimo"]["note"], "mimo note must match its table score"
print("OK - 6 checks: gemini deepest-diver; claude most-vivid and terse; corrected claude and mimo notes")

PUBLIC = ROOT / "public/strata/cast-your-model"
PUBLIC.mkdir(parents=True, exist_ok=True)
payload = json.dumps(out, ensure_ascii=False, indent=1).replace("\u2014", "\\u2014")
for dst in (HERE / "casting.json", PUBLIC / "casting.json"):
    dst.write_text(payload, encoding="utf-8")
    print(f"wrote {dst} ({dst.stat().st_size//1024} KB, {len(ordered)} models, "
          f"{sum('note' in v for v in ordered)} notes, {sum('sig' in v for v in ordered)} signatures)")

# Publish the inputs needed for the advertised repository-root recipe. The table's
# numbers and signature quotations stay unchanged; its editorial notes match this layer.
reader_source = SRC.replace("imagery density; 'defl'", "imagery and lexical-diversity proxy; 'defl'")
reader_source = re.sub(
    r"^(- \*\*([\w.-]+)\*\* \u2014 )(.+)$",
    lambda m: m.group(1) + models.get(m.group(2), {}).get("note", m.group(3)),
    reader_source, flags=re.M,
)
(PUBLIC / "model_notes.md").write_text(reader_source.replace("\u2014", "&#8212;"), encoding="utf-8")
builder = Path(__file__).read_bytes()
for dst in (PUBLIC / "build_casting.py", ROOT / "public/checks/research/cast-your-model/build_casting.py"):
    dst.parent.mkdir(parents=True, exist_ok=True)
    dst.write_bytes(builder)
print("published builder and input table for the reader recipe")
