#!/usr/bin/env python3 """Полигон, пакет-8 (замер 1, часть 2): профиль канала БАНКНОТЫ — то, чего recall против сида не видит. Первая часть замера (`joint_recall.py`) упёрлась в пре-зарегистрированный стоп: банкнота гонялась с непустым банком, свободных от инъекции термов сида осталось 2 из 51, и совместный recall против подписи объявлен неизмеримым на существующих артефактах. Этот скрипт добывает то, что ИЗМЕРИМО и на что цензура не действует: 1. ЦЕНЗУРА ПО ГЛАВАМ. Инъекция глоссария пер-чанковая, а не книжная: терм, инъектированный в главе 2, в главе 7 модели не показывался. Значит пара (терм, глава) даёт куда больший знаменатель со свободным шансом, чем терм целиком. Считаем именно пары. 2. ОБЪЕКТИВНЫЙ ПРОФИЛЬ эмиссии банкноты: частота и рассеяние по главам каждой предложенной поверхности. Это заменяет разметку «годный/мусор» там, где разметка была бы моим вкусом: пороги берём НЕ свои, а боевые (`emitMinFreq`=5, `runeLen`≥2 из miner_emit.go) — вопрос ставится как «сколько предложений банкноты прошло бы фильтры, которые движок уже применяет к майнеру», и ответ на него проверяем кем угодно. $0. Читает только КОПИИ баз и срез. """ from __future__ import annotations import argparse import json import sqlite3 import subprocess import sys from collections import defaultdict from pathlib import Path import yaml sys.path.insert(0, str(Path(__file__).parent)) from joint_recall import parse_banknote, split_banknote # noqa: E402 (одна реализация парсера, не две) EMIT_MIN_FREQ = 5 # miner_emit.go:31 — боевой порог частоты эмиссии EMIT_MIN_RUNES = 2 # miner_emit.go — боевой порог длины def norm_keys(harness: Path, strings: list[str]) -> dict[str, str]: uniq = sorted({s for s in strings if s.strip()}) p = subprocess.run([str(harness), "-mode", "norm"], input="\n".join(uniq), capture_output=True, text=True, check=True) out = {} for line in p.stdout.split("\n"): if line: src, _, key = line.partition("\t") out[src] = key return out def main() -> int: ap = argparse.ArgumentParser() ap.add_argument("--seed", required=True, type=Path) ap.add_argument("--chunks", required=True, type=Path) ap.add_argument("--db", required=True, type=Path, help="КОПИЯ базы прогона банкноты (основной)") ap.add_argument("--harness", required=True, type=Path) ap.add_argument("--out", type=Path) a = ap.parse_args() rows = [json.loads(l) for l in a.chunks.open(encoding="utf-8") if l.strip()] by_chapter: dict[int, str] = defaultdict(str) for r in rows: by_chapter[r["chapter"]] += "\n" + r["source"] text = "\n".join(by_chapter[c] for c in sorted(by_chapter)) con = sqlite3.connect(f"file:{a.db}?mode=ro", uri=True) # инъекция по главам injected_by_ch: dict[int, set[str]] = defaultdict(set) for ch, blob in con.execute("select chapter, injected_ids from retrieval_state where injected_ids<>''"): for ident in json.loads(blob): injected_by_ch[ch].add(ident.split("\x1f")[0]) # банкнота по главам bank_by_ch: dict[int, list[dict]] = defaultdict(list) q = ("select j.chapter, cp.response_text, cp.finish_reason from checkpoints cp " "join jobs j on j.id=cp.job_id where cp.role='translator' and cp.response_text like '%TM-BANK%'") for ch, resp, finish in con.execute(q): _, block, malformed = split_banknote(resp or "") if malformed or not block or finish != "stop": continue ents, _ = parse_banknote(block) bank_by_ch[ch].extend(ents) con.close() doc = yaml.safe_load(a.seed.read_text(encoding="utf-8")) or {} terms = doc.get("terms") or [] strings = [t.get("src") or "" for t in terms] for t in terms: strings.extend(al.get("alias", "") for al in (t.get("aliases") or [])) for ents in bank_by_ch.values(): strings.extend(e["src"] for e in ents) for s in injected_by_ch.values(): strings.extend(s) nk = norm_keys(a.harness, strings) bank_keys_by_ch = {ch: {nk.get(e["src"], e["src"]) for e in ents} for ch, ents in bank_by_ch.items()} inj_keys_by_ch = {ch: {nk.get(s, s) for s in ss} for ch, ss in injected_by_ch.items()} # --- 1. пары (терм, глава) -------------------------------------------------------------------- pairs_total = pairs_free = pairs_free_hit = pairs_cens = pairs_cens_hit = 0 free_rows = [] for t in terms: src = t.get("src") or "" surfaces = [src] + [al.get("alias", "") for al in (t.get("aliases") or []) if al.get("alias")] keys = {nk.get(s, s) for s in surfaces if s} for ch in sorted(by_chapter): body = by_chapter[ch] if not any(s and s in body for s in surfaces): continue pairs_total += 1 hit = bool(keys & bank_keys_by_ch.get(ch, set())) if keys & inj_keys_by_ch.get(ch, set()): pairs_cens += 1 pairs_cens_hit += int(hit) else: pairs_free += 1 pairs_free_hit += int(hit) free_rows.append({"src": src, "type": t.get("type", "") or "term", "chapter": ch, "hit": hit, "occ": sum(body.count(s) for s in surfaces if s)}) print("=== 1. Цензура по парам (терм × глава) ===") print(f"пар «терм сида встречается в главе»: {pairs_total}") print(f" инъектирован в этой главе (шанса не было): {pairs_cens}" f" из них банкнота всё равно предложила: {pairs_cens_hit}") print(f" НЕ инъектирован (свободный шанс): {pairs_free}" f" банкнота предложила: {pairs_free_hit}" + (f" = {pairs_free_hit / pairs_free:.3f}" if pairs_free else "")) if free_rows: print(" свободные пары (топ-15 по вхождениям в главе):") for r in sorted(free_rows, key=lambda r: -r["occ"])[:15]: print(f" гл.{r['chapter']:<3d} {r['src']}\t{r['type']}\t{r['occ']}×\t{'НАШЛА' if r['hit'] else '—'}") # --- 2. объективный профиль эмиссии банкноты -------------------------------------------------- all_ents = [e for ents in bank_by_ch.values() for e in ents] prof = {} for e in all_ents: s = e["src"] if s in prof: continue freq = text.count(s) chs = sum(1 for ch in by_chapter if s in by_chapter[ch]) prof[s] = {"type": e["type"], "freq": freq, "chapters": chs, "runes": len(s)} n = len(prof) passes = [s for s, p in prof.items() if p["freq"] >= EMIT_MIN_FREQ and p["runes"] >= EMIT_MIN_RUNES] multi = [s for s, p in prof.items() if p["chapters"] >= 2] once = [s for s, p in prof.items() if p["freq"] <= 1] print("\n=== 2. Объективный профиль эмиссии банкноты (боевые пороги майнера) ===") print(f"уникальных поверхностей: {n}") print(f" прошли бы emitMinFreq≥{EMIT_MIN_FREQ} и runeLen≥{EMIT_MIN_RUNES}: {len(passes)} = {len(passes)/n:.3f}") print(f" встречаются в ≥2 главах: {len(multi)} = {len(multi)/n:.3f}") print(f" встречаются в срезе ≤1 раза (разовые): {len(once)} = {len(once)/n:.3f}") print("\n прошедшие оба боевых порога:") for s in sorted(passes, key=lambda s: -prof[s]["freq"]): p = prof[s] print(f" {s}\t{p['type']}\t{p['freq']}×\tглав:{p['chapters']}") if a.out: a.out.write_text(json.dumps({ "pairs": {"total": pairs_total, "censored": pairs_cens, "censored_hit": pairs_cens_hit, "free": pairs_free, "free_hit": pairs_free_hit}, "free_rows": free_rows, "profile": prof, }, ensure_ascii=False, indent=1), encoding="utf-8") return 0 if __name__ == "__main__": sys.exit(main())