#!/usr/bin/env python3 """exp14 — материал: trap-чанки, Stage-II главы (детерм. D), реконструкция глоссарий- инъекции из rerun-store (rig-фиделити A1′, как exp12_blocks), P0-baseline paragraph-KPI. $0. Выход: /home/ubuntu/books/gu-zhenren/exp14/material.json (ВНЕ git). """ from __future__ import annotations import json, re, sqlite3, statistics from pathlib import Path RERUN = Path("/home/ubuntu/books/gu-zhenren/rerun") OUT = Path("/home/ubuntu/books/gu-zhenren/exp14"); OUT.mkdir(parents=True, exist_ok=True) DB = RERUN / "guzhenren-rerun.db" # ── Заморожённый выбор (§1.0) ────────────────────────────────────────────────────── TRAP_CHUNKS = [(7, 0), (8, 0), (8, 1), (9, 0), (10, 0), (11, 0), (11, 1), (24, 0), (24, 1)] STAGE2 = [19, 17, 18] EXCLUDE = {(5, 0), (20, 0)} # экспорт-баг D35.4a EDITOR_HEADER = ("КАНОНИЧЕСКИЕ ПЕРЕВОДЫ имён и терминов (в черновике эти сущности должны быть " "переданы ИМЕННО этими формами — приводи к ним любые расхождения, склоняя по " "контексту; не вводи иных вариантов и не меняй ничего другого):") GLOSS_HEADER = ("ГЛОССАРИЙ (используй эти утверждённые переводы имён и терминов последовательно; " "строки с пометкой ⟨проверить⟩ — неподтверждённые кандидаты):") def load_records(): d = json.load(open(RERUN / "records.json")) return {(r["chapter"], r["chunk_idx"]): r for r in d}, d def paras(t: str): return [p for p in re.split(r"\n\s*\n", t.strip()) if p.strip()] def sents(p: str): return [s for s in re.split(r"(?<=[.!?…])\s+", p.strip()) if s.strip()] def dialogue_D(source: str) -> float: """exp12 §1.1: доля символов внутри «“…”» (U+201C..U+201D) от не-пробельных.""" nonspace = sum(1 for c in source if not c.isspace()) inq, indq = 0, False for c in source: if c == "“": indq = True; continue if c == "”": indq = False; continue if indq and not c.isspace(): inq += 1 return inq / max(1, nonspace) def reconstruct_injection(): """editor_constraint (dst-only, approved, dedup) + bilingual_glossary (src→dst[+⟨проверить⟩]) per chunk из injected_ids + glossary. Sticky union chunk0∪.. внутри главы (exp12 A5).""" con = sqlite3.connect(DB); con.row_factory = sqlite3.Row gl = {(r["src"], r["sense"], r["since_ch"], r["until_ch"]): (r["dst"], r["status"]) for r in con.execute("SELECT src,sense,since_ch,until_ch,dst,status FROM glossary")} inj = {} for r in con.execute("SELECT chapter,chunk_idx,injected_ids,n_sticky,n_evicted FROM retrieval_state"): inj[(r["chapter"], r["chunk_idx"])] = (json.loads(r["injected_ids"] or "[]"), r["n_sticky"], r["n_evicted"]) con.close() def keydec(k): src, sense, since, until = k.split("\x1f") return (src, sense, int(since), int(until)) def block_for(keys): recs = [] for k in keys: src, sense, since, until = keydec(k) dst, status = gl.get((src, sense, since, until), ("", "auto")) if dst.strip(): recs.append((src, dst.strip(), status == "approved")) seen, el = set(), [] for src, dst, conf in recs: if conf and dst not in seen: seen.add(dst); el.append(f"- «{dst}»") eb = (EDITOR_HEADER + "\n" + "\n".join(el)) if el else "" glines = [f"{src} → {dst}" + ("" if conf else " ⟨проверить⟩") for src, dst, conf in recs] gb = (GLOSS_HEADER + "\n" + "\n".join(glines)) if glines else "" return eb, gb, len(el), len(glines) out = {} chapters = sorted(set(ch for ch, _ in inj)) for ch in chapters: cidxs = sorted(c for (cc, c) in inj if cc == ch) chunk0keys = inj.get((ch, 0), ([], 0, 0))[0] for c in cidxs: keys, nst, nev = inj[(ch, c)] active = list(keys) for k in chunk0keys: # sticky scene-inertia within chapter if c > 0 and k not in active: active.append(k) eb, gb, ne, ng = block_for(active) out[f"{ch}/{c}"] = {"editor_constraint": eb, "bilingual_glossary": gb, "n_constraint": ne, "n_gloss": ng, "n_sticky": nst, "n_evicted": nev} # chapter-level union (для P1b/P1c whole-chapter арма) allkeys = [] for c in cidxs: for k in inj[(ch, c)][0]: if k not in allkeys: allkeys.append(k) eb, gb, ne, ng = block_for(allkeys) out[f"{ch}/ALL"] = {"editor_constraint": eb, "bilingual_glossary": gb, "n_constraint": ne, "n_gloss": ng} return out def p0_kpi(recs): """P0 baseline: sentences/narrative-paragraph по 55 committed чанкам (freeze-число).""" sp = [] for (ch, ck), r in recs.items(): if (ch, ck) in EXCLUDE: continue for p in paras(r["final"]): if p.strip().startswith("—"): continue sp.append(len(sents(p))) return {"n_paras": len(sp), "median": statistics.median(sp), "mean": round(statistics.mean(sp), 3), "share_1sent": round(sum(1 for x in sp if x == 1) / len(sp), 3)} def main(): recs, _ = load_records() inj = reconstruct_injection() # verify: trap chunks have reconstructed blocks print("=== Инъекция реконструирована (trap-чанки) ===") for ch, ck in TRAP_CHUNKS: b = inj[f"{ch}/{ck}"] print(f" {ch}.{ck}: {b['n_constraint']} constraint / {b['n_gloss']} gloss " f"(sticky={b['n_sticky']} evict={b['n_evicted']})") # Stage-II D print("\n=== Stage-II главы: детерм. D (диалого-плотность) ===") d_scores = {} for ch in STAGE2: src = "".join(recs[(ch, c)]["source"] for (cc, c) in recs if cc == ch) d_scores[ch] = round(dialogue_D(src), 3) print(f" гл.{ch}: D={d_scores[ch]}") # P0 KPI kpi = p0_kpi(recs) print(f"\n=== P0 baseline paragraph-KPI (freeze-сверка) ===\n {kpi}") # export material for arms mat = {"trap_chunks": [], "stage2": {}, "kpi_p0": kpi, "stage2_D": d_scores} for ch, ck in TRAP_CHUNKS: r = recs[(ch, ck)] mat["trap_chunks"].append({"chapter": ch, "chunk_idx": ck, "source": r["source"], "draft": r["draft"], "final_p0": r["final"], "editor_constraint": inj[f"{ch}/{ck}"]["editor_constraint"], "bilingual_glossary": inj[f"{ch}/{ck}"]["bilingual_glossary"]}) for ch in STAGE2: mat["stage2"][ch] = [{"chunk_idx": c, "source": recs[(ch, c)]["source"], "draft": recs[(ch, c)]["draft"], "final_p0": recs[(ch, c)]["final"], "editor_constraint": inj[f"{ch}/{c}"]["editor_constraint"], "bilingual_glossary": inj[f"{ch}/{c}"]["bilingual_glossary"]} for (cc, c) in sorted(recs) if cc == ch] (OUT / "material.json").write_text(json.dumps(mat, ensure_ascii=False), encoding="utf-8") (OUT / "injection_blocks.json").write_text(json.dumps(inj, ensure_ascii=False, indent=1), encoding="utf-8") print(f"\n→ {OUT/'material.json'} ({len(mat['trap_chunks'])} trap chunks, {len(STAGE2)} stage-II chapters)") if __name__ == "__main__": main()