diff --git a/eval/role_topology/drift.py b/eval/role_topology/drift.py index 766ac2ae..adc75a6b 100644 --- a/eval/role_topology/drift.py +++ b/eval/role_topology/drift.py @@ -45,7 +45,12 @@ OUT = Path.home() / "books" / "role-topology" CORP = Path.home() / "books" / "gu-zhenren" / "labels" / "raw" / "corpus.jsonl" N_CHAPTERS = 10 # заказ промта: 8–10 -DRIFT_ARMS = ("F3", "A", "D_") # черновик · связка · однопроходка +# ⚠ ПОПРАВКА К ФРИЗУ 07.08, внесена ДО чтения боевых чисел дрейфа и с объявленной причиной. +# Первый состав ("F3","A","D_") нёс КОНФАУНД, найденный проверкой сообщений: банк терминов +# подаётся черновику и `D_` (block="law"), а арму `A` — НЕТ (block=None, это боевой бейзлайн +# без банкноты). На оси «консистентность термина» это означало бы замер БАНКА под видом замера +# топологии. `A_law` — тот же редактор С банком — делает сравнение честным. +DRIFT_ARMS = ("F3", "A", "A_law", "D_") # черновик · связка без банка · связка с банком · однопроходка CEIL_DRIFT = 0.60 # ⚠ Глоб покрывает ТОТ ЖЕ тег, которым идут покупки. Правило заведено после того, как касса # армов глобила `bo2-*`, а покупки шли тегом `bo4-*`, и $0.29309 прошли мимо потолка. @@ -181,25 +186,97 @@ def terms(ch: list[dict], min_ch: int = 3) -> list[str]: return sorted(t for t, s in seen.items() if len(s) >= min_ch) +def variants(arm: str, ch: list[dict]) -> dict: + """Какую из ДВУХ конкурирующих русских форм термина арм выбрал в каждой главе. + + Банк `inject_probe.TERMS` держит на каждый zh-термин пару соперничающих форм и регулярки для + их опознания. Дрейф — это когда арм в главе k пишет одну форму, а в главе k+1 другую, при + том что zh-термин тот же. Считается детерминированно, судья не нужен. + """ + out: dict[str, dict[int, set]] = {} + for zh, (_v1, _v2, r1, r2) in BO.IP.TERMS.items(): + for c in ch: + if zh not in c["source"]: + continue + txt = text(arm, c["uid"]) + if not txt.strip(): + continue + got = set() + if re.search(r1, txt, re.I): + got.add(0) + if re.search(r2, txt, re.I): + got.add(1) + if got: + out.setdefault(zh, {})[c["no"]] = got + return out + + def cmd_score() -> None: ch = chapters() - have = [c for c in ch if text("F3", c["uid"]).strip()] - if not have: - print("текстов нет — сначала --arms") - return - print(f"ДРЕЙФ на {len(have)} подряд идущих главах ({have[0]['no']}…{have[-1]['no']})\n") - print(f"{'арм':6s}{'ты/вы смеш.':>13s}{'новых вариантов/гл.':>21s}{'знаков':>9s}") - for arm in DRIFT_ARMS: - ts = [text(arm, c["uid"]) for c in have] - ty = sum(1 for t in ts if re.search(r"\bты\b", t) and re.search(r"\bвы\b", t, re.I)) + print(f"ДРЕЙФ НА СВЯЗНОМ ОТРЕЗКЕ: главы {ch[0]['no']}…{ch[-1]['no']}\n") + have = {a: [c for c in ch if text(a, c["uid"]).strip()] for a in DRIFT_ARMS} + print("покрытие отрезка армами:") + for a in DRIFT_ARMS: + miss = [c["no"] for c in ch if c not in have[a]] + print(f" {a:4s} {len(have[a])}/{len(ch)} глав" + (f" · нет: {miss}" if miss else "")) + print("\n⚠ у связки нога черновика: глава без годного черновика выпадает целиком.") + print(" Однопроходка этого отказного режима не имеет ПО ПОСТРОЕНИЮ.\n") + + # ⚠ МЕТРИКА ИСПРАВЛЕНА ИСПОЛНЕНИЕМ. Первая версия считала, какую из ДВУХ форм банка выбрал + # арм, и давала «расхождений нет» у всех троих. Проверка показала, что ВТОРАЯ форма не + # срабатывает НИ РАЗУ (0 попаданий на 8 терминах × 3 армах × 10 главах), то есть ноль был + # тривиален. При этом у арма есть ТРЕТЬЯ форма, банку неизвестная: 蛊师 передаётся то как + # «гу-мастер», то «Мастер Гу», то просто «мастер». Здесь считается то, что видно без + # выравнивания и сравнимо МЕЖДУ АРМАМИ на одних главах: держит ли арм канонную форму там, + # где zh-термин стоит в исходнике, и сколько раз этот признак ПЕРЕКЛЮЧАЕТСЯ между соседними + # главами. Переключение и есть дрейф; удержание — консистентность. + print(f"{'арм':5s}{'клеток':>8s}{'канон держится':>16s}{'ПЕРЕКЛЮЧЕНИЙ':>14s}{'на переход':>12s}" + f"{'ты/вы смеш.':>13s}") + common = [c for c in ch if all(text(a, c["uid"]).strip() for a in DRIFT_ARMS)] + print(f" (общих глав у всех армов: {len(common)})") + per_arm = {} + for a in DRIFT_ARMS: + cells = hold = flips = trans = 0 + detail: dict[str, list] = {} + for zh, (_v1, _v2, r1, _r2) in BO.IP.TERMS.items(): + seq = [] + for c in common: + if zh not in c["source"]: + continue + cells += 1 + ok = bool(re.search(r1, text(a, c["uid"]), re.I)) + hold += ok + seq.append((c["no"], ok)) + for (_n1, o1), (_n2, o2) in zip(seq, seq[1:]): + trans += 1 + flips += o1 != o2 + if len(seq) > 1: + detail[zh] = seq + per_arm[a] = detail + ty = sum(1 for c in common + if re.search(r"(?8d}{hold:>13d}/{cells}{flips:>14d}" + f"{(flips / trans if trans else 0):>12.2f}{ty:>10d}/{len(common)}") + + print("\nПОТЕРМИННО (+ канон держится, − арм написал что-то другое):") + for a in DRIFT_ARMS: + print(f" {a}:") + for zh, seq in sorted(per_arm[a].items()): + line = " ".join(f"{n}{'+' if o else '−'}" for n, o in seq) + fl = sum(1 for x, y in zip(seq, seq[1:]) if x[1] != y[1]) + print(f" {zh:6s} {line} переключений {fl}") + + print("\nРОСТ СЛОВАРЯ (новых слов на главу; растущая кривая = арм не помнит решений):") + for a in DRIFT_ARMS: vocab, new = set(), [] - for t in ts: - w = set(re.findall(r"[а-яё]{4,}", t.lower())) + for c in have[a]: + w = set(re.findall(r"[а-яё]{4,}", text(a, c["uid"]).lower())) new.append(len(w - vocab)) vocab |= w - print(f"{arm:6s}{ty:>10d}/{len(ts)}{st.mean(new):>21.0f}{sum(len(t) for t in ts):>9d}") - print("\n⚠ полные метрики консистентности термина и шва — после закупки, здесь только то,") - print(" что считается из одного текста; терминные требуют выравнивания zh↔ru по главам.") + half = len(new) // 2 + print(f" {a:5s} первая половина {st.mean(new[:half]):.0f}/гл · " + f"вторая {st.mean(new[half:]):.0f}/гл · всего уникальных {len(vocab)}") if __name__ == "__main__":