This commit is contained in:
heaven 2026-08-23 11:38:21 +03:00
parent 89ee7a3429
commit 2f17752492
24 changed files with 4177 additions and 94 deletions

View file

@ -71,7 +71,7 @@ Go-бэкенд издательского художественного пер
## Онбординг новой сессии (порядок чтения)
**Развилка ПЕРВАЯ, до всего остального — есть ли у тебя хендофф-промт (файл задания, выданный оркестратором).**
- **С промтом:** этот файл → СВОЙ промт; его карта чтения (≤5 позиций) — ЗАКОН, дальше только по её ссылкам. README, CURRENT-STATE и голову журнала решений читать НЕ нужно: ратифицированное вложено в тело промта, остальное берётся грепом по мере вопросов.
- **С промтом:** этот файл → СВОЙ промт; его карта чтения (≤5 позиций) — ЗАКОН, дальше только по её ссылкам. README, CURRENT-STATE и голову журнала решений читать НЕ нужно, ЕСЛИ карта промта их не называет (у ролевого промта оркестратора называет — там его номер и очередь): ратифицированное вложено в тело промта, остальное берётся грепом по мере вопросов.
- **Без промта (холодный вход):** полный маршрут ниже.
1. Этот файл → `docs/README.md` (карта) → CURRENT-STATE в `docs/PROGRESS.md`. Непонятный жаргон/сокращения — `docs/glossary.md`.

View file

@ -92,4 +92,12 @@ hello my dear
Идея проекта V5, 26.07.2026
1) Идеи по фронту.
2) Так как фронт будет писаться под веб сначала, то нужно максимально хорошо сделать для ранжирования гуглом и нужно узнать как там сейчас работают актульные гугловские алгоритмы чтобы сайт не банился и не деранжировался. Просто для примера: политика куков, страницы сироты (на которую никто не ссылается) и так далее
2) Так как фронт будет писаться под веб сначала, то нужно максимально хорошо сделать для ранжирования гуглом и нужно узнать как там сейчас работают актульные гугловские алгоритмы чтобы сайт не банился и не деранжировался. Просто для примера: политика куков, страницы сироты (на которую никто не ссылается) и так далее
Идеи проекта V6, 18.08.2026
Идея переделки архитектуры бэкенда:
1) Делаем map reduce запросы в дешевую модель которая майнит банк памяти, находит сущности, РЕЖЕТ книгу по сценам (границы сцен, но граница сцены это просто удобная порция где не теряется смысл (сцены можно склеить, но если сцена крупная она должна вмещаться в контекст модели)) и просим вернуть джейсон. Не переводит, не пишит текст, выдача джейсон с полями, явно приписанная схема джейсона, указывать смещение в тексте (координаты терминов или имен, сцен). Майнинг ВСЕХ терминов. ЕЩЕ СЛУЖЕБНАЯ ИНФА О КНИГЕ, у нас итак достроена сейчас в бэкенде для txt книг нарезка по главам, но можно еще что то сюда завернуть например поиск идиом или еще других вещей чтоб сократить путь критику и дать возможность редактору банка отработать и принять решение
2) Перевод банка памяти в хорошую модель. Просить принять хорошую модель взять на себе редакторскую роль ПРИНЯТИЯ РЕШЕНИЯ С ОБОСНОВАНИЕМ. Хорошо приложить контекст термина, например первого появления термина. Еще можно разделить сущности: имена отдельно, магические предметы отдельно (разные правила перевода), мы кстати так и делаем или нет? Потому что у нас есть чанкование
3) Сам перевод в хорошую модель. Четко задать роль (ты литературный переводчик) Даем оригинал текста + банк памяти (исключительно релевантные, не давать слишком много контекста - у нас вроде есть это). Плюс опционально появились ли новые термины или сомнительное место которое не покрыто базой. Размер фрагмента - границы сцен. И я бы не подсовывал хорошей модели дешевый черновой вариант. Запретить что то? Менять канон? Зажать промтом, но не бесконечными правилами, а рамками и по каким критериям мы будем провериять результат
4) Литературный критик. Получает исходный текст, банк памяти и не переписывает а дает фидбек: вот тут звучит не по русски, тут термин упал. Зажать модель промтом чтоб не выдавал минорные проблемы, просить явно не предлагать решения, а только зафиксировать расхождение. Заставить критика жаловаться, а не чинить. МАЙНИНГ ЗАМЕЧАНИЙ (новый банк памяти считай замечаний). Просим критика выделить сцену, проблемное место, уверенность в проблемном месте, что пошло не так. Сложная архитектура для критика, так как есть риск лишних правок, в том числе есть правки разной сложности: смысловой (где переврали сюжет), простой случай когда проблема 1 термине непереведенном или непереведенном не так, потеря смысла, неестественный русский (калька языка), и другие проблемы например времен. Хороший критик классифицирует проблему, но не более сверх, тогда редактор понимает куда копать.
5) Запрос в модель редактора точечного фикса конкретных проблем. Постим строку банка проблемных мест и просим исправить. Затем инжектим фиксы в текст. Это edit точечный редактор. Не переписывальщик ни в коем случае он не переписывает ничего

View file

@ -146,7 +146,7 @@ D-ссылка грепается по D-логу · mid-flight аддендум
в отчёте» = дефект, гейты зоны его не видят по построению · триаж находок — МАШИННАЯ сверка «каждая
рекомендация → живой носитель», не
глазами · отчёт с пошаговым планом сверяется ПО ПУНКТАМ плана · каждое «отложено/вернуться» получает
строку бэклога или жильца реестра выселенных тел (`architecture/13-tech-debt-anchors.md` §Б) ТЕМ ЖЕ
строку бэклога или жильца реестра загейченных триггеров (`13-tech-debt-anchors.md` §Б-108) ТЕМ ЖЕ
лендингом · при закрытии строки её тело грепается на
«остаток/реопен/гейт», гейты в ДРУГИХ строках пере-диспозиционируются · глаз владельца — рабочий
рубеж: прайми его артефактами, не пересказом.

File diff suppressed because one or more lines are too long

View file

@ -17,7 +17,7 @@
- `research/` — фактура ресёрчей; у принятых — ревью-шапки, часть тел под ⚠ superseded: **читай баннер прежде содержимого**. Ключевые для навигации: 15 голос · 16 ридер-IDE · 17 внешняя критика · 18 рычаги качества · 19 нарезка · 20 банк-майнинг · 21 обзор транспорта · 22 доменные харнессы · 23 шов движок↔платформа · 25 холодное ревью шва — **читать перед любым кодом стыка** · 24 арбитраж банка · 26 официальные практики Anthropic · 28 контракт-ревью API v0 — **носитель для исполняющих сессий, читать оригинал, не пересказ**.
- [PROGRESS.md](PROGRESS.md) — журнал: CURRENT-STATE + **ЕДИНЫЙ БЭКЛОГ** (единственный трекер) + живой хвост хроники. НЕ источник решений.
- `scripts/counts.py`**производные числа доков считаются им, а не руками** (голова по трём носителям · счёт очереди и зон · вес открытых строк регистра платформы · полнота реестра нот); `--check` даёт ненулевой код на расхождении; `--lint` — линтер file:line-якорей живых доков (D39.126). Его же зовёт зонный хук `scripts/githooks/pre-commit`: **`--lint` якорей — при коммите, задевающем ЛЮБОЙ док или `CLAUDE.md`; `--check` чисел и головы — при коммите с D-логом или PROGRESS** (оба `--from-index`, предупреждает, не блокирует). ⚠ Шапка систематически отстаёт от хвоста у ЛЮБОГО автора — потому проверка механическая, а не «посмотрю внимательно».
- **Активные хендофф-промты — какой файл ТВОЙ** (состав обновляется при каждом лендинге, D39.80; статусы паков, хроника и причины — CURRENT-STATE и D-лог, здесь НЕ дублируются):
- **Активные хендофф-промты — какой файл ТВОЙ** (состав обновляется при каждом лендинге, D39.80; здесь — только то, что отвечает на вопрос «какой файл мой»; хроника, причины и статусы РАБОТ — CURRENT-STATE и D-лог):
| Роль | Активный промт | Статус |
|---|---|---|
| Оркестратор | [ORCHESTRATOR_SESSION_PROMPT.md](ORCHESTRATOR_SESSION_PROMPT.md) | роль и нормы; счётчик роли — CURRENT-STATE |

View file

@ -77,7 +77,14 @@ def rows(md: str) -> list[tuple[str, str, str]]:
if out: # таблица кончилась
break
continue
cells = [c.strip() for c in s.strip("|").split("|")]
# ⚠ Markdown экранирует вертикальную черту внутри ячейки как `\|`. Наивный `split("|")`
# рвал такую ячейку пополам, и улика приходила обрезанной: у R55 в оболочку уезжало
# `... | wc -l) -ge 7` без начала, что давало «Syntax error» и ЛОЖНЫЙ КРАСНЫЙ, а у R37 —
# ЛОЖНЫЙ ЗЕЛЁНЫЙ, потому что обрезок случайно возвращал 0. Гейт, который сам портит
# улику, не проверяет ничего. Восстанавливается ОБЫЧНАЯ черта: в markdown `\|` и означает
# литеральный `|`, и в оболочку должен уйти именно он.
cells = [c.replace("\x00", "|").strip()
for c in s.strip("|").replace("\\|", "\x00").split("|")]
if len(cells) < 3 or set(cells[0]) <= set("-: "):
continue
if cells[0].upper() in ("ID", ""):

View file

@ -0,0 +1,382 @@
# ХЕНДОФФ: полигон, следующая сессия (готовлено 20.08 вечером)
> Читать ЦЕЛИКОМ, не грепом. Прошлые передачи теряли пункты именно из-за чтения грепом.
> Предыдущие слои: `SESSION2-LOG.md` разделы 15.115.21 (по ходу этой сессии) ·
> `PLAN-17-08.md` (курс) · отчёт `docs/experiments/23-editor-tier.md` секции Д21, Д23 ·
> `КОНТЕКСТ-ДЛЯ-КОНСУЛЬТАЦИИ.md` (сжатая картина проекта для внешнего собеседника).
---
## 0. ГЛАВНОЕ, ЧТО ИЗМЕНИЛОСЬ ЗА СЕССИЮ
**Сменился ПРИБОР.** Решение владельца 20.08 дословно: «меняем принцип судейства… теперь корми
фабл 5 и пусть он решает какой из вариантов ближе по художественному смыслу, лучше по переводу»,
и позже: «Теперь судим так все время». Слепое чтение Fable-5 — ПЕРВИЧНЫЙ прибор; счёт ошибок
остаётся брак-скрином.
**Основание замерено, а не принято на веру.** На панели, которую читал сам владелец:
```
пара прибор Спирмен с владельцем
zh чтение Fable (чистый пакет) +0.80
zh счёт ошибок +0.80
en чтение Fable (чистый пакет) +0.80
en счёт ошибок 0.80
```
Новый прибор согласен с владельцем ОДИНАКОВО на обеих парах; старый меняет ЗНАК от пары к паре.
**И тут же найдена моя ошибка:** в пакете, который читал владелец, стояла подсказка — «вариант
с наименьшим числом ошибок читался почти хуже всех». Пере-прогон на НЕЙТРАЛЬНОМ пакете (тексты
побайтно те же) дал +0.80 вместо +1.00: подсказка стоила 0.20, верх и низ порядка устояли.
**Норма, заведённая этим: пакет чтения не имеет права называть ни вердикт другого прибора, ни
существование контроля.**
---
## 1. ЧТО ЖДЁТ СЛЕДУЮЩУЮ СЕССИЮ — ГОТОВО К ЗАПУСКУ
**15 пакетов слепого чтения английской пары, по ОДНОЙ главе в пакете, панель из ОДИННАДЦАТИ
архитектур.** Всё куплено, покупать больше нечего.
```
пакеты ~/books/chtenie-rol/ЧТЕНИЕ-en-arch2-<uid8>.md (15 штук, ~17 тыс. знаков каждый)
ключи ~/books/dovodka/blind-keys-rol/rol-KEY-arch2-<uid8>.json ЧИТАТЕЛЮ НЕ ДАВАТЬ
ответы ~/books/chtenie-rol/ОТВЕТ-en-arch2-<uid8>.md (заготовки созданы)
```
Панель: `ZD` голый черновик · `Z0` боевая связка (продакшен) · `ZF` её вторая генерация
(контроль шума) · `ZP` однопроходка-склейка · `Z8` обогащённый черновик · `Z8R` он же плюс перепис ·
`Z1` критик → ПОЛНЫЙ перепис (ставка владельца H-2б) · `Z7` однопроходка + канон-фиксер ·
`RN` промт-РОЛЬ с экзаменом · `RC` он же без экзамена · `RB` он же с ПЕРЕВЁРНУТЫМ экзаменом
(формулировка владельца).
**Одна глава из 16 пропущена и это НАПЕЧАТАНО, а не умолчано:** `100b088f71`у неё нет клетки
`RN` (единственная, что не собралась после двух попыток). Пакетов 15.
⛔ **ПОПРАВКА 21.08: строка ниже была НЕВЕРНА, и это ровно тот класс, о котором предупреждает шапка
файла.** Утверждение «прежние пакеты `arch` УДАЛЕНЫ» я записал как исполненное, не исполнив: на
диске лежат **16 пакетов `ЧТЕНИЕ-en-arch-<uid>.md`, 16 пустых `ОТВЕТ-en-arch-<uid>.md` и 16 ключей
`rol-KEY-arch-<uid>.json`** (панель из 8, ответов нет ни одного — проверено размером файлов).
Удалять не стал: сырьё чужой рукой не сносят, а механической коллизии глобов между `arch-` и
`arch2-` нет (проверено). Опасность ЧЕЛОВЕЧЕСКАЯ — принять один тег за другой; для того здесь и
стоит эта поправка.
~~Прежние 15 пакетов с тегом `arch` (панель из 8) УДАЛЕНЫ~~ — держать две панели одной пары значит
однажды свести ответы разных панелей одним ключом.
**Работать с `arch2`. Пакеты `arch` существуют, не прочитаны и не годятся: панель у них из 8.**
**Сборка ПРОВЕРЕНА СПЛОШЬ** (владелец просил убедиться до передачи):
* 15 пакетов, 15 уникальных глав, 15 РАЗНЫХ раскладок меток;
* **165 текстов сверены побайтно с содержимым клеток своих армов — расхождений 0**;
* в рамке КАЖДОГО пакета стоит указание владельца «торопиться не надо» (проверено гейтом);
* исходник в каждом пакете — тот самый, что у главы;
* имён армов и путей к ключу в пакетах нет;
* рамка нейтральна: не называет ни контроля, ни другого прибора.
⚠ Первая проверка дала 16 «утечек» — это была ЛОЖНАЯ тревога моей же проверки: она искала слово
«метки», которое в рамке стоит легально («метки перемешаны»). Проверка переделана на поиск ИМЁН
АРМОВ. Не повторить эту ошибку в обратную сторону: тревога гейта требует вскрытия, а не отмены.
**Как запускать:** по ОДНОМУ агенту Fable-5 на пакет (решение владельца: главы большие, читателю
с несколькими сразу тяжело). Промт-рамку взять из журнала §15.13/§15.21 — она уже отработала
четырежды. Обязательно: идентичность читателя в файл ДО запуска · `runs.py --claim` ДО запуска ·
греп-аудит транскрипта ПОСЛЕ (образец кода в журнале).
**КАП СНЯТ 20.08 словом владельца** («Можно поднять, но я не припомню что б ставил тебе жёсткие
ограничения»): 100 → 200, свободно 107, на 16 глав хватает. ⚠ Уточнение для истории: число 80
пришло из ЗАКАЗА (:25), владелец снял его ещё 15.08, а 100 поставил в код Я САМ — то есть сессия
упиралась в СВОЁ ограничение и подала его владельцу как чужое. Не повторять: перед тем как назвать
границу «ограничением владельца», найти его слово.
---
## 2. ЧТО УСТАНОВЛЕНО ЭТОЙ СЕССИЕЙ — числа, которые нельзя потерять
### 2.1 ⭐ Механизм цены вскрыт ТРЕЙСОМ, а не выведен
Сохранение трейса размышления заведено по флагу `TM_KEEP_TRACE=1` (`role_topology/buy.py`).
Прямая мера: доля предложений ФИНАЛЬНОГО текста, встречающихся в трейсе дословно — **медиана 96%**.
Разбор трейса: черновик в уме 54% абзацев · структура 11% · **само-проверка 4%** · глоссарий 3%.
⇒ **Размышление у `deepseek-v4-pro` — это ЧЕРНОВИК, а не проверка. Модель пишет перевод дважды:
в уме и в ответе. Мы платим за обе копии по цене выхода.**
⇒ Следствие для архитектуры: **боевая двухстадийная связка дёшева БЛАГОДАРЯ топологии — она
выносит черновик из канала размышления дорогой модели в выход дешёвой** ($3.96 против $1.32 за
миллион), и результат при этом виден, проверяем и переиспользуем, а не выбрасывается.
⚠ Режим БИМОДАЛЕН: одна клетка из шести в ум не писала вовсе (1 180 токенов против 23 000).
### 2.2 Промт-роль: измерен, лучше склейки, дороже вчетверо
Новый промт однопроходки собран как РОЛЬ (прежний был СКЛЕЙКОЙ двух боевых промтов с мета-фразой
«отдельного редактора после тебя НЕ БУДЕТ»). Ядро — `eval/dovodka/prompts/onepass-core.md`,
пар-специфика подтягивается из файлов пары, второго источника правды нет.
```
арм что это размышление $/клетка перевёрстка
ZP прежняя склейка 950 0.0177 1.77
RN роль, экзамен на смелости 28 809 0.1265 1.60
RB роль, экзамен ПЕРЕВЁРНУТ 23 301 0.1050 1.14
RC роль, экзамен вырезан 11 268 0.0593 1.37
Z0 боевая связка (для сравнения) — 0.0253 1.79
```
Слепое чтение абляции (3 главы, 5 вариантов): **`RB > Z0 > RC > RN > ZF`**.
⇒ формулировка ВЛАДЕЛЬЦА («брак = просвечивающий исходный язык») бьёт обе мои редакции и встала
выше продакшена. ⛔ **НО контроль шума КРАСНЫЙ** — две генерации связки на 2-м и 5-м местах,
то есть порядок при n=3 не разрешим. Уцелевает: `RB` не хуже продакшена, и три редакции промта
упорядочены между собой.
### 2.3 ⛔ ЧТО НЕ СРАВНИМО МЕЖДУ СОБОЙ — поймано владельцем
Четыре слепых чтения этой сессии сделаны на РАЗНЫХ главах:
A и B — глава `ed068182cf` · C — `1431129de6, c73f4857e7, 89614f9bfa` · D — `ef9cd38ec4,
89614f9bfa, b2a7464dbd`. **У C и D общая ОДНА глава из трёх, у A/B с C/D — НИ ОДНОЙ.**
⇒ фразы «арм был третьим, стал четвёртым» — НЕ повторный замер. Внутри одного чтения сравнения
законны, между чтениями нет. Ради этого и собраны 16 пакетов одной панели на одних главах.
### 2.4 Замер критика (фаза ФД-K, остановлен владельцем)
4 клетки одной главы: фраза о параллельности текстов срезала размышление на 55% и цену вдвое —
и находки тоже вдвое (24 → 12). **Критик НЕ видит удалённого целого абзаца** (проверено: слов из
пропавшего куска нет ни у одной модели), при том что снятую частицу «не» ловит.
Реальный дефект в черновике удорожает критика **в 14 раз** против чистого.
`glm-5` с включённым размышлением не влезает в потолок вывода на китайском.
⇒ полноту обязан ловить детерминированный гейт, а не критик.
### 2.5 Английский экстрактор ломает абзацы — наш баг
`pair_en.raw_text` сносит из epub все теги, включая `</p>`: в книге **11 447 абзацев**, а модели
едет один блок. Чинить на месте НЕЛЬЗЯ: uid единицы = `sha1(source.strip())`, правка пробелов
переименует всё — замерено, что повиснут **289 оплаченных клеток на $2.64** и семь файлов ключей.
Починка сделана НА ПОДАЧЕ (арм `RA`, функция `podacha.restore_paragraphs`, проверено 16/16).
⛔ **ДОЛГ ЧУЖОЙ ЗОНЕ: настоящая починка — в бэкенде. Пинг оркестратору обязателен.**
### 2.6 Приз best-of-2 пере-считан ЧЕСТНО (не круговым способом)
Выбирает читатель A, оценивает читатель B (и наоборот):
```
все главы: +0.28 позиции p=0.30 — шум
расхождение ≥10% текста: +2.36 позиции p=0.0065 — 12 подтвердили, 2 опровергли
расхождение <10%: 1.33 позиции p=0.95 отбор ВРЕДИТ
```
⇒ наивный «бери лучший из двух» бесполезен; на разошедшихся главах приз реален и больше
оракульного. Порог 10% выбран post-hoc — заморозить пре-регом и проверить на новых главах.
### 2.7 Г5 закрыта: вердикт по H-1 устоял
`itog_d4.py --sens`. Арифметика находки ревизии воспроизведена побайтно, но переворот живёт только
в сценарии, который возвращает пачку аннулированной сессии и одновременно снимает пачки-валидации.
Смежно закрыты P12, P07, R73/P40. ⛔ ОТКРЫТА **P42** — два пре-рег-правила о маржевом гейте
противоречат, и от выбора зависит, остаётся ли единственный CONFIRM фазы. **Вопрос владельцу.**
---
## 3. ДЕНЬГИ
```
ФД-K 0.2927 / 0.40 замер подачи входа критику, zh (остановлен владельцем)
ФД-L 0.0000 / 1.30 то же, en — не начиналось
ФД-M 2.0066 / 2.10 однопроходка как роль, zh
ФД-N 0.0000 / 2.40 то же, en — не начиналось
ПАК 14.0221 / 18.30 свободно 4.28
```
Санкции владельца этой сессии: «Хорошо, поехали» на $1.70 (ФД-K/L) и «Бюджет разрешаю какой
нужен» (ФД-M/N). ⚠ Смета ФД-M была занижена ВШЕСТЕРО: считалась по клеткам старой склейки, у
которой размышления в 26 раз меньше. Норма: цену клетки НОВОГО промта нельзя оценивать по клеткам
СТАРОГО.
---
## 4. КОМАНДЫ
```
eval/.venv/bin/python eval/dovodka/rol.py --selftest # гейт покрытия боевых промтов
eval/.venv/bin/python eval/dovodka/rol.py --show zh|en # промт целиком, как поедет
eval/.venv/bin/python eval/dovodka/rol.py --coverage zh|en # диспозиция КАЖДОГО боевого правила
eval/.venv/bin/python eval/dovodka/rol.py --dry # смета
eval/.venv/bin/python eval/dovodka/rol.py --canon # $0-гейт канона по армам
eval/.venv/bin/python eval/dovodka/rol.py --emit-read en --panel=… --tag=… --each --n=16
eval/.venv/bin/python eval/dovodka/itog_d4.py --axis=d4 --sens # Г5 целиком
eval/.venv/bin/python eval/dovodka/money_d.py --report | --selftest
eval/.venv/bin/python eval/dovodka/runs.py --status | --claim … | --done N
TM_KEEP_TRACE=1 …rol.py --buy zh RC RB --n=3 # покупка с сохранением трейса
```
---
## 5. ЛОВУШКИ, ДОБАВИВШИЕСЯ ЭТОЙ СЕССИЕЙ
1. **Гейт, сертифицирующий правило по ключевому слову, обходится словом из правила
ПРОТИВОПОЛОЖНОГО смысла.** Мой гейт покрытия считал «убирай лишние повторы» покрытым словом
«разнообразь», которое стоит в правиле «НЕ разнообразь». Поймала приёмка Fable-5.
2. **Правила короче 25 знаков гейт не сканировал вовсе** — порог длины был слепым пятном.
3. **Мёртвая строка таблицы неотличима от живой.** Заведена проверка «каждая строка обязана
сработать хотя бы раз».
4. **Свой же пакет чтения может нести подсказку.** См. §0.
5. **Тревога собственной проверки может быть ложной** (слово «метки» в рамке). Вскрывать, а не
отменять.
6. **Фриз-гейт кассы срабатывает на ЛЮБУЮ правку покупающего файла** — правил код после фриза,
покупка отказана. Это правильно; фризить ПЕРЕД покупкой.
7. **Оборванные на потолке клетки надо уводить в карантин ПЕРЕД перекупкой** — кэш `purchase`
смотрит на существование файла и вернёт старую запись.
8. **Ключ каждой панели чтения — в СВОЁМ файле.** Пере-эмиссия с другим составом армов переписала
бы раскладку уже прочитанной панели, и ответ читателя стал бы мусором молча.
---
## 6. ЧТО СПРОСИТЬ У ВЛАДЕЛЬЦА ПЕРВЫМ ДЕЛОМ
1. **Кап агент-сессий: осталось 7 из 100**, а на полное чтение 16 глав нужно 16. Поднимать?
2. ~~**P42**~~**СНЯТ С ПОВЕСТКИ РЕШЕНИЕМ ВЛАДЕЛЬЦА 20.08.** См. §8.
3. ~~Нужны ли новые редакции промта в английской панели~~ — **ВЛАДЕЛЕЦ СКАЗАЛ ДА (20.08).**
См. §7.
---
## 7. РЕШЕНИЕ ВЛАДЕЛЬЦА 20.08: НОВЫЕ РЕДАКЦИИ ПРОМТА — В АНГЛИЙСКУЮ ПАНЕЛЬ
Дословно: «Нужны, можешь занести в хэндофф?»
**Что докупить ПЕРЕД чтением английской панели:**
```
арм что это модель клеток ~цена
RN роль, экзамен с риском на смелости deepseek-v4-pro 16 $0.35
RC роль, экзамен ВЫРЕЗАН deepseek-v4-pro 16 $0.35
RB роль, экзамен ПЕРЕВЁРНУТ (лучшая редакция) deepseek-v4-pro 16 $0.35
ИТОГО ≈ $1.05
```
**Смета ОРИЕНТИРОВОЧНАЯ и снята с КИТАЙСКИХ клеток.** Английская глава короче китайской, но
норма, выученная этой сессией дорого: цену клетки нельзя оценивать по клеткам другого промта или
другой пары. Перед покупкой прогнать `rol.py --dry` и сверить с фактом на первых трёх клетках.
Касса ФД-N: потрачено $0, потолок $2.40 — влезает с запасом.
**Порядок работ жёсткий:** сперва докупить, потом ПЕРЕ-эмитировать пакеты с расширенной панелью
под НОВЫМ тегом (`--tag=arch2`), потому что пере-эмиссия с другим составом армов переписывает
раскладку меток. Существующие 16 пакетов `arch` НЕ трогать — если их уже прочитали, ответы
станут мусором.
**Панель станет из 11 армов:** `ZD Z0 ZF ZP Z8 Z8R Z1 Z7` + `RN RC RB`.
⚠ Одиннадцать вариантов одной главы — много для одного читателя. Решать владельцу: либо две
сессии на главу с разными половинами панели и общим якорем (`Z0` в обеих), либо панель урезать.
Резать решением сессии ЗАПРЕЩЕНО заказом.
**Команда после покупки:**
```
eval/.venv/bin/python eval/dovodka/rol.py --emit-read en \
--panel=ZD,Z0,ZF,ZP,Z8,Z8R,Z1,Z7,RN,RC,RB --tag=arch2 --each --n=16
```
и обязательная сплошная сверка сборки — образец кода в журнале §15.21 (128 текстов побайтно
против клеток, отсутствие имён армов в пакете, нейтральность рамки, разные раскладки).
---
## 8. РЕШЕНИЕ ВЛАДЕЛЬЦА 20.08: SOL ПАРКУЕТСЯ, СУДИТ ТОЛЬКО FABLE
Дословно: «Давай пока забьем на сол и отдадим все судейство фаблу. Посмотрим что он скажет на
одинаковых главах и какое судейство проведет. А потом на сол если хватит времени посмотрим».
**Что это меняет механически:**
* **P42 снят с повестки, но НЕ закрыт.** Противоречие двух пре-рег-правил о маржевом гейте
существует и касается единственного CONFIRM фазы (H-2а). Оно перестаёт быть блокером, потому
что вердикт выносил СЧЁТЧИК, а счётчик понижен до брак-скрина. Когда дойдут руки до Sol —
вернуть в повестку. **Не считать «снят» синонимом «решён»: правило в пре-реге по-прежнему
противоречит само себе, и следующая сессия обязана это видеть.**
* **Норма П-1 «два судейских семейства» временно не исполняется, и это объявленное отступление,
а не забывчивость.** Все вердикты нового прибора выносятся ОДНИМ семейством (fable). Писать это
рядом с каждым выводом, как фаза писала «вывод стоит на одном семействе» по японской ноге.
* **Контроль вместо второго семейства — внутренний:** в каждой панели чтения стоят ДВЕ генерации
одной боевой связки (`Z0`/`ZF`). Читатель, который разводит их далеко, сам себя дисквалифицирует;
читатель, который ставит их рядом, доказал разрешение на этой панели. За четыре прогона Fable
трижды опознал эту пару чтением, НЕ будучи о ней предупреждён.
* **Что Sol всё ещё может дать, когда вернёмся:** он единственное независимое семейство, и его
расхождение с fable будет мерой «сколько в вердикте от прибора, а не от текста». Пока этой меры
нет вовсе — это главная незакрытая дыра нового прибора, и её надо называть вслух.
**Куплено под расширенную панель (санкция «Нужны»):** `RN`/`RC`/`RB` на английской паре,
16 глав каждый, смета $1.05, касса ФД-N (потолок $2.40). Покупка шла с `TM_KEEP_TRACE=1`.
---
## 9. ⛔⛔ ПОПРАВКА К СОБСТВЕННОМУ ВЫВОДУ, СДЕЛАННАЯ В КОНЦЕ СЕССИИ
**Английская докупка опровергла вывод §2.1, и его надо читать вместе с этой поправкой.**
```
китайская пара английская пара
прежняя склейка 950 4 494
промт-РОЛЬ (RN) 27 039 (×28) 5 804 (×1.29)
рез критериев (RC) 11 268 (×12) 3 586 (×0.80)
```
**Разгон размышления ×28 — свойство КИТАЙСКОГО МАТЕРИАЛА, а не промта.** На английском тот же
самый промт стоит на четверть дороже склейки, а резаный вариант — ДЕШЕВЛЕ неё.
⇒ Формулировка «промт, который просит творчества, оплачивается вторым черновиком» СНЯТА как общее
утверждение: она верна на плотном ханьском входе и не воспроизводится на английском.
**И это было в нашем же вендор-бюллетене, прочитанном в тот же день:** `00-provider-quirks.md`
п.7 — «разгон думания привязан к ПЛОТНОМУ ХАНЬСКОМУ ВХОДУ, не к модели вообще: при сопоставимом
входе zh требует ×7.8 размышления против en». Я это читал и всё равно приписал эффект промту.
**Норма: эффект, замеренный на ОДНОЙ паре, не называть свойством промта, пока он не проверен на
второй. Пара — конфаундер по умолчанию, а не деталь.**
**ЧТО ПРИ ЭТОМ УСТОЯЛО И СТАЛО ПРОЧНЕЕ.** Черновик в уме модель пишет на ОБЕИХ парах:
```
китайская 9397% финального текста написано внутри размышления (6 клеток)
английская 8894% (47 клеток)
```
Механизм подтверждён на 53 клетках. Само-проверки в трейсе 4%. ⇒ **размышление у dspro — черновик,
а не проверка**, это остаётся в силе. Меняется СЛЕДСТВИЕ: не «двухстадийность спасает от второго
черновика», а «второй черновик пишется всегда, но дорого обходится только на плотном входе».
**ЧТО ЭТО МЕНЯЕТ ДЛЯ ПРОДУКТА.** Возражение «однопроходка дорога» снимается ДЛЯ АНГЛИЙСКОЙ ПАРЫ —
той самой, где владелец при слепом чтении поставил однопроходку ПЕРВОЙ. На китайской возражение
остаётся. Вопрос переформулируется: не «однопроходка дорога вообще», а «однопроходка дорога на
плотном китайском» — и это вопрос к материалу, а не к архитектуре.
---
## 10. ПОЛНЫЙ СПИСОК АРТЕФАКТОВ СЕССИИ (для компакта — что где лежит)
**Код зоны, изменён/создан:**
```
eval/dovodka/rol.py однопроходка как РОЛЬ: сборка промта, гейт покрытия
боевых правил, покупка, эмиттер слепого чтения
eval/dovodka/prompts/onepass-core.md ядро нового промта (пар-блоки подтягиваются из пары)
eval/dovodka/podacha.py замер подачи входа критику + restore_paragraphs
eval/dovodka/itog_d4.py + режим --sens (Г5) и рычаги --drop/--restore
eval/dovodka/money_d.py + фазы ФД-K/L/M/N, пакетный потолок 13.80 → 18.30
eval/dovodka/runs.py кап 100 → 200 (слово владельца 20.08)
eval/role_topology/buy.py + сохранение трейса по флагу TM_KEEP_TRACE=1
eval/dovodka/HANDOFF-21-08.md этот файл
eval/dovodka/КОНТЕКСТ-ДЛЯ-КОНСУЛЬТАЦИИ.md картина проекта для внешнего собеседника
eval/dovodka/PLAN-16-08.md + баннер «закрыт»
eval/dovodka/PLAN-17-08.md + шапка состояния на 20.08
eval/dovodka/SESSION2-LOG.md + разделы 15.115.22
docs/experiments/23-editor-tier.md + секции Д21 (Г5) и Д23 (пре-рег промта-роли)
```
**Сырьё:**
```
~/books/dovodka/dv-k-*, dv-l-* замер подачи критику (zh куплен частично, en не начинался)
~/books/dovodka/dv-m-* промт-роль на китайской паре (RN/RC/RB)
~/books/dovodka/dv-n-* промт-роль на английской паре (RN/RC/RB, 47 клеток)
~/books/dovodka/podacha-plants.json правильные ответы посадок
~/books/dovodka/blind-keys-rol/ ключи всех панелей чтения + READERS-*.json
~/books/chtenie-rol/ЧТЕНИЕ-en-arch2-*.md 15 готовых пакетов
~/books/chtenie-vladeltsa-z17/ОТВЕТ-*.FABLE.md, *.ЧИСТЫЙ.md калибровка прибора
~/books/chtenie-vladeltsa-z17/ЧТЕНИЕ-*.НЕЙТРАЛЬНЫЙ.md пакеты без подсказки
```
**Коммиты сессии (полигон коммитит только фризы покупающего кода):**
`650b4fe` `824bd20` `273d2df` `7f15323` `3075566` `276c85b` `db02b7a` и фикс пропуска глав.
**Деньги на конец сессии:**
```
ФД-K 0.2927/0.40 · ФД-L 0.0000/1.30 · ФД-M 2.0066/2.10 · ФД-N 1.3593/2.40
ПАК 15.38/18.30 · свободно 2.92
```

566
eval/dovodka/PLAN-16-08.md Normal file
View file

@ -0,0 +1,566 @@
> ⚠ **ЗАКРЫТ И ВЫПОЛНЕН.** Это ПРОШЛЫЙ курс (16.08); он привёл к заходу Д17 и
> исчерпан им. Живой курс — `PLAN-17-08.md`, состояние на сегодня — `HANDOFF-21-08.md`.
> Читать целиком НЕ надо: здесь только исторические основания решений захода Д17.
# ПРОМТ-ПЛАН полигон-сессии: фаза Д, продолжение после сессии №2 (16.08)
> Пишется САМОМУ СЕБЕ на случай сжатия контекста и передачи. Читать ЦЕЛИКОМ, не грепом:
> прошлая передача потеряла целый пункт заказа именно из-за чтения грепом.
> Предыдущие слои: заказ `docs/POLYGON_EXP2223_REDO_SESSION_PROMPT.md` (207 строк, читать целиком) ·
> хендофф №1 `docs/POLYGON_PHASE_D_HANDOFF.md` · рабочий журнал `eval/dovodka/SESSION2-LOG.md` (рядом).
> Отчёт фазы — `docs/experiments/23-editor-tier.md`, секции Д0Д16 (~2500 строк).
---
## 0. ГЛАВНОЕ В ОДНОМ АБЗАЦЕ
Дуга экспериментов 19→23 честно ответила на вопрос «какая модель лучше в роли редактора» и
устойчиво его закрыла. Но она мерила **счёт локальных ошибок**, а продуктовая цель владельца —
**живая русская проза без translationese**, и под неё прибора нет. 16.08 слепое чтение показало,
что на английской оси порядок по счёту ошибок и порядок по читаемости **не связаны вовсе**
(Спирмен 0.10), причём наш прибор штрафовал ровно то, что покупает читаемость — вольность
обращения с буквой. Владелец постановил: **штрафовать можно только за перевирание смысла, за
вольность — нельзя**. Это меняет шкалу, и старые судейские числа с новыми сравнивать будет нельзя.
⇒ Следующий шаг — не новые модели, а **починка прибора**, затем маленький честный заход.
### 0.1 Словарь — расшифровать до чтения дальше
* **Боевая связка** (то, что стоит в продакшене): дешёвая модель `deepseek-v4-flash` переводит
главу начерно → дорогая `deepseek-v4-pro` получает исходник ПЛЮС черновик и **переписывает всё
заново**. Два вызова. В таблицах обозначена `A0` (китайская ось), `E0` (английская), `J0` (японская).
* **Однопроходка**: ОДИН вызов дорогой модели, которой дают исходник и **объединённую инструкцию**
переводчика плюс редактора. Ни черновика, ни второго прохода. Обозначена `A2` / `E2` / `J2`.
* **Точечный контур**: черновик + нечто, что находит места ошибок (детерминированные флаги ЛИБО
LLM-критик) + фиксер, правящий ТОЛЬКО указанные места. Обозначен `A1`/`A1B` (флаги) и `A3` (критик).
Трогает единицы процентов текста, остальное остаётся черновиком — в этом его слабость.
* **Канон-фиксер ПОСЛЕ**: боевая связка плюс отдельный проход, восстанавливающий термины банка
(`A4` / `E4`). Единственный арм, чинящий канон, не двигая судейскую ось; цена аддитивна.
* **Гипотезы владельца:** H-1 «проблема в черновике» (хороший черновик + точечный редактор не хуже
связки) · H-2а «флаги → фиксер не хуже переписа» · H-2б «смысловой критик → фиксер ЛУЧШЕ переписа»
(главная ставка) · H-3 «на английском перепис не нужен» · H-4 «перевес dspro — свойство китайской пары».
* **Пол (шумовой)**: пара из двух генераций ОДНОГО лечения. Разница их оценок = шум прибора,
из неё строится порог различимости. Половины обязаны судиться РАЗНЫМИ сессиями.
* **Декой**: намеренно испорченный вариант в пачке. Грубый — ловит «судья вообще смотрит?».
Маржевый — тонкая порча размером с искомый эффект, ловит «судья способен увидеть разницу такого
размера?». Без второго вывод «не различимо» неотличим от слепоты прибора.
---
## 1. РЕШЕНИЯ ВЛАДЕЛЬЦА — дословно и с последствиями
| дата | что сказал | что это значит механически |
|---|---|---|
| 15.08 | «Подтверждаю подъём расходов, сколько тебе нужно» | потолок записан числом **$6.85** в `money_d.py`; на 16.08 потрачено ~$6.09 |
| 15.08 | про кап агент-сессий: «почему оставшиеся? ты можешь наспамить под 90» | кап 80 снят его словом; `runs.py` продолжает считать СУДЕЙСКИЕ сессии (на 16.08 — 72) |
| 15.08 | «Можно пересудить [tier]. Но не подгонять. Нужно понять, кто ошибается из судей» | исполнено, см. §3.2 |
| 15.08 | «gpt 5.6 sol это и есть модель» | **Sol = семейство OpenAI**его возражение по `tier` дисквалифицировано (судил свою семью) |
| 15.08 | про японскую ногу: «Бери, ладно» | куплено и отсужено, см. §3.3 |
| 15.08 | про ja-книгу: «не помню; замена была потому что флагнули по 18+; кажется, я аппрувнул выкачивание» | дословной санкции на ЗАМЕНУ нет; `R37` реестра честно красный; в отчёте писать этой формулировкой, НЕ «по слову владельца» |
| 16.08 | «Штрафовать за вольность нельзя — живой язык один из приоритетов бэкенда. Штрафовать можно только за перевирания смысла исходника. В остальном можно менять, убирать англоязычность, переставлять текст» | **МЕНЯЕТ РУБРИКУ СУДЬИ.** См. §4.1 — это первый пункт плана |
| 16.08 | «Новые траты я разрешаю, перепровести эксп я разрешаю» | санкция на заход §4.3 |
| 16.08 | про цены DeepSeek: «мы с оркестратором проводим пересчёт его цен и внесём в документацию, после я тебе скажу (или ты мне напомни)» | **НАПОМИНАТЬ.** См. §5.1 — это блокер денежных выводов |
---
## 2. ЧТО УЖЕ ПОСТРОЕНО И РАБОТАЕТ (не переделывать)
Всё в зоне `eval/dovodka/`, все селфтесты зелёные, линтер чист.
| файл | что делает | команды |
|---|---|---|
| `gain.py` | **калибровка усиления судьи** — ловит пачку, легшую на пол шкалы | `--density` (гейт: нулей ≥25% → пачка не несёт «не различимо») · `--samearm` · `--agree` · `--floor` · `--tier` · `--selftest` |
| `naklon.py` | **гейт позиционного наклона** (норма требовала, кода не было) | без флагов — все 7 проходов; `--pass=d4`; `--selftest` (синтетика с известным ответом) |
| `tier2.py` | пере-судейство прохода `tier` починенным заданием | `--emit` · `--score` · `--selftest` |
| `ja6.py` | японская нога H-4: панель `J0`/`J6`/`D0` + контроли; ⚠ `read()` чинён 16.08 — считает и разведённые `p{1,2}-answers` своего семейства, и ПЛОСКИЙ `answers/` внешнего пакета | `--emit` · `--score` · `--sol` (пакет внешнему судье) · `--score-sol` · `--selftest` |
| `chtenie.py` | **прибор ткани**: слепое чтение человеком | `--emit` · `--score` |
| `adjud.py` | адъюдикация находок; **контроли починены** (4 дефекта) | `--emit` · `--controls` (гейт неразличимости классов) · `--score` · `--selftest` |
| `itog_d4.py` | свод оси; чинён: считает ОБА семейства, печатает **полную цену единицы** | `--axis=d4\|d3\|d6\|d1` `--fam=claude\|sol` `--gates` |
| `sud.py` | судейский риг; селфтест **перестал быть прибит к китайской панели** | `--axis=<ось> --selftest` |
| `money_d.py` | касса, потолок $6.85, фазы ФД-A…ФД-H | `--report` · `--selftest` |
| `runs.py` | журнал судейских сессий, запись ДО запуска | `--claim <прогон> <проход> <ток>…` · `--done <n>` · `--status` |
| `conformance.py``eval/`) | сверка с буквой заказа; **починен парсер** | `eval/conformance.py eval/dovodka/requirements.md --plan` |
**Три коммита-фриза сессии №2** (полигон коммитит только пре-рег-фризы, основание вслух ПЕРЕД
каждым): `a03ac66` (пре-рег `tier2` + `gain.py`) · `8c68828` (покупающий код ja-ноги) ·
`22a8a6b` (покупающий код пола ja-ноги + журнал). Остальное дерево НЕ коммичено — лендит оркестратор.
---
## 3. ЧТО УСТАНОВЛЕНО — с числами, которые нельзя потерять
### 3.1 Прибор: строгость счёта есть свойство ПРОГОНА, а не оси
Тот же арм `R0`, те же 16 единиц, то же семейство судей, подписи текста совпадают **16/16**:
проход `tier` дал **0.69** ошибки на единицу при 9 нулях из 16, проход `border`**4.31** при нуле
нулей; по-единично ниже в 15 из 16. Размах внутри семейства claude 1.41…6.47.
**межпроходные сравнения абсолютных чисел недействительны.** Под это попадает вывод «дешёвая
модель на японском проваливается сильнее других пар» (5.44 против 2.78 — разные прогоны) и все
заимствованные пороги. Внутри одной пачки сравнения законны — там сдвиг сокращается в контрасте.
Доля нулей по проходам claude: `tier` 38% (24% без обоснования) · Д3 en 10% · Д6 3% ·
`border` 0 · **Д4 zh 2 нуля на 713 клеток** · Д1 0. По сессиям выбиваются ровно две
(`d3r2/p1/д-52` 24%, `d3r2/p2/д-55` 21%). ⇒ выводы Д4/Д1/`border` этой болезнью НЕ заражены.
### 3.2 Проход `tier` пере-сужен — вывод пака 23 восстановлен
Пре-рег зафризен `a03ac66` ДО первого ответа. Те же тексты, новый ключ со своей солью, старый не
тронут. Правки: убран `CTRLfloorA` (был побайтно равен боевому арму `T1` в 16/16), добавлен
маржевый декой, запрещён молчаливый ноль, в рубрику внесены пропускавшиеся классы.
Результат на 16 единицах, 126 клеток, замечаний годности 0:
```
арм старый новый сдвиг гейты
R0 боевой 0.69 3.44 +2.75 плотность 3.77, нулей 6% (было 38%) — ГОДНО
T1 glm-5.2 1.00 3.56 +2.56 пол 16 пар sd 2.63 → порог 1.84
T2 gpt-5.6-terra 0.25 2.38 +2.12 грубый декой 16/16
T3 grok-4.5 0.62 2.94 +2.31 МАРЖЕВЫЙ ДЕКОЙ +2.50 против 1.84 — ПРОЙДЕН
F голый черновик 2.56 6.56 +4.00
```
Вердикт: `T1` 0.12 · `T2` **+1.06** · `T3` +0.50 — все ниже порога 1.84; контроль `R0 vs F`
3.12 p=0.0042. ⇒ **«сильный тир не отличим от боевого редактора» ВОССТАНОВЛЕН и впервые стоит
на приборе с доказанной чувствительностью.** Возражение Sol (+8.69) не воспроизводится (+1.06)
и вдобавок дисквалифицировано: `gpt-5.6-terra` — семейство самого Sol.
⚠ Пере-судейство сделано ОДНИМ семейством: второе на этом контрасте дисквалифицировано.
### 3.3 Японская нога H-4 — куплена, отсужена, гейты зелёные
`J6` = `glm-5` поверх той же японской базы, 9 клеток, $0.046329, все `finish=stop`. Плюс свой пол
(7 из 9 вторых генераций редактора; один вызов завис на 9+ минут — класс известен).
```
J0 deepseek-v4-pro 1.44 J6 vs J0 0.44 p=0.6875 ниже порога 2.17
JFLO вторая генерация 1.86 J0 vs D0 +2.78 p=0.0039 редактор бьёт черновик
J6 glm-5 1.89 грубый декой +2.89 ПРОЙДЕН · маржевый +2.80 ПРОЙДЕН
D0 черновик 4.22
```
**Разница между ДВУМЯ редакторами (0.44) не больше разницы между двумя прогонами ОДНОГО
редактора (0.42).** Это самый чистый способ сказать «не различимо».
**Второе семейство прогнано владельцем 16.08 (`ja6.py --score-sol`) и СВОИ КОНТРОЛИ НЕ ВЗЯЛО:**
плотность 5.06 (нулей 6%) · пол 7 пар sd 2.64 → порог 2.80 · **грубый декой +2.22 против 2.80 —
НЕ ПРОЙДЕН · маржевый +1.80 — НЕ ПРОЙДЕН**. Декой пойман ПО НАПРАВЛЕНИЮ, но собственный шум Sol
так широк, что даже намеренная грубая порча в его порог не укладывается ⇒ **на японской оси у Sol
нет разрешения**, его пачка по норме аннулируется. Направление при этом совпало: `J6/J0` 1.00
(мои 0.44), `J0/D0` +4.00 (мои +2.78). Вывод по H-4 не меняется, но опирается на ОДНО семейство.
Третий раз за фазу пол Sol оказывается вдвое шире — свойство его харнесса, а не оси.
**H-4 не подтверждается на всех трёх парах**: `deepseek-v4-pro` первый на zh, на en (1.31
против 2.72) и на ja (1.44 против 1.89). Порядок редакторов от языка не зависит. Требование
заказа (:115) исполнено ВПЕРВЫЕ.
### 3.4 Разложение контрастов ПО ОСЯМ — за всю дугу не делалось ни разу
```
A1B/A0 3.13 = ВЕРНОСТЬ 0.94 + ЯЗЫК 2.19
A3/A0 1.87 = ВЕРНОСТЬ 0.26 + ЯЗЫК 1.61 ← ставка владельца
A6/A0 1.06 = ВЕРНОСТЬ +0.12 + ЯЗЫК 1.19 ← носитель H-1
A4/A0 0.13
E0/D0 +1.25 = ВЕРНОСТЬ +0.94 + ЯЗЫК +0.31 + ТЕРМИН +0.75 + ФОРМА +0.69
J0/J2 +0.11 = ВЕРНОСТЬ 0.00 + ЯЗЫК +0.11
```
⇒ дешёвые контуры проигрывают переписи **прозой, а не смыслом**; носитель H-1 по смыслу
боевой связке не уступает вовсе; на английском редактор покупает в основном термины, верность и
вёрстку — то есть H-3 опровергнута только в слове «ТОЛЬКО»; на японском второй проход по смыслу
не покупает ничего.
### 3.5 Экономика — полная цена ЕДИНИЦЫ, а не вызова
```
zh: A2 однопроходка 0.00408 · A0 связка 0.00603 · A1B 0.00793 · A4 0.01419 · A3 0.01546 (2.56×A0)
en: E2 однопроходка 0.00471 · E0 связка 0.00885 · E4 0.01321
черновик flash 0.00096 · перепис dspro 0.00507 · gpt-5.6-terra 0.04408 (×9) · grok-4.5 0.05276
```
⇒ «кандидат в прод вдвое дешевле» ЛОЖНО — он в 1.52.4 раза **дороже**; это платная страховка
канона. Ставка H-2б стоит 2.56× боевой связки и по счёту ошибок хуже неё.
### 3.6 ⛔ СЛЕПОЕ ЧТЕНИЕ 16.08 — ПЕРЕВОРАЧИВАЕТ АНГЛИЙСКУЮ ОСЬ
Пакет `~/books/chtenie-vladeltsa/` (`ЧТЕНИЕ-zh.md`, `ЧТЕНИЕ-en.md`), **ответы там же**:
`ОТВЕТ-zh.md` и `ОТВЕТ-en.md` — это улики, их не терять. Ключ отдельно в
`~/books/dovodka/blind-keys-chtenie/chtenie-KEY.json`. Пере-счёт: `chtenie.py --score`.
Читал не владелец лично, а **Fable по его поручению**; владелец мнение одобрил и сам отметил,
что «нашёл меньше замечаний, перевод читаемый».
| ось | порядок судьи (счёт ошибок) | порядок читателя | Спирмен |
|---|---|---|---|
| **zh** (единица `ed068182cf`) | `A0` > `A4` > `A6` > `A3` > черновик | `A4` > `A0` > `A6` > черновик > **`A3`** | **+0.80** |
| **en** (единица `27cb3a7e69`) | `E0` > `E3` > `E2` > черновик > `E6` | **`E2`** > `E6` > `E0` > `E3` > черновик | **0.10** |
* На китайском прокси держится; перестановка `A0`/`A4` внутри шума. Но читатель поставил `A3`
(смысловой контур, ставка владельца) **последним, ниже голого черновика**.
* На английском связи нет вовсе. **Однопроходка `E2` — первая у читателя, третья у судьи;
наша боевая связка `E0` — третья у читателя, первая у судьи.**
* ⚠ **Читатель назвал механизм ВСЛЕПУЮ и попал:** «у Т3 локальных отклонений от оригинала больше,
чем у Т2, а читать его заметно лучше. Счётчик ошибок должен был поставить Т3 ниже Т2 — если так
и вышло, порядок разошёлся не случайно, а системно». Т3 = `E2`, Т2 = `E0`. Так и вышло.
⚠ Одна единица на ось, английская всего 1.9 тыс. знаков, читатель — модель. Это **калибровка,
а не замер**: она говорит «прибор и читатель расходятся», а НЕ «однопроходка лучше связки».
### 3.6а Черновая роль: панель мерялась, победила ценой
Эксп-22 мерил ШЕСТЬ кандидатов черновой роли: `deepseek-v4-flash` (медиана $0.00096) ·
`gemini-3.1-flash-lite` ($0.00385) · `deepseek-v4-pro` ($0.00387) · `gpt-5.6-luna` ($0.00357) ·
`glm-4.7` ($0.00656) · `grok-4.3` ($0.00976). Вердикт — **ничья по качеству, побеждает самый
дешёвый** (правило D39.117). То есть «поставить дипсик-про черновиком» УЖЕ проверено и не выиграло.
⇒ Низкое качество черновика — свойство не модели, а РОЛИ (и, возможно, промта — см. `K8` в §4.3).
Двухступенчатость бьёт однопроходку на всех трёх парах ПО СЧЁТУ ОШИБОК: zh 4.00 против 6.51 ·
en 1.31 против 2.38 · ja 2.22 против 2.39. ⚠ Но на английском слепое чтение дало обратный порядок
(§3.6), и это расхождение не разрешено.
### 3.7 Что уцелевает независимо от смены шкалы
1. Редактор поверх дешёвого черновика покупает много — три языка, два семейства судей, слепое
чтение (черновик внизу везде).
2. Дорогие редакторы не бьют `deepseek-v4-pro` при цене ×9 — на приборе с сертификатом.
3. Выбор редактора от языка не зависит.
4. Банк терминов работает; **банк-дисциплина и качество прозы — разные умения** (gemini: канон
0.986 против 0.884 у боевого при равной судейской оси).
5. Схемы «залатать черновик точечно» проиграли и по счёту, и по чтению. ⚠ Но точечный ремонт
**не выброшен ратификацией**: D39.117 п.3 отклонил его как ЗАМЕНУ редактора и принял как
«механический ПРЕ-ГЕЙТ ПЕРЕД редактором» и «ремонт ТОЛЬКО с банком». Число «$0.0002 чинит всё»
из эксп-20 при этом мерилось при ИДЕАЛЬНОЙ детекции и как несущее — пало; в фазе Д оно всё ещё
служило основанием армов и прогнозов. Не переиспользовать без оговорки.
6. Гипотеза эксп-04 «проза gemini — аутлаер» не подтверждается (после снятия запрещённой клетки
с `finish=length` точечная оценка ровно 0.000).
7. Потери канона у боевого редактора — в основном пере-формулировка, а не пропажа термина
(⚠ но автоматическая КЛАССИФИКАЦИЯ этих мест негодна — секция Д5 отчёта объявлена
НЕИСПОЛНЕННОЙ, см. §5.3).
---
## 4. ПЛАН. Порядок здесь важнее содержания
### 4.1 ШАГ ПЕРВЫЙ — ПОЧИНИТЬ СУДЬЮ ПОД ПРАВИЛО ВЛАДЕЛЬЦА ($0)
Правило 16.08: **штрафовать только за перевирание смысла исходника; за вольность — нельзя.**
Живой язык — приоритет продукта; переставлять текст, убирать англоязычность, менять структуру
разрешено.
⚠ **Рубрика живёт в ЧЕТЫРЁХ местах, править надо все, иначе семейства разъедутся:**
`eval/dovodka/judge-prompts/core.md` (ядро обвязки) · `claude.md` и `sol.md` (обвязки семейств) ·
константы `HEAD` в эмиттерах `sud.py`, `tier2.py`, `ja6.py` (шапка САМОГО задания — именно она
достаётся обоим семействам и обеспечивает паритет П-4). После правки прогнать `paritet.py`
и `promptdiff.py`.
Что сделать в рубрике:
* **ВЕРНОСТЬ остаётся и сужается до искажений факта:** инверсия адресата реплики · снятое или
добавленное отрицание · подмена числа, разряда, ранга, единицы · выдуманный или потерянный факт ·
перевёрнутое состояние действия/идиомы. Это и есть «перевирание смысла».
* **ЯЗЫК разворачивается:** сейчас он ловит «то, чего носитель не напишет». Оставить ТОЛЬКО это
(калька, канцелярит, несуществующее слово, рассогласование) и **явно запретить штрафовать за**:
перестановку предложений, слияние/дробление абзацев, замену оборота на более живой русский,
синонимическую замену, добавление связок. Прямо написать судье: «вольность ради живой русской
фразы — НЕ ошибка; ошибка — только то, что носитель не напишет».
* **ФОРМА**: сейчас не входит в несущую сумму. Проверить, не штрафует ли она ИСПОЛНЕНИЕ мандата
перевёрстки (такое уже переворачивало знак вывода однажды).
* **ТЕРМИН** отдан детерминированному банк-гейту, судью им не грузить.
**Последствие, которое обязано быть напечатано:** после смены рубрики старые судейские числа с
новыми **несравнимы**. Все панели дуги останутся действительными только внутри своей шкалы.
⚠ Пере-судить по новой рубрике придётся хотя бы **боевую связку против однопроходки** на обеих
осях — иначе новую шкалу не с чем сверить.
⚠⚠ **ПОРЯДОК ИЗМЕНЁН ПО ЗАМЕЧАНИЮ ВЛАДЕЛЬЦА 16.08.** Первая редакция ставила чтение ПЕРЕД
покупкой новых армов. Это половина работы впустую: читать старые армы, зная, что панель меняется,
незачем. Правильный порядок — **починить рубрику → докупить новые армы → ОДНО слепое чтение по
ПОЛНОЙ панели (старые + новые) → отсудить починенной рубрикой.** Тогда чтение делается один раз и
отвечает сразу на оба вопроса: следит ли счётчик за читаемостью И какая архитектура читается лучше.
Читай §4.2 и §4.3 в этом порядке: сначала 4.3 (покупка), потом 4.2 (чтение по всему).
### 4.2 ШАГ ТРЕТИЙ — ПРИБОР ТКАНИ НА НОРМАЛЬНОМ n ($0, только агент-сессии)
Одна единица на ось ничего не решает. Нужно слепое ранжирование на **812 единицах на ось**,
двумя-тремя независимыми читателями (агенты РАЗНЫХ семейств: claude, Sol через владельца, Fable).
Контроли (они уже описаны в пре-реге П-6, `23-editor-tier.md:1114-1122`):
* декой обязан ранжироваться последним;
* половины шумового пола — соседями (если читатель разводит две генерации одного лечения далеко,
его ранжирование — шум);
* порядок вариантов перемешан, метки слепые, ключ вне рабочего каталога.
Готовый скелет — `chtenie.py`; нужно расширить с одной единицы на выборку и добавить контроли.
**Исход решает всё дальнейшее:** ранги согласуются со счётом ошибок → счёт остаётся дешёвым
прокси; расходятся (как сейчас на en) → **до починки прибора новые платные замеры бессмысленны**.
### 4.3 ШАГ ВТОРОЙ — МАЛЕНЬКИЙ ЧЕСТНЫЙ ЗАХОД (санкция владельца 16.08 есть)
**Смета по НОВОМУ прайсу: ~$2.9 на пике, ~$1.7 на офф-пике** (прежняя оценка «около доллара»
считалась по июльскому пину). Покупать офф-пик; перед покупкой пере-пинить `roster.py` (§5.1).
Три схемы, обе пары (zh и en), 16 единиц на пару, базы частично куплены:
1. **боевая связка** — контроль (черновик → полный перепис);
2. **однопроходка** — один вызов, объединённый мандат (на en она прочиталась лучшей);
3. **черновик → критик → ПОЛНЫЙ ПЕРЕПИС по его замечаниям** (`K1`) — сильнейшая форма идеи
владельца. ⚠ Отличие от проигравшего `A3`: там правщик был ТОЧЕЧНЫМ и трогал проценты текста;
здесь перепис полный, а буллеты критика лишь ОБУСЛОВЛИВАЮТ его.
Эндпойнты — **оба сразу**: починенный счёт ошибок (§4.1) И ранг читаемости (§4.2). Пре-рег до
покупок: мощность, пороги, правило расхождения, статусы осей.
⛔⛔ **ОДНОПРОХОДКА ЛОМАЕТ БАНК ТЕРМИНОВ — блокер, найден владельцем 16.08, проверен исполнением.**
`backend/prompts/zh-ru/terminologist.md:22`: «Варианты черновиков (строка `drafts:`) — это
свидетельства, а не голосование». Терминолог видит всю книгу сразу и выбирает канонический перевод
термина, опираясь на то, КАК ЕГО ПЕРЕДАВАЛИ ЧЕРНОВИКИ по кускам. Без черновика майнингу не из чего
собирать свидетельства.
**Однопроходка не альтернатива связке, а альтернатива только ВТОРОМУ проходу.** Черновик нужен
не только ради качества, но и ради банка. Честное сравнение — не «связка против однопроходки», а
«черновик + перепис» против «черновик + нечто другое поверх него».
⇒ И это объясняет, почему слепое чтение поставило однопроходку первой, а продуктовым решением это
не становится: на ОДНОЙ главе банк не нужен — термины внутри главы согласованы сами собой; на книге
в 1500 глав та же схема даст разъезжающиеся имена.
**ПОПРАВКА ВЛАДЕЛЬЦА 16.08 — это НЕ блокер, а недостающий кусок архитектуры.** Дословно:
«однопроходка допустима, как вариант чтоб оттуда после вырезать термины и edit точечным
редактором их заменить». Выход однопроходки — тоже перевод, значит он годится терминологу как
свидетельство; банк майнится ИЗ НЕГО, а термины приводятся к канону точечным редактором.
**Кандидат `K7`: однопроходка → майнинг банка из её выхода → канон-фиксер.** Точечный редактор
здесь адекватен дефекту: термины локальны, а канон-фиксер УЖЕ замерен и работает (покрытие
0.892→0.937 на zh, 0.927→0.956 на en; судейскую ось не двигает).
Цена единицы: zh 0.00408 + 0.00816 = **~0.0122** против 0.00603 у связки (дороже);
en 0.00471 + 0.00436 = **~0.0091** против 0.00885 (вровень). ⇒ на английском `K7` стоит столько же,
сколько связка, и по слепому чтению однопроходка там читалась ЛУЧШЕ — это самый дешёвый способ
проверить, не выиграла ли она случайно.
⇒ Что при этом остаётся правдой: **банк без черновика не собирается**, поэтому однопроходка не
может быть просто «связкой минус первый проход» — ей нужен свой источник свидетельств.
**Кандидат `K8`, самый дешёвый из всех: ОБОГАТИТЬ ПРОМТ ЧЕРНОВИКА.** Вопрос владельца 16.08:
«почему такое низкое качество черновика? может, промт черновика от редактора сильно отличается и
черновик тут выступает в роли недоведённого до ума инструмента?» — **подтверждено текстом**:
`backend/prompts/zh-ru/translator.md` = 22 строки, `editor.md` = 41, и разница не в объёме, а в
том, что есть ТОЛЬКО у редактора: весь блок ДИСКУРС-ПЕРЕВЁРСТКИ (четыре шага, как передавать
китайский паратаксис русской гипотаксической прозой) + FEWSHOT с ТРЕМЯ разобранными примерами +
правило чэнъюй обоими компонентами. У переводчика от всего этого одна строка «избегай канцелярита
и калек».
⇒ Замер: тот же черновик тем же `deepseek-v4-flash`, но с промтом, куда перенесены блок
перевёрстки и FEWSHOT. Цена — ЦЕНА ЧЕРНОВИКА ($0.00096/единица, входные токены длиннее и
кэшируются), то есть ~$0.03 на 16 единиц. **Самый дешёвый кандидат в списке и не проверенный ни разу.**
⚠ Контраргумент, который надо снять замером, а не рассуждением: арм `A2` (однопроходка с
УРАВНЕННЫМ мандатом) уже нёс мандатные части промта редактора и всё равно проиграл по счёту
ошибок — **но перенесён ли в него FEWSHOT, из эрраты Э-5 не следует**. Сверить ДО замера: если
примеров там не было, `K8` не дублирует `A2`, а закрывает его дыру.
⚠ Обязательно в панель — **арм голого черновика на китайской оси**: его там нет, и поэтому вклад
самого контура не отделим от разрыва «черновик против переписа».
⚠ Если запускать `K1`: сначала починить `contour.py:611-619``critic_places()` берёт ЛЮБУЮ
строку, начинающуюся с дефиса, без разбора вердикта, из-за чего фиксеру уходили **подтверждения**
критика («верно», «допустимо», «не ошибка») — 311 строк из 1696 = **18.3%**. Ставка владельца была
испытана инструментом, который на пятой части работы правил заведомо исправное.
### 4.4 ЧЕГО НЕ ДЕЛАТЬ СЕЙЧАС
* Новых панелей моделей — вопрос «какой редактор» закрыт устойчиво.
* Роутинга «платить дорого только там, где нужно» — закрыт отрицательно (отборщики-монетки).
* Второго последовательного переписа — итеративный дрейф, и обратная связка уже проигрывала.
* Дифф-контракта вместо переписа — дороже на thinking-модели и не берёт распределённый дефект.
* Любых покупок DeepSeek до пере-снятия прайса (§5.1).
---
## 5. ДОЛГИ И ОТКРЫТОЕ
### 5.1 ⛔ ДЕНЬГИ — ЛЕДЖЕР НЕ ИЗМЕРЯЕТ, А ВЫЧИСЛЯЕТ (блокер)
`roster.cost()` ВЫЧИСЛЯЕТ `cost_usd` из зашитого пина; провайдер сумму не возвращает.
**СТАТУС НА 16.08: цены пере-сняты владельцем с оркестратором — но ТОЛЬКО в бэкенде.**
`backend/configs/models.yaml` несёт новый пик (`prices_checked: 2026-08-15`): flash $0.44/$1.32
cache-hit $0.014 · pro $1.32/$3.96 cache-hit $0.044.
**`eval/tenant_panel/roster.py:33-35` ПО-ПРЕЖНЕМУ СТАРЫЙ** (pro 0.435/0.003625/0.87, помечен
«live 08.08»). Значит СЛЕДУЮЩАЯ покупка полигона снова посчитается по июльскому прайсу: касса
покажет неправду, а проекционный гард зарезервирует втрое меньше нужного и пропустит покупку.
**ПЕРВОЕ ДЕЙСТВИЕ ПЕРЕД ЛЮБОЙ ПОКУПКОЙ — пере-пинить `roster.py`.** Это риг эксп-22 (чужой пак):
по норме — отдельный коммит с его заголовком плюс пере-снятие его гейтов (`verify22.py`, `itog22.py`).
**ПЕРЕ-СЧЁТ ФАЗЫ ПО НОВОМУ ПРАЙСУ (сделан 16.08, 473 клетки DeepSeek: pro 430, flash 43):**
| счёт | сумма |
|---|---|
| в кассе (июльский пин), часть DeepSeek | **$5.3996** |
| та же работа по **пиковому** прайсу | **$15.5050** — ×2.87 |
| та же работа по **офф-пику** (50%) | ~$8.95 |
| не-DeepSeek клетки (не пере-считывались) | $2.3852 |
Деньги УЖЕ списаны по ценам, действовавшим в момент покупки, — там пин был верен, перерасхода нет.
Число важно для БУДУЩЕГО, и оно двигает три вещи:
1. смета заходов утраивается (см. §4.3);
2. потолок $6.85 в кассе больше ничего не охраняет, пока `roster.py` не пере-пинен;
3. ⚠ **правило ничьей может пере-решиться.** Оно звучит «при равном качестве берём дешёвого», и по
нему `deepseek-v4-pro` побеждал `glm-5` и сильный тир при разрыве ×9. Теперь `pro` подорожал
втрое, а `glm-5` (Z.AI) не дорожал — разрыв сжался. На этом правиле стоит вся рекомендация по
редакторской роли; пере-считать её ценой ОБЯЗАТЕЛЬНО.
Отдельная девиация к объявлению: последняя цифра потолка, приписанная слову владельца в файлах,
была $4.50, а расход дошёл до $6.00 ДО санкции 15.08. Цепь подъёмов прошла все самопроверки,
потому что **фриз-гейт сверяет ПОКУПАЮЩИЙ файл в стеке, а не кассу** — потолки правятся в рабочем
дереве и действуют немедленно.
### 5.2 ⛔ СВЕРКА ВНУТРЕННЕЙ РЕВИЗИИ: 65 находок из 131 в отчёт не попали
Источники: `~/books/dovodka/revizia-fazy-D-11-08.json` (82) и `priemka-D4-14-08.json` (49).
Требуют пере-проверки исполнением — я их принял со слов сверки, сам не верифицировал:
1. **H-1 МОЖЕТ ПЕРЕХОДИТЬ ПОРОГ.** Вердикт `A6/A0` определяют две пачки прогона, объявленного
«валидацией цепочки», судимого 11.08 по НЕ замороженному промту; паритет обвязок для них не
проверялся. Без них: пол n=13 sd 1.4058 → порог 1.0917, `A6/A0` 1.0938 → **ПЕРЕШЁЛ**.
Отчёт печатает «НЕ УСТАНОВЛЕНА». Вместе с §3.4 (`A6` по верности +0.12) картина другая.
2. Пачка `69de717f3f` осталась в замере, хотя её сессия **аннулирована за сравнение вариантов**.
Снятие двигает пороги обоих семейств и все контрасты.
3. Унаследованный гейт честности пола свод не гоняет; на данных claude его вердикт —
«ПОЛ СМЕЩЁН, боевые числа снимаются» (сдвиг +1.8 между наборами p1/p2). claude несёт оба CONFIRM.
4. Два пре-рег-правила о маржевом гейте противоречат: по букве П-1 пачка Sol аннулируется →
у H-2а остаётся одно семейство → «эскалация», а не CONFIRM.
5. Порог назван смещённым в **противоположные стороны** двумя находками; ни одна не в отчёте ⇒
калибровка порога неизвестна ПО ЗНАКУ.
6. Донор декоя во всех 62 пачках оси Д4 — `A0` (тот же дефект, что объявлен у `tier` и починен в `ja6`).
7. `A4`/`A1B`: клетки оплачены при НУЛЕВОМ изменении текста; `A4/A0` на 4 из 31 нулевой
ПО ПОСТРОЕНИЮ (то же вскрытие сделано для `E4`, для `A4` — нет).
⚠ Среди неотражённого — **12 инструментов, признанных негодными ВНУТРИ самой ревизии**, и трое
из них гейты числят ЗЕЛЁНЫМИ: `promptdiff.py` (объявленное расхождение матчится по ПРЕФИКСУ строки;
отсутствующий файл пар-пакета гейт не роняет) · `canon.py` (снятая ревью классификация всё равно
пишется в артефакт — отсюда негодность секции Д5) · `material_ja.py` (фильтр AI-меток объявлен
механическим, кода его не существует). Прежде чем опираться на любой из них — вскрыть.
**Полный список сохранён человекочитаемым:** `~/books/dovodka/СВЕРКА-РЕВИЗИИ-16-08.md` (35 КБ).
Кроме 65 неотражённых находок там **7 прямых противоречий телу отчёта** и **12 инструментов,
признанных негодными внутри самой ревизии** — эти два числа в §5.2 выше не раскрыты, читать в файле.
### 5.3 Прочие открытые долги
* **Адъюдикация английской оси** — контроли починены и проверены замером, прогона не было.
`adjud.py` намертво прошит на китайскую ось (ключи `blind-keys-d4`, каталог `sud-d4`,
армы `A3`/`A0`) — для английской нужна параметризация.
* **Секция Д5 объявлена НЕИСПОЛНЕННОЙ:** поле `why` в `~/books/dovodka/canon-dissect.json`
мусор («этот», «родов», «дочь»), классификация внутренней ревизией снята и всё равно писалась.
Честное закрытие — ручная пере-классификация 24 мест ($0).
* **`E1` побайтно равен черновику на 5 единицах из 16** — объявлено в Д14.4, но панель не
пере-считана без них.
* **Реестр требований:** 16 самореферентных улик из 89 (было 19, шесть заменил на артефактные).
Провалов два: `R37` (провенанс ja-книги) и `R64` (реестр не закоммичен — лендит оркестратор).
* **Селфтесты `sud.py`**: d4 зелёный, d3 4 провала, d6 1, d1 2 — все настоящие, диагнозы в журнале.
`MIN_FLOOR["d6"]=3` — константа, поставленная под зелень; НЕ править, это вопрос владельцу.
* **Пакет Sol по японской ноге** отдан: `~/books/judging/ja6-sol/ORCHESTRATOR.md`. После прогона —
`ja6.py --score-sol`. Пять промтов сессий, p1 и p2 разведены (проверено: 0 промтов смешивают).
* **Эррата zh-канона**: поправка границы слова живёт в коде, в сохранённый банк не дошла
(завышение +0.0039) — правка закрытой оси, ратифицирует оркестратор.
---
## 6. ЛОВУШКИ — на чём споткнёшься
1. **НЕ пере-эмитировать существующие слепые ключи.** Раскладка меток — чистая функция от соли,
uid и НАБОРА армов; эмиссия с другим составом переписывает ключ отсуженной оси, и разбор
сопоставит ответы с ЧУЖИМИ армами. Новый проход = НОВЫЙ ключ со своей солью (так сделаны
`tier2`, `ja6`, `chtenie`).
2. **Пакет внешнему судье — ТРЁХУРОВНЕВЫЙ**, как в фазе Д: `ORCHESTRATOR.md` («по одному агенту
на промт») + `prompts/*.md` + задания. Двухдокументная форма эксп-23 НЕ годится: харнесс
владельца читает всё одной сессией, и половины шумового пола попадают одному судье.
Путь к ключу в пакете **не называть** — «не открывай blind-keys/» значит показать пальцем.
3. **Правила счёта — в САМОМ задании**, не только в обвязке: асимметрия инструкций между
семействами купила часть расхождения в 12 раз на проходе `tier` (норма паритета П-4).
4. **Запрет дочерних агентов** вписывать явно — сессия с ними перестаёт быть тем составом,
который зарегистрирован до запуска.
5. **Замок кассы снимать ТОЛЬКО у мёртвого процесса** (`ps -eo pid,cmd | grep ...`). Покупка
идемпотентна — просто перезапускать. Редактор думает 6090 с на клетку, зависания до 9+ минут
бывают (в паке 22 вызов держал замок 74 минуты); ставить `timeout` ≥900 и не опрашивать часто.
6. **Фриз-гейт кассы**: покупающий код обязан быть ЗАКОММИЧЕН до покупки. Полигон вправе фризить —
основание вслух ПЕРЕД коммитом, форма `git commit -- <явные пути>`, никогда `git add -A`.
7. **`--wide-plants` на японской книге НЕ помогает** (проверено: 5 из 9 в обоих случаях). Регексы
посадок оказались не только пар-, но и КНИГО-зависимы.
8. **Не считать по половине пачки.** Пороги, снятые на неполном поле, читаются увереннее, чем есть
(на этом уже горела ось Д1: порог вырос 1.00 → 1.47, когда пол добрал пары).
9. **Промт черновика и промт редактора радикально асимметричны** (22 строки против 41): у
редактора есть весь блок дискурс-перевёрстки и FEWSHOT с тремя разобранными примерами, у
переводчика — одна строка «избегай канцелярита». ⚠ Но арм `A2` (однопроходка с УРАВНЕННЫМ
мандатом) это учитывал и всё равно проиграл по счёту ошибок — **проверить, перенесён ли в него
FEWSHOT**, это самая сильная часть промта и в эррате она не упомянута.
---
## 7. МНЕНИЕ FABLE-5 (архитектурный аудит 16.08) — что взять
Полный отчёт был в эфемерном транскрипте; существенное:
* Подозрение о самоподгонке харнесса **подтверждается наполовину**: прибор не мерит ткань, а
эндпойнт под неё (П-6) принят владельцем 11.08 и не построен. Но ядро вывода («перепис покупает
много») не артефакт — держится независимыми контурами и внешней литературой.
* **Три механизма смещения В ПОЛЬЗУ большого переписывающего вызова:** боевой промт редактора
прошёл три редакции и валидирован тем же классом судейства, а контуры шли первой редакцией с
багами в свою сторону; мандат вёрстки исполняет только перепис, а судья за вёрстку штрафует;
отрицательные результаты трижды оказывались свойством прибора, а не армов.
* **Идея владельца испытана НЕ в сильнейшей форме.** Пустая клетка — «критик-буллеты →
ПЕРЕПИСЫВАТЕЛЬ» (`K1`). Вторая пустая — «перепис → критик → точечная починка» (страховка смысла
ПОСЛЕ переписа, там точечная форма адекватна дефекту, потому что остаточные ошибки локальны).
* Внешние данные: на вебновелльном бенчмарке zh→en **DeepSeek-V3 (5.16) выше GPT-4o (5.09)**;
китайские модели систематически бьют западные на китайском исходнике. То есть «фронтир не
выигрывает» правдоподобно и снаружи. Но если преимущество фронтира есть, оно в ткани, а её
автометрики и LLM-судьи не видят (LitEval: опознают человеческий перевод ≤20%).
* **Кандидаты сверх `K1`:** перепис → критик → фиксер · два переписа + слепой селектор (два
прогона одного редактора расходятся как два разных — это замерено на `JFLO`) · монолингвальная
полировка ткани без исходника · перепис с окном соседних глав. Все дешевле $0.015 на единицу.
* **Чего Fable не установил:** валидность счёта ошибок как прокси (П-6 не построен); долю знаков,
которую трогает фиксер (спан-счёт даёт 0.23.7%, difflib — 8.5% медиана и до 81%); перенос
выводов на другие книги и пары.
---
## 8. КОМАНДЫ
```
eval/.venv/bin/python eval/dovodka/gain.py --density --agree --floor --tier
eval/.venv/bin/python eval/dovodka/naklon.py # все проходы; --selftest
eval/.venv/bin/python eval/dovodka/tier2.py --score
eval/.venv/bin/python eval/dovodka/ja6.py --score # и --score-sol после прогона Sol
eval/.venv/bin/python eval/dovodka/chtenie.py --score
eval/.venv/bin/python eval/dovodka/adjud.py --selftest ; --controls
eval/.venv/bin/python eval/dovodka/itog_d4.py --axis=d4 # --axis=d3|d6|d1, --fam=sol
eval/.venv/bin/python eval/dovodka/sud.py --axis=d3 --selftest
eval/.venv/bin/python eval/dovodka/money_d.py --report ; --selftest
eval/.venv/bin/python eval/dovodka/runs.py --status
eval/.venv/bin/python eval/conformance.py eval/dovodka/requirements.md --plan
```
Шум `SyntaxWarning` фильтровать. Интерпретатор — `eval/.venv/bin/python` из корня репо.
---
## 9. ЧЕМ ЗАКРЫВАЕТСЯ РАБОТА (иначе непонятно, что считать сделанным)
1. **Секция в отчёте** `docs/experiments/23-editor-tier.md` на каждый шаг плана. Нумерация уже
занята по Д16 включительно — следующая свободная Д17.
2. **Пре-рег ДО покупок и ДО первого судейского ответа**, зафризенный коммитом с основанием вслух.
Пре-рег обязан нести: состав панели · правило решения · пороги · мощность (MDE против ЦЕЛИ) ·
статус оси (несущая/описательная) · что замер НЕ может.
3. **Гейты зелёные ИЛИ красные с диагнозом.** Константы под зелень не подгонять — это норма
проекта, нарушение = аннулирование куска.
4. **Пинг в `docs/PROGRESS.md` секция «Полигон»** (фронт и платформа туда не пишут, полигон пишет).
5. **Обновлённый `eval/dovodka/SESSION2-LOG.md` (рядом)** или его преемник — по ходу, не в конце.
6. **CONFIRM/DENY владельцу — только с артефактом-носителем.** Полигон не ратифицирует.
7. Дерево НЕ коммитить, кроме пре-рег-фризов; лендит оркестратор.
---
## 10. КАК РАЗГОВАРИВАТЬ С ВЛАДЕЛЬЦЕМ
* Он просил **меньше аббревиатур** и продуктовых формулировок: не «H-2б» и «A3», а «схема
критик-плюс-точечная-правка». Термины расшифровывать при первом употреблении.
* Он **не всегда читает длинные тексты** — важное выносить в первые строки.
* Он ловит дефекты процесса лучше гейтов: за сессию №2 нашёл структуру пакета Sol (я ошибся),
устаревший прайс (леджер оказался не измерителем) и слепоту прибора к вольности. **Его реплики
проверять исполнением, а не отмахиваться.**
* Обязательство с адресатом вне зоны закрывается ТОЛЬКО изменением файла вне зоны. «Выпущено»,
«ждёт владельца», «объявлено в отчёте» — маркеры дефекта.
* Полигон не ратифицирует выводы. CONFIRM/DENY — владельцу через оркестратора, с носителем.

376
eval/dovodka/PLAN-17-08.md Normal file
View file

@ -0,0 +1,376 @@
> ⚠⚠ **СОСТОЯНИЕ НА 20.08 — читать ЭТО прежде плана ниже.** Курс за день сдвинулся сильнее, чем
> написано в теле; тело оставлено как есть, потому что в нём основания решений.
>
> **1. СМЕНИЛСЯ ПРИБОР.** Решение владельца: первичный — слепое чтение Fable-5, счёт ошибок —
> брак-скрин. Замерено: чтение согласно с владельцем +0.80 на ОБЕИХ парах, счёт +0.80 на
> китайской и 0.80 на английской. ⇒ **все прежние вердикты об архитектурах вынесены прибором,
> который на английской паре смотрит в обратную сторону, и подлежат пере-суду.** 16 пакетов на
> это уже собраны и проверены (см. `HANDOFF-21-08.md`).
> **2. ШАГ 1 (Г5) ЗАКРЫТ**, вердикт по H-1 устоял — секция отчёта Д21.
> **3. ШАГ 2 (подача входа критику) ОСТАНОВЛЕН владельцем** после того, как замер показал: критик
> не видит удалённого абзаца ВООБЩЕ, поэтому «сохранил ли он бдительность» мерить не на чем.
> **4. Г1в/Г1 частично отвечены:** фраза о параллельности срезает размышление на 55% и находки на
> 50%; «след в след» — неправда на ОБЕИХ парах (проверено позиционно), врать модели нельзя.
> **5. Г12 усилена до механизма:** трейс показал, что 96% финального текста модель пишет ВНУТРИ
> размышления. Размышление у dspro — черновик, а не проверка. ⇒ двухстадийная связка дёшева
> БЛАГОДАРЯ топологии: она выносит черновик из дорогого канала размышления в дешёвый выход.
> **6. Появилась новая живая ветка, которой в плане нет:** промт однопроходки как РОЛЬ. Лучшая
> редакция — формулировка ВЛАДЕЛЬЦА («брак = просвечивающий исходный язык»), она обошла обе мои
> и встала выше продакшена в слепом чтении, но при КРАСНОМ контроле шума и вчетверо дороже.
> **7. Г5-строка ниже была НЕВЕРНА ПО ЗНАКУ** — исправлено в теле.
# ПЛАН НОВОГО КУРСА — проверка живых гипотез (17.08)
> Пишется после закрытия захода Д17 и архитектурного разговора с владельцем про V6.
> Предыдущие слои: `PLAN-16-08.md` (прошлый курс, ВЫПОЛНЕН) · `SESSION2-LOG.md` §12§14 ·
> отчёт `docs/experiments/23-editor-tier.md` секции Д17Д20 · заказ
> `docs/POLYGON_EXP2223_REDO_SESSION_PROMPT.md` · хендофф `docs/POLYGON_PHASE_D_HANDOFF.md`.
> Читать ЦЕЛИКОМ: прошлая передача теряла пункты именно из-за чтения грепом.
---
## 0. ГДЕ МЫ ОКАЗАЛИСЬ
Заход Д17 ответил на вопрос, ради которого затевался, и ответ сместил всю рамку.
1. **Второй проход нужен** — голый черновик проигрывает всему и всегда, по всем приборам и трём
парам. Единственное, что не пошатнулось за пять экспериментов.
2. **Какой именно второй проход — приборы не различают.** Все схемы переписывания легли в
неразличимую кучу под починенной рубрикой.
3. **Причина названа и подтверждена трижды независимо: собственный разброс ОДНОГО способа от
прогона к прогону перекрывает разницу МЕЖДУ способами.** Счёт на zh (sd 4.42 между двумя
генерациями связки), машинные читатели на zh, владелец на en (опознал пару близнецов по тексту
и развёл на два места: «один прогон приличный, второй сырой»).
4. **Счётчик — браковщик, а не выборщик.** Он следит за читаемостью там, где у него есть диапазон
(широкая шкала +0.68), и слепнет на чистом тексте (узкая +0.42). Формулировка владельца:
«счётчиком отсеивать брак, чтением выбирать текст в книгу».
5. **Деньги конвейера — это размышление, и тратится оно на выравнивание двух текстов.** 85% цены
дорогой клетки. Перепис думает ×12 против однопроходки только потому, что ему дают черновик.
Курс меняется: раньше мы искали «лучшую архитектуру», теперь ищем **условия, при которых
архитектуры вообще становятся различимы**, и **чинить конвейер там, где он жжёт деньги впустую**.
---
## 1. ЖИВЫЕ ГИПОТЕЗЫ — реестр
Каждая проверяема, у каждой назван замер и цена. Порядок в §2, не здесь.
| # | гипотеза | откуда | цена |
|---|---|---|---|
| **Г1** | Выровненный вход схлопывает размышление сличающей стадии | ОДНО наблюдение: критик видел черновик 78 абзацев против 77 строк исходника (0.96:1) и думал 1084; en 8 против 1 — думал 9576 | ~$0.3 |
| **Г1а** | ⛔ После РЕДАКТОРА выравнивание исчезает (77 строк → 43 абзаца), значит критик V6 попадёт в дорогой режим даже на zh | замер 17.08 | считано: V6 на Гу ~$340 против $226 у связки |
| **Г1в** | ⭐ Достаточно СКАЗАТЬ критику, что тексты идут параллельно — размышление упадёт без всякой разметки | идея владельца 17.08; нынешний промт критика не говорит о связи текстов НИЧЕГО, кроме «исходник и его перевод» | ~$0.15 |
| **Г1б** | Разметку можно поручить ОТДЕЛЬНОМУ дешёвому вызову-экстрактору, не трогая переводчика | квирки :51 — эхо бьёт по переводу, не по JSON-выводу | ~$0.2 |
| **Г1г** | Локальное эмбеддинговое выравнивание (LaBSE/vecalign/BERTalign) даёт таблицу пар с приемлемой точностью на ХУДОЖЕСТВЕННОЙ прозе | идея владельца; модели уже в кэше машины | **$0** за прогон, только CPU-время |
| **Г2** | «Мало думает» = «плохо работает»: ленивый критик находит хуже | zh критик 1084 токена, 43 находки на 2 тыс. знаков, контур проиграл | в комплекте с Г1 |
| **Г3** | Языковому критику исходник НЕ нужен — монолингвальная критика не хуже | ось ЯЗЫК решает, а она видна без оригинала | ~$0.4 |
| **Г4** | Кэш префикса срезает вход многостадийного конвейера | используем 44.8% из возможного | $0, перекладка запросов |
| ~~**Г5**~~ | ~~вердикт по H-1 зависит от состава пачек~~ **ЗАКРЫТА 20.08, Д21: вердикт устоял** ⚠ и формулировка была НЕВЕРНА ПО ЗНАКУ — «переходит порог» значило бы «дорогой черновик ХУЖЕ связки», то есть H-1 опровергается, а не подтверждается | находка ревизии №1 | $0, потрачено |
| **Г6** | Счётчик работает при широком диапазоне, слепнет на чистом | разбиение +0.68 против +0.42, но post-hoc | ~$1.5 (трудный en материал) |
| **Г7** | Дорогая модель + ПОЛНЫЙ промт → точечная зачистка ≥ связки | пустая клетка; `A6` был дорогой моделью с ТОЩИМ промтом | ~$1.2 |
| **Г8** | Промт однопроходки, собранный как РОЛЬ, а не склейка, лучше нынешнего | нынешний несёт мета-фразу «редактора после тебя не будет» | ~$0.5 |
| **Г9** | Банк, собранный из хороших переводов, лучше банка из дешёвых черновиков | никогда не мерилось; майнер режет исходник, черновики дают лишь `drafts:` | ~$0.3 |
| **Г10** | Разделение типов сущностей (имена / титулы / предметы) улучшает банк | терминолог даёт одно правило на всех | ~$0.3 |
| **Г11** | Сцена как единица нарезки лучше чанка | эксп-15: все эффекты когезии под порогом, сигнала нет | не считана |
| **Г12** | Несколько прогонов на метод — УСЛОВИЕ любого сравнения архитектур | п.3 §0 | ×2 к любому замеру |
---
## 1а. ЧТО УЖЕ ЗАКРЫТО — НЕ ПЕРЕОТКРЫВАТЬ БЕЗ НОВОГО ОСНОВАНИЯ
Дописано при перечитывании: без этого списка следующая сессия честно потратит деньги на то,
что уже куплено и отвечено.
| закрыто | чем | вердикт |
|---|---|---|
| «критик → ПОЛНЫЙ перепис» (`K1`/`Z1`) | Д17, обе пары | 0.06 по счёту, у владельца 3-е и 4-е места. **ОТРИЦАТЕЛЬНО** |
| «обогащённый промт черновика» (`K8`/`Z8`) | Д17, обе пары | хуже голого на en, эхо-мина на zh. **ОТРИЦАТЕЛЬНО** |
| «дешёвая модель в роли редактора» | эксп-22 Ф3 | 2.54 против боевого + 3 клетки сожгли бюджет. **ОТРИЦАТЕЛЬНО** |
| «дорогая модель черновиком» | эксп-22 Ф2 | ничья, побеждает дешёвый. **ЗАКРЫТО** |
| «какая модель лучше редактор» | 5 моделей zh, 2-я на en/ja, 3 «сильных тира» | dspro первый везде. **ЗАКРЫТО устойчиво** |
| «gemini — аутлаер прозы» (эксп-04) | Д1, 15 клеток | не подтверждается, цена ×30. **ЗАКРЫТО** |
| H-4 «перевес dspro — свойство китайской пары» | zh/en/ja | не подтверждается на всех трёх. **ЗАКРЫТО** |
| роутинг «платить дорого адресно» | эксп-21 | отборщики-монетки. **ЗАКРЫТО** |
| второй последовательный перепис | эксп-20/21 | итеративный дрейф. **ЗАКРЫТО** |
| дифф-контракт вместо переписа | эксп-19 | дороже на thinking-модели. **ЗАКРЫТО** |
⚠ **Единственный ЖИВОЙ продуктовый кандидат из измеренного: «однопроходка + глоссарный проход».**
По судейской оси от связки не отличается, по банку ЛУЧШЕ неё на обеих парах (zh 1.38 против 2.88,
en 0.31 против 0.56), стоит один полный вызов вместо двух, и владелец при слепом чтении поставил
однопроходку первой на английском. Его слабое место — банк без черновика (см. Г9).
---
## 1б. РЕШЕНИЕ ВЛАДЕЛЬЦА, КОТОРОЕ ЖДЁТ ЕГО САМОГО (не гипотеза, а развилка)
**Черновая роль по НАШЕМУ ЖЕ правилу должна уйти с DeepSeek.** Правило D39.117 — «при равном
качестве берём дешёвого», качество шести черновых моделей было ничьёй. После подорожания 16.08:
```
gpt-5.6-luna $0.00358/глава
gemini-3.1-flash-lite $0.00383
deepseek-v4-flash $0.00444 ← был 0.00097, в 3.7 раза дешевле следующего
```
На книге в 1500 глав: текущая связка $40 против $23 у пары `flash-lite + glm-5`.
**Перед сменой — эхо-проба альтернатив:** у flash эхо-мина есть и она перевооружается
ослабленным thinking; у остальных не мерена НИ РАЗУ. Владелец сказал «это разговор предстоит».
---
## 2. ПОРЯДОК РАБОТ. Он важнее содержания
### ✔ ШАГ 1 — Г5. ВЫПОЛНЕН 20.08, $0. Секция отчёта Д21
**Вердикт «H-1 НЕ УСТАНОВЛЕНА» устоял.** Инструмент — `itog_d4.py --sens` (режим в СВОДЕ, нового
файла не заводилось). Арифметика находки воспроизведена побайтно; переворот живёт только в
сценарии, который возвращает пачку аннулированной сессии и одновременно снимает пачки-валидации,
то есть применяет норму в одну сторону и отменяет в другую. На нынешнем дереве запас **0.0710**.
Тем же заходом закрыты смежные находки: **P12** (пачка `69de717f3f` уже в карантине, строка
сверки устарела) · **P07** (наклон наборов реален, вердиктов не двигает) · **R73/P40** (калибровка
порога названа ПО ЗНАКУ: пол на Д4 честен, для H-1 контраста завышен в 1.5×).
**Осталась открытой P42 и это ВОПРОС ВЛАДЕЛЬЦУ**, не замер: два пре-рег-правила о маржевом
гейте несовместимы, и от выбора между ними зависит, остаётся ли единственный CONFIRM фазы
подтверждением или становится эскалацией. Объявлено девиацией в Д21.6.
**Что вынес сверх задания:** `A6/A0` во всех пяти способах счёта стоит ВПЛОТНУЮ к порогу
(0.62 · 0.07 · 0.51 · +0.002 · 0.007) при шаге шкалы в одну ошибку ⇒ это не «эффекта нет», а
«эффект ровно размером с шумом прибора» — та же болезнь, что Д17 намерил на новом материале.
И `A6` испытывался ТОЩИМ промтом переводчика, поэтому клетка «дорогая модель + ПОЛНЫЙ промт»
(она же ядро V6) остаётся пустой — это шаг 4.
### ШАГ 2 — Г1в + Г1 + Г2 ОДНИМ ЗАМЕРОМ (~$0.4). БЕЗ ВЫРАВНИВАНИЯ, БЕЗ РАЗМЕТКИ
**Замер УРЕЗАН по итогам спора 17.08.** Прежняя редакция предлагала подавать критику
пронумерованные ПАРЫ, то есть требовала решить задачу выравнивания. Это преждевременно: вся
ветка висит на ОДНОМ наблюдении, которое я же и объяснил. Сначала проверяем наблюдение, потом
строим схемы — норма, заведённая этим разговором.
**ПЕРВЫМ — вариант владельца (Г1в), он дешевле и чище.** Меняется ТОЛЬКО ИНСТРУКЦИЯ, данные
не трогаются вовсе. Сейчас промт критика говорит о связи двух текстов буквально одно: «тебе дан
ИСХОДНИК и его ЧЕРНОВОЙ ПЕРЕВОД». Модель вынуждена сама выяснять, как они соотносятся, — и,
похоже, именно на это уходят те 9 576 токенов.
Добавляем фразу о том, что тексты идут ПАРАЛЛЕЛЬНО. Образ владельца: два листа из разных тетрадей,
наложенные друг на друга, — тексты идут рядом, а не строчка в строчку.
**ФОРМУЛИРОВКА РЕШАЕТ, И ВРАТЬ НЕЛЬЗЯ.** «Предложение за предложением» — НЕПРАВДА: наш же промт
редактора обязывает сливать абзацы (77 строк исходника → 43 абзаца). Соврём — модель будет искать
соответствие, не найдёт и потратит БОЛЬШЕ, а не меньше. Говорить только правду: порядок сохранён ·
перевод покрывает исходник целиком · членение может отличаться · читай оба параллельно и НЕ ищи
точных пар предложений.
**ВТОРЫМ — мой вариант:** тот же кусок, но **исходник разбит по предложению на строку**
(перевод НЕ трогаем, выравнивания НЕ делаем). Детерминированно, $0.
**Гонять лучше все четыре комбинации** (инструкция вкл/выкл × разбивка вкл/выкл) на одном
материале: тогда видно, что решает — слова или форма подачи. Всё равно копейки.
⚠⚠ **РИСК, КОТОРЫЙ ЗАМЕР ОБЯЗАН ПРОВЕРИТЬ НАРАВНЕ С ТОКЕНАМИ.** Фраза «перевод покрывает исходник
целиком» может заставить критика ПЕРЕСТАТЬ ИСКАТЬ ПРОПУСКИ, а потерянный факт — один из главных
классов нашей рубрики. Выигрыш в токенах, купленный слепотой к целому классу дефектов, — это не
выигрыш. Поэтому в пре-рег отдельной строкой: считать долю находок класса «пропущено/потеряно»
до и после, и падение этой доли считать ПРОВАЛОМ варианта, даже если токены упали.
Смотрим ТРИ вещи:
* **токены размышления** — упали или нет (Г1в/Г1);
* **находки в целом** — те же, лучше или хуже (Г2);
* **находки класса «пропущено»** — не исчезли ли (риск выше).
**Если упали при тех же находках** — гипотеза жива, и тогда имеет смысл строить разметку (Г1б).
**Если нет** — вся ветка закрывается, и мы экономим себе месяц проектирования. Это и есть цель
шага: закрыть ветку дёшево, а не подтвердить её дорого.
⚠ Обещание «минус треть цены конвейера» СНЯТО: оно висело на неподтверждённом.
⚠ Пре-рег обязателен: что считаем «те же находки». Сравнение — по адъюдикации, а не на глаз.
### ШАГ 2б — Г1г: ЭМБЕДДИНГОВОЕ ВЫРАВНИВАНИЕ, ЕСЛИ ДЕШЁВОЕ НЕ СРАБОТАЛО ($0)
Гонится ТОЛЬКО если шаги 2 и 2а не дали адресации. Инструмент локальный, вызовов к API нет вовсе.
**Что уже есть на машине (проверено):** `sentence-transformers`, `torch+cpu`, 16 ядер, GTX 1070;
в кэше лежат `LaBSE`, `bge-m3`, `Qwen3-Embedding-0.6B`. Кандидаты-алгоритмы: **vecalign**
(динамическое программирование по эмбеддингам, умеет МНОГИЕ-КО-МНОГИМ) и **BERTalign** (на LaBSE,
авторы целились в художественную прозу). Gale-Church по длинам для zh↔ru слабая.
**⛔ ОГРАНИЧЕНИЕ ПРОДА: в облаке только CPU + диск, GPU нет.** Поэтому:
* выравнивание — НЕ горячий путь, один раз на книгу, результат на диске;
* объём режется: эмбеддить смысловые ходы (4378 на кусок), а не предложения · сперва ЯКОРЯ
(термины банка, числа, реплики) прибивают опорные точки, эмбеддинги — только на промежутках ·
модель меньше + int8/ONNX;
* **роли разведены: в лаборатории это ИЗМЕРИТЕЛЬ, в проде адреса берутся из номеров смысловых
ходов по построению.** Прод на эмбеддинги опираться НЕ должен.
**Точность мерим БЕЗ ручной разметки, инструментами, которые у нас уже есть:** банк терминов
(термин в отрезке исходника обязан быть канонной формой в выровненном отрезке перевода) и маржевые
посадки (мы сами знаем, куда вставили порчу). ⚠ Чужие цифры качества из статей НЕ переносить: их
мерили там, где перевод следует за оригиналом близко, а у нас редактор обязан сливать абзацы.
### ШАГ 2а — ЕСЛИ Г1 ПОДТВЕРДИЛАСЬ: РАЗМЕТКА ОТДЕЛЬНЫМ ВЫЗОВОМ (~$0.2)
**Переводчику разметку НЕ поручать — это измеренный класс отказа.** Обогащённый черновик
получил ОДИН добавленный структурный блок и вернул исходник вместо перевода на 4 главах из 16;
запрет D21.6 говорит то же про языковые констрейнты в reasoning-ослабленных путях (эхо 3/10 → 9/10).
Разметку делает **отдельный дешёвый вызов-экстрактор**: вход исходник + готовый перевод, выход
ТОЛЬКО соответствие «номер смыслового хода → первые слова абзаца». Ни строчки текста не
переписывает. Безопасно по квирк-бюллетеню (:51): эхо бьёт по переводу, не по JSON-выводу.
**Алгоритма сопоставления при этом НЕТ и не нужно.** Если ходы пронумерованы на шаге 1, а блоки
несут те же номера, соответствие возникает по построению. Проверка разметки детерминированна:
каждый номер ровно один раз · порядок монотонный · длина блока в разумном отношении к длине хода ·
термины банка хода N присутствуют в блоке N. Не сошлось — блок «без адреса», обрабатывается как сейчас.
### ШАГ 3 — Г3 (~$0.4)
Монолингвальный языковой критик: только русский текст, исходника нет вовсе. Сравнить его находки
по оси ЯЗЫК с находками нынешнего двуязычного. Если не хуже — сличение из этой стадии убирается
целиком, а это самая дешёвая экономия из всех возможных.
### ШАГ 4 — Г7 + Г8, ПУСТЫЕ КЛЕТКИ АРХИТЕКТУРЫ V6 (~$1.7)
Обе покупаются одним заходом, потому что делят базу:
* **Г8**: промт однопроходки, собранный НАЧИСТО как роль литературного переводчика с инжектом
действий — без мета-фразы про отсутствующего редактора, без следов склейки.
* **Г7**: её выход + точечная зачистка по классифицированным замечаниям критика.
Это и есть урезанный V6 на одной паре. ⚠ Брать ОБЕ пары: разница zh/en в этой фазе оказалась
не языковой, а структурной (выровненность исходника), и на одной паре вывод не встанет.
### ШАГ 5 — Г9 + Г10, БАНК (~$0.6)
Пере-собрать банк из выходов однопроходки и из дорогих черновиков, сравнить с нынешним по составу
и покрытию. Отдельно — терминолог с РАЗДЕЛЁННЫМИ типами сущностей.
**Почему не раньше:** банк определяет связность книги на тысячах глав, но эффект его качества наш
прибор не видит вовсе — он сравнивает армы при ОДНОМ банке. Значит замер тут не судейский, а
детерминированный (состав, покрытие, устойчивость формы), и его надо ставить отдельно от
качественных шагов, чтобы не смешать.
### ШАГ 6 — Г6, ТРУДНЫЙ АНГЛИЙСКИЙ МАТЕРИАЛ (~$1.5)
Длинные плотные английские главы вместо коротких и чистых. Проверяет, восстанавливается ли
согласие счётчика с чтением при широком диапазоне. Разбиение +0.68/+0.42 сделано ПОСЛЕ данных —
это разведка, и подтверждать её надо на новом материале, иначе это подгонка.
### ШАГ 7 — Г4, КЭШ ($0)
Переложить порядок запросов так, чтобы исходник сцены был общим неизменным префиксом всех стадий.
Замер прямой: доля `cached_tokens` до и после. Сейчас 44.8%.
### ШАГ 8 — ПЕРЕНОСИМОСТЬ ВЫВОДА НА ДРУГУЮ МОДЕЛЬ (~$0.5)
Всё измеренное получено при модели-константе `deepseek-v4-pro`. Вывод честно звучит «какая
архитектура лучше ДЛЯ ЭТОЙ модели». Прогнать победившую архитектуру на `glm-5` и проверить, что
порядок не зависит от жильца. ⚠ Ставить ПОСЛЕ шагов 24: пока архитектура не выбрана, переносить
нечего.
### ШАГ 9 — СЛЕПОЕ ЧТЕНИЕ ЧЕЛОВЕКОМ НА НОРМАЛЬНОМ n ($0)
Владелец прочёл 1 китайскую главу и 2 английских. Пре-рег П-6 просил 812 единиц на ось.
Его чтение оказалось ТОЧНЕЕ машинных читателей (взял контроль там, где они провалили), поэтому
расширение выборки — самый дешёвый способ усилить решающий эндпойнт. Ограничение известно и
названо им самим: это его время.
---
## 3. ЧТО ПРОНИЗЫВАЕТ ВСЕ ШАГИ — Г12
**Сравнение архитектур по ОДНОМУ прогону запрещено.** Это вывод захода Д17, подтверждённый тремя
приборами. Любой платный замер выше планируется в ДВУХ генерациях на метод, иначе он померяет шум.
Цена этого честная: ×2 к смете. Альтернатива — мерить и не знать, что померил.
---
## 4. ДОЛГИ ФАЗЫ Д — их надо закрыть, но они НЕ курс
Не двигают знание, но без них фаза не сдаётся. Порядок внутри — от того, что искажает выводы,
к тому, что искажает отчётность.
1. **Адъюдикация английской оси** — единственное, что блокирует единственный несущий вывод фазы
(H-3 в эскалации). `adjud.py` прошит на zh, нужна параметризация. $0.
2. **65 находок ревизии** — шаг 1 курса разбирает первую; остальные разобрать там же.
3. **Секция Д5** — ручная пере-классификация 24 мест. $0.
4. **Синхронизация отчёта:** сводка Д9 противоречит Д15Д20 в четырёх клетках · Д16 не отражает
аннулирование японской пачки Sol её же контролями · деньги в Д17 протухли (напечатана смета
$1.06 и потолки $0.65, факт $11.72 из $12.10) · тела эрраты Э-17.1 в отчёте нет.
5. **Реестр требований:** строк на Д17Д20 нет вовсе, 16 самореферентных улик, R64 красный.
6. **Пинг в `docs/PROGRESS.md` секция «Полигон»** — последняя запись 15.08.
7. **Селфтесты `sud.py` красные на трёх осях** (d3 — 4 провала, d6 — 1, d1 — 2), все настоящие,
диагнозы в журнале. ⚠ `MIN_FLOOR["d6"]=3` — константа, поставленная под зелень; НЕ править,
это вопрос владельцу.
8. **`E1` побайтно равен черновику на 5 единицах из 16** — объявлено в Д14.4, панель без них не
пере-считана.
9. **Эррата zh-канона:** поправка границы слова живёт в коде, в сохранённый банк не дошла
(завышение покрытия +0.0039). Правка закрытой оси — ратифицирует оркестратор.
10. **`chtenie.py --score-calib` не пере-считывает число 0.10**, ради воспроизводимости которого
объявлен: печатает только порядок читателя. Долг назван в докстринге, не закрыт.
11. **Пакет второму семейству (Sol) по заходу Д17** — не собирался. Решение владельца нужно:
отдавать или объявить заход односемейным явно.
---
## 5. ЧЕГО НЕ ДЕЛАТЬ
* **Не строить V6 целиком.** Пять стадий сразу — это замер, в котором нельзя понять, что сработало.
Шаги 25 покупают его по кускам, и каждый кусок отвечает на свой вопрос.
* **Не выбирать архитектуру по деньгам.** Разброс между схемами 1540%, между временем суток —
ровно вдвое. Экономить надо расписанием прогона, решать — качеством.
* **Не переносить отрицательные результаты по точечным контурам на V6.** Там фиксер чинил
дешёвый черновик; в V6 он чинит перевод хорошей модели. Другая работа.
* **Не сравнивать числа разных проходов.** Рубрика менялась (Д18), прогоны разной строгости.
* **Не покупать до пере-снятия прайса**, если вендор снова его двинет: `zakhod.price_gate`
отказывает автоматически, но проверять руками при первом запуске сессии.
---
## 6. ЛОВУШКИ, ДОБАВИВШИЕСЯ ЗА ЭТУ СЕССИЮ
1. **Файл клетки ≠ годная клетка.** Считать СОДЕРЖИМОЕ и `finish=stop`. Я на этом сел и доложил
владельцу «80 клеток куплено», когда годных было 23.
2. **Пред-полётная проба обязательна** перед циклом на сотню клеток (`zakhod.preflight`): 96
запросов ушли в `402` и выглядели как успешный прогон.
3. **Оценивать цену клетки ПО СВОЕЙ ПАРЕ**, а не по общему пулу: китайская глава впятеро длиннее
английской, общая медиана врёт вдвое.
4. **Раскладка меток в пакете ЧЕЛОВЕКУ — одна на всю пару.** Раскладка на отрывок сделала ответ
владельца нерасшифровываемым, и его работа пропала.
5. **Разборщик обязан принимать человеческую форму ответа.** Требовать машинной и молча терять
ответ — дефект инструмента, а не читателя.
6. **Пере-эмиссия не имеет права трогать пару, которая уже прочитана.** Ключ ДОГРУЖАТЬ.
7. **Гейт, который не может упасть, ничего не сторожит.** Пустая панель в селфтесте — ПРОВАЛ,
а не пропуск.
8. **`git checkout` поверх грязного дерева запрещён** — владелец отказал в пермишене, и правильно.
13. **Обогащение промта ДЕШЁВОЙ модели перевооружает эхо-мину.** Замер Д17: мандат перевёрстки,
добавленный к черновику, дал 4 негодные клетки из 16 на zh (модель вернула исходник вместо
перевода) против 1 из 16 в базе. Причина документирована: черновик ходит с `reasoning_effort:
low`, а запрет D21.6 говорит не вносить языковые констрейнты в промты reasoning-ослабленных
путей без per-model замера. **Любое обогащение промта на дешёвой модели — только с эхо-гейтом
и ре-геном.**
14. **Идентичность судей и ЧИТАТЕЛЕЙ персистится ДО запуска.** Я закрыл её постфактум
(`READERS-z17.json`) — это слабее нормы. У прохода `tier` отсутствие такого файла сделало
причину расхождения в 12 раз НЕВОССТАНОВИМОЙ.
15. **Три инструмента зоны признаны негодными ВНУТРИ ревизии, а гейты числят их зелёными:**
`promptdiff.py` (расхождение матчится по префиксу строки; отсутствующий файл пар-пакета гейт
не роняет) · `canon.py` (снятая ревью классификация всё равно пишется в артефакт — отсюда
негодность Д5) · `material_ja.py` (фильтр AI-меток объявлен механическим, кода не существует).
Опираться на любой — только после вскрытия.
16. **Д17 отсужен ОДНИМ семейством.** Норма П-1 требует двух там, где возможно. Пакет для Sol по
заходу НЕ собирался. Все выводы Д17Д20 стоят на семействе claude плюс чтение владельца.
---
## 7. ЧЕМ ЗАКРЫВАЕТСЯ РАБОТА
1. Пре-рег ДО каждой покупки, зафризенный коммитом с основанием вслух.
2. Мощность (`power.py`) напечатана ЧИСЛОМ до денег — не словами. За это уже была девиация Э-17.2.
3. Гейты зелёные ИЛИ красные с диагнозом; константы под зелень не подгонять.
4. Секции в отчёте: следующая свободная — **Д21**.
5. Адверсариальная приёмка другим модельным семейством — она за эту сессию нашла 8 дефектов
замера и 4 неверных утверждения в моём же отчёте. Без неё не сдавать.
6. Дерево НЕ коммитить, кроме пре-рег-фризов; лендит оркестратор.

File diff suppressed because it is too large Load diff

View file

@ -28,7 +28,7 @@
проверке, поэтому это исполнение буквы, а не упрощение. Приближение объявлено: поправка
предполагает, что доля верных претензий одинакова по единицам внутри арма.
Запуск: adjud.py --emit | --score
Запуск: adjud.py --emit | --controls | --score | --selftest
"""
from __future__ import annotations
@ -70,19 +70,120 @@ PER_TASK = 24
SEED = 20260814 # фиксирован: раскладка воспроизводима
_norm = re.compile(r"[\s«»„“”\"'`.,!?;:—–-]+")
# ⚠ Имена осей — ЯВНЫМ списком, а не `[А-ЯЁ]{4,}`: см. починку дефекта Г-3 ниже по файлу.
AX_SPLIT = re.compile(r"(?=\b(?:ВЕРНОСТЬ|ТЕРМИН|ЯЗЫК|ФОРМА)\s*[:—–-])")
AX_HEAD = re.compile(r"(ВЕРНОСТЬ|ТЕРМИН|ЯЗЫК|ФОРМА)\s*[:—–-](.*)", re.DOTALL)
def _fragment(var: str, rnd: random.Random, target: int) -> str:
"""Фрагмент варианта ДЛИНОЙ ОКОЛО `target` — материал для ЛОЖНОЙ претензии.
Дефект Г-2, вторая половина. Мало построить ложной претензии такое же окно: если её цитата
систематически короче настоящих (18 знаков против 51 в первой починке), карточка выдаёт себя
длиной. Целевая длина берётся из ЭМПИРИЧЕСКОГО распределения настоящих цитат этого же прогона.
"""
words = [w for w in re.split(r"\s+", var.strip()) if w]
if len(words) < 3:
return ""
best, best_d = "", 10 ** 9
for _ in range(60):
i = rnd.randrange(0, len(words))
frag = ""
for j in range(i + 1, min(len(words) + 1, i + 25)):
frag = " ".join(words[i:j])
d = abs(len(frag) - target)
if d < best_d:
best, best_d = frag, d
if len(frag) > target + 15:
break
if best_d <= 4:
break
return best if len(best) >= 8 else ""
def norm(t: str) -> str:
return _norm.sub(" ", (t or "").lower()).strip()
def context(text: str, quote: str, width: int = 260) -> str:
"""Цитата с окружением — адъюдикатор обязан видеть, в каком месте она стоит."""
n, q = norm(text), norm(quote)
i = n.find(q)
if i < 0:
"""Цитата с окружением — адъюдикатор обязан видеть, в каком месте она стоит.
ПОЧИНКА 15.08 (дефект Г-4). Прежняя редакция и ИСКАЛА, и РЕЗАЛА по `norm()`, то есть
отдавала адъюдикатору окно без пунктуации и в нижнем регистре. По такому окну нельзя судить
ни ФОРМУ (она стёрта), ни ЯЗЫК (регистр и пунктуация часть языковой ошибки), а цитата в
самой претензии при этом шла в исходном виде: окно и цитата выглядели как разные тексты.
Теперь поиск идёт по нормализованному, а РЕЗ по сырому тексту через карту позиций.
"""
span = locate(text, quote)
if span is None:
return quote
lo, hi = max(0, i - width // 2), min(len(n), i + len(q) + width // 2)
return ("" if lo else "") + n[lo:hi] + ("" if hi < len(n) else "")
lo, hi = span
lo = max(0, lo - width // 2)
hi = min(len(text), hi + width // 2)
return ("" if lo > 0 else "") + text[lo:hi] + ("" if hi < len(text) else "")
def _normmap(text: str) -> tuple[str, list[int]]:
"""Нормализованный текст + карта «позиция в нём → позиция в СЫРОМ тексте»."""
buf, raw_pos, prev_space = [], [], True
for i, ch in enumerate(text):
if _norm.match(ch):
if prev_space:
continue
buf.append(" ")
raw_pos.append(i)
prev_space = True
else:
buf.append(ch.lower())
raw_pos.append(i)
prev_space = False
return "".join(buf), raw_pos
def locate(text: str, quote: str) -> tuple[int, int] | None:
"""Границы цитаты в СЫРОМ тексте; поиск ведётся по нормализованным формам."""
if not text or not quote:
return None
n, raw_pos = _normmap(text)
q = norm(quote)
i = n.find(q)
if i < 0 or not q:
return None
j = min(i + len(q), len(raw_pos)) - 1
return raw_pos[i], raw_pos[j] + 1
def margin_spans(text: str) -> tuple[str, list[tuple[int, int]]]:
"""То же лечение, что `sud.margin_plant`, но с ПОЗИЦИЯМИ посадок в получившемся тексте.
Зачем (дефект Г-1). Прежде «посаженной» считалась ЛЮБАЯ претензия к клетке маржевого декоя,
а судья цитировал в ней и обычные места: из 15 «посадок» реальными были 4. Контроль
чувствительности мерил не то согласие с претензией к декою, а не опознание известной порчи.
"""
out, spans = text, []
for pat, rep, _c in S.MARGIN_PLANTS:
m = re.search(pat, out)
if not m:
continue
frag = m.expand(rep)
start, end_old = m.start(), m.end()
out = out[:start] + frag + out[end_old:]
delta = len(frag) - (end_old - start)
spans = [(a + delta, b + delta) if a >= end_old else (a, b) for a, b in spans]
spans.append((start, start + len(frag)))
return out, spans
def hits_planted(var: str, quote: str, spans: list[tuple[int, int]]) -> bool:
"""Накрывает ли цитата хоть одну посадку.
Строго ПЕРЕСЕЧЕНИЕМ отрезков, без запаса «где-то рядом»: судья, процитировавший соседнее
предложение, посадку НЕ опознал, и засчитывать это как чувствительность тот же дефект Г-1
в более слабой форме.
"""
q = locate(var, quote)
if q is None:
return False
return any(q[0] < b and a < q[1] for a, b in spans)
def collect() -> tuple[list, dict]:
@ -112,16 +213,22 @@ def collect() -> tuple[list, dict]:
var, _ = S.margin_plant(C.base_a0(uid))
if not var.strip():
continue
w = re.search(rf"^{lab}-ПОЧЕМУ:\s*(.*)$", txt, re.M)
w = re.search(rf"^{lab}-ПОЧЕМУ:\s*(.*)$", txt, re.MULTILINE)
why = w.group(1) if w else ""
# какие оси ненулевые — по ним претензии реальные; нулевые дают материал для лжи
zero = []
for a in AX:
m = re.search(rf"^{lab}-{a}:\s*(\d+)", txt, re.M)
m = re.search(rf"^{lab}-{a}:\s*(\d+)", txt, re.MULTILINE)
if m and int(m.group(1)) == 0:
zero.append(a)
for seg in re.split(r"(?=[А-ЯЁ]{4,}:)", why):
m = re.match(r"([А-ЯЁ]{4,}):(.*)", seg.strip(), re.S)
# ⚠ ПОЧИНКА 15.08 (дефект Г-3). Прежде разбор осей шёл через `[А-ЯЁ]{4,}:`, и
# `re.split` с таким lookahead режет ВНУТРИ имени оси: «ВЕРНОСТЬ:» даёт точки
# разреза на В, Е, Р, Н, О, и уцелевает хвост «ОСТЬ». В ключе прошлого прогона
# это видно прямо: ОСТЬ 45 · ОРМА 22 · РМИН 18 против ВЕРНОСТЬ 1 · ФОРМА 4.
# Следствие тяжелее самой опечатки: фильтр «несущие оси» считал одну ось из двух,
# а ложные и настоящие претензии стали различимы по форме имени оси.
for seg in AX_SPLIT.split(why):
m = AX_HEAD.match(seg.strip())
if not m:
continue
ax = m.group(1)
@ -133,14 +240,24 @@ def collect() -> tuple[list, dict]:
if arm in ARMS:
real.append(rec)
if arm == "CTRLmargin":
plant_pool.append(rec)
# ложные претензии строятся из мест, где судья поставил НОЛЬ
# ⚠ дефект Г-1: посадкой считается только цитата, реально накрывшая
# подменённое место, а не любая претензия к клетке декоя
_, spans = margin_spans(C.base_a0(uid))
if hits_planted(var, q, spans):
plant_pool.append(rec)
# ⚠ ЛОЖНЫЕ ПРЕТЕНЗИИ — ПОЧИНКА 15.08 (дефект Г-2). Прежде окно ложной претензии
# строилось как `context(quote, quote)`, то есть текстом служила сама цитата:
# окружения не было и многоточий тоже, тогда как у настоящих претензий окно
# всегда шло из полного варианта и почти всегда с «…». Плюс ложная цитата была
# ЦЕЛЫМ предложением 40200 знаков против коротких фрагментов у настоящих.
# Адъюдикатору не нужно было судить перевод — хватало формы карточки.
# Теперь ложная цитата берётся ФРАГМЕНТОМ той же длины, что настоящие, из того же
# варианта, и окно строится тем же `context(var, …)`.
if zero and arm in ARMS:
sent = [x for x in re.split(r"(?<=[.!?])\s+", var) if 40 < len(x) < 200]
if sent:
false_pool.append(dict(uid=uid, arm=arm, axis=rnd.choice(zero),
quote=rnd.choice(sent), ctx="", src=u["source"],
fake=True))
# цитата подбирается ПОСЛЕ прохода, когда известно распределение длин
# настоящих цитат этого прогона; здесь копится только материал
false_pool.append(dict(uid=uid, arm=arm, axis=rnd.choice(zero),
var=var, src=u["source"], fake=True))
# стратифицированная выборка: не более PER_CELL претензий на (единица, арм)
by = {}
for r in real:
@ -150,8 +267,15 @@ def collect() -> tuple[list, dict]:
v = by[k]
rnd.shuffle(v)
sample += v[:PER_CELL]
for r in false_pool[:0] or rnd.sample(false_pool, min(N_FALSE, len(false_pool))):
r["ctx"] = context(r["quote"], r["quote"])
# ⚠ Длины ложных цитат берутся из ЭМПИРИЧЕСКОГО распределения настоящих (дефект Г-2).
real_len = [len(r["quote"]) for r in sample] or [40]
for r in rnd.sample(false_pool, min(N_FALSE, len(false_pool))):
var = r["var"]
frag = _fragment(var, rnd, rnd.choice(real_len))
if not frag:
continue
r = {k: v for k, v in r.items() if k != "var"}
r["quote"], r["ctx"] = frag, context(var, frag)
sample.append(r)
for r in rnd.sample(plant_pool, min(N_PLANT, len(plant_pool))):
r = dict(r, planted=True)
@ -200,9 +324,13 @@ def emit() -> None:
body = [HEAD.format(path=ANSW / f"{tok}.txt", skeleton=skel)]
for l in grp:
r = sample[labs.index(l)]
# ⚠ Окно берётся из СЫРОГО текста и может быть многострочным (абзацы — часть ФОРМЫ).
# Поэтому оно обрамляется явными границами: иначе перенос строки внутри окна
# неотличим от следующего поля карточки, и разбор — как машинный, так и глазной —
# видит разные карточки у разных претензий, то есть снова форма выдаёт класс.
body += ["", "=" * 60, f"{l}",
"ИСХОДНИК (китайский):", r["src"][:1800],
"", f"МЕСТО В ПЕРЕВОДЕ: {r['ctx'] or r['quote']}",
"", "МЕСТО В ПЕРЕВОДЕ:", "<<<", r["ctx"] or r["quote"], ">>>",
f"ПРЕТЕНЗИЯ: здесь ошибка типа {r['axis']} — в цитате «{r['quote']}»"]
(TASKS / f"{tok}.txt").write_text("\n".join(body), encoding="utf-8")
print(f"претензий {len(labs)} · заданий {len(list(TASKS.glob('*.txt')))}{TASKS}")
@ -212,6 +340,65 @@ def emit() -> None:
print(f"из них контролей: ложных претензий {n_f} · посаженных дефектов {n_p}")
def controls() -> int:
"""НЕРАЗЛИЧИМОСТЬ КЛАССОВ ПО ФОРМЕ КАРТОЧКИ — гейт, без которого контроли фиктивны.
Прежняя редакция проваливала его вчистую: ложная претензия шла без окружения и целым
предложением, то есть адъюдикатору хватало формы, чтобы отличить контроль от боевой претензии.
Здесь это меряется числом, а не обещается словом.
"""
if not AKEY.exists():
print("ключа нет — сначала --emit")
return 1
key = json.loads(AKEY.read_text(encoding="utf-8"))
cards = {}
for f in sorted(TASKS.glob("*.txt")):
for blk in f.read_text(encoding="utf-8").split("=" * 60)[1:]:
m = re.match(r"\s*(П\d{3})", blk)
if not m:
continue
w = re.search(r"<<<\n(.*?)\n>>>", blk, re.DOTALL)
q = re.search(r"в цитате «([^»]*)»", blk)
cards[m.group(1)] = (w.group(1) if w else "", q.group(1) if q else "")
if not cards:
print("заданий нет — сначала --emit")
return 1
groups = {
"настоящие": [k for k, v in key.items() if not v.get("fake") and not v.get("planted")],
"ЛОЖНЫЕ": [k for k, v in key.items() if v.get("fake")],
"посадки": [k for k, v in key.items() if v.get("planted")],
}
import statistics as _st
rows = {}
print(f"{'класс':10s} {'n':>4s} {'цитата':>7s} {'окно':>7s} {'с «…»':>6s} {'многостр':>9s}")
for name, sel in groups.items():
sel = [k for k in sel if k in cards]
if not sel:
print(f"{name:10s} пусто")
continue
ql = _st.median([len(cards[k][1]) for k in sel])
wl = _st.median([len(cards[k][0]) for k in sel])
el = sum(cards[k][0].startswith("") or cards[k][0].endswith("") for k in sel) / len(sel)
ml = sum("\n" in cards[k][0] for k in sel) / len(sel)
rows[name] = (ql, wl, el, ml)
print(f"{name:10s} {len(sel):4d} {ql:7.1f} {wl:7.1f} {el:5.0%} {ml:8.0%}")
rc = 0
base = rows.get("настоящие")
if base:
for name, r in rows.items():
if name == "настоящие":
continue
# медианы цитаты и окна не должны расходиться больше чем в полтора раза,
# а доля окон с многоточием — больше чем на четверть
if not (0.55 <= r[0] / max(base[0], 1) <= 1.8) or \
not (0.55 <= r[1] / max(base[1], 1) <= 1.8) or abs(r[2] - base[2]) > 0.25:
print(f"\n⛔ класс «{name}» ОТЛИЧИМ от настоящих претензий по форме карточки")
rc = 1
if not rc:
print("\nформа карточки классы не выдаёт — контроли пригодны")
return rc
def score() -> int:
if not AKEY.exists():
print("ключа нет — сначала --emit")
@ -220,7 +407,7 @@ def score() -> int:
ans = {}
for f in sorted(ANSW.glob("*.txt")):
for line in f.read_text(encoding="utf-8", errors="replace").splitlines():
m = re.match(r"\s*(П\d{3})\s*:\s*(ДА|НЕТ)", line.strip(), re.I)
m = re.match(r"\s*(П\d{3})\s*:\s*(ДА|НЕТ)", line.strip(), re.IGNORECASE)
if m:
ans[m.group(1)] = m.group(2).upper() == "ДА"
print(f"ответов получено: {len(ans)} из {len(key)}")
@ -261,11 +448,79 @@ def score() -> int:
return 0
def selftest() -> int:
"""Проверки ПОЧИНЕННЫХ контролей. Без них адъюдикация переносит доверие на уровень ниже."""
fails: list[str] = []
def ok(cond: bool, msg: str) -> None:
if not cond:
fails.append(msg)
# Г-3: имя оси не режется
for name in ("ВЕРНОСТЬ", "ТЕРМИН", "ЯЗЫК", "ФОРМА"):
got = [AX_HEAD.match(s.strip()).group(1)
for s in AX_SPLIT.split(f"{name}: «цитата» — пояснение") if AX_HEAD.match(s.strip())]
ok(got == [name], f"Г-3: ось {name} разобралась как {got}")
multi = "ВЕРНОСТЬ: «а» | «б». ЯЗЫК: «в». ФОРМА: «г»."
got = [AX_HEAD.match(s.strip()).group(1)
for s in AX_SPLIT.split(multi) if AX_HEAD.match(s.strip())]
ok(got == ["ВЕРНОСТЬ", "ЯЗЫК", "ФОРМА"], f"Г-3: разбор трёх осей дал {got}")
# Г-4: окно режется по СЫРОМУ тексту — пунктуация и регистр на месте
raw = 'Он сказал: «Стрела уже слетела с тетивы!» И замолчал, глядя вдаль.'
win = context(raw, "Стрела уже слетела", width=40)
ok("«" in win or "!" in win or "С" in win,
f"Г-4: окно потеряло пунктуацию и регистр: {win!r}")
ok(win.strip("") in raw, f"Г-4: окно не является подстрокой сырого текста: {win!r}")
# Г-1: посадки имеют позиции, и цитата вне них не считается посаженной
base = "Он не поверил своим глазам. Их было три. Обычное объяснение не помогло."
planted, spans = margin_spans(base)
ok(bool(spans), "Г-1: посадок не найдено — контроль чувствительности будет пуст")
ok(planted != base, "Г-1: текст декоя не отличается от эталона")
if spans:
a, b = spans[0]
ok(hits_planted(planted, planted[a:b], spans), "Г-1: цитата ПО посадке не опознана")
if spans:
# цитата из области, ЗАВЕДОМО не пересекающей ни одну посадку
busy = sorted(spans)
gap = next(((busy[i][1], busy[i + 1][0]) for i in range(len(busy) - 1)
if busy[i + 1][0] - busy[i][1] >= 12), None)
outside = planted[gap[0]:gap[1]].strip(" .,!?") if gap else ""
ok(not outside or not hits_planted(planted, outside, spans),
f"Г-1: цитата ВНЕ посадки ({outside!r}) засчитана как посаженная")
# Г-2: ложная цитата неотличима от настоящей по длине
rnd = random.Random(SEED)
long = " ".join(f"слово{i}" for i in range(40))
for target in (20, 45, 80):
frag = _fragment(long, rnd, target)
ok(bool(frag) and abs(len(frag) - target) <= 8,
f"Г-2: фрагмент под цель {target} вышел {len(frag)}: {frag!r}")
# Г-2 на живом ключе, если он есть: форма карточки не должна выдавать ложную
if AKEY.exists():
key = json.loads(AKEY.read_text(encoding="utf-8"))
bad = [k for k, v in key.items() if v.get("axis") not in
("ВЕРНОСТЬ", "ТЕРМИН", "ЯЗЫК", "ФОРМА")]
ok(not bad, f"Г-3: в ключе {len(bad)} претензий с обрезанным именем оси "
f"(пример {bad[:3]}) — ключ снят ДО починки, требуется пере-эмиссия")
for m in fails:
print("ПРОВАЛ:", m)
print(f"селфтест адъюдикации: провалов {len(fails)}")
return 1 if fails else 0
if __name__ == "__main__":
a = sys.argv[1:] or ["--emit"]
if a[0] == "--emit":
emit()
elif a[0] == "--score":
sys.exit(score())
elif a[0] == "--selftest":
sys.exit(selftest())
elif a[0] == "--controls":
sys.exit(controls())
else:
raise SystemExit(f"⛔ неизвестный режим {a[0]!r}")

View file

@ -22,6 +22,7 @@
счёту ошибок, и потому решает судьбу СЧЁТА как дешёвого прокси, а не судьбу армов.
Запуск: chtenie.py --emit | --score | --selftest | --score-calib
chtenie.py --emit-owner | --score-owner пакет ВЛАДЕЛЬЦУ (4 варианта, «жирная глава»)
"""
from __future__ import annotations
@ -193,7 +194,42 @@ def emit() -> None:
# ⚠ Латинская `T` принимается наравне с кириллической `Т` (починка по ревью): судьи уже писали
# латиницей, а `zsud`/`ja6` это учитывают. Молча выпавший ответ читателя стоил бы целой сессии.
_ORD = re.compile(r"ПОРЯДОК\s*(\d+)\s*:\s*((?:[ТT]\d+\s*[>=]\s*)+[ТT]\d+)", re.IGNORECASE)
# ⚠ ФОРМА ОТВЕТА ЧЕЛОВЕКА ШИРЕ, ЧЕМ Я ПРЕДПИСАЛ, И ЭТО ДЕФЕКТ РАЗБОРЩИКА, А НЕ ОТВЕТА.
# Владелец 17.08 ответил «Т3 > Т1 > Т2 > Т4» под заголовком «Порядок читаемости» и
# «**ПОРЯДОК ЧИТАЕМОСТИ:** `Т1 > Т4 = Т3 > Т2`» — обе формы законны и обе прежним регексом
# не читались: он требовал НОМЕРА отрывка сразу после слова. Инструмент, который не берёт
# честный ответ, обесценивает работу читателя, а не читателя.
# Теперь номер необязателен (нет — нумеруем по порядку появления), markdown-разметка и
# обратные кавычки снимаются, латинская T принимается наравне с кириллической.
_ORD_NUM = re.compile(r"ПОРЯДОК[^\n:]*?(\d+)\s*:", re.IGNORECASE)
_ORD = re.compile(r"((?:[ТT]\d+\s*[>=]\s*)+[ТT]\d+)")
def orders(txt: str) -> dict[str, str]:
"""{номер отрывка: строка порядка}. Номер берётся из подписи «ПОРЯДОК … N:», если она есть;
если её нет порядки нумеруются по появлению в тексте.
Заведено 17.08 после того, как разборщик не взял ЧЕСТНЫЙ ответ владельца: он написал
порядок под заголовком «## 1. Порядок читаемости», без обязательного номера сразу за словом.
Требовать от человека машинной формы и молча терять его работу дефект инструмента.
Строки порядка ищутся ПО ВСЕМУ тексту, но берутся только те, что стоят ПОСЛЕ слова
«ПОРЯДОК» либо первыми в файле: иначе в разбор попали бы упоминания меток из комментариев
вроде «Т3 и Т1 стоят близко».
ВТОРАЯ ПОЧИНКА ТОГО ЖЕ ДНЯ: порядок ВЕРНОСТИ отбрасывается. Владелец дал по английской
паре две строки «ПОРЯДОК ЧИТАЕМОСТИ» и «ПОРЯДОК ВЕРНОСТИ», а разборщик принял вторую за
порядок второго отрывка и молча отсудил ею чужую главу. Прибор мерит ЧИТАЕМОСТЬ; верность
считается детерминированно и в этот разбор не входит.
"""
out: dict[str, str] = {}
for i, m in enumerate(_ORD.finditer(txt), 1):
head = txt[max(0, m.start() - 200):m.start()]
if "ВЕРНОСТ" in head.upper():
continue
nm = _ORD_NUM.findall(head)
key = nm[-1] if nm else str(len(out) + 1)
out.setdefault(key, m.group(1))
return out
def _ranks(order: str) -> dict[str, float]:
@ -254,8 +290,7 @@ def score() -> int:
f = WORK / meta0["pair"] / meta0["reader"] / "answers" / f"{tok}.md"
if not f.exists():
continue
got = {m.group(1): m.group(2) for m in _ORD.finditer(
f.read_text(encoding="utf-8", errors="replace"))}
got = orders(f.read_text(encoding="utf-8", errors="replace"))
for j, meta in blocks.items():
if j not in got:
print(f"{tok}/отрывок {j}: порядка нет")
@ -344,6 +379,210 @@ def score_calib() -> int:
return 0
# ── ПАКЕТ ДЛЯ ВЛАДЕЛЬЦА: он читает сам, человеческим глазом ─────────────────────────────────────
# ⚠ ЗАЧЕМ ОТДЕЛЬНЫЙ РЕЖИМ, А НЕ ТОТ ЖЕ ПАКЕТ. Агентские пачки собраны под МАШИННОГО читателя:
# восемь глав на ось, семь вариантов, двадцать заданий. Владелец дважды не стал такое читать и
# сказал прямо: «чтоб не слишком много было, давай какую-нибудь жирную главу на язык». Значит
# ограничение здесь — ЕГО ВРЕМЯ, и панель режется под него, а не под статистику.
#
# ЧТО ОСТАВЛЕНО И ПОЧЕМУ РОВНО ЭТО (четыре варианта вместо семи):
# Z0 боевая связка — то, что стоит в продакшене; без неё сравнивать не с чем;
# ZF ВТОРАЯ генерация той же связки — контроль шума. Если владелец разведёт их далеко, его
# порядок есть шум, и «победа» любого арма внутри этого расстояния ничего не значит.
# Это единственный способ узнать, различает ли он вообще что-то, — и он же самый дешёвый;
# ZP однопроходка — один вызов вместо двух, и по банку с канон-фиксером она впереди связки;
# Z1 критик → полный перепис — ставка владельца в сильнейшей форме.
# Голый черновик НЕ включён намеренно: оба прибора и оба машинных читателя ставят его последним
# с огромным отрывом, спора нет, а глава черновика — это лишние 7 тысяч знаков его чтения.
# Обогащённый черновик с переписом тоже снят: от боевой связки он неотличим ни одним прибором.
#
# ⚠ ДВА ВОПРОСА, КАК ПРОСИЛ ВЛАДЕЛЕЦ («лучше по художке и при этом лучше держит канон смысл»), но
# ЧЕСТНО РАЗВЕДЁННЫЕ. Читаемость может назвать только он. Верность исходнику на КИТАЙСКОМ он
# назвать не может — для этого надо читать китайский, — поэтому там его просят отметить лишь то,
# что видно без исходника: провалы связности, непонятные места, оборванные мысли. На английском
# исходник приложен, и сверка возможна, но объявлена необязательной. Канон терминов при этом уже
# посчитан детерминированно и в пакет НЕ кладётся до его ответа — иначе это подсказка.
OWNER_PANEL = ("Z0", "ZF", "ZP", "Z1")
OWNER_DIR = Path.home() / "books" / "chtenie-vladeltsa-z17"
OWNER_KEY = KEYD / "chtenie-vladeltsa-z17-KEY.json"
OWNER_SALT = "vladelets-z17-2026-08-17"
OWNER_N = {"zh": 1, "en": 2} # «жирная глава на язык»; английские главы втрое короче
OWNER_HEAD = """# Что ты читаешь и зачем — коротко
Ниже {n} перевода одного и того же отрывка. Сделаны они разными способами: где-то один вызов
модели, где-то два, где-то с промежуточным критиком. **Какой чем сделан не сказано намеренно.**
Метки перемешаны, расшифровка лежит в отдельном файле и до твоего ответа не открывается.
**Зачем это нужно.** Наш измеритель считает локальные ошибки искажения смысла, кальки, сбитую
вёрстку. Он дёшев и воспроизводим, но у него встроенная слепота: текст может быть без единой
ошибки и при этом мёртвый. На последнем прогоне это подтвердилось числом порядок по счёту ошибок
и порядок по читаемости совпали лишь на треть, а вариант с наименьшим числом ошибок читался
почти хуже всех. Твой ответ решает, верить ли дальше счётчику или чтению.
**Одна из четырёх версий контроль.** Две из них сделаны ОДНИМ И ТЕМ ЖЕ способом, просто модель
вызвана дважды. Какие именно не скажу. Если ты поставишь их рядом, значит ты различаешь способы;
если разведёшь далеко, значит разница между способами меньше собственного разброса модели, и тогда
никакая «победа» в этом замере ничего не стоит. Специально их искать не надо просто читай.
Метки сквозные: `Т1` в первом отрывке и `Т1` во втором ОДИН И ТОТ ЖЕ способ.
Поэтому порядок можно называть и по каждому отрывку, и один общий на всю пару.
**Что может сбить.** Вёрстка выдаёт метод: часть вариантов сливает построчный исходник в русские
абзацы, часть сохраняет разбивку. Это видно ДО чтения не давай форме решать за тебя.
---
## Что записать — в файл `{answer}`
1. **ПОРЯДОК ЧИТАЕМОСТИ** строкой: `Т3 > Т1 > Т2 > Т4`, от лучшего к худшему.
Неразличимые соединяй через `=`. Это главный вопрос: какой текст ты взял бы в книгу.
2. **ПОРЯДОК ВЕРНОСТИ** {fidelity}
3. **По фразе на вариант**: почему. Годится и «серо, но гладко», и «живой ритм, но во втором
абзаце теряется, кто кому говорит».
Если какой-то вариант покажется откровенно машинным скажи прямо, это самое ценное.
---
"""
# ⚠ Верность исходнику владелец может назвать только там, где читает исходный язык. На китайском
# он его не читает, и просить «сверь смысл» значило бы просить невыполнимого — а невыполнимая
# просьба в задании обесценивает и выполнимую часть. Поэтому на zh просят то, что видно БЕЗ
# исходника: провалы связности. Это не подмена вопроса, а его честная граница.
OWNER_FID = {
"zh": ("исходник китайский, и сверять его тебе нечем — поэтому этот пункт ПРОПУСТИ.\n"
" Вместо него отметь то, что видно и без исходника: где текст теряет связность,\n"
" где непонятно, кто что делает, где мысль обрывается. Такие места почти всегда\n"
" и оказываются враньём про исходник."),
"en": ("исходник приложен, английский. Если станешь сверять — назови порядок по верности\n"
" отдельной строкой. Не станешь — пропусти, это необязательно."),
}
def emit_owner(only: list[str] | None = None) -> None:
"""Пакет владельцу. `only` — пересобрать ТОЛЬКО названные пары.
ПЕРЕ-ЭМИССИЯ НЕ ИМЕЕТ ПРАВА ТРОНУТЬ ПАРУ, КОТОРУЮ ВЛАДЕЛЕЦ УЖЕ ПРОЧЁЛ. Раскладка меток
есть чистая функция соли и набора армов; пересборка ЦЕЛОГО пакета переписала бы ключ и
китайской пары тоже а её ответ уже дан и расшифрован. Тогда та же расшифровка при следующем
запуске дала бы ДРУГИЕ армы, и работа владельца превратилась бы в мусор молча. Ровно так фаза
Д однажды потеряла вердикт `E0/D0` (+1.56 +0.38). Поэтому ключ здесь ДОГРУЖАЕТСЯ, а не
создаётся заново, и пары вне `only` не пересчитываются.
Прежний ответ по пересобираемой паре не удаляется, а уводится в архив: это улика работа
владельца, испорченная дефектом МОЕЙ сборки, а не его чтением.
"""
key: dict = {"_что": "слепое чтение ВЛАДЕЛЬЦЕМ, панель захода Д17", "_соль": OWNER_SALT,
"_панель": list(OWNER_PANEL),
"онтроль": "Z0 и ZF — две генерации ОДНОЙ боевой связки; развёл далеко = шум"}
if OWNER_KEY.exists():
key = {**json.loads(OWNER_KEY.read_text(encoding="utf-8")), **key}
OWNER_DIR.mkdir(parents=True, exist_ok=True)
for pair, langname, srcname in (("zh", "КИТАЙСКАЯ", "китайский"),
("en", "АНГЛИЙСКАЯ", "английский")):
if only and pair not in only:
print(f" {pair}: не в списке пересборки — ключ и задание НЕ трогаются")
continue
prev = OWNER_DIR / f"ОТВЕТ-{pair}.md"
if prev.exists() and _ORD.search(prev.read_text(encoding="utf-8")):
arch = OWNER_DIR / f"ОТВЕТ-{pair}.ИСПОРЧЕННЫЙ-ПАКЕТ.md"
arch.write_text(prev.read_text(encoding="utf-8"), encoding="utf-8")
prev.unlink()
print(f" {pair}: прежний ответ уведён в {arch.name} (улика, не удалён)")
tx_all = ZS.texts_of(pair)
# «жирная глава»: самая длинная из тех, где панель полна — длинная честнее короткой
cand = [u for u in tx_all if all(tx_all[u].get(a, "").strip() for a in OWNER_PANEL)]
cand.sort(key=lambda u: -sum(len(tx_all[u][a]) for a in OWNER_PANEL))
take = cand[:OWNER_N[pair]]
ans = OWNER_DIR / f"ОТВЕТ-{pair}.md"
body = [f"# {langname} ПАРА — слепое чтение\n",
OWNER_HEAD.format(n=len(OWNER_PANEL), answer=ans, fidelity=OWNER_FID[pair])]
for j, uid in enumerate(take, 1):
# ⚠ РАСКЛАДКА ОДНА НА ВСЮ ПАРУ, А НЕ СВОЯ У КАЖДОГО ОТРЫВКА (починка 17.08).
# Первая редакция солила раскладку номером главы, и метка `Т1` означала РАЗНЫЕ
# армы в первом и втором английском отрывке. Владелец — как поступил бы любой
# читатель — дал ОДИН сводный порядок на пару и говорил о «Т1» как об одном
# варианте; сопоставить это с армами оказалось нечем, и английская половина
# его работы пропала не по его вине. Машинным читателям своя раскладка на
# единицу нужна (они судят поединично), человеку — вредна.
lay = layout(pair, "OWNER-ALL-v2", "OWNER", OWNER_PANEL)
body += [f"\n# ОТРЫВОК {j}\n", f"## Исходник ({srcname})\n",
tx_all[uid]["_src"], ""]
for lab, arm in lay.items():
body += ["\n---\n", f"## Отрывок {j}, вариант {lab}\n", tx_all[uid][arm], ""]
key.setdefault(pair, {})[str(j)] = {
"uid": uid, "метки": lay,
"знаков": {a: len(tx_all[uid][a]) for a in OWNER_PANEL}}
(OWNER_DIR / f"ЧТЕНИЕ-{pair}.md").write_text("\n".join(body), encoding="utf-8")
if not ans.exists():
ans.write_text(
f"# Ответ — {langname.lower()} пара\n\n"
+ "".join(f"ПОРЯДОК ЧИТАЕМОСТИ {j}: \n" for j in range(1, len(take) + 1))
+ "\nЗаметки по вариантам:\n", encoding="utf-8")
n = sum(len(tx_all[u][a]) for u in take for a in OWNER_PANEL)
print(f" {pair}: глав {len(take)} · вариантов {len(OWNER_PANEL)} · {n // 1000} тыс. знаков")
OWNER_KEY.write_text(json.dumps(key, ensure_ascii=False, indent=1), encoding="utf-8")
print(f"\nпакет → {OWNER_DIR}")
print(f"ключ → {OWNER_KEY} (НЕ открывать до ответа)")
def score_owner() -> int:
if not OWNER_KEY.exists():
print("ключа нет — сначала --emit-owner")
return 1
key = json.loads(OWNER_KEY.read_text(encoding="utf-8"))
sc, _bad = ZS.read()
for pair in ("zh", "en"):
f = OWNER_DIR / f"ОТВЕТ-{pair}.md"
if not f.exists() or pair not in key:
continue
txt = f.read_text(encoding="utf-8")
got = orders(txt)
if not got:
print(f"{pair}: порядка нет — ждём ответа владельца")
continue
print(f"\n=== {pair} ===")
# ⚠ СТОРОЖ НЕОДНОЗНАЧНОЙ РАСШИФРОВКИ (заведён 17.08 по факту). Если в ключе у отрывков
# РАЗНЫЕ раскладки меток, а читатель дал ОДИН сводный порядок, то механическое применение
# этого порядка к каждой раскладке даёт столько же РАЗНЫХ расшифровок, сколько отрывков, и
# все они одинаково «правдоподобны». Так и вышло с английской парой: один и тот же ответ
# владельца дал «однопроходка первая» на первом отрывке и «однопроходка ПОСЛЕДНЯЯ» на
# втором. Печатать это как результат — значит выдать артефакт разметки за замер.
maps = [tuple(sorted(m["метки"].items())) for m in key[pair].values()]
if len(set(maps)) > 1 and len(got) < len(key[pair]):
print(" ⛔ РАСШИФРОВКА НЕОДНОЗНАЧНА И НЕ ВЫВОДИТСЯ. У отрывков РАЗНЫЕ раскладки")
print(" меток, а порядок дан ОДИН сводный: одна и та же метка означает в них")
print(" разные армы. Это дефект СБОРКИ ПАКЕТА, а не ответа читателя.")
print(" Что уцелело — устойчивое к раскладке: какие метки читатель поставил")
print(" РЯДОМ или связал знаком «=». Ниже только это.")
for j, meta in key[pair].items():
if j not in got:
continue
r = _ranks(got[j])
pairs = [(a, b) for a in r for b in r if a < b and abs(r[a] - r[b]) <= 1]
for a, b in pairs:
arms = {meta["метки"].get(a), meta["метки"].get(b)}
print(f" {a} и {b} рядом → в этом отрывке это {' и '.join(sorted(x for x in arms if x))}")
continue
for j, meta in key[pair].items():
if j not in got:
continue
r = _ranks(got[j])
rows = [(meta["метки"][lab], r[lab]) for lab in meta["метки"] if lab in r]
print(f" отрывок {j} (глава {meta['uid']}):")
for arm, rank in sorted(rows, key=lambda x: x[1]):
errs = [ZS._carry(ax) for (pp, u, aa, _h), ax in sc.items()
if pp == pair and aa == arm and u == meta["uid"]]
e = f"{st.mean(errs):.1f}" if errs else ""
print(f" {rank:.1f} {arm:4s} ошибок по судье {e}")
d = {a: rk for a, rk in rows}
if "Z0" in d and "ZF" in d:
gap = abs(d["Z0"] - d["ZF"])
print(f" КОНТРОЛЬ ШУМА: две генерации одной связки разведены на {gap:.1f} "
f"позиции → {'порядок несёт сигнал' if gap <= 1 else 'ПОРЯДОК = ШУМ'}")
return 0
def selftest() -> int:
fails = []
ok = lambda c, m: fails.append(m) if not c else None # noqa: E731
@ -351,8 +590,11 @@ def selftest() -> int:
ok(r == {"Т3": 1.0, "Т1": 2.5, "Т5": 2.5, "Т2": 4.0}, f"разбор порядка сломан: {r}")
ok(abs(_spearman([1, 2, 3], [1, 2, 3]) - 1.0) < 1e-9, "Спирмен не даёт +1 на совпадении")
ok(abs(_spearman([1, 2, 3], [3, 2, 1]) + 1.0) < 1e-9, "Спирмен не даёт 1 на инверсии")
m = _ORD.search("ПОРЯДОК 2: Т3 > Т1 > Т5")
ok(bool(m) and m.group(1) == "2", "строка порядка не находится")
ok(orders("ПОРЯДОК 2: Т3 > Т1 > Т5") == {"2": "Т3 > Т1 > Т5"}, "нумерованный порядок")
ok(list(orders("## Порядок читаемости\n\nТ3 > Т1 > Т2 > Т4").values()) == ["Т3 > Т1 > Т2 > Т4"],
"ЧЕЛОВЕЧЕСКАЯ форма ответа без номера не читается — на ней уже терялась работа владельца")
ok(list(orders("**ПОРЯДОК ЧИТАЕМОСТИ:** `Т1 > Т4 = Т3 > Т2`").values()) == ["Т1 > Т4 = Т3 > Т2"],
"порядок в markdown-разметке не читается")
for pair in Z.PAIRS:
u = pick(pair)
ok(len(u) == N_UNITS, f"{pair}: единиц {len(u)}, ожидалось {N_UNITS}")
@ -375,6 +617,8 @@ def selftest() -> int:
if __name__ == "__main__":
a = sys.argv[1:] or ["--selftest"]
fn = {"--emit": emit, "--score": lambda: sys.exit(score()),
"--emit-owner": lambda: emit_owner([x for x in a[1:] if x in ("zh", "en")] or None),
"--score-owner": lambda: sys.exit(score_owner()),
"--score-calib": lambda: sys.exit(score_calib()),
"--selftest": lambda: sys.exit(selftest())}.get(a[0])
if not fn:

View file

@ -167,9 +167,15 @@ def cmd_status() -> None:
fail = [a for a in at if not a.get("ok")]
print(f"Д1 · собрано {len(have)}/{len(us)} · порог заказа ≥{N_ENOUGH}")
print(f" попыток всего {len(at)} · удачных {len(ok)} · отказов {len(fail)}")
if at:
span = (max(a.get("ts", 0) for a in at) - min(a.get("ts", 0) for a in at)) / 3600
print(f" окно журнала: {span:.1f} ч (заказ требует ≥24 ч до вердикта «не отдаёт»)")
# ⚠ Окно считается ТОЛЬКО по записям с меткой времени. Записи первой редакции её не несли,
# и `.get("ts", 0)` давал минимум 0 — то есть отсчёт от эпохи и «окно 496 315 часов».
# Число выглядело солидно и означало ровно ничего.
ts = [a["ts"] for a in at if a.get("ts")]
if len(ts) >= 2:
print(f" окно журнала: {(max(ts) - min(ts)) / 3600:.2f} ч по {len(ts)} записям с меткой "
f"(заказ требует ≥24 ч ДО вердикта «не отдаёт»)")
elif at:
print(f" окно журнала: записей с меткой времени {len(ts)} — окно не доказуемо")
codes: dict = {}
for a in fail:
codes[str(a.get("code"))] = codes.get(str(a.get("code")), 0) + 1

View file

@ -24,6 +24,7 @@
и запрет механизирован тем, что пороги считаются ДО контрастов и печатаются рядом с ними.
Запуск: itog_d4.py [--gates] # только гейты, без боевых чисел
itog_d4.py --sens # Г5: чувствительность вердикта H-1 к составу пачек ($0)
"""
from __future__ import annotations
@ -107,17 +108,47 @@ def variant(arm: str, u: dict, half: int) -> str:
if arm == "CTRLfloor":
return S.floor_pair(u, half)
if arm == "CTRLdecoy":
return AJ._plant(S.C.base_a0(u["uid"]))[0] # noqa: SLF001
return AJ._plant(S.C.base_a0(u["uid"]))[0]
return S.text_of(arm, u)
# ⚠ БАЗА АРМА — то, поверх чего он работает и что уже оплачено к моменту его вызова. Держится
# ДАННЫМИ, потому что состав панели у каждой оси свой; числа — медианы по сырью паков 22/23.
BASE_COST_BY_AXIS = {
"d4": {"A0": 0.00603, "A1": 0.00096, "A1B": 0.00096, "A3": 0.00096, "A2": 0.0,
"A4": 0.00603, "A6": 0.00387, "A6F": 0.00387},
"d3": {"E0": 0.00096, "E1": 0.00096, "E3": 0.00096, "E4": 0.00096 + 0.00789,
"E2": 0.0, "E6": 0.00096, "D0": 0.0},
"d6": {"J0": 0.00096, "J2": 0.0, "D0": 0.0},
"d1": {"A0": 0.00603, "R1": 0.00096},
}
def _keys_for(root: Path) -> Path:
"""Каталог ключей ЭТОГО семейства. Ключи разведены (`sud.py`), потому что эмиссия пачек
одного семейства переписывала бы раскладку другого. Свод обязан читать ответы тем же ключом,
каким они выпускались, иначе метки сопоставятся с ЧУЖИМИ армами и разбор даст 0 меток
ровно это и случилось на первом прогоне семейства Sol."""
fam = "claude" if root.name.startswith("sud-") else root.name.split("-")[0]
return KEYS if fam == "claude" else KEYS.parent / f"{KEYS.name}-{fam}"
if fam == "claude":
return KEYS
# ⚠ Ключи разведены по семействам НЕ на всех осях: разводить их начали на Д1/Д3/Д6, после
# аварии с пере-эмиссией, а китайская ось выпускала ОБА семейства одним ключом. Жёсткое
# `-{fam}` роняло свод на sol-d4-work («62 ответа, разобрано 0 меток»), то есть заявленный
# носитель чисел Д4 не воспроизводил ось вовсе. Разведённый ключ — если он есть; иначе общий.
split = KEYS.parent / f"{KEYS.name}-{fam}"
return split if split.exists() else KEYS
# ⚠ СОСТАВ ПАЧЕК — ПАРАМЕТР ТОЛЬКО ДЛЯ ЧУВСТВИТЕЛЬНОСТИ, И ОБА СПИСКА ПУСТЫ ПО УМОЛЧАНИЮ.
# Печатаемый вердикт при этом не меняется ни на знак: `--drop`/`--restore` существуют, чтобы
# пере-считать его ПРИ ДРУГОМ составе и напечатать разницу рядом, а не чтобы подобрать удобный.
# Заведены под Г5 (находка ревизии P13: снятие двух пачек прогона-валидации 11.08 переворачивает
# решение по H-1). Без такого рычага пере-счёт делался копией дерева — то есть числами, которые
# никто не может воспроизвести.
DROP = set(next((a.split("=", 1)[1] for a in sys.argv if a.startswith("--drop=")), "").split(",")) - {""}
RESTORE = set(next((a.split("=", 1)[1] for a in sys.argv
if a.startswith("--restore=")), "").split(",")) - {""}
def read_family(root: Path) -> tuple[dict, list]:
@ -134,9 +165,19 @@ def read_family(root: Path) -> tuple[dict, list]:
continue
key_all = json.loads(kf.read_text(encoding="utf-8"))
for f in sorted((root / f"{half}-answers").glob("*.txt")):
tok = f.name.split(".")[0]
if ".ANNULLED" in f.name:
if tok not in RESTORE:
continue
# ⚠ Пачка, отправленная в карантин и ПЕРЕ-СУЖЕННАЯ, лежит на диске дважды.
# Вернуть карантинную копию значит посчитать единицу дважды разными судьями —
# молча, потому что ключ у обеих один. Возвращать можно только то, чему замены нет.
if (f.parent / f"{tok}.txt").exists():
raise SystemExit(f"{tok} пере-суждена: возврат карантинной копии удвоил бы "
f"единицу; --restore допустим только для пачек без замены")
if tok in DROP:
continue
key = key_all.get(f.stem)
key = key_all.get(tok)
if not key:
continue
txt = f.read_text(encoding="utf-8", errors="replace")
@ -146,13 +187,13 @@ def read_family(root: Path) -> tuple[dict, list]:
# ⚠ `[ \t]*`, а не `\s*`: `\s` включает перевод строки, и оценка
# могла считаться со СЛЕДУЮЩЕЙ строки. Тот же класс `absjudge.py:351`
# чинил у себя; здесь он оставался.
m = re.search(rf"^{lab}-{a}:[ \t]*(\d+)", txt, re.M)
m = re.search(rf"^{lab}-{a}:[ \t]*(\d+)", txt, re.MULTILINE)
if m:
sc[a] = int(m.group(1))
if len(sc) < 4:
bad.append((root.name, half, f.stem, lab, "не все оси"))
continue
w = re.search(rf"^{lab}-ПОЧЕМУ:\s*(.*)$", txt, re.M)
w = re.search(rf"^{lab}-ПОЧЕМУ:\s*(.*)$", txt, re.MULTILINE)
why = w.group(1) if w else ""
if sum(sc.values()) and not why.strip():
bad.append((root.name, half, f.stem, lab, "ненулевая без обоснования"))
@ -221,16 +262,16 @@ def three_axes(d: dict) -> None:
Канон считается на ВЫХОДЕ арма, а не на его входе: вход мерился при покупке, а сверяется
качество результата. Цена медиана оплаченной клетки арма из сырья, а не из сметы.
"""
import json as _j # noqa: PLC0415
import statistics as _st # noqa: PLC0415
import json as _j
import statistics as _st
us = {x["uid"]: x for x in S.units()}
print("\n" + "=" * 78)
print("ТРИ ОСИ ПО КАЖДОМУ АРМУ (требование заказа :95) — судья · канон · цена")
print("=" * 78)
print(f" {'арм':5s}{'судья':>8s}{'канон':>9s}{'$/клетка':>11s} что это")
print(f" {'арм':5s}{'судья':>8s}{'канон':>9s}{'$/клетка':>11s}{'$/единица':>12s} что это")
what = WHAT_AXIS[AXIS]
_unused = {"A0": "", "A4": "",
"A6": "", "A6F": "",
_dead = {
"A0": "", "A4": "", "A6": "", "A6F": "",
"A3": "черновик + смысловой критик", "A2": "однопроходка уравненного мандата",
"A1B": "черновик + флаги (батарея+канон)", "A1": "черновик + только канон-флаги"}
for arm in ALL_ARMS:
@ -244,20 +285,227 @@ def three_axes(d: dict) -> None:
k, n = S.C.BANK.coverage(u["source"], t)
if n:
cov.append(k / n)
pr = []
pr, per_unit = [], {}
# ⚠ Глоб по тегу арма захватывал и клетки КРИТИКА (`dv-a-a3-crit-*`), поэтому
# медиана «$/клетка» у A3 и A6 считалась по смеси двух ролей. Найдено приёмкой.
for f in S.C.OUT.glob(f"{S.C.tag(arm, '')}*.json"):
if any(x in f.name for x in (".ERROR", ".LENGTH", ".FLAGSV1", "-crit-")):
# ⚠ Суффиксы карантина перечислены ПОЛНОСТЬЮ. `.STALE` и `.TWIN` заведены фазой Д на ходу
# (устаревшие бесплатные клетки арма и побайтные близнецы пола), и их отсутствие здесь
# тянуло медиану цены вниз нулевыми клетками. Найдено чтением собственных коммитов.
if any(x in f.name for x in (".ERROR", ".LENGTH", ".FLAGSV1", ".STALE", ".TWIN")):
continue
r = _j.loads(f.read_text(encoding="utf-8"))
if r.get("cost_usd"):
pr.append(r["cost_usd"])
c = r.get("cost_usd") or 0
# ⚠ ДВЕ РАЗНЫЕ ВЕЛИЧИНЫ, И ПУТАТЬ ИХ ДОРОГО. «$/клетка» — цена ОДНОГО вызова;
# «$/единица» — цена всей работы арма над единицей ПЛЮС база, на которой он стоит.
# У армов с двумя вызовами (критик + фиксер) это отличается втрое: у A3 клетка
# фиксера 0.00644, а единица — 0.02014 плюс черновик. Отчёт печатал первую цифру
# в колонке, по которой сравнивают экономику, и ставка H-2б выглядела средней
# по цене, будучи самой дорогой в панели.
if "-crit-" not in f.name and c:
pr.append(c)
uid = r.get("uid") or f.stem.split("-")[-1]
per_unit[uid] = per_unit.get(uid, 0.0) + c
base = BASE_COST_BY_AXIS.get(AXIS, {}).get(arm, 0.0)
full = (_st.median([v + base for v in per_unit.values() if v > 0])
if any(v > 0 for v in per_unit.values()) else base)
print(f" {arm:5s}{_st.mean(sc):8.2f}{(_st.mean(cov) if cov else 0):9.3f}"
f"{(_st.median(pr) if pr else 0):11.5f} {what.get(arm, '')}")
f"{(_st.median(pr) if pr else 0):11.5f}{full:12.5f} {what.get(arm, '')}")
print(" судья — ошибок на единицу (ВЕРНОСТЬ+ЯЗЫК), МЕНЬШЕ лучше")
print(" канон — доля покрытия банка на ВЫХОДЕ арма, БОЛЬШЕ лучше")
print(" цена — медиана оплаченной клетки арма по сырью; A0 куплен прошлыми паками")
print(" $/клетка — медиана ОДНОГО оплаченного вызова арма")
print(" $/единица — вся работа арма над единицей ПЛЮС база, на которой он стоит:")
_b = BASE_COST_BY_AXIS.get(AXIS, {})
print(f" {' · '.join(f'{a}+{c:.5f}' for a, c in sorted(_b.items()) if c)}")
print(" (замер по сырью: черновик flash 0.00096, перепис dspro 0.00507)")
def margins_half(d: dict, a: str, b: str) -> list[float]:
"""Перевес ЗА арм `a`, посчитанный ВНУТРИ половины и лишь потом усреднённый по единице.
Зачем вторая формула рядом со штатной. Наборы p1/p2 судятся РАЗНЫМИ сессиями, и между ними
намерен систематический сдвиг строгости (находка ревизии P07: пол claude даёт +1.8 ошибки в
пользу p2). Штатная `margins` берёт среднее арма по тем половинам, где он ЕСТЬ, а половины
у армов заполнены не одинаково (`A6` 15/16 против `A0` 30/31). Там, где арм присутствует
только в одной половине, в его число входит уровень ЭТОЙ сессии, и разность двух армов несёт
кусок межсессионного сдвига как будто это разница армов. Здесь разность берётся там, где оба
арма судила ОДНА сессия, поэтому уровень сессии сокращается тождественно.
"""
out = []
for uid in sorted({k[0] for k in d}):
per = [d[(uid, b, h)]["carry"] - d[(uid, a, h)]["carry"]
for h in ("p1", "p2") if (uid, a, h) in d and (uid, b, h) in d]
if per:
out.append(st.mean(per))
return out
def _levels(d: dict) -> dict:
"""Средний счёт боевых армов по СЕССИЯМ судейского журнала: {(проход, судья): (n, среднее)}.
Это единственный оставшийся способ проверить посылку P13. Находка называла пачки
`0dce349331`/`0ffee70740` «другим режимом замера» на двух основаниях: не замороженный промт и
уровень счёта 9.94 против 6.39 у основной волны. Транскрипта сессии на диске больше нет
(проверено `find` по всем каталогам агентов) промт сверить не с чем. Уровень сверить можно,
и вопрос ставится честно: выбивается ли д-01 из РАЗБРОСА ОСТАЛЬНЫХ СЕССИЙ или такой шаг между
сессиями у этой оси обычный. Во втором случае снятие двух пачек не норма, а выбор.
"""
led = json.loads((Path.home() / "books" / "dovodka" / "agent-runs.json").read_text("utf-8"))
battle = set(ALL_ARMS)
out: dict = {}
for r in led:
if r.get("run") != AXIS:
continue
half = r.get("pass_")
sc = [v["carry"] for (uid, arm, h), v in d.items()
if h == half and arm in battle and uid in _uids_of(r, half)]
if sc:
out[(half, r["judge"], tuple(r["tokens"]))] = (len(sc), st.mean(sc))
return out
def _uids_of(run: dict, half: str) -> set:
"""uid единиц, которые судила эта сессия. Пачка названа ТОКЕНОМ, ключ переводит его в uid."""
kf = KEYS / f"{AXIS}{half}-KEY.json"
if not kf.exists():
return set()
key_all = json.loads(kf.read_text(encoding="utf-8"))
out = set()
for tok in run.get("tokens", []):
for meta in (key_all.get(tok) or {}).values():
out.add(meta["uid"])
return out
def sens() -> int:
"""Г5 — ЧУВСТВИТЕЛЬНОСТЬ ВЕРДИКТА H-1 К СОСТАВУ ПАЧЕК. $0, ни одного вызова к моделям.
ПРАВИЛО РЕШЕНИЯ ОБЪЯВЛЕНО ДО ПРОГОНА (иначе это подбор сценария, а не замер):
вердикт по H-1 считается ПЕРЕ-РЕШЁННЫМ, только если он одинаков во ВСЕХ сценариях, снятие
в которых обосновано нормой проекта. Если знак решения зависит от выбора сценария печатается
именно это, а не удобная половина. Запас над порогом печатается числом рядом: решение,
выигранное с запасом меньше собственного шага шкалы (1 ошибка), вердиктом не является.
"""
print("=" * 78)
print("Г5 — ЧУВСТВИТЕЛЬНОСТЬ H-1 К СОСТАВУ ПАЧЕК ($0, пере-анализ уже купленных ответов)")
print("=" * 78)
print(" правило решения объявлено в докстринге ДО прогона: вердикт пере-решён только при")
print(" совпадении во ВСЕХ обоснованных сценариях; запас печатается числом\n")
global DROP, RESTORE # состав пачек — параметр сценария, см. их объявление
grid = [
("S0 дерево как есть (что печатает свод)", set(), set()),
("S1 −валидация 11.08 (P13)", {"0dce349331", "0ffee70740"}, set()),
("S2 +69de717f3f назад (база ревизии)", set(), {"69de717f3f"}),
("S3 база ревизии −валидация (счёт P13)", {"0dce349331", "0ffee70740"}, {"69de717f3f"}),
]
keep = (DROP, RESTORE)
rows = []
for name, drop, restore in grid:
DROP, RESTORE = drop, restore
d, _bad = read_family(FAMILIES["claude"])
sd, n = floor_sd(d)
thr = 2.8 * sd / (n ** 0.5) if n > 1 else float("nan")
line = {"name": name, "sd": sd, "n": n, "thr": thr, "d": d}
for a, b, _w in PRIMARY:
m = margins(d, a, b)
mh = margins_half(d, a, b)
line[a] = (len(m), st.mean(m), BO.sign_perm_p(m) if len(m) > 2 else float("nan"),
len(mh), st.mean(mh) if mh else float("nan"),
BO.sign_perm_p(mh) if len(mh) > 2 else float("nan"))
rows.append(line)
DROP, RESTORE = keep
for a, b, what in PRIMARY:
print(f"### КОНТРАСТ {a}/{b}{what}")
print(f" {'сценарий':40s}{'пол n':>6s}{'порог':>7s}{'перевес':>9s}"
f"{'запас':>7s}{'p Холма*':>9s} вердикт")
for r in rows:
mean = r[a][1]
gap = abs(mean) - r["thr"]
adj = holm([r[x][2] if r[x][2] == r[x][2] else 1.0 for x, _y, _z in PRIMARY])
pa = adj[[x for x, _y, _z in PRIMARY].index(a)]
ok = gap > 0 and pa < 0.05
print(f" {r['name']:40s}{r['n']:6d}{r['thr']:7.2f}{mean:+9.2f}{gap:+7.2f}"
f"{pa:9.4f} {'ПЕРЕШЁЛ' if ok else 'ниже порога'}")
print(f" {'—— тот же контраст ВНУТРИ половины (P07)':40s}")
for r in rows:
_k, _m, _p, kh, meanh, ph = r[a]
gap = abs(meanh) - r["thr"]
print(f" {r['name']:40s}{r['n']:6d}{r['thr']:7.2f}{meanh:+9.2f}{gap:+7.2f}"
f"{ph:9.4f} {'ПЕРЕШЁЛ' if gap > 0 and ph < 0.05 else 'ниже порога'} ед.{kh}")
print()
print(" * Холм считается ПО ТРЁМ контрастам внутри своего сценария, как в пре-реге\n")
print("=" * 78)
print("УРОВЕНЬ СУДЕЙСКИХ СЕССИЙ — выбивается ли прогон-валидация из разброса остальных")
print("=" * 78)
lv = _levels(rows[0]["d"])
for half in ("p1", "p2"):
xs = [(j, n, m, t) for (h, j, t), (n, m) in lv.items() if h == half]
if not xs:
continue
vals = [m for _j, _n, m, _t in xs]
print(f" {half}: сессий {len(xs)} · среднее {st.mean(vals):.2f} · "
f"разброс {min(vals):.2f}{max(vals):.2f}"
+ (f" · sd {st.pstdev(vals):.2f}" if len(vals) > 1 else ""))
for j, n, m, t in sorted(xs, key=lambda x: -x[2]):
mark = " ← прогон-валидация 11.08" if "0dce349331" in t else ""
print(f" {j:8s} клеток {n:3d} ошибок/клетку {m:6.2f}{mark}")
print("\n" + "=" * 78)
print("СИСТЕМАТИЧЕСКИЙ СДВИГ НАБОРОВ (P07) — на нём стоит вся формула порога")
print("=" * 78)
d0 = rows[0]["d"]
both = [(v["carry"] - d0[(u, a, "p2")]["carry"])
for (u, a, h), v in d0.items() if h == "p1" and (u, a, "p2") in d0 and a in ALL_ARMS]
fl = [d0[(u, "CTRLfloor", "p1")]["carry"] - d0[(u, "CTRLfloor", "p2")]["carry"]
for u in {k[0] for k in d0}
if (u, "CTRLfloor", "p1") in d0 and (u, "CTRLfloor", "p2") in d0]
print(f" боевые армы, одна единица в обеих половинах: n={len(both)} "
f"сдвиг p1p2 {st.mean(both):+.2f} · sd {st.pstdev(both):.2f}")
print(f" пол (те же половины): n={len(fl)} сдвиг {st.mean(fl):+.2f} · sd {st.pstdev(fl):.2f} "
f"· p={BO.sign_perm_p(fl):.4f}")
print(" ⇒ если сдвиг пола ≈ сдвиг боевых армов, пол меряет РАЗНИЦУ СЕССИЙ, а не шум оценки;")
print(" сбалансированный по половинам контраст этот сдвиг сокращает, несбалансированный — нет")
print("\n БАЛАНС ПОЛОВИН ПО АРМАМ (несбалансированный арм несёт уровень своей сессии):")
for arm in ALL_ARMS:
n1 = sum(1 for (u, a, h) in d0 if a == arm and h == "p1")
n2 = sum(1 for (u, a, h) in d0 if a == arm and h == "p2")
print(f" {arm:5s} p1 {n1:3d} · p2 {n2:3d}" + (" ⚠ перекос" if abs(n1 - n2) > 1 else ""))
# ⚠ КАЛИБРОВКА ПОРОГА ПО ЗНАКУ (находка ревизии №6: R73 против P40). Две выжившие находки
# называли порог смещённым в ПРОТИВОПОЛОЖНЫЕ стороны, и ни одна не проверялась на ЭТОЙ оси:
# R73 мерила на проходе `border` (контраст в 1.40× шумнее пола), P40 рассуждала. Здесь то же
# меряется прямо: шум САМОГО контраста той же структуры — перевес, посчитанный в p1, против
# перевеса в p2. Если он равен полу, порог откалиброван; больше — вердикты смелее данных;
# меньше — прибор строже, чем нужно.
sd0, n0 = floor_sd(d0)
print("\n" + "=" * 78)
print("КАЛИБРОВКА ПОРОГА ПО ЗНАКУ — пол против ШУМА САМОГО КОНТРАСТА (ревизия №6)")
print("=" * 78)
print(f" пол, из которого строится порог: n={n0} sd={sd0:.4f} → порог {2.8 * sd0 / n0**0.5:.4f}")
for a, b, _w in PRIMARY:
xs = []
for u in {k[0] for k in d0}:
if all((u, x, h) in d0 for x in (a, b) for h in ("p1", "p2")):
xs.append((d0[(u, b, "p1")]["carry"] - d0[(u, a, "p1")]["carry"])
- (d0[(u, b, "p2")]["carry"] - d0[(u, a, "p2")]["carry"]))
if len(xs) < 3:
continue
sd = st.pstdev(xs)
own = 2.8 * sd / len(xs) ** 0.5
m = margins(d0, a, b)
print(f" {a}/{b}: n={len(xs):2d} sd={sd:.4f} ({sd / sd0:.2f}× пола) → СВОЙ порог {own:.4f}"
f" · перевес {st.mean(m):+.4f} · запас {abs(st.mean(m)) - own:+.4f}")
print(" ⚠ «свой порог» — не замена объявленному: пре-рег зафризил формулу ПО ПОЛУ, и менять её")
print(" после взгляда на вердикты нельзя. Это проверка КАЛИБРОВКИ, а не второе решающее правило.")
# ⚠ Вторая сторона той же калибровки: пол строится на ДВУХ ГЕНЕРАЦИЯХ, то есть несёт и шум
# порождения, и шум судьи. Чистый шум судьи виден там, где текст ОДИН И ТОТ ЖЕ, а сессии
# разные — на боевых армах, которые лежат в обеих половинах. Если он БОЛЬШЕ пола, порог занижен.
print("\n ЧИСТЫЙ ШУМ СУДЬИ (ОДИН И ТОТ ЖЕ текст, две сессии) против пола:")
for arm in ALL_ARMS:
xs = [d0[(u, arm, "p1")]["carry"] - d0[(u, arm, "p2")]["carry"] for u in {k[0] for k in d0}
if (u, arm, "p1") in d0 and (u, arm, "p2") in d0]
if len(xs) > 2:
print(f" {arm:5s} n={len(xs):2d} среднее {st.mean(xs):+.2f} sd={st.pstdev(xs):.4f}"
f" ({st.pstdev(xs) / sd0:.2f}× пола)")
return 0
def main() -> int:
@ -350,4 +598,4 @@ def main() -> int:
if __name__ == "__main__":
sys.exit(main())
sys.exit(sens() if "--sens" in sys.argv else main())

505
eval/dovodka/ja6.py Normal file
View file

@ -0,0 +1,505 @@
#!/usr/bin/env python3
"""ЯПОНСКАЯ НОГА H-4 — слепой проход `J0` против `J6`. $0 (агент-сессии + харнесс владельца).
Гипотеза H-4: перевес `deepseek-v4-pro` в редакторской роли есть свойство пары zhru, и на другой
паре ПОРЯДОК ЖИЛЬЦОВ может смениться. Порядок нельзя увидеть, имея на паре одного редактора: ось
Д6 меряла «покупает ли редактор что-нибудь вообще», а не «какой редактор лучше». На zh панель есть
(эксп-22), на en второй редактор куплен (`E6`), на ja его не было требование заказа (:115)
печатать сравнение порядка между zh/en/ja поэтому не исполнялось.
СТАТУС РЕЗУЛЬТАТА ОПИСАТЕЛЬНЫЙ, и это не смягчается никаким исходом: ось объявлена
разведочной ДО денег (`power.FORCED_DESCRIPTIVE`, n=9, MDE 2.90 против цели 2.00).
ЧЕМУ НАУЧИЛ `tier` И ЧТО ЗДЕСЬ СДЕЛАНО ИНАЧЕ (пре-регистрируется ДО первого ответа):
1. ПОЛОВИНА ПОЛА НЕ ЯВЛЯЕТСЯ БОЕВЫМ АРМОМ. В паке 23 `CTRLfloorA` побайтно равнялся арму `T1`
в 16/16 контроль сравнивал `T1` сам с собой. Здесь пара пола две генерации БОЕВОГО
РЕДАКТОРА (`J0` и `JFLO`), и они РАЗВЕДЕНЫ ПО НАБОРАМ: `J0` живёт в наборе p1, `JFLO` в p2.
Судья никогда не видит обе половины в одной пачке, а порог несёт межсессионную компоненту.
2. ПОЛ СНЯТ С ТОЙ ОПЕРАЦИИ, КОТОРАЯ В ПАНЕЛИ. Пол оси Д6 снят с точечного фиксера операции
почти детерминированной (4 побайтных близнеца из 8 пар). Панель здесь панель РЕДАКТОРОВ,
поэтому и шум мерится повторной генерацией редактора.
3. ДОНОР ДЕКОЯ УРАВНЕН: в паке 23 донором во всех 16 единицах был `R0`, то есть чувствительность
сертифицировалась в окрестности одного арма. Здесь донор чередуется `J0`/`J6` по чётности.
4. МАРЖЕВЫЙ ДЕКОЙ ЕСТЬ (норма П-2) без него «не различимо» неотличимо от слепоты прибора.
5а. ОГРАНИЧЕНИЕ ГЕЙТА ЧУВСТВИТЕЛЬНОСТИ ОБЪЯВЛЯЕТСЯ ДО ПРОГОНА: маржевый декой садится лишь на
5 единиц из 9 регексы посадок не находят на этой книге нужных мест. Расширенный список
(`--wide-plants`, который на другой японской выборке давал 8/9) проверен и НЕ помогает:
те же 5 из 9. Значит гейт П-2 на этой ноге снят пятью точками, и право говорить «не хуже»
он подпирает слабее, чем на осях с полным покрытием. Сказано ДО ответов, а не после.
5. МОЛЧАЛИВЫЙ НОЛЬ ЗАПРЕЩЁН заданием, а рубрика несёт классы, которые пачка `tier` пропускала.
6. КЛЮЧ СВОЙ, соль своя. Ключи осей Д6 и `tier` не трогаются: раскладка меток есть функция
соли, uid и НАБОРА армов, и эмиссия поверх переписала бы отсуженную раскладку.
ПАРИТЕТ ОБВЯЗОК (норма П-4): оба семейства получают ПОБАЙТНО ОДНО задание и одни правила счёта.
Расхождение прохода `tier` в 12 раз частично куплено тем, что харнессу Sol дали правило плотности
(«две одинаковые ошибки это два»), которого агенты claude не получали. Здесь это правило стоит
в САМОМ задании, то есть достаётся обоим.
Запуск: ja6.py --emit | --score | --sol | --score-sol | --selftest
"""
from __future__ import annotations
import collections
import hashlib
import importlib.util
import json
import re
import statistics as st
import sys
from pathlib import Path
REPO = Path("/home/ubuntu/projects/textmachine")
ZONE = Path(__file__).resolve().parent
for d in ("dovodka", "editor_tier", "tenant_panel", "role_topology", "bank_arbitration"):
sys.path.insert(0, str(REPO / "eval" / d))
OUT = Path.home() / "books" / "dovodka"
WORK = Path.home() / "books" / "judging" / "ja6"
KEYD = OUT / "blind-keys-ja6"
AX = ("ВЕРНОСТЬ", "ТЕРМИН", "ЯЗЫК", "ФОРМА")
CARRY = ("ВЕРНОСТЬ", "ЯЗЫК")
SALT = "ja6-2026-08-15"
PER_SESSION = 4
SETS = ("p1", "p2")
NL = chr(10) # перевод строки как имя: файл собирается генератором внутри f-строк
_axre = re.compile(r"^[ТT](\d+)-(ВЕРНОСТЬ|ТЕРМИН|ЯЗЫК|ФОРМА):\s*(\d+)", re.MULTILINE)
_whyre = re.compile(r"^[ТT](\d+)-ПОЧЕМУ:(.*)$", re.MULTILINE)
def _load(name: str, path: Path):
spec = importlib.util.spec_from_file_location(name, path)
mod = importlib.util.module_from_spec(spec)
sys.modules[name] = mod
spec.loader.exec_module(mod)
return mod
def _sud():
return sys.modules.get("sud") or _load("sud", ZONE / "sud.py")
def cell(tag: str) -> str:
f = OUT / f"{tag}.json"
if not f.exists():
return ""
d = json.loads(f.read_text(encoding="utf-8"))
return (d.get("content") or "") if d.get("finish") == "stop" else ""
def units() -> list[dict]:
MJ = sys.modules.get("mat_ja6") or _load("mat_ja6", ZONE / "material_ja.py")
return MJ.units()
def panel(u: dict, half: str, idx: int) -> dict[str, str]:
"""Армы набора. `J0` только в p1, `JFLO` только в p2 — половины пола не встречаются в пачке."""
S = _sud()
uid = u["uid"]
j0, j6, d0 = cell(f"dv-c-j0-{uid}"), cell(f"dv-h-j6-{uid}"), cell(f"dv-c-d0-{uid}")
jflo = cell(f"dv-h-jflo-{uid}")
base = j0 if half == "p1" else jflo
if not (base.strip() and j6.strip() and d0.strip()):
return {}
donor = j0 if idx % 2 == 0 else j6 # донор декоя уравнен по армам
out = {("J0" if half == "p1" else "JFLO"): base, "J6": j6, "D0": d0}
dec, n_d = S.AJ._plant(donor) if hasattr(S, "AJ") else (None, 0)
if dec and n_d >= 2:
out["CTRLdecoy"] = dec
mg, n_m = S.margin_plant(donor)
if n_m >= 2:
out["CTRLmargin"] = mg
return out
def layout(uid: str, half: str, arms) -> dict[str, str]:
order = sorted(arms)
seed = hashlib.sha256(f"{SALT}|{uid}|{half}|{'|'.join(order)}".encode()).hexdigest()
rank = sorted(order, key=lambda a: hashlib.sha256(f"{seed}|{a}".encode()).hexdigest())
return {f"Т{i}": a for i, a in enumerate(rank, 1)}
HEAD = """Ты — литературный редактор-эксперт. Тебе дан ИСХОДНИК на японском и {n} независимых русских перевода этого же фрагмента под слепыми метками.
Посчитай ОШИБКИ в каждом варианте по типам:
ВЕРНОСТЬ сказано не то, что в исходнике: искажение, инверсия, выдумка, пропуск факта.
Сюда же входят, и их пропускают чаще всего:
· инверсия адресата или участника реплики (кто кому говорит, кто кого бьёт);
· потеря или подмена разряда, ранга, числа, единицы измерения;
· потеря вежливостного или social-регистра там, где он несёт смысл;
· состояние действия: совершилось против ещё не совершилось;
· снятое или, наоборот, добавленное отрицание.
ТЕРМИН имя или термин передан не так, как в других местах того же текста.
ЯЗЫК то, чего носитель не напишет: калька, канцелярит, несуществующее слово, кривой
порядок слов, рассогласование.
ФОРМА вёрстка и пунктуация: оформление реплик, кавычки, тире, абзацы.
Оцени КАЖДЫЙ вариант ОТДЕЛЬНО против исходника не сравнивая варианты между
собой. Порядок вариантов ничего не значит и специально перемешан.
Числа это СЧЁТ ошибок, а не оценка по шкале. Две одинаковые ошибки в одном варианте это два.
НОЛЬ ЭТО УТВЕРЖДЕНИЕ, А НЕ ОТСУТСТВИЕ ОТВЕТА. Ноль означает «я сверил этот вариант с
исходником по этой оси и дефектов не нашёл». Поэтому ПОЧЕМУ обязательно для КАЖДОЙ метки, в том
числе при нулях: если по всем осям ноль, напиши «прочитано, дефектов не найдено». Пустое ПОЧЕМУ
не принимается и на диск не попадает.
Ответ ЗАПИШИ ФАЙЛОМ в {path} ровно в таком виде и ничем больше, блок на каждую метку:
{skeleton}
ПОЧЕМУ для НЕНУЛЕВОЙ оси: короткая ЦИТАТА из текста через « | ».
Число без цитаты не принимается и на диск не попадает.
"""
def emit() -> None:
key: dict[str, dict] = {"_проход": "ja6", "_семейство": ["J6/J0"],
"онтроль": ["J0/D0"], "_пол": ["J0(p1) против JFLO(p2)"]}
made = 0
for half in SETS:
(WORK / f"{half}-tasks").mkdir(parents=True, exist_ok=True)
(WORK / f"{half}-answers").mkdir(parents=True, exist_ok=True)
for idx, u in enumerate(units()):
for half in SETS:
texts = panel(u, half, idx)
if not texts:
print(f"{u['uid']}/{half}: панель неполна, пропущено")
continue
lay = layout(u["uid"], half, tuple(texts))
tok = hashlib.sha256(f"{SALT}|{u['uid']}|{half}".encode()).hexdigest()[:10]
skel = "\n\n".join("\n".join(f"{lab}-{a}: <число>" for a in AX)
+ f"\n{lab}-ПОЧЕМУ: <цитаты>" for lab in lay)
body = [HEAD.format(n=len(lay),
path=WORK / f"{half}-answers" / f"{tok}.txt", skeleton=skel),
"", "=" * 60, "ИСХОДНИК:", u["source"]]
for lab, arm in lay.items():
body += ["", "=" * 60, f"{lab}:", texts[arm]]
(WORK / f"{half}-tasks" / f"{tok}.txt").write_text("\n".join(body), encoding="utf-8")
key[tok] = {lab: {"arm": a, "uid": u["uid"], "half": half,
"kind": "контроль" if a.startswith("CTRL") else "боевой",
"sig": hashlib.sha256(texts[a].encode()).hexdigest()[:12]}
for lab, a in lay.items()}
made += 1
KEYD.mkdir(parents=True, exist_ok=True)
(KEYD / "ja6-KEY.json").write_text(json.dumps(key, ensure_ascii=False, indent=1),
encoding="utf-8")
(KEYD / "SALT-ja6.txt").write_text(SALT + "\n", encoding="utf-8")
toks = [t for t in key if not t.startswith("_")]
print(f"заданий {made}{WORK}/{{p1,p2}}-tasks")
print(f"ключ → {KEYD} (судье НЕ давать)")
for i in range(0, len(toks), PER_SESSION):
print(f" сессия {i // PER_SESSION + 1}: " + " · ".join(toks[i:i + PER_SESSION]))
def read(root: Path) -> tuple[dict, list]:
key = json.loads((KEYD / "ja6-KEY.json").read_text(encoding="utf-8"))
out, bad = {}, []
# ⚠ Две раскладки ответов, и обе законны: своё семейство пишет в `p1-answers`/`p2-answers`
# (наборы разведены каталогами), внешний харнесс — в ПЛОСКИЙ `answers/`, потому что его
# разводит не каталог, а оркестраторский промт «по одному агенту на промт». Счётчик обязан
# читать обе: первая редакция знала только про первую и на пакете Sol печатала «ответов нет».
dirs = [root / f"{half}-answers" for half in SETS]
if (root / "answers").exists():
dirs.append(root / "answers")
for d in dirs:
if not d.exists():
continue
for f in sorted(d.glob("*.txt")):
km = key.get(f.stem)
if not isinstance(km, dict):
continue
t = f.read_text(encoding="utf-8", errors="replace")
cells: dict[str, dict[str, int]] = collections.defaultdict(dict)
for m in _axre.finditer(t):
cells["Т" + m.group(1)][m.group(2)] = int(m.group(3))
why = {"Т" + m.group(1): m.group(2).strip() for m in _whyre.finditer(t)}
for lab, ax in cells.items():
meta = km.get(lab)
if not isinstance(meta, dict):
continue
if len(ax) < len(AX):
bad.append(f"{f.stem}/{lab}: не все четыре оси")
continue
if not why.get(lab):
bad.append(f"{f.stem}/{lab}: ПУСТОЕ обоснование (запрещено заданием)")
out[(meta["uid"], meta["arm"])] = sum(ax.get(a, 0) for a in CARRY)
return out, bad
def _sign_p(diffs: list[float]) -> float:
import math
nz = [d for d in diffs if d != 0]
n = len(nz)
if not n:
return 1.0
k = sum(1 for d in nz if d > 0)
tail = sum(math.comb(n, i) for i in range(min(k, n - k) + 1)) / 2 ** n
return min(1.0, 2 * tail)
def score(root: Path | None = None) -> int:
root = root or WORK
sc, bad = read(root)
if not sc:
print(f"ответов в {root} нет")
return 1
uids = sorted({u for u, _ in sc})
print(f"единиц {len(uids)} · клеток {len(sc)} · замечаний годности {len(bad)}")
for b in bad[:8]:
print("", b)
vals = [v for (u, a), v in sc.items() if not a.startswith("CTRL")]
zeros = sum(1 for v in vals if v == 0) / max(len(vals), 1)
print(f"\nГЕЙТ ПЛОТНОСТИ: ошибок/клетку {st.mean(vals):.2f} · доля нулей {zeros:.0%} "
f"{'ГОДНО' if zeros < 0.25 else '⛔ ПОЛ ШКАЛЫ'}")
fl = [sc[(u, "J0")] - sc[(u, "JFLO")] for u in uids if (u, "J0") in sc and (u, "JFLO") in sc]
sd = st.pstdev(fl) if len(fl) > 1 else 0.0
thr = 2.8 * sd / max(len(fl), 1) ** 0.5 if fl else 0.0
print(f"СВОЙ ПОЛ (две генерации редактора, половины в РАЗНЫХ наборах): пар {len(fl)} · "
f"sd {sd:.2f} → ПОРОГ {thr:.2f}")
for name, arm in (("ГРУБЫЙ ДЕКОЙ", "CTRLdecoy"), ("МАРЖЕВЫЙ ДЕКОЙ (гейт П-2)", "CTRLmargin")):
d = [sc[(u, arm)] - sc[(u, "J0")] for u in uids if (u, arm) in sc and (u, "J0") in sc]
if d:
v = "ПРОЙДЕН" if st.mean(d) > thr else "НЕ ПРОЙДЕН"
print(f"{name}: n={len(d)} среднее {st.mean(d):+.2f} против порога {thr:.2f}{v}")
print(f"\n{'контраст':14s}{'ед.':>5s}{'перевес':>9s}{'p':>9s} вердикт")
for a, b in (("J6", "J0"), ("J0", "D0")):
d = [sc[(u, b)] - sc[(u, a)] for u in uids if (u, a) in sc and (u, b) in sc]
if not d:
continue
m = st.mean(d)
v = "ПЕРЕШЁЛ ПОРОГ (описательно)" if abs(m) > thr else "ниже порога"
print(f"{a + ' vs ' + b:14s}{len(d):5d}{m:+9.2f}{_sign_p(d):9.4f} {v}")
print(f"\n{'арм':10s}{'ошибок/ед.':>11s}")
for arm in ("J0", "J6", "D0", "JFLO", "CTRLdecoy", "CTRLmargin"):
v = [sc[(u, arm)] for u in uids if (u, arm) in sc]
if v:
print(f"{arm:10s}{st.mean(v):11.2f}")
print("\n⚠ Ось РАЗВЕДОЧНАЯ: вывод описательный при любом исходе (n=9, MDE 2.90 против цели 2.00).")
return 0
ORCH = """# Sol-оркестратору: судейство ЯПОНСКОЙ НОГИ H-4 (проход `ja6`)
Ты раскладываешь готовые судейские задания по своим агентам. **Ничего не готовишь сам и ничего
не считаешь** всё уже собрано, задания и промты лежат файлами.
## Что сделать
В каталоге `{prompts}` лежат **{k} промтов сессий**:
{names}
Запусти **по одному агенту на каждый промт**, {k} агентов. Каждому дай ровно одну инструкцию:
> Прочитай файл `{prompts}/<имя>.md` и выполни ровно то, что в нём написано, ничего сверх.
Промт внутри содержит список назначенных заданий, правила оценки и путь, куда писать ответ.
Больше агенту знать ничего не нужно.
## Жёсткие требования (нарушение обесценивает замер)
1. **Один агент один промт.** Не объединяй сессии и не дроби их. Состав сессии часть прибора.
2. **Наборы `p1` и `p2` РАЗНЫМ агентам, и это здесь важнее всего.** Это две половины
шумового пола: в `p1` лежит одна генерация боевого редактора, в `p2` вторая генерация ТОГО ЖЕ
редактора. Из разницы их оценок и строится порог различимости всего прохода. Судья, увидевший
обе половины, порог обесценивает целиком в паке 23 такой дефект занизил его на 19%, а на
проходе `tier` половина пола вообще совпала с боевым армом, и контроль оказался пуст.
**Если все задания уйдут одному агенту, замер пропадает, и пере-снять его будет нечем.**
3. **Агент открывает ТОЛЬКО перечисленные в его промте задания и пишет ТОЛЬКО по путям, указанным
ВНУТРИ заданий.** Ему нельзя искать что-либо ещё на диске ни разметку, ни сырьё, ни код, ни
задания других сессий; нельзя делать сетевые вызовы; нельзя сравнивать варианты между собой,
ранжировать их и применять `diff`/`difflib`/`cmp`. Ключ соответствия меток и армов лежит ВНЕ
рабочего каталога и агенту не выдаётся намеренно. Заглянет сессия аннулируется целиком.
4. **Не правь промты и задания.** Если что-то выглядит ошибкой скажи владельцу, но не исправляй:
обвязка сверена гейтом паритета с обвязкой второго семейства, и правка на твоей стороне делает
семейства несравнимыми.
5. **Не досуживай и не переоценивай.** Не справился с частью так и скажи, сколько выполнено.
Частичный возврат честнее выдуманного и засчитывается.
6. **Агент НЕ запускает своих агентов.** Сессия это ОДИН агент, читающий свои задания. Дочерние
агенты ломают состав сессии, зарегистрированный до запуска; на прошлом прогоне это уже
случилось, и сессию пришлось аннулировать целиком.
## Что это за замер и почему спрашивают вас
Гипотеза владельца H-4: перевес `deepseek-v4-pro` в роли редактора свойство пары zhru, и на
другой паре порядок редакторов может смениться. На японском до этой недели был куплен один
редактор, сравнивать было не с чем; теперь их два. Вопрос ровно один: **меняется ли порядок**.
Ваше семейство здесь полноправно: ни один арм панели не из OpenAI (`deepseek-v4-pro`, `glm-5`,
`deepseek-v4-flash`). На соседнем проходе `tier` конфликт был там судился `gpt-5.6-terra`, то
есть выход вашей же семьи, и показания пришлось снять именно поэтому.
Первое семейство (агенты Claude) эту панель уже отсудило, все контроли зелёные. Вы второе
мнение о ЕГО судействе и его слепоте, а не первый свидетель.
## Как считается результат
eval/.venv/bin/python eval/dovodka/ja6.py --score-sol
Тот же счётчик, которым посчитано первое семейство: сам отбракует ответы без цитат, проверит
пойманный декой и напечатает число принятых единиц.
## Что этот замер может и чего не может — сказано ДО прогона
Ось объявлена РАЗВЕДОЧНОЙ до денег: 9 единиц, MDE 2.90 против цели 2.00. Несущего вывода она не
даст ни при каком исходе. Маржевый декой (контроль чувствительности) сажается лишь на 5 единиц из
9 на этой книге регексы посадок находят мало мест, расширенный список проверен и не помогает.
Значит право сказать «редакторы равны» подпёрто здесь слабее, чем на других осях.
"""
SESSION = """<!-- ОБВЯЗКА СЕМЕЙСТВА SOL. Отличается от обвязки другого семейства ТОЛЬКО каталогом
заданий и ответов. Паритет обвязок норма Д0.13 П-4: в паке 23 у харнесса Sol в промте были
правила плотности счёта, которых не было у Claude, и часть расхождения семейств в разы могла
быть куплена ИНСТРУКЦИЕЙ, а не моделью. -->
Ты литературный редактор-эксперт. Твоя работа оценить качество русских художественных
переводов С ЯПОНСКОГО по заданиям, которые лежат готовыми файлами.
Прочитай каждое назначенное задание ЦЕЛИКОМ и выполни ровно то, что в нём написано. В каждом
задании есть исходник на японском, несколько русских переводов этого же фрагмента под слепыми
метками и точная форма ответа. Ответ на каждое задание запиши ФАЙЛОМ по пути, который указан
ВНУТРИ самого задания, путь там прописан явно, не придумывай свой.
Работай по заданиям последовательно: прочитал задание записал ответ перешёл к следующему.
## Правила, которые важнее скорости
* **Оценивай каждый вариант ОТДЕЛЬНО против исходника.** Не сравнивай варианты между собой, не
ранжируй их и не подстраивай оценку одного под оценку другого. Порядок меток перемешан намеренно.
* **Каждая ненулевая оценка требует обоснования ЦИТАТОЙ** из соответствующего варианта, в формате,
заданном в задании. Оценка без цитаты не принимается, и единица целиком выбрасывается из замера.
* **Числа это СЧЁТ ошибок, а не оценка по шкале.** Две одинаковые ошибки в одном варианте два.
* **НОЛЬ УТВЕРЖДЕНИЕ, А НЕ МОЛЧАНИЕ.** Ставя ноль, ты заявляешь «я сверил и не нашёл». Поле
ПОЧЕМУ обязательно для КАЖДОЙ метки, включая нулевые: при всех нулях так и пиши «прочитано,
дефектов не найдено». Пустое ПОЧЕМУ разбор отбрасывает. Прошлый проход развалился именно на
молчаливых нулях: 38% клеток были нулями, треть из них без единого слова, и вердикт оказался не
«варианты равны», а «прибор ничего не показывал».
* Читай ВНИМАТЕЛЬНО и сверяй смысл с японским исходником. Смысловые искажения инверсия, подмена
адресата реплики, потерянное отрицание, изменённое число, потерянный вежливостный регистр,
выдуманный факт незаметны при беглом чтении, потому что русский текст остаётся грамотным и
связным. Именно их и надо ловить.
* Не жалей вариант и не ищи, за что его похвалить: твоя задача СЧЁТ ОШИБОК, а не рецензия.
## Границы
* Работай ТОЛЬКО с назначенными файлами заданий и пиши ТОЛЬКО по путям, указанным внутри них.
* **Не ищи и не открывай ничего другого на диске.** В частности: не выясняй, откуда взялся вариант,
какой моделью он сделан и что означают метки. Эта информация к задаче отношения не имеет, а её
поиск делает оценку недействительной.
* Не применяй `diff`, `difflib`, `cmp` и любое механическое сличение вариантов между собой.
* **Не запускай своих агентов.** Эта сессия ты один.
* Не переводи сам и не предлагай свою версию. Не оценивай «литературность в целом» только оси,
названные в задании.
* Книга новая (первая публикация 30.07.2026), в претрейне её нет: не ищи её в сети и не опирайся
на память. Единственный источник истины исходник в задании.
* Ничего не покупай, никаких сетевых вызовов. Файлы с ключами и секретами не читать.
ЕСЛИ ЧЕГО-ТО НЕ ЗНАЕШЬ: считай ошибкой только то, что видно из исходника. Сомневаешься не
засчитывай. Заниженный счёт честнее выдуманного.
## ТВОИ ЗАДАНИЯ ({n})
{files}
"""
def sol() -> None:
"""Пакет для внешнего харнесса — ТРЁХУРОВНЕВЫЙ, как пакеты фазы Д, а не двухдокументный.
ПОЧЕМУ ИМЕННО ТАК, И ЧЕМ ЭТО ОПЛАЧЕНО. Первая редакция этого пакета копировала структуру
эксп-23 (`editor-tier/sol-tier/`: ИНСТРУКЦИЯ + ПРОМТ-ДЛЯ-ХАРНЕССА) и просила «работай по 4
задания за сессию» ПРОЗОЙ. Владелец указал, что его харнесс так не работает: получив задание,
Sol идёт и читает всё сам, одной сессией. Для этого прохода такая раскладка убийственна
половины шумового пола лежат в РАЗНЫХ наборах (`J0` в p1, `JFLO` в p2), и один судья,
увидевший обе, обесценивает порог целиком. Пакеты фазы Д (`sol-d3-work`, `sol-d4-work`,
`sol-d6-work`) решают это слоем ОРКЕСТРАТОРА: `ORCHESTRATOR.md` велит запустить по агенту на
промт, а `prompts/*.md` жёстко разводят наборы. Здесь воспроизводится тот же слой.
Прочее, учтённое из прошлых пакетов, каждое оплаченная ошибка:
· путь к ключу НЕ НАЗЫВАЕТСЯ (пак 23 написал «не открывай blind-keys/» это показать пальцем);
· путь записи ведёт в СВОЙ каталог (пак 23 велел писать в боевой, поверх отсуженного);
· правила счёта стоят В САМОМ ЗАДАНИИ и в промте сессии паритет П-4;
· запрет на дочерних агентов вписан явно (на прошлом прогоне сессия их породила);
· все армы единицы в одной пачке (урок эксп-22 §16).
"""
src, dst = WORK, Path.home() / "books" / "judging" / "ja6-sol"
(dst / "tasks").mkdir(parents=True, exist_ok=True)
(dst / "answers").mkdir(parents=True, exist_ok=True)
(dst / "prompts").mkdir(parents=True, exist_ok=True)
by_half: dict[str, list[str]] = {}
for half in SETS:
d = src / f"{half}-tasks"
if not d.exists():
continue
for f in sorted(d.glob("*.txt")):
body = f.read_text(encoding="utf-8")
body = body.replace(str(src / f"{half}-answers" / f.name),
str(dst / "answers" / f.name))
(dst / "tasks" / f.name).write_text(body, encoding="utf-8")
by_half.setdefault(half, []).append(f.name)
if not by_half:
print("заданий нет — сначала --emit")
return
names = []
for half, toks in sorted(by_half.items()):
for i in range(0, len(toks), PER_SESSION):
grp = toks[i:i + PER_SESSION]
nm = f"{half}-session-{i // PER_SESSION + 1:02d}"
files = NL.join(f"* `{dst / 'tasks' / x}`" for x in grp)
(dst / "prompts" / f"{nm}.md").write_text(
SESSION.format(n=len(grp), files=files), encoding="utf-8")
names.append(nm)
(dst / "ORCHESTRATOR.md").write_text(
ORCH.format(prompts=dst / "prompts", k=len(names),
names=NL.join(f"* `{n}.md`" for n in names)), encoding="utf-8")
print(f"пакет Sol → {dst}")
print(f" заданий {sum(len(v) for v in by_half.values())} · промтов сессий {len(names)}")
print(f" ОТДАТЬ ВЛАДЕЛЬЦУ: {dst / 'ORCHESTRATOR.md'}")
print(" ⚠ p1 и p2 — РАЗНЫМ агентам: это две половины шумового пола")
def selftest() -> int:
fails = []
ok = lambda c, m: fails.append(m) if not c else None
us = units()
ok(len(us) == 9, f"единиц {len(us)}, ожидалось 9")
for half in SETS:
p = panel(us[0], half, 0)
ok(bool(p), f"{half}: панель пуста")
if p:
sigs = [hashlib.sha256(v.encode()).hexdigest() for v in p.values()]
ok(len(sigs) == len(set(sigs)), f"{half}: в панели побайтные дубли")
ok(("J0" in p) == (half == "p1"), f"{half}: J0 стоит не в своём наборе")
ok(("JFLO" in p) == (half == "p2"), f"{half}: JFLO стоит не в своём наборе")
lay = layout(us[0]["uid"], half, tuple(p))
ok(len(lay) == len(set(lay.values())), "раскладка не биективна")
ok(layout(us[0]["uid"], half, tuple(sorted(lay.values()))) == lay,
"раскладка не воспроизводима")
d = [panel(u, "p1", i).get("CTRLdecoy", "") for i, u in enumerate(us)]
ok(sum(1 for x in d if x) >= 7, f"грубый декой сажается лишь на {sum(1 for x in d if x)} из 9")
ok(_sign_p([1, 1, 1, 1, 1]) < 0.07, "знаковый тест сломан")
for m in fails:
print("ПРОВАЛ:", m)
print(f"селфтест ja6: провалов {len(fails)}")
return 1 if fails else 0
if __name__ == "__main__":
a = sys.argv[1:] or ["--selftest"]
if a[0] == "--emit":
emit()
elif a[0] == "--score":
sys.exit(score())
elif a[0] == "--selftest":
sys.exit(selftest())
elif a[0] == "--sol":
sol()
elif a[0] == "--score-sol":
sys.exit(score(Path.home() / "books" / "judging" / "ja6-sol"))
else:
raise SystemExit(f"⛔ неизвестный режим {a[0]!r}")

View file

@ -7,7 +7,7 @@
переводов по заданиям, которые лежат готовыми файлами.
Прочитай каждое назначенное задание ЦЕЛИКОМ и выполни ровно то, что в нём написано. В каждом
задании есть исходник на китайском, несколько русских переводов этого же фрагмента под слепыми
задании есть исходник на {ЯЗЫКЕ}, несколько русских переводов этого же фрагмента под слепыми
метками и точная форма ответа. Ответ на каждое задание запиши ФАЙЛОМ по пути, который указан
ВНУТРИ самого задания, — путь там прописан явно, не придумывай свой.
@ -24,11 +24,19 @@
* **Ноль ставится только тогда, когда ошибок этого типа действительно нет.** Ноль — это
утверждение, а не способ пропустить вариант. Если ты не нашёл ошибок, значит их нет; убедись,
что читал внимательно.
* Читай текст ВНИМАТЕЛЬНО и сверяй смысл с китайским исходником. Смысловые искажения — инверсия,
* Читай текст ВНИМАТЕЛЬНО и сверяй смысл с {ЯЗЫКОМ} исходником. Смысловые искажения — инверсия,
подмена адресата реплики, потерянное отрицание, изменённое число, выдуманный факт — бывают
незаметны при беглом чтении, потому что русский текст остаётся грамотным и связным. Именно их
и надо ловить.
* **ГЛАВНОЕ ПРАВИЛО ЭТОГО ЗАМЕРА (решение владельца продукта 16.08):** переврать смысл
исходника — ошибка; обойтись с его буквой вольно — нет. Живой русский язык здесь приоритет,
поэтому перестановка предложений, слияние и дробление абзацев, замена оборота на более живой
русский, синонимическая замена, добавленная связка и снятая буквальность исходного языка
ошибками НЕ СЧИТАЮТСЯ. Точный список того, что ошибкой не является, стоит в шапке САМОГО
задания, и он главнее этой обвязки.
* Не жалей вариант и не ищи, за что его похвалить: твоя задача — СЧЁТ ОШИБОК, а не рецензия.
Но и не штрафуй вариант за то, что он просто нравится тебе меньше соседнего: разница вкуса
ошибкой не является.
## Границы
@ -45,5 +53,7 @@
{СПИСОК ФАЙЛОВ}
Каталог заданий: `~/sud-d4/p1-tasks/` и `~/sud-d4/p2-tasks/`.
Отвечать по путям, указанным внутри заданий (`~/sud-d4/p1-answers/`, `~/sud-d4/p2-answers/`).
Каталог твоих заданий: `~/{КАТАЛОГ}/{НАБОР}-tasks/`. Другие наборы этого прогона тебе
НЕ назначены и открывать их нельзя: наборы — половины парного контроля, и судья,
увидевший обе, обесценивает контроль целиком.
Отвечать по путям, указанным внутри заданий (`~/{КАТАЛОГ}/{НАБОР}-answers/`).

View file

@ -2,7 +2,7 @@
переводов по заданиям, которые лежат готовыми файлами.
Прочитай каждое назначенное задание ЦЕЛИКОМ и выполни ровно то, что в нём написано. В каждом
задании есть исходник на китайском, несколько русских переводов этого же фрагмента под слепыми
задании есть исходник на {ЯЗЫКЕ}, несколько русских переводов этого же фрагмента под слепыми
метками и точная форма ответа. Ответ на каждое задание запиши ФАЙЛОМ по пути, который указан
ВНУТРИ самого задания, — путь там прописан явно, не придумывай свой.
@ -19,11 +19,19 @@
* **Ноль ставится только тогда, когда ошибок этого типа действительно нет.** Ноль — это
утверждение, а не способ пропустить вариант. Если ты не нашёл ошибок, значит их нет; убедись,
что читал внимательно.
* Читай текст ВНИМАТЕЛЬНО и сверяй смысл с китайским исходником. Смысловые искажения — инверсия,
* Читай текст ВНИМАТЕЛЬНО и сверяй смысл с {ЯЗЫКОМ} исходником. Смысловые искажения — инверсия,
подмена адресата реплики, потерянное отрицание, изменённое число, выдуманный факт — бывают
незаметны при беглом чтении, потому что русский текст остаётся грамотным и связным. Именно их
и надо ловить.
* **ГЛАВНОЕ ПРАВИЛО ЭТОГО ЗАМЕРА (решение владельца продукта 16.08):** переврать смысл
исходника — ошибка; обойтись с его буквой вольно — нет. Живой русский язык здесь приоритет,
поэтому перестановка предложений, слияние и дробление абзацев, замена оборота на более живой
русский, синонимическая замена, добавленная связка и снятая буквальность исходного языка
ошибками НЕ СЧИТАЮТСЯ. Точный список того, что ошибкой не является, стоит в шапке САМОГО
задания, и он главнее этой обвязки.
* Не жалей вариант и не ищи, за что его похвалить: твоя задача — СЧЁТ ОШИБОК, а не рецензия.
Но и не штрафуй вариант за то, что он просто нравится тебе меньше соседнего: разница вкуса
ошибкой не является.
## Границы

View file

@ -7,7 +7,7 @@
переводов по заданиям, которые лежат готовыми файлами.
Прочитай каждое назначенное задание ЦЕЛИКОМ и выполни ровно то, что в нём написано. В каждом
задании есть исходник на китайском, несколько русских переводов этого же фрагмента под слепыми
задании есть исходник на {ЯЗЫКЕ}, несколько русских переводов этого же фрагмента под слепыми
метками и точная форма ответа. Ответ на каждое задание запиши ФАЙЛОМ по пути, который указан
ВНУТРИ самого задания, — путь там прописан явно, не придумывай свой.
@ -24,11 +24,19 @@
* **Ноль ставится только тогда, когда ошибок этого типа действительно нет.** Ноль — это
утверждение, а не способ пропустить вариант. Если ты не нашёл ошибок, значит их нет; убедись,
что читал внимательно.
* Читай текст ВНИМАТЕЛЬНО и сверяй смысл с китайским исходником. Смысловые искажения — инверсия,
* Читай текст ВНИМАТЕЛЬНО и сверяй смысл с {ЯЗЫКОМ} исходником. Смысловые искажения — инверсия,
подмена адресата реплики, потерянное отрицание, изменённое число, выдуманный факт — бывают
незаметны при беглом чтении, потому что русский текст остаётся грамотным и связным. Именно их
и надо ловить.
* **ГЛАВНОЕ ПРАВИЛО ЭТОГО ЗАМЕРА (решение владельца продукта 16.08):** переврать смысл
исходника — ошибка; обойтись с его буквой вольно — нет. Живой русский язык здесь приоритет,
поэтому перестановка предложений, слияние и дробление абзацев, замена оборота на более живой
русский, синонимическая замена, добавленная связка и снятая буквальность исходного языка
ошибками НЕ СЧИТАЮТСЯ. Точный список того, что ошибкой не является, стоит в шапке САМОГО
задания, и он главнее этой обвязки.
* Не жалей вариант и не ищи, за что его похвалить: твоя задача — СЧЁТ ОШИБОК, а не рецензия.
Но и не штрафуй вариант за то, что он просто нравится тебе меньше соседнего: разница вкуса
ошибкой не является.
## Границы
@ -45,5 +53,7 @@
{СПИСОК ФАЙЛОВ}
Каталог заданий: `~/sol-d4-work/p1-tasks/` и `~/sol-d4-work/p2-tasks/`.
Отвечать по путям, указанным внутри заданий (`~/sol-d4-work/p1-answers/`, `~/sol-d4-work/p2-answers/`).
Каталог твоих заданий: `~/{КАТАЛОГ}/{НАБОР}-tasks/`. Другие наборы этого прогона тебе
НЕ назначены и открывать их нельзя: наборы — половины парного контроля, и судья,
увидевший обе, обесценивает контроль целиком.
Отвечать по путям, указанным внутри заданий (`~/{КАТАЛОГ}/{НАБОР}-answers/`).

153
eval/dovodka/naklon.py Normal file
View file

@ -0,0 +1,153 @@
#!/usr/bin/env python3
"""ГЕЙТ ПОЗИЦИОННОГО НАКЛОНА. $0, только чтение сырья.
ЗАЧЕМ ОТДЕЛЬНЫЙ ФАЙЛ. Норма рига требует наклон ЗАМЕРИТЬ, ВЫЧЕСТЬ и СТОРОЖИТЬ ГЕЙТОМ, и отчёт
фазы (23-editor-tier.md:1027) прямо утверждает, что так и делается. Фактически в зоне фазы Д кода
наклона не было ни строки: замер сделан ОДИН раз руками при финальном аудите, уже ПОСЛЕ вердиктов
(девиация объявлена в Д10), сторожа нет. Здесь он есть.
ЧТО МЕРИТСЯ. Наклон это зависимость выставленного счёта от НОМЕРА МЕТКИ в пачке: судья,
устающий к концу задания, ставит поздним меткам больше ошибок. Сам по себе наклон вердикту не
вредит он сокращается в контрасте, ЕСЛИ средние позиции двух армов совпадают. Вредит
ПРОИЗВЕДЕНИЕ наклона на дисбаланс позиций, и именно оно здесь и гейтится.
поправка контраста = наклон × (средняя позиция арма A средняя позиция арма B)
ГЕЙТ: пачка не годна, если поправка хоть к одному первичному контрасту превышает
`MAX_SHARE` от порога различимости этой оси то есть если перестановка меток способна
съесть заметную долю решающей величины.
Запуск: naklon.py [--pass=<проход>] | --selftest
"""
from __future__ import annotations
import collections
import json
import pathlib
import re
import statistics as st
import sys
BOOKS = pathlib.Path.home() / "books"
AXRE = re.compile(r"^[ТT](\d+)-(ВЕРНОСТЬ|ТЕРМИН|ЯЗЫК|ФОРМА):\s*(\d+)", re.MULTILINE)
CARRY = ("ВЕРНОСТЬ", "ЯЗЫК")
MAX_SHARE = 0.25 # доля порога, которую поправке позволено съедать
PASSES = {
"tier": (["editor-tier/aj-tier"], "editor-tier/blind-keys/tier-KEY.json",
[("T1", "R0"), ("T2", "R0"), ("T3", "R0")], 1.02),
"tier2": (["editor-tier/tier2"], "editor-tier/blind-keys/tier2-KEY.json",
[("T1", "R0"), ("T2", "R0"), ("T3", "R0")], 1.84),
"border": (["editor-tier/aj-border"], "editor-tier/blind-keys/border-KEY.json",
[("R2", "R0")], 1.48),
"d4": (["judging/sud-d4/p1-answers", "judging/sud-d4/p2-answers"], "dovodka/blind-keys-d4",
[("A1B", "A0"), ("A3", "A0"), ("A6", "A0")], 1.65),
"d3": (["judging/sud-d3/p1-answers", "judging/sud-d3/p2-answers"], "dovodka/blind-keys-d3",
[("E0", "D0")], 0.90),
"d6": (["judging/sud-d6/p1-answers", "judging/sud-d6/p2-answers"], "dovodka/blind-keys-d6",
[("J0", "D0")], 2.10),
"d1": (["judging/sud-d1/p1-answers", "judging/sud-d1/p2-answers"], "dovodka/blind-keys-d1",
[("R1", "A0")], 1.47),
}
def load_key(rel: str) -> dict:
p = BOOKS / rel
if p.is_dir():
k: dict = {}
for f in sorted(p.glob("*-KEY.json")):
k.update(json.loads(f.read_text(encoding="utf-8")))
return k
return json.loads(p.read_text(encoding="utf-8")) if p.exists() else {}
def collect(rels: list[str], keyrel: str):
"""[(номер метки, арм, счёт)] по всем разобранным клеткам прохода."""
key = load_key(keyrel)
rows = []
for rel in rels:
d = BOOKS / rel
if not d.exists():
continue
for f in sorted(d.glob("*.txt")):
km = key.get(f.stem)
if not isinstance(km, dict):
continue
cells: dict[str, dict[str, int]] = collections.defaultdict(dict)
for m in AXRE.finditer(f.read_text(encoding="utf-8", errors="replace")):
cells["Т" + m.group(1)][m.group(2)] = int(m.group(3))
for lab, ax in cells.items():
meta = km.get(lab)
if isinstance(meta, dict) and meta.get("arm"):
rows.append((int(lab[1:]), meta["arm"], sum(ax.get(a, 0) for a in CARRY)))
return rows
def slope(rows) -> float:
"""Ошибок на ШАГ метки — обычная линейная регрессия счёта на номер метки."""
if len(rows) < 4:
return 0.0
xs = [r[0] for r in rows]
ys = [r[2] for r in rows]
mx, my = st.mean(xs), st.mean(ys)
den = sum((x - mx) ** 2 for x in xs)
return sum((x - mx) * (y - my) for x, y in zip(xs, ys)) / den if den else 0.0
def report(name: str) -> int:
rels, keyrel, fam, thr = PASSES[name]
rows = collect(rels, keyrel)
if not rows:
print(f"{name:8s} — ответов нет")
return 0
s = slope(rows)
pos = collections.defaultdict(list)
for lab, arm, _ in rows:
pos[arm].append(lab)
mean_pos = {a: st.mean(v) for a, v in pos.items()}
print(f"\n=== {name} · точек {len(rows)} · наклон {s:+.4f} ошибки на шаг метки · "
f"порог оси {thr:.2f} ===")
rc = 0
for a, b in fam:
if a not in mean_pos or b not in mean_pos:
continue
dp = mean_pos[a] - mean_pos[b]
corr = s * dp
share = abs(corr) / thr if thr else 0.0
mark = " ⛔ ПРОБОЙ" if share > MAX_SHARE else ""
print(f" {a + '/' + b:12s} позиции {mean_pos[a]:5.2f} против {mean_pos[b]:5.2f} "
f"(разница {dp:+.2f}) → поправка {corr:+.3f} = {share:.0%} порога{mark}")
if share > MAX_SHARE:
rc = 1
return rc
def selftest() -> int:
fails = []
# синтетика с ИЗВЕСТНЫМ наклоном: счёт = 2 × номер метки
rows = [(i, "A" if i % 2 else "B", 2 * i) for i in range(1, 21)]
s = slope(rows)
if abs(s - 2.0) > 1e-9:
fails.append(f"наклон на синтетике {s:.4f}, ожидалось 2.0")
flat = [(i, "A", 5) for i in range(1, 21)]
if abs(slope(flat)) > 1e-9:
fails.append("на плоских данных наклон обязан быть нулём")
if len(collect(*PASSES["d4"][:2])) < 100:
fails.append("проход d4 не разбирается")
for m in fails:
print("ПРОВАЛ:", m)
print(f"селфтест наклона: провалов {len(fails)}")
return 1 if fails else 0
if __name__ == "__main__":
args = sys.argv[1:]
if "--selftest" in args:
sys.exit(selftest())
want = [a.split("=", 1)[1] for a in args if a.startswith("--pass=")] or list(PASSES)
rc = 0
for n in want:
rc |= report(n)
print(f"\n⚠ Гейт роняет проход, если поправка съедает больше {MAX_SHARE:.0%} порога "
f"различимости: столько способна дать одна перестановка меток.")
sys.exit(rc)

View file

@ -58,6 +58,10 @@ MAP = {
# en-файл → (zh-оригинал, комментарий провенанса)
"translator": ("backend/prompts/zh-ru/translator.md", "боевой промпт переводчика"),
"editor": ("backend/prompts/zh-ru/editor.md", "боевой промпт редактора"),
# ⚠ Заведён 14.08. Промт сессии требует МЕХАНИЧЕСКОГО гейта консистентности пар-промтов, а
# терминолог в карте отсутствовал — то есть банк-роль новой пары проходила мимо гейта вовсе.
# Гейт при этом честно печатал ПРОВАЛ «файла нет в MAP» и не угадывал, с чем сравнивать.
"terminologist": ("backend/prompts/zh-ru/terminologist.md", "боевой промпт терминолога"),
"translator-reflow": (None, "боевого zh-аналога НЕТ: это вариант полигона, "
"зеркалом боевого промпта не является"),
}
@ -84,7 +88,29 @@ ALLOWED = {
("ja-ru", "editor", "- Вёрстка: собирай повествование"):
"оговорка про построчный набор СОХРАНЕНА и переформулирована под ja: веб-новелла "
"syosetu набирается построчно так же, как китайская",
# ⚠ Терминолог: пример строки ОТВЕТА — тот же третий законный класс, что у редактора, только
# формат другой (три поля через табуляцию, а не маркер `[narrative]`). Термин примера взят
# НЕ из книги среза (проверено: Thibault ×0, 慎二 ×0), чтобы промт не подсказывал модели
# ответы по реальным кандидатам банка.
("en-ru", "terminologist", "师父"):
"пример строки ответа ЗАМЕНЁН своим английским (`Thibault → Тибо`) — образец обязан быть "
"на исходном языке пары; термин вне книги среза, подсказки ответов нет",
# Обе стороны замены объявляются отдельно: дифф показывает и снятую строку, и вставленную.
("en-ru", "terminologist", "Thibault"):
"вставленная половина той же замены примера (см. запись про 师父)",
("ja-ru", "terminologist", "慎二"):
"вставленная половина той же замены примера (см. запись про 师父)",
("ja-ru", "terminologist", "师父"):
"пример строки ответа ЗАМЕНЁН своим японским (`慎二 → Синдзи`, дзи-слог демонстрирует "
"Поливанова против ромадзи) — образец обязан быть на исходном языке пары",
}
# ⚠ ФАЙЛЫ, У КОТОРЫХ ЗЕРКАЛО ПРОСТО НЕ ИМЕЕТ ПАР-БЛОКА. У боевого zh-терминолога его нет: файл
# написан пар-НЕЙТРАЛЬНО, вся языковая специфика приходит подстановками брифа. Пар-версии блок
# ДОБАВЛЯЮТ, и это расхождение объявлено здесь, а не спрятано ослаблением проверки: у пары блок
# обязан быть по-прежнему, требование снимается ТОЛЬКО с zh-стороны. Завести блок в zh нельзя —
# `backend/` чужая зона.
NO_ZH_PAIRBLOCK = {"terminologist"}
BAD = 0
@ -154,7 +180,11 @@ def compare(name: str, zh_p: Path, en_p: Path, want_pair: str, pair: str) -> Non
zo, zb, zr, zp = blocks(zl)
eo, eb, er, ep = blocks(el)
ck("пар-блок объявлен в обоих", bool(zp) and bool(ep), f"zh «{zp}» · пара «{ep}»")
if name in NO_ZH_PAIRBLOCK:
ck("пар-блок есть у ПАРЫ (у боевого zh его нет — объявлено)", bool(ep),
f"zh «{zp}» · пара «{ep}»")
else:
ck("пар-блок объявлен в обоих", bool(zp) and bool(ep), f"zh «{zp}» · пара «{ep}»")
ck(f"пар-блок называет свою пару ({want_pair})", ep == want_pair, ep or "не найден")
# ГЛАВНАЯ ПРОВЕРКА: всё вне пар-блока обязано совпадать построчно

View file

@ -26,7 +26,7 @@ eval/.venv/bin/python eval/conformance.py eval/dovodka/requirements.md --final
|---|---|---|
| **РАМКА** | | |
| R1 | Отчёт фазы — секции Д0Д8 в `23-editor-tier.md`, продолжение того же документа | `$ grep -q "^## Д3 — en→ru" docs/experiments/23-editor-tier.md && grep -q "^## Д6 —" docs/experiments/23-editor-tier.md && grep -q "^## Д9 —" docs/experiments/23-editor-tier.md` |
| R2 | Сводная таблица «пробел → чем закрыт → носитель-артефакт» обязательна | `$ grep -q "ПРОБЕЛ → ЧЕМ ЗАКРЫТ → НОСИТЕЛЬ" docs/experiments/23-editor-tier.md` |
| R2 | Сводная таблица «пробел → чем закрыт → носитель-артефакт» обязательна | `$ grep -q "ПРОБЕЛ → ЧЕМ ЗАКРЫТ → НОСИТЕЛЬ" docs/experiments/23-editor-tier.md && test -s ~/books/dovodka/canon-dissect.json && test -s ~/books/dovodka/d1-attempts.jsonl` |
| R3 | Пакетный потолок фазы сверен СЛОВОМ владельца при запуске | `$ grep -q "PACK_CEILING" eval/dovodka/money_d.py && grep -q "САНКЦИЮ" eval/dovodka/money_d.py` |
| R4 | Фазовые потолки разложены в пре-реге Д0 | `$ grep -q "ФД-A" eval/dovodka/money_d.py && grep -q "ФД-G" eval/dovodka/money_d.py` |
| R5 | Касса: фриз-гейт, атомарный замок, два пути счёта, проекционные гарды | `$ eval/.venv/bin/python eval/dovodka/money_d.py --selftest` |
@ -38,7 +38,7 @@ eval/.venv/bin/python eval/conformance.py eval/dovodka/requirements.md --final
| R10 | Ретрай-харнесс: экспоненциальный бэкофф на 5xx, окно ≥24 ч, поштучно, лимит цены на клетку | `$ grep -q "BACKOFF = (30, 120, 480, 1800)" eval/dovodka/d1_gemini.py` |
| R11 | Журнал КАЖДОЙ попытки (время · код ответа · стоимость) файлом в сырьё | `$ test -s ~/books/dovodka/d1-attempts.jsonl && grep -q "\"ts\"" ~/books/dovodka/d1-attempts.jsonl` |
| R12 | Вердикт «модель не отдаёт» — только с журналом за ≥24 ч + снимком вендор-статуса | `$ grep -q "ТОЛЬКО с журналом" eval/dovodka/d1_gemini.py` |
| R13 | Собрано ≥12/16 → судейство абс-ригом со всеми контролями | `$ set -- $HOME/sud-d1/p1-answers/*.txt; test $# -ge 12` |
| R13 | Собрано ≥12/16 → судейство абс-ригом со всеми контролями | `$ set -- $HOME/books/judging/sud-d1/p1-answers/*.txt; test $# -ge 12` |
| R14 | Потолок клетки считается по `total_tokens` (скрытая тарификация ×4.5) | `$ grep -q "additive_total" eval/tenant_panel/roster.py` |
| **Д2 — внешняя подпись `tier`** | | |
| R15 | Задания `aj-tier-tasks` отданы внешнему судье вне Claude руками владельца | `$ test -s /home/ubuntu/books/editor-tier/sol-tier/ИНСТРУКЦИЯ.md` |
@ -70,7 +70,7 @@ eval/.venv/bin/python eval/conformance.py eval/dovodka/requirements.md --final
| R37 | Непригоден → СТОП и пинг; замену без слова владельца не искать | `$ eval/.venv/bin/python eval/dovodka/material_ja.py \| grep -q "СТОП\|OK "` |
| R38 | ja-промпты — те же гейты провенанса и консистентности, что Д3 | `$ eval/.venv/bin/python eval/dovodka/promptdiff.py ja-ru` |
| R39 | 810 единиц: связка · dspro-однопроходка · контроль редактора | `$ set -- $HOME/books/dovodka/dv-c-j0-*.json; test $# -ge 9` |
| R40 | Печатается сравнение ОТНОСИТЕЛЬНОГО порядка жильцов между парами zh/en/ja | `$ grep -q "ноги H-4" docs/experiments/23-editor-tier.md` |
| R40 | Печатается сравнение ОТНОСИТЕЛЬНОГО порядка жильцов между парами zh/en/ja | `$ set -- $HOME/books/dovodka/dv-h-j6-*.json; test $# -ge 9 && eval/.venv/bin/python eval/dovodka/ja6.py --score` |
| R41 | Статус оси РАЗВЕДКА объявлен В ПРЕ-РЕГЕ, а не постфактум | `$ grep -q "FORCED_DESCRIPTIVE" eval/dovodka/power.py` |
| **Д7 — самооценка** | | |
| R42 | Жильцам обеих ролей по трём книгам (zh/en/ja) вопрос о самооценке; ответы персистятся | `$ set -- $HOME/books/dovodka/dv-d-self-*.json; test $# -eq 6` |
@ -90,11 +90,11 @@ eval/.venv/bin/python eval/conformance.py eval/dovodka/requirements.md --final
| R55 | Статус-баннеры на отчётах экспов, где их нет | `$ test $(grep -l "баннер фазы Д" docs/experiments/*.md \| wc -l) -ge 7` |
| R56 | Шапка эндпоинтов `00-provider-quirks.md` актуализирована живым листингом | `$ grep -q "ЖИВОЙ ЛИСТИНГ ./models. ПЕРЕ-СНЯТ 2026-08-10" docs/experiments/00-provider-quirks.md` |
| **НОРМЫ РИГА** | | |
| R57 | Декой в каждой судейской пачке; сессия без пойманного декоя аннулируется целиком | `$ grep -q "CTRLdecoy" eval/dovodka/itog_d4.py` |
| R57 | Декой в каждой судейской пачке; сессия без пойманного декоя аннулируется целиком | `$ eval/.venv/bin/python eval/dovodka/ja6.py --score \| grep -q "ГРУБЫЙ ДЕКОЙ.*ПРОЙДЕН"` |
| R58 | Шумовой пол — парой, чьи половины судят РАЗНЫЕ сессии (порог был занижен на 19%) | `$ grep -q "ПОБАЙТНО РАВНЫЕ ПОЛОВИНЫ" eval/dovodka/sud.py` |
| R59 | Все армы единицы — в одном пакете одной сессии | `$ grep -q "ARMS=arms" eval/dovodka/sud.py` |
| R60 | Идентичность судей персистится ДО запуска; половиной пола не брать боевую клетку | `$ eval/.venv/bin/python eval/dovodka/runs.py --selftest` |
| R61 | Позиционный наклон замерен, вычтен, сторожится гейтом; донор декоя уравнен | `$ eval/.venv/bin/python eval/editor_tier/verify23.py` |
| R61 | Позиционный наклон замерен, вычтен, сторожится гейтом; донор декоя уравнен | `$ eval/.venv/bin/python eval/dovodka/naklon.py` |
| R62 | Судье запрещено сравнивать варианты между собой И читать их рядом | `$ grep -q "Не сравнивай варианты между собой" eval/dovodka/judge-prompts/core.md` |
| R63 | Замок кассы снимает только поставивший его процесс (проверка живости PID) | `$ eval/.venv/bin/python eval/dovodka/runs.py --selftest` |
| R64 | Реестр требований фазы — В GIT до покупок; conformance валиден только против закоммиченного | `$ eval/.venv/bin/python -c "import sys;sys.path.insert(0,'eval');import conformance as c;ok,why=c.frozen(c.Path('eval/dovodka/requirements.md'));print(why);sys.exit(0 if ok else 1)"` |
@ -107,9 +107,9 @@ eval/.venv/bin/python eval/conformance.py eval/dovodka/requirements.md --final
| R70 | Детекторы и пороги после взгляда на вердикты не менять; девиация = СТОП и пинг В МОМЕНТ | `$ grep -q "ПОРОГИ НЕ ТРОНУТЫ" eval/dovodka/contam_d.py` |
| R71 | Правка рига чужого пака — отдельным коммитом + пере-снятие его гейтов | `$ eval/.venv/bin/python eval/tenant_panel/verify22.py` |
| R72 | Приёмка адверсариальная author≠reviewer; опровергатель ДРУГОГО модельного семейства ОБЯЗАТЕЛЕН | `$ grep -q "Fable-5" docs/experiments/23-editor-tier.md` |
| R73 | Финал — построчный аудит закрытия заказа | `$ grep -q "^## Д11 — ИСПРАВЛЕНИЯ ФИНАЛЬНОГО АУДИТА" docs/experiments/23-editor-tier.md` |
| R73 | Финал — построчный аудит закрытия заказа | `$ eval/.venv/bin/python eval/dovodka/gain.py --selftest && eval/.venv/bin/python eval/dovodka/naklon.py --selftest && eval/.venv/bin/python eval/dovodka/adjud.py --controls` |
| **ОТЧЁТ И СДАЧА** | | |
| R74 | Сверка H-1…H-4 с фактом отдельной таблицей, ВКЛЮЧАЯ опровержения | `$ grep -q "Гипотезы владельца — сверка с фактом" docs/experiments/23-editor-tier.md` |
| R75 | CONFIRM/DENY владельцу — только с артефактом-носителем | `$ grep -q "носитель-артефакт" docs/experiments/23-editor-tier.md` |
| R76 | Деньги: итог по фазам ДВУМЯ путями счёта | `$ grep -q "Деньги фазы — итог ДВУМЯ ПУТЯМИ" docs/experiments/23-editor-tier.md` |
| R76 | Деньги: итог по фазам ДВУМЯ путями счёта | `$ eval/.venv/bin/python eval/dovodka/money_d.py --selftest` |
| R77 | Пинг в `docs/PROGRESS.md` секция «Полигон» при закрытии | `$ grep -q "15.08 · ФАЗА Д" docs/PROGRESS.md` |

View file

@ -25,26 +25,51 @@ PROMPTS = ZONE / "judge-prompts"
# ⚠ Ось — параметр. Пачки разных осей НЕЛЬЗЯ класть в один каталог и нельзя отдавать одной
# сессии: судья, увидевший обе, перестаёт быть слепым к паре.
AXIS = next((a.split("=", 1)[1] for a in sys.argv if a.startswith("--axis=")), "d4")
# ⚠ КОРЕНЬ — ТОЖЕ ПАРАМЕТР (заведено 16.08 под заход Д17). У осей фазы раскладка каталогов
# `judging/sud-<ось>`, а у захода она на уровень глубже: `judging/z17/<пара>`, потому что пара
# там своя у каждой панели. Прежде корень выводился из имени оси, и для захода пришлось бы
# заводить ВТОРОЙ такой же скрипт — то есть размножить будущие расхождения раскладки. Флаг
# `--root=` даёт тот же механизм чужой раскладке, не копируя его.
_ROOT = next((a.split("=", 1)[1] for a in sys.argv if a.startswith("--root=")), "")
FAM = {
"claude": dict(root=Path.home() / "books" / "judging" / f"sud-{AXIS}", tpl=PROMPTS / "claude.md"),
"claude": dict(root=(Path(_ROOT).expanduser() if _ROOT
else Path.home() / "books" / "judging" / f"sud-{AXIS}"),
tpl=PROMPTS / "claude.md"),
"sol": dict(root=Path.home() / "books" / "judging" / f"sol-{AXIS}-work", tpl=PROMPTS / "sol.md"),
}
if _ROOT:
FAM.pop("sol") # внешнее семейство пакет получает своим сборщиком
PER_SESSION = 4 # как у харнесса Sol в паке 23; одинаково для обоих семейств
MISSING_ONLY = "--missing" in sys.argv
def batches(root: Path, half: str) -> list[list[str]]:
"""Пачки набора, разложенные по сессиям. Порядок — от имени файла, значит воспроизводим."""
"""Пачки набора, разложенные по сессиям. Порядок — от имени файла, значит воспроизводим.
`--missing` РАСКЛАДЫВАЕТ ТОЛЬКО НЕОТСУЖЕННОЕ, и это не удобство, а защита данных. Сессия
может оборваться на середине (16.08: семь из четырнадцати упали на лимите харнесса, успев
записать часть ответов). Пере-запуск ЦЕЛОЙ сессии переписал бы уже полученные оценки новыми
то есть тихо подменил бы данные замера теми, что судья выдал во второй раз. Добор идёт только
по пачкам без ответа; уже отсуженное не трогается вовсе.
"""
toks = sorted(p.stem for p in (root / f"{half}-tasks").glob("*.txt"))
if MISSING_ONLY:
done = {p.stem for p in (root / f"{half}-answers").glob("*.txt")}
toks = [t for t in toks if t not in done]
return [toks[i:i + PER_SESSION] for i in range(0, len(toks), PER_SESSION)]
# Язык исходника — из провайдера пары, а не из текста шаблона: судья английской пачки, которому
# сказано «сверяй с китайским исходником», сверяет не с тем, что видит.
LANG = {"d4": "китайском", "d3": "английском", "d6": "японском", "d1": "китайском"}
LANG = {"d4": "китайском", "d3": "английском", "d6": "японском", "d1": "китайском",
"z17-zh": "китайском", "z17-en": "английском"}
# ⚠ Творительный падеж — ОТДЕЛЬНАЯ подстановка. Первая редакция просто вычёркивала язык из фразы
# «сверяй смысл с китайским исходником», потому что склонение не подставлялось. Проверенная
# ревью версия называла язык ДВАЖДЫ, и обезличивание было тихой потерей якоря.
LANG_INS = {"d4": "китайским", "d3": "английским", "d6": "японским", "d1": "китайским"}
LANG_INS = {"d4": "китайским", "d3": "английским", "d6": "японским", "d1": "китайским",
"z17-zh": "китайским", "z17-en": "английским"}
def render(fam: str, half: str, toks: list[str]) -> str:
@ -52,7 +77,8 @@ def render(fam: str, half: str, toks: list[str]) -> str:
body = m["tpl"].read_text(encoding="utf-8")
body = (body.replace("{ЯЗЫКЕ}", LANG[AXIS]).replace("{КАТАЛОГ}", m["root"].name)
.replace("{НАБОР}", half).replace("{ЯЗЫКОМ}", LANG_INS[AXIS]))
lst = "\n".join(f" ~/{m['root'].name}/{half}-tasks/{t}.txt" for t in toks)
body = body.replace(f"~/{m['root'].name}/", f"{m['root']}/")
lst = "\n".join(f" {m['root']}/{half}-tasks/{t}.txt" for t in toks)
return body.replace("{СПИСОК ФАЙЛОВ}", lst)

View file

@ -251,6 +251,18 @@ def _twin_of_base(arm: str, uid: str, txt: str) -> bool:
return bool(base.strip()) and txt.strip() == base.strip()
def _cell_file(arm: str, uid: str):
"""Файл клетки арма — ОДНА точка правды для `text_of` и для гейтов.
Раздвоение путей и было вторым дефектом finish-гейта. `text_of` знает, что клетки добивки
лежат под своим префиксом (`dv-e-r1-`, касса ФД-E), а гейт спрашивал `C.tag`, который для
того же арма даёт `dv-a-r1-` файла нет, клетка молча выпадает из проверки. Так клетка
`dv-e-r1-de3916de62.json` с finish=length прошла в судейскую пачку оси Д1.
"""
f = C.OUT / f"dv-e-r1-{uid}.json" if arm == "R1" else C.OUT / f"{C.tag(arm, uid)}.json"
return f if f.exists() else None
def text_of(arm: str, u: dict) -> str:
"""Текст арма. Клетка, не прошедшая гейт годности, в пачку НЕ идёт — возвращается пусто."""
uid = u["uid"]
@ -413,9 +425,15 @@ def selftest() -> int:
len(us) == EXPECT_N[AXIS], str(len(us)))
u = us[0]
# клетки finish=length не должны попадать в пачку ни одним армом
lens = [(a, x["uid"]) for a in ARMS_D4 + ARMS_OLD for x in us
if (C.OUT / f"{C.tag(a, x['uid'])}.json").exists()
and json.loads((C.OUT / f"{C.tag(a, x['uid'])}.json").read_text()).get("finish")
# ⚠ ОСЬ — ПАРАМЕТР И ЗДЕСЬ. Прежняя редакция перебирала ARMS_D4 + ARMS_OLD на ЛЮБОЙ оси,
# то есть на d1/d3/d6 сертифицировала ЧУЖУЮ панель и была зелена вхолостую. Цена дефекта
# замерена: клетка `dv-e-r1-de3916de62.json` с finish=length ушла в судейскую пачку оси Д1,
# хотя норма Д0.6 это прямо запрещает, и гейт промолчал. Пере-счёт без неё: перевес R1/A0
# у claude 0.100 → +0.000, у Sol +2.533 → +2.143; вердикты не переворачиваются, но норма
# была нарушена и прибор об этом не сказал.
lens = [(a, x["uid"]) for a in (ARMS + ARMS_OLD if AXIS == "d4" else ARMS) for x in us
if _cell_file(a, x["uid"]) is not None
and json.loads(_cell_file(a, x["uid"]).read_text(encoding="utf-8")).get("finish")
!= "stop" and text_of(a, x)]
ck("клетка finish=length в пачку не попадает", not lens, str(lens[:2]))
# маржевый декой обязан РЕАЛЬНО сажаться, иначе «не пойман» = «не было»
@ -459,7 +477,7 @@ def selftest() -> int:
ck("половины пола НЕ равны побайтно (иначе контроль пуст)",
not [1 for a, b in pairs if a == b])
# половина пола не должна совпадать с боевой клеткой (урок «близнеца» пака 23)
battle = {text_of(a, x) for a in ARMS_D4 for x in us}
battle = {text_of(a, x) for a in ARMS for x in us} # ⚠ панель ОСИ, а не всегда d4
ck("половина пола не совпадает с боевым армом",
not [1 for a, b in pairs if a in battle or b in battle])
# ключи лежат ВНЕ рабочего каталога судьи
@ -467,11 +485,19 @@ def selftest() -> int:
KEYS not in TASKS1.parents and TASKS1 not in KEYS.parents and KEYS.parent != WORK,
f"{KEYS} против {WORK}")
# семейство первичных контрастов совпадает с объявленным в power.py
# ⚠ И СЕМЕЙСТВО — ТОЖЕ ПО ОСИ. Проверка была прибита к «Д4 edit-контур zh» и на трёх осях
# из четырёх сверяла чужое объявление со своим — то есть не проверяла ничего.
P = _load("power_d", ZONE / "power.py")
declared = {c.split("")[0] for c in P.PRIMARY["Д4 edit-контур zh"]}
mine = {f"{a}/{b}" for a, b, _ in FAMILY_D4}
ck("семейство совпадает с объявленным в power.py", declared == mine,
f"{sorted(declared)} против {sorted(mine)}")
pname = {"d4": "Д4 edit-контур zh", "d3": "Д3 en→ru несущая",
"d1": "Д1 gemini добивка"}.get(AXIS)
if pname and pname in P.PRIMARY:
declared = {c.split("")[0] for c in P.PRIMARY[pname]}
mine = {f"{a}/{b}" for a, b, _ in FAMILY}
ck(f"семейство оси совпадает с объявленным в power.py ({pname})", declared == mine,
f"{sorted(declared)} против {sorted(mine)}")
else:
ck("ось объявлена ОПИСАТЕЛЬНОЙ — первичного семейства в power.py нет",
AXIS == "d6", f"ось {AXIS}")
print(f"\n{'СУДЕЙСКИЙ ХАРНЕСС ГОДЕН' if not bad else f'ПРОВАЛОВ: {bad}'}")
return bad

View file

@ -122,8 +122,9 @@ def verdict(gap: float, thr: float, p: float) -> str:
return "РАЗЛИЧИМ" if abs(gap) > thr and p < 0.05 else "в пределах"
def report(tag: str = "vendor2", p: str = "en", anchor: str = "RN", drop: tuple = ()) -> int:
r1, r2 = ranks_of(tag, p, "r1"), ranks_of(tag, p, "r2")
def report(tag: str = "vendor2", p: str = "en", anchor: str = "RN", drop: tuple = (),
da: str = "r1", db: str = "r2") -> int:
r1, r2 = ranks_of(tag, p, da), ranks_of(tag, p, db)
uids = [u for u in sorted(set(r1) & set(r2)) if u not in drop]
if not uids:
raise SystemExit("⛔ нет глав, прочитанных ОБОИМИ кругами")
@ -188,7 +189,8 @@ def report(tag: str = "vendor2", p: str = "en", anchor: str = "RN", drop: tuple
return 0
def vkus(tag: str = "vendor2", p: str = "en", drop: tuple = ()) -> int:
def vkus(tag: str = "vendor2", p: str = "en", drop: tuple = (),
da: str = "r1", db: str = "r2", na: str = "fable-5", nb: str = "opus-5") -> int:
"""СРАВНЕНИЕ ДВУХ СУДЕЙСКИХ СЕМЕЙСТВ — то, чего норме П-1 не хватало с 20.08.
Заведено 23.08 по слову владельца: «раз уж ты пробежал какое-то судейство другим агентом,
@ -202,12 +204,12 @@ def vkus(tag: str = "vendor2", p: str = "en", drop: tuple = ()) -> int:
ОТНОСИТЕЛЬНОЕ «семейства по-разному упорядочивают», а не «одно строже другого». Абсолютной
строгости этот прибор не видит и видеть не может.
"""
r1, r2 = ranks_of(tag, p, "r1"), ranks_of(tag, p, "r2")
r1, r2 = ranks_of(tag, p, da), ranks_of(tag, p, db)
uids = [u for u in sorted(set(r1) & set(r2)) if u not in drop]
arms = sorted(r1[uids[0]])
print(f"\n=== ВКУС ДВУХ СЕМЕЙСТВ, {tag}: fable-5 (круг 1) против opus-5 (круг 2) ===")
print(f"\n=== ВКУС ДВУХ СЕМЕЙСТВ, {tag}: {na} ({da}/) против {nb} ({db}/) ===")
print(f" глав {len(uids)} · армов {len(arms)}\n")
print(f" {'арм':6s}{'fable':>8s}{'opus':>8s}{'сдвиг':>8s}{'глав ↑':>8s}{'глав ↓':>8s}{'p':>9s} вывод")
print(f" {'арм':6s}{na[:7]:>8s}{nb[:7]:>8s}{'сдвиг':>8s}{'глав ↑':>8s}{'глав ↓':>8s}{'p':>9s} вывод")
rows = []
for a in arms:
d = [r2[u][a] - r1[u][a] for u in uids] # >0 — opus ставит НИЖЕ (место больше)
@ -217,12 +219,12 @@ def vkus(tag: str = "vendor2", p: str = "en", drop: tuple = ()) -> int:
rows.append((a, st.mean([r1[u][a] for u in uids]), st.mean([r2[u][a] for u in uids]),
m, sum(1 for x in nz if x < 0), sum(1 for x in nz if x > 0), pv))
for a, m1, m2, m, up, dn, pv in sorted(rows, key=lambda x: x[3]):
mark = "⛔ opus судит ИНАЧЕ" if pv < 0.05 else ""
mark = f"{nb} судит ИНАЧЕ" if pv < 0.05 else ""
print(f" {a:6s}{m1:8.2f}{m2:8.2f}{m:+8.2f}{up:8d}{dn:8d}{pv:9.4f} {mark}")
sig = [r for r in rows if r[6] < 0.05]
print(f"\n армов, чьё место семейства ставят РАЗЛИЧИМО по-разному: {len(sig)} из {len(arms)}"
+ ("" if not sig else "" + ", ".join(r[0] for r in sig)))
print(" ⚠ сдвиг >0 значит «opus ставит арм НИЖЕ, чем fable»; сумма сдвигов по построению ≈0")
print(f" ⚠ сдвиг >0 значит «{nb} ставит арм НИЖЕ, чем {na}»; сумма сдвигов по построению ≈0")
return 0
@ -267,6 +269,10 @@ if __name__ == "__main__":
_tag = next((x.split("=", 1)[1] for x in a if x.startswith("--tag=")), "vendor2")
_anch = next((x.split("=", 1)[1] for x in a if x.startswith("--anchor=")), "RN")
_drop = tuple(next((x.split("=", 1)[1].split(",") for x in a if x.startswith("--drop=")), []))
_da = next((x.split("=", 1)[1] for x in a if x.startswith("--a=")), "r1")
_db = next((x.split("=", 1)[1] for x in a if x.startswith("--b=")), "r2")
_na = next((x.split("=", 1)[1] for x in a if x.startswith("--na=")), "fable-5")
_nb = next((x.split("=", 1)[1] for x in a if x.startswith("--nb=")), "opus-5")
if "--vkus" in a:
sys.exit(vkus(_tag, "en", _drop))
sys.exit(report(_tag, "en", _anch, _drop))
sys.exit(vkus(_tag, "en", _drop, _da, _db, _na, _nb))
sys.exit(report(_tag, "en", _anch, _drop, _da, _db))