Freeze the panel-0 pre-registration and its counter result: register the foreign-phase arm E6, build the blind source etalon, and name three defects found in the instrument itself

This commit is contained in:
heaven 2026-08-22 22:27:42 +03:00
parent 1ccd5e4edf
commit bedc39a751
4 changed files with 2167 additions and 7 deletions

View file

@ -3982,3 +3982,351 @@ DeepSeek, отношение от пина не зависит, обе пары,
**Открытый денежный вопрос:** 12 клеток умерли по таймауту и записаны с нулевой ценой; списал ли
вендор токены за оборванное соединение, по диску не определить. Потенциально невидимый кассе
расход $0.10.3.
---
## Д32 — ⭐ ПАНЕЛЬ-0: «АРХИТЕКТУРА × СЛАБЫЙ ЖИЛЕЦ». ПРЕ-РЕГ, ЗАПИСАН ДО СБОРКИ ПАКЕТОВ
> ⚠ **Номер Д31 намеренно пуст и зарезервирован** за вердиктом по вендорам: планом 22.08 (§7 п.6)
> он выдан Шагу 2 — второму чтению панели `vendor2`, — и до метрики (ii) того шага вердикт печатать
> запрещено. Дыра в нумерации есть бронь, а не потерянная секция.
Замер отвечает на **минимаксный вопрос владельца** дословно: «даже на худших вендорах наша
архитектура должна отрабатывать лучше, чем худшие вендоры на другой архитектуре». На диске лежит
природный эксперимент, за который уже заплачено: один и тот же жилец `glm-5` работает на одних и
тех же 16 английских главах в ДВУХ топологиях — редактором поверх боевого черновика (`E6`) и
однопроходкой (`RG`). Панель $0.
### Д32.0 ⛔ БЛОКЕР СБОРКИ, КОТОРЫЙ ПРИШЛОСЬ СНЯТЬ ПЕРВЫМ
`rol.py` арма `E6` **не знал**: единственное его вхождение в файле было комментарием об эррате.
Клетки `dv-g-e6-*` куплены другим ригом (`en_axis.py --e6`) из кассы **ФД-G**, лежат под буквой
чужой фазы и несут ДРУГУЮ схему — нет полей `pair`, `podacha`, `gen`, зато `role="editor3"`.
**Чем это было опасно, и это ровно класс «реализация не то, чем названа».** Неизвестное имя арма
падало в последнюю ветку `read_texts` (`ZK.cell(ZK.tag(…))`), та строила несуществующий путь
`dv-j-e6-<uid>`, файла не находила и возвращала **пустую строку**; `emit_read` выбрасывает главу,
у которой пуст хоть один арм. Команда `--emit-read --panel=RG,E6,…` не упала бы и не соврала бы
вслух — она собрала бы панель из скольких-то глав, и число это зависело бы от того, чьи файлы
случайно нашлись рядом.
**Починка** (`rol.py`): заведён реестр `FOREIGN` — армы чужих фаз со своим префиксом, кассой,
ригом-владельцем и ожидаемыми полями клетки; загрузчик `foreign_text` на любой беде даёт **СТОП**,
а не пустую строку; чистая функция `foreign_defect` проверяет клетку **по содержимому**, а не по
имени файла (имя мы строим сами, оно не удостоверяет ничего): `arm` · `uid` · `role` · модель ·
`finish=stop` · непустой `content`. Единая точка `arm_text` перечисляет все четыре реестра, и имя,
не известное ни одному, роняет сборку — прежде это была тихая пустая строка.
**Гейты проверены ФАЛЬСИФИКАЦИЕЙ, а не зеленью.** Селфтест кормит гейт шестью заведомо больными
клетками, полученными мутацией настоящей: чужой арм · чужая глава · чужая роль · чужая модель ·
`finish=length` · пустой `content`. Отдельно проверено, что при обезвреженном `foreign_defect`
селфтест краснеет на шести пунктах, при снятой проверке пары — на одном, при возврате пустой
строки вместо СТОПа — на одном. Гейт, зелёный только на здоровом входе, не сторожит ничего: ровно
этим болел селфтест блокера Б8 — он фильтровал арм по наличию файла клетки, файлов не было, и
гейт зеленел вхолостую.
### Д32.1 СОСТАВ ПАНЕЛИ И ЗАМОРОЖЕННЫЙ СПИСОК ГЛАВ
Тег панели **новый**; существующие теги не пере-эмитируются (пере-эмиссия с другим составом армов
переписала бы раскладку уже прочитанной панели, и ответы стали бы мусором молча).
| арм | что это | модель | промт | потолок вывода | клеток |
|---|---|---|---|---|---|
| `ZD` | голый черновик | `deepseek-v4-flash` | translator, 3268 зн. | 32000 | 16/16 |
| `Z0` | черновик → боевой редактор | `deepseek-v4-pro` | editor, **4404 зн.** | **16000** | 16/16 |
| `ZF` | он же, ВТОРАЯ генерация (пол) | `deepseek-v4-pro` | editor, **4404 зн.** | **32000** | 16/16 |
| `E6` | черновик → редактор `glm-5` | `glm-5` | editor, **4404 зн.** | **16000** | 16/16 |
| `RG` | однопроходка-роль | `glm-5` | onepass, 7909 зн. | 32000 | 16/16 |
**Системный промт `Z0`, `ZF` и `E6` — ПОБАЙТНО ОДИН** (сверено исполнением). Значит:
* `Z0 ↔ E6`**чистый контраст жильца**: топология, промт, вход и черновик тождественны, меняется
только модель;
* `Z0 ↔ ZF`**пол**: тот же запрос, вторая генерация;
* `E6 ↔ RG`**контраст топологии при одном жильце**, тот самый минимаксный.
Шестнадцать глав — пересечение, полное у всех пяти армов. Список заморожен ДО чтения (порядок —
детерминированный отбор захода Д17):
```
f2274720c9 53f1a12dff b065a92dc0 26c3bff1d4 49ca859c94 100b088f71 197f98eb61 d3237e1dea
eefdade2c3 27cb3a7e69 3bd6481182 8e50448cea 90a20cb443 001e6ac4eb c3517980c7 5a8f48d24a
```
**Объём: 26 243 знака исходника = 7.89 главы Гу.** Панель бесплатная и потому идёт на 7.89, а не
на стандартные 6 глав Гу: стандарт заведён для ПОКУПАЕМЫХ ранговых панелей. За все 80 клеток уже
заплачено $0.906 (`ZD` 0.016 · `Z0` 0.124 · `ZF` 0.668 · `RG` 0.046 · `E6` 0.052).
### Д32.2 ШАПКА: ЖИЛЕЦ УРАВНЕН, И УРАВНЕН МЕХАНИЗМОМ, А НЕ СОВПАДЕНИЕМ
Сверено по клеткам, 16 из 16 у обоих армов: модель `glm-5``model_returned` та же) ·
`finish=stop` · **`reasoning_tokens=0`**.
⚠ И причина нуля названа кодом, а не наблюдением: у `RG` объявленный уровень `THINK_LEVEL["RG"]
= "off"`, то есть подавление ростера остаётся; `E6` покупался прямым `ROSTER.call_kwargs` без
переопределения. На проводе у обоих **`extra_body: {"thinking": {"type": "disabled"}}`** —
одно и то же наше подавление. Это блокер Б7 в обоих армах СРАЗУ, и именно поэтому он их не
разводит: конфаундер общий. `temperature: 0.3` у обоих; честно ли Z.ai её принимает — по-прежнему
НЕ ПРОВЕРЕНО (дыра ревью §4).
### Д32.3 ⛔ ЧТО В ЭТОЙ ПАНЕЛИ НЕ УРАВНЕНО — НАЗЫВАЕТСЯ ДО ЗАМЕРА, А НЕ ПОСЛЕ
1. **Объём инструкции. `RG` 7909 знаков против 4404 у `E6` — ×1.80.** Контраст топологии
неизбежно несёт на себе и промт: у однопроходки и у редактора не может быть одного промта.
⇒ читать вывод только так: «топология ВМЕСТЕ с её промтом». Направление конфаундера играет
ЗА однопроходку (в эксп-21 разница объёма ×1.92 работала на длинный промт), поэтому проигрыш
`RG` был бы выводом СИЛЬНЕЕ объявленного, а выигрыш — слабее.
2. **⛔ Потолки вывода РАЗНЫЕ, и это новая находка, ревью 21.08 её не несёт.** `Z0` и `E6` куплены
под 16000, `ZF` и `RG` — под 32000. Асимметрия внутри самой пары пола `Z0/ZF`. Обрывов нет
(все `finish=stop`), но **у `Z0` максимум `completion_tokens` = 15 835 при потолке 16 000 —
99.0%**: распределение длины размышления у половин пола цензуровано по-разному. Селфтест
захода, объявленный сторожем («ZF = ТОТ ЖЕ запрос, что у боевой связки»), сличает только
`msgs` и к телу вызова слеп — тот же класс В7.
3. **`RG` черновика не видит вовсе** — он однопроходка. «Буфер» здесь означает не «второй проход»,
а «дорогая чужая модель сделала черновик, а `glm-5` только правил».
4. **Цена клетки `ZF` впятеро выше `Z0`** при том же промте и модели — следствие ценового пина
DeepSeek (Б11), а не свойство арма. Замера не касается: панель бесплатна.
5. **Ни `E6`, ни `RG` не хранят `call_kwargs` и `reasoning_text`** — провенанс восстановлен по
коду покупки и по токенам, прямого доказательства провода на диске нет (долг Шага 6).
### Д32.4 ПЕРВИЧНЫЙ ЭНДПОЙНТ — ДЕФЕКТ-КЛАССЫ, А НЕ МЕСТА
**Почему не места.** Мощностная таблица консилиума 22.08: на английской паре эффект в ОДНО место
стоит 68 глав Гу, в половину места — 271. Панель-0 — 7.89. ⇒ **ранговый эндпойнт этой панели
объявлен ОПИСАТЕЛЬНЫМ ЗАРАНЕЕ**, до всякого числа. Несущим объявляется счётный: дефект-класс,
ломающийся в 2030% глав при ~0 у компаратора, разрешается уже на 2530 единицах.
Прибор — `eval/dovodka/schet.py`, $0, детерминированный. Четыре класса объявлены ЗДЕСЬ и до снятия:
| класс | что считается |
|---|---|
| **род** | форма 1 л. ед. ч. прош. вр. с неверным родом. Три подкласса, не пересекаются: **«эталон»** — пол голоса внутреннего рассказа по окну установлен, форма ему противоречит; **«разнобой»** — пол по окну НЕ устанавливается, а арм ставит в одном слое ОБА рода (дефект внутренний, исходника для него не требуется; дефектом считается МЕНЬШИНСТВО форм); **«речь»** — род, не принадлежащий ни одному объявленному цитируемому говорящему |
| **язык** | латиница в русском выходе, КРОМЕ объявленных эталоном иноязычных вставок и римских цифр |
| **приём** | авторский типографский приём исходника (разрядка `P A T I E N C E`), потерянный выходом |
| **банк** | термины банка, чья канонная форма в выходе встречается реже, чем в исходнике (`canon_gaps`) |
**Класс «приём» применим к ОДНОЙ главе из шестнадцати** (`197f98eb61`, `P A T I E N C E`) — это
анекдот, а не класс, и он объявляется описательным ДО того, как будет посчитан.
**⛔ Отбор форм 1 л. ужесточён после проверки исполнением, и первая редакция была негодной.** Она
брала любую форму на `-л/-ла` с местоимением «я» в окне ±40 знаков — и считала третьи лица
(«Габриэль пожал плечами») и СУЩЕСТВИТЕЛЬНЫЕ на `-л` («Пьющий пепел»). На главе `f2274720c9` она
давала 2ж/5м там, где настоящих форм три и все мужские. **Предварительный сигнал «D0 3/1 · E6 3/2 ·
RG 0/5», записанный в план 22.08, снят этим ригом и потому НЕ ЯВЛЯЕТСЯ показанием** — он
воспроизводится только негодной редакцией счётчика. В пакеты читателей он не идёт (запрет плана),
и в выводы тоже не идёт.
Починка: местоимение обязано стоять в синтаксической связи — сразу перед глаголом (через 03
служебных слова) или сразу после него. Правило разделения «повествование / прямая речь» —
механическое и объявлено здесь: абзац-реплика начинается с тире, речь идёт до первого закрывающего
тире перед словами автора, всё после него — повествование.
### Д32.4а ⛔ ЭТАЛОН СНЯТ ПО ИСХОДНИКУ И СЛЕПО К ПЕРЕВОДАМ
Требование п.3а плана: «эталон грепов фиксируется ДО снятия, иначе их можно подогнать под
увиденное». Исполнено буквально. Каждую из 16 глав читали **три независимых агента** — строитель
и два скептика, одному из которых выдан мандат ОПРОВЕРГНУТЬ вывод. Каждому был открыт **ровно один
файл**: английский исходник его главы. Русские переводы, код зоны, отчёт и `~/books` были закрыты
запретом в промте.
**⚠⚠ ПЕРВЫЙ КРУГ ЭТАЛОНА ОКАЗАЛСЯ НЕГОДЕН, И ПОЙМАЛА ЭТО ЕГО ЖЕ СВЕРКА.** Вопрос был задан про
«пол рассказчика», а книга **рамочная**: хронист Jean-François пишет третьим лицом, а рассказ
героя идёт от первого лица внутри кавычек, часто без закрывающей. Половина строителей сочла
внутренний рассказ «прямой речью» и объявила первое лицо отсутствующим — **на восьми главах из
шестнадцати**; скептики это сняли на четырёх, а на остальных согласились с ошибкой. Круг
переспрошен с НАЗВАННОЙ рамкой. Переводы агентам оставались закрыты в обоих кругах — то есть
исправлена ПОСТАНОВКА ВОПРОСА, а не подогнан ответ; оба круга лежат в `eval/dovodka/etalon-panel0.json`
целиком, расхождение видно построчно.
**Девиация, которую надо назвать вслух.** Строя счётчик, я до фриза эталона видел выход армов на
нескольких главах — иначе нельзя было поймать, что первая редакция отбора форм считает
существительные. Это увиденное повлияло на ПРИБОР (регекс ужесточён) и не могло повлиять на
ЭТАЛОН: эталон снят агентами, которым выход был закрыт. Считаю девиацию объявленной и не
компенсируемой иначе.
**Расход агент-сессий.** Построение эталона стоило 48 сессий в первом круге и 48 во втором. В
журнале `runs.py` они НЕ записаны: журнал держит судейские и читательские сессии, привязанные к
токенам панелей, а эти агенты не судят ничего и токенов не держат. Число называю здесь, чтобы оно
не пропало: при капе 200 и израсходованных 98 читательских это отдельная статья, и её учёт —
вопрос владельцу, а не решение сессии.
### Д32.5 ПРАВИЛО РЕШЕНИЯ, ОБЪЯВЛЕННОЕ ДО ЧИСЕЛ
* **Первичная величина — число ГЛАВ С ≥1 ФАТАЛОМ** любого из четырёх классов (агрегат).
* **Тест — парный знаковый по главам, `p < 0.05`.** Реализация одна на зону (`zsud._sign_p`),
второй не заводится.
* По-классовые разбивки — **вторичные**, печатаются рядом и вердикта не несут.
* Ранги слепого чтения — **описательные**, объявлено выше.
* Вердикт формулируется как **«интеракция архитектура × жилец на ИСПЫТАННЫХ жильцах»**, НЕ как
«минимакс»: худший редактор не искался, связка двухвендорна (черновик — DeepSeek).
### Д32.6 СВЕРКИ ДО ЧТЕНИЯ
1. **Исходник у всех армов побайтно один — и это доказано построением, а не сличением.**
`uid = sha1(source.strip())[:10]` (`bakeoff.uid_of`), а совпадающий uid у клетки `E6` и клетки
`RG` есть на всех 16 главах. Разный исходник дал бы разный uid.
2. **Потолки вывода напечатаны** — таблица Д32.1, асимметрия названа в Д32.3.
3. **Промт `E6` распечатан `--wire`-аналогом и прочитан.** Аналог собран **кодом его собственного
рига** (`en_axis`: исходник, черновик `D0`, `PR.editor_msgs` пары en, тело вызова строит
РОСТЕР НАПРЯМУЮ), а не пере-написан в `rol.py`, — иначе гейт показывал бы, что я думаю о
клетке, а не что ушло в модель. Прочитано вслух: промт **пар-корректен** (несёт «МЕРЫ
английской традиции… НЕ пересчитывай в метрические», «КАЛЬКИ en→ru», французский слой) —
блокера Б1 здесь нет; глоссарий доехал (`Dior → «Диор»`, `Reyne → «Рейн»`, `Celene → «Селена»`
и ещё пять). ⚠ И ровно на этом промте видно то, что Шаг 7 плана называет второй половиной мины:
**в глоссарии есть имена и нет ни одного поля пола** — «Рейн» приезжает к модели как строка,
без указания, он это или она.
4. `--verify-read` обязан вернуть **EXIT=0**; ключ пишется ДО ответов; раскладка своя на каждую
главу; один читатель на главу.
### Д32.7 ⛔ ТРИ ДЕФЕКТА НАШЛИСЬ В САМОМ ПРИБОРЕ, И НАШЛИ ИХ ГЛАЗА, А НЕ ГЕЙТ
Порядок работы был такой: блокер сборки снят → эталон снят агентами вслепую → классы и правило
решения объявлены → счётчик прогнан → **улики прочитаны глазами по каждому попаданию** → найдены
три дефекта прибора → починены → счётчик прогнан заново. Печатаю оба свода, чтобы читатель судил
сам: правка, сделанная ПОСЛЕ первых чисел, обязана предъявлять и старые числа.
1. **Правило слоёв не знало ВОЗОБНОВЛЕНИЯ реплики.** Русская вёрстка диалога чередуется:
«— речь, — слова автора. — речь». Первая редакция обрывала речь на первом тире и всё дальнейшее
звала повествованием. Цена замерена: на главе `49ca859c94` реплика Жан-Франсуа «…как и я,
мадемуазель Кастия. Нет, я сбежал в Огюстен…» ложилась в ПОВЕСТВОВАНИЕ, и мужская форма
мужского говорящего шла в дефекты рассказчицы — **у всех трёх армов сразу**. Прибор производил
брак вместо того, чтобы его находить.
2. **Речь проверялась даже там, где пол голоса не установлен.** На `5a8f48d24a` собственная
реплика рассказчицы «— Где я была?» уходила в дефекты как «род ж не принадлежит ни одному
говорящему главы»: в списке говорящих стоят только Персиваль и Вульфрик (оба «м»), а сама
рассказчица в него не входит по определению. Теперь неустановленный пол голоса выключает
проверку речи целиком.
3. **Класс «язык» был слеп к латинице В РАЗРЯДКУ.** Регекс требовал двух букв подряд, а
`P A T I E N C E` — восемь одиночных через пробел. Поймано чтением: **`RG` единственный
сохранил авторскую разрядку главы `197f98eb61` — и сохранил её ПО-АНГЛИЙСКИ.** Свод хвалил бы
арм за непереведённое слово. ⚠ И это снимает предварительный сигнал плана «разрядку потерял сам
черновик → потеряли и `E6`, и `RG`»: разрядку сохранил `ZF` (кириллицей, верно) и `RG`
(латиницей, не переведя), потеряли `ZD`, `Z0` и `E6`.
Каждая починка закрыта ФАЛЬСИФИКАЦИЕЙ в селфтесте (подсаженный брак обязан ловиться, чистый текст
обязан давать ноль), и правки двигали числа в ОБЕ стороны: первая убрала дефекты у всех армов,
третья добавила один арму `RG`.
**⛔ И ЧЕТВЁРТАЯ НАХОДКА — НЕ В ПРИБОРЕ, А В ДВИЖКЕ ЗАМЕРА: ЛАТЕНТНАЯ МИНА ДВУХ КОНТУРОВ ВЫСТРЕЛИЛА.**
Селфтест `rol.py` с 22.08 печатал предупреждение: в процессе живут ДВА экземпляра `contour`, и
`wire()` настраивал только один — второй оставался на китайской паре; тогда было записано «до
данных не добралось». 23.08 добралось. Скрипт, честно позвавший `PAIRS["en"]["wire"]()` и сразу
спросивший текст БЕСПЛАТНОГО арма, получил **пустую строку**: `free_text` идёт в контур ЗАХОДА, тот
искал китайскую клетку английской главы, не нашёл и вернул пусто с одним `warning`. В своде это
читалось бы как «у арма на этой главе мало дефектов» — пустой текст дефектов не имеет по построению.
Починено в корне: пара включается в ОБОИХ экземплярах одной командой (`rol._wire`), проверка
селфтеста оставлена сторожем на случай третьего экземпляра. Дополнительно счётчик **выбрасывает из
счёта главу с пустым текстом хоть у одного арма и печатает это вслух** — арм с пропавшей клеткой
не имеет права выглядеть лучшим.
### Д32.8 ⭐ РЕЗУЛЬТАТ СЧЁТНОГО ПРИБОРА
**Попаданий (не глав), 16 глав, четыре класса. Слева — до починок прибора, справа — после.**
| класс | ZD | Z0 | ZF | RG | E6 |
|---|---|---|---|---|---|
| **род** | 2 → **0** | 2 → **0** | 2 → **0** | 8 → **5** | 4 → **1** |
| **язык** | 2 → **2** | 0 → **0** | 0 → **0** | 8 → **9** | 0 → **0** |
| **приём** | 1 → **1** | 1 → **1** | 0 → **0** | 0 → **0** | 1 → **1** |
| **банк** | 25 → **25** | 9 → **9** | 7 → **7** | 10 → **10** | 9 → **9** |
| **фаталов всего** | 30 → **28** | 12 → **10** | 9 → **7** | 26 → **24** | 14 → **11** |
| **глав с ≥1 фаталом** | 13 → **13** | 10 → **8** | 8 → **6** | 11 → **12** | 10 → **8** |
**Частота глав с дефектом и 95% интервал Уилсона** (требование плана §4: по редким классам печатать
частоту с интервалом, а не вердикт):
| класс | ZD | Z0 | ZF | RG | E6 |
|---|---|---|---|---|---|
| род | 0/16 [0.00;0.19] | 0/16 [0.00;0.19] | 0/16 [0.00;0.19] | **2/16** [0.03;0.36] | 1/16 [0.01;0.28] |
| язык | 2/16 [0.03;0.36] | 0/16 [0.00;0.19] | 0/16 [0.00;0.19] | **4/16** [0.10;0.49] | 0/16 [0.00;0.19] |
| приём | 1/16 | 1/16 | 0/16 | 0/16 | 1/16 |
| банк | 12/16 [0.51;0.90] | 7/16 [0.23;0.67] | 6/16 [0.18;0.61] | 7/16 [0.23;0.67] | 7/16 [0.23;0.67] |
| **все** | 13/16 [0.57;0.93] | 8/16 [0.28;0.72] | 6/16 [0.18;0.61] | 12/16 [0.51;0.90] | 8/16 [0.28;0.72] |
**Парный знаковый тест. ① — правило, объявленное планом (бинарно «глава с ≥1 фаталом»);
② — вторичное, число фаталов на главу.**
| контраст | ① бинарно | ② счётно |
|---|---|---|
| `Z0 E6` (**один промт, разные жильцы**) | 0 глав различия, p=1.0000 | 1, 1 глава, p=1.0000 |
| `RG E6` (**один жилец, разные топологии**) | +4, 4 главы, **все за `E6`**, p=0.1250 | +13, 6 глав, **все за `E6`**, **p=0.0312 РАЗЛИЧИМО** |
| `Z0 RG` | 4, 4 главы, все за `Z0`, p=0.1250 | 14, 6 глав, все за `Z0`, **p=0.0312 РАЗЛИЧИМО** |
| `ZF RG` | 6, 6 глав, все за `ZF`, **p=0.0312 РАЗЛИЧИМО** | 17, 8 глав, все за `ZF`, **p=0.0078 РАЗЛИЧИМО** |
| `ZD Z0` | +5, 5 глав, все за `Z0`, p=0.0625 | +18, 10 глав, все за `Z0`, **p=0.0020 РАЗЛИЧИМО** |
| `ZD E6` | +5, 5 глав, все за `E6`, p=0.0625 | +17, 9 глав, все за `E6`, **p=0.0039 РАЗЛИЧИМО** |
| `ZD RG` | +1, 5 глав, 3 за `RG`, p=1.0000 | +4, 11 глав, 7 за `RG`, p=0.5488 |
**ПОТОЛОК МОЩНОСТИ, ПЕЧАТАЕТСЯ РЯДОМ С КАЖДЫМ «НЕ РАЗЛИЧИМО».** При всех расхождениях в одну
сторону двусторонний знаковый p = 2/2^k, то есть **p<0.05 недостижим, пока глав с ненулевой
разницей меньше шести**. Классы «род» (2 главы) и «язык» (4 главы) эта панель не разрешит НИКОГДА,
как бы односторонен ни был результат. «Не различимо» здесь означает «прибор слеп», а не «армы
одинаковы» — и по построению, а не по невезению.
### Д32.9 ⭐⭐ ЧТО ЭТО ЗНАЧИТ — И ЧЕГО НЕ ЗНАЧИТ
**Читается это так.** Возьмите один и тот же слабый жилец `glm-5` с погашенным размышлением.
* Поставьте его **редактором** над боевым черновиком (`E6`) — он даёт **8 глав из 16 с браком,
1 попадание по роду, 0 по языку**. Боевой `deepseek-v4-pro` в той же топологии, с тем же
побайтно промтом (`Z0`) — **8 из 16, 0 и 0**. Между ними **НОЛЬ глав различия** по первичному
правилу и 1 фатал по вторичному. То есть по классу А смена жильца внутри архитектуры прибором
НЕ ловится.
* Поставьте того же жильца **однопроходкой** (`RG`) — **12 глав из 16, 5 попаданий по роду,
9 по языку**. Против `E6` разница в 6 главах, **все шесть в одну сторону**, p=0.0312 по
вторичному правилу; против второй генерации связки `ZF` — 6 глав, все в одну сторону, p=0.0312
**по ПЕРВИЧНОМУ**.
**Буфер существует и замерен.** Топология «дорогой чужой черновик → слабый жилец редактором»
снимает у слабого жильца 4 из 5 дефектов рода и все 9 дефектов языка по сравнению с тем же
жильцом, делающим работу в одиночку.
⛔⛔ **И ЧЕГО ЭТО НЕ ЗНАЧИТ — ПЯТЬ ОГОВОРОК, КАЖДАЯ СНИМАЕТ ЧАСТЬ ВЫВОДА.**
1. **Первичное правило контраст `RGE6` НЕ ЗАКРЫВАЕТ** (p=0.1250). Закрывает вторичное и закрывает
`ZFRG`. По конъюнкции, которой фаза меряет вендоров, это «направление единогласно, значимость
не достигнута». Печатать «однопроходка различимо хуже редактора» нельзя; печатать «различимо
хуже второй генерации связки» — можно.
2. **Это интеракция «архитектура × жилец на ИСПЫТАННЫХ жильцах», а не минимакс.** Худший редактор
не искался. `glm-5` — не «худший вендор», а единственный второй, которого мы пробовали в этой
роли.
3. **Буфер ДВУХВЕНДОРЕН.** Черновик делает `deepseek-v4-flash`. Архитектура, которая «спасает
слабого жильца», сама содержит клетку другого вендора — и на неё же опирается. Утверждение
«наша архитектура вендоро-независима» этим замером НЕ проверено.
4. **Буфер чинит только то, что черновик донёс, и это видно прямо здесь.** Разрядку
`P A T I E N C E` черновик `ZD` потерял → потерял и `E6`. Уцелела она у `ZF` (вторая генерация
связки, кириллицей) и у `RG` (латиницей, не переведя). Контракт «что обязан гарантировать
черновик» — не украшение, а условие работы буфера.
5. **Конфаундер объёма промта играет ЗА однопроходку** (×1.80), значит её проигрыш — вывод
более сильный, чем объявленный, а не более слабый. Это единственная оговорка, работающая в
пользу вывода, и потому её мало.
**Отдельно — то, чего никто не заказывал, а замер отдал бесплатно.** Черновик `deepseek-v4-flash`
за $0.00055 за клетку дал **0 дефектов рода на всех 16 главах** — там, где `glm-5` однопроходкой за
впятеро большие деньги дал 5. Дешёвая модель в узкой роли не уступила дорогой в широкой.
Но она же дала **25 потерь банка против 910 у всех прочих** и 2 непереведённых английских слова:
черновик хорош ровно там, где его страхуют.
### Д32.10 ⭐⭐ НАХОДКА ПОД ВОПРОС 0, ПОЛУЧЕННАЯ ЗА $0
Эталон, снятый по исходнику вслепую, дал побочный результат, которого никто не искал, и он бьёт
прямо в метавопрос владельца.
**В 2 окнах из 8, где есть рассказчик от первого лица, его пол ПО ОКНУ НЕ УСТАНАВЛИВАЕТСЯ вовсе.**
Голос главы `5a8f48d24a` — Селена (цепочка «обращение по имени → немедленное „Где я была?“»
замыкается жёстко), но во всём окне нет ни одного `she` о ней; голос `c3517980c7` — Гейб, и точно
так же ни одного `he`. Это установлено ТРЕМЯ независимыми чтениями каждой главы, причём двоим из
трёх был выдан мандат опровергнуть.
**Пайплайн, который видит только главу, по построению слеп примерно в четверти случаев.** Он не
«ошибается» — ему нечем узнать. Узнать может ТОЛЬКО книжная память: тот же голос (Селена) ведёт
ещё три окна нашей выборки, и там пол устанавливается прямо. То же у Габриэля: 3 окна из 4.
**А в банке полей пола НЕТ.** `bank-en.json` несёт имена и канонные формы — «Рейн», «Селена»,
«Диор» — и ни одного поля рода; это видно на распечатке промта `E6` (Д32.6 п.3). То есть
механизм, который единственный мог бы закрыть эти 25% глав, в наших прогонах **не существовал**.
⇒ Это первое ПРЯМОЕ и бесплатное подтверждение тезиса Шага 7 плана: «решение „банк чинит пол“ не
имеет ни одного замера», и «пол плывёт у всех» его не опровергает — механизма просто не было.
Теперь известно и сколько он стоил бы: без него ~25% первололичных глав неразрешимы в принципе.
⚠ Оценка 2 из 8 снята на маленькой выборке: интервал Уилсона [0.07; 0.59]. Число — порядок
величины, не константа.

File diff suppressed because it is too large Load diff

View file

@ -83,12 +83,29 @@ CORE = ZONE / "prompts" / "onepass-core.md"
MAX_OUT = 32000 # как в заходе Д17: обрыв на потолке стоит денег и не даёт данных
# ── ПАРА КАК ДАННЫЕ. Ни одна ветка ниже не спрашивает «а это китайский?» ──────────────────────
# ⛔⛔ МИНА ДВУХ ЭКЗЕМПЛЯРОВ КОНТУРА — ОБЕЗВРЕЖЕНА ЗДЕСЬ, А НЕ ОБЪЯВЛЕНА. Селфтест с 22.08 печатал
# предупреждение: в процессе живут ДВА независимых `contour` (свой у `rol` через `en_axis`, свой у
# `zakhod`), и `wire()` настраивал ТОЛЬКО первый — второй оставался на китайской паре. Тогда было
# записано «до данных не добралось». 23.08 добралось: скрипт, честно позвавший `PAIRS["en"]["wire"]()`
# и сразу спросивший текст БЕСПЛАТНОГО арма, получил ПУСТУЮ СТРОКУ — `free_text` идёт в контур
# ЗАХОДА, тот искал китайскую клетку английской главы, не нашёл и вернул пусто с одним warning'ом.
# В своде это выглядело бы как «у арма на этой главе мало дефектов»: пустой текст дефектов не имеет.
# ⇒ Пара включается в ОБОИХ экземплярах одной командой. Проверка селфтеста остаётся сторожем на
# случай третьего экземпляра.
_WIRE_OWN = {"zh": lambda: C.configure(PARA.ZH), "en": EN.wire}
def _wire(p: str) -> None:
_WIRE_OWN[p]()
ZK.PAIRS[p]["wire"]()
PAIRS = {
"zh": {"letter": "m", "phase": "ФД-M", "onepass": "A2",
"wire": lambda: C.configure(PARA.ZH), "units": lambda: C.units_ok(),
"wire": lambda: _wire("zh"), "units": lambda: C.units_ok(),
"para": lambda: PARA.ZH},
"en": {"letter": "n", "phase": "ФД-N", "onepass": "E2",
"wire": EN.wire, "units": EN.units, "para": lambda: PARA.EN},
"wire": lambda: _wire("en"), "units": EN.units, "para": lambda: PARA.EN},
}
# ── ВОЗВРАТ АВТОРСКИХ АБЗАЦЕВ ИСХОДНИКУ ──────────────────────────────────────────────────────
# ⚠⚠ ПОЧЕМУ ЭТО ОТДЕЛЬНЫЙ АРМ, А НЕ ПОЧИНКА ЭКСТРАКТОРА. Наш английский экстрактор
@ -444,6 +461,101 @@ def chosen(p: str) -> list[dict]:
return ZK.chosen(p)
# ── АРМЫ ЧУЖИХ ФАЗ: клетки куплены ДРУГИМ РИГОМ, и схема клетки у них СВОЯ ────────────────────
# ⛔ ПОЧЕМУ НЕ СТРОКА В `ARMS`. `ARMS` — реестр того, что покупает ЭТОТ риг: из него растут тег
# (`dv-n-…` на en), смета, гейт покупки и объявленный уровень размышления. `E6` ничем этим не
# является: его 16 клеток куплены `en_axis.py --e6` из кассы ФД-G, лежат под буквой ЧУЖОЙ фазы
# (`dv-g-e6-…`) и несут ДРУГИЕ поля — нет ни `pair`, ни `podacha`, ни `gen`, зато `role="editor3"`.
# Вписать его в `ARMS` значило бы объявить покупаемым то, что этот риг покупать не умеет и не должен.
#
# ⚠⚠ ЧЕМ ЭТО БЫЛО ОПАСНО ДО ПРАВКИ — И ЭТО РОВНО КЛАСС «РЕАЛИЗАЦИЯ НЕ ТО, ЧЕМ НАЗВАНА». Имя, не
# известное ни одному реестру, падало в последнюю ветку `read_texts` (`ZK.cell(ZK.tag(…))`), та
# строила `dv-j-e6-<uid>`, файла не находила и возвращала ПУСТУЮ СТРОКУ. А `emit_read` выбрасывает
# главу, у которой пуст хоть один арм. То есть команда `--panel=RG,E6,Z0,ZF` не упала бы и не
# соврала бы вслух — она собрала бы панель из СКОЛЬКИХ-ТО глав, и число это зависело бы от того,
# чьи файлы случайно нашлись. ⇒ ниже всё ГРОМКО: неизвестный арм — СТОП, негодная клетка — СТОП.
def _e6_wire(uid: str) -> tuple[list[dict], str, dict]:
"""WIRE-АНАЛОГ `E6` — СОБСТВЕННЫМ КОДОМ ЕГО РИГА (`en_axis.cmd_e6`), не пере-написанным здесь.
Пере-написанный промт показал бы, что Я думаю о клетке, а не что ушло в модель, то есть
ровно тот самый класс, против которого гейт `--wire` и заведён. Поэтому вызываются те же
функции в том же порядке: исходник и черновик берутся у `en_axis`, сообщения собирает боевой
`PR.editor_msgs` пары en, тело вызова строит РОСТЕР НАПРЯМУЮ (`ROSTER.call_kwargs`), а не
`rol.call_kwargs` у `E6` иной потолок вывода, и подменять его нашим значило бы напечатать
не тот провод (класс В7: несущий контраст, собранный под разными потолками).
"""
src = next(u["source"] for u in EN.units() if u["uid"] == uid)
msgs = C.PR.editor_msgs(src, EN.base_draft(uid), EN._law(src), pair="en-ru") # noqa: SLF001
return msgs, EN.EDITOR2, ROSTER.call_kwargs(EN.EDITOR2, msgs)
FOREIGN = {
"E6": {"pair": "en", "prefix": "dv-g-e6-", "model": "glm-5", "role": "editor3",
"phase": "ФД-G", "rig": "eval/dovodka/en_axis.py --e6", "wire": _e6_wire,
"что": "ВТОРОЙ редактор glm-5 поверх боевого черновика D0 (носитель гипотезы H-4)"},
}
def foreign_defect(d: dict, arm: str, uid: str) -> str:
"""Почему клетка чужой фазы НЕ годится в панель. Пусто = годна.
Проверки идут по СОДЕРЖИМОМУ клетки, а не по имени файла: имя мы строим сами, и оно не
удостоверяет ничего. Клетка обязана сама сказать, чей она арм, какой главы, какой модели и
какой роли, иначе под именем `E6` в панель уедет что угодно, что лежит по этому пути.
Функция ЧИСТАЯ (dict строка) намеренно: только так селфтест может уронить её на заведомо
больном входе, не подкладывая порченых файлов в `~/books`.
"""
spec = FOREIGN[arm]
got_model = d.get("model_returned") or d.get("model")
for what, got, want in (("arm", d.get("arm"), arm), ("uid", d.get("uid"), uid),
("role", d.get("role"), spec["role"]),
("модель", got_model, spec["model"])):
if got != want:
return f"поле {what}={got!r}, ожидалось {want!r}"
if d.get("finish") != "stop":
return f"finish={d.get('finish')!r} — клетка оборвана либо мертва"
if not (d.get("content") or "").strip():
return "content пуст при finish=stop — деньги списаны, текста нет (класс В5)"
return ""
def foreign_text(p: str, arm: str, uid: str) -> str:
"""Текст арма чужой фазы. Любая беда — СТОП, а НЕ пустая строка: молча выброшенная глава
меняет состав панели, объявленный пре-регом, и делает это невидимо."""
spec = FOREIGN[arm]
if spec["pair"] != p:
raise SystemExit(f"⛔ арм {arm} существует только на паре «{spec['pair']}», запрошена «{p}»"
f"его клетки купил {spec['rig']} из кассы {spec['phase']}")
f = OUT / f"{spec['prefix']}{uid}.json"
if not f.exists():
raise SystemExit(f"⛔ клетки {arm} для главы {uid} нет на диске ({f.name}). Этот риг её "
f"купить не может — она из кассы {spec['phase']}, риг {spec['rig']}")
d = json.loads(f.read_text(encoding="utf-8"))
why = foreign_defect(d, arm, uid)
if why:
raise SystemExit(f"⛔ клетка {arm}/{uid} НЕГОДНА: {why}")
return d["content"]
def arm_text(p: str, arm: str, uid: str) -> str:
"""ОДНА точка правды о том, откуда берётся текст арма. Неизвестное имя — СТОП.
Прежде неизвестное имя молча уходило в риг захода и возвращало пусто (см. блок выше). Теперь
реестры перечислены явно, и опечатка в `--panel=` роняет сборку, а не режет панель.
"""
if arm in FOREIGN:
return foreign_text(p, arm, uid)
if arm in FREE:
return free_text(p, arm, uid)
if arm in ARMS:
return cell(tag(p, arm, uid))
if arm in ZK.ARMS:
return ZK.cell(ZK.tag(p, arm, uid))
raise SystemExit(f"⛔ арм {arm!r} не известен НИ ОДНОМУ реестру: бесплатные {FREE}, "
f"покупаемые этим ригом {tuple(ARMS)}, армы захода {ZK.ARMS}, "
f"чужих фаз {tuple(FOREIGN)}. Панель НЕ собирается")
# ⛔⛔ БЛОКЕРЫ Б6/Б7 РЕВЬЮ 21.08. Ростер кодирует ОДНИМ режимом `none` («ручку не шлём») две
# ПРОТИВОПОЛОЖНЫЕ вещи: у `deepseek-v4-pro` вендор-дефолт — размышление HIGH, у `grok-4.3` — LOW
# (квирк-бюллетень :60/:80). Значит «одинаковая конфигурация по ростеру» означает РАЗНОЕ
@ -817,6 +929,40 @@ def cmd_selftest() -> int:
else:
ok, d = True, ""
ck(f"Б6 {a}: на провод уходит объявленный уровень «{lvl}»", ok, d)
# ── ШАГ 1 п.0 ПЛАНА 22.08: АРМЫ ЧУЖИХ ФАЗ (`E6`) ────────────────────────────────────────
# ⚠ ГЕЙТ ПРОВЕРЯЕТСЯ НА ЗАВЕДОМО БОЛЬНОМ ВХОДЕ. «Зелено на здоровом» не сторожит ничего —
# ровно так селфтест Б8 был зелен вхолостую на арме, у которого файлов не было вовсе. Больной
# вход делается МУТАЦИЕЙ НАСТОЯЩЕЙ клетки, чтобы проверка не разъехалась с реальной схемой.
for fa, spec in FOREIGN.items():
pp = spec["pair"]
PAIRS[pp]["wire"]()
us = chosen(pp)
have = [u["uid"] for u in us if (OUT / f"{spec['prefix']}{u['uid']}.json").exists()]
ck(f"{fa}: клетка есть на КАЖДОЙ главе пары {pp}", len(have) == len(us),
f"{len(have)} из {len(us)}")
if not have:
continue
fu = have[0]
real = json.loads((OUT / f"{spec['prefix']}{fu}.json").read_text(encoding="utf-8"))
ck(f"{fa}: настоящая клетка гейт ПРОХОДИТ", not foreign_defect(real, fa, fu),
foreign_defect(real, fa, fu))
for name, patch in (("чужой арм", {"arm": "RG"}), ("чужая глава", {"uid": "0" * 10}),
("чужая роль", {"role": "onepass"}),
("чужая модель", {"model_returned": "grok-4.3"}),
("обрыв finish=length", {"finish": "length"}),
("пустой content", {"content": " "})):
ck(f"{fa}: гейт РОНЯЕТ больной вход «{name}»",
bool(foreign_defect(dict(real, **patch), fa, fu)))
other = next((x for x in PAIRS if x != pp), pp)
for name, fn in ((f"{fa} на чужой паре {other}", lambda: foreign_text(other, fa, fu)),
("неизвестное имя арма", lambda: arm_text(pp, "ZZZ", fu))):
try:
fn()
ck(f"{fa}: {name} — это СТОП", False, "вернулось молча, без исключения")
except SystemExit:
ck(f"{fa}: {name} — это СТОП", True)
print(f"\n{'ЗАМЕР ГОДЕН К ПОКУПКЕ' if not bad else f'ПРОВАЛОВ: {bad}'}")
return bad
@ -880,9 +1026,7 @@ def read_texts(p: str, uid: str) -> dict:
"""Тексты панели чтения. Пусто у любого арма — глава в пакет не идёт целиком."""
out = {"_src": next(u["source"] for u in chosen(p) if u["uid"] == uid)}
for a in READ_PANEL:
t = free_text(p, a, uid) if a in FREE else (
cell(tag(p, a, uid)) if a in ARMS else ZK.cell(ZK.tag(p, a, uid)))
out[a] = t
out[a] = arm_text(p, a, uid)
return out
@ -1027,14 +1171,19 @@ def wire_dump(p: str, only: str = "") -> int:
("Z7 канон-фиксер", C.fix_msgs(src, d, ["КАНОН: термин X"]), ARMS_MODEL["Z0"]),
("ZP однопроходка-склейка", C.a2_msgs(src), ARMS_MODEL["Z0"])]
cases += [(f"{a} однопроходка-роль", rol_msgs(p, src, a), ARMS[a][0]) for a in ARMS]
# ⚠ Армы чужих фаз печатаются ТЕМ ЖЕ гейтом и рядом с остальными: панель их смешивает, значит
# и читать их провод надо в одном месте. Тело вызова у них СВОЁ — см. `_e6_wire`.
cases = [(n, m, mo, None) for n, m, mo in cases]
cases += [(f"{fa} {sp['что']} · касса {sp['phase']}", *sp["wire"](u["uid"]))
for fa, sp in FOREIGN.items() if sp["pair"] == p]
base = None
for name, msgs, model in cases:
for name, msgs, model, own_kw in cases:
arm = name.split()[0]
if only and only != arm:
continue
sysm = "\n\n".join(x["content"] for x in msgs if x["role"] == "system")
base = base or len(sysm)
kw = call_kwargs(model, msgs, arm)
kw = own_kw if own_kw is not None else call_kwargs(model, msgs, arm)
print("\n" + "=" * 100)
print(f"АРМ {name} · модель {model} · размышление: {THINK_LEVEL.get(arm, 'как у ростера')}")
print(f"инструкции {len(sysm)} знаков (×{len(sysm) / base:.2f} к первому арму) · "

524
eval/dovodka/schet.py Normal file
View file

@ -0,0 +1,524 @@
#!/usr/bin/env python3
"""СЧЁТЧИКИ ДЕФЕКТ-КЛАССОВ — первичный прибор панели-0. $0, детерминированно.
ЗАЧЕМ ОТДЕЛЬНЫЙ ПРИБОР, КОГДА ЕСТЬ СЛЕПОЕ ЧТЕНИЕ. Ранговый прибор при наших размерах безнадёжен:
эффект в ОДНО место стоит 40 глав Гу на китайской паре и 68 на английской (мощностная таблица
консилиума 22.08). Панель-0 7.8 главы Гу, её MDE 2 места. ранговый эндпойнт этой панели
объявлен ОПИСАТЕЛЬНЫМ ЗАРАНЕЕ, а несущим становится СЧЁТНЫЙ: дефект-класс, ломающийся в 2030%
глав при ~0 у компаратора, ловится уже на 2530 единицах.
И ЭТО ЖЕ ДОЛГ ШАГА 5.2 ПЛАНА. Покупать 15 глав Гу под вопрос о кросс-главной консистентности,
не имея счётчиков, значит повторить класс В20 ревью оплаченные клетки, которых не прочёл ни один
прибор. Счётчики отлаживаются ЗДЕСЬ, на уже купленном, и только потом едут в дорогой замер.
ЧТО СЧИТАЕТСЯ (четыре класса, объявлены ДО снятия пре-рег Д32.0):
род формы 1 л. ед. ч. прош. вр. с неверным родом. ТРИ непересекающихся подкласса:
«эталон» пол голоса внутреннего рассказа по окну УСТАНОВЛЕН, форма ему противоречит;
«разнобой» пол по окну НЕ устанавливается, но арм ставит в одном слое ОБА рода
(источник-независимый дефект: винить за выбор нельзя, за два сразу можно);
«речь» род, не принадлежащий ни одному объявленному цитируемому говорящему
язык непереведённый кусок исходного языка (латиница), КРОМЕ римских цифр и вставной
французской РЕПЛИКИ (её и только её боевой пар-мандат велит оставить чужой; имена,
топонимы и «шато/мадемуазель» он велит передавать кириллицей латиница на их месте
есть тот самый дефект). На всю панель такая реплика ОДНА: «A farmer's son, oui.»
приём авторский типографский приём исходника (разрядка «P A T I E N C E»), ПОТЕРЯННЫЙ выходом
банк термины банка, чья канонная форма в выходе встречается реже, чем в исходнике
ЭТАЛОН СНЯТ ПО ИСХОДНИКУ И СЛЕПО К ПЕРЕВОДАМ, И ИНАЧЕ ОН НЕ СТОИТ НИЧЕГО. Требование п.3а
плана: «эталон грепов фиксируется ДО снятия, иначе их можно подогнать под увиденное». Исполнено
буквально: каждую главу читали три независимых агента (строитель + два скептика, один из них с
мандатом ОПРОВЕРГНУТЬ), которым был открыт РОВНО ОДИН файл английский исходник главы; русские
переводы, код зоны и отчёт им были закрыты. Расхождения разрешены руками и названы в отчёте
поимённо. Эталон лежит рядом файлом и фризится своим коммитом ДО сборки пакетов.
ПЕРВЫЙ КРУГ ЭТАЛОНА БЫЛ НЕГОДЕН, И ЭТО ПОЙМАЛА ЕГО ЖЕ СВЕРКА. Вопрос был задан про «пол
рассказчика», а книга РАМОЧНАЯ: хронист пишет в третьем лице, а рассказ героя идёт от первого
лица внутри кавычек. Половина строителей сочла внутренний рассказ «прямой речью» и объявила
первое лицо отсутствующим на восьми главах из шестнадцати. Круг переспрошен с НАЗВАННОЙ рамкой;
переводы агентам оставались закрыты в обоих кругах, то есть исправлена постановка вопроса, а не
подогнан ответ. Оба круга лежат в эталоне, расхождение видно.
ПРИБОР ПЕЧАТАЕТ СТРОКИ, А НЕ ТОЛЬКО ЧИСЛА (требование п.3а). Счёт без улик не проверяем никем,
включая меня: ровно так «13 катастроф» эксп-13 оказались пятью-шестью при оверфлаге судьи.
ЧЕГО ПРИБОР НЕ УМЕЕТ, И ЭТО ОБЪЯВЛЕНО ДО ЗАМЕРА:
· рассказчик 1-го лица без имени лукапу по банку недоступен род берётся из эталона главы,
а не из банка (полей пола в `bank-en.json` нет вовсе, см. Шаг 7 плана);
· разделение «повествование против прямой речи» механическое (см. `_speech_spans`) и у разных
армов может лечь по-разному поэтому классификация КАЖДОГО попадания печатается, а число
попаданий, сменивших класс между армами, выносится в свод отдельной строкой;
· класс «приём» на этой панели применим к ОДНОЙ главе из шестнадцати это анекдот, а не класс,
и он объявляется описательным до того, как будет посчитан.
Запуск: schet.py --selftest гейты прибора, включая ФАЛЬСИФИКАЦИЮ на подсаженном браке
schet.py --report [армы] таблица по главам и агрегат со знаковым тестом
schet.py --hits <АРМ> [uid] все попадания с контекстом для чтения глазами
"""
from __future__ import annotations
import importlib.util
import json
import re
import sys
from pathlib import Path
REPO = Path("/home/ubuntu/projects/textmachine")
ZONE = Path(__file__).resolve().parent
for d in ("dovodka", "editor_tier", "tenant_panel", "role_topology", "bank_arbitration"):
sys.path.insert(0, str(REPO / "eval" / d))
def _load(name: str, path: Path):
spec = importlib.util.spec_from_file_location(name, path)
mod = importlib.util.module_from_spec(spec)
sys.modules[name] = mod
spec.loader.exec_module(mod)
return mod
# ⚠ Тексты армов берутся У `rol`, а не читаются заново: там уже стоят реестры источников и гейт
# годности клетки чужой фазы. Второй загрузчик разъехался бы с ним молча.
R = _load("rol_schet", ZONE / "rol.py")
ZS = _load("zsud_schet", ZONE / "zsud.py") # знаковый тест — один на зону, не второй
ETALON = ZONE / "etalon-panel0.json"
PAIR = "en"
PANEL = ("ZD", "Z0", "ZF", "RG", "E6")
# ── КЛАСС «РОД» ──────────────────────────────────────────────────────────────────────────────
# ⚠ ОТБОР ФОРМ УЖЕСТОЧЁН ПОСЛЕ ПРОВЕРКИ ИСПОЛНЕНИЕМ, И ПЕРВАЯ РЕДАКЦИЯ БЫЛА НЕГОДНОЙ. Она брала
# любую форму на -л/-ла, у которой в окне ±40 знаков стояло «я», — и считала третьи лица
# («Габриэль пожал плечами») и СУЩЕСТВИТЕЛЬНЫЕ на -л («Пьющий пепел»). На главе `f2274720c9` она
# давала 2ж/5м там, где настоящих форм 1 л. три и все мужские. ⇒ местоимение обязано стоять в
# синтаксической связи: сразу перед глаголом (через 03 служебных слова) или сразу после него.
_ADV = (r"(?:не|уже|бы|тоже|также|всё|ещё|лишь|только|чуть|едва|снова|вновь|даже|так|там|тут|"
r"потом|тогда|сразу|вдруг|наконец|почти|словно|будто|нехотя|молча|тихо|быстро)\s+")
_FORM = r"[а-яё]+л(?:ась|ся|а)?"
_RE_ROD = (re.compile(rf"\\s+(?:{_ADV}){{0,3}}({_FORM})\b", re.I),
re.compile(rf"\b({_FORM})\s+(?:{_ADV}){{0,1}}я\b", re.I))
_FEM = ("ла", "лась")
# ── КЛАСС «ЯЗЫК» ─────────────────────────────────────────────────────────────────────────────
_RE_LAT = re.compile(r"[A-Za-z][A-Za-z'\-]+")
# ⚠ Римская цифра — НЕ кусок исходного языка: «Людовик VIII» законен в русской прозе и стоит во
# всех армах одинаково. Считать её браком значило бы вписать в счётчик константу шума.
_RE_ROMAN = re.compile(r"^[IVXLCDM]+$")
# ⛔ ЛАТИНИЦА В РАЗРЯДКУ — ОТДЕЛЬНЫЙ ДЕТЕКТОР, И ЕГО НЕ БЫЛО. Первая редакция `_RE_LAT` требовала
# от слова ДВУХ букв подряд, а `P A T I E N C E` — восемь одиночных букв через пробел: класс «язык»
# был к ней слеп по построению. Поймано чтением улик: `RG` единственный сохранил авторскую разрядку
# главы `197f98eb61` — и сохранил её ПО-АНГЛИЙСКИ, то есть приём удержал, а слово не перевёл.
# Без этого детектора свод печатал бы «приём цел, языка нет» и хвалил бы арм за непереведённое слово.
_RE_LAT_SP = re.compile(r"(?:(?<=\s)|^)(?:[A-Za-z]\s){2,}[A-Za-z](?=[\s.,!?:;—»]|$)")
# ── КЛАСС «ПРИЁМ» ────────────────────────────────────────────────────────────────────────────
# Разрядка: три и более одиночных буквы через пробел. Ищется В ВЫХОДЕ по факту наличия ЛЮБОЙ
# разрядки, а не по совпадению букв: русское слово другое, приём тот же.
_RE_SPACED = re.compile(r"(?:(?<=\s)|^)(?:[A-Za-zА-Яа-яЁё]\s){2,}[A-Za-zА-Яа-яЁё](?=[\s.,!?:;—]|$)")
def etalon() -> dict:
if not ETALON.exists():
raise SystemExit(f"⛔ эталона нет ({ETALON.name}). Он снимается ПО ИСХОДНИКУ и ДО замера — "
"снять его после того, как счётчик показал числа, значит подогнать")
return json.loads(ETALON.read_text(encoding="utf-8"))
def _paras(t: str) -> list[tuple[int, str]]:
"""(смещение, абзац). Абзац — строка: русская вёрстка реплик поабзацная."""
out, pos = [], 0
for line in t.split("\n"):
if line.strip():
out.append((pos, line))
pos += len(line) + 1
return out
def _speech_spans(t: str) -> list[tuple[int, int]]:
"""Куски текста, лежащие в ПРЯМОЙ РЕЧИ, а не в повествовании.
Правило механическое и объявлено ДО замера: абзац-реплика начинается с тире; каждое
«закрывающее» тире (`, `, `. `, `? `, `! `, ` `) ПЕРЕКЛЮЧАЕТ слой. Куски чётные
речь, нечётные слова автора, то есть повествование. Нет ни одного тире речью считается
весь абзац.
ПЕРВАЯ РЕДАКЦИЯ БЫЛА НЕГОДНОЙ, И ПОЙМАЛО ЕЁ ЧТЕНИЕ УЛИК ГЛАЗАМИ, А НЕ ГЕЙТ. Она обрывала
речь на ПЕРВОМ тире и всё дальнейшее звала повествованием то есть не знала ВОЗОБНОВЛЕНИЯ
реплики, а русская вёрстка диалога именно чередуется: « речь, слова автора. речь». Цена
ошибки замерена: на главе `49ca859c94` реплика Жан-Франсуа «как и я, мадемуазель Кастия. Нет,
я сбежал в Огюстен» ложилась в ПОВЕСТВОВАНИЕ, и мужская форма мужского говорящего шла в
дефекты рассказчицы у ВСЕХ ТРЁХ армов сразу. Это класс «прибор производит брак вместо того,
чтобы его находить»; общий знаменатель парного контраста не портил, но абсолютные числа врал.
"""
out = []
for pos, line in _paras(t):
s = line.lstrip()
if not s.startswith(("", "", "-")):
continue
head = pos + (len(line) - len(s))
cuts = [m.start() + 1 for m in re.finditer(r"[,.!?…»]\s*[—–]\s", s)]
b = [0, *cuts, len(s)]
out += [(head + b[k], head + b[k + 1]) for k in range(len(b) - 1) if k % 2 == 0]
return out
def rod_hits(text: str) -> list[dict]:
"""Все формы 1 л. ед. ч. прош. вр. с родом, местом и классом «речь/повествование»."""
sp = _speech_spans(text)
seen, out = set(), []
for rx in _RE_ROD:
for m in rx.finditer(text):
i = m.start(1)
if i in seen:
continue
seen.add(i)
w = m.group(1).lower()
lo, hi = max(0, i - 60), min(len(text), m.end(1) + 30)
out.append({"i": i, "слово": w,
"род": "ж" if w.endswith(_FEM) else "м",
"речь": any(a <= i < b for a, b in sp),
"ктx": text[lo:hi].replace("\n", " ")})
return sorted(out, key=lambda x: x["i"])
def rod_defects(text: str, et: dict) -> list[dict]:
"""Попадания, чей род ЭТАЛОНУ ПРОТИВОРЕЧИТ. Два ПОДКЛАССА, и они не пересекаются.
ПОДКЛАСС «ЭТАЛОН» работает там, где пол голоса внутреннего рассказа ПО ОКНУ УСТАНОВЛЕН.
Тогда каждая форма слоя повествования обязана нести именно его. Это сильная проверка.
ПОДКЛАСС «РАЗНОБОЙ» работает там, где пол по окну НЕ устанавливается, и он ИСТОЧНИК-
НЕЗАВИСИМ. Окно в 1600 знаков часто не даёт улики вовсе; арм видел ровно то же окно и большего
знать не мог, поэтому винить его за выбор рода нельзя. Но нельзя и не винить за ДВА рода
сразу в одном слое одной главы: это ошибка внутренняя, для неё исходника не требуется. Ровно
её владелец увидел сам («дрейф пола Рейн между отрывками, у всех четырёх армов») и пометил
как то, «что счётчик не увидит». Теперь увидит.
ПРЯМАЯ РЕЧЬ проверяется СЛАБО и это объявляется: говорящих несколько, дефектом считается
только род, не принадлежащий НИ ОДНОМУ объявленному говорящему. А если хоть у одного из них
пол по окну не устанавливается, речь не проверяется ВОВСЕ иначе законная форма неизвестного
говорящего пошла бы в дефекты, и счётчик стал бы производить брак вместо того, чтобы его
находить.
"""
voice = et.get("род_голоса")
speakers = et.get("говорящие", [])
allowed = ({voice} | {s.get("род") for s in speakers}) - {None, "unknown"}
# ⛔ РЕЧЬ НЕ ПРОВЕРЯЕТСЯ, ЕСЛИ ХОТЬ ОДИН ГОВОРЯЩИЙ ГЛАВЫ НЕ ИМЕЕТ ОБЪЯВЛЕННОГО ПОЛА — включая
# САМОГО РАССКАЗЧИКА. Первая редакция этого не делала, и вот что выходило: на главе
# `5a8f48d24a` пол голоса по окну не устанавливается, в списке остались только Персиваль и
# Вульфрик (оба «м»), и собственная реплика рассказчицы «— Где я была?» шла в дефекты как
# «род ж не принадлежит ни одному говорящему главы». Прибор ловил не арм, а сам себя.
speech_blind = voice in (None, "unknown") or any(
s.get("род") in (None, "unknown") for s in speakers)
bad, narr = [], []
for h in rod_hits(text):
if h["речь"]:
if allowed and not speech_blind and h["род"] not in allowed:
bad.append(dict(h, подкласс="речь",
почему=f"род {h['род']} не принадлежит ни одному говорящему главы "
f"{sorted(allowed)}"))
continue
narr.append(h)
if voice in ("ж", "м") and h["род"] != voice:
bad.append(dict(h, подкласс="эталон",
почему=f"повествование: голос рассказа {voice}, форма {h['род']}"))
if voice not in ("ж", "м") and len({h["род"] for h in narr}) > 1:
# ⚠ Дефектом считается МЕНЬШИНСТВО форм, а не все: разнобой из 3ж+1м есть одна ошибка,
# а не четыре, и мерить его надо так же, как подкласс «эталон», иначе агрегат перекосит.
few = min({"ж", "м"}, key=lambda g: sum(1 for h in narr if h["род"] == g))
for h in narr:
if h["род"] == few:
bad.append(dict(h, подкласс="разнобой",
почему="в слое повествования одной главы стоят ОБА рода; пол по "
"окну не устанавливается, поэтому дефект — меньшинство форм"))
return bad
def lang_defects(text: str, et: dict) -> list[dict]:
"""Куски исходного языка. Законные иноязычные вставки эталона и римские цифры не в счёт."""
# ⚠ БЕЛЫЙ СПИСОК — НЕ «всё французское», а РОВНО вставная реплика. Основание — боевой
# пар-мандат `en-ru/translator.md`, секция ФРАНЦУЗСКИЙ СЛОЙ: имена и топонимы ТРАНСКРИБИРУЮТСЯ,
# мадемуазель/месье/шато идут РУССКОЙ традицией галлицизмов, и чужими остаются только вставные
# французские РЕПЛИКИ. Значит `château` или `Capitaine` латиницей в русском выходе есть тот
# самый дефект, который класс ловит, а не законная вставка.
ok = {w.lower() for ins in et.get("латиницааконна", []) for w in _RE_LAT.findall(ins)}
out, taken = [], []
for m in _RE_LAT_SP.finditer(text):
w = m.group(0)
flat = w.replace(" ", "")
taken.append((m.start(), m.end()))
if flat.lower() in ok or _RE_ROMAN.match(flat):
continue
lo, hi = max(0, m.start() - 45), min(len(text), m.end() + 25)
out.append({"i": m.start(), "слово": w, "ктx": text[lo:hi].replace("\n", " "),
"почему": "латиница В РАЗРЯДКУ: авторский приём удержан, слово НЕ переведено"})
for m in _RE_LAT.finditer(text):
w = m.group(0)
if w.lower() in ok or _RE_ROMAN.match(w) or any(a <= m.start() < b for a, b in taken):
continue
lo, hi = max(0, m.start() - 45), min(len(text), m.end() + 25)
out.append({"i": m.start(), "слово": w, "ктx": text[lo:hi].replace("\n", " "),
"почему": "латиница вне вставной французской реплики"})
return sorted(out, key=lambda x: x["i"])
def priem_defects(text: str, et: dict) -> list[dict]:
"""Авторский типографский приём исходника, которого в выходе НЕТ."""
if not et.get("приёмы"):
return []
if _RE_SPACED.search(text):
return []
return [{"i": -1, "слово": "", "ктx": "; ".join(x["текст"] for x in et["приёмы"]),
"почему": "разрядка исходника в выходе отсутствует"}]
def bank_defects(src: str, text: str) -> list[dict]:
return [{"i": -1, "слово": g, "ктx": "", "почему": "канонная форма банка не доехала"}
for g in R.C.canon_gaps(src, text)]
CLASSES = ("род", "язык", "приём", "банк")
def defects(src: str, text: str, et: dict) -> dict:
return {"род": rod_defects(text, et), "язык": lang_defects(text, et),
"приём": priem_defects(text, et), "банк": bank_defects(src, text)}
# ── СВОД ─────────────────────────────────────────────────────────────────────────────────────
def collect(arms: tuple = PANEL) -> tuple[list, dict]:
et_all = etalon()
R.PAIRS[PAIR]["wire"]()
us, data, empty = [], {}, []
for u in R.chosen(PAIR):
if u["uid"] not in et_all:
continue
tx = {a: R.arm_text(PAIR, a, u["uid"]) for a in arms}
# ⛔ ГЛАВА С ПУСТЫМ ТЕКСТОМ ХОТЬ У ОДНОГО АРМА ИЗ СЧЁТА ВЫБРАСЫВАЕТСЯ, И ВСЛУХ. Пустой текст
# дефектов не имеет по построению, то есть арм с пропавшей клеткой выглядел бы ЛУЧШИМ.
# Ровно так мина двух контуров чуть не подарила черновику «чистую» главу (см. rol._wire).
if not all(tx[a].strip() for a in arms):
empty.append((u["uid"], [a for a in arms if not tx[a].strip()]))
continue
us.append(u)
for a in arms:
data[(u["uid"], a)] = defects(u["source"], tx[a], et_all[u["uid"]])
if empty:
print(f" ⛔ ГЛАВ ВЫБРОШЕНО ИЗ СЧЁТА (пустой текст у арма): {len(empty)}")
for uid, aa in empty:
print(f" {uid}: пусто у {aa}")
return us, data
def report(arms: tuple = PANEL) -> int:
et_all = etalon()
us, data = collect(arms)
print(f"\n=== СЧЁТНЫЙ ПРИБОР ПАНЕЛИ-0 · пара {PAIR} · глав {len(us)} · армы {' '.join(arms)} ===")
print("⚠ первичная величина — ЧИСЛО ГЛАВ С ≥1 ФАТАЛОМ (агрегат по четырём классам);\n"
" по-классовые разбивки вторичны; ранги этой панели объявлены ОПИСАТЕЛЬНЫМИ заранее\n")
for cl in CLASSES:
print(f" {'глава':12s}" + "".join(f"{a:>6s}" for a in arms) + f" класс «{cl}»")
for u in us:
row = [len(data[(u["uid"], a)][cl]) for a in arms]
mark = "" if len(set(row)) == 1 else ""
print(f" {u['uid']:12s}" + "".join(f"{x:6d}" for x in row) + mark)
print(f" {'ИТОГО':12s}"
+ "".join(f"{sum(len(data[(u['uid'], a)][cl]) for u in us):6d}" for a in arms) + "\n")
print(f" {'глава':12s}" + "".join(f"{a:>6s}" for a in arms) + " ВСЕ КЛАССЫ (фаталов)")
tot = {a: 0 for a in arms}
chap = {a: 0 for a in arms}
for u in us:
row = []
for a in arms:
n = sum(len(data[(u["uid"], a)][c]) for c in CLASSES)
tot[a] += n
chap[a] += n > 0
row.append(n)
print(f" {u['uid']:12s}" + "".join(f"{x:6d}" for x in row))
print(f" {'фаталов':12s}" + "".join(f"{tot[a]:6d}" for a in arms))
print(f" {'глав с ≥1':12s}" + "".join(f"{chap[a]:6d}" for a in arms) + f" из {len(us)}")
# ⚠ Сколько попаданий сменило класс «речь/повествование» между армами — цена механического
# правила. Печатается всегда, а не только когда мала.
moved = 0
for u in us:
cls = [{h["слово"]: h["речь"] for h in rod_hits(R.arm_text(PAIR, a, u["uid"]))}
for a in arms]
keys = set().union(*cls)
moved += sum(1 for k in keys if len({c[k] for c in cls if k in c}) > 1)
print(f"\n ⚠ попаданий, сменивших класс «речь/повествование» между армами: {moved}")
def _test(title: str, val) -> None:
print(f"\n {title}")
for i, a in enumerate(arms):
for b in arms[i + 1:]:
d = [val(u["uid"], a) - val(u["uid"], b) for u in us]
nz = [x for x in d if x]
p = ZS._sign_p(d) # noqa: SLF001
print(f" {a} {b}: сумма {sum(d):+4d} · глав с разницей {len(nz):2d} "
f"(в пользу {b}: {sum(1 for x in nz if x > 0)}) · p={p:.4f}"
f"{' РАЗЛИЧИМО' if p < 0.05 else ''}")
def _all(uid: str, a: str) -> int:
return sum(len(data[(uid, a)][c]) for c in CLASSES)
# ⛔ ПЕРВИЧНОЕ ПРАВИЛО ПЕЧАТАЕТСЯ ПЕРВЫМ И ПЕЧАТАЕТСЯ ВСЕГДА, каким бы ни вышло. Оно объявлено
# п.3б плана ДО чисел: величина — БИНАРНАЯ «есть ли в главе хоть один фатал», тест — парный
# знаковый. Подменить его на счётный, увидев, что счётный красивее, значит подогнать правило
# под результат; счётный идёт НИЖЕ и назван вторичным, как и объявлено.
_test("① ПЕРВИЧНОЕ ПРАВИЛО п.3б — бинарно «глава с ≥1 фаталом», парный знаковый:",
lambda uid, a: int(_all(uid, a) > 0))
_test("② вторичное — ЧИСЛО фаталов на главу:", _all)
for cl in CLASSES:
_test(f"③ вторичное, по классу «{cl}»:", lambda uid, a, c=cl: len(data[(uid, a)][c]))
# ── ЧАСТОТА С ИНТЕРВАЛОМ, А НЕ ВЕРДИКТ ──────────────────────────────────────────────────
# ⛔ Требование плана 22.08 §4: «редкий класс (ломается в ~10% глав) при 15 главах Гу НЕ
# РАЗРЕШАЕТСЯ — по редким печатать ЧАСТОТУ С ИНТЕРВАЛОМ, а не вердикт». Здесь оно и исполняется.
# ⚠ И печатается ПОТОЛОК МОЩНОСТИ знакового теста: при всех расхождениях в одну сторону
# двусторонний p = 2/2^k, то есть p<0.05 недостижим, пока глав с ненулевой разницей меньше
# шести. Класс, который ломается на трёх главах из шестнадцати, эта панель не разрешит НИКОГДА,
# как бы односторонен ни был результат. Число печатается, чтобы «не различимо» не читалось как
# «одинаково».
k = 1
while 2 / 2 ** k >= 0.05:
k += 1
print(f"\n ④ ЧАСТОТА ГЛАВ С ДЕФЕКТОМ (доля из {len(us)}) и 95% интервал Уилсона.")
print(f" ⚠ потолок мощности: знаковый тест даёт p<0.05 не раньше, чем на {k} главах с "
f"ненулевой разницей, ВСЕ в одну сторону (2/2^{k} = {2 / 2 ** k:.4f}).")
print(f" {'класс':8s}" + "".join(f"{a:>22s}" for a in arms))
for cl in (*CLASSES, "ВСЕ"):
line = f" {cl:8s}"
for a in arms:
n = len(us)
x = sum(1 for u in us if (len(data[(u["uid"], a)][cl]) if cl != "ВСЕ"
else _all(u["uid"], a)) > 0)
ph, z = x / n, 1.96
den = 1 + z * z / n
c = (ph + z * z / (2 * n)) / den
hw = z * ((ph * (1 - ph) / n + z * z / (4 * n * n)) ** 0.5) / den
line += f"{f'{x}/{n} [{max(0, c - hw):.2f};{min(1, c + hw):.2f}]':>22s}"
print(line)
print(f"\n ⚠ ПОПАДАНИЙ ВСЕГО (не глав) — описательно, парности не имеет:")
for cl in CLASSES:
print(f" {cl:8s}" + "".join(
f"{sum(len(data[(u['uid'], a)][cl]) for u in us):>22d}" for a in arms))
print(f"\n эталон: {ETALON.name} · глав в эталоне {len(et_all)}")
return 0
def hits(arm: str, uid: str = "") -> int:
"""Все попадания с контекстом — чтобы улики читались ГЛАЗАМИ, а не принимались на слово."""
et_all = etalon()
R.PAIRS[PAIR]["wire"]()
for u in R.chosen(PAIR):
if u["uid"] not in et_all or (uid and not u["uid"].startswith(uid)):
continue
et = et_all[u["uid"]]
t = R.arm_text(PAIR, arm, u["uid"])
d = defects(u["source"], t, et)
n = sum(len(v) for v in d.values())
print(f"\n{'=' * 100}\n{u['uid']} арм {arm} фаталов {n} "
f"эталон: рассказчик {et.get('род_голоса')} · "
f"говорящие {[(s['имя'], s['род']) for s in et.get('говорящие', [])]}")
for h in rod_hits(t):
print(f" род [{h['род']}] {'речь ' if h['речь'] else 'повес'} "
f"{h['слово']:14s}{h['ктx']}")
for cl in CLASSES:
for x in d[cl]:
print(f"{cl:6s} {x['слово'][:24]:24s} {x['почему']}")
return 0
def cmd_selftest() -> int:
bad = 0
def ck(n: str, ok: bool, d: str = "") -> None:
nonlocal bad
bad += not ok
print(f"[{'OK ' if ok else 'ПРОВАЛ'}] {n}" + (f" {d}" if d else ""))
# ── 1. ОТБОР ФОРМ. Гейт стоит на том, чем первая редакция болела.
for txt, want in (("Габриэль пожал плечами. — Я рассказал ей всё.", ["рассказал"]),
("Фиби. Пьющий пепел. Битва при Санкте.", []),
("Я поймала себя на мягкой улыбке.", ["поймала"]),
("— Дело не в Диор, — прорычал я. — Поедешь с нами.", ["прорычал"]),
("Это была молитва, поняла я.", ["поняла"]),
("Он повесил голову, и я отвернулся к лошади.", ["отвернулся"])):
got = [h["слово"] for h in rod_hits(txt)]
ck(f"отбор форм: {txt[:44]!r}", got == want, f"нашлось {got}, ожидалось {want}")
# ── 2. РЕЧЬ ПРОТИВ ПОВЕСТВОВАНИЯ — механическое правило на наблюдённых формах панели.
for txt, want in (("— Дело не в Диор, — прорычал я. — Поедешь с нами.", False),
("— Я велел тебе сказать мэтру, что вернусь.", True),
("Я подошёл к Хоакину и сжал его плечо.", False),
("— Где я была? — Глаза мои сузились.", True),
("— Благого рассвета, брат. — Я кивнул здоровяку.", False),
# ⛔ ВОЗОБНОВЛЕНИЕ РЕПЛИКИ ПОСЛЕ СЛОВ АВТОРА — на этом первая редакция и
# ломалась: третий кусок абзаца снова речь, а не повествование.
("— Вряд ли, — фыркнул Жан-Франсуа. — Нет, я сбежал в Огюстен.", True),
("— Да, — сказал он, — но я ушёл первым.", True)):
h = rod_hits(txt)
ck(f"речь/повествование: {txt[:44]!r}", bool(h) and h[0]["речь"] == want,
f"{'речь' if h and h[0]['речь'] else 'повествование'}, ожидалось "
f"{'речь' if want else 'повествование'}")
# ── 3. ⛔ ФАЛЬСИФИКАЦИЯ: счётчик ОБЯЗАН найти брак, ПОДСАЖЕННЫЙ в заведомо чистый текст.
# Гейт, который зелен только на здоровом входе, не сторожит ничего — этим был болен селфтест
# Б8: он фильтровал арм по наличию файла клетки, файлов не было, и гейт зеленел вхолостую.
et_ok = {"род_голоса": "ж", "говорящие": [],
"латиницааконна": ["oui"], "приёмы": [{"текст": "P A T I E N C E"}]}
clean = "Я поймала себя на улыбке.\nЭто была молитва, поняла я.\nОн сказал oui, и я ушла.\nТ Е Р П Е Н И Е"
ck("фальсификация: чистый текст даёт НОЛЬ фаталов",
sum(len(v) for v in defects("", clean, et_ok).values()) == 0,
str({k: [x["слово"] for x in v] for k, v in defects("", clean, et_ok).items() if v}))
for name, sick, cl in (
("род сломан в повествовании", clean.replace("Я поймала", "Я поймал"), "род"),
("род сломан вторым местом", clean.replace("поняла я", "понял я"), "род"),
("непереведённый кусок исходника", clean.replace("и я ушла", "и я ушла, swift as silver"), "язык"),
("разрядка потеряна", clean.replace("\nТ Е Р П Е Н И Е", ""), "приём"),
# ⛔ приём УДЕРЖАН, но слово НЕ ПЕРЕВЕДЕНО — латиница в разрядку. Первая редакция
# класса «язык» этого не видела: её регекс требовал двух букв подряд.
("разрядка удержана, но по-английски",
clean.replace("Т Е Р П Е Н И Е", "P A T I E N C E"), "язык")):
got = defects("", sick, et_ok)
ck(f"фальсификация: счётчик ЛОВИТ «{name}»", bool(got[cl]),
f"класс «{cl}»: {[x['слово'] or x['почему'] for x in got[cl]] or 'ПУСТ — брак не пойман'}")
# обратная сторона: объявленная эталоном вставка браком НЕ считается
ck("законная иноязычная вставка эталона браком НЕ считается",
not lang_defects("Он сказал oui.", et_ok))
ck("римская цифра браком НЕ считается", not lang_defects("Людовик VIII умер.", et_ok))
# ── 4. ЭТАЛОН ПОКРЫВАЕТ ПАНЕЛЬ, И КАЖДАЯ ГЛАВА В НЁМ ОБЪЯВЛЕНА ЯВНО.
# ⚠ «Главы нет в эталоне» обязано быть ПРОВАЛОМ, а не тихим пропуском: `collect` отбирает
# только главы эталона, и молчаливая недостача урезала бы панель ровно тем классом, против
# которого весь план и написан.
if not ETALON.exists():
ck("эталон снят и лежит рядом", False, f"нет файла {ETALON.name}")
else:
et_all = etalon()
R.PAIRS[PAIR]["wire"]()
us = [u["uid"] for u in R.chosen(PAIR)]
ck("эталон покрывает ВСЕ главы панели", all(u in et_all for u in us),
f"нет в эталоне: {[u for u in us if u not in et_all]}")
need = ("род_голоса", "говорящие", "латиницааконна", "приёмы", "определим_по_окну")
miss = {u: [k for k in need if k not in et_all.get(u, {})] for u in us if u in et_all}
ck("у каждой главы эталона объявлены все четыре поля",
not any(miss.values()), str({u: v for u, v in miss.items() if v}))
ck("род рассказчика объявлен ЗНАЧЕНИЕМ, а не пропуском",
all(et_all[u].get("род_голоса") in ("ж", "м", None) for u in us if u in et_all),
str({u: et_all[u].get("род_голоса") for u in us
if u in et_all and et_all[u].get("род_голоса") not in ("ж", "м", None)}))
print(f"\n{'ПРИБОР ГОДЕН' if not bad else f'ПРОВАЛОВ: {bad}'}")
return bad
if __name__ == "__main__":
a = sys.argv[1:] or ["--report"]
if a[0] == "--selftest":
sys.exit(1 if cmd_selftest() else 0)
elif a[0] == "--report":
sys.exit(report(tuple(x for x in a[1:] if x.isupper()) or PANEL))
elif a[0] == "--hits":
sys.exit(hits(a[1], a[2] if len(a) > 2 else ""))
else:
raise SystemExit(f"⛔ неизвестный режим {a[0]!r}; знаю --selftest --report --hits")