Accept D39.51 measurements as D39.52: format example fixes the language slip and joins the build, P1 consistency fails to separate

This commit is contained in:
Claude (backend session) 2026-07-26 22:05:20 +03:00
parent 7692457e9a
commit 21e7099133
4 changed files with 306 additions and 2 deletions

File diff suppressed because one or more lines are too long

View file

@ -1158,3 +1158,7 @@ API-529-долг закрыт: 8-осевой refute-by-default воркфлоу
## D39.51 — Дизайн «дисциплина банкноты + дыра языка ответа» РАТИФИЦИРОВАН, стройка санкционирована (владелец: «идём», 26.07): языковой экран на ВЫХОДЕ + счётчик OffLanguage + фильтр свёртки банкноты + нормализация кавычек в свёртке (Q1) + агрегация из чекпойнтов (A1) + kwic_width в пар-данные (файл zh-ru НЕ шипуем) + doc-хвост. Судья ОТЛОЖЕН; мини-прогон D39.40 — В ХОЛОДНОЙ конфигурации; allow_short — на нём (26.07, оркестратор №8). ✅ ## D39.51 — Дизайн «дисциплина банкноты + дыра языка ответа» РАТИФИЦИРОВАН, стройка санкционирована (владелец: «идём», 26.07): языковой экран на ВЫХОДЕ + счётчик OffLanguage + фильтр свёртки банкноты + нормализация кавычек в свёртке (Q1) + агрегация из чекпойнтов (A1) + kwic_width в пар-данные (файл zh-ru НЕ шипуем) + doc-хвост. Судья ОТЛОЖЕН; мини-прогон D39.40 — В ХОЛОДНОЙ конфигурации; allow_short — на нём (26.07, оркестратор №8). ✅
**Отчёт:** `docs/archive/reports/PACK20_BANKNOTE_DESIGN_2026-07-26.md` (ревью-шапка). Принципиальные решения: (1) **гарантия языка — на выходе, не на входе** (предикат offLanguage по `*unicode.RangeTable` через конфиг гейта `target_script`, обязателен при enabled, вне снапшота, $0; направление отказа объявлено: отвергнутый законный латинизм дешевле англо-канона); (2) **кавычки — в свёртке (Q1), не в парсере/NormalizeSourceKey** (Q2/Q3 платят волной за косметику; Q1 $0 и ретроактивен; NormVersion не двигается — тест-страж); (3) **агрегация банкноты — производное из чекпойнтов (A1), без новых таблиц**; инвариант «прогон без новых черновиков даёт байт-идентичную карту» — пином; предложения отвергнутых попыток входят (решение: предложение = улика, не канон); (4) **судья-с-декоем НЕ строится** (≈$0.025/книга — цена не аргумент; его положительное решение = решение о переводе за владельца, против D39.47; популяция неизвестна до холодного старта; сначала P1); (5) **три расхождения сессии с записками оркестратора приняты** (allow_short = базовая версия = перепокупка книги → на холодный прогон; kwic-файл zh-ru не шипуем; судья отложен). **Замеры полигону:** пример формата в промпте (≈$0.009, критерий до прогона: 0 не-целевых строк ×3 против 22/40) · P1 consistency ($0: Σchunks-предложивших / chunks-содержащих; порог засчитывается при ≥80% термов и ≥60% мусора; арм-без-фактора = голая частота обязана провалиться; положительный исход = право на ПОРЯДОК, не на выброс строк) · приёмка предиката на живом сырье 147 вызовов (живо, подтверждено: ровно 27 строк §3.4 и ни одной сверх). **Мини-прогон D39.40 = холодная конфигурация** (пустой сид, банкнота+терминолог ON, --verify-bank, ОТДЕЛЬНАЯ БД) — пять ответов за ≤$0.23: банкнота без цензуры · совместный recall (замер 1 состоится) · язык при пустом якоре (счётчик OffLanguage как прибор) · побочки allow_short · второй сэмпл для агрегации. Приёмка стройки: мутации M-Я1/M-Я2 + регресс 16 живых строк + пин общности (латинская цель переворачивает вердикты) + пин инварианта агрегации + сырьё-приёмка. **Отчёт:** `docs/archive/reports/PACK20_BANKNOTE_DESIGN_2026-07-26.md` (ревью-шапка). Принципиальные решения: (1) **гарантия языка — на выходе, не на входе** (предикат offLanguage по `*unicode.RangeTable` через конфиг гейта `target_script`, обязателен при enabled, вне снапшота, $0; направление отказа объявлено: отвергнутый законный латинизм дешевле англо-канона); (2) **кавычки — в свёртке (Q1), не в парсере/NormalizeSourceKey** (Q2/Q3 платят волной за косметику; Q1 $0 и ретроактивен; NormVersion не двигается — тест-страж); (3) **агрегация банкноты — производное из чекпойнтов (A1), без новых таблиц**; инвариант «прогон без новых черновиков даёт байт-идентичную карту» — пином; предложения отвергнутых попыток входят (решение: предложение = улика, не канон); (4) **судья-с-декоем НЕ строится** (≈$0.025/книга — цена не аргумент; его положительное решение = решение о переводе за владельца, против D39.47; популяция неизвестна до холодного старта; сначала P1); (5) **три расхождения сессии с записками оркестратора приняты** (allow_short = базовая версия = перепокупка книги → на холодный прогон; kwic-файл zh-ru не шипуем; судья отложен). **Замеры полигону:** пример формата в промпте (≈$0.009, критерий до прогона: 0 не-целевых строк ×3 против 22/40) · P1 consistency ($0: Σchunks-предложивших / chunks-содержащих; порог засчитывается при ≥80% термов и ≥60% мусора; арм-без-фактора = голая частота обязана провалиться; положительный исход = право на ПОРЯДОК, не на выброс строк) · приёмка предиката на живом сырье 147 вызовов (живо, подтверждено: ровно 27 строк §3.4 и ни одной сверх). **Мини-прогон D39.40 = холодная конфигурация** (пустой сид, банкнота+терминолог ON, --verify-bank, ОТДЕЛЬНАЯ БД) — пять ответов за ≤$0.23: банкнота без цензуры · совместный recall (замер 1 состоится) · язык при пустом якоре (счётчик OffLanguage как прибор) · побочки allow_short · второй сэмпл для агрегации. Приёмка стройки: мутации M-Я1/M-Я2 + регресс 16 живых строк + пин общности (латинская цель переворачивает вердикты) + пин инварианта агрегации + сырьё-приёмка.
## D39.52 — Замеры D39.51 закрыты ($0.00960; пакет-8 итого $0.12273): (а) строка примера формата ЧИНИТ срыв языка (22/120 → 0/120) — ИДЁТ В ПРОМПТ РОЛИ данными вместе со стройкой пп.17; (б) P1 consistency НЕ разделяет (вырожденность 0.740, критерий провален, частота — ноль) — права на порядок подачи нет, пере-замер на холодном прогоне $0 (26.07, оркестратор №8). ✅
Приёмка воспроизведением из сырья: (а) 0/120 не-целевых строк в арме с примером — независимый пересчёт; фактор один (дифф запросов — только три строки примера; пример-терм 师父 не входит ни в выборку, ни в якорь, ни в сид); мощность 0.104 названа до прогона, удвоение повторов не берём (страховка = экран + OffLanguage живьём). (б) ре-ран скорера: 54/73 вырождено, критерий ≥80/≥60 не выполнен нигде, частотный арм — независимое подтверждение §5.1 на другом наборе и другой разметке (три слепых модельных разметчика, согласие 0.9730.986; оговорка «разметчики — модели» названа). **Хвост в пак-21: утечка алфавита** — «甲等 → категория A» (7 строк): языковой предикат пропускает (кириллица есть), подписанные dst латиницы не содержат вовсе; кандидаты — банк-линт латиницы в dst или строгая форма предиката (§2.2-Б дизайна). **Вопросы холодного мини-прогона пополнены:** + пере-замер P1 на популяции без цензуры ($0 из чекпойнтов). Стройка бэкенда: пп.17 §8 + п.8 «строка примера в промпте роли» — санкционирована, ждёт релея.

View file

@ -784,3 +784,189 @@ Precision 0/10. **Оговорка, обязательная:** я подста
ответа без якоря (§3.4) — на en→ru он опаснее всего; и `kwic_width` как пар-данные (§6.3). ответа без якоря (§3.4) — на en→ru он опаснее всего; и `kwic_width` как пар-данные (§6.3).
4. **Чего второй книгой не купить:** цену расширения эмиссии. Она упирается в холодный старт с 4. **Чего второй книгой не купить:** цену расширения эмиссии. Она упирается в холодный старт с
пустым банком на ЛЮБОЙ паре, и это по-прежнему единственная неизмеренная цифра. пустым банком на ЛЮБОЙ паре, и это по-прежнему единственная неизмеренная цифра.
---
## §7 Добор по D39.51: два замера. ПРЕ-РЕГИСТРАЦИЯ (написана ДО трат и ДО счёта)
Санкция D39.51 (26.07): (а) пример формата в промпте роли — арм «3×40 без якоря + строка примера на
целевом письме», 3 повтора, ≈$0.009; (б) P1 consistency — $0 из сохранённых артефактов.
### §7.1 Замер (а): помогает ли строка примера удержать язык ответа
**Фактор ровно один, проверено диффом, а не глазом.** Вариант промпта собран копией
`backend/prompts/zh-ru/terminologist.md` в скретчпад (репозиторий не тронут); единственное отличие —
три строки после спецификации формата:
```
Пример строки ответа:
师父 наставник
```
Батчи, ключи и user-часть запроса **побайтно те же**, что у контрольного арма `gNOANCHOR`
(проверено сравнением построенных запросов). Пример-терм `师父` не входит ни в выборку, ни в якорь,
ни в сид и **не встречается в срезе ни разу** — подсказать эталон он не может по построению.
**Арм без фактора** — существующий `gNOANCHOR` (3×40, без якоря, без примера, 3 повтора, 120 строк
ответа, из них 22 не на целевом письме).
**Предикат «не-целевая строка»** (тот же, которым отвечал бэкендерам): в принятом парсером ответе нет
ни одной руны кириллицы. Смешанные строки (кириллица + латиница) считаются отдельно и в критерий не
входят — предикат их по построению пропускает.
**Критерий, названный ДО прогона:** арм с примером проходит, если даёт **0 не-целевых строк** из 120.
Любая не-целевая строка = фактор не сработал.
**Мощность — называю ДО прогона, потому что после будет поздно.** Срыв в контроле случался не в
каждом прогоне: 1 прогон из 3, **2 батча из 9**, 22 строки из 120. Если считать батч независимой
попыткой, то при НУЛЕВОМ эффекте вероятность получить 0 срывов на 9 батчах равна (7/9)⁹ = **0.104**.
То есть даже чистый проход — это «согласуется с починкой» с одним шансом из десяти на совпадение, а
не доказательство. Провал же (хоть одна не-целевая строка) — сильная улика против фактора.
Усиление до 6 повторов уронило бы ложный проход до ~0.011 и стоило бы ещё ≈$0.009; санкция названа
на 3 повтора, поэтому гоню 3 и цифру ложного прохода публикую рядом с результатом.
### §7.2 Замер (б): P1 consistency как разделитель термов и мусора
**Метрика.** Для каждой из 74 поверхностей банкноты: Σ единиц, где чтец объявил строку термином,
делить на Σ единиц, где строка физически встречается.
**Отклонение от формулировки санкции, названное заранее.** Санкция говорит «чанков». Тексты боевых
чанков прогона не сохранены (в БД лежат хеши и вердикты, не исходник чанка), поэтому единицей взята
**ГЛАВА** — атрибуция главы у чекпойнта банкноты есть точная. Цена отклонения измерена, а не
оценена: доля поверхностей, встречающихся в ≤1 единице, составляет 0.743 по главам и 0.730 по
чанкам моей нарезки — то есть более мелкая единица метрику не спасает, а вырожденность есть
свойство десятиглавного среза, не единицы.
**Разметка.** 74 поверхности размечены на TERM/JUNK **тремя независимыми разметчиками вслепую**:
им дана строка исходника, предложенный черновиком перевод и до двух контекстов — и НЕ даны ни
частота, ни главы, ни сама метрика (иначе метка коррелировала бы с арм-без-фактора). Метка —
большинство из трёх; согласие публикуется; расхождения решаю сам по контекстам и помечаю.
Это прямой ответ на слабость, записанную мной же в §2.5 как непокрытую («разметчик один»).
**Критерий, названный ДО счёта (из санкции):** P1 засчитывается как разделитель, если существует
порог, при котором выше него оказывается **≥80% TERM**, а ниже — **≥60% JUNK**.
**Арм без фактора:** голая частота на том же наборе и том же критерии. Она **обязана провалиться**
иначе P1 не несёт информации сверх частоты, и вся конструкция беспредметна (§5.1 уже показал, что
частота термы и не-термы не разделяет; здесь это проверяется на ДРУГОМ наборе и другой разметке).
**Заранее названный стоп.** Если вырожденных поверхностей (встречаются в ≤1 главе, поэтому P1 = 1.0
по построению) окажется больше половины набора, критерий §7.2 объявляется **неприменимым на этих
артефактах**: на такой популяции «≥80% TERM выше порога» достигается тривиально и ничего не значит.
В этом случае пишется, чего стоит измерить P1 по-настоящему, а цифра не подгоняется.
### §7.3 Результат (а): пример формата язык УДЕРЖИВАЕТ — критерий выполнен
Факт **$0.00960** (смета ≤$0.0232, названная до трат), 9 вызовов, 120 принятых строк ответа.
| Арм | Строк | НЕ на целевом письме | Смешанных (кир.+лат.) |
|---|---|---|---|
| 3×40 без якоря, **без примера** (контроль) | 120 | **22** | 3 |
| 3×40 без якоря, **со строкой примера** | 120 | **0** | 7 |
**Критерий §7.1 (0 не-целевых строк) ВЫПОЛНЕН.** Три строки промпта — `Пример строки ответа: 师父
наставник` — сняли полный срыв в английский там, где его не удержал ни якорь (его нет), ни объявление
`{{target_lang}}`, ни весь русскоязычный промпт.
**Мощность, названная ДО прогона, и она же ограничение вывода.** Срыв в контроле происходил не в
каждом прогоне: 2 батча из 9. Значит при НУЛЕВОМ эффекте шанс получить 0 срывов на 9 батчах равен
(7/9)⁹ = **0.104**. Читать результат следует как «согласуется с починкой, один шанс из десяти на
совпадение», а не как доказательство. Удвоение повторов уронило бы эту цифру до ~0.011 и стоило бы
ещё ≈$0.010 — решение не моё, но цифра названа.
### §7.4 Побочный результат (а): пример НЕ чинит утечку алфавита, и экран её не поймает
Смешанных строк стало не меньше, а больше (7 против 3), и все они — одно и то же место:
| Терм | Подпись владельца | Арм с примером | Контроль |
|---|---|---|---|
| `甲等` | «класс **А**» (кириллица) | «категория **A**» (латиница) | «Ранг **A**» |
| `乙等` | «класс **Б**» | «Класс **B**» · «уровень **B**» | «Категория **B**» |
| `丙等` | «класс **В**» | «Класс **C**» · «уровень **C**» | «Категория **C**» |
Сравнение 7 против 3 **причинным считать нельзя**: в контроле те же самые термы в прогоне r2 уехали
целиком в английский («Grade C», «Grade B») и попали в колонку «не на целевом письме», а не
«смешанных». То есть поведение одно и то же в обоих армах — модель берёт буквенные обозначения из
ЛАТИНСКОГО алфавита, — а пример лишь перевёл окружающее слово на русский.
Значение для стройки: **предикат «в ответе есть руны целевого письма» пропускает все семь.**
«категория A» с латинской `A` от подписанного «класс А» с кириллической `А` отличается байтом и не
сойдётся с каноном никогда, а языковой экран промолчит. Ни один подписанный dst сида (53 строки)
латинских букв не содержит вовсе — то есть эталон здесь однозначен, и правило «в целевой строке
латиница допустима только внутри латинского имени собственного» проверяемо. Это не возражение против
экрана: это второй дефект, который экран по построению не покрывает, и теперь он измерен дважды.
### §7.5 Результат (б): P1 consistency НЕ разделяет термы и мусор
**Разметка — три независимых слепых разметчика**, попарное согласие **0.973 · 0.986 · 0.986**,
единогласно 72 из 74; расхождения ровно два (`第三蛊`, `镇族蛊虫`), взято большинство. Метка:
**37 TERM · 37 JUNK** — набор сбалансирован сам собой, подгонки не потребовалось.
**Заранее названный стоп сработал.** Вырожденных поверхностей (встречаются в ≤1 главе среза, поэтому
P1 = 1.0 по построению) — **54 из 73 = 0.740**, что больше половины. И вырожденность бесполезна не
«в среднем», а адресно: среди этих 54 ровно **27 TERM и 27 JUNK**. Обе метки получают одинаковый
максимум, разделять там нечем.
| Набор | Метрика | Лучший порог | TERM выше | JUNK ниже | Критерий ≥0.80/≥0.60 |
|---|---|---|---|---|---|
| весь (73) | **P1** | — | 1.000 | 0.000 | **НЕ ВЫПОЛНЕН** |
| весь (73) | частота (арм-без-фактора) | >4 | 0.189 | 0.861 | НЕ ВЫПОЛНЕН |
| невырожденные (19) | **P1** | >0.5 | 0.200 | 1.000 | **НЕ ВЫПОЛНЕН** |
| невырожденные (19) | частота (арм-без-фактора) | >7 | 0.300 | 0.778 | НЕ ВЫПОЛНЕН |
Порог — не единственный способ прочитать метрику, поэтому считаю и площадь под кривой (вероятность,
что случайный TERM получит балл выше случайного JUNK; 0.5 = ноль информации):
| | Весь набор | Невырожденные (19) |
|---|---|---|
| **P1 consistency** | **0.438** | 0.583 |
| Частота (арм-без-фактора) | **0.500** | 0.433 |
**P1 на полном наборе ниже случайного угадывания.** Арм-без-фактора отработал ровно как обязан был:
голая частота даёт 0.500 — ноль информации, независимое подтверждение §5.1 на ДРУГОМ наборе и ДРУГОЙ
разметке. На 19 невырожденных P1 даёт 0.583 — формально выше монетки, фактически 10 против 9
объектов, и лучшее, чего метрика достигает, это 2 TERM из 10 выше порога.
**Вердикт: положительного P1 нет.** Права на порядок подачи метрика не заработала. Внутри среза
видно и почему: два верхних места по P1 действительно заняты термами (`灵泉` 1.00, `学堂家老` 0.75),
но на самом дне рядом с `无` (0.10, JUNK) и `少年` (0.14, JUNK) стоит `魔道巨擘` (0.00, TERM), а вся
середина 0.50 — ровная смесь.
**Отклонение от формулировки санкции, названное в §7.2 и подтверждённое замером.** Единица — глава,
а не чанк: тексты боевых чанков не сохранены. Цена отклонения измерена: доля вырожденных 0.740 по
главам против 0.730 по чанкам моей нарезки — более мелкая единица метрику не спасает.
**Чего стоит измерить P1 по-настоящему:** данных банкноты существует только на 10 глав, а метрика
требует, чтобы поверхность встречалась во МНОГИХ единицах. Нужен прогон черновой волны с включённой
банкнотой на объёме, где типичная поверхность попадает в 510 единиц, — то есть та же полная книга,
что и для холодного старта. Отдельного замера P1 при этом не потребуется: он считается $0 из
чекпойнтов того же прогона.
### §7.6 Деньги добора и критик полноты
| Замер | Вызовов | $ |
|---|---|---|
| (а) арм с примером формата | 9 | **0.00960** |
| (б) P1 consistency | 0 | **0** |
| **Добор итого** | 9 | **0.00960** |
| **Пакет-8 нарастающим итогом** | 156 | **$0.12273** |
| Проверка критика | Итог |
|---|---|
| Фактор в (а) ровно один | доказано диффом собранных запросов: ключи и user-часть побайтно те же, отличие — три строки |
| Пример не подсказывает эталон | `师父` нет ни в выборке, ни в каноне, ни в сиде, и не встречается в срезе ни разу |
| Мощность (а) названа ДО прогона | да, 0.104 при нулевом эффекте |
| Разметчик в (б) не один | три независимых, слепых, согласие 0.9730.986 |
| Разметка не видела метрику | да: разметчикам не давали ни частоту, ни главы, ни P1 |
| Арм-без-фактора в (б) провалился, как обязан | да, AUC 0.500 |
| **НЕ покрыто** | (а) одна модель, 3 повтора, одна пара; разметчики (б) — модели, а не люди, и их согласие 0.98 может отражать общий системный вкус, а не истину |
> **Ревью-приписка к §7 (оркестратор №8, 26.07): ОБА ЗАМЕРА ПРИНЯТЫ, D39.52.** Воспроизведено: (а) арм
> с примером — 0 из 120 строк не на целевом письме (независимый пересчёт по raw_example своим кодом);
> (б) ре-ран `p1_consistency.py` на сохранённых артефактах d51 — вырожденность 54/73 = 0.740, критерий
> не выполнен на полном и невырожденном наборах, частотный арм-без-фактора нулевой. Оговорка мощности
> (а) — 0.104 — принята как названная честно; удвоение повторов НЕ берём: гарантия — экран п.1, живой
> монитор — счётчик OffLanguage холодного прогона. Побочная находка «утечка алфавита» (7 строк вида
> «категория A»; предикат п.1 их пропускает, подписанные dst латиницы не содержат) — записана хвостом
> в пак-21 (кандидаты: банк-линт по латинице в dst / строгая форма §2.2-Б).

112
eval/pkg7/p1_consistency.py Normal file
View file

@ -0,0 +1,112 @@
#!/usr/bin/env python3
"""Полигон, пакет-8 (добор D39.51-б): P1 consistency как разделитель термов и мусора. $0.
Метрика: Σ единиц, где чтец объявил строку термином / Σ единиц, где строка встречается. Единица
ГЛАВА (тексты боевых чанков прогона не сохранены; отклонение и его цена названы в §7.2 отчёта).
Арм без фактора голая частота на том же наборе и том же критерии: если частота проходит, P1 не
несёт информации сверх неё и вся конструкция беспредметна.
Критерий из санкции, названный ДО счёта: существует порог, выше которого 80% TERM, а ниже 60%
JUNK. Дополнительно печатается вырожденность (строки, встречающиеся в 1 единице, получают 1.0 по
построению и разделять не могут) при её доле >0.5 критерий объявляется неприменимым.
"""
from __future__ import annotations
import argparse
import json
from pathlib import Path
def best_threshold(scores: dict[str, float], labels: dict[str, str], want_term=0.80, want_junk=0.60):
"""Ищем порог, дающий ≥want_term доли TERM выше и ≥want_junk доли JUNK ниже. Возвращаем лучший
по сумме двух долей и признак, выполнен ли критерий хоть на одном пороге."""
terms = [s for s, l in labels.items() if l == "TERM" and s in scores]
junk = [s for s, l in labels.items() if l == "JUNK" and s in scores]
if not terms or not junk:
return None
cands = sorted({scores[s] for s in scores})
best = None
for t in cands:
# "выше порога" = строго больше или равно; пробуем оба, берём лучший — порог как таковой
# заранее не назывался, назывались только доли.
for mode in (">=", ">"):
hi = (lambda v: v >= t) if mode == ">=" else (lambda v: v > t)
tp = sum(1 for s in terms if hi(scores[s])) / len(terms)
tn = sum(1 for s in junk if not hi(scores[s])) / len(junk)
row = (tp, tn, t, mode, tp >= want_term and tn >= want_junk)
if best is None or (row[0] + row[1]) > (best[0] + best[1]):
best = row
return best
def report(name: str, scores: dict[str, float], labels: dict[str, str]) -> dict:
b = best_threshold(scores, labels)
if b is None:
print(f"{name}: нет обоих классов")
return {}
tp, tn, t, mode, ok = b
n_t = sum(1 for s, l in labels.items() if l == "TERM" and s in scores)
n_j = sum(1 for s, l in labels.items() if l == "JUNK" and s in scores)
print(f"{name}: лучший порог {mode}{t:.3f} → TERM выше {tp:.3f} ({n_t} шт) · JUNK ниже {tn:.3f} ({n_j} шт)"
f" критерий (≥0.80/≥0.60): {'ВЫПОЛНЕН' if ok else 'НЕ ВЫПОЛНЕН'}")
return {"tpr": tp, "tnr": tn, "threshold": t, "mode": mode, "passes": ok, "n_term": n_t, "n_junk": n_j}
def main() -> int:
ap = argparse.ArgumentParser()
ap.add_argument("--surfaces", required=True, type=Path)
ap.add_argument("--labels", required=True, type=Path)
ap.add_argument("--chunks", required=True, type=Path)
ap.add_argument("--out", type=Path)
a = ap.parse_args()
items = json.loads(a.surfaces.read_text(encoding="utf-8"))
labels = json.loads(a.labels.read_text(encoding="utf-8"))["majority"]
rows = [json.loads(l) for l in a.chunks.open(encoding="utf-8") if l.strip()]
by_ch: dict[int, str] = {}
for r in rows:
by_ch[r["chapter"]] = by_ch.get(r["chapter"], "") + "\n" + r["source"]
p1, freq, degen = {}, {}, []
for it in items:
occ = it["occ_chapters"]
prop = [c for c in it["proposed_chapters"] if c in occ]
if not occ:
continue # предложена там, где не встречается — вне определения метрики
p1[it["src"]] = len(prop) / len(occ)
freq[it["src"]] = sum(by_ch[c].count(it["src"]) for c in occ)
if len(occ) <= 1:
degen.append(it["src"])
n = len(p1)
print(f"поверхностей в счёте: {n} TERM={sum(1 for s in p1 if labels.get(s)=='TERM')} "
f"JUNK={sum(1 for s in p1 if labels.get(s)=='JUNK')}")
print(f"ВЫРОЖДЕННЫХ (встречаются в ≤1 главе → P1 = 1.0 по построению): {len(degen)} = {len(degen)/n:.3f}")
dt = sum(1 for s in degen if labels.get(s) == "TERM")
print(f" из них TERM {dt}, JUNK {len(degen)-dt}обе метки получают одинаковый максимум")
print("\n--- весь набор ---")
r_all = report("P1 consistency ", p1, labels)
r_fq = report("частота (арм-без-фактора)", freq, labels)
live = {s: v for s, v in p1.items() if s not in degen}
live_f = {s: v for s, v in freq.items() if s not in degen}
print(f"\n--- только НЕвырожденные ({len(live)} поверхностей) ---")
r_live = report("P1 consistency ", live, labels)
r_lf = report("частота (арм-без-фактора)", live_f, labels)
print("\nневырожденные поверхности (P1 · частота · метка):")
for s in sorted(live, key=lambda s: -live[s]):
print(f" {s}\t{live[s]:.2f}\t{live_f[s]:3d}\t{labels.get(s)}")
if a.out:
a.out.write_text(json.dumps({"p1": p1, "freq": freq, "degenerate": degen,
"all": {"p1": r_all, "freq": r_fq},
"live": {"p1": r_live, "freq": r_lf}},
ensure_ascii=False, indent=1), encoding="utf-8")
return 0
if __name__ == "__main__":
raise SystemExit(main())