397 lines
27 KiB
Python
397 lines
27 KiB
Python
#!/usr/bin/env python3
|
||
# -*- coding: utf-8 -*-
|
||
"""
|
||
bank.py — АУДИТ УДЕРЖАНИЯ БАНКА на движковых прогонах 蛊真人 (zh→ru, 25 глав).
|
||
|
||
Стадия 0 п.1 плана `eval/dovodka/KONSILIUM-30-08.md` §5. Стоимость $0: только артефакты на диске.
|
||
|
||
═══════════════════════════════════════════════════════════════════════════════════════════════
|
||
⚠⚠ ПОПРАВКА КУРСА, НАЙДЕННАЯ ИСПОЛНЕНИЕМ 30.08 — ЧИТАТЬ ПРЕЖДЕ ЧИСЕЛ
|
||
═══════════════════════════════════════════════════════════════════════════════════════════════
|
||
Консилиум назначил аудит прогона `acceptance`, НЕ ЗНАЯ ДВУХ ВЕЩЕЙ:
|
||
|
||
1. ⭐ РЯДОМ ЛЕЖИТ БОЛЕЕ ПОЗДНИЙ И БОЛЕЕ БОЕВОЙ ПРОГОН `rerun` (11.07, `~/books/gu-zhenren/rerun/`).
|
||
Он собран из боевого `pipeline-c1.yaml` post-D33 байт-в-байт: промты `v1-reflow` (не `v0-draft`),
|
||
редактор БИЛИНГВ (D30.1), переведены все 57 чанков (у `acceptance` — 54 из 57). Движковых
|
||
промахов там 27 против 55. ⇒ ОБЪЕКТ АУДИТА — ОБА, и `rerun` важнее.
|
||
⚠ Боевым он всё равно НЕ является: редактор `glm-5`, а не `deepseek-v4-pro`.
|
||
|
||
2. ⛔ ЭТУ РАБОТУ УЖЕ ДЕЛАЛИ В ИЮЛЕ, и результат сильнее плановой заявки. `docs/archive/
|
||
PROGRESS-2026-07-10-13.md:149` (замер B5 D10, независимый скрипт `rerun/d10_consistency.py`):
|
||
presence 91.0% (647/711) · occurrence 95.8% (3072/3207)
|
||
по типам: имена 98.6% · клички 100% · места 96.0% · термины 94.9% · ТИТУЛЫ 78.7%
|
||
промахи: decl_gap 0 · inflection_gap 54 · genuine_absent 10 ⇒ эффективная 98.6%
|
||
⛔ Сами СКРИПТЫ не пережили переезд машины — лежали ВНЕ git (`PROGRESS-2026-07-10-13.md:190`
|
||
перечисляет их поимённо; в каталоге остались только данные). Это ЧЕТВЁРТАЯ тихая поломка
|
||
переезда после путей `~/books`, обнулённых mtime и подчёркиваний в `.env`. Класс один:
|
||
ЧТО ВНЕ GIT, ТО ПЕРЕЕЗД УБИВАЕТ МОЛЧА. Этот файл — в git намеренно.
|
||
⇒ Июльские числа здесь не пересказываются как свои: они ЭТАЛОН СВЕРКИ. Совпадение = две
|
||
независимые реализации через полтора месяца согласились. Расхождение = находка, а не шум.
|
||
|
||
═══════════════════════════════════════════════════════════════════════════════════════════════
|
||
ПРЕ-РЕГИСТРАЦИЯ — ПРАВИЛО РЕШЕНИЯ ЗАПИСАНО ДО ПЕРВОГО ЧИСЛА
|
||
═══════════════════════════════════════════════════════════════════════════════════════════════
|
||
ЧТО МЕРЯЕМ. Вопрос 0 в его ТЕРМИННОЙ половине: удерживает ли банк канонные формы через 25 глав.
|
||
Голосовая половина им НЕ закрывается ПО ПОСТРОЕНИЮ — колонки `first_person`/`speech` пусты 0/49
|
||
(Д32.16, консилиум §5 п.1).
|
||
|
||
ЕДИНИЦА — пара (термин, чанк), в которой ключ термина СРАБОТАЛ в китайском оригинале чанка. Ровно
|
||
это условие включает инжекцию записи в промт, поэтому только такие пары и подсудны.
|
||
|
||
ДИСПОЗИЦИИ (`backend/internal/membank/memory.go:121,650-663` — прочитано, не пересказано):
|
||
approved → CONFIRMED (авторитетно) · auto/draft → AMBIGUOUS (кандидат, обязательности нет).
|
||
Блок ограничений редактора — CONFIRMED-only (`memory.go:745-750`).
|
||
⇒ ОСНОВНОЙ ЭНДПОЙНТ — CONFIRMED. С AMBIGUOUS-записи спрашивать соблюдения нельзя: движок её
|
||
таковой не объявлял, и `mempostcheck.go:52-64` прямо зовёт наказание за неё инверсией контракта.
|
||
|
||
ПРИНЯТОЕ МНОЖЕСТВО ФОРМ = базовый `dst` ∪ `decl.forms`. Это РАТИФИЦИРОВАННОЕ D24.4, а не мой
|
||
выбор: проверка одних косвенных давала 37 ложных промахов из 55 на этих же данных (nominative_gap).
|
||
|
||
ТРИ СТРУКТУРНЫХ ИСКЛЮЧЕНИЯ — каждое установлено кодом или данными, ни одно не «для красоты»:
|
||
1. ЧАНКИ БЕЗ ПЕРЕВОДА. Гейт покрытия пометил их флагом и не переводил вовсе; в параллельном
|
||
файле у них нет секции `[ПЕРЕВОД]`. Отсутствие термина там — дефект ПОКРЫТИЯ, не банка.
|
||
⚠ Печатается отдельной строкой, а не заминается.
|
||
2. ОДНОСИМВОЛЬНЫЕ ХАНЬСКИЕ КЛЮЧИ без `allow_short` (蛊, 转). `memory.go:319` УДАЛЯЕТ такой ключ
|
||
из автомата — записи не инжектировались НИКОГДА, спрашивать за них нельзя. (В живом коде есть
|
||
ещё и демот таких промахов, `mempostcheck.go:122`, но он от D39.39 — ПОЗЖЕ этих прогонов.)
|
||
3. STICKY (scene-inertia, A5) — ключа в чанке нет по построению, `mempostcheck.go:31-33`
|
||
исключает их сознательно. Тоже печатается числом.
|
||
|
||
ПРАВИЛО РЕШЕНИЯ (порог консилиума §5 п.1; двусмысленность его второй половины разрешена ЗДЕСЬ,
|
||
до чисел, и разрешение объявлено явно):
|
||
A. взвешенная доля промахов CONFIRMED > 2% ⇒ «банк держит» ложна в сильной форме;
|
||
B. «любая сущность топ-50» двусмысленна — записей всего 49, и буквальное чтение убивает гипотезу
|
||
одной случайной строкой. Читаю как проверку на СИСТЕМАТИЧЕСКИЙ провал ядра: термин из ТОП-10
|
||
по весу ключа с долей промахнутых чанков >= 20%.
|
||
⚠ Обе печатаются всегда и не пересматриваются после просмотра чисел.
|
||
|
||
КЛАССИФИКАЦИЯ ПРОМАХА (июльская номенклатура, чтобы числа были сравнимы):
|
||
decl_gap — принятая форма есть, но её не увидели бы БЕЗ union D24.4 (контроль: должен быть 0);
|
||
inflection_gap — основа dst присутствует в форме, которой в сиде НЕТ (обычно мн.ч.) ⇒ читатель
|
||
видит верный термин, это дефект ЗАСЕВА, а не дрейф;
|
||
genuine_absent — основы нет вовсе ⇒ КАНДИДАТ дрейфа, требующий чтения глазами. НЕ дрейф сам по себе.
|
||
⚠ Граница признана честно: определитель основы здесь — ЭВРИСТИКА общего префикса, а не морфология
|
||
(в движке стеммер настоящий). Поэтому `genuine_absent` объявляется КАНДИДАТОМ и подлежит чтению.
|
||
|
||
ДВА РЕЖИМА МАТЧИНГА, потому что эталоны построены по-разному:
|
||
native — longest-match, как автомат движка (古月 внутри 古月漠北 не считается);
|
||
substring — независимая подстрока на термин, как июльский скрипт: он этим и обошёл alias-слепое
|
||
пятно D24.2, поймав 古月→Гуюэ ×2. Числа июля сравнивать нужно с ЭТИМ режимом.
|
||
|
||
ВТОРОЙ КОНТУР — СВЕРКА С ДВИЖКОВЫМ ПОСТ-ЧЕКОМ (`postcheck_gate: false` ⇒ флаггер пишет промахи в
|
||
`retrieval_state`). Расхождение ЛЮБОГО знака — находка:
|
||
движок нашёл, я нет ⇒ ложная тревога гейта (ожидаемо: базы писаны ДО фикса D24.4);
|
||
я нашёл, движок нет ⇒ слепота гейта — то, чего он не смотрит.
|
||
|
||
ЧЕГО ПРИБОР НЕ УМЕЕТ: он не читает смысл — термин в правильной форме не на своём месте он засчитает.
|
||
"""
|
||
|
||
import argparse
|
||
import json
|
||
import re
|
||
import sqlite3
|
||
import sys
|
||
from collections import defaultdict
|
||
from pathlib import Path
|
||
|
||
RUNS = {
|
||
"acceptance": {
|
||
"db": Path.home() / "books/gu-zhenren/acceptance/guzhenren-acc-a.db",
|
||
"parallel": Path.home() / "books/gu-zhenren/acceptance/guzhenren-25ch-parallel.txt",
|
||
"book_id": "guzhenren-acc-a",
|
||
"note": "10.07, промты v0-draft, редактор glm-5 монолингв, 54/57 чанков переведены",
|
||
},
|
||
"rerun": {
|
||
"db": Path.home() / "books/gu-zhenren/rerun/guzhenren-rerun.db",
|
||
"parallel": Path.home() / "books/gu-zhenren/rerun/rerun-parallel.txt",
|
||
"book_id": None, # берётся из базы: имя книги у пере-прогона своё
|
||
"note": "11.07, промты v1-reflow, редактор glm-5 БИЛИНГВ, 57/57 чанков — ближе к боевому",
|
||
},
|
||
}
|
||
|
||
WEIGHTED_MISS_MAX = 0.02
|
||
TOP_N = 10
|
||
CORE_MISS_MAX = 0.20
|
||
# Эвристика основы для КЛАССИФИКАЦИИ промаха (на сам вердикт не влияет — метрики A и B считают
|
||
# промахи, а не классы, поэтому это число ничего подогнать не может).
|
||
# ⚠ 4 знака ЛОМАЮТСЯ НА ЧЕРЕДОВАНИИ СОГЛАСНЫХ: «очистить»[:4] = «очис», а в тексте стоит «очищает» —
|
||
# и верно переведённый термин уезжает в `genuine_absent`, то есть в кандидаты дрейфа. Поймано
|
||
# чтением текста 30.08 на 炼化 → «очистить и подчинить»: все 7 его «промахов» суть верный перевод
|
||
# при ПУСТОМ `decl.forms`. 3 знака чередование переживают; цена — более грубое сито.
|
||
STEM_MIN = 3
|
||
|
||
CHAPTER_RE = re.compile(r"^【 Глава (\d+) 】", re.M)
|
||
# ⚠ Флаг в скобках есть у `acceptance` и ОТСУТСТВУЕТ у `rerun` (`--- чанк 0 ---`). Первая редакция
|
||
# требовала скобок — и на `rerun` разобрала НОЛЬ чанков, после чего прибор напечатал «порог не
|
||
# пробит» и «гипотеза не опровергнута» НА ПУСТОМ ЗНАМЕНАТЕЛЕ. Отсюда же гейт `assert_measured`.
|
||
CHUNK_RE = re.compile(r"^--- чанк (\d+)(?: \[([^\]]*)\])? ---$", re.M)
|
||
HAN_RE = re.compile(r"[一-鿿]")
|
||
|
||
|
||
def load_glossary(con, book_id):
|
||
aliases = defaultdict(list)
|
||
for term_id, alias in con.execute(
|
||
"select term_id, alias from glossary_aliases where book_id=?", (book_id,)
|
||
):
|
||
aliases[term_id].append(alias)
|
||
rows = []
|
||
for (tid, src, dst, typ, status, gender, decl, allow_short) in con.execute(
|
||
"select id, src, dst, type, status, gender, decl, allow_short "
|
||
"from glossary where book_id=? order by id",
|
||
(book_id,),
|
||
):
|
||
forms = []
|
||
if decl:
|
||
try:
|
||
forms = [f for f in json.loads(decl).get("forms", []) if f]
|
||
except json.JSONDecodeError:
|
||
pass
|
||
keys = [src] + aliases[tid]
|
||
# Структурное исключение 2: односимвольный ханьский ключ без allow_short в автомат не попал.
|
||
eligible = [k for k in keys if allow_short or not (len(k) == 1 and HAN_RE.match(k))]
|
||
rows.append(
|
||
{
|
||
"id": tid, "src": src, "dst": dst, "type": typ, "status": status,
|
||
"gender": gender, "keys": eligible, "banned": [k for k in keys if k not in eligible],
|
||
"decl_forms": forms, "accepted": sorted({dst, *forms}, key=len, reverse=True),
|
||
"confirmed": status == "approved",
|
||
"stem": dst[:STEM_MIN] if len(dst) >= STEM_MIN else dst,
|
||
}
|
||
)
|
||
return rows
|
||
|
||
|
||
def split_chunks(text):
|
||
out = []
|
||
chapters = list(CHAPTER_RE.finditer(text))
|
||
for i, cm in enumerate(chapters):
|
||
ch = int(cm.group(1))
|
||
body = text[cm.end(): chapters[i + 1].start() if i + 1 < len(chapters) else len(text)]
|
||
marks = list(CHUNK_RE.finditer(body))
|
||
for j, km in enumerate(marks):
|
||
seg = body[km.end(): marks[j + 1].start() if j + 1 < len(marks) else len(body)]
|
||
translated = "[ПЕРЕВОД]" in seg
|
||
zh, ru = (seg.split("[ПЕРЕВОД]", 1) if translated else (seg, ""))
|
||
out.append((ch, int(km.group(1)), km.group(2) or "", zh.replace("[ОРИГ]", ""), ru,
|
||
translated))
|
||
return out
|
||
|
||
|
||
def assert_measured(what, n, ctx):
|
||
"""ГЕЙТ ПРОТИВ ЗЕЛЕНИ НА ПУСТОТЕ. Прибор, ничего не измеривший, обязан ОТКАЗАТЬ, а не напечатать
|
||
«порог не пробит»: пустой знаменатель читается как успех и ровно так и был прочитан 30.08, когда
|
||
парсер не понял формат `rerun` и вердикт «гипотеза не опровергнута» встал на нуле наблюдений."""
|
||
if n <= 0:
|
||
print(f"\n⛔⛔ ПРИБОР ОТКАЗЫВАЕТ: {what} = 0 ({ctx}). Вердикт НЕ выносится: "
|
||
"нулевой знаменатель — это не «в пределах», это отсутствие замера.")
|
||
sys.exit(2)
|
||
|
||
|
||
def count_keys(zh, terms, mode):
|
||
"""mode=native — longest-match (автомат движка); mode=substring — независимо на термин (июль)."""
|
||
hits = {}
|
||
if mode == "substring":
|
||
for t in terms:
|
||
n = sum(zh.count(k) for k in t["keys"])
|
||
if n:
|
||
hits[t["id"]] = n
|
||
return hits
|
||
occupied = bytearray(len(zh))
|
||
ordered = sorted(
|
||
(t for t in terms if t["keys"]),
|
||
key=lambda t: max(len(k) for k in t["keys"]), reverse=True,
|
||
)
|
||
for t in ordered:
|
||
n = 0
|
||
for key in sorted(t["keys"], key=len, reverse=True):
|
||
start = 0
|
||
while (p := zh.find(key, start)) >= 0:
|
||
if not any(occupied[p:p + len(key)]):
|
||
occupied[p:p + len(key)] = b"\x01" * len(key)
|
||
n += 1
|
||
start = p + 1
|
||
if n:
|
||
hits[t["id"]] = n
|
||
return hits
|
||
|
||
|
||
def engine_state(con, book_id):
|
||
st = {}
|
||
for ch, idx, n_miss, detail, sticky, spoil, evict, exact in con.execute(
|
||
"select chapter, chunk_idx, n_postcheck_miss, postcheck_detail, n_sticky, "
|
||
"n_spoiler_blocked, n_evicted, n_exact_hits from retrieval_state where book_id=?",
|
||
(book_id,),
|
||
):
|
||
items = json.loads(detail) if detail else []
|
||
st[(ch, idx)] = {
|
||
"confirmed_miss": {d["src"] for d in items if d.get("disp") == "confirmed"},
|
||
"ambiguous_miss": {d["src"] for d in items if d.get("disp") == "ambiguous"},
|
||
"n_miss": n_miss, "sticky": sticky, "spoiler": spoil,
|
||
"evicted": evict, "exact": exact,
|
||
}
|
||
return st
|
||
|
||
|
||
def classify(term, ru):
|
||
"""Промах уже установлен: ни одной принятой формы нет. Чем он является?"""
|
||
ru_low = ru.lower()
|
||
if any(f and f.lower() in ru_low for f in term["decl_forms"]):
|
||
return "decl_gap" # контроль: обязан быть 0 при union D24.4
|
||
if term["stem"] and term["stem"].lower() in ru_low:
|
||
return "inflection_gap" # основа есть, формы в сиде нет — дефект ЗАСЕВА
|
||
return "genuine_absent" # КАНДИДАТ дрейфа, читать глазами
|
||
|
||
|
||
def audit(run, mode, verbose):
|
||
cfg = RUNS[run]
|
||
con = sqlite3.connect(f"file:{cfg['db']}?mode=ro", uri=True)
|
||
book_id = cfg["book_id"] or con.execute("select book_id from glossary limit 1").fetchone()[0]
|
||
terms = load_glossary(con, book_id)
|
||
by_id = {t["id"]: t for t in terms}
|
||
chunks = split_chunks(cfg["parallel"].read_text(encoding="utf-8"))
|
||
engine = engine_state(con, book_id)
|
||
|
||
print("=" * 98)
|
||
print(f"АУДИТ УДЕРЖАНИЯ БАНКА — прогон `{run}` ({book_id}), режим матчинга: {mode}")
|
||
print(f" {cfg['note']}")
|
||
print(" правило решения — в шапке файла, записано ДО чисел")
|
||
print("=" * 98)
|
||
banned = sorted({k for t in terms for k in t["banned"]})
|
||
print(f"записей {len(terms)}: confirmed {sum(t['confirmed'] for t in terms)}, "
|
||
f"ambiguous {sum(not t['confirmed'] for t in terms)} | "
|
||
f"ключей вне автомата (single-Han ban): {', '.join(banned) or '—'}")
|
||
print(f"чанков {len(chunks)}, из них переведено {sum(c[5] for c in chunks)}; "
|
||
f"строк retrieval_state {len(engine)}")
|
||
assert_measured("разобрано чанков параллельного файла", len(chunks),
|
||
f"формат {cfg['parallel'].name} не распознан")
|
||
assert_measured("переведённых чанков", sum(c[5] for c in chunks), "нечего проверять")
|
||
|
||
fired, fired_w, miss_ch, miss_w = (defaultdict(int) for _ in range(4))
|
||
klass = defaultdict(int)
|
||
kandidaty, rows = [], []
|
||
agree = only_engine = only_mine = 0
|
||
|
||
for ch, idx, flag, zh, ru, translated in chunks:
|
||
if not translated:
|
||
continue # структурное исключение 1
|
||
hits = count_keys(zh, terms, mode)
|
||
ru_low = ru.lower()
|
||
eng = engine.get((ch, idx), {})
|
||
eng_miss_all = eng.get("confirmed_miss", set()) | eng.get("ambiguous_miss", set())
|
||
for tid, n in hits.items():
|
||
t = by_id[tid]
|
||
fired[tid] += 1
|
||
fired_w[tid] += n
|
||
# ⚠ РЕГИСТР: сравнение регистро-НЕчувствительное. Прямое `in` дало ложный промах на
|
||
# 开窍 → «открытие апертуры» (гл.5/ч1): в тексте «Открытие апертуры удалось!» — термин
|
||
# верен, заглавная лишь начинает предложение. Поймано чтением 30.08.
|
||
held = any(f and f.lower() in ru_low for f in t["accepted"])
|
||
if not held:
|
||
miss_ch[tid] += 1
|
||
miss_w[tid] += n
|
||
k = classify(t, ru)
|
||
klass[(k, t["confirmed"])] += 1
|
||
if k == "genuine_absent" and t["confirmed"]:
|
||
kandidaty.append((ch, idx, t["src"], t["dst"], t["type"], n))
|
||
eng_miss = t["src"] in eng_miss_all
|
||
if not held and eng_miss:
|
||
agree += 1
|
||
elif eng_miss and held:
|
||
only_engine += 1
|
||
rows.append(("ЛОЖНАЯ ТРЕВОГА ГЕЙТА", ch, idx, t["src"], t["dst"], t["status"], n))
|
||
elif not held and not eng_miss:
|
||
only_mine += 1
|
||
rows.append(("СЛЕПОТА ГЕЙТА", ch, idx, t["src"], t["dst"], t["status"], n))
|
||
|
||
def block(conf, label):
|
||
ids = [i for i in fired if by_id[i]["confirmed"] == conf]
|
||
c_all, c_miss = sum(fired[i] for i in ids), sum(miss_ch[i] for i in ids)
|
||
w_all, w_miss = sum(fired_w[i] for i in ids), sum(miss_w[i] for i in ids)
|
||
print(f"\n--- {label} ---")
|
||
print(f" пар (термин, чанк) со сработавшим ключом : {c_all}")
|
||
print(f" из них промах (presence-провал) : {c_miss}"
|
||
+ (f" ⇒ presence {1 - c_miss / c_all:.1%}" if c_all else ""))
|
||
print(f" вхождений ключа всего / промахнуто : {w_all} / {w_miss}"
|
||
+ (f" ⇒ occurrence {1 - w_miss / w_all:.1%}" if w_all else ""))
|
||
return (w_miss / w_all) if w_all else 0.0, c_all, c_miss
|
||
|
||
weighted, conf_all, conf_miss = block(True, "CONFIRMED — ОСНОВНОЙ ЭНДПОЙНТ")
|
||
assert_measured("пар CONFIRMED со сработавшим ключом", conf_all,
|
||
"ни один подписанный ключ не сработал — матчер или данные не те")
|
||
block(False, "AMBIGUOUS — справка: обязательности движок не объявлял")
|
||
|
||
print("\n--- классификация промахов (июльская номенклатура, для сверки) ---")
|
||
for k in ("decl_gap", "inflection_gap", "genuine_absent"):
|
||
c, a = klass[(k, True)], klass[(k, False)]
|
||
note = {"decl_gap": "контроль union D24.4 — обязан быть 0",
|
||
"inflection_gap": "дефект ЗАСЕВА (мн.ч.), не дрейф",
|
||
"genuine_absent": "КАНДИДАТ дрейфа — читать глазами"}[k]
|
||
print(f" {k:<16} confirmed {c:>4} ambiguous {a:>4} — {note}")
|
||
eff_num = conf_all - klass[("genuine_absent", True)] - klass[("decl_gap", True)]
|
||
if conf_all:
|
||
print(f" эффективная консистентность CONFIRMED = {eff_num}/{conf_all} = {eff_num / conf_all:.1%}"
|
||
" (читатель видит верный термин: инфлексия вне сида ему не мешает)")
|
||
|
||
print(f"\n--- метрика B: ядро (топ-{TOP_N} по весу ключа среди CONFIRMED) ---")
|
||
core = sorted((i for i in fired if by_id[i]["confirmed"]),
|
||
key=lambda i: fired_w[i], reverse=True)[:TOP_N]
|
||
core_fail = []
|
||
print(f" {'термин':<34}{'вес':>6}{'чанков':>8}{'промах':>8}{'доля':>7}")
|
||
for i in core:
|
||
share = miss_ch[i] / fired[i]
|
||
if share >= CORE_MISS_MAX:
|
||
core_fail.append(by_id[i]["src"])
|
||
print(f" {by_id[i]['src'] + ' → ' + by_id[i]['dst']:<34}{fired_w[i]:>6}{fired[i]:>8}"
|
||
f"{miss_ch[i]:>8}{share:>6.0%}{' ⛔' if share >= CORE_MISS_MAX else ''}")
|
||
|
||
print("\n--- ВТОРОЙ КОНТУР: сверка с движковым пост-чеком ---")
|
||
print(f" согласие : {agree}")
|
||
print(f" ⚠ ложная тревога гейта (форма В ТЕКСТЕ есть): {only_engine}"
|
||
" ← ожидаемо: базы писаны ДО фикса D24.4")
|
||
print(f" ⚠ слепота гейта (промах, гейт молчит) : {only_mine}")
|
||
if verbose and rows:
|
||
print(f" {'класс':<24}{'гл':>4}{'чанк':>6} {'src':<12}{'dst':<30}{'статус':<10}{'вес':>5}")
|
||
for kind, ch, idx, src, dst, status, n in rows:
|
||
print(f" {kind:<24}{ch:>4}{idx:>6} {src:<12}{dst:<30}{status:<10}{n:>5}")
|
||
|
||
if kandidaty:
|
||
print(f"\n--- КАНДИДАТЫ ДРЕЙФА (genuine_absent, confirmed): {len(kandidaty)} — ЧИТАТЬ ГЛАЗАМИ ---")
|
||
print(f" {'гл':>4}{'чанк':>6} {'src':<12}{'dst':<30}{'тип':<10}{'вес':>5}")
|
||
for ch, idx, src, dst, typ, n in kandidaty:
|
||
print(f" {ch:>4}{idx:>6} {src:<12}{dst:<30}{typ:<10}{n:>5}")
|
||
|
||
print("\n--- зоны, которые прибор НЕ судит (печатаются, а не заминаются) ---")
|
||
print(f" чанков без перевода (гейт покрытия) : {sum(1 for c in chunks if not c[5])}")
|
||
print(f" sticky-переносов (инерция сцены, A5) : {sum(e['sticky'] for e in engine.values())}")
|
||
print(f" вытеснено бюджетом токенов : {sum(e['evicted'] for e in engine.values())}")
|
||
print(f" спойлер-блокировок : {sum(e['spoiler'] for e in engine.values())}")
|
||
|
||
print("\n" + "=" * 98)
|
||
a_fail = weighted > WEIGHTED_MISS_MAX
|
||
print(f"МЕТРИКА A: взвешенная доля промахов CONFIRMED = {weighted:.2%} при пороге "
|
||
f"{WEIGHTED_MISS_MAX:.0%} ⇒ {'⛔ ПОРОГ ПРОБИТ' if a_fail else 'в пределах'}")
|
||
print(f"МЕТРИКА B: терминов ядра с долей промахов >= {CORE_MISS_MAX:.0%}: {len(core_fail)}"
|
||
f"{' — ' + ', '.join(core_fail) if core_fail else ''} ⇒ "
|
||
f"{'⛔ ПОРОГ ПРОБИТ' if core_fail else 'в пределах'}")
|
||
print("ВЫВОД (строгий presence): гипотеза «банк держит» в СИЛЬНОЙ форме "
|
||
+ ("⛔ ЛОЖНА" if (a_fail or core_fail) else "не опровергнута"))
|
||
print("⚠ Границы: терминная половина Вопроса 0 · июльский профиль · редактор glm-5, не боевой "
|
||
"deepseek-v4-pro.")
|
||
print("=" * 98)
|
||
|
||
|
||
def main():
|
||
ap = argparse.ArgumentParser(description="аудит удержания банка, $0")
|
||
ap.add_argument("--run", choices=[*RUNS, "both"], default="both")
|
||
ap.add_argument("--match", choices=["native", "substring"], default="native",
|
||
help="native — автомат движка; substring — как независимый июльский скрипт")
|
||
ap.add_argument("--verbose", action="store_true", help="печатать построчную сверку с гейтом")
|
||
a = ap.parse_args()
|
||
for run in ([*RUNS] if a.run == "both" else [a.run]):
|
||
audit(run, a.match, a.verbose)
|
||
print()
|
||
return 0
|
||
|
||
|
||
if __name__ == "__main__":
|
||
sys.exit(main())
|