Freeze the third-edition role-topology pre-registration before paid calls: declared contrast family, computed power, re-declared ceilings, purchase order
This commit is contained in:
parent
fcdab81a88
commit
769a2c8451
11 changed files with 1838 additions and 560 deletions
|
|
@ -1,324 +1,615 @@
|
|||
# 21. Топология ролей перевода: чем платим за второй проход и есть ли ему замена
|
||||
|
||||
**Полигон-сессия 06.08.2026.** Исполнение `docs/POLYGON_ROLE_TOPOLOGY_SESSION_PROMPT.md`
|
||||
(оркестратор №15, санкция владельца 06.08, D39.108). Зона: `eval/role_topology/` + этот файл +
|
||||
пинг в PROGRESS «Полигон». Потолки предварительные: Ф0 ≤$0.70 · Ф1 ≤$1.00 · Ф2а ≤$2.50 · Ф2б ≤$3.00.
|
||||
**Полигон-сессия, вторая редакция 07.08.2026.** Исполнение `docs/POLYGON_ROLE_TOPOLOGY_SESSION_PROMPT.md`
|
||||
(оркестратор №15, санкция владельца 06.08 D39.108; расширение и санкция на траты 07.08).
|
||||
Зона: `eval/role_topology/` + этот файл + пинг в PROGRESS «Полигон».
|
||||
|
||||
> **СТАТУС: Ф0 ИСПОЛНЕНА ЧАСТИЧНО, ПЛАТНЫЕ ФАЗЫ НЕ ЗАПУСКАЛИСЬ. Потрачено $0.**
|
||||
> Разделы §0–§2 — пре-регистрация и результаты бесплатной метрологии. Блокер запуска назван в §0.2.
|
||||
> **СТАТУС: замер закончен, вторая редакция.** Первая редакция (06.08) снята целиком: её риг
|
||||
> измерял ПОЗИЦИЮ варианта в промпте, а не качество арма (§3). Ниже — только текущее состояние.
|
||||
> Пре-рег второй редакции зафризен коммитом `d472599` **до первой платной покупки**.
|
||||
> Потрачено второй редакцией **$1.683796** (пак целиком, обе редакции — **$4.306626**);
|
||||
> потолки второй редакции соблюдены, потолки ФАЗ из промта — нет (§2.7).
|
||||
> Числа сверяются `eval/role_topology/verify_report.py` — 44 проверки, ненулевой код = не сдавать.
|
||||
|
||||
---
|
||||
|
||||
## ИТОГ (для оркестратора)
|
||||
## ИТОГ — вход для решения владельца
|
||||
|
||||
*Заполняется после Ф2. Сейчас закрыты только вопросы метрологии — см. §2.*
|
||||
⚠ **Решения на этих числах принимать нельзя.** Пак даёт согласованные НАПРАВЛЕНИЯ, но ни один
|
||||
контраст не проходит поправку Holm на пять сравнений (лучший Holm 0.0555), шумовой пол пайплайна
|
||||
не измерен вовсе, и три из четырёх «значимых» контрастов несёт один судья — аварийная лёгкая
|
||||
замена. Что нужно доснять, чтобы получить решаемый ответ, — §8.
|
||||
|
||||
**Что уже решено данными и меняет план пака:**
|
||||
**Единственное направление, устойчивое ко всему, что проверялось:** второй проход поверх дешёвого
|
||||
черновика покупает качество на zh (+1.88). Переживает выброс одной и двух единиц (0 из 15 и 0 из
|
||||
105), исключение единиц с механическим конфаундом (+1.65), не объясняется длиной; подтверждено
|
||||
батареей (утечка ханьцзы 24→0) и слепым чтением. Ограничения: значимость несёт gemini (у luna
|
||||
+0.84 [−0.72, +2.31]); Holm не проходит; покупается ЯЗЫК и ТЕРМИН, ВЕРНОСТЬ ноль не покидает.
|
||||
|
||||
1. **Детектор «выдуманное слово» построен и работает** (§2.1): precision 0.78 / recall 0.70 на
|
||||
2177 размеченных словотипах против **1.00 / 0.10** у боевых чекеров, вне выборки 6/6. Это не
|
||||
починка бага, а закрытие дыры, которую движок объявил сам: `sanitizer.go:429` дословно —
|
||||
«detection needs a morphology pass (Ф2), so it stays silent here».
|
||||
2. **Детектор «смысловая инверсия» через QE-ранкер — ОТРИЦАТЕЛЬНЫЙ РЕЗУЛЬТАТ** (§2.2), проверенный
|
||||
контрольным декоем. Следствие для плана: арм C фазы 2б в режиме «реальные детекторы» по классу
|
||||
инверсий питать нечем; развилка вынесена владельцу в §0.3.
|
||||
3. **Три метрики из шести, которые я построил по букве промта, мерили шум и были сняты замером**
|
||||
(§2.3). Это цена, которую платит любая батарея без прогонки на чистом тексте, и она названа
|
||||
числами, а не общими словами.
|
||||
**Не установлено — «однопроходка проигрывает связке».** Измерен арм D, то есть однопроходка
|
||||
С МАНДАТОМ, который этот же отчёт признаёт вредным. Рекомендуемая однопроходка — D_; её контраст
|
||||
с A не куплен, косвенная оценка −0.67 (p≈0.30). Плюс контраст `D/A` смешивает три фактора: файл
|
||||
промта, наличие черновика на входе и закон-блок (у D он есть, у A нет).
|
||||
|
||||
---
|
||||
**Не установлено — «второй проход покупает качество на en» (+1.00).** ДИ накрывает ноль при
|
||||
выбрасывании одной единицы в 8 случаях из 12; перестановочный p=0.073; нижняя граница +0.04
|
||||
зависит от розыгрыша bootstrap (ноль накрывается у 16.5% сидов).
|
||||
|
||||
## §0. ПРЕ-РЕГИСТРАЦИЯ
|
||||
|
||||
Фриз коммитом ДО первого платного вызова. Изменения после фриза = новый experiment-ID; девиации —
|
||||
явным списком в §5. Amend фриза запрещён.
|
||||
|
||||
### 0.1. Вопрос и что считается ответом
|
||||
|
||||
Какая топология целевая — «черновик + переписыватель», «черновик + гейты + точечный ремонт»,
|
||||
«однопроходка сильной моделью», гибрид с маршрутизацией — и кто жильцы ролей.
|
||||
|
||||
**Ответом считается** таблица «арм → детерминированная батарея · MQM-lite двух судей · цена за
|
||||
принятую 1000 слов (p50/p95) · дрейф на связном отрезке», где различия названы ОТНОСИТЕЛЬНО порога
|
||||
различимости Ф0.6, а не абсолютно. **Ответом НЕ считается** ранжирование армов по среднему баллу
|
||||
судьи: эксп-20 §5.4 намерил, что судья согласен сам с собой на идентичном входе в 56% клеток и
|
||||
разниц меньше ~2:1 не разрешает.
|
||||
|
||||
Армы равноправны. «Правильного ответа», которого ждут, не существует; при неразличимости дефолт —
|
||||
самый дешёвый и детерминированный арм.
|
||||
|
||||
### 0.2. Материал — ОТКРЫТЫЙ БЛОКЕР
|
||||
|
||||
Промт заказывает невиданный срез: свежая zh-вебновелла без опубликованного ru-перевода, рассеянные
|
||||
окна для дисперсии + связный отрезок 8–10 глав для дрейф-метрик.
|
||||
|
||||
**Такого текста в дереве нет.** Инвентарь `~/books` с провенансом по докам: `gu-zhenren` (蛊真人) —
|
||||
претрейн-канон, ровно тот случай, который строка 55 бэклога объявляет негодным дословно;
|
||||
`jinpingmei` — минский байхуа, PD; `isekai_majutsushi_jp` — ja, не zh (exp11 §41);
|
||||
`fifty_shades_1_en`, `Empire_of_the_Dawn` — en. Каталог `eval/data/samples/webnovel/zh/`, куда
|
||||
харнесс `webnovel_slice.py:14` ждёт корпус, никогда не создавался — строка 55 висит именно поэтому.
|
||||
То, что exp07 называл «невиданным вебновелл-текстом», — это само 蛊真人 (`07-coverage-precision.md:85`),
|
||||
и его же хвост предписывает «перепроверить **на корпусе владельца**».
|
||||
|
||||
**Норма при этом однозначна:** корпус кладёт владелец (`webnovel_slice.py:1-3` — «запускается по
|
||||
появлению корпуса владельца»). Полигон текст не добывает.
|
||||
|
||||
**Развилка передана владельцу 06.08** (варианты: владелец кладёт файлы · санкционирует добычу
|
||||
полигоном · Ф2 идёт на 蛊真人 с объявленной границей). До ответа платные фазы не стартуют.
|
||||
**Оговорка о свежести, которую важно не потерять:** дата публикации контаминацию не снимает —
|
||||
катофы моделей разные. Чище всего берутся последние главы ИДУЩЕГО сериала (июнь–август 2026), и
|
||||
контаминация всё равно меряется (continuation/cloze + поиск fan-переводов), а не предполагается.
|
||||
|
||||
### 0.3. Армы
|
||||
|
||||
**Ф2а — «переписывание как класс».** Черновики фризятся и подаются идентичными всем армам-редакторам
|
||||
(парный дизайн, McNemar).
|
||||
|
||||
| Арм | Что |
|
||||
|---|---|
|
||||
| F | do-nothing пол: черновик как есть через гейты. Все редакторские армы мерятся против него |
|
||||
| A | байт-боевой бейзлайн flash→dspro (без банкноты — объявленная девиация бейзлайна, не чинить) |
|
||||
| B | связка flash→glm-5-OFF full-regen |
|
||||
| D | однопроходка сильной модели С мандатом перевёрстки |
|
||||
| D′ | она же БЕЗ мандата — деконфаунд «модель против мандата» |
|
||||
|
||||
**Ф2б — ремонт и маршрутизация.** Гейт входа: детекторы Ф0.4 валидированы.
|
||||
|
||||
| Арм | Что |
|
||||
|---|---|
|
||||
| C | черновик (с мандатом перевёрстки) → детекторы+гейты → точечный ремонт дешёвым фиксером; два режима: oracle-флаги (потолок) и реальные детекторы (пол); после каждого фикса полный ре-гейт, кап 2 итерации |
|
||||
| E | обратная связка: сильный черновик → дешёвый фиксер по флагу |
|
||||
| G | guarded+routed: QE/гейты скорят все окна → стиль-пасс ТОЛЬКО флагнутым, правка принимается только если внешний гейт видит улучшение (max(draft, edited)) |
|
||||
|
||||
**⚠ ПОПРАВКА К АРМАМ C и G ПО РЕЗУЛЬТАТУ Ф0.4 (§2.2).** QE-ранкер локальную смысловую инверсию не
|
||||
детектирует. Следствия, объявленные ДО запуска:
|
||||
- арм **C** гонится в oracle-режиме и в режиме «реальные детекторы», но реальный режим питается
|
||||
ТОЛЬКО детектором выдуманных слов + существующими $0-гейтами; класс «инверсия» в реальном
|
||||
режиме остаётся ненайденным, и разрыв oracle↔реальность печатается как главный результат арма;
|
||||
- арм **G** маршрутизирует не по QE-баллу инверсии, а по батарее Ф0.5 + QE как ГРУБОМУ фильтру
|
||||
(декой доказал: рассогласование сегмента ранкер видит, Δ +7.1) — то есть G ловит «сегмент поехал
|
||||
целиком», а не «сегмент поехал в одном слове». Это сужение арма, и оно объявлено здесь.
|
||||
|
||||
**Reflow-план кодом (Q4c, research/19 §C1.3) — ДИСПОЗИЦИЯ ОТКАЗА.** Не гоню. Основание: эксп-20 §5.2
|
||||
уже проверил гипотезу «мандат в промпте заменяет второй проход» слепым судом и ОПРОВЕРГ её (русский
|
||||
3:12, вёрстка 5:10 в пользу связки), а деконфаунд мандата в этом паке несёт арм D′ — отдельный
|
||||
кодовый reflow-план добавил бы третий способ спросить то же самое при бюджете, которого нет.
|
||||
|
||||
### 0.4. Метрики и пороги
|
||||
|
||||
Порядок первичности задан не вкусом, а измеренным шумом инструментов.
|
||||
|
||||
1. **Детерминированная батарея Ф0.5 — первична.** Шума не имеет: повтор даёт то же число.
|
||||
Состав и границы каждой проверки — §2.3.
|
||||
2. **MQM-lite двух судей** — счёт типизированных ошибок (спан+тип+severity на 1000 слов).
|
||||
Pairwise-преференс — только для стиль-осей. Судья не судит армы своего семейства-жильца;
|
||||
вердикты раскладываются по семействам судей. Агрегация Bradley–Terry с bootstrap-CI.
|
||||
3. **Цена за принятую 1000 слов** — p50/p95, с кэшем/батчем/ретраями; перезаписанные вызовы тоже
|
||||
деньги (леджер = нижняя граница).
|
||||
4. **Дрейф на связном отрезке** — швы, ты/вы-стабильность, рост банка.
|
||||
|
||||
**Порог различимости берётся из Ф0.6 и записывается ДО Ф2.** Правило вердикта: «арм X бьёт Y, если
|
||||
перевес по оси больше порога Ф0.6»; средние исходы = «неопределённо»; при неразличимости побеждает
|
||||
самый дешёвый и детерминированный арм.
|
||||
|
||||
### 0.5. Прогнозы (сверка с фактом идёт в отчёт)
|
||||
|
||||
Записаны до запуска, чтобы отчёт мог показать, где я ошибся.
|
||||
|
||||
| # | Прогноз | На чём основан |
|
||||
| Вопрос | Ответ | Оговорка |
|
||||
|---|---|---|
|
||||
| П1 | F (do-nothing) НЕ будет последним; по общему вердикту он в пределах порога от A | эксп-20: на dspro второй проход отрицателен (общий 5:1 в пользу одного прохода) |
|
||||
| П2 | B (glm-5) выиграет русскую прозу и проиграет верности/термам | эксп-20 §5: у glm свип вёрстки 6:0, у dspro верность 5:1 |
|
||||
| П3 | D (однопроходка сильной) выиграет качеством и проиграет ценой ≥2× | цена сильного тира против flash |
|
||||
| П4 | G даст лучшее отношение цена/качество среди всех армов | он единственный не платит за неизменённое |
|
||||
| П5 | Разрыв oracle↔реальные детекторы в арме C будет БОЛЬШЕ, чем разрыв между армами Ф2а | recall детектора слов 0.70, детектора инверсий нет вовсе |
|
||||
| П6 | Батарея разведёт армы там, где судья скажет «неразличимо» | у батареи шум ноль, у судьи 56% самосогласия |
|
||||
| Нужен ли второй проход | Да по направлению, у трёх инструментов | значимость несёт один судья; Holm не проходит |
|
||||
| Не выгоднее ли сразу сильной моделью | **Нет** — единственный вывод, устойчивый к смене судьи | Holm не проходит (p=0.021, Holm 0.063) |
|
||||
| Кто жилец роли редактора | `deepseek-v4-pro`: качество неотличимо от `glm-5`, цена вдвое ниже | — |
|
||||
| Что покупает второй проход | **ЯЗЫК** (+1.11) и **ТЕРМИН** (+0.45), не верность | §2.1а |
|
||||
| Что делает мандат перевёрстки | бьёт по **ФОРМЕ** (−0.47), прочие оси нулевые | контраст `D/D_` изолирует мандат НЕ чисто (§3) |
|
||||
| Что покупает закон-блок | **ВЕРНОСТЬ** +0.63 [+0.33, +0.97] | суммарный перевес +0.33 гасит это осями |
|
||||
|
||||
### 0.6. Смета
|
||||
**Цена (p50 / p95, промт требовал оба).** Редакторский проход `deepseek-v4-pro` $0.00388 / $0.00518;
|
||||
однопроходка $0.00487 / $0.00792; `glm-5` в роли редактора $0.00892 / $0.00956. На хвосте разрыв
|
||||
между связкой и однопроходкой растёт, а не исчезает — «равная цена» была свойством медианы. ⚠ Цена черновика в цену связки **не сложена**: армы правят
|
||||
замороженный черновик из корпуса пакета-6, чья цена не сохранена и чей производитель по записи
|
||||
не устанавливается (§2.6). Цифра «связка дешевле/дороже однопроходки» из этого замера НЕ следует.
|
||||
|
||||
Ф0 ≤$0.70 (предзамер судей ≤$0.60) · вахта эффорта ≤$0.05 · Ф1 ≤$1.00 · Ф2а ≤$2.50 · Ф2б ≤$3.00.
|
||||
Живые стопы механизмом + проекционный гард. Подъём или пере-нарезка — только словом владельца ДО
|
||||
запуска фазы. Не влезает — стоп и пинг, не резать молча.
|
||||
**Четыре ограничения вывода.** (1) Материал zh моделью узнаётся (4/5); пара en чище (1/5) и даёт
|
||||
то же направление. (2) Дрейф на связном отрезке глав не мерен. (3) Армы C/E/G не гнались. (4) Ни
|
||||
один результат не переживает поправку на множественность — нужен либо больший n, либо сужение
|
||||
таблицы до заранее объявленного одного контраста.
|
||||
|
||||
### 0.7. Что считается провалом фазы
|
||||
## §0 Пре-рег второй редакции (зафризен `d472599` до трат)
|
||||
|
||||
Ф0.6 не даёт порога различимости в потолке → СТОП и пинг с расчётом мощности, а не молчаливое
|
||||
уменьшение числа окон. Детекторы Ф0.4 не валидируются → арм C идёт только oracle-режимом, и это
|
||||
записывается как граница, а не как результат.
|
||||
**Вопрос.** Какая топология целевая и кто жильцы ролей. Армы равноправны, «правильного ответа»
|
||||
не существует, работа — замер.
|
||||
|
||||
**Материал.** Две пары, книги из `~/books` (в git не попадают; в отчёт печатаются только метрики).
|
||||
|
||||
| Пара | Книга | Контаминация (собственная проба) | Единиц |
|
||||
|---|---|---|---|
|
||||
| zh→ru | 蛊真人 (gu-zhenren) | узнавание **4/5**, клоуз 1/5 — книга модели ИЗВЕСТНА | 16 |
|
||||
| en→ru | Kristoff, «Empire of the Dawn» | узнавание **1/5**, клоуз 0/5 — чисто | 12 |
|
||||
|
||||
Порог пробы объявлен там же: «узнавание ≥3/5 = книга известна»; контроль пробы — 金瓶梅
|
||||
(4/5 и 3/5). Основной материал порог **превышает** — объявленное ограничение, а не умолчание;
|
||||
пара en заведена именно чтобы вывод не стоял на одной узнанной книге.
|
||||
|
||||
Отбор единиц детерминирован: середина распределения длин, дедуп по нормализованной подписи
|
||||
(снимаются заголовок главы и пробелы), ключ артефакта — хеш источника, не позиционный индекс.
|
||||
Максимальная попарная близость 16 отобранных zh-единиц — 0.060. Пара en стратифицирована:
|
||||
6 единиц с французским слоем (≥4 токена с диакритикой, ≥2 разных) и 6 без него.
|
||||
|
||||
**Армы.**
|
||||
|
||||
| Арм | Что | Закон-блок | Мышление |
|
||||
|---|---|---|---|
|
||||
| F | do-nothing пол: черновик `deepseek-v4-flash` как есть | — | — |
|
||||
| A | байт-боевой бейзлайн: черновик → `deepseek-v4-pro` | нет (промт стр.29) | вендор-дефолт |
|
||||
| A_law | то же С законом — деконфаунд закон-блока | да | вендор-дефолт |
|
||||
| B | черновик → `glm-5` | да | **OFF** (промт стр.62) |
|
||||
| D | однопроходка `deepseek-v4-pro` С мандатом перевёрстки | да | вендор-дефолт |
|
||||
| D_ | она же БЕЗ мандата — деконфаунд «модель против мандата» | да | вендор-дефолт |
|
||||
|
||||
Контрасты: `A/F` · `B/F` · `D/A` · `D/D_` · `A_law/A`. Каждый изолирует ровно один фактор —
|
||||
проверено сборкой сообщений: у A и A_law совпадают system[0] и user, различие только в наличии
|
||||
закон-блока; у D и D_ совпадают закон-блок и user, различается только мандат. Текст закона
|
||||
пинится промтом: `HEADER_LAW_PLAIN`, **без оговорки области**.
|
||||
|
||||
Пара en идёт **без закон-блока у всех армов** — объявленная девиация: подписанного глоссария для
|
||||
книги нет, а выдумывать канон полигон не вправе. Следствие: контрасты пары мерят чистую
|
||||
топологию, фактор банка мерится на zh контрастом `A_law/A`.
|
||||
|
||||
**Критерии.** Вердикт единицы засчитывается, если знак перевеса сохранился в **обоих порядках**
|
||||
у **обоих судей**. Основная статистика — непрерывный перевес по единицам с bootstrap-ДИ
|
||||
(5000 ресемплов, сид 20260807); порог — «интервал не накрывает ноль».
|
||||
|
||||
**Потолки, объявленные до трат:** армы zh ≤$0.70 · судейство zh ≤$1.70 · армы en ≤$0.55 ·
|
||||
судейство en ≤$1.45 · проба судьи-замены ≤$0.10. Соблюдены все (§2.7).
|
||||
|
||||
---
|
||||
|
||||
## §1. Что уже стоит на дереве ($0)
|
||||
## §1 Метрология: три независимых инструмента
|
||||
|
||||
| Файл | Что делает | Проверен |
|
||||
Первая редакция мерила одним инструментом, и когда у него нашёлся отказ, проверить его оказалось
|
||||
нечем. Здесь три контура, устроенные так, чтобы не делить общий слепой участок.
|
||||
|
||||
**(1) Судьи по ключам.** `gpt-5.6-luna` + `gemini-3.1-flash-lite`, оба вне семейств, населяющих
|
||||
армы (deepseek, glm). Протокол — счёт типизированных ошибок по осям ВЕРНОСТЬ/ТЕРМИН/ЯЗЫК/ФОРМА,
|
||||
слепые метки, персист каждого голоса отдельным файлом.
|
||||
|
||||
*Раскладка.* Порядок — внешний цикл: на каждую клетку (контраст × единица × судья) ровно по
|
||||
одному голосу каждого порядка. Перевес единицы = среднее двух зеркальных голосов, позиция
|
||||
вычитается тождественно. Проверка исправности — равенство числа голосов по порядкам (фактически 156/156 zh и
|
||||
98/98 en; 160/160 — это плановая сетка, а не факт), а НЕ малость позиционной форы: зеркало убирает позицию из перевеса арма, но не из
|
||||
среднего по всем голосам.
|
||||
|
||||
*Позиционная фора измерена и велика:* **zh +3.60**, **en +1.45** ошибки в пользу первого варианта.
|
||||
Это масштаб, с которым сравнивается перевес арма: на zh перевес +1.88 МЕНЬШЕ форы — потому
|
||||
небалансированная раскладка первой редакции и давала ложные вердикты.
|
||||
|
||||
*Замена второго судьи — внешний блокер, объявляю.* `grok-4.3` выпал на HTTP 403 «used all
|
||||
available credits or reached its monthly spending limit» (250 голосов). Замена выбрана по трём
|
||||
условиям промта (не из семейства армов, не из семейства первого судьи, влезает в объявленный
|
||||
потолок) и проведена через **положительный контроль до трат** (D39.46б): на контрольном декое
|
||||
пака поймала посаженную деградацию **6/6 в обоих порядках** со зеркальными счетами
|
||||
(В1=0/В2=3 и В1=3/В2=0). Оговорка: это лёгкая модель, её согласие с luna не эквивалентно прежней
|
||||
паре luna+grok.
|
||||
|
||||
*Отказы фильтра.* 10 клеток zh пришли **без объекта сообщения**, `finish_reason` =
|
||||
`content_filter: PROHIBITED_CONTENT` — составная строка, описанная в quirks 00 (D22.6, D22.8а).
|
||||
Все 10 — на одной единице. **Уточнение к quirks:** оговорка «на violence/SFW Gemini-судья жив»
|
||||
на вебновелле держится не целиком — отказ приходит выборочно и молча, поэтому Google непригоден
|
||||
как ЕДИНСТВЕННЫЙ второй контур на этом материале.
|
||||
|
||||
*Шум судьи — посылка промта стр.15, проверена.* Пере-замер на клетках разброса: абсолютный счёт
|
||||
точно повторился в **1 клетке из 6**, разброс 2.5 при уровне 7.7 (≈32% относительного шума).
|
||||
Посылка подтверждена. ⚠ Клейм первой редакции «судьи не выдумывают перевес на идентичных текстах,
|
||||
0 из 18» **снят как тавтология**: когда в обе позиции подан побайтно один текст, совпадение счёта
|
||||
гарантировано построением, а не свойством судьи.
|
||||
|
||||
**(2) Детерминированная батарея.** 9 проверок, судья не нужен, шума нет. Две во второй редакции
|
||||
переписаны, потому что печатали числа из одних ложных срабатываний:
|
||||
|
||||
* `check_numbers` — китайская сторона теперь собирает составное числительное целиком, как русская
|
||||
(прежде 两千三百 давало {2000, 300} против «две тысячи триста» = {2300}, то есть на безупречном
|
||||
переводе печатались две потери и одна выдумка); добраны собирательные («десятки тысяч», «более
|
||||
сотни»); заведён закрытый список неколичественных оборотов (`百分之一` — доля, не «сто»).
|
||||
Проверка синтетикой с известным ответом: **16/16**.
|
||||
* `check_gender` — кандидатом считается только форма имени, у которой есть именительный разбор:
|
||||
косвенная форма по определению не подлежащее, и соседний глагол согласован с другим словом
|
||||
(«Воля Фан Юаня **была** тверда»). Все 6 «рассогласований» прошлого прогона были этим классом.
|
||||
|
||||
**(3) Слепое чтение.** Метки армов сняты, порядок вариантов перемешан детерминированным ключом от
|
||||
uid единицы, карта раскладки хранится отдельно и читателям не выдаётся. Читатели — другого
|
||||
семейства, чем судьи по ключам, и задача другая: не «посчитай ошибки по осям», а «прочитай как
|
||||
редактор издательства и назови места цитатой».
|
||||
|
||||
⚠ **Дефект инструкции, объявляю:** буквы вариантов перемешиваются на каждой единице, а читателям
|
||||
это сказано не было, и они построили сквозные «профили А–Е». Профили выброшены как артефакт;
|
||||
поединичные ранги расшифрованы по карте и использованы. В инструкцию читателя это надо вписать.
|
||||
|
||||
---
|
||||
|
||||
## §2 Результаты
|
||||
|
||||
### 2.1 zh→ru, судьи (16 единиц, 320 голосов)
|
||||
|
||||
```
|
||||
контраст единиц за перв. за втор. ничья перевес 95% ДИ
|
||||
A против F 15 3 0 12 +1.88 [+0.72, +3.02] ← не накрывает ноль
|
||||
B против F 15 5 0 10 +2.33 [+0.85, +3.95] ← не накрывает ноль
|
||||
D против A 15 0 1 14 −1.72 [−2.95, −0.50] ← не накрывает ноль
|
||||
D против D_ 15 0 1 14 −1.05 [−2.18, −0.07] ← не накрывает ноль
|
||||
A_law против A 15 0 0 15 +0.33 [−0.57, +1.18]
|
||||
```
|
||||
|
||||
Одна единица выпала из вердиктов: на ней 10 отказов фильтра у второго судьи. Счёт побед почти
|
||||
весь в ничьих, потому что правило «знак совпал в обоих порядках у обоих судей» очень строгое;
|
||||
непрерывный перевес с интервалом информативнее и заказан промтом (стр.32).
|
||||
|
||||
|
||||
### 2.1а Разбор таблицы: по судьям, по осям, с поправкой на множественность
|
||||
|
||||
⚠ **Эффект, который несёт один судья, есть свойство судьи.** Раздельно:
|
||||
|
||||
```
|
||||
контраст gpt-5.6-luna gemini-3.1-flash-lite
|
||||
A против F +0.84 [−0.72, +2.31] +3.03 [+2.17, +3.90] *
|
||||
B против F +1.44 [−0.34, +3.41] +3.33 [+1.97, +4.73] *
|
||||
D против A −1.94 [−3.62, −0.16] * −1.20 [−2.17, −0.37] * ← оба
|
||||
D против D_ −0.38 [−2.16, +1.25] −1.60 [−2.73, −0.70] *
|
||||
A_law против A +0.91 [−0.41, +2.25] −0.20 [−0.87, +0.47]
|
||||
```
|
||||
|
||||
Устойчив к смене судьи ровно один контраст — `D против A`.
|
||||
|
||||
⚠ **Поправка на множественность.** Точный знаковый перестановочный тест по единицам + Holm по
|
||||
семейству из пяти контрастов:
|
||||
|
||||
```
|
||||
A против F p=0.0111 Holm=0.0555
|
||||
B против F p=0.0117 Holm=0.0555
|
||||
D против A p=0.0209 Holm=0.0626
|
||||
D против D_ p=0.0933 Holm=0.1865
|
||||
A_law против A p=0.5001 Holm=0.5001
|
||||
```
|
||||
|
||||
**Ни один контраст не проходит 0.05 после поправки.** ДИ в §2.1 не скорректированы и читаются как
|
||||
описание величины, а не как тест. Формально значимых результатов пак не даёт; даёт согласованные
|
||||
направления при n=15.
|
||||
|
||||
**Разложение по осям** — суммарный перевес гасит разнонаправленные эффекты, поэтому он менее
|
||||
информативен, чем покомпонентный:
|
||||
|
||||
```
|
||||
контраст ВЕРНОСТЬ ТЕРМИН ЯЗЫК ФОРМА
|
||||
A против F +0.23 [−0.22,+0.72] +0.45 [+0.13,+0.77]* +1.11 [+0.52,+1.80]* +0.10 [−0.13,+0.33]
|
||||
B против F +0.85 [+0.18,+1.50]* +0.52 [+0.17,+0.85]* +1.08 [+0.47,+1.83]* −0.12 [−0.77,+0.30]
|
||||
D против A −0.48 [−1.02,+0.07] −0.33 [−0.73,+0.05] −0.57 [−1.02,−0.15]* −0.33 [−0.52,−0.15]*
|
||||
D против D_ +0.03 [−0.60,+0.60] −0.28 [−0.58,+0.03] −0.33 [−0.88,+0.18] −0.47 [−0.82,−0.13]*
|
||||
A_law против A +0.63 [+0.33,+0.97]* +0.02 [−0.32,+0.32] −0.18 [−0.53,+0.15] −0.13 [−0.40,+0.08]
|
||||
```
|
||||
|
||||
Каждый фактор попадает в свою ось: второй проход покупает **ЯЗЫК** и **ТЕРМИН**, а не верность;
|
||||
мандат перевёрстки бьёт по **ФОРМЕ** и только по ней; закон-блок покупает **ВЕРНОСТЬ** (+0.63), и
|
||||
именно этот эффект терялся в суммарном +0.33.
|
||||
|
||||
### 2.2 en→ru, судьи (12 единиц, 192 голоса) — переносится ли вывод на другую пару
|
||||
|
||||
```
|
||||
контраст ед. за перв. за втор. ничья перевес 95% ДИ фр-страта plain
|
||||
A против F 12 2 0 10 +1.00 [+0.04, +1.79] +0.33 +1.67
|
||||
B против F 12 3 0 9 +1.31 [+0.35, +2.25] +1.75 +0.88
|
||||
D против A 12 0 1 11 −0.81 [−2.58, +0.73] +0.21 −1.83
|
||||
D против D_ 12 2 0 10 +0.48 [−0.83, +1.69] +1.04 −0.08
|
||||
```
|
||||
|
||||
**Направление переносится, величина — нет.** Второй проход покупает качество на обеих парах.
|
||||
Преимущество связки над однопроходкой на en вдвое меньше и ноль не покидает: на паре, где
|
||||
исходник уже гипотактичен, разница между топологиями сжимается. Мандат перевёрстки на zh вредит
|
||||
значимо (−1.05), на en ноль не покидает — это и есть содержание вывода «мандат = пар-данные».
|
||||
|
||||
### 2.3 Слепое чтение (третий контур, независимо от судей)
|
||||
|
||||
**zh, 16 единиц, два читателя:**
|
||||
|
||||
| арм | ср. ранг | первых мест | последних | против черновика F |
|
||||
|---|---|---|---|---|
|
||||
| A | 2.56 | 3 | 1 | 12 побед / 4 |
|
||||
| A_law | 2.62 | 5 | 0 | 13 / 3 |
|
||||
| B | 3.31 | 3 | 2 | 11 / 5 |
|
||||
| D_ | 3.31 | 5 | 4 | 8 / 8 |
|
||||
| F | 4.06 | 0 | 4 | — |
|
||||
| D | 5.12 | 0 | 5 | 5 / 11 |
|
||||
|
||||
**en, 12 единиц:** D_ 2.50 · B 2.58 · D 3.00 · A 3.08 · F 3.83. Черновик последний, первых мест 0.
|
||||
|
||||
**Чтение согласуется с судьями по всем четырём значимым контрастам:** черновик хуже связок;
|
||||
однопроходка с мандатом на zh хуже всех и хуже самого черновика; закон-блок неразличим
|
||||
(2.56 против 2.62); на en порядок армов другой и мандат перестаёт быть решающим.
|
||||
|
||||
Наблюдение обоих читателей, независимо: **низ рейтинга почти всегда решает механический слой, а
|
||||
не перевод** — диалоговая типографика (прямая речь в кавычках вместо тире, авторский текст под
|
||||
тире, две реплики в одном абзаце), слипшиеся абзацы, непереведённые остатки (`cultivation`,
|
||||
`特产ом`, китайские заголовки), протёкшая markdown-разметка. Класс ловится детерминированно.
|
||||
|
||||
### 2.4 Детерминированная батарея, zh (16 единиц)
|
||||
|
||||
```
|
||||
арм ед. типогр. ханьцзы потеря вел. ты/вы микс род свер. род ошиб.
|
||||
F 16 1 24 6 5 111 1
|
||||
A 16 2 0 5 6 99 1
|
||||
B 16 0 2 6 5 112 1
|
||||
D 16 2 0 5 5 86 0
|
||||
D_ 16 2 2 6 4 94 0
|
||||
A_law 16 2 11 7 6 103 1
|
||||
```
|
||||
|
||||
Единственная ось, где армы расходятся уверенно, — **утечка ханьцзы**: черновик несёт 24 знака,
|
||||
редакторский проход вычищает до 0–2. Это независимое от судьи подтверждение, что второй проход
|
||||
что-то делает. Оставшиеся 11 у A_law вскрыты поединично: непереведённый заголовок главы
|
||||
`第二十五节:春光正明媚` и буква разряда `丙`; тот же заголовок стоит в черновике, арм без закон-блока
|
||||
его вычистил, арм с закон-блоком — нет. Поединичный промах, не системная протечка: проверено, что
|
||||
закон-блок даёт в промпт всего 5 иероглифов. По остальным осям армы неразличимы — печатается как
|
||||
есть, не интерпретируется.
|
||||
|
||||
### 2.5 Французский слой (en, 6 единиц страты `fr`, судья не нужен)
|
||||
|
||||
```
|
||||
арм фр-токенов латиница передано расщеплено
|
||||
F 18 0 7 0
|
||||
A 18 0 6 0
|
||||
B 18 0 7 1
|
||||
D 18 0 5 0
|
||||
D_ 18 0 6 0
|
||||
```
|
||||
|
||||
Латиничных утечек нет ни у одного арма, расщепление формы имени внутри окна — один случай.
|
||||
Класс **имён и топонимов армы не разделяет**: `François` → Франсуа, `Rive Cœur` → Рив-Кёр,
|
||||
`Fabién` → Фабьен одинаково у всех.
|
||||
|
||||
Разделяет другое, и видно это только чтением: **офранцуженное нарицательное**. `château` в трёх
|
||||
разных фрагментах раскалывает армы одинаково — «замок» против «шато». Зеркальный случай: арм,
|
||||
лучше всех прочитавший французское ИМЯ дворца, хуже всех прочитал английское НАРИЦАТЕЛЬНОЕ
|
||||
`silversaint`, превратив титул в фамилию «Сильверсейнт». Ошибка включается на словах, которые
|
||||
*выглядят* именем. Третий класс — вставные французские реплики (`oui`, `gens d'armes`, `Vampyr!`)
|
||||
— разводит армы по стратегии, а не по качеству: сохранить латиницей · перевести · транскрибировать
|
||||
кириллицей; третий путь порождает несуществующие русские слова («капитэны», «уи»).
|
||||
|
||||
### 2.6 Экономика
|
||||
|
||||
⚠ **Нога черновика не восстановима, и это ограничивает раздел.** Армы A/A_law/B правят
|
||||
ЗАМОРОЖЕННЫЙ черновик из корпуса пакета-6 (`~/books/gu-zhenren/labels/raw/corpus.jsonl`). В записи
|
||||
корпуса нет поля модели, usage не сохранён, цена того вызова утрачена. Черновики, купленные этим
|
||||
паком (`bo2-DRAFT-*`, $0.00104 медиана), — это ПЕРЕ-ЗАМЕР цены роли на тех же исходниках, а не
|
||||
нога данной связки: побайтно они с корпусными не совпадают ни на одной из 16 единиц (максимальная
|
||||
близость 0.195). Складывать их в одну цифру нельзя — найдено адверсариальным ревью 07.08.
|
||||
|
||||
| Арм | $/единицу (собственный вызов) |
|
||||
|---|---|
|
||||
| F | — (цена утрачена, см. выше) |
|
||||
| A | 0.00388 |
|
||||
| A_law | 0.00410 |
|
||||
| D | 0.00487 |
|
||||
| D_ | 0.00533 |
|
||||
| B | 0.00892 |
|
||||
|
||||
Сравнение «связка против однопроходки по цене» из этого замера **не выводится**: у связки две
|
||||
ноги, и вторая не измерена. Что выводится: редакторский проход `deepseek-v4-pro` стоит $0.00388,
|
||||
то есть дешевле одного прохода однопроходки ($0.00487) — при том что редактор читает И исходник,
|
||||
И черновик, а однопроходка только исходник; `glm-5` в той же роли вдвое дороже ($0.00892) при
|
||||
неотличимом качестве.
|
||||
|
||||
**Пере-замер цены черновой роли (самостоятельное число).** На 16 единицах zh — медиана $0.00104
|
||||
с учётом ре-генов; **ре-ген потребовался на 6 единицах из 16 = 38%** (одна — дважды) против
|
||||
документированных в quirks 15%; ожидаемая цена черновика с учётом ре-генов $0.00121, а не $0.00104. На 12 единицах en — 0 ре-генов. Эхо-мина подтверждённо привязана
|
||||
к плотному CJK, а её частота — измеримое свойство материала, а не константа.
|
||||
|
||||
### 2.7 Деньги
|
||||
|
||||
**Пак целиком (обе редакции): $4.306626.** Вторая редакция — $1.683796; снятая первая — $2.622830
|
||||
(её артефакты сохранены как сырьё снятого замера и в выводы не входят, но деньги потрачены).
|
||||
|
||||
| Блок второй редакции | Потрачено | Потолок |
|
||||
|---|---|---|
|
||||
| `eval/role_topology/list_models.py` | живой листинг моделей по 7 ключам; слаги для пре-рега снимаются в день запуска | исполнением, §2.4 |
|
||||
| `eval/role_topology/detect_word.py` | детектор «выдуманное слово», 4 накопительных слоя | `selfcheck.py`, 12 пинов |
|
||||
| `eval/role_topology/align.py` | монотонное выравнивание zh↔ru (Гейл–Чёрч), $0, без моделей | `selfcheck.py`, 5 пинов |
|
||||
| `eval/role_topology/qe_bench.py` | обёртка MetricX-24 в reference-free режиме + оконный мини-бенч | `selfcheck.py --qe`, 3 пина |
|
||||
| `eval/role_topology/qe_segment.py` | посегментный QE + контрольный декой | исполнением, §2.2 |
|
||||
| `eval/role_topology/battery.py` | детерминированная батарея, 9 проверок | 16 пинов правил + прогон по 91 единице |
|
||||
| `eval/role_topology/selfcheck.py` | ревью исполнением всего харнесса; ненулевой код = харнесс не годен | сам |
|
||||
| армы zh | $0.469169 | $0.70 |
|
||||
| судейство zh | $0.739291 | $1.70 |
|
||||
| армы en | $0.188682 | $0.55 |
|
||||
| судейство en | $0.283024 | $1.45 |
|
||||
| проба судьи-замены | $0.003630 | $0.10 |
|
||||
| **итого вторая редакция** | **$1.683796** | — |
|
||||
|
||||
Потолки второй редакции соблюдены. ⚠ **Потолки ФАЗ из промта — нет.** Ф2а как стадия (обе
|
||||
редакции) = $2.5862 при потолке $2.50; Ф1 = $1.0747 при потолке $1.00, причём 99% этого потрачено
|
||||
ДО подъёма потолка. Обе строки относятся к первой редакции и объявлены здесь потому, что деньги
|
||||
пака считаются по паку, а не по редакции.
|
||||
|
||||
Касса общая (`buy.Ledger`): леджер читается **с диска** по префиксам фазы, гард **проекционный**
|
||||
(`потрачено + ожидание` против потолка; ожидание = p95 уже купленных вызовов той же роли). Каждая
|
||||
запись несёт `usage` и `total_tokens`, поэтому цена пере-считывается из токенов;
|
||||
`verify_report.independent_price` считает её вторым путём, с правилом биллинга, выписанным заново
|
||||
по quirks 00 — иначе ошибка ПРАВИЛА проходит обе стороны сверки незамеченной.
|
||||
|
||||
Класс биллинга Gemini (`additive_total`, D22.3) реализован. ⚠ Величину недоучёта первой редакции
|
||||
восстановить **нельзя**: `total_tokens` тогда не персистился.
|
||||
|
||||
---
|
||||
|
||||
## §2. Ф0 — метрология (исполнено, $0)
|
||||
## §3 Девиации текущей редакции
|
||||
|
||||
### 2.1. Детектор «выдуманное слово»: построен, базлайн бит
|
||||
Читается вместе с §2: каждая девиация ограничивает то, как читать соответствующее число.
|
||||
|
||||
**Земля.** `~/books/gu-zhenren/labels/labels/k3.jsonl` — 2177 размеченных словотипов из шести
|
||||
реальных прогонов, 10 помечены `defect`. Разметка сделана ЧУЖОЙ сессией (пакет-6, 26.07) до и вне
|
||||
этого экспа ⇒ требование «разметка вторым контуром» выполнено построением, а не моей рукой.
|
||||
| Девиация | Причина | Что ограничивает |
|
||||
|---|---|---|
|
||||
| Пара en идёт **без закон-блока** у всех армов | подписанного глоссария для книги не существует, выдумывать канон полигон не вправе | контрасты en мерят чистую топологию; фактор банка мерится только на zh (`A_law/A`) |
|
||||
| Пара en потребовала **своего пар-пакета промптов** (`prompts/en-ru/`) | боевой промт редактора лежит в `backend/prompts/zh-ru/` и несёт китаеспецифику в 4 местах | сравнение пар корректно лишь настолько, насколько пар-пакеты эквивалентны вне этих 4 мест (§5.2) |
|
||||
| Второй судья — `gemini-3.1-flash-lite` вместо `grok-4.3` | кредиты xAI исчерпаны (HTTP 403 на 250 голосах) | лёгкая модель; согласие с luna не эквивалентно прежней паре luna+grok |
|
||||
| `REPS_PER_ORDER = 1` | мощность добрана единицами (8→16), а не повторами | разброс повтора внутри клетки не оценивается; оценивается разброс по единицам (bootstrap) |
|
||||
| Одна единица zh выпала из вердиктов | 10 отказов фильтра у второго судьи на ней | таблица §2.1 идёт на n=15, а не 16 |
|
||||
| Три единицы en судились после смены набора | отбор был недетерминирован и починен по ходу | голоса «осиротевших» единиц лежат на диске и в свод не входят; контроль баланса считает только живые |
|
||||
|
||||
**Базлайн взят из кода, а не из заголовка.** `metrics.json` того же набора даёт боевым чекерам на
|
||||
k3 precision 1.0 / recall 0.1. Чтение `checks/sanitizer.go:408-436` объясняет почему: боевой
|
||||
`detectBrokenWords` ловит ровно два узких класса — невозможные кириллические биграммы с ь/ъ и
|
||||
кириллично-латинские гомоглифы, — а морфологический проход в нём отложен ЯВНО (`:429`
|
||||
«needs a morphology pass (Ф2), so it stays silent here»). ⇒ формулировка «мой детектор бьёт баг»
|
||||
неверна; верная — он закрывает объявленную дыру.
|
||||
**Найдено адверсариальным ревью ВТОРОЙ редакции (07.08), объявляю:**
|
||||
|
||||
| Слой | tp | fp | fn | precision | recall |
|
||||
| Дефект | Следствие | Статус |
|
||||
|---|---|---|
|
||||
| Арм F берёт ТЕКСТ из корпуса пакета-6, а ЦЕНУ — из докупленных черновиков; тексты не совпадают ни на одной единице (близость ≤0.195), поля модели в записи корпуса нет | сложение «черновик + редактор» в одну цену снято, §2.6 переписан | исправлено в отчёте, замер не переделывался |
|
||||
| Ответы слепых читателей не персистировались: модуль умел только выпускать пакеты | §2.3 была невоспроизводима | ранги сохранены в `blind/READINGS.json`, приёмник и `--score` заведены; полные разборы утрачены, требуют ре-рана |
|
||||
| ДИ не скорректированы на пять сравнений | ни один контраст не проходит Holm 0.05 | добавлен §2.1а с перестановочным тестом и Holm |
|
||||
| Суммарный перевес гасит разнонаправленные оси | эффект закон-блока на ВЕРНОСТИ (+0.63) терялся в суммарном +0.33 | добавлено разложение по осям |
|
||||
| Значимость трёх контрастов несёт один судья — аварийная лёгкая замена | «второй проход покупает качество» опирается на один инструмент из двух | добавлен разбор по судьям; вывод переведён в «направление» |
|
||||
| Контраст `D/D_` изолирует мандат НЕ чисто: у D_ своя вёрсточная строка, у D — блок из 4 правил включая не-вёрсточное про чэнъюй, системный промт ×1.92 | −0.47 по ФОРМЕ может нести любой из трёх факторов | объявлено; чистый контраст требует ре-рана с уравненными промптами |
|
||||
| Термин «банкнота» употреблялся для закон-блока; по глоссарию банкнота — канал выноса кандидатов из ответа переводчика, маркера `⟦TM-BANK-v1⟧` нет ни в одном арме | требование промта стр.29 о банкноте НЕ исполнено | переименовано в «закон-блок»; сама банкнота остаётся незамеренной (§4) |
|
||||
| Разброс судьи-замены на повторе не измерен (у неё измерен только декой) | шум второго инструмента неизвестен | открыто (§4) |
|
||||
|
||||
**Первая редакция (06.08) снята целиком.** Причина одна и структурная: её раскладка судей была
|
||||
2:1 вместо зеркальной, а позиционная фора судьи (+4.65 ошибки) превышала измеряемые эффекты —
|
||||
то есть таблица вердиктов отражала позицию варианта в промпте. Дополнительно четыре арма
|
||||
отклонялись от промта необъявленно, и все четыре в одну сторону. Числа первой редакции не
|
||||
подлежат сравнению с числами второй; её артефакты (`bo-*`, `jv-*`) сохранены как сырьё снятого
|
||||
замера.
|
||||
|
||||
## §4 Что осталось незакрытым
|
||||
|
||||
1. **Дрейф на связном отрезке 8–10 глав** — швы между чанками, стабильность ты/вы, рост банка.
|
||||
Промт заказывал; мерится всё на рассеянных единицах. Требует другого масштаба покупки.
|
||||
2. **Армы C (точечный ремонт), E (обратная связка), G (guarded+routed)** — не гнались. Клеймы
|
||||
первой редакции сняты: там C засчитывал «фиксер вернул не ту же строку», а G вообще не гнался
|
||||
как арм — была только арифметическая проекция цены.
|
||||
3. **Слепое чтение владельца на финалистах** (промт стр.72) — не исполнено, требует владельца.
|
||||
4. **Ф1 как отбор жильцов** — скрин из 12 моделей убил 2; состав Ф2 фактически назначен ссылкой
|
||||
на эксп-20. Клейм «Ф1 отобрала жильцов» снят.
|
||||
5. **Оси Ф1, объявленные и не замеренные:** детерминизм повтора при T=0, p95 латентности,
|
||||
batch API, отключаемость размышления как ось скрина.
|
||||
6. **MQM-lite со спанами и severity, нормировка на 1000 слов, Bradley–Terry** — не реализованы;
|
||||
реализован счёт ошибок по осям плюс bootstrap-ДИ по единицам.
|
||||
7. **Терминолог-прогон банка среза** (Ф0.3) — карточки персонажей читаются из подписанного сида
|
||||
(51 ключ), но банк терминов прогоном не строился.
|
||||
8. **Величина недоучёта биллинга Gemini в первой редакции** — невосстановима.
|
||||
9. **Контаминация по моделям, населяющим армы** (`deepseek-v4-pro`, `glm-5`) не мерена: проба
|
||||
сделана одной моделью.
|
||||
10. **Банкнота как канал** (`⟦TM-BANK-v1⟧`, вынос кандидатов из ответа переводчика) не подавалась
|
||||
ни одному арму — требование промта стр.29 не исполнено, эффект канала не измерен.
|
||||
11. **Разброс судьи-замены на повторе** не измерен; у неё проверен только декой.
|
||||
12. **Чистый контраст мандата** — требует ре-рана с уравненными по объёму и составу промптами.
|
||||
13. **Цена черновика, который реально правят армы**, утрачена: усилие «мерить на тех же единицах»
|
||||
было доведено до покупки, но не до подачи купленного в армы.
|
||||
|
||||
---
|
||||
|
||||
## §5 Гипотезы, родившиеся по ходу
|
||||
|
||||
**5.1 Мандат перевёрстки бьёт по ФОРМЕ, и только по ней.** Разложение по осям: zh `D/D_`
|
||||
ФОРМА −0.47 [−0.82, −0.13], ВЕРНОСТЬ/ТЕРМИН/ЯЗЫК ноль не покидают. Фактор попадает в ту ось, на
|
||||
которую нацелен, — это косвенное свидетельство исправности судейского протокола.
|
||||
|
||||
⚠ **Гипотеза «мандат есть пар-данные» НЕ подтверждена и снята до гипотезы.** Я выводил её из того,
|
||||
что на zh эффект значим, а на en нет; это классическая ошибка — разница между значимым и
|
||||
незначимым сама не значима. Прямой тест интеракции: разница эффектов пар −1.53, ДИ [−3.28, +0.22],
|
||||
p≈0.09 — ноль не покидает. Плюс на en отсутствует доза: слияние абзацев мандатом на zh −11.4
|
||||
абзаца, на en −1.07, то есть в ~7 раз меньше при вдвое более узкой шкале эффектов. Данные
|
||||
совместимы с ОДНИМ общим эффектом на обеих парах. Проверка требует отдельного замера с
|
||||
достаточной дозой на второй паре.
|
||||
|
||||
**5.2 Пар-специфика ПРОМПТА редактора измерима и мала; про движок вывода НЕТ.** Диффом
|
||||
`backend/prompts/zh-ru/editor.md` против заведённого `prompts/en-ru/editor.md` — 7 ханков, из них
|
||||
пар-специфичных 4 (меры `时辰`/`成`, рамка «китайский паратаксис», оговорка про построчную разбивку,
|
||||
пример с чэнъюй); знаменатель 42 строки включает блок FEWSHOT, который в прод не уходит.
|
||||
|
||||
⚠ **Клейм «новая пара стоит ~4 клауз, Go править не надо» СНЯТ.** Замер этого не показывает:
|
||||
движок в паке не запускался ни разу, перенесены 3 ролевых промпта из 7 (нет classifier,
|
||||
editor-mono, judge-selector, terminologist, translator-banknote), а `langpack.go` требует для пары
|
||||
набор файлов данных с fail-loud и прямо оговаривает, что новое семейство исходного языка требует
|
||||
новых полей пакета, парсинга и каналов майнера. Проверено только то, что **промпт редактора**
|
||||
переносится правкой четырёх клауз. Полная цена новой пары этим паком не измерена.
|
||||
|
||||
**5.3 Частота эха — измеримое свойство модели на материале, а не константа.** Ре-ген потребовался
|
||||
на 6 единицах из 16 (38%) против документированных 15%; 0 на 12 единицах en. ⇒ Ожидаемая цена черновика
|
||||
обязана включать частоту ре-гена, и она пар-зависима.
|
||||
|
||||
**5.4 Позиционная фора судьи пар-зависима.** zh +3.60, en +1.45 при одной и той же паре судей.
|
||||
Возможная причина — длина и плотность варианта. Если так, парный протокол на длинных единицах
|
||||
систематически шумнее. Не проверено.
|
||||
|
||||
**5.5 Механический слой решает низ рейтинга, а не перевод.** Оба слепых читателя независимо
|
||||
назвали главным источником непечатного текста диалоговую типографику, слипшиеся абзацы,
|
||||
непереведённые остатки и протёкшую markdown-разметку. ⇒ Дешёвый детерминированный пост-процессор
|
||||
снимает большую часть разрыва между армами, чем выбор редактора.
|
||||
|
||||
---
|
||||
|
||||
## §6 Диспозиции строк бэклога
|
||||
|
||||
* **55** (эмпирика на претрейн-классике предварительна) — **частично закрыта**: замер повторён на
|
||||
контаминационно чистом материале (en, узнавание 1/5), направление вывода то же.
|
||||
* **82** (прототип проверки рода) — **закрыт прототипом**: `battery.check_gender` + карточки из
|
||||
подписанного сида, фильтр по именительному падежу; 86–112 сверок на арм на 16 единицах.
|
||||
* **12** (HARD-детектор величин 成/万) — **закрыт прототипом**: `battery.check_numbers`,
|
||||
симметричный разбор обеих сторон, синтетика 16/16.
|
||||
* **46** (Hunspell в движок не строить) — **не задет**: детектор выдуманного слова остаётся
|
||||
полигон-прототипом, движкового решения не предлагается.
|
||||
* **65 / D39.22** (вопросы итерации №2 редакторов) — **не закрыты**: армы C/E/G не гнались.
|
||||
* **106** — не задета этим паком.
|
||||
|
||||
---
|
||||
|
||||
## §8 Что нужно доснять, чтобы числа стали решаемыми
|
||||
|
||||
Пак даёт направления, а не решения. Ниже — минимальный набор покупок, после которого на числах
|
||||
можно принимать перестройку бэкенда. Цены посчитаны по фактической стоимости голоса этого пака
|
||||
($0.0024 в среднем по паре судей; grok — $0.007).
|
||||
|
||||
| # | Что доснять | Зачем | Оценка |
|
||||
|---|---|---|---|
|
||||
| 1 | **Шумовой пол пайплайна**: один арм дважды end-to-end на идентичном входе, 16 единиц | Сейчас нечем отделить разницу топологий от стохастики модели. Заказано промтом (Ф0.6), не исполнено. Без этого перевес ±1.9 не интерпретируется | $0.21 |
|
||||
| 2 | **Контраст `D_` против `A`** прямым судейством | Рекомендуемая однопроходка — БЕЗ мандата, а измерена С мандатом. Косвенная оценка −0.67 (p≈0.30) решения не даёт. Армы уже куплены, нужны только голоса | $0.15 |
|
||||
| 3 | **Уравненные промты `D` и `D_`** и их пере-гон | Контраст мандата смешан с объёмом промта (×1.92) и с не-вёрсточным правилом про чэнъюй | $0.35 |
|
||||
| 4 | **Удвоение единиц до 32** на двух несущих контрастах | При n=15 ничто не проходит Holm; мощность добирается единицами | $0.51 |
|
||||
| 5 | **Третий судья** (`grok-4.3`, нужны кредиты xAI) | Три из четырёх эффектов несёт один судья — лёгкая аварийная замена. Нужен арбитр | $1.12 |
|
||||
| 6 | **Арм F из собственных черновиков**: пере-гон A/A_law/B на купленных черновиках | Сейчас текст арма F и его цена происходят из разных вызовов; провенанс замороженного черновика невосстановим | $0.56 |
|
||||
| | **Итого** | | **$2.90** |
|
||||
|
||||
Без пунктов 1 и 5 числа останутся неинтерпретируемыми независимо от объёма остальных покупок.
|
||||
|
||||
**Бесплатные починки, обязательные до следующей покупки:**
|
||||
|
||||
* гейт ре-гена черновика проверяет ТОЛЬКО письменность исходника; принята и оплачена попытка
|
||||
с долей латиницы 0.79 (английский перевод вместо русского) — гейт обязан проверять и целевую
|
||||
письменность;
|
||||
* из пары en выбросить единицу `129b73ad5a` — это выходные данные и библиография, а не проза;
|
||||
фильтр страты `plain` («нет токенов с диакритикой») притягивает служебные страницы;
|
||||
* карта раскладки слепого чтения лежит в одном каталоге с пакетами — вынести;
|
||||
* судейские ответы содержат только числа (111 знаков), рассуждение не персистируется — аудит-следа
|
||||
от числа к тексту нет; включить сохранение обоснования;
|
||||
* три докстринга противоречат исполняемому коду (`bakeoff.py` про «черновик стоит $0» и про
|
||||
«≥2 повторах из 3», `judges.py` про состав судей).
|
||||
|
||||
**Три провод-факта, добытых паком, ещё не занесены в `00-provider-quirks.md`** (файл вне зоны
|
||||
полигона — вопрос к лендингу): fail-closed Gemini приходит БЕЗ объекта `message`, а не с пустым
|
||||
`content`; `gpt-5.6-luna` без ручки эффорта выжигает бюджет на рассуждение и отдаёт пустой ответ;
|
||||
`glm-5` гасится `extra_body: {"thinking": {"type": "disabled"}}`.
|
||||
|
||||
---
|
||||
|
||||
## §9 Пре-рег ТРЕТЬЕЙ редакции (объявлен до покупок)
|
||||
|
||||
Составлен после рецензии плана §8, проведённой ДО трат. Рецензия пере-считала мощность
|
||||
исполнением и изменила план в трёх местах; ниже — итог, а не первоначальный список.
|
||||
|
||||
**Семейство сравнений объявляется ЗАРАНЕЕ — три первичных контраста:** `A/F`, `B/F`, `D_/A`.
|
||||
Остальные (`D/D_`, `A_law/A`, вся пара en) — описательные, поправке не подлежат и решений не несут.
|
||||
|
||||
⚠ Это НЕ применяется задним числом ко второй редакции. На её данных семейство было из пяти
|
||||
контрастов, и корректный вывод остаётся прежним: ни один не проходит Holm (лучший 0.0555).
|
||||
Сужение семейства после того, как p-значения увидены, есть пост-хок отбор. Для справки: на тех же
|
||||
данных семейство из трёх дало бы 0.0333 всем трём — именно поэтому оно объявляется ВПЕРЁД, а не
|
||||
назад.
|
||||
|
||||
**Мощность посчитана, а не предположена** (знаковый перестановочный тест, Holm, 600 симуляций):
|
||||
|
||||
| n | A/F | B/F | D/A | D/D_ | A_law/A |
|
||||
|---|---|---|---|---|---|
|
||||
| боевые чекеры (их `metrics.json`) | 1 | 0 | 9 | **1.000** | **0.100** |
|
||||
| С0 нет в словаре pymorphy3 | 9 | 34 | 1 | 0.209 | 0.900 |
|
||||
| С1 + вайтлист банка/канона книги | 9 | 28 | 1 | 0.243 | 0.900 |
|
||||
| С2 + палладиевская форма (со склонением) | 8 | 11 | 2 | 0.421 | 0.800 |
|
||||
| **С3 + разложимость на известные части** | **7** | **2** | **3** | **0.778** | **0.700** |
|
||||
| 15 | 0.57 | 0.57 | 0.46 | 0.23 | 0.05 |
|
||||
| 32 | 0.99 | 0.97 | 0.94 | 0.70 | 0.15 |
|
||||
|
||||
Слои накопительные и печатаются по одному — норма D39.46(а) («у каждого фактора обязан быть арм
|
||||
без него») применена к слоям детектора.
|
||||
Объявляется вместе с ограничением: **32 достаточно только если истинный эффект равен
|
||||
наблюдённому.** Если истина у нижней границы ДИ, требуется n≈128–192, а в корпусе после дедупа
|
||||
**всего 60 уникальных единиц** — то есть такой замер этим материалом недостижим, и это надо знать
|
||||
до покупки, а не после. Контраст `A_law/A` не достигает мощности ни при каком доступном n
|
||||
(0.39 при n=80) — единицы под него не покупаются, он остаётся описательным.
|
||||
|
||||
**Выделенная валидация: 6/6** на посадках k1 пробы 18 — независимый набор чужой сессии, в настройке
|
||||
не участвовал. ⚠ Набор ЛЕГЧЕ живого: посадки сконструированы заведомо не-словами. Читать как верхнюю
|
||||
границу.
|
||||
**Потолки третьей редакции** (прежние кассы исчерпаны не полностью: армы $0.469 из $0.70,
|
||||
судейство $0.739 из $1.70): армы ≤$1.60 · судейство ≤$3.20 · проба разброса судьи ≤$0.15.
|
||||
Объявлены ДО фазы, потому что леджеры читают диск по префиксам и новые покупки лягут в те же
|
||||
кассы; без пере-объявления первая же крупная покупка ушла бы в `skipped` с пустым выходом, а свод
|
||||
молча выбросил бы единицу — тот же класс тихого усечения, что снял первую редакцию.
|
||||
|
||||
**Проверка на подгонку.** Свободный параметр слоя С2 (глубина усечения окончания) прогнан по
|
||||
диапазону: k∈{(1,), (1,2), (1,2,3)} при пороге 2 слогов дают ОДНИ И ТЕ ЖЕ 0.778/0.700. Вердикт по
|
||||
параметру не двигается ⇒ подгонки по нему нет.
|
||||
**Порядок покупок задан каскадом зависимостей** — иначе платим дважды:
|
||||
|
||||
**Ущерб от ложного флага — отдельным числом, как требует промт:** из 9 флагов ложных 2 (22%);
|
||||
столько починок арма C правили бы здоровое слово.
|
||||
|
||||
**Дефекты САМОЙ ЗЕМЛИ, найденные проверкой посылки** (пинятся `selfcheck.py`, чтобы отчёт не считал
|
||||
recall по номинальным 10 позитивам):
|
||||
- `вперди` и `силённый` в `corpus.jsonl` ОТСУТСТВУЮТ вовсе, даже подстрокой, хотя README набора
|
||||
объявляет этот файл источником ID («смещения считаны по строкам corpus.jsonl»);
|
||||
- `ной` — валидное русское слово (императив «ныть»); в класс попало из-за разреза токена на границе
|
||||
с иероглифом (`伤ной`), то есть это дефект утечки, а не выдуманное слово. Один из трёх моих
|
||||
«пропусков» — ошибка класса в метке, а не промах детектора. В классе остаётся 9 позитивов;
|
||||
- два выживших ложных срабатывания (`льшим`, `льшую`) — фантомы сборщика пула: комбинирующий знак
|
||||
ударения U+0301 в «бо́льшим» разрезал токен (51 из 53 вхождений идут после «о», 2 — после ударения).
|
||||
|
||||
**⚠ Сужение собственного вывода по итогам Go-оверлея.** Первая формулировка была «диакритика ломает
|
||||
любой словный чекер». Проверка исполнением (оверлей вне репозитория, движок read-only) её сузила:
|
||||
`ExportNormalize` знак снимает корректно, а `SanitizeOutput` на тексте с ударением и без даёт
|
||||
ОДИНАКОВЫЙ вердикт (total=0 в обоих). ⇒ боевой путь не страдает; страдает наивная токенизация в
|
||||
полигонном коде. Моя батарея нормализует до токенизации (§2.3).
|
||||
|
||||
**Оставшаяся слепая зона детектора, названная явно:** `привлеклось` = «при» + «влеклось», обе части
|
||||
настоящие. Класс «валидная приставка + валидное слово, не образующие реального слова» морфологией
|
||||
не берётся — нужен корпусный или языко-модельный сигнал.
|
||||
|
||||
### 2.2. Детектор «смысловая инверсия» через QE: ОТРИЦАТЕЛЬНЫЙ РЕЗУЛЬТАТ
|
||||
|
||||
**Инфраструктура поднята с нуля** (на машине её не было): CometKiwi / xCOMET / wmt23-cometkiwi-xl —
|
||||
все `gated: auto` на HuggingFace, токена у сессии нет; `unbabel-comet` 2.2.7 не импортируется на
|
||||
Python 3.14 (`functools._HashedSeq` удалён). Взята незагейченная **`google/metricx-24-hybrid-large-v2p6`**
|
||||
(mT5-large 1.23B, штатный reference-free режим, CPU-загрузка 7 с). Две несовместимости пойманы
|
||||
исполнением: protobuf 4.25 против py3.14 и обязательный `use_cache=False` против transformers 4.57
|
||||
(с кэшем маска кросс-внимания на токен короче выхода энкодера — падает на любой длине).
|
||||
Отклонение от буквы промта («CometKiwi/xCOMET») объявлено здесь.
|
||||
|
||||
⚠ Шкала MetricX перевёрнута: больше = хуже. Ориентация запинена в `selfcheck.py` — спутать её
|
||||
значит получить детектор, флагающий ровно здоровые сегменты.
|
||||
|
||||
Замерено на ДВУХ размерах модели — заявлять отрицательный результат по младшей, когда доступна
|
||||
старшая, преждевременно.
|
||||
|
||||
| Замер | large (1.23B) | XL (3.7B) | Чтение |
|
||||
| Ярус | Что | Цена | Почему в этом месте |
|
||||
|---|---|---|---|
|
||||
| **уровень окна**, 6 окон × 2 класса | Δ +0.035 (4/6) | — | порчу не видит |
|
||||
| **сегмент**, k1 выдуманное слово | Δ +0.073 (4/6) | Δ +0.981 (4/6) | чувствительность растёт с моделью |
|
||||
| **сегмент**, k2 смысловая инверсия | Δ −0.001 (3/6) | Δ +1.147 (5/6) | то же, но n=6 ⇒ p≈0.11, НЕ установлено |
|
||||
| **абсолютный порог**, 81 здоровый сегмент | лучшая precision 0.17 | лучшая precision **0.16** | детектором быть не может НИ НА ОДНОЙ |
|
||||
| **ДЕКОЙ** (D39.46б), n=69 | Δ +7.107 (65/69) | Δ +7.000 (59/69) | ранкер на этом материале ЧУВСТВИТЕЛЕН |
|
||||
| 0 | $0-починки, пре-рег, потолки | $0 | до всего |
|
||||
| 1 | Разброс судьи-замены на повторе | $0.094 | решает, нужен ли третий судья за $1.6–4.0 |
|
||||
| 1 | Пол рига: `A` против `A′` на замороженном черновике | $0.212 | отрицательный контроль к декою + разделение дисперсии |
|
||||
| 2 | Арм F из собственных черновиков + пере-судейство 4 контрастов | $0.884 | строго ДО добора единиц |
|
||||
| 2 | Уравненные промты `D`/`D_` + пере-гон | $0.451 | строго ДО контраста `D_/A` |
|
||||
| 2 | `D_` против `A` прямым судейством | $0.144 | последний из «армы уже куплены» |
|
||||
| 3 | Добор до 32 единиц по всей таблице | $1.194 | после всех смен армов |
|
||||
| 4 | `grok-4.3` на расходящихся контрастах | $2.00 | только как арбитраж; требует кредитов xAI |
|
||||
|
||||
Декой — несущая часть замера: без него «не увидел посадку» и «слеп на этом материале» неразличимы,
|
||||
а это разные вердикты. Он показывает, что модель работает и формат входа верен (сверен с авторским
|
||||
`metricx24/predict.py:_make_input`).
|
||||
**Три решения рецензии, принятые против первоначального плана:**
|
||||
|
||||
**Главный вывод — не «ранкер плох», а «два режима расходятся».** Абсолютный порог и парное сравнение
|
||||
отвечают на разные вопросы, и это ровно граница между двумя армами фазы 2б:
|
||||
1. **Третий судья — не покупка мощности.** Разложение дисперсии: он снижает стандартную ошибку
|
||||
на 4–8%, что эквивалентно +2.4 единицы за $1.6–4.0; удвоение единиц даёт −31% за $0.51.
|
||||
Разброс несёт разнородность единиц, а не судейский шум. Grok остаётся оправдан как арбитр там,
|
||||
где судьи расходятся (`A/F`: +0.84 против +3.03), и как покрытие молчаливых отказов Gemini.
|
||||
2. **«Шумовой пол пайплайна» переименован в «пол рига» и снят с блокирующих.** При замороженном
|
||||
черновике повтор одного арма меряет стохастику модели в роли редактора, а не пайплайна; и она
|
||||
уже сидит внутри разброса по единицам, который берут bootstrap и перестановочный тест. Ценность
|
||||
пола другая: отрицательный контроль (при отсутствии разницы перевес обязан быть нулём) и
|
||||
разделение дисперсии на «разнородность единиц» против «шум вызова».
|
||||
3. **Покупка «пост-процессор против арма» ($0.289) ОТМЕНЕНА до траты.** Гипотеза была, что
|
||||
дешёвая детерминированная правка механического слоя закроет часть разрыва `A/F`. Замер $0:
|
||||
механический брак действительно сосредоточен в черновике (73 закавыченных реплики против 11–22
|
||||
у армов, 6 markdown-разметок против 0–2, 24 ханьцзы против 0), но доминирующий класс
|
||||
детерминированно НЕ чинится — черновик берёт `«…»` под внутренний монолог, что в русском наборе
|
||||
является нормой, а отличить мысль от произнесённой реплики требует понимания, а не правила.
|
||||
Пост-процессор (`postproc.py`, правила проверены синтетикой 8/8) чинит только разметку и
|
||||
служебную преамбулу — семь случаев на весь корпус, эффект заведомо нулевой.
|
||||
|
||||
- **арм C** ищет дефект в ОДНОМ тексте, образца нет ⇒ ему нужен абсолютный порог. Порога нет:
|
||||
precision ≤0.17 на обеих моделях, баллы здоровых сегментов (медиана 6.1, p90 11.4, макс 16–18)
|
||||
полностью перекрывают баллы испорченных. **Арм C по классу «инверсия» не питаем.**
|
||||
- **арм G** сравнивает ДВЕ версии одного сегмента ⇒ ему нужна парная дельта. Она состоятельна:
|
||||
на механической инверсии полярности (72 пары, построены снятием/вставкой отрицания при личном
|
||||
глаголе) медиана Δ **+1.490**, направление **66/72 = 92%**, знаковый тест **p < 1e-5**.
|
||||
**Арм G питаем.**
|
||||
|
||||
⚠ Граница парного замера объявлена заранее и остаётся в силе: механическая инверсия ЛЕГЧЕ ручной
|
||||
посадки (голое «не» — сильный поверхностный сигнал, а посадки пробы 18 меняли смысл переписыванием
|
||||
оборота). Поэтому 92% читается как ВЕРХНЯЯ граница парного режима; на реальном классе оценка — те
|
||||
самые 5/6 при p≈0.11, то есть не установлено.
|
||||
|
||||
**Следствия для пака** (внесены в §0.3 ДО запуска): арм C в реальном режиме не покрывает класс
|
||||
инверсий и печатает разрыв oracle↔реальность как главный результат; арм G сохраняет свой QE-гейт,
|
||||
но его пропускная способность обязана быть откалибрована на РЕАЛЬНЫХ правках — §2.5.
|
||||
|
||||
### 2.3. Детерминированная батарея: построена, и три её метрики сняты собственным замером
|
||||
|
||||
Девять проверок по спецификациям `docs/research/12-failure-modes-ru-target.md` (у каждого из 12
|
||||
режимов отказа русской цели там есть раздел «Детекция» — правила взяты оттуда, а не придуманы).
|
||||
16 пинов правил на синтетике с известным ответом + прогон по 91 реальной единице.
|
||||
|
||||
**Главное, что дал прогон по чистому тексту: правила, написанные по букве спецификации, в трёх
|
||||
случаях из шести мерили шум.** Числа до и после — на 91 единице, на единицу:
|
||||
|
||||
| Метрика | было | стало | что было не так |
|
||||
|---|---|---|---|
|
||||
| нарушений типографики | 3.87 | **0.18** | ёлочка в начале строки считалась нарушением (228 срабатываний) — это легитимная цитата/мысль; прописная после атрибуции (119) — в основном имена собственные, которым прописная положена |
|
||||
| единиц с транслит-междометиями | 0.64 | **0.00** | подстрочный матчинг: 77 «ара» внутри «барабан», 13 «ауч» внутри «паучьей». Чинится границами слова |
|
||||
| потеряно / появилось величин | 0.70 / 5.56 | **0.25 / 0.27** | не разбирались русские числительные словами («пятьсот»); малые числительные в русском живут местоимениями («один из них»), которым в 一个 нет величины. Пол величины 100 выбран свипом 8 конфигураций |
|
||||
| не-палладиевских имён | 1.05 | **0.07** | склонённые транскрипции («Чжэна» — 24 срабатывания) и ПЕРЕВЕДЁННЫЕ имена («Первопредок», «Кровавокрылая»), которым палладиевская форма не положена |
|
||||
| омографов без ё | 1.67 | **снята** | 130 из 152 — обычное множественное «все», 22 — «небо» в прямом значении. Различить «все/всё» без контекста нельзя; вместо счёта меряется политика ё и её единообразие |
|
||||
|
||||
Живые остатки на том же корпусе: ханьцзы в финале 4 знака на 91 единицу (1 единица); не-палладиевских
|
||||
имён **6 срабатываний / 5 различных слов** на 2184 кандидата, из них 2 — настоящие сигналы; смешение
|
||||
ты/вы в 0.31 единицы (нижняя граница: смешение между разными собеседниками легитимно); медиана доли
|
||||
слов черновика, доживших до финала, 0.942.
|
||||
|
||||
*(Расхождение 5↔6 поймано верификатором `verify_report.py`, а не глазами: в первую редакцию отчёта
|
||||
попало число РАЗЛИЧНЫХ слов, посчитанное дедуплицирующим путём, при 6 фактических срабатываниях.
|
||||
Ровно тот класс ошибки, ради которого верификатор и написан.)*
|
||||
|
||||
**Объявленные границы батареи** (в отчёт, не в примечание): величины 1–99 вне охвата; «длины
|
||||
предложений против нативного ру-корпуса» не считаются абсолютно — нативного русского референс-корпуса
|
||||
в дереве нет, и подставлять чужую константу нельзя (тот же класс ошибки, что снятый жанровый словарь
|
||||
D39.47), поэтому распределение сравнивается МЕЖДУ АРМАМИ; ты/вы без speaker-ID — сигнал к просмотру,
|
||||
не счёт ошибок; род прош. времени работает только при явной близости имени и глагола.
|
||||
|
||||
### 2.4. Живой листинг моделей
|
||||
|
||||
Снят по всем семи ключам, все отвечают. Против `00-provider-quirks.md` появились новые жильцы, и
|
||||
они пойдут в кандидаты Ф1 слагами дня запуска: **grok-4.5** · **kimi-k3** · **glm-5-turbo**,
|
||||
**glm-5.2** · **gemini-3.5-flash**, **gemini-3.6-flash**. У DeepSeek листинг прежний (два слага) —
|
||||
но урок календаря D39.61 в силе: «слаг живой ≠ модель та же», и вахта реестра 108 (пере-проба
|
||||
маппинга эффорта `deepseek-v4-pro`) остаётся первым платным шагом Ф1.
|
||||
**Добор единиц требует нормировки счёта на 1000 слов.** Проверено: на текущих 16 однородных по
|
||||
длине единицах нормировка не меняет ничего (z 2.457 → 2.429), но добавляемые 16 короче — 8 из них
|
||||
ниже текущего минимума на 30%, — и без нормировки они добавят дисперсии больше, чем сигнала.
|
||||
|
||||
---
|
||||
|
||||
## §3. Девиации от промта
|
||||
## §7 Воспроизведение
|
||||
|
||||
| # | Девиация | Основание |
|
||||
|---|---|---|
|
||||
| 1 | QE-ранкер — MetricX-24 вместо CometKiwi/xCOMET | все варианты CometKiwi/xCOMET `gated`, токена нет; `unbabel-comet` не встаёт на py3.14 |
|
||||
| 2 | Reflow-план кодом (Q4c) не гонится | §0.3: гипотеза уже опровергнута эксп-20 §5.2, деконфаунд несёт арм D′ |
|
||||
| 3 | Арм C реального режима не покрывает класс инверсий; арм G сужен | §2.2, объявлено ДО запуска |
|
||||
| 4 | Счёт омографов ё снят из метрик | §2.3, снят собственным замером как ложный |
|
||||
```
|
||||
eval/.venv/bin/python eval/role_topology/bakeoff.py --arms # армы zh (кэш: купленное не перекупается)
|
||||
eval/.venv/bin/python eval/role_topology/bakeoff.py --judge # судейство zh
|
||||
eval/.venv/bin/python eval/role_topology/bakeoff.py --score # свод zh, $0
|
||||
eval/.venv/bin/python eval/role_topology/pair_en.py --units|--arms|--judge|--score
|
||||
eval/.venv/bin/python eval/role_topology/blind_read.py --emit zh|en # пакеты слепого чтения
|
||||
eval/.venv/bin/python eval/role_topology/verify_report.py # верификатор чисел отчёта
|
||||
eval/.venv/bin/python eval/role_topology/selfcheck.py # самопроверка харнесса, $0
|
||||
```
|
||||
|
||||
## §4. Открыто
|
||||
|
||||
- **Материал Ф0.2** — на владельце (§0.2). Блокирует все платные фазы.
|
||||
- Ф0.3 банк среза, Ф0.6 предзамер судей, Ф1, Ф2а, Ф2б — не запускались.
|
||||
Артефакты — `~/books/role-topology/` (вне git). Текст книг в репозиторий и в отчёт не попадает.
|
||||
Ненулевой код возврата `verify_report.py` = отчёт не сдаётся.
|
||||
|
|
|
|||
|
|
@ -7,7 +7,8 @@
|
|||
|
||||
Армы. Черновики ФРИЗЯТСЯ и подаются идентичными всем редакторским армам — это парный дизайн, и он
|
||||
даёт кратный выигрыш мощности бесплатно. Черновики берутся готовые из корпуса пакета-6 (их сделал
|
||||
боевой `deepseek-v4-flash`), поэтому арм F и вход армов A/B стоят $0.
|
||||
боевой `deepseek-v4-flash` — ⚠ поля модели в записи корпуса НЕТ, провенанс не проверяем),
|
||||
поэтому арм F не имеет собственной цены: она утрачена вместе с породившим вызовом.
|
||||
|
||||
F do-nothing пол: черновик как есть. Все редакторские армы мерятся ПРОТИВ НЕГО, а не друг
|
||||
против друга — иначе «B лучше A» ничего не говорит о том, нужен ли второй проход вообще.
|
||||
|
|
@ -18,8 +19,9 @@
|
|||
D′ она же БЕЗ мандата перевёрстки — деконфаунд «модель против мандата».
|
||||
|
||||
Протокол сравнения задан замером Ф0.6, а не вкусом: судьи не выдумывают перевес на идентичных
|
||||
текстах (0 из 18), но их АБСОЛЮТНЫЙ счёт ошибок плавает ⇒ сравниваем только ПАРНО, зеркальной
|
||||
раскладкой, и вердикт засчитываем при перевесе в ≥2 повторах из 3 у обоих судей.
|
||||
текстах, но их АБСОЛЮТНЫЙ счёт ошибок плавает ⇒ сравниваем только ПАРНО, полным скрещиванием
|
||||
порядков; вердикт требует совпадения знака в ОБОИХ порядках у обоих судей, а несущая статистика —
|
||||
непрерывный перевес по единицам с bootstrap-ДИ и поправкой Holm по семейству контрастов.
|
||||
|
||||
Контрасты выбраны так, чтобы каждый отвечал на отдельный вопрос, а не наращивал число сравнений:
|
||||
A против F — покупает ли боевой второй проход хоть что-то поверх черновика;
|
||||
|
|
@ -71,8 +73,13 @@ REPS_PER_ORDER = 1
|
|||
|
||||
# ⚠ ПОТОЛКИ ВТОРОЙ РЕДАКЦИИ объявлены ДО первой покупки (санкция владельца 07.08 «деньги
|
||||
# потратить разрешаю»). Первая редакция Ф2а стоила $1.3777 и признана негодной по раскладке.
|
||||
CEIL_ARMS = 0.70
|
||||
CEIL_JUDGE = 1.70
|
||||
# ⚠ ПОТОЛКИ ТРЕТЬЕЙ РЕДАКЦИИ, объявлены в §9 отчёта ДО первой покупки. Кассы читают диск по
|
||||
# префиксам `bo2-*`/`jv2-*`, то есть новые покупки ложатся в ТЕ ЖЕ счётчики; без пере-объявления
|
||||
# первая крупная покупка ушла бы в `skipped` с пустым выходом, а свод молча выбросил бы единицу.
|
||||
# Тихое усечение n — тот же класс, что снял первую редакцию, поэтому потолок поднимается ЗАРАНЕЕ
|
||||
# и печатается, а не подгоняется в момент отказа.
|
||||
CEIL_ARMS = 1.60
|
||||
CEIL_JUDGE = 3.20
|
||||
LED_ARMS = BUY.Ledger("армы Ф2а-2", CEIL_ARMS, ("bo2-*.json",), default_expect=0.03)
|
||||
LED_JUDGE = BUY.Ledger("судейство Ф2а-2", CEIL_JUDGE, ("jv2-*.json",), default_expect=0.008)
|
||||
|
||||
|
|
@ -141,7 +148,11 @@ def units(n: int = N_UNITS) -> list[dict]:
|
|||
continue # тот же текст под другим заголовком главы
|
||||
by_sig[sig] = uid_of(u["source"])
|
||||
uniq.setdefault(uid_of(u["source"]), u)
|
||||
keys = sorted(uniq, key=lambda k: len(uniq[k]["source"]))
|
||||
# Ключ (длина, uid), а не одна длина: одинаковая длина иначе разрешалась бы порядком обхода
|
||||
# словаря, то есть неявной стабильностью. Здесь она сейчас есть, а в паре en её не было —
|
||||
# там тот же приём на `set()` дал РАЗНЫЕ наборы единиц в двух вызовах подряд (поймано
|
||||
# исполнением 07.08). Сверено: явный ключ даёт тот же набор, что и прежний, побайтно.
|
||||
keys = sorted(uniq, key=lambda k: (len(uniq[k]["source"]), k))
|
||||
lo = (len(keys) - n) // 2
|
||||
out = []
|
||||
for k in keys[lo:lo + n]:
|
||||
|
|
@ -222,13 +233,50 @@ def run_draft(u: dict) -> dict:
|
|||
msgs.append({"role": "user", "content": user})
|
||||
kw = dict(model=DRAFT_MODEL, messages=msgs, max_tokens=16000,
|
||||
extra_body={"reasoning_effort": "low"})
|
||||
return BUY.purchase(LED_ARMS, f"bo2-DRAFT-{u['uid']}", DRAFT_MODEL, client(DRAFT_MODEL), kw,
|
||||
arm="DRAFT", uid=u["uid"])
|
||||
# РЕ-ГЕН ПРИ ЭХЕ — боевое лечение по quirks 00 строка 86: на весах 0731 эхо СТОХАСТИЧНО
|
||||
# ПО ВЫЗОВУ (два побайтно одинаковых запроса дали долю ханьцзы 0.000 и 0.831), частота при
|
||||
# `effort:low` — 3 из 20. Поймано исполнением здесь же: первый купленный черновик вернул
|
||||
# исходник целиком. Ре-ген на flash в 7.6 раза дешевле эскалации на pro, поэтому боевая
|
||||
# схема гонит N=1 ре-ген до эскалации — и цена ре-гена ЛОЖИТСЯ В ЭКОНОМИКУ, а не прячется.
|
||||
for attempt in (1, 2, 3):
|
||||
tag = f"bo2-DRAFT-{u['uid']}" + ("" if attempt == 1 else f"-r{attempt}")
|
||||
rec = BUY.purchase(LED_ARMS, tag, DRAFT_MODEL, client(DRAFT_MODEL), kw,
|
||||
arm="DRAFT", uid=u["uid"], attempt=attempt)
|
||||
if rec.get("skipped"):
|
||||
return rec
|
||||
bad = draft_reject_reason(rec["content"])
|
||||
if not bad:
|
||||
return rec
|
||||
print(f" ⚠ {bad} в черновике {u['uid'][:6]} (попытка {attempt}) — ре-ген")
|
||||
return rec
|
||||
|
||||
|
||||
def draft_reject_reason(text: str) -> str:
|
||||
"""Годен ли черновик. Пусто = годен.
|
||||
|
||||
⚠ ПОЧИНКА 07.08 по адверсариальному ревью. Прежняя версия проверяла ТОЛЬКО письменность
|
||||
ИСХОДНИКА и потому ловила половину класса: артефакт `bo2-DRAFT-34749d6ccc-r2` прошёл гейт и
|
||||
был оплачен, имея долю латиницы 0.79 и кириллицы 0.00 — то есть модель вернула АНГЛИЙСКИЙ
|
||||
перевод вместо русского. Гейт, предлагаемый как боевое лечение эхо-мины, обязан проверять и
|
||||
цель: пустой выход · эхо исходной письменности · выход не на целевом языке.
|
||||
"""
|
||||
t = (text or "").strip()
|
||||
if not t:
|
||||
return "пустой выход"
|
||||
if B.check_cjk_leak(t)["over_prod_threshold"]:
|
||||
return "эхо исходника"
|
||||
letters = [c for c in t if c.isalpha()]
|
||||
if letters and sum(1 for c in letters if "а" <= c.lower() <= "я" or c.lower() == "ё") / len(letters) < 0.5:
|
||||
return "выход не на целевом языке"
|
||||
return ""
|
||||
|
||||
|
||||
def draft_cost(uid: str) -> float:
|
||||
f = OUT / f"bo2-DRAFT-{uid}.json"
|
||||
return json.loads(f.read_text(encoding="utf-8"))["cost_usd"] if f.exists() else 0.0
|
||||
"""Цена черновика = ВСЕ его попытки. Ре-ген оплачен, значит он в цене топологии."""
|
||||
tot = 0.0
|
||||
for f in OUT.glob(f"bo2-DRAFT-{uid}*.json"):
|
||||
tot += json.loads(f.read_text(encoding="utf-8")).get("cost_usd", 0.0)
|
||||
return tot
|
||||
|
||||
|
||||
def run_arm(arm: str, u: dict) -> dict:
|
||||
|
|
@ -349,10 +397,12 @@ def margins_by_order(a: str, b: str, uid: str, judge: str) -> dict[int, list[flo
|
|||
def position_bias() -> float:
|
||||
"""Средняя разница «ошибки второго варианта − ошибки первого» по ВСЕМ голосам.
|
||||
|
||||
Это контроль исправности рига, а не результат: при честном зеркале и достаточном n фора
|
||||
должна быть около нуля по построению (каждая пара судится в обоих порядках). Крупное
|
||||
ненулевое значение означает, что раскладка сломана — ровно это и случилось с первой
|
||||
редакцией (+4.65), где порядок 0 весил вдвое больше порядка 1.
|
||||
⚠ ЧИТАЕТСЯ НЕ ТАК, как я написал в первой редакции этой функции. Зеркало НЕ гонит эту
|
||||
величину к нулю: если судья штрафует вторую позицию, то `e2 − e1 > 0` при любом порядке, и
|
||||
среднее остаётся большим. Зеркало убирает позицию из ПЕРЕВЕСА АРМА (перевес усредняется по
|
||||
двум порядкам), а не из этой статистики. Поэтому число здесь — ИЗМЕРЕННАЯ ВЕЛИЧИНА ФОРЫ,
|
||||
и она полезна как масштаб: с чем сравнивать перевес арма. Исправность раскладки проверяется
|
||||
другим — равенством числа голосов по порядкам (см. `verify_report.check_second_edition`).
|
||||
"""
|
||||
d = []
|
||||
for f in OUT.glob("jv2-*.json"):
|
||||
|
|
@ -377,6 +427,22 @@ def boot_ci(xs: list[float], reps: int = 5000, alpha: float = 0.05) -> tuple[flo
|
|||
return (means[int(alpha / 2 * reps)], means[int((1 - alpha / 2) * reps) - 1])
|
||||
|
||||
|
||||
def sign_perm_p(xs: list[float]) -> float:
|
||||
"""Точный двусторонний знаковый перестановочный тест: перебираются ВСЕ 2^n расстановок знаков.
|
||||
|
||||
Заведён 07.08 по адверсариальному ревью. Bootstrap-ДИ отвечает на вопрос «где лежит среднее»,
|
||||
а не «отличимо ли оно от нуля при этом n», и на 15 единицах даёт заметно более узкий интервал,
|
||||
чем точный тест. Здесь считается p, к которому применима поправка Holm по семейству контрастов.
|
||||
"""
|
||||
n = len(xs)
|
||||
if n == 0 or n > 20:
|
||||
return float("nan")
|
||||
obs = abs(sum(xs))
|
||||
hits = sum(1 for mask in range(1 << n)
|
||||
if abs(sum(x if mask >> i & 1 else -x for i, x in enumerate(xs))) >= obs - 1e-12)
|
||||
return hits / (1 << n)
|
||||
|
||||
|
||||
def cmd_score() -> None:
|
||||
"""Свод. $0: читает персистированные голоса и прогоняет батарею по выходам армов."""
|
||||
us = units()
|
||||
|
|
@ -418,6 +484,74 @@ def cmd_score() -> None:
|
|||
print(f"{a + ' против ' + b:16s}{n:8d}{wins_a:11d}{wins_b:11d}{tie:8d}{eff:+10.2f}"
|
||||
f" [{lo:+.2f}, {hi:+.2f}]{star}")
|
||||
|
||||
print("\nПО СУДЬЯМ ОТДЕЛЬНО. ⚠ Эффект, который несёт ОДИН судья, — свойство судьи, а не армов.")
|
||||
print(f"{'контраст':16s}" + "".join(f"{j.split('-')[0]:>26s}" for j in J.JUDGES))
|
||||
for a, b in CONTRASTS:
|
||||
cells = []
|
||||
for judge in J.JUDGES:
|
||||
per = [statistics.mean([statistics.mean(bo[0]), statistics.mean(bo[1])])
|
||||
for u in us
|
||||
if len(bo := margins_by_order(a, b, u["uid"], judge)) == 2]
|
||||
if not per:
|
||||
cells.append(f"{'—':>26s}")
|
||||
continue
|
||||
lo, hi = boot_ci(per)
|
||||
mark = " " if lo <= 0 <= hi else "*"
|
||||
cells.append(f"{statistics.mean(per):+8.2f} [{lo:+.2f},{hi:+.2f}]{mark:>2s}")
|
||||
print(f"{a + ' против ' + b:16s}" + "".join(cells))
|
||||
print(" * = ДИ судьи не накрывает ноль. Вывод держится только там, где звёздочки у ОБОИХ.")
|
||||
|
||||
print("\nПОПРАВКА НА МНОЖЕСТВЕННОСТЬ. В таблице пять контрастов; ДИ выше НЕ скорректированы.")
|
||||
print("Точный знаковый перестановочный тест по единицам + Holm по семейству:")
|
||||
ps = []
|
||||
for a, b in CONTRASTS:
|
||||
per = []
|
||||
for u in us:
|
||||
pj = []
|
||||
for judge in J.JUDGES:
|
||||
bo = margins_by_order(a, b, u["uid"], judge)
|
||||
if len(bo) == 2:
|
||||
pj += [statistics.mean(bo[0]), statistics.mean(bo[1])]
|
||||
if len(pj) == 2 * len(J.JUDGES):
|
||||
per.append(statistics.mean(pj))
|
||||
ps.append((f"{a} против {b}", sign_perm_p(per), len(per)))
|
||||
order = sorted(range(len(ps)), key=lambda i: ps[i][1])
|
||||
holm, running = [0.0] * len(ps), 0.0
|
||||
for rank, i in enumerate(order):
|
||||
running = max(running, ps[i][1] * (len(ps) - rank))
|
||||
holm[i] = min(1.0, running)
|
||||
for (name, p, n), h in zip(ps, holm):
|
||||
print(f" {name:16s} n={n:2d} p={p:.4f} Holm={h:.4f} "
|
||||
f"{'проходит 0.05' if h < 0.05 else 'НЕ проходит 0.05'}")
|
||||
|
||||
print("\nРАЗЛОЖЕНИЕ ПО ОСЯМ. Сумма осей гасит разнонаправленные эффекты — печатается покомпонентно.")
|
||||
print(f"{'контраст':16s}" + "".join(f"{ax:>22s}" for ax in J.AXES))
|
||||
for a, b in CONTRASTS:
|
||||
cells = []
|
||||
for ax in J.AXES:
|
||||
per = []
|
||||
for u in us:
|
||||
ms = []
|
||||
for judge in J.JUDGES:
|
||||
for o in (0, 1):
|
||||
f = OUT / f"jv2-{a}v{b}-{u['uid']}-o{o}-{judge}-r1.json"
|
||||
if not f.exists():
|
||||
continue
|
||||
p = J.parse(json.loads(f.read_text(encoding="utf-8"))["content"])
|
||||
if p[f"В1-{ax}"] is None:
|
||||
continue
|
||||
e1, e2 = p[f"В1-{ax}"], p[f"В2-{ax}"]
|
||||
ms.append((e2 - e1) if o == 0 else (e1 - e2))
|
||||
if len(ms) == 2 * len(J.JUDGES):
|
||||
per.append(statistics.mean(ms))
|
||||
if not per:
|
||||
cells.append(f"{'—':>22s}")
|
||||
continue
|
||||
lo, hi = boot_ci(per)
|
||||
cells.append(f"{statistics.mean(per):+6.2f} [{lo:+.2f},{hi:+.2f}]"
|
||||
+ ("*" if not lo <= 0 <= hi else " "))
|
||||
print(f"{a + ' против ' + b:16s}" + "".join(cells))
|
||||
|
||||
print("\nДЕТЕРМИНИРОВАННАЯ БАТАРЕЯ (первична — шума не имеет)\n")
|
||||
print(f"{'арм':5s}{'ед.':>5s}{'типогр.':>9s}{'ханьцзы':>9s}{'потеря вел.':>13s}"
|
||||
f"{'ты/вы микс':>12s}{'род свер.':>11s}{'род ошиб.':>11s}{'$/единицу':>11s}")
|
||||
|
|
|
|||
144
eval/role_topology/blind_read.py
Normal file
144
eval/role_topology/blind_read.py
Normal file
|
|
@ -0,0 +1,144 @@
|
|||
#!/usr/bin/env python3
|
||||
"""ТРЕТИЙ КОНТУР — слепое ЧТЕНИЕ выходов. $0 по ключам провайдеров.
|
||||
|
||||
Зачем нужен. Первая редакция пака мерила качество ровно одним инструментом — судьями по ключам
|
||||
(`gpt-5.6-luna`, `grok-4.3`), и когда у этого инструмента нашлась позиционная фора +4.65 ошибки,
|
||||
проверить его оказалось нечем: батарея говорит только о механических дефектах, а сам я выходы
|
||||
систематически не читал. Один инструмент без второго мнения — это не замер, а доверие.
|
||||
|
||||
Чем этот контур ОТЛИЧАЕТСЯ от судей, а не дублирует их:
|
||||
* другое семейство моделей (читатели — не из семей, населяющих армы, и не из семей судей),
|
||||
поэтому общий слепой участок семейства не переносится;
|
||||
* другая задача: не «посчитай ошибки по осям», а «прочитай как редактор издательства и назови
|
||||
конкретные места цитатой». Счёт ошибок и чтение ловят разное;
|
||||
* ранжирование ВСЕХ армов сразу, а не парно — позиционная фора парного протокола сюда
|
||||
структурно не переносится, зато появляется своя (порядок в списке), и она гасится
|
||||
перестановкой на каждой единице.
|
||||
|
||||
Слепота. Метки армов снимаются, варианты перемешиваются детерминированным ключом от uid, карта
|
||||
раскладки лежит ОТДЕЛЬНЫМ файлом и читателю не выдаётся. Расшифровка — только при сведении.
|
||||
|
||||
Запуск: --emit разложить пакеты чтения (и карту) по файлам
|
||||
--map показать карту раскладки (после чтения)
|
||||
"""
|
||||
from __future__ import annotations
|
||||
import hashlib
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
REPO = Path("/home/ubuntu/projects/textmachine")
|
||||
sys.path.insert(0, str(REPO / "eval" / "role_topology"))
|
||||
|
||||
OUT = Path.home() / "books" / "role-topology"
|
||||
PACKETS = OUT / "blind"
|
||||
# ⚠ Карта раскладки лежит ВНЕ каталога пакетов (починка 07.08 по ревью). Прежде `zh-MAP.json`
|
||||
# лежал рядом с 28 пакетами: читателю запрещали её открывать инструкцией, а не расположением.
|
||||
# Запрет, который держится только на послушании читателя, слепотой не является.
|
||||
KEYS = OUT / "blind-keys"
|
||||
|
||||
INSTRUCTION = """Ты — редактор русского издательства, готовящий книгу к печати.
|
||||
|
||||
Ниже ИСХОДНИК и несколько независимых переводов одного и того же фрагмента. Кто их сделал —
|
||||
не сообщается намеренно.
|
||||
|
||||
Прочитай каждый вариант ЦЕЛИКОМ как читатель, а не как чекер. Затем по КАЖДОМУ варианту:
|
||||
1. Назови конкретные места, которые не пойдут в печать — ЦИТАТОЙ из варианта (не пересказом).
|
||||
Классы, которые важны: искажение смысла исходника; выдуманное или несуществующее русское
|
||||
слово; translationese (калька синтаксиса, канцелярит, неестественный порядок слов);
|
||||
потеря или подмена фактуры (числа, имена, полярность утверждения); сбой вёрстки и диалога.
|
||||
2. Отметь, чем вариант ЛУЧШЕ остальных, если есть чем.
|
||||
Затем расставь варианты в порядке пригодности к печати, от лучшего к худшему, и назови ОДИН
|
||||
решающий довод за первое место — тоже цитатой.
|
||||
|
||||
Отвечай строго в таком виде:
|
||||
ВАРИАНТ <буква>
|
||||
ДЕФЕКТЫ: <класс> — «цитата» | <класс> — «цитата» | ... (или «нет»)
|
||||
СИЛЬНОЕ: <одной строкой или «нет»>
|
||||
(повторить для каждого варианта)
|
||||
РАНГ: <буквы через > , например В > А > Б>
|
||||
ДОВОД: <одна фраза с цитатой>
|
||||
"""
|
||||
|
||||
|
||||
def shuffle_key(uid: str, arms: list[str]) -> list[str]:
|
||||
"""Детерминированная перестановка от uid: воспроизводима и не зависит от порядка армов."""
|
||||
h = hashlib.sha1(uid.encode()).digest() # noqa: S324
|
||||
return [a for _, a in sorted(zip(h[:len(arms)], sorted(arms)))]
|
||||
|
||||
|
||||
def emit(pack: str) -> None:
|
||||
if pack == "zh":
|
||||
import bakeoff as M # noqa: PLC0415
|
||||
us, arms, prefix = M.units(), ["F", *M.ARMS], "zh"
|
||||
else:
|
||||
import pair_en as M # noqa: PLC0415
|
||||
us, arms, prefix = M.units(), ["F", *M.ARMS], "en"
|
||||
PACKETS.mkdir(parents=True, exist_ok=True)
|
||||
letters = "АБВГДЕЖЗ"
|
||||
written, mapping = 0, {}
|
||||
for u in us:
|
||||
texts = {a: M.text_of(a, u) for a in arms}
|
||||
have = [a for a in shuffle_key(u["uid"], arms) if texts[a].strip()]
|
||||
if len(have) < 2:
|
||||
continue
|
||||
body = [INSTRUCTION, "\n=== ИСХОДНИК ===\n", u["source"]]
|
||||
for i, a in enumerate(have):
|
||||
body.append(f"\n\n=== ВАРИАНТ {letters[i]} ===\n{texts[a]}")
|
||||
mapping.setdefault(u["uid"], {})[letters[i]] = a
|
||||
(PACKETS / f"{prefix}-{u['uid']}.txt").write_text("".join(body), encoding="utf-8")
|
||||
written += 1
|
||||
KEYS.mkdir(parents=True, exist_ok=True)
|
||||
(KEYS / f"{prefix}-MAP.json").write_text(json.dumps(mapping, ensure_ascii=False, indent=1),
|
||||
encoding="utf-8")
|
||||
print(f"пакетов {written} → {PACKETS}/{prefix}-*.txt · карта в {KEYS} (вне каталога пакетов)")
|
||||
|
||||
|
||||
def score(pack: str) -> None:
|
||||
"""Свод чтения по персистированным рангам. $0.
|
||||
|
||||
⚠ Заведён 07.08 по адверсариальному ревью второй редакции. Прежде модуль умел только
|
||||
ВЫПУСКАТЬ пакеты, а ответы читателей нигде не оседали — ранги переносились в свод руками.
|
||||
Это нарушение закона промта стр.32 («per-vote персист КАЖДОГО голоса»), рождённого
|
||||
ревью-шапкой эксп-20: замер без персиста невоспроизводим. Ранги прошлого прогона
|
||||
восстановлены в `READINGS.json`, полные разборы утрачены и требуют ре-рана читателей.
|
||||
"""
|
||||
import statistics # noqa: PLC0415
|
||||
from collections import defaultdict # noqa: PLC0415
|
||||
f = KEYS / "READINGS.json"
|
||||
if not f.exists():
|
||||
print("нет READINGS.json — чтение не персистировано")
|
||||
return
|
||||
d = json.loads(f.read_text(encoding="utf-8"))
|
||||
m = json.loads((KEYS / f"{pack}-MAP.json").read_text(encoding="utf-8"))
|
||||
ranks = d[f"ranks_{pack}"]
|
||||
pos: dict[str, list[int]] = defaultdict(list)
|
||||
wins: dict[str, int] = defaultdict(int)
|
||||
for uid, order in ranks.items():
|
||||
letters = order.split()
|
||||
for i, letter in enumerate(letters):
|
||||
pos[m[uid][letter]].append(i + 1)
|
||||
wins[m[uid][letters[0]]] += 1
|
||||
print(f"СЛЕПОЕ ЧТЕНИЕ {pack} · единиц {len(ranks)}\n")
|
||||
print(f"{'арм':7s}{'ср.ранг':>9s}{'первых':>8s}{'медиана':>9s}")
|
||||
for arm in sorted(pos, key=lambda a: statistics.mean(pos[a])):
|
||||
print(f"{arm:7s}{statistics.mean(pos[arm]):9.2f}{wins[arm]:8d}"
|
||||
f"{statistics.median(pos[arm]):9.1f}")
|
||||
print(f"\n⚠ {d['provenance']}")
|
||||
|
||||
|
||||
def show_map(pack: str) -> None:
|
||||
f = KEYS / f"{pack}-MAP.json"
|
||||
print(f.read_text(encoding="utf-8") if f.exists() else "карты нет")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
a = sys.argv[1:] or ["--emit", "zh"]
|
||||
if a[0] == "--emit":
|
||||
emit(a[1] if len(a) > 1 else "zh")
|
||||
elif a[0] == "--score":
|
||||
score(a[1] if len(a) > 1 else "zh")
|
||||
elif a[0] == "--map":
|
||||
show_map(a[1] if len(a) > 1 else "zh")
|
||||
else:
|
||||
print(__doc__)
|
||||
|
|
@ -67,13 +67,18 @@ class Ledger:
|
|||
|
||||
|
||||
def usage_of(u, ch) -> dict:
|
||||
"""Токены вызова в однородном виде. `total_tokens` персистится ВСЕГДА (см. шапку)."""
|
||||
"""Токены вызова в однородном виде. `total_tokens` персистится ВСЕГДА (см. шапку).
|
||||
|
||||
`ch` может быть None — это ответ без сообщения (fail-closed фильтра); токены при этом
|
||||
провайдер обычно всё равно тарифицирует, поэтому usage читается, а не обнуляется.
|
||||
"""
|
||||
cached = getattr(getattr(u, "prompt_tokens_details", None), "cached_tokens", 0) or 0
|
||||
reasoning = getattr(getattr(u, "completion_tokens_details", None), "reasoning_tokens", 0) or 0
|
||||
msg = getattr(ch, "message", None) if ch is not None else None
|
||||
return dict(prompt_tokens=u.prompt_tokens or 0, cached_tokens=cached,
|
||||
completion_tokens=u.completion_tokens or 0, reasoning_tokens=reasoning,
|
||||
total_tokens=getattr(u, "total_tokens", 0) or 0,
|
||||
reasoning_chars=len(getattr(ch.message, "reasoning_content", None) or ""))
|
||||
reasoning_chars=len(getattr(msg, "reasoning_content", None) or ""))
|
||||
|
||||
|
||||
def priced(model: str, us: dict) -> float:
|
||||
|
|
@ -106,7 +111,24 @@ def purchase(led: Ledger, tag: str, model: str, client, kw: dict, **extra) -> di
|
|||
(OUT / f"{tag}.ERROR.json").write_text(json.dumps(rec, ensure_ascii=False, indent=1),
|
||||
encoding="utf-8")
|
||||
return rec
|
||||
ch, us = r.choices[0], usage_of(r.usage, r.choices[0])
|
||||
ch = r.choices[0] if r.choices else None
|
||||
# ⚠ ОТВЕТ БЕЗ СООБЩЕНИЯ. Поймано исполнением 07.08: `ch.message` пришёл None, и прогон упал
|
||||
# на 130 неоплаченных клетках. Это не «пустой content», а отсутствие самого объекта сообщения —
|
||||
# так выглядит fail-closed фильтра (quirks 00: у Gemini `finish_reason` приходит СОСТАВНОЙ
|
||||
# строкой `'content_filter: PROHIBITED_CONTENT'`, точные матчеры на неё мертвы).
|
||||
# Клетка записывается как ДАННЫЕ (отказ провода с причиной), а не роняет фазу: провал одного
|
||||
# вызова — это факт о модели на этом материале, и он нужен в отчёте.
|
||||
if ch is None or getattr(ch, "message", None) is None:
|
||||
rec = dict(tag=tag, model=model, model_returned=getattr(r, "model", ""),
|
||||
finish=str(getattr(ch, "finish_reason", "no-choice")), content="",
|
||||
no_message=True, latency_s=round(time.time() - t0, 1),
|
||||
cost_usd=priced(model, usage_of(r.usage, None)) if r.usage else 0.0,
|
||||
**(usage_of(r.usage, None) if r.usage else {}), **extra)
|
||||
(OUT / f"{tag}.NOMSG.json").write_text(json.dumps(rec, ensure_ascii=False, indent=1),
|
||||
encoding="utf-8")
|
||||
print(f" ⚠ ответ без сообщения: {tag} finish={rec['finish']}")
|
||||
return rec
|
||||
us = usage_of(r.usage, ch)
|
||||
rec = dict(tag=tag, model=model, model_returned=r.model, finish=ch.finish_reason,
|
||||
content=ch.message.content or "", latency_s=round(time.time() - t0, 1),
|
||||
cost_usd=priced(model, us), **us, **extra)
|
||||
|
|
|
|||
|
|
@ -18,7 +18,8 @@
|
|||
|
||||
Выбор судей — не вкусовой. Промт: «судья не судит армы своего семейства-жильца». Армы Ф2 населяют
|
||||
DeepSeek (`deepseek-v4-pro`) и Z.AI (`glm-5`), поэтому судьи берутся из ДРУГИХ семейств:
|
||||
`grok-4.3` (xAI) и `gpt-5.6-luna` (OpenAI). Оба прошли скрин Ф1, оба дёшевы, оба быстры.
|
||||
`gpt-5.6-luna` (OpenAI) и `gemini-3.1-flash-lite` (Google) — см. JUDGES ниже:
|
||||
`grok-4.3` выпал на исчерпании кредитов xAI и заменён через положительный контроль.
|
||||
|
||||
Дисциплина протокола (промт, §ЗАКОН):
|
||||
· КАЖДЫЙ голос персистится отдельным файлом — ревью-шапка эксп-20 прямо запретила повторять
|
||||
|
|
@ -63,7 +64,21 @@ OUT.mkdir(parents=True, exist_ok=True)
|
|||
# чтением кода до запуска. Голоса Ф2а помечаются префиксом `jv-bo-` и считаются отдельно.
|
||||
CEILING_USD = 0.60
|
||||
PHASE_PREFIX = "jv-" # что считать «своими» тратами; bakeoff подменяет на "jv-bo-"
|
||||
JUDGES = ("gpt-5.6-luna", "grok-4.3")
|
||||
# ⚠ ЗАМЕНА ВТОРОГО СУДЬИ 07.08 — ВНЕШНИЙ БЛОКЕР, объявляю.
|
||||
# `grok-4.3` выпал не по замыслу: ключ xAI вернул HTTP 403 «Your team has either used all available
|
||||
# credits or reached its monthly spending limit» на 158 голосах zh и 92 en. Отказы записаны
|
||||
# отдельными `*.ERROR.json`, купленные голоса не отравлены.
|
||||
# Замена выбрана по трём условиям промта (стр.32): судья не из семейства, населяющего армы
|
||||
# (deepseek, glm), и не из семейства первого судьи (OpenAI) ⇒ остаются Google, Mistral, Moonshot.
|
||||
# Mistral ($0.0128/голос) и kimi ($0.024) не влезали в ОБЪЯВЛЕННЫЙ остаток потолка, а поднимать
|
||||
# потолок задним числом — ровно то, за что снята первая редакция. Взят `gemini-3.1-flash-lite`
|
||||
# ($0.0022/голос).
|
||||
# ПОЛОЖИТЕЛЬНЫЙ КОНТРОЛЬ ДО ТРАТ (D39.46б): на контрольном декое пака замена поймала посаженную
|
||||
# деградацию 6/6 в ОБОИХ порядках, со зеркальными счетами (В1=0/В2=3 и В1=3/В2=0) — то есть на
|
||||
# этой клетке её позиционная фора близка к нулю. Стоимость проверки $0.00363.
|
||||
# ⚠ Оговорка, которую отчёт обязан нести: это ЛЁГКАЯ модель, и её согласие с luna не эквивалентно
|
||||
# прежней паре luna+grok. Голоса grok, купленные первой редакцией, к этим армам не относятся.
|
||||
JUDGES = ("gpt-5.6-luna", "gemini-3.1-flash-lite")
|
||||
REPS = 3
|
||||
OPENAI_FAMILY = {"gpt-5.6-luna"}
|
||||
|
||||
|
|
|
|||
450
eval/role_topology/pair_en.py
Normal file
450
eval/role_topology/pair_en.py
Normal file
|
|
@ -0,0 +1,450 @@
|
|||
#!/usr/bin/env python3
|
||||
"""ВТОРАЯ ЯЗЫКОВАЯ ПАРА — en→ru на Kristoff, «Empire of the Dawn».
|
||||
|
||||
Зачем. Промт эксп-21 заказывал одну пару (zh→ru), и первая редакция мерила топологию только на
|
||||
ней. Но цель проекта №1 канона — ОДИН движок на все пары, и вывод о топологии, снятый на одной
|
||||
паре, этот вопрос не закрывает: ревью-вопрос по умолчанию «заработает ли пара, которой в репо
|
||||
ещё НЕТ, без правки Go?» здесь звучит как «переживёт ли вывод смену пары». Расширение — по
|
||||
слову владельца 07.08.
|
||||
|
||||
Почему именно эта книга. Собственная проба контаминации пака: `kristoff-en` узнаётся моделью
|
||||
1/5 и даёт 0/5 по клоуз-тесту, тогда как `gu-zhenren` — 4/5 узнавания при пороге «≥3/5 = книга
|
||||
модели ИЗВЕСТНА». То есть материал второй пары ЧИЩЕ основного, и часть оговорки «эмпирика на
|
||||
претрейн-классике предварительна» (строка 55 бэклога) закрывается именно здесь.
|
||||
|
||||
Что тут особенного — ЭДЖ-КЕЙС, названный владельцем. Английский текст несёт французский
|
||||
культурный слой тремя разными классами, и переводчик обязан развести их по-разному:
|
||||
* имена и топонимы (`François`, `Fabién`, `Château`, `Lumière`) — передаются по ФРАНЦУЗСКИМ
|
||||
правилам чтения, а не по английским, и одинаково на всю книгу;
|
||||
* вставные французские реплики (`Merci, saint-père`, `mon dieu`) — чужая речь внутри чужой,
|
||||
её сохранение/перевод есть решение, а не мелочь;
|
||||
* офранцуженная английская лексика (`façade`) — обычное русское слово.
|
||||
Отбор единиц СТРАТИФИЦИРОВАН по этому признаку: половина с французским слоем, половина без.
|
||||
Стратификация объявлена ДО покупок и позволяет читать эффект отдельно на трудном материале.
|
||||
|
||||
Банкноты у армов этой пары НЕТ — объявленная девиация: подписанного глоссария для книги не
|
||||
существует, а выдумывать канон полигон не вправе (CLAUDE.md). Следствие: контрасты пары мерят
|
||||
ЧИСТУЮ топологию, без фактора банка; фактор банка мерится на zh контрастом A_law против A.
|
||||
|
||||
Запуск: --units инвентарь и стратификация ($0)
|
||||
--arms черновики и армы
|
||||
--judge судейство контрастов
|
||||
--score свод ($0)
|
||||
"""
|
||||
from __future__ import annotations
|
||||
import html
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
import time
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
|
||||
REPO = Path("/home/ubuntu/projects/textmachine")
|
||||
for sub in ("editor_contract", "bank_arbitration", "editor_harness", "role_topology"):
|
||||
sys.path.insert(0, str(REPO / "eval" / sub))
|
||||
|
||||
import battery as B # noqa: E402
|
||||
import buy as BUY # noqa: E402
|
||||
import editor_wire_probe as P # noqa: E402
|
||||
import judges as J # noqa: E402
|
||||
import probe as Q # noqa: E402
|
||||
from bakeoff import boot_ci, client, uid_of # noqa: E402
|
||||
|
||||
import statistics # noqa: E402
|
||||
|
||||
EPUB = Path.home() / "books" / "Kristoff_Jay_-_Empire_of_the_Vampire_3_-_Empire_of_the_Dawn.epub"
|
||||
OUT = Path.home() / "books" / "role-topology"
|
||||
|
||||
N_PER_STRATUM = 6
|
||||
UNIT_CHARS = 1600
|
||||
REPS_PER_ORDER = 1
|
||||
CEIL_ARMS = 0.55
|
||||
CEIL_JUDGE = 1.45
|
||||
LED_ARMS = BUY.Ledger("армы en→ru", CEIL_ARMS, ("en2-*.json",), default_expect=0.03)
|
||||
LED_JUDGE = BUY.Ledger("судейство en→ru", CEIL_JUDGE, ("jve-*.json",), default_expect=0.008)
|
||||
|
||||
DRAFT_MODEL = "deepseek-v4-flash"
|
||||
ARMS = {
|
||||
"A": dict(model="deepseek-v4-pro", contract="full", think=None),
|
||||
"B": dict(model="glm-5", contract="full", think="off"),
|
||||
"D": dict(model="deepseek-v4-pro", contract="direct-reflow", think=None),
|
||||
"D_": dict(model="deepseek-v4-pro", contract="direct", think=None),
|
||||
}
|
||||
CONTRASTS = [("A", "F"), ("B", "F"), ("D", "A"), ("D", "D_")]
|
||||
|
||||
# Французский слой опознаётся по диакритике — признак механический и не требует словаря.
|
||||
_RE_FR = re.compile(r"\b\w*[éèêëàâäçôöûùüïî]\w*\b")
|
||||
_RE_LAT = re.compile(r"[A-Za-zÀ-ɏ]")
|
||||
|
||||
|
||||
def raw_text() -> str:
|
||||
z = zipfile.ZipFile(EPUB)
|
||||
parts = []
|
||||
for n in sorted(x for x in z.namelist() if x.endswith((".xhtml", ".html", ".htm"))):
|
||||
t = re.sub(r"<[^>]+>", " ", z.read(n).decode("utf-8", "ignore"))
|
||||
parts.append(re.sub(r"[ \t]+", " ", html.unescape(t)))
|
||||
return "\n".join(parts)
|
||||
|
||||
|
||||
def chunks() -> list[str]:
|
||||
"""Единицы боевого размера, нарезанные ПО ГРАНИЦАМ ПРЕДЛОЖЕНИЙ.
|
||||
|
||||
Рвать посреди фразы нельзя: обрубок меняет задачу переводчика и портит сравнение армов.
|
||||
"""
|
||||
text = raw_text()
|
||||
sents = re.split(r"(?<=[.!?”\"])\s+", text)
|
||||
out, buf = [], ""
|
||||
for s in sents:
|
||||
buf += s + " "
|
||||
if len(buf) >= UNIT_CHARS:
|
||||
out.append(buf.strip())
|
||||
buf = ""
|
||||
return [c for c in out if len(c) <= UNIT_CHARS * 1.4]
|
||||
|
||||
|
||||
_SERVICE = ("also by ", "about the author", "copyright", "all rights reserved", "isbn",
|
||||
"newsletter", "st. martin", "thank you for buying", "table of contents",
|
||||
"for email updates", "click here", "acknowledgments")
|
||||
|
||||
|
||||
def is_prose(c: str) -> bool:
|
||||
"""Проза ли это, или служебная страница книги.
|
||||
|
||||
⚠ Заведено 07.08 по адверсариальному ревью: единица `129b73ad5a` страты `plain` оказалась
|
||||
выходными данными и библиографией («ALSO BY JAY KRISTOFF», 18 названий, копирайт, рассылка).
|
||||
Армы её ПЕРЕВОДИЛИ, и она ушла в судейство и в слепое чтение. Фильтр страты был «нет токенов
|
||||
с диакритикой» — служебные страницы он притягивает по построению, потому что диакритики там
|
||||
нет никогда.
|
||||
|
||||
Два сигнала, оба механические: маркеры издательского аппарата и ПЛОТНОСТЬ ЗАВЕРШЁННЫХ
|
||||
ПРЕДЛОЖЕНИЙ. У прозы точек/восклицаний/вопросов на 1000 знаков много; у списка названий и
|
||||
выходных данных — мало, там строки без терминальной пунктуации.
|
||||
"""
|
||||
low = c.lower()
|
||||
if any(m in low for m in _SERVICE):
|
||||
return False
|
||||
return len(re.findall(r"[.!?]", c)) / max(1, len(c)) * 1000 >= 8.0
|
||||
|
||||
|
||||
def units() -> list[dict]:
|
||||
"""12 единиц: 6 с французским слоем, 6 без. Отбор детерминированный — по длине внутри страты.
|
||||
|
||||
Порог страты `fr`: ≥4 токена с диакритикой, из них ≥2 РАЗНЫХ. Одиночное `façade` французским
|
||||
слоем не является, и страту им засорять нельзя.
|
||||
"""
|
||||
fr, plain = [], []
|
||||
for c in chunks():
|
||||
if not is_prose(c):
|
||||
continue
|
||||
toks = _RE_FR.findall(c)
|
||||
if len(toks) >= 4 and len(set(toks)) >= 2:
|
||||
fr.append(c)
|
||||
elif not toks:
|
||||
plain.append(c)
|
||||
out = []
|
||||
for name, pool in (("fr", fr), ("plain", plain)):
|
||||
# ⚠ Ключ сортировки — (длина, uid), а НЕ одна длина. Поймано исполнением 07.08: два
|
||||
# вызова подряд вернули разные наборы единиц. Причина — `sorted(set(...), key=len)`:
|
||||
# при равной длине порядок задаётся обходом МНОЖЕСТВА строк, а он рандомизирован по
|
||||
# процессам (хеш-сид). Отбор при этом выглядел детерминированным. Последствие было бы
|
||||
# тихим и дорогим: армы покупаются на один набор, судейство идёт по другому.
|
||||
pool = sorted(set(pool), key=lambda c: (len(c), uid_of(c)))
|
||||
lo = (len(pool) - N_PER_STRATUM) // 2
|
||||
for c in pool[lo:lo + N_PER_STRATUM]:
|
||||
out.append(dict(source=c, uid=uid_of(c), stratum=name,
|
||||
fr_tokens=sorted(set(_RE_FR.findall(c)))))
|
||||
return out
|
||||
|
||||
|
||||
PAIR = Path(__file__).parent / "prompts" / "en-ru"
|
||||
BRIEF = {"source_lang": "en", "target_lang": "ru", "genre": "тёмное фэнтези",
|
||||
"audience": "взрослые читатели фэнтези", "title": "Empire of the Dawn",
|
||||
"venuti": "0.60", "honorifics": "keep", "transcription": "practical",
|
||||
"footnotes": "minimal"}
|
||||
|
||||
|
||||
def render_pair(path: Path, text: str, draft: str) -> tuple[str, str]:
|
||||
"""Рендер пар-пакета ТЕМ ЖЕ путём, что боевой: снятие комментариев, дроп FEWSHOT, подстановка.
|
||||
|
||||
⚠ Флаг `en=True` харнесса экспов 18–20 сюда НЕ годится и был пойман исполнением: он означает
|
||||
«переводить НА английский» (трек zh→en экспа-19), и первая сборка армов этой пары уехала бы
|
||||
с промптом «editor of a translation into English» и брифом «с языка zh на язык en».
|
||||
Здесь источник английский, цель русская — это другая пара, и ей нужен свой пар-пакет.
|
||||
"""
|
||||
canon = P.strip_comments(path.read_text(encoding="utf-8"))
|
||||
sys_part, user = canon.split(P.USER_SEP, 1)
|
||||
core = sys_part.split(P.FEWSHOT_SEP, 1)[0].strip()
|
||||
old = P.RENDER_VARS
|
||||
P.RENDER_VARS = BRIEF
|
||||
try:
|
||||
return P.render(core, text, draft), P.render(user.strip(), text, draft)
|
||||
finally:
|
||||
P.RENDER_VARS = old
|
||||
|
||||
|
||||
def translator_msgs(text: str) -> tuple[str, str]:
|
||||
return render_pair(PAIR / "translator.md", text, "")
|
||||
|
||||
|
||||
def build_msgs(arm: str, u: dict, draft: str) -> list[dict]:
|
||||
tpl = {"full": "editor.md", "direct": "translator.md",
|
||||
"direct-reflow": "translator-reflow.md"}[ARMS[arm]["contract"]]
|
||||
sys_msg, user = render_pair(PAIR / tpl, u["source"], draft)
|
||||
return [{"role": "system", "content": sys_msg}, {"role": "user", "content": user.strip()}]
|
||||
|
||||
|
||||
def run_draft(u: dict) -> dict:
|
||||
sys_msg, user = translator_msgs(u["source"])
|
||||
kw = dict(model=DRAFT_MODEL, max_tokens=16000, extra_body={"reasoning_effort": "low"},
|
||||
messages=[{"role": "system", "content": sys_msg}, {"role": "user", "content": user}])
|
||||
# Эхо-мина у DeepSeek описана для плотного CJK; на латинице класс не подтверждён, поэтому
|
||||
# сторож здесь ловит не «ханьцзы», а НЕПЕРЕВЕДЁННУЮ ЛАТИНИЦУ — тот же дефект, другой алфавит.
|
||||
for attempt in (1, 2, 3):
|
||||
tag = f"en2-DRAFT-{u['uid']}" + ("" if attempt == 1 else f"-r{attempt}")
|
||||
rec = BUY.purchase(LED_ARMS, tag, DRAFT_MODEL, client(DRAFT_MODEL), kw,
|
||||
arm="DRAFT", uid=u["uid"], attempt=attempt)
|
||||
if rec.get("skipped") or latin_share(rec["content"]) <= 0.15:
|
||||
return rec
|
||||
print(f" ⚠ латиница в черновике {u['uid'][:6]} (попытка {attempt}) — ре-ген")
|
||||
return rec
|
||||
|
||||
|
||||
def latin_share(text: str) -> float:
|
||||
letters = [c for c in text if c.isalpha()]
|
||||
return len(_RE_LAT.findall("".join(letters))) / max(1, len(letters))
|
||||
|
||||
|
||||
def draft_text(uid: str) -> str:
|
||||
f = OUT / f"en2-DRAFT-{uid}.json"
|
||||
best = ""
|
||||
for g in sorted(OUT.glob(f"en2-DRAFT-{uid}*.json")):
|
||||
c = json.loads(g.read_text(encoding="utf-8")).get("content", "")
|
||||
if c and latin_share(c) <= 0.15:
|
||||
best = c
|
||||
return best or (json.loads(f.read_text(encoding="utf-8"))["content"] if f.exists() else "")
|
||||
|
||||
|
||||
def draft_cost(uid: str) -> float:
|
||||
return sum(json.loads(g.read_text(encoding="utf-8")).get("cost_usd", 0.0)
|
||||
for g in OUT.glob(f"en2-DRAFT-{uid}*.json"))
|
||||
|
||||
|
||||
def run_arm(arm: str, u: dict) -> dict:
|
||||
spec = ARMS[arm]
|
||||
model = spec["model"]
|
||||
kw: dict = dict(model=model, messages=build_msgs(arm, u, draft_text(u["uid"])),
|
||||
max_tokens=16000, temperature=0.4)
|
||||
if spec["think"] == "off":
|
||||
if not model.startswith("glm"):
|
||||
raise SystemExit(f"гашение размышления у {model} не описано в quirks — не гадаю")
|
||||
kw["extra_body"] = {"thinking": {"type": "disabled"}}
|
||||
return BUY.purchase(LED_ARMS, f"en2-{arm}-{u['uid']}", model, client(model), kw,
|
||||
arm=arm, uid=u["uid"], contract=spec["contract"], think=spec["think"])
|
||||
|
||||
|
||||
def text_of(arm: str, u: dict) -> str:
|
||||
if arm == "F":
|
||||
return draft_text(u["uid"])
|
||||
f = OUT / f"en2-{arm}-{u['uid']}.json"
|
||||
return json.loads(f.read_text(encoding="utf-8"))["content"] if f.exists() else ""
|
||||
|
||||
|
||||
# --- ФРАНЦУЗСКИЙ СЛОЙ: детерминированная проверка, судья не нужен -------------------------------
|
||||
|
||||
def check_french(source: str, final: str) -> dict:
|
||||
"""Как обошлись с французским слоем. Три числа, каждое читается само по себе.
|
||||
|
||||
`leaked` — французский токен остался ЛАТИНИЦЕЙ в русском тексте. Это дефект передачи:
|
||||
читатель кириллического издания получает нечитаемое слово.
|
||||
`rendered` — токен передан кириллицей хотя бы раз (передача состоялась).
|
||||
`split` — один и тот же французский токен передан РАЗНЫМИ кириллическими формами внутри
|
||||
одной единицы. Это прокси главной продуктовой боли — консистентности имени на всю книгу;
|
||||
глоссария у книги нет, поэтому мерится расщепление внутри окна, а не против канона.
|
||||
"""
|
||||
toks = sorted({t for t in _RE_FR.findall(source) if len(t) >= 4})
|
||||
leaked = [t for t in toks if re.search(rf"\b{re.escape(t)}\b", final)]
|
||||
# Кириллические кандидаты-передачи: слова с заглавной буквы вокруг позиций, где стоял токен.
|
||||
cyr_words = re.findall(r"\b[А-ЯЁ][а-яё]+(?:-[А-ЯЁ]?[а-яё]+)?\b", final)
|
||||
forms: dict[str, set[str]] = {}
|
||||
for t in toks:
|
||||
head = _fold(t[:3])
|
||||
cand = {w for w in cyr_words if _fold_cyr(w[:3]) == head}
|
||||
if cand:
|
||||
forms[t] = cand
|
||||
split = {t: sorted(v) for t, v in forms.items() if len(v) > 1}
|
||||
return dict(fr_tokens=len(toks), leaked=leaked, leaked_n=len(leaked),
|
||||
rendered_n=len(forms), split=split, split_n=len(split))
|
||||
|
||||
|
||||
_FR2LAT = str.maketrans("éèêëàâäçôöûùüïî", "eeeeaaacoouuuii")
|
||||
_CYR2LAT = {"а": "a", "б": "b", "в": "v", "г": "g", "д": "d", "е": "e", "ж": "j", "з": "z",
|
||||
"и": "i", "й": "i", "к": "k", "л": "l", "м": "m", "н": "n", "о": "o", "п": "p",
|
||||
"р": "r", "с": "s", "т": "t", "у": "u", "ф": "f", "х": "h", "ц": "c", "ч": "c",
|
||||
"ш": "s", "щ": "s", "ы": "i", "э": "e", "ю": "u", "я": "a", "ё": "e"}
|
||||
|
||||
|
||||
def _fold(s: str) -> str:
|
||||
return s.lower().translate(_FR2LAT)
|
||||
|
||||
|
||||
def _fold_cyr(s: str) -> str:
|
||||
return "".join(_CYR2LAT.get(c, c) for c in s.lower())
|
||||
|
||||
|
||||
def cmd_units() -> None:
|
||||
us = units()
|
||||
print(f"единиц {len(us)} · страты: "
|
||||
f"{ {s: sum(1 for u in us if u['stratum'] == s) for s in ('fr', 'plain')} }")
|
||||
for u in us:
|
||||
print(f" {u['uid'][:6]} [{u['stratum']:5s}] {len(u['source']):5d} зн · "
|
||||
f"фр-токенов {len(u['fr_tokens']):2d} {u['fr_tokens'][:6]}")
|
||||
|
||||
|
||||
def cmd_arms() -> None:
|
||||
us = units()
|
||||
print(f"единиц {len(us)} · армов {len(ARMS)} + черновик · потолок ${CEIL_ARMS}")
|
||||
for u in us:
|
||||
rec = run_draft(u)
|
||||
if rec.get("skipped"):
|
||||
print("⛔ потолок — стоп")
|
||||
return
|
||||
print(f" DRAFT {u['uid'][:6]} [{u['stratum']:5s}] {len(rec['content']):5d} зн "
|
||||
f"латиница {latin_share(rec['content']):.3f} ${rec['cost_usd']:.5f}")
|
||||
time.sleep(0.2)
|
||||
for arm in ARMS:
|
||||
for u in us:
|
||||
rec = run_arm(arm, u)
|
||||
if rec.get("skipped"):
|
||||
print("⛔ потолок — стоп")
|
||||
return
|
||||
print(f" {arm:3s} {u['uid'][:6]} finish={rec.get('finish','?'):8s} "
|
||||
f"{len(rec['content']):5d} зн размышл {rec.get('reasoning_tokens',0):6d} "
|
||||
f"${rec['cost_usd']:.5f}")
|
||||
time.sleep(0.2)
|
||||
print(f"\nпотрачено ${LED_ARMS.spent():.6f}")
|
||||
|
||||
|
||||
def cmd_judge() -> None:
|
||||
us = units()
|
||||
total = len(us) * len(CONTRASTS) * len(J.JUDGES) * 2 * REPS_PER_ORDER
|
||||
print(f"голосов к покупке {total} · потолок ${CEIL_JUDGE}")
|
||||
n = 0
|
||||
for a, b in CONTRASTS:
|
||||
for u in us:
|
||||
ta, tb = text_of(a, u), text_of(b, u)
|
||||
if not ta.strip() or not tb.strip():
|
||||
print(f" ⚠ {a}/{b} {u['uid'][:6]}: пустой выход — пропуск")
|
||||
continue
|
||||
for judge in J.JUDGES:
|
||||
for order in (0, 1):
|
||||
for rep in range(1, REPS_PER_ORDER + 1):
|
||||
v1, v2 = (ta, tb) if order == 0 else (tb, ta)
|
||||
rec = BUY.purchase(
|
||||
LED_JUDGE, f"jve-{a}v{b}-{u['uid']}-o{order}-{judge}-r{rep}",
|
||||
judge, J.client(judge), J.vote_kw(judge, J.packet(u["source"], v1, v2)),
|
||||
contrast=f"{a}v{b}", uid=u["uid"], order=order, judge=judge,
|
||||
stratum=u["stratum"])
|
||||
if rec.get("skipped"):
|
||||
print("⛔ потолок судейства — стоп")
|
||||
return
|
||||
n += 1
|
||||
time.sleep(0.15)
|
||||
print(f" {a} против {b}: голосов {n} · ${LED_JUDGE.spent():.4f}")
|
||||
print(f"\nголосов {n} · ${LED_JUDGE.spent():.6f}")
|
||||
|
||||
|
||||
def _margins(a: str, b: str, uid: str, judge: str) -> dict[int, list[float]]:
|
||||
out: dict[int, list[float]] = {}
|
||||
for order in (0, 1):
|
||||
for rep in range(1, REPS_PER_ORDER + 1):
|
||||
f = OUT / f"jve-{a}v{b}-{uid}-o{order}-{judge}-r{rep}.json"
|
||||
if not f.exists():
|
||||
continue
|
||||
p = J.parse(json.loads(f.read_text(encoding="utf-8"))["content"])
|
||||
if p["В1-ВЕРНОСТЬ"] is None:
|
||||
continue
|
||||
e1 = sum(p[f"В1-{ax}"] or 0 for ax in J.AXES)
|
||||
e2 = sum(p[f"В2-{ax}"] or 0 for ax in J.AXES)
|
||||
out.setdefault(order, []).append((e2 - e1) if order == 0 else (e1 - e2))
|
||||
return out
|
||||
|
||||
|
||||
def cmd_score() -> None:
|
||||
us = units()
|
||||
print("ПАРНЫЕ ВЕРДИКТЫ en→ru. Знак перевеса обязан совпасть в ОБОИХ порядках у ОБОИХ судей.\n")
|
||||
print(f"{'контраст':14s}{'ед.':>5s}{'за перв.':>10s}{'за втор.':>10s}{'ничья':>7s}"
|
||||
f"{'перевес':>10s}{'95% ДИ':>18s}{'фр-страта':>11s}{'plain':>8s}")
|
||||
print("-" * 93)
|
||||
for a, b in CONTRASTS:
|
||||
wa = wb = tie = 0
|
||||
per_unit, by_str = [], {"fr": [], "plain": []}
|
||||
for u in us:
|
||||
pj, ms = {}, []
|
||||
for judge in J.JUDGES:
|
||||
bo = _margins(a, b, u["uid"], judge)
|
||||
if len(bo) < 2:
|
||||
continue
|
||||
m0, m1 = statistics.mean(bo[0]), statistics.mean(bo[1])
|
||||
ms += [m0, m1]
|
||||
pj[judge] = 1 if (m0 > 0 and m1 > 0) else (-1 if (m0 < 0 and m1 < 0) else 0)
|
||||
if len(pj) < len(J.JUDGES):
|
||||
continue
|
||||
per_unit.append(statistics.mean(ms))
|
||||
by_str[u["stratum"]].append(statistics.mean(ms))
|
||||
vals = set(pj.values())
|
||||
wa += vals == {1}
|
||||
wb += vals == {-1}
|
||||
tie += vals not in ({1}, {-1})
|
||||
if not per_unit:
|
||||
print(f"{a + ' пр. ' + b:14s}{'нет голосов':>20s}")
|
||||
continue
|
||||
lo, hi = boot_ci(per_unit)
|
||||
f_m = statistics.mean(by_str["fr"]) if by_str["fr"] else float("nan")
|
||||
p_m = statistics.mean(by_str["plain"]) if by_str["plain"] else float("nan")
|
||||
print(f"{a + ' пр. ' + b:14s}{wa + wb + tie:5d}{wa:10d}{wb:10d}{tie:7d}"
|
||||
f"{statistics.mean(per_unit):+10.2f} [{lo:+.2f}, {hi:+.2f}]{f_m:+11.2f}{p_m:+8.2f}")
|
||||
|
||||
print("\nФРАНЦУЗСКИЙ СЛОЙ (детерминированно, судья не нужен)\n")
|
||||
print(f"{'арм':5s}{'ед.':>5s}{'фр-токенов':>12s}{'латиница':>10s}{'передано':>10s}"
|
||||
f"{'расщеплено':>12s}{'$/единицу':>11s}")
|
||||
print("-" * 65)
|
||||
frs = [u for u in us if u["stratum"] == "fr"]
|
||||
for arm in ("F", *ARMS):
|
||||
rows, costs = [], []
|
||||
for u in frs:
|
||||
t = text_of(arm, u)
|
||||
if not t.strip():
|
||||
continue
|
||||
rows.append(check_french(u["source"], t))
|
||||
c = draft_cost(u["uid"]) if arm in ("F", "A", "B") else 0.0
|
||||
f = OUT / f"en2-{arm}-{u['uid']}.json"
|
||||
if arm != "F" and f.exists():
|
||||
c += json.loads(f.read_text(encoding="utf-8"))["cost_usd"]
|
||||
costs.append(c)
|
||||
if not rows:
|
||||
continue
|
||||
print(f"{arm:5s}{len(rows):5d}{sum(r['fr_tokens'] for r in rows):12d}"
|
||||
f"{sum(r['leaked_n'] for r in rows):10d}{sum(r['rendered_n'] for r in rows):10d}"
|
||||
f"{sum(r['split_n'] for r in rows):12d}"
|
||||
f"{(statistics.median(costs) if costs else 0):11.5f}")
|
||||
|
||||
print("\nОБЩАЯ БАТАРЕЯ (языконезависимые проверки)\n")
|
||||
print(f"{'арм':5s}{'ед.':>5s}{'типогр.':>9s}{'ты/вы микс':>12s}")
|
||||
print("-" * 31)
|
||||
for arm in ("F", *ARMS):
|
||||
rows = [B.run_unit(B.Unit(source=u["source"], draft=draft_text(u["uid"]),
|
||||
final=text_of(arm, u), cards={}))
|
||||
for u in us if text_of(arm, u).strip()]
|
||||
if rows:
|
||||
print(f"{arm:5s}{len(rows):5d}{sum(r['typography']['violations'] for r in rows):9d}"
|
||||
f"{sum(r['ty_vy']['mixed_in_unit'] for r in rows):12d}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
cmd = {"--units": cmd_units, "--arms": cmd_arms, "--judge": cmd_judge,
|
||||
"--score": cmd_score}.get(sys.argv[1] if len(sys.argv) > 1 else "")
|
||||
if cmd:
|
||||
cmd()
|
||||
else:
|
||||
print(__doc__)
|
||||
128
eval/role_topology/postproc.py
Normal file
128
eval/role_topology/postproc.py
Normal file
|
|
@ -0,0 +1,128 @@
|
|||
#!/usr/bin/env python3
|
||||
"""ДЕТЕРМИНИРОВАННЫЙ ПОСТ-ПРОЦЕССОР ЧЕРНОВИКА. $0 по ключам провайдеров.
|
||||
|
||||
Зачем. Оба слепых читателя независимо назвали главным источником непечатного текста НЕ перевод,
|
||||
а механический слой: прямая речь в кавычках вместо тире, служебная преамбула модели, протёкшая
|
||||
markdown-разметка, авторский текст под тире. Если дешёвая детерминированная правка поверх
|
||||
черновика закрывает существенную часть разрыва «черновик против редактора» (+1.88), то второй
|
||||
проход покупает меньше, чем кажется, и часть запланированных покупок уточняет цену того, что не
|
||||
нужно. Проверка стоит только голосов судей — сам пост-процессор бесплатен.
|
||||
|
||||
Правила закрытые и проверяемые. Каждое чинит класс, названный читателями ЦИТАТОЙ, и ни одно не
|
||||
трогает содержание перевода: пост-процессор не переставляет слова, не сливает абзацы и не правит
|
||||
лексику. Он снимает ровно то, что является браком вывода, а не браком перевода.
|
||||
|
||||
⚠ Чего он НЕ делает и почему: слипшиеся абзацы не разделяются (границу хода детерминированно не
|
||||
установить, а неверная разбивка хуже слипшейся), непереведённые остатки не переводятся (это
|
||||
работа модели), калька не правится (это суждение).
|
||||
|
||||
Запуск: --selfcheck проверка правил на входах с известным ответом, $0
|
||||
--emit породить арм F_post из черновиков, $0
|
||||
"""
|
||||
from __future__ import annotations
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
REPO = Path("/home/ubuntu/projects/textmachine")
|
||||
sys.path.insert(0, str(REPO / "eval" / "role_topology"))
|
||||
|
||||
# Служебная преамбула модели перед текстом. Поймана слепым чтением дословно:
|
||||
# «Вот перевод фрагмента на русский язык в соответствии с вашими требованиями:».
|
||||
_RE_PREAMBLE = re.compile(
|
||||
r"^\s*(?:вот\s+)?(?:перевод|отредактированн\w+\s+текст|результат)[^\n:]{0,80}:\s*\n+",
|
||||
re.I)
|
||||
_RE_MD_HEAD = re.compile(r"^\s{0,3}#{1,6}\s+", re.M) # ### Раздел 17
|
||||
_RE_MD_BOLD = re.compile(r"\*\*(.+?)\*\*", re.S) # **Глава 19**
|
||||
_RE_MD_ITAL = re.compile(r"(?<!\*)\*(?!\s)(.+?)(?<!\s)\*(?!\*)", re.S)
|
||||
_RE_HR = re.compile(r"^\s*(?:---+|===+|\*\s*\*\s*\*)\s*$", re.M)
|
||||
# Реплика, целиком взятая в кавычки на отдельной строке: «…» или "…" в начале абзаца.
|
||||
_RE_QUOTED_LINE = re.compile(r'^[ \t]*[«"„]\s*(.+?)\s*[»"“]([ \t]*[,.—-].*)?$', re.M)
|
||||
_RE_MULTI_BLANK = re.compile(r"\n{3,}")
|
||||
|
||||
|
||||
def strip_service(text: str) -> str:
|
||||
"""Убрать служебную преамбулу и markdown-разметку, протёкшую в тело книги."""
|
||||
t = _RE_PREAMBLE.sub("", text)
|
||||
t = _RE_MD_HEAD.sub("", t)
|
||||
t = _RE_MD_BOLD.sub(r"\1", t)
|
||||
t = _RE_MD_ITAL.sub(r"\1", t)
|
||||
t = _RE_HR.sub("", t)
|
||||
return _RE_MULTI_BLANK.sub("\n\n", t).strip()
|
||||
|
||||
|
||||
def dialogue_dash(text: str) -> str:
|
||||
"""Реплику, стоящую отдельным абзацем в кавычках, оформить русским тире.
|
||||
|
||||
⚠ Правило намеренно узкое: трогается только строка, которая ЦЕЛИКОМ является закавыченной
|
||||
репликой (возможно со словами автора после закрывающей кавычки). Кавычки ВНУТРИ абзаца не
|
||||
трогаются — там они могут быть цитатой, названием или мыслью, и различить это детерминированно
|
||||
нельзя. Узкое правило чинит меньше, но не портит.
|
||||
"""
|
||||
def repl(m: re.Match) -> str:
|
||||
body, tail = m.group(1), (m.group(2) or "")
|
||||
if len(body) < 2:
|
||||
return m.group(0)
|
||||
return f"— {body}{tail}"
|
||||
return _RE_QUOTED_LINE.sub(repl, text)
|
||||
|
||||
|
||||
def process(text: str) -> str:
|
||||
return dialogue_dash(strip_service(text))
|
||||
|
||||
|
||||
CASES: list[tuple[str, str, str]] = [
|
||||
("служебная преамбула",
|
||||
"Вот перевод фрагмента на русский язык в соответствии с вашими требованиями:\n\nОн вышел.",
|
||||
"Он вышел."),
|
||||
("markdown-заголовок", "### Раздел семнадцатый\n\nОн вышел.", "Раздел семнадцатый\n\nОн вышел."),
|
||||
("markdown-жирный", "**Глава 19: Цикада!**\n\nТекст.", "Глава 19: Цикада!\n\nТекст."),
|
||||
("реплика в ёлочках", '«Время летит. Садись», — сказал он.', "— Время летит. Садись, — сказал он."),
|
||||
("реплика в лапках", '"Спасибо", — ответил племянник.', "— Спасибо, — ответил племянник."),
|
||||
("кавычки ВНУТРИ абзаца не трогаются",
|
||||
'Он вспомнил «Тысячу и одну ночь» и усмехнулся.',
|
||||
'Он вспомнил «Тысячу и одну ночь» и усмехнулся.'),
|
||||
("уже тире — не трогается", "— Время летит. Садись, — сказал он.",
|
||||
"— Время летит. Садись, — сказал он."),
|
||||
("чистый текст не меняется", "Он вышел за дверь. Небо было серым.",
|
||||
"Он вышел за дверь. Небо было серым."),
|
||||
]
|
||||
|
||||
|
||||
def selfcheck() -> int:
|
||||
bad = 0
|
||||
for name, src, want in CASES:
|
||||
got = process(src)
|
||||
ok = got == want
|
||||
bad += not ok
|
||||
print(f"[{'OK ' if ok else 'ПРОВАЛ'}] {name}" + ("" if ok else f"\n получено: {got!r}\n ждали: {want!r}"))
|
||||
print(f"\n{'ПРАВИЛА ЧИСТЫ' if not bad else f'ПРОВАЛОВ: {bad}'} ({len(CASES) - bad}/{len(CASES)})")
|
||||
return bad
|
||||
|
||||
|
||||
def emit() -> None:
|
||||
"""Породить выходы арма F_post: пост-процессор поверх ТОГО ЖЕ черновика, что правят армы."""
|
||||
import json # noqa: PLC0415
|
||||
import bakeoff as BO # noqa: PLC0415
|
||||
OUT = Path.home() / "books" / "role-topology"
|
||||
changed = 0
|
||||
for u in BO.units():
|
||||
src = BO.text_of("F", u)
|
||||
dst = process(src)
|
||||
(OUT / f"bo2-F_post-{u['uid']}.json").write_text(
|
||||
json.dumps(dict(tag=f"bo2-F_post-{u['uid']}", arm="F_post", uid=u["uid"],
|
||||
model="$0-postproc", cost_usd=0.0, content=dst,
|
||||
changed=dst != src, delta_chars=len(dst) - len(src)),
|
||||
ensure_ascii=False), encoding="utf-8")
|
||||
changed += dst != src
|
||||
print(f"единиц {len(BO.units())} · пост-процессор изменил {changed}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
a = sys.argv[1:] or ["--selfcheck"]
|
||||
if a[0] == "--selfcheck":
|
||||
sys.exit(1 if selfcheck() else 0)
|
||||
elif a[0] == "--emit":
|
||||
emit()
|
||||
else:
|
||||
print(__doc__)
|
||||
47
eval/role_topology/prompts/en-ru/editor.md
Normal file
47
eval/role_topology/prompts/en-ru/editor.md
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
<!-- ПАР-ПАКЕТ en→ru, заведён полигоном 07.08 для эксп-21. Производный от боевого
|
||||
backend/prompts/zh-ru/editor.md. Изменены РОВНО четыре места, и все четыре — те, где
|
||||
боевой файл говорит о китайском: строка мер, рамка дискурс-перевёрстки, оговорка про
|
||||
построчную разбивку исходника и пример с чэнъюй. Остальное перенесено дословно.
|
||||
Смысл замера: если новая пара требует ТОЛЬКО этих правок в данных, общность движка
|
||||
держится; если потребует правки Go — не держится. -->
|
||||
Ты — литературный редактор художественного перевода на русский язык. Тебе даны ИСХОДНЫЙ текст и его черновой перевод.
|
||||
Жанр книги: {{genre}}. Аудитория: {{audience}}. Книга: «{{title}}».
|
||||
|
||||
Твой мандат — художественная редактура черновика со сверкой по исходнику:
|
||||
- Сверяй смысл черновика с исходным текстом (язык «{{source_lang}}»): исправляй искажения смысла, неверно понятые реалии, кальки жестов и идиом — переводчик мог передать выражение буквально. Опирайся на исходник, но пиши по-русски, а не подстрочником.
|
||||
- Правь стиль: убирай кальки с языка «{{source_lang}}», канцелярит, лишние повторы, неестественные конструкции; пиши живым литературным русским. НО повтор, который стоит в тех же (параллельных) позициях самого исходника, — это авторский рефрен: сохраняй его, не «разнообразь» лексику там, где автор намеренно повторяется.
|
||||
- Имена собственные, термины и реалии передавай СТРОГО по приведённому глоссарию (если он приложен): приводи любые расхождения к каноническим формам, склоняя по контексту; не придумывай иных вариантов их передачи.
|
||||
- Вёрстка: собирай повествование в естественные русские абзацы. Реплики прямой речи начинай с нового абзаца через тире «—». Максимума длины абзаца нет.
|
||||
- Сохраняй ПОЛНОТУ черновика: не выбрасывай и не добавляй предложения; не пересочиняй сцены.
|
||||
- Единицы и приёмы (общие для en→ru): МЕРЫ приводи к привычным читателю единицам (имперские длины и веса — метрическими эквивалентами либо привычными приблизительными оборотами). ЧУЖОЙ ЯЗЫК ВНУТРИ ИСХОДНИКА (французские имена, топонимы, вставные реплики) — отдельный слой: имена и топонимы транскрибируй по правилам чтения их собственного языка, а не по английским, и держи ОДНУ форму на весь текст; вставные реплики сохраняй чужими. СТИХИ и названия классики — по смыслу, не транслитерацией. Род и форму имён и терминов сверяй по глоссарию.
|
||||
- Хонорифики: {{honorifics}}. Баланс форенизация/доместикация: {{venuti}}.
|
||||
|
||||
Выведи ТОЛЬКО отредактированный текст перевода — без служебных преамбул, комментариев, пояснений, заметок о правках и markdown-заголовков.
|
||||
|
||||
ДИСКУРС-ПЕРЕВЁРСТКА (приведение английского повествовательного членения к русской абзацной норме — это КОНВЕНЦИЯ русского языка, а не вольность):
|
||||
1. Молча определи смысловые ходы фрагмента: единый фокал/наблюдатель; действие и его непосредственная реакция; одна тема рассуждения; границы — смена говорящего, времени, места или фокала.
|
||||
2. Один повествовательный ход оформляй ОДНИМ русским абзацем. Английская проза часто дробит ход на короткие абзацы ради ритма — в русском издательском наборе такая дробь читается как обрывочность: собирай их обратно причастными и деепричастными оборотами, подчинительными союзами и связками. Не сливай ТОЛЬКО ради длины и не теряй смысловые атомы.
|
||||
3. Каждую новую реплику начинай с нового абзаца через тире «—»; слова автора при той же реплике оставляй в её абзаце. Английские кавычки прямой речи заменяй русским тире.
|
||||
4. Разрешено объединять и делить русские предложения; число предложений и абзацев совпадать с исходником НЕ обязано. Запрещено терять смысловые атомы (участников, действия, числа, полярность, заголовки, а также идиомы: их передавай смыслом, не сворачивая в общее место, и не калькируя дословно).
|
||||
|
||||
---FEWSHOT---
|
||||
Примеры требуемой ПЕРЕВЁРСТКИ (показана структура вывода, не стиль):
|
||||
|
||||
[narrative N:1] Исходник дробью — «He stepped outside.» / «The sky was grey.» / «He sighed.» / «The road ran on.» — становится ОДНИМ абзацем:
|
||||
Он вышел за дверь. Небо было серым; вздохнув, он смотрел, как дорога уходит вдаль.
|
||||
|
||||
[dialogue 1:N] Исходник — «The uncle frowned. "Time flies. Sit down," he said. "Thank you," the nephew replied.» — становится:
|
||||
Дядя нахмурился.
|
||||
— Время летит. Садись, — сказал он.
|
||||
— Спасибо, — ответил племянник.
|
||||
|
||||
[focal-shift] При смене наблюдателя/места/времени — новый абзац, даже если в исходнике это продолжение той же строки.
|
||||
|
||||
---USER---
|
||||
Исходный текст (язык «{{source_lang}}»):
|
||||
|
||||
{{text}}
|
||||
|
||||
Черновик перевода для редактуры:
|
||||
|
||||
{{draft}}
|
||||
33
eval/role_topology/prompts/en-ru/translator-reflow.md
Normal file
33
eval/role_topology/prompts/en-ru/translator-reflow.md
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
<!-- ПАР-ПАКЕТ en→ru, арм D (однопроходка С мандатом перевёрстки). Собран так же, как
|
||||
zh-ru-аналог эксп-20: боевой translator этой пары + блок ДИСКУРС-ПЕРЕВЁРСТКИ из
|
||||
editor.md ЭТОЙ ЖЕ пары. Формулировка мандата одна на пару — иначе контраст D/D_
|
||||
мерил бы разницу текстов, а не наличие мандата. -->
|
||||
Ты — профессиональный литературный переводчик с языка «{{source_lang}}» на язык «{{target_lang}}».
|
||||
Жанр книги: {{genre}}. Аудитория: {{audience}}. Книга: «{{title}}».
|
||||
|
||||
Правила перевода (translation brief):
|
||||
- Хонорифики: {{honorifics}}. Транскрипция имён и реалий: {{transcription}}.
|
||||
- Баланс форенизация/доместикация (0 — полная адаптация, 1 — сохранение чужого): {{venuti}}.
|
||||
- Сноски: {{footnotes}}.
|
||||
- Переводи ВСЕ предложения исходника: ничего не пропускай, не сокращай и не добавляй от себя.
|
||||
- Вёрстка — по правилам ДИСКУРС-ПЕРЕВЁРСТКИ, приведённым ниже.
|
||||
- Пиши живым литературным русским языком; избегай канцелярита и калек с исходного языка.
|
||||
|
||||
Единицы и приёмы (общие для en→ru):
|
||||
- МЕРЫ переводи в привычные читателю единицы: имперские длины и веса (foot, mile, inch, yard, pound, stone) передавай метрическими эквивалентами либо привычными русскому читателю приблизительными («в двух шагах», «на добрую версту»), сам термин не транслитерируй.
|
||||
- ЧУЖОЙ ЯЗЫК ВНУТРИ ИСХОДНИКА (французские имена, топонимы и реплики) — отдельный слой, и он передаётся ТРЕМЯ разными способами: имена и топонимы (François, Fabién, Château, Lumière) транскрибируй по правилам чтения ИСХОДНОГО языка этих имён, а не по английским, и держи ОДНУ форму на весь текст; вставные французские реплики сохраняй чужими, давая смысл там, где он не ясен из контекста; офранцуженную английскую лексику (façade и подобные) переводи обычным русским словом.
|
||||
- СТИХИ и названия классики передавай ПО СМЫСЛУ (не транслитерацией), опираясь на существующие русские переводы.
|
||||
- Род и форму имён собственных и терминов бери из приложенного глоссария (если он есть).
|
||||
|
||||
Выведи ТОЛЬКО перевод, без служебных преамбул, комментариев, пояснений и markdown-заголовков.
|
||||
|
||||
ДИСКУРС-ПЕРЕВЁРСТКА (приведение английского повествовательного членения к русской абзацной норме — это КОНВЕНЦИЯ русского языка, а не вольность):
|
||||
1. Молча определи смысловые ходы фрагмента: единый фокал/наблюдатель; действие и его непосредственная реакция; одна тема рассуждения; границы — смена говорящего, времени, места или фокала.
|
||||
2. Один повествовательный ход оформляй ОДНИМ русским абзацем. Английская проза часто дробит ход на короткие абзацы ради ритма — в русском издательском наборе такая дробь читается как обрывочность: собирай их обратно причастными и деепричастными оборотами, подчинительными союзами и связками. Не сливай ТОЛЬКО ради длины и не теряй смысловые атомы.
|
||||
3. Каждую новую реплику начинай с нового абзаца через тире «—»; слова автора при той же реплике оставляй в её абзаце. Английские кавычки прямой речи заменяй русским тире.
|
||||
4. Разрешено объединять и делить русские предложения; число предложений и абзацев совпадать с исходником НЕ обязано. Запрещено терять смысловые атомы (участников, действия, числа, полярность, заголовки, а также идиомы: их передавай смыслом, не сворачивая в общее место, и не калькируя дословно).
|
||||
|
||||
---USER---
|
||||
Переведи следующий фрагмент:
|
||||
|
||||
{{text}}
|
||||
28
eval/role_topology/prompts/en-ru/translator.md
Normal file
28
eval/role_topology/prompts/en-ru/translator.md
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
<!-- ПАР-ПАКЕТ en→ru, заведён полигоном 07.08 для эксп-21. Производный от боевого
|
||||
backend/prompts/zh-ru/translator.md. Изменён РОВНО один блок — «Единицы и приёмы»,
|
||||
единственное место файла, где боевой промт говорит о конкретной паре. Все остальные
|
||||
строки перенесены дословно, чтобы сравнение топологий между парами не смешивалось
|
||||
с разницей формулировок. Это и есть замер «сколько стоит новая пара»: данные, не Go. -->
|
||||
Ты — профессиональный литературный переводчик с языка «{{source_lang}}» на язык «{{target_lang}}».
|
||||
Жанр книги: {{genre}}. Аудитория: {{audience}}. Книга: «{{title}}».
|
||||
|
||||
Правила перевода (translation brief):
|
||||
- Хонорифики: {{honorifics}}. Транскрипция имён и реалий: {{transcription}}.
|
||||
- Баланс форенизация/доместикация (0 — полная адаптация, 1 — сохранение чужого): {{venuti}}.
|
||||
- Сноски: {{footnotes}}.
|
||||
- Переводи ВСЕ предложения исходника: ничего не пропускай, не сокращай и не добавляй от себя.
|
||||
- Вёрстка — по нормам русского языка, НЕ копируй построчную разбивку исходника: реплики прямой речи оформляй с нового абзаца через тире «—».
|
||||
- Пиши живым литературным русским языком; избегай канцелярита и калек с исходного языка.
|
||||
|
||||
Единицы и приёмы (общие для en→ru):
|
||||
- МЕРЫ переводи в привычные читателю единицы: имперские длины и веса (foot, mile, inch, yard, pound, stone) передавай метрическими эквивалентами либо привычными русскому читателю приблизительными («в двух шагах», «на добрую версту»), сам термин не транслитерируй.
|
||||
- ЧУЖОЙ ЯЗЫК ВНУТРИ ИСХОДНИКА (французские имена, топонимы и реплики) — отдельный слой, и он передаётся ТРЕМЯ разными способами: имена и топонимы (François, Fabién, Château, Lumière) транскрибируй по правилам чтения ИСХОДНОГО языка этих имён, а не по английским, и держи ОДНУ форму на весь текст; вставные французские реплики сохраняй чужими, давая смысл там, где он не ясен из контекста; офранцуженную английскую лексику (façade и подобные) переводи обычным русским словом.
|
||||
- СТИХИ и названия классики передавай ПО СМЫСЛУ (не транслитерацией), опираясь на существующие русские переводы.
|
||||
- Род и форму имён собственных и терминов бери из приложенного глоссария (если он есть).
|
||||
|
||||
Выведи ТОЛЬКО перевод, без служебных преамбул, комментариев, пояснений и markdown-заголовков.
|
||||
|
||||
---USER---
|
||||
Переведи следующий фрагмент:
|
||||
|
||||
{{text}}
|
||||
|
|
@ -1,295 +1,281 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Сверка НЕСУЩИХ ЧИСЕЛ отчёта `docs/experiments/21-role-topology.md` с сырьём.
|
||||
"""ВЕРИФИКАТОР ЧИСЕЛ ОТЧЁТА эксп-21 (вторая редакция). Ненулевой код возврата = отчёт не сдаётся.
|
||||
|
||||
Мандат промта: «Верификатор чисел отчёта отдельным скриптом; ненулевой код возврата = отчёт не
|
||||
сдаётся». Смысл в том, что число попадает в текст через мою же аналитику, и опечатка, устаревшая
|
||||
цифра после пере-съёмки и факт визуально неотличимы. Здесь каждое заявленное число выводится из
|
||||
персистированных артефактов ЗАНОВО и сравнивается с тем, что написано в отчёте — включая сам текст
|
||||
отчёта, чтобы расхождение ловилось, даже если я забуду поправить таблицу.
|
||||
Правило: каждое число отчёта пере-выводится ИЗ СЫРЬЯ и сверяется с напечатанным. Верификатор
|
||||
не читает выводы — он читает артефакты и текст, и падает на расхождении.
|
||||
|
||||
Три вещи, которых у первой редакции не было и которые тут есть по её разбору:
|
||||
* ДЕНЬГИ ВТОРЫМ НЕЗАВИСИМЫМ ПУТЁМ. `prices.cost` считает цену и при записи, и при сверке, поэтому
|
||||
их совпадение доказывает целостность персистенции, а НЕ правильность правила тарификации:
|
||||
ошибка внутри `cost` (как отсутствовавшая ветка Gemini) проходит обе стороны незамеченной.
|
||||
`independent_price` выписывает правило заново, по строкам quirks 00.
|
||||
* ИСПРАВНОСТЬ РАСКЛАДКИ проверяется равенством числа голосов по порядкам, а не малостью
|
||||
позиционной форы: зеркало убирает позицию из перевеса АРМА, а не из среднего по всем голосам.
|
||||
* АРМЫ СВЕРЯЮТСЯ С БУКВОЙ ПРОМТА исполнением — сборкой сообщений, а не чтением докстрингов.
|
||||
|
||||
Запуск: eval/.venv/bin/python eval/role_topology/verify_report.py
|
||||
"""
|
||||
from __future__ import annotations
|
||||
import json
|
||||
import re
|
||||
import statistics
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
REPO = Path("/home/ubuntu/projects/textmachine")
|
||||
sys.path.insert(0, str(REPO / "eval" / "role_topology"))
|
||||
sys.path.insert(0, str(REPO / "eval" / "bank_arbitration"))
|
||||
RAW = Path.home() / "books" / "role-topology"
|
||||
REPORT = REPO / "docs" / "experiments" / "21-role-topology.md"
|
||||
DOC = REPO / "docs" / "experiments" / "21-role-topology.md"
|
||||
|
||||
FAILS = 0
|
||||
|
||||
|
||||
def ck(claim: str, ok: bool, got: str = "") -> None:
|
||||
global FAILS
|
||||
global FAILS # noqa: PLW0603
|
||||
if not ok:
|
||||
FAILS += 1
|
||||
print(f"[{'OK ' if ok else 'ПРОВАЛ'}] {claim}" + ("" if ok else f" — сырьё даёт {got}"))
|
||||
|
||||
|
||||
def main() -> None:
|
||||
text = REPORT.read_text(encoding="utf-8")
|
||||
import detect_word as D # noqa: PLC0415
|
||||
|
||||
# §2.1 — таблица слоёв детектора выводится заново из земли, а не читается из json.
|
||||
rows = [json.loads(l) for l in D.LABELS.read_text(encoding="utf-8").splitlines() if l.strip()]
|
||||
bank = D.load_bank()
|
||||
expect = {0: (9, 34, 1), 1: (9, 28, 1), 2: (8, 11, 2), 3: (7, 2, 3)}
|
||||
for layer, want in expect.items():
|
||||
s = D.score(rows, layer, bank)
|
||||
got = (s["tp"], s["fp"], s["fn"])
|
||||
ck(f"§2.1 слой С{layer} = tp/fp/fn {want}", got == want, str(got))
|
||||
s3 = D.score(rows, 3, bank)
|
||||
ck("§2.1 итоговые precision 0.778 / recall 0.700",
|
||||
abs(s3["precision"] - 0.778) < 0.001 and abs(s3["recall"] - 0.700) < 0.001,
|
||||
f"{s3['precision']:.3f}/{s3['recall']:.3f}")
|
||||
ck("§2.1 таблица отчёта несёт те же 0.778 / 0.700",
|
||||
"0.778" in text and "0.700" in text)
|
||||
|
||||
import editor_wire_probe as WP # noqa: PLC0415
|
||||
plants = [w for rs in WP.DEFECTS.values() for cls, old, new, _, _ in rs if cls == "k1"
|
||||
for w in new.split() if w not in old.split()]
|
||||
hit = sum(1 for w in plants if D.verdict(w, bank)[0])
|
||||
ck("§2.1 выделенная валидация 6/6", (hit, len(plants)) == (6, 6), f"{hit}/{len(plants)}")
|
||||
|
||||
ck("§2.1 ущерб: ложных 2 из 9 флагов (22%)",
|
||||
s3["fp"] == 2 and s3["tp"] + s3["fp"] == 9, f"{s3['fp']}/{s3['tp'] + s3['fp']}")
|
||||
|
||||
# §2.2 — QE. Числа берутся из персистированного сырья прогона, а не пере-считываются моделью
|
||||
# (пере-счёт стоил бы 20 минут CPU и не добавил бы проверки: артефакт и есть сырьё).
|
||||
# ⚠ Артефакты РАЗВЕДЕНЫ ПО МОДЕЛЯМ. Первая редакция харнесса писала в общее имя, и прогон XL
|
||||
# молча затёр сырьё large — поймано этим же верификатором, отчёт бы остался «со слов сессии».
|
||||
import statistics # noqa: PLC0415
|
||||
expect_qe = {
|
||||
"large": dict(decoy=(69, 7.107, 65), k1=(6, 4), k2=(6, 3), clean=81),
|
||||
"xl": dict(decoy=(69, 7.000, 59), k1=(6, 4), k2=(6, 5), clean=81),
|
||||
}
|
||||
for tag, want in expect_qe.items():
|
||||
seg, dec = RAW / f"qe-segments-{tag}.json", RAW / f"qe-decoy-{tag}.json"
|
||||
if not seg.exists() or not dec.exists():
|
||||
ck(f"§2.2 сырьё QE ({tag}) на месте", False, f"нет qe-*-{tag}.json")
|
||||
continue
|
||||
d = json.loads(dec.read_text(encoding="utf-8"))
|
||||
wn, wm, wp = want["decoy"]
|
||||
ck(f"§2.2 {tag}: декой n={wn}, медиана {wm:+.3f}, направление {wp}/{wn}",
|
||||
len(d) == wn and abs(statistics.median(d) - wm) < 0.01
|
||||
and sum(1 for x in d if x > 0) == wp,
|
||||
f"n={len(d)} медиана={statistics.median(d):.3f} "
|
||||
f"плюсовых={sum(1 for x in d if x > 0)}")
|
||||
srows = json.loads(seg.read_text(encoding="utf-8"))
|
||||
by = {(r["window"], r["variant"], tuple(r["di"])): r["score"] for r in srows}
|
||||
for cls in ("k1", "k2"):
|
||||
want_n, want_pos = want[cls]
|
||||
deltas = [sc - by[(w, "clean", di)] for (w, v, di), sc in by.items()
|
||||
if v == cls and (w, "clean", di) in by
|
||||
and abs(sc - by[(w, "clean", di)]) > 1e-9]
|
||||
ck(f"§2.2 {tag}: парная Δ класса {cls} — {want_n} сегментов, Δ>0 в {want_pos}",
|
||||
len(deltas) == want_n and sum(1 for x in deltas if x > 0) == want_pos,
|
||||
f"{len(deltas)} сегментов, Δ>0 в {sum(1 for x in deltas if x > 0)}")
|
||||
clean = [r["score"] for r in srows if r["variant"] == "clean"]
|
||||
ck(f"§2.2 {tag}: здоровых сегментов {want['clean']}", len(clean) == want["clean"],
|
||||
str(len(clean)))
|
||||
|
||||
# §2.5 — калибровка гейта арма G. Считается из персистированных баллов, а не пере-моделируется:
|
||||
# артефакт и есть сырьё, а пере-счёт стоил бы полчаса CPU без добавления проверки.
|
||||
gu = RAW / "qe-guard-units-all.json"
|
||||
if not gu.exists():
|
||||
ck("§2.5 сырьё калибровки на месте", False, "нет qe-guard-units-all.json")
|
||||
def independent_price(model: str, r: dict) -> float:
|
||||
"""Цена по прайсу, посчитанная НЕ через `prices.cost` — второй независимый путь."""
|
||||
from prices import CANDIDATES # noqa: PLC0415
|
||||
_, _, pin, pcache, pout, _, _ = CANDIDATES[model]
|
||||
pt, ca = r.get("prompt_tokens", 0), r.get("cached_tokens", 0)
|
||||
ct, rt = r.get("completion_tokens", 0), r.get("reasoning_tokens", 0)
|
||||
tt = r.get("total_tokens", 0)
|
||||
if model.startswith("grok"):
|
||||
out = ct + rt # xAI: размышление СВЕРХ completion
|
||||
elif model.startswith("gemini"):
|
||||
out = max(tt - pt, ct + rt) # Gemini: размышление только в total (D22.3)
|
||||
else:
|
||||
import statistics # noqa: PLC0415
|
||||
from math import comb # noqa: PLC0415
|
||||
rows = json.loads(gu.read_text(encoding="utf-8"))
|
||||
d = [r["final"] - r["draft"] for r in rows]
|
||||
better = sum(1 for x in d if x < 0)
|
||||
k = max(better, len(d) - better)
|
||||
p = min(1.0, 2 * sum(comb(len(d), i) for i in range(k, len(d) + 1)) / (2 ** len(d)))
|
||||
ck("§2.5 единичный уровень: n=91, улучшил 43, медиана Δ 0.0000",
|
||||
len(d) == 91 and better == 43 and abs(statistics.median(d)) < 1e-9,
|
||||
f"n={len(d)} улучшил={better} медиана={statistics.median(d):+.4f}")
|
||||
ck("§2.5 знаковый тест p = 0.6752", abs(p - 0.6752) < 0.0005, f"{p:.4f}")
|
||||
ck("§2.5 паритет |Δ|<0.25 в 55 единицах",
|
||||
sum(1 for x in d if abs(x) < 0.25) == 55, str(sum(1 for x in d if abs(x) < 0.25)))
|
||||
out = ct # DeepSeek/OpenAI/Z.AI: размышление ВНУТРИ completion
|
||||
return (pt - ca) / 1e6 * pin + ca / 1e6 * pcache + out / 1e6 * pout
|
||||
|
||||
# §2.2 шаг 3 — мощность парного режима.
|
||||
pw = RAW / "qe-power-large.json"
|
||||
if not pw.exists():
|
||||
ck("§2.2 сырьё мощности на месте", False, "нет qe-power-large.json")
|
||||
else:
|
||||
import statistics # noqa: PLC0415
|
||||
rows = json.loads(pw.read_text(encoding="utf-8"))
|
||||
d = [r["flipped"] - r["base"] for r in rows]
|
||||
ck("§2.2 парная мощность: 72 пары, медиана +1.490, направление 66/72",
|
||||
len(d) == 72 and abs(statistics.median(d) - 1.490) < 0.001
|
||||
and sum(1 for x in d if x > 0) == 66,
|
||||
f"n={len(d)} медиана={statistics.median(d):+.3f} "
|
||||
f"плюсовых={sum(1 for x in d if x > 0)}")
|
||||
|
||||
# §2.3 — батарея. Пере-прогоняется по тому же корпусу целиком: это единственный способ
|
||||
# заметить, что правило поехало после правки.
|
||||
import battery as B # noqa: PLC0415
|
||||
corp = Path.home() / "books" / "gu-zhenren" / "labels" / "raw" / "corpus.jsonl"
|
||||
units = [json.loads(l) for l in corp.read_text(encoding="utf-8").splitlines() if l.strip()]
|
||||
units = [u for u in units if (u.get("final") or "").strip()]
|
||||
ck("§2.3 единиц с финалом 91", len(units) == 91, str(len(units)))
|
||||
typo = han = lost = inv = cand = nonconf = calq = 0
|
||||
for u in units:
|
||||
f = u["final"]
|
||||
typo += B.check_typography(f)["violations"]
|
||||
han += B.check_cjk_leak(f)["han_chars"]
|
||||
nm = B.check_numbers(u.get("source") or "", f)
|
||||
lost += nm["lost_n"]
|
||||
inv += nm["invented_n"]
|
||||
p = B.check_palladius(f, set())
|
||||
cand += p["name_candidates"]
|
||||
nonconf += p["nonconformant_text"]
|
||||
calq += bool(B.check_translationese(f)["calque_interjections"])
|
||||
n = len(units)
|
||||
for label, got, want in (("нарушений типографики/ед 0.18", typo / n, 0.18),
|
||||
("ханьцзы всего 4", han, 4),
|
||||
("потеряно величин/ед 0.27", lost / n, 0.275),
|
||||
("появилось величин/ед 0.27", inv / n, 0.27),
|
||||
("не-палладиевских срабатываний 6", nonconf, 6),
|
||||
("единиц с кальками 0", calq, 0)):
|
||||
ck(f"§2.3 {label}", abs(got - want) < 0.005, f"{got:.3f}" if isinstance(got, float)
|
||||
else str(got))
|
||||
ck("§2.3 кандидатов в имена 2184", cand == 2184, str(cand))
|
||||
def money(pat: str) -> float:
|
||||
return sum(json.loads(f.read_text(encoding="utf-8")).get("cost_usd", 0.0) or 0.0
|
||||
for f in RAW.glob(pat))
|
||||
|
||||
# §2.6 — вахта эффорта. Числа выводятся из сырья вызовов, а не из моей сводки.
|
||||
ew = sorted(RAW.glob("ew-*.json"))
|
||||
if not ew:
|
||||
ck("§2.6 сырьё вахты на месте", False, "нет ew-*.json")
|
||||
else:
|
||||
recs = [json.loads(f.read_text(encoding="utf-8")) for f in ew]
|
||||
by = {}
|
||||
for r in recs:
|
||||
by.setdefault(r["tag"].split("-")[1], []).append(r)
|
||||
ck("§2.6 армы: дефолт 12 · low 12 · xhigh 1",
|
||||
(len(by.get("default", [])), len(by.get("low", [])), len(by.get("xhigh", [])))
|
||||
== (12, 12, 1),
|
||||
str({k: len(v) for k, v in by.items()}))
|
||||
ck("§2.6 все вызовы finish=stop", all(r["finish"] == "stop" for r in recs),
|
||||
str(sorted({r["finish"] for r in recs})))
|
||||
med = {k: statistics.median(r["reasoning_chars"] for r in v) for k, v in by.items()}
|
||||
ck("§2.6 reasoning медианы: дефолт 2645.5 · low 2514 · xhigh 27185",
|
||||
(med.get("default"), med.get("low"), med.get("xhigh")) == (2645.5, 2514, 27185),
|
||||
str(med))
|
||||
pt = {k: sorted({r["prompt_tokens"] for r in v}) for k, v in by.items()}
|
||||
ck("§2.6 prompt_tokens: дефолт и low = 1945, xhigh = 2024 (серверная реакция)",
|
||||
pt.get("default") == [1945] and pt.get("low") == [1945] and pt.get("xhigh") == [2024],
|
||||
str(pt))
|
||||
spent = sum(r["cost_usd"] for r in recs)
|
||||
ck("§2.6 потрачено $0.047361 и потолок $0.05 НЕ пробит",
|
||||
abs(spent - 0.047361) < 1e-6 and spent <= 0.05, f"${spent:.6f}")
|
||||
ck("§2.6 запрос побайтно один во всех вызовах",
|
||||
len({json.dumps(r["messages"], ensure_ascii=False, sort_keys=True)
|
||||
for r in recs}) == 1, "запросы различаются — интерливинг недействителен")
|
||||
ck("отчёт заявляет ту же сумму", "0.047361" in text)
|
||||
|
||||
# §2.7 — Ф1. Цена пере-считывается ИЗ usage, а не читается из записанной: именно расхождение
|
||||
# этих двух путей и вскрыло аддитивный биллинг xAI.
|
||||
from prices import cost as price_of # noqa: PLC0415
|
||||
allscr = [json.loads(f.read_text(encoding="utf-8")) for f in sorted(RAW.glob("scr-*.json"))]
|
||||
allscr = [r for r in allscr if not r.get("skipped")]
|
||||
# Арм боевой конфигурации flash считается ОТДЕЛЬНО от скрина: он куплен после и по другой
|
||||
# ручке. Смешивать их — значит потерять именно то различие, ради которого арм и заводился.
|
||||
scr = [r for r in allscr if not r["tag"].startswith("flashlow-")]
|
||||
flashlow = [r for r in allscr if r["tag"].startswith("flashlow-")]
|
||||
if not scr:
|
||||
ck("§2.7 сырьё скрина на месте", False, "нет scr-*.json")
|
||||
else:
|
||||
ck("§2.7 клеток скрина 48", len(scr) == 48, str(len(scr)))
|
||||
ck("§2.7 арм боевой конфигурации flash: 4 клетки", len(flashlow) == 4, str(len(flashlow)))
|
||||
ck("§2.7 арм flash(low): выход НЕ пуст ни в одной клетке",
|
||||
all(r["content"].strip() for r in flashlow),
|
||||
f"{sum(1 for r in flashlow if not r['content'].strip())} пустых")
|
||||
recomputed = sum(price_of(r["model"], r["prompt_tokens"], r["cached_tokens"],
|
||||
r["completion_tokens"], r["reasoning_tokens"])
|
||||
for r in allscr)
|
||||
ck("§2.7 пере-счёт цены Ф1 из usage даёт $1.074724",
|
||||
abs(recomputed - 1.074724) < 1e-5, f"${recomputed:.6f}")
|
||||
ck("§2.7 отчёт признаёт превышение потолка Ф1", "1.074724" in text and "7.5%" in text)
|
||||
ck("§2.7 записанная цена совпадает с пере-счётом (аддитивный биллинг применён)",
|
||||
abs(sum(r["cost_usd"] for r in allscr) - recomputed) < 1e-5,
|
||||
f"записано ${sum(r['cost_usd'] for r in allscr):.6f}")
|
||||
flash = [r for r in scr if r["model"] == "deepseek-v4-flash"]
|
||||
ck("§2.7 deepseek-v4-flash на ДЕФОЛТЕ: 4/4 пустых при finish=length",
|
||||
len(flash) == 4 and all(r["finish"] == "length" and not r["content"].strip()
|
||||
for r in flash),
|
||||
f"{sum(1 for r in flash if not r['content'].strip())}/{len(flash)} пустых")
|
||||
grok = [r for r in scr if r["model"] == "grok-4.5"]
|
||||
ck("§2.7 у grok-4.5 размышление ПРЕВЫШАЕТ completion (улика аддитивности)",
|
||||
all(r["reasoning_tokens"] > r["completion_tokens"] for r in grok))
|
||||
|
||||
# Дисциплина отчёта: заявленные деньги обязаны сходиться с сырьём двумя путями.
|
||||
ck("статус-баннер отчёта соответствует состоянию фаз",
|
||||
"ВСЕ ФАЗЫ ИСПОЛНЕНЫ" in text)
|
||||
|
||||
# §2.9/§2.11 — бейк-офф и маршрутизация. Вердикты пере-считываются из ПЕРСИСТИРОВАННЫХ голосов,
|
||||
# а не читаются из сводки: сводку писал я, голоса писал провод.
|
||||
import judges as JJ # noqa: PLC0415
|
||||
def main() -> None: # noqa: PLR0915
|
||||
if not DOC.exists():
|
||||
ck("отчёт на месте", False, "нет файла")
|
||||
sys.exit(1)
|
||||
text = DOC.read_text(encoding="utf-8")
|
||||
import bakeoff as BO # noqa: PLC0415
|
||||
us = BO.units()
|
||||
ck("§2.9 единиц бейк-оффа 8", len(us) == 8, str(len(us)))
|
||||
for a, b, want in (("A", "F", (6, 0)), ("B", "F", (7, 0)),
|
||||
("D", "A", (2, 0)), ("D", "D_", (3, 0))):
|
||||
wa = wb = 0
|
||||
for ui in range(len(us)):
|
||||
pj = {}
|
||||
for judge in JJ.JUDGES:
|
||||
marg = []
|
||||
for rep in (1, 2, 3):
|
||||
o = (rep - 1) % 2
|
||||
f = RAW / f"jv-bo-{a}v{b}-u{ui}-o{o}-{judge}-r{rep}.json"
|
||||
if not f.exists():
|
||||
continue
|
||||
p = JJ.parse(json.loads(f.read_text(encoding="utf-8"))["content"])
|
||||
if p["В1-ВЕРНОСТЬ"] is None:
|
||||
continue
|
||||
e1 = sum(p[f"В1-{x}"] or 0 for x in JJ.AXES)
|
||||
e2 = sum(p[f"В2-{x}"] or 0 for x in JJ.AXES)
|
||||
marg.append((e2 - e1) if o == 0 else (e1 - e2))
|
||||
if len(marg) >= 2:
|
||||
pos = sum(1 for m in marg if m > 0)
|
||||
neg = sum(1 for m in marg if m < 0)
|
||||
pj[judge] = 1 if pos >= 2 and pos > neg else (
|
||||
-1 if neg >= 2 and neg > pos else 0)
|
||||
if len(pj) < 2:
|
||||
continue
|
||||
v = set(pj.values())
|
||||
wa += v == {1}
|
||||
wb += v == {-1}
|
||||
ck(f"§2.9 контраст {a} против {b} = {want[0]}:{want[1]}", (wa, wb) == want, f"{wa}:{wb}")
|
||||
import pair_en as E # noqa: PLC0415
|
||||
import judges as J # noqa: PLC0415
|
||||
|
||||
# §2.12 — карточки персонажей. Пин на то, что проверка рода НЕ инертна: пустые карточки
|
||||
# молча возвращают нули, и именно так этот пробел прожил весь пак незамеченным.
|
||||
print("=== СТРУКТУРА ЗАМЕРА ===")
|
||||
us_zh, us_en = BO.units(), E.units()
|
||||
ck("zh: 16 единиц", len(us_zh) == 16, str(len(us_zh)))
|
||||
ck("en: 12 единиц, страты 6/6", len(us_en) == 12
|
||||
and sum(1 for u in us_en if u["stratum"] == "fr") == 6, str(len(us_en)))
|
||||
ck("zh: единицы уникальны по нормализованной подписи",
|
||||
len({BO._dedup_sig(u["source"]) for u in us_zh}) == 16)
|
||||
ck("отбор единиц ДЕТЕРМИНИРОВАН (повторный вызов даёт тот же набор)",
|
||||
[u["uid"] for u in BO.units()] == [u["uid"] for u in us_zh]
|
||||
and [u["uid"] for u in E.units()] == [u["uid"] for u in us_en])
|
||||
|
||||
print("\n=== АРМЫ ПРОТИВ БУКВЫ ПРОМТА (проверка исполнением) ===")
|
||||
a_msgs = BO.build_msgs("A", us_zh[0])
|
||||
ck("арм A идёт БЕЗ банкноты (промт стр.29)",
|
||||
not any("КАНОНИЧЕСКИЕ ПЕРЕВОДЫ" in m["content"] for m in a_msgs))
|
||||
ck("закон пинится БЕЗ оговорки области (промт стр.29)",
|
||||
not any("глоссарий обязателен для употреблений" in m["content"]
|
||||
for arm in BO.ARMS for m in BO.build_msgs(arm, us_zh[0])))
|
||||
ck("A и A_law различаются ТОЛЬКО банкнотой",
|
||||
a_msgs[0]["content"] == BO.build_msgs("A_law", us_zh[0])[0]["content"]
|
||||
and a_msgs[-1]["content"] == BO.build_msgs("A_law", us_zh[0])[-1]["content"])
|
||||
d, d_ = BO.build_msgs("D", us_zh[0]), BO.build_msgs("D_", us_zh[0])
|
||||
ck("D и D_ различаются ТОЛЬКО мандатом",
|
||||
d[1]["content"] == d_[1]["content"] and d[0]["content"] != d_[0]["content"])
|
||||
bs = [json.loads(f.read_text(encoding="utf-8")) for f in RAW.glob("bo2-B-*.json")]
|
||||
ck("арм B: мышление ПОГАШЕНО (промт стр.62)",
|
||||
bool(bs) and all(r.get("reasoning_tokens", 0) == 0 for r in bs),
|
||||
f"максимум {max((r.get('reasoning_tokens', 0) for r in bs), default=-1)}")
|
||||
|
||||
print("\n=== РАСКЛАДКА СУДЕЙ ===")
|
||||
# Считаются только голоса ЖИВЫХ единиц: на диске лежат ещё голоса единиц, отобранных до
|
||||
# починки недетерминированного отбора (§3 п.9). Они не участвуют в своде и не должны
|
||||
# участвовать в контроле баланса — иначе контроль мерит мусор, а не раскладку.
|
||||
for pref, live in (("jv2-", {u["uid"] for u in us_zh}), ("jve-", {u["uid"] for u in us_en})):
|
||||
by = {0: 0, 1: 0}
|
||||
for f in RAW.glob(f"{pref}*.json"):
|
||||
if f.name.endswith((".ERROR.json", ".NOMSG.json")):
|
||||
continue
|
||||
r = json.loads(f.read_text(encoding="utf-8"))
|
||||
if r.get("uid") in live:
|
||||
by[r.get("order", 0)] += 1
|
||||
ck(f"{pref}: голосов порядка 0 и порядка 1 ПОРОВНУ ({by[0]}/{by[1]})",
|
||||
by[0] == by[1] and by[0] > 0, f"o0={by[0]} o1={by[1]}")
|
||||
ck("судьи вне семейств, населяющих армы",
|
||||
not any(j.startswith(("deepseek", "glm")) for j in J.JUDGES), str(J.JUDGES))
|
||||
bias_zh = BO.position_bias()
|
||||
ck("позиционная фора zh напечатана в отчёте", f"{bias_zh:+.2f}".replace("+", "+") in text
|
||||
or f"{abs(bias_zh):.2f}" in text, f"{bias_zh:+.2f}")
|
||||
|
||||
print("\n=== ТАБЛИЦЫ ПЕРЕВЕСОВ (пере-вывод из голосов) ===")
|
||||
for a, b in BO.CONTRASTS:
|
||||
per = []
|
||||
for u in us_zh:
|
||||
pj = []
|
||||
for judge in J.JUDGES:
|
||||
bo = BO.margins_by_order(a, b, u["uid"], judge)
|
||||
if len(bo) < 2:
|
||||
continue
|
||||
pj += [statistics.mean(bo[0]), statistics.mean(bo[1])]
|
||||
if len(pj) == 4:
|
||||
per.append(statistics.mean(pj))
|
||||
if not per:
|
||||
ck(f"zh {a}/{b}: голоса на месте", False, "нет")
|
||||
continue
|
||||
eff = statistics.mean(per)
|
||||
lo, hi = BO.boot_ci(per)
|
||||
zh_sec = text.split("### 2.1", 1)[-1].split("### 2.2", 1)[0]
|
||||
printed = re.search(rf"^{re.escape(a)} против {re.escape(b)}\s+(\d+)\s+\d+\s+\d+\s+\d+\s+"
|
||||
rf"([+−-]?[\d.]+)\s+\[", zh_sec, re.M)
|
||||
ck(f"zh {a}/{b}: перевес {eff:+.2f}, ДИ [{lo:+.2f}, {hi:+.2f}] — совпадает с отчётом",
|
||||
bool(printed) and abs(float(printed.group(2).replace("−", "-")) - eff) < 0.005
|
||||
and int(printed.group(1)) == len(per),
|
||||
f"n={len(per)} перевес {eff:+.2f}; в отчёте {printed.group(0)[:40] if printed else '—'}")
|
||||
|
||||
for a, b in E.CONTRASTS:
|
||||
per = []
|
||||
for u in us_en:
|
||||
pj = []
|
||||
for judge in J.JUDGES:
|
||||
bo = E._margins(a, b, u["uid"], judge)
|
||||
if len(bo) < 2:
|
||||
continue
|
||||
pj += [statistics.mean(bo[0]), statistics.mean(bo[1])]
|
||||
if len(pj) == 4:
|
||||
per.append(statistics.mean(pj))
|
||||
if not per:
|
||||
ck(f"en {a}/{b}: голоса на месте", False, "нет")
|
||||
continue
|
||||
eff = statistics.mean(per)
|
||||
# ⚠ Искать в ГРАНИЦАХ РАЗДЕЛА: метки контрастов в таблицах zh и en одинаковы, и поиск по
|
||||
# всему файлу возвращает китайскую строку. Поймано исполнением: сверка «совпадает» при
|
||||
# совпадающих числах падала, потому что сравнивала en-перевес с zh-строкой.
|
||||
en_sec = text.split("### 2.2", 1)[-1].split("### 2.3", 1)[0]
|
||||
pr = re.search(rf"^{re.escape(a)} против {re.escape(b)}\s+(\d+)\s+\d+\s+\d+\s+\d+\s+"
|
||||
rf"([+−-]?[\d.]+)\s+\[", en_sec, re.M)
|
||||
ck(f"en {a}/{b}: перевес {eff:+.2f} на n={len(per)} — совпадает с отчётом",
|
||||
bool(pr) and abs(float(pr.group(2).replace("−", "-")) - eff) < 0.005
|
||||
and int(pr.group(1)) == len(per), f"n={len(per)} перевес {eff:+.2f}")
|
||||
|
||||
print("\n=== ДЕНЬГИ ===")
|
||||
recs = [json.loads(f.read_text(encoding="utf-8"))
|
||||
for f in list(RAW.glob("bo2-*.json")) + list(RAW.glob("en2-*.json"))
|
||||
+ list(RAW.glob("jv2-*.json")) + list(RAW.glob("jve-*.json"))
|
||||
+ list(RAW.glob("jc-*.json"))]
|
||||
recs = [r for r in recs if not r.get("skipped") and "prompt_tokens" in r]
|
||||
# Сверка ПОКЛЕТОЧНАЯ, а не по сумме: записанная цена округлена до 6 знаков, и на 500+ записях
|
||||
# округление копится до величины, которую тугой допуск на сумму читает как расхождение правила.
|
||||
worst = max((abs(r["cost_usd"] - independent_price(r["model"], r)) for r in recs), default=0.0)
|
||||
a_sum = sum(r["cost_usd"] for r in recs)
|
||||
b_sum = sum(independent_price(r["model"], r) for r in recs)
|
||||
ck(f"цена сходится ДВУМЯ независимыми путями поклеточно ({len(recs)} записей, "
|
||||
f"максимальное расхождение ${worst:.9f})",
|
||||
worst < 1e-6, f"записано ${a_sum:.6f} · независимо ${b_sum:.6f} · худшая клетка ${worst:.9f}")
|
||||
ck("total_tokens персистирован во ВСЕХ записях",
|
||||
all("total_tokens" in r for r in recs),
|
||||
f"{sum(1 for r in recs if 'total_tokens' not in r)} без него")
|
||||
blocks = [("армы zh", "bo2-*.json", BO.CEIL_ARMS), ("судейство zh", "jv2-*.json", BO.CEIL_JUDGE),
|
||||
("армы en", "en2-*.json", E.CEIL_ARMS), ("судейство en", "jve-*.json", E.CEIL_JUDGE),
|
||||
("проба судьи-замены", "jc-*.json", 0.10)]
|
||||
total = 0.0
|
||||
for name, pat, ceil in blocks:
|
||||
v = money(pat)
|
||||
total += v
|
||||
ck(f"{name}: ${v:.6f} ≤ потолок ${ceil:.2f} и напечатано в отчёте",
|
||||
v <= ceil and f"{v:.6f}" in text, f"${v:.6f}")
|
||||
ck(f"итог ${total:.6f} напечатан в отчёте", f"{total:.6f}" in text, f"${total:.6f}")
|
||||
|
||||
print("\n=== ЭКОНОМИКА ===")
|
||||
# ⚠ Цена печатается ПО СОБСТВЕННОМУ ВЫЗОВУ арма, без ноги черновика. Основание: армы правят
|
||||
# ЗАМОРОЖЕННЫЙ черновик корпуса пакета-6, чья цена не сохранена, а докупленные `bo2-DRAFT-*`
|
||||
# с ним побайтно не совпадают — их сложение приписывало бы связке цену другого текста.
|
||||
for arm in BO.ARMS:
|
||||
costs = [json.loads((RAW / f"bo2-{arm}-{u['uid']}.json").read_text(encoding="utf-8"))["cost_usd"]
|
||||
for u in us_zh if (RAW / f"bo2-{arm}-{u['uid']}.json").exists()]
|
||||
med = statistics.median(costs)
|
||||
ck(f"{arm}: ${med:.5f}/единицу (собственный вызов) напечатано в отчёте",
|
||||
f"{med:.5f}" in text, f"{med:.5f}")
|
||||
same = sum(1 for u in us_zh
|
||||
if BO.text_of("F", u).strip()
|
||||
== json.loads((RAW / f"bo2-DRAFT-{u['uid']}.json").read_text(
|
||||
encoding="utf-8"))["content"].strip())
|
||||
ck("подмена ноги черновика ОБЪЯВЛЕНА: текст арма F не совпадает с докупленным черновиком",
|
||||
same == 0 and "не совпадают" in text.replace("побайтно они с корпусными не совпадают",
|
||||
"не совпадают"),
|
||||
f"совпало {same}/{len(us_zh)}")
|
||||
med_draft = statistics.median([BO.draft_cost(u["uid"]) for u in us_zh])
|
||||
ck(f"пере-замер цены черновой роли ${med_draft:.5f} напечатан отдельным числом",
|
||||
f"{med_draft:.5f}" in text, f"{med_draft:.5f}")
|
||||
|
||||
print("\n=== ПОПРАВКА НА МНОЖЕСТВЕННОСТЬ И РАЗБОР ПО СУДЬЯМ ===")
|
||||
ps = []
|
||||
for a, b in BO.CONTRASTS:
|
||||
per = []
|
||||
for u in us_zh:
|
||||
pj = []
|
||||
for judge in J.JUDGES:
|
||||
bo = BO.margins_by_order(a, b, u["uid"], judge)
|
||||
if len(bo) == 2:
|
||||
pj += [statistics.mean(bo[0]), statistics.mean(bo[1])]
|
||||
if len(pj) == 2 * len(J.JUDGES):
|
||||
per.append(statistics.mean(pj))
|
||||
ps.append(BO.sign_perm_p(per))
|
||||
order_i = sorted(range(len(ps)), key=lambda i: ps[i])
|
||||
holm, run = [0.0] * len(ps), 0.0
|
||||
for rank, i in enumerate(order_i):
|
||||
run = max(run, ps[i] * (len(ps) - rank))
|
||||
holm[i] = min(1.0, run)
|
||||
ck("отчёт объявляет, что НИ ОДИН контраст не проходит Holm 0.05",
|
||||
all(h >= 0.05 for h in holm) and "Ни один контраст не проходит 0.05" in text,
|
||||
f"минимальный Holm {min(holm):.4f}")
|
||||
ck("отчёт печатает разбор ПО СУДЬЯМ (эффект одного судьи не есть свойство арма)",
|
||||
"ПО СУДЬЯМ" in text or "по судьям" in text)
|
||||
ck("отчёт печатает разложение ПО ОСЯМ",
|
||||
"ВЕРНОСТЬ" in text and "разложение по осям" in text.lower())
|
||||
|
||||
print("\n=== ТРЕТИЙ КОНТУР ПЕРСИСТИРОВАН ===")
|
||||
rd = RAW / "blind-keys" / "READINGS.json"
|
||||
ck("ранги слепого чтения лежат на диске, ВНЕ каталога пакетов", rd.exists())
|
||||
ck("карта раскладки вынесена из каталога пакетов (слепота держится расположением)",
|
||||
not list((RAW / "blind").glob("*MAP*")) and bool(list((RAW / "blind-keys").glob("*MAP*"))))
|
||||
if rd.exists():
|
||||
d = json.loads(rd.read_text(encoding="utf-8"))
|
||||
ck("персист несёт оговорку о том, что полные разборы утрачены",
|
||||
"НЕ персистированы" in d.get("provenance", ""))
|
||||
ck("рангов zh 16 и en 12", len(d["ranks_zh"]) == 16 and len(d["ranks_en"]) == 12)
|
||||
|
||||
print("\n=== БАТАРЕЯ ===")
|
||||
import battery as B # noqa: PLC0415
|
||||
ck("check_numbers симметричен: 两千三百 против «две тысячи триста» НЕ даёт дефекта",
|
||||
not (B.check_numbers("他有两千三百块灵石。", "У него две тысячи триста камней.")["lost"]
|
||||
or B.check_numbers("他有两千三百块灵石。", "У него две тысячи триста камней.")["invented"]))
|
||||
ck("check_numbers ловит настоящую потерю: 三千五百 против «три тысячи»",
|
||||
bool(B.check_numbers("三千五百人", "три тысячи человек")["lost"]))
|
||||
cards = B.load_cards()
|
||||
ck("§2.12 карточки построены из подписанного сида (51 ключ)", len(cards) == 51,
|
||||
str(len(cards)))
|
||||
ck("§2.12 проверка рода НЕ инертна на реальном тексте",
|
||||
B.check_gender(units[0]["final"], cards)["checked"] > 0,
|
||||
"0 сверок — карточки не доехали")
|
||||
ck("карточки построены из подписанного сида", len(cards) == 51, str(len(cards)))
|
||||
ck("check_gender отсекает косвенную форму имени",
|
||||
B.check_gender("Воля Фан Юаня была тверда.", cards)["mismatched"] == 0)
|
||||
ck("check_gender ловит настоящее рассогласование",
|
||||
B.check_gender("Фан Юань была спокойна.", cards)["mismatched"] == 1)
|
||||
|
||||
ra = RAW / "repair-arm.json"
|
||||
if ra.exists():
|
||||
r = json.loads(ra.read_text(encoding="utf-8"))
|
||||
ck("§2.10 арм C: oracle 16/16, реальные 8/16",
|
||||
r["oracle"] == [16, 16] and r["real"] == [8, 16], str(r))
|
||||
else:
|
||||
ck("§2.10 сырьё арма C на месте", False, "нет repair-arm.json")
|
||||
|
||||
rt = RAW / "route-arm.json"
|
||||
if rt.exists():
|
||||
r = json.loads(rt.read_text(encoding="utf-8"))
|
||||
ck("§2.11 гейт флагает 5 черновиков из 8", len(r["flagged_F"]) == 5,
|
||||
str(len(r["flagged_F"])))
|
||||
ck("§2.11 арм E: найдено 6 естественных дефектов, починено 6",
|
||||
(r["e_found"], r["e_fixed"]) == (6, 6), f"{r['e_found']}/{r['e_fixed']}")
|
||||
else:
|
||||
ck("§2.11 сырьё армов E/G на месте", False, "нет route-arm.json")
|
||||
print("\n=== ОТКАЗЫ ПРОВОДА ЗАПИСАНЫ, А НЕ ПОТЕРЯНЫ ===")
|
||||
nomsg = list(RAW.glob("jv2-*.NOMSG.json")) + list(RAW.glob("jve-*.NOMSG.json"))
|
||||
err = list(RAW.glob("jv2-*.ERROR.json")) + list(RAW.glob("jve-*.ERROR.json"))
|
||||
ck(f"отказы фильтра ({len(nomsg)}) записаны и объявлены в отчёте",
|
||||
bool(nomsg) and str(len(nomsg)) in text, str(len(nomsg)))
|
||||
ck(f"отказы xAI ({len(err)}) записаны ОТДЕЛЬНЫМ тегом (кэш успеха не отравлен)",
|
||||
all(not (RAW / f.name.replace(".ERROR.json", ".json")).exists()
|
||||
or json.loads((RAW / f.name.replace(".ERROR.json", ".json")).read_text(
|
||||
encoding="utf-8")).get("content") for f in err))
|
||||
|
||||
print(f"\n{'ВСЕ ЧИСЛА ОТЧЁТА СХОДЯТСЯ' if not FAILS else f'РАСХОЖДЕНИЙ: {FAILS}'}")
|
||||
sys.exit(1 if FAILS else 0)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue