#!/usr/bin/env python3 """РЕВЬЮ ИСПОЛНЕНИЕМ для харнесса эксп-21 (мандат самопроверки, D39.46/промт §Мандат). Зачем. Полигон регулярно ошибается, и ошибка рига неотличима от находки, пока её не поймали отдельным кодом (D37: заявленные «0 ошибок» = 36 ошибок; эксп-20: FEWSHOT утёк в боевой арм и был пойман только регрессией на длину промпта). Здесь пинится всё, на чём стоят выводы Ф0.4, включая дефекты, найденные в САМОЙ ЗЕМЛЕ, — иначе следующий ре-ран молча получит другие числа. Проверки, которые НЕ требуют весов QE, идут всегда. Проверки MetricX идут только если модель уже скачана (`--qe`), потому что 4.6 ГБ весов не должны быть условием прогона детектора слов. Запуск: eval/.venv/bin/python eval/role_topology/selfcheck.py ~/.venvs/tm-qe/bin/python eval/role_topology/selfcheck.py --qe """ from __future__ import annotations import json import sys from pathlib import Path REPO = Path("/home/ubuntu/projects/textmachine") sys.path.insert(0, str(REPO / "eval" / "role_topology")) sys.path.insert(0, str(REPO / "eval" / "bank_arbitration")) sys.path.insert(0, str(REPO / "eval" / "editor_contract")) FAILS = 0 def ck(name: str, ok: bool, got: str = "") -> None: global FAILS if not ok: FAILS += 1 print(f"[{'OK ' if ok else 'ПРОВАЛ'}] {name}" + ("" if ok else f" — {got}")) def check_word_detector() -> None: import detect_word as D # noqa: PLC0415 bank = D.load_bank() ck("С1: вайтлист банка непуст и несёт канон", len(bank) > 100 and "юаня" in bank, f"{len(bank)} форм") # Сигналы поодиночке. Ловушка этого детектора — слой, который «улучшает» precision, гася # настоящие дефекты; поэтому каждый слой проверяется И на пропуск, И на срабатывание. ck("С0: обычное русское слово знакомо словарю", D.known("сокровище")) ck("С0: выдуманное слово словарю незнакомо", not D.known("дерзновяжной")) ck("С2: реальная транскрипция опознана как транскрипция", D.translit_shape("фанчжэнь")) ck("С2: склонённая транскрипция опознана", D.translit_shape("цунях")) ck("С2: выдуманное слово НЕ опознано как транскрипция", not D.translit_shape("дерзновяжной")) ck("С3: продуктивное сложение разбирается", D.decomposes("высокорангового")) ck("С3: суффиксальная деривация разбирается", D.decomposes("женьшеневого")) ck("С3 (регрессия снятой редакции): «силённый» НЕ разбирается", not D.decomposes("силённый"), "вернулся жадный цикл «известное начало + любой хвост», гасивший 6 дефектов из 10") # Выделенная валидация. Пинится ЗНАЧЕНИЕ, а не факт запуска: если посадки перестанут ловиться, # это регрессия детектора, и она обязана быть громкой. import editor_wire_probe as WP # noqa: PLC0415 plants = [w for rs in WP.DEFECTS.values() for cls, old, new, _, _ in rs if cls == "k1" for w in new.split() if w not in old.split()] hit = sum(1 for w in plants if D.verdict(w, bank)[0]) ck(f"holdout: посадки k1 пробы 18 ловятся {hit}/{len(plants)}", (hit, len(plants)) == (6, 6), f"{hit}/{len(plants)}") rows = [json.loads(l) for l in D.LABELS.read_text(encoding="utf-8").splitlines() if l.strip()] s = D.score(rows, 3, bank) ck("k3: полный детектор даёт tp=7 fp=2 fn=3", (s["tp"], s["fp"], s["fn"]) == (7, 2, 3), f"tp={s['tp']} fp={s['fp']} fn={s['fn']}") ck("k3: оба выживших ложных срабатывания — известная пара U+0301", sorted(s["fp_words"]) == ["льшим", "льшую"], str(sorted(s["fp_words"]))) # ⚠ ДЕФЕКТЫ ЗЕМЛИ, найденные при проверке посылки. Пинятся здесь, чтобы отчёт не пересчитал # recall по номинальным 10 позитивам, когда часть из них не того класса и не находится. corp = D.LABELS.parent.parent / "raw" / "corpus.jsonl" big = "\n".join((json.loads(l).get("draft") or "") + "\n" + (json.loads(l).get("final") or "") for l in corp.read_text(encoding="utf-8").splitlines() if l.strip()) ck("земля: «вперди» и «силённый» ОТСУТСТВУЮТ в corpus.jsonl (дефект набора пакета-6)", "вперди" not in big and "силённый" not in big, "они появились — набор пере-собран, числа recall пере-считать") ck("земля: «ной» — валидное русское слово, а не выдуманное (класс метки неверен)", D.known("ной")) def check_alignment() -> None: import align as Q # noqa: PLC0415 zh = ["天很黑。", "他走了。", "风停了,雨也停了。"] ru = ["Было темно.", "Он ушёл.", "Ветер стих.", "Дождь тоже прекратился."] pairs = Q.align(zh, ru) ck("выравнивание: монотонно и без потерь по zh", [i for a, _ in pairs for i in a] == list(range(len(zh))), str([a for a, _ in pairs])) ck("выравнивание: монотонно и без потерь по ru", [i for _, b in pairs for i in b] == list(range(len(ru))), str([b for _, b in pairs])) ck("выравнивание: 1→2 распознано на последней паре", any(len(a) == 1 and len(b) == 2 for a, b in pairs), str(pairs)) ck("разбиение zh: терминаторы не съедены", len(Q.split_zh("甲。乙!丙?")) == 3, str(Q.split_zh("甲。乙!丙?"))) ck("разбиение ru: аббревиатуры не рвут строку по строчной букве", len(Q.split_ru("Он сказал. она молчала.")) == 1, str(Q.split_ru("Он сказал. она молчала."))) def check_qe() -> None: from qe_bench import QE # noqa: PLC0415 qe = QE() good = qe.score("他没有来。", "Он не пришёл.") bad = qe.score("他没有来。", "Он пришёл.") # САМАЯ ОПАСНАЯ ОШИБКА этого детектора — перепутанная ориентация шкалы: он тогда флагает # ровно здоровые сегменты, и это выглядит как работающий детектор. ck(f"QE: шкала перевёрнута (хуже = больше); {good:.2f} < {bad:.2f}", bad > good, f"верный {good:.3f}, инверсия {bad:.3f}") ck("QE: детерминизм повтора", abs(qe.score("他没有来。", "Он не пришёл.") - good) < 1e-4) ck("QE: пустой кандидат штрафуется", qe.score("他没有来。", "") > good) def main() -> None: check_word_detector() check_alignment() if "--qe" in sys.argv: check_qe() else: print("[ПРОП ] проверки QE пропущены (запустить ~/.venvs/tm-qe/bin/python … --qe)") print(f"\n{'ХАРНЕСС ЧИСТ' if not FAILS else f'ПРОВАЛОВ: {FAILS}'}") sys.exit(1 if FAILS else 0) if __name__ == "__main__": main()