169 lines
9 KiB
Python
169 lines
9 KiB
Python
#!/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())
|