#!/usr/bin/env python3 """exp12 — реконструкция per-chunk инъекционных блоков глоссария из приёмочного store. $0. Воспроизводит ровно то, что боевой редактор A1 (glm-5 mono) видел: - editorConstraintBlock (renderEditorConstraintBlock, memory.go): CONFIRMED dst-формы, строки «- «dst»», дедуп по dst. Заголовок editorConstraintHeader. - bilingualGlossaryBlock (renderGlossaryBlock, memory.go): «src → dst» (+ ⟨проверить⟩ для не-CONFIRMED). Заголовок glossaryBlockHeader. Для билингв-армов (редактор видит src). Активные ключи чанка = injected_ids(chunk) ∪ injected_ids(chunk0) для chunk>0 (аппрокс sticky A5 внутри главы; выбранные главы — 2-чанковые, sticky несёт ровно активные chunk0). Ключ injected_id = «src\x1fsense\x1fsince\x1funtil» → строка glossary по (src,sense,since,until). CONFIRMED ⇔ status='approved' и окно since/until валидно для главы. Выход: diag/blocks.json {(ch/chunk): {editor_constraint, bilingual_glossary, keys[]}} """ from __future__ import annotations import json, sqlite3 from pathlib import Path DIAG = Path("/home/ubuntu/books/gu-zhenren/diag") DB = DIAG / "store-copy.db" SEL = [5, 7, 14] # пре-регистрированный выбор (exp12_extract.py) EDITOR_HEADER = ("КАНОНИЧЕСКИЕ ПЕРЕВОДЫ имён и терминов (в черновике эти сущности должны быть " "переданы ИМЕННО этими формами — приводи к ним любые расхождения, склоняя по " "контексту; не вводи иных вариантов и не меняй ничего другого):") GLOSS_HEADER = ("ГЛОССАРИЙ (используй эти утверждённые переводы имён и терминов последовательно; " "строки с пометкой ⟨проверить⟩ — неподтверждённые кандидаты):") def main() -> None: con = sqlite3.connect(DB) con.row_factory = sqlite3.Row # glossary lookup by (src,sense,since,until) gl = {} for r in con.execute("SELECT src,sense,since_ch,until_ch,dst,status FROM glossary"): gl[(r["src"], r["sense"], r["since_ch"], r["until_ch"])] = (r["dst"], r["status"]) # injected_ids per selected chunk inj = {} for r in con.execute( "SELECT chapter,chunk_idx,injected_ids FROM retrieval_state WHERE chapter IN (%s)" % ",".join(map(str, SEL)) ): inj[(r["chapter"], r["chunk_idx"])] = json.loads(r["injected_ids"] or "[]") con.close() def keydec(k: str): src, sense, since, until = k.split("\x1f") return (src, sense, int(since), int(until)) out = {} for ch in SEL: chunks = sorted(c for (cc, c) in inj if cc == ch) chunk0 = inj.get((ch, 0), []) for c in chunks: active = list(inj.get((ch, c), [])) if c > 0: # sticky: carry chunk0's exact matches (A5 scene inertia within chapter) for k in chunk0: if k not in active: active.append(k) # map to records recs = [] for k in active: src, sense, since, until = keydec(k) dst, status = gl.get((src, sense, since, until), ("", "auto")) if not dst.strip(): continue confirmed = (status == "approved") # windows all 0/0 here → spoiler-valid recs.append({"src": src, "dst": dst.strip(), "confirmed": confirmed}) # editor constraint block: CONFIRMED only, dedup by dst seen, elines = set(), [] for r in recs: if not r["confirmed"] or r["dst"] in seen: continue seen.add(r["dst"]); elines.append(f"- «{r['dst']}»") editor_block = (EDITOR_HEADER + "\n" + "\n".join(elines)) if elines else "" # bilingual glossary block: src → dst (+ ⟨проверить⟩ if not confirmed) glines = [] for r in recs: line = f"{r['src']} → {r['dst']}" if not r["confirmed"]: line += " ⟨проверить⟩" glines.append(line) gloss_block = (GLOSS_HEADER + "\n" + "\n".join(glines)) if glines else "" out[f"{ch}/{c}"] = { "editor_constraint": editor_block, "bilingual_glossary": gloss_block, "n_constraint_terms": len(elines), "n_gloss_terms": len(glines), } (DIAG / "blocks.json").write_text(json.dumps(out, ensure_ascii=False, indent=1), encoding="utf-8") for k, v in out.items(): print(f"\n=== chunk {k}: {v['n_constraint_terms']} constraint / {v['n_gloss_terms']} gloss terms ===") print(v["editor_constraint"][:400]) if __name__ == "__main__": main()