#!/usr/bin/env python3 """Метрика «дрейф ПОДПИСАННЫХ сущностей ВНЕ инъецированного блока» — класс 花酒行者. Найдена пробой 18 (`docs/experiments/18-editor-wire-probe.md`): инъекция селективна и ограничена бюджетом 800 токенов, поэтому блок несёт лишь то, что сработало в чанке. Всё остальное подписанное не защищено ничем — а редактор его переписывает. Здесь это считается числом. Знаменатель — ПОДПИСАННЫЕ (approved, `gold.jsonl` in_bank) строки, чей src встречается в исходнике окна и которых НЕТ в тест-блоке пробы 18. Скорится присутствие канонической формы в выходе. $0: работает по уже персистированному сырью. """ from __future__ import annotations import json import re import sys import unicodedata from pathlib import Path REPO = Path("/home/ubuntu/projects/textmachine") RAW = Path.home() / "books" / "gu-zhenren" / "bank-arbitration" sys.path.insert(0, str(REPO / "eval" / "bank_arbitration")) from inject_probe import pick_windows, TERMS # noqa: E402 GOLD = {r["bank_src"]: r["gold_dst"] for r in (json.loads(l) for l in open(REPO / "eval/bank_arbitration/gold/gold.jsonl", encoding="utf-8")) if r.get("in_bank")} # Стем-регексы канонических форм. КОНСЕРВАТИВНО: скорим только строки, у которых форма # опознаётся устойчивым стемом. Отброшенные строки печатаются — знаменатель честный. STEM = { "方源": r"фан\s+юан", "方正": r"фан\s+чжэн", "古月": r"гуюэ", # «Гу Юэ» раздельно НЕ матчится — это и есть дрейф "古月漠北": r"гуюэ\s+мобэй", "蛊虫": r"гу-черв", "本命蛊": r"гу\s*жизни", "希望蛊": r"гу\s*надежды", "酒虫": r"винн\w*\s*черв", "家老": r"старейшин", "花酒行者": r"монах\s*цветочного\s*вина", "青茅山": r"гор\w*\s*цинмао", "凡人": r"смертн", "一转": r"перв\w*\s*ранг", "三转": r"трет\w*\s*ранг", "甲等": r"класс\w*\s+а\b", "乙等": r"класс\w*\s+б\b", "丙等": r"класс\w*\s+в\b", "丁等": r"класс\w*\s+г\b", } # Не скорятся: 转 «ранг» (слишком общая форма — ложные срабатывания внутри «первый ранг»), # 炼化 «очистить и подчинить» (глагольная перифраза, устойчивого стема нет). DROPPED = {"转", "炼化"} def norm(s: str) -> str: return " ".join(unicodedata.normalize("NFKC", s or "").casefold().replace("ё", "е").split()) def offblock_cells(block_terms: set[str] | None = None): """[(окно, src, gold_dst, regex)] — подписанные строки окна вне блока.""" block = set(TERMS) if block_terms is None else block_terms out = [] for k, (_, buf, _) in enumerate(pick_windows()): for src, dst in GOLD.items(): if src in block or src in DROPPED or src not in STEM: continue if src in buf: out.append((k, src, dst, STEM[src])) return out def score(tags: dict[str, str], label_of=lambda k: None): """tags: {метка арма: шаблон тега с {k}}. Печатает долю канонических форм вне блока.""" cells = offblock_cells() print(f"знаменатель: {len(cells)} клеток (подписанные строки в исходнике окна, ВНЕ блока), " f"{len({c[1] for c in cells})} уникальных сущностей") print(f"не скорятся (нет устойчивого стема): {', '.join(DROPPED)}\n") rows = {} for arm, pat in tags.items(): hit = tot = 0 misses = [] for k, src, dst, rx in cells: f = RAW / (pat.format(k=k) + ".json") if not f.exists(): continue out = norm(json.load(open(f, encoding="utf-8"))["content"]) tot += 1 if re.search(rx, out): hit += 1 else: misses.append(f"w{k}:{src}«{dst}»") rows[arm] = (hit, tot, misses) pct = f"{100*hit/tot:.0f}%" if tot else "—" print(f" {arm:34s} {hit:3d}/{tot:<3d} ({pct})") return rows if __name__ == "__main__": print("=== ЧЕРНОВИК (что редактор получил на вход) ===") cells = offblock_cells() hit = 0 dmiss = [] for k, src, dst, rx in cells: d = norm(json.load(open(RAW / f"inj-w{k}-B.json", encoding="utf-8"))["content"]) if re.search(rx, d): hit += 1 else: dmiss.append(f"w{k}:{src}«{dst}»") print(f" канон в черновике: {hit}/{len(cells)}") print(f" чего в черновике не было: {', '.join(dmiss)}\n") print("=== ВЫХОД РЕДАКТОРА ===") rows = score({ "A0 full-regen БЕЗ блока": "edp-A0-w{k}", "A5 full-regen БЕЗ блока (2-й розыгр.)": "edp-A5-w{k}", "A1 full-regen + закон-блок": "edp-A1-w{k}", "A2 full-regen + блок (неверная 元海)": "edp-A2-w{k}", }) print("\n=== что именно потеряно (арм A1, блок есть, но эти строки в него не попали) ===") for m in rows["A1 full-regen + закон-блок"][2]: print(" ", m)