textmachine/eval/exp15/straddle_traps.py

110 lines
6.3 KiB
Python
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/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()