#!/usr/bin/env python3
"""exp15 — DETERMINISTIC-first straddle-trap builder (guard 1, owner/orch 2026-07-18). Finds cohesion
traps that ACTUALLY straddle a flat-A0-30 greedy boundary (the common denominator), from the seed +
surface heuristics — no flaky/expensive LLM for the deterministic core. Kimi ENRICHES T-ana/T-ell only.

Types built here:
  T-ent (NEGATIVE CONTROL): a seed entity in chunk_i tail AND chunk_{i+1} head. NOTE the glossary
     injection already enforces its canonical dst per-chunk, so carryover should add ~nothing — a
     negative control that confirms the trap set discriminates (T-ana should move, T-ent should not).
  T-ana (PRIMARY, carryover-discriminating): an anaphor (他/她/它/自己/这/那) at chunk_{i+1} head whose
     antecedent (a person-entity) is in chunk_i tail. Without carryover the chunk_{i+1} translator lacks
     the antecedent -> may mis-resolve gender/referent (ru past tense marks gender).
Undecidable a-priori on this material (guard 3, do NOT imitate): T-tense/T-cat/T-gen/T-reg (density
  3/80 in mining = prose property, not slice size).

Each trap: id, type, boundary_idx, anchor_span (chunk_i tail sent), dependent_span (chunk_{i+1} head
sent), phenomenon, expected_ru, crosses_A0_cut=True. Run: python straddle_traps.py
"""
from __future__ import annotations
import json
import sys
from pathlib import Path

sys.path.insert(0, str(Path(__file__).resolve().parent))
from chunker import greedy_chunks, split_source_sentences
from mem_select import build_bank, normalize_source_key

SD = Path("/home/ubuntu/books/gu-zhenren/exp15")
FLAT = SD / "S2prime_big_flat.txt"
SEED = "/home/ubuntu/books/gu-zhenren/guzhenren-seed-v2.yaml"
OUT = SD / "straddle_ledger.json"
ANAPHORS = ("他", "她", "它", "自己", "这", "那", "其", "此")


def person_gender(bank, src):
    for e in bank.entries:
        if e.src == src:
            # gender is on the seed row; look it up from the raw seed (bank doesn't keep it)
            return None
    return None


def main():
    import yaml
    seed = {t["src"]: t for t in yaml.safe_load(Path(SEED).read_text(encoding="utf-8"))["terms"]}
    bank = build_bank(SEED)
    text = FLAT.read_text(encoding="utf-8")
    ch = greedy_chunks(text)
    traps = []
    for i in range(len(ch) - 1):
        tail_sents = split_source_sentences(ch[i])[-2:]
        head_sents = split_source_sentences(ch[i + 1])[:2]
        tail = "".join(tail_sents)
        head = "".join(head_sents)
        nt, nh = normalize_source_key(tail), normalize_source_key(head)
        # T-ent straddle (negative control)
        for e in bank.entries:
            if e.status != "approved" or not e.norm_keys or not e.dst:
                continue
            if any(k in nt for k in e.norm_keys) and any(k in nh for k in e.norm_keys):
                traps.append({
                    "id": f"T-ent-{len(traps)+1}", "type": "T-ent", "control": True,
                    "boundary_idx": i, "entity_src": e.src, "entity_dst": e.dst,
                    "anchor_span": tail, "dependent_span": head,
                    "phenomenon": f"Сущность {e.src} введена в чанке {i}, повторена в чанке {i+1} через разрез "
                                  f"— НО глоссарий-инъекция даёт «{e.dst}» в обоих чанках (NEGATIVE CONTROL: "
                                  "carryover не должен ничего добавить).",
                    "expected_ru": f"{e.dst} — консистентная каноническая форма в обоих чанках.",
                    "crosses_A0_cut": True,
                })
                break  # one T-ent per boundary
        # T-ana straddle (primary): anaphor at head, antecedent = last person-name in tail
        hstart = head.lstrip("　 \n")[:3]
        if any(hstart.startswith(p) for p in ANAPHORS):
            # find the last person-name in the tail
            antecedent = None
            for e in bank.entries:
                if e.status == "approved" and seed.get(e.src, {}).get("type") in ("name",) and e.norm_keys \
                        and any(k in nt for k in e.norm_keys):
                    g = seed.get(e.src, {}).get("gender", "")
                    antecedent = (e.src, e.dst, g)
            g = antecedent[2] if antecedent else ""
            gtag = {"male": "муж. род (пошёл/сказал)", "female": "жен. род (пошла/сказала)",
                    "hidden": "род скрыт", "": "род по контексту"}.get(g, "род по контексту")
            traps.append({
                "id": f"T-ana-{len(traps)+1}", "type": "T-ana", "control": False,
                "boundary_idx": i,
                "antecedent": antecedent[0] if antecedent else None,
                "antecedent_dst": antecedent[1] if antecedent else None,
                "anchor_span": tail, "dependent_span": head,
                "phenomenon": f"Анафора «{hstart}…» в начале чанка {i+1}; антецедент "
                              f"{'('+antecedent[0]+')' if antecedent else '(в чанке '+str(i)+')'} — по ДРУГУЮ "
                              "сторону разреза. Без carryover переводчик чанка i+1 не видит антецедент.",
                "expected_ru": f"Верный референт/{gtag}"
                               + (f'; антецедент = {antecedent[1]}' if antecedent else ''),
                "crosses_A0_cut": True,
            })
    from collections import Counter
    by = Counter(t["type"] for t in traps)
    OUT.write_text(json.dumps({"n": len(traps), "by_type": dict(by),
                               "note": "deterministic-first (guard 1); T-ent=negative control (glossary-handled); "
                                       "T-ana=primary carryover-discriminating; T-tense/cat/gen/reg=undecidable a-priori (guard 3)",
                               "traps": traps}, ensure_ascii=False, indent=2), encoding="utf-8")
    print(f"deterministic straddle traps: {dict(by)} (total {len(traps)}) -> {OUT}")
    print("  T-ent = negative control (glossary enforces); T-ana = primary (carryover-discriminating)")
    print(f"  T-ana deterministic = {by.get('T-ana',0)}; quota >=20 -> kimi enrichment needed for +{max(0,20-by.get('T-ana',0))}")


if __name__ == "__main__":
    main()
