diff --git a/docs/experiments/23-editor-tier.md b/docs/experiments/23-editor-tier.md new file mode 100644 index 00000000..7d0b57d6 --- /dev/null +++ b/docs/experiments/23-editor-tier.md @@ -0,0 +1,896 @@ +# 23. Сильный тир редакторской роли и граница `glm-5` + +**Полигон-сессия, 09.08.2026.** Номер 23 присвоен оркестратором №16 при лендинге 10.08. Зона: +`eval/editor_tier/` + этот файл. Санкция владельца на потолок пака $4.00 (09.08). + +> ⚠⚠ **РЕВЬЮ-ШАПКА ПРИЁМКИ (оркестратор №16, 10.08) — шапка первична над телом.** Числа пака +> подтверждены независимым пересчётом мимо харнесса (border/tier/деньги/репликация/внешний судья — +> до знака; LOO по сессиям устойчив; аннулированные прогоны воспроизводятся из сырья, «фантомные» +> −0.29/−0.42 подтверждены невыводимыми). **Мандат пака, потолок $4.00 и санкции подтверждены +> владельцем задним числом 10.08.** **Выводы НЕ ратифицированы** — владелец признал постановку +> неполной и заказал **фазу Д** (`docs/POLYGON_EXP2223_REDO_SESSION_PROMPT.md`): добивка +> gemini · внешняя подпись `tier` · несущие оси en/ja · edit-контур · канон-вскрытие; отчёт фазы — +> секции Д0–Д8 этого файла. Поправки приёмки к телу (чек-лист §Д8 промта), главные: счёт +> агент-сессий = **58**, не 50 (§6/§14) · противоречие §9/§13 против §12в о внешнем контуре · +> наклон `border` по текущему сырью **+0.708**, не +0.646 · боевые `aj-border*` — пере-штампованные +> копии `replication-runA` (mtime 23:31:54), исходный mtime-провенанс судейства утрачен · в голосах +> `replication-runB` — judge-имена прогона A (`ingest` читает JUDGES-файл прогона A). + +> ⚠ **ЗАКАЗА НА ЭТОТ ПАК НЕ СУЩЕСТВУЕТ.** Промта на эксп-23 в `docs/` нет: пак назначен сессией +> себе самой, владелец санкционировал только ДЕНЬГИ. Оркестратор уже пометил его как незаявленный. +> Пре-рег и реестр требований, на которые ссылается этот отчёт, написаны той же сессией и заказом +> не являются — читать их как самообязательство, а не как контракт. Содержательно пак закрывает +> дыру, объявленную самим эксп-22 (§13а: сильный тир был срезан из панели), но и она была не +> заданием, а его находкой. Список к подписи владельца — §14. + +> **СТАТУС: замер закончен.** Потрачено **$2.907359** при пакетном потолке $4.00; судейство обоих +> проходов пере-снято после приёмки (`tier` — из-за отсутствия декоя, `border` — из-за подменённой +> базы арма, §0 и §3), пере-прогоны покупок не потребовали. +> Числа пере-считываются `eval/editor_tier/itog23.py` из персистированного сырья и сторожатся +> `eval/editor_tier/verify23.py` — ненулевой код возврата значит «отчёт не сдаётся». +> +> ⚠ **Чего гейты НЕ доказывают, вопреки прежней редакции этой шапки.** (1) Гейт чисел сторожит, +> что число ПРИСУТСТВУЕТ в тексте, а не что предложение вокруг него истинно; пороговые и +> сравнительные утверждения проверяются только чтением. (2) `eval/conformance.py --final` гонялся +> против реестра требований, который живёт в сессионном scratchpad и в репозиторий не попадает, — +> читатель его пере-снять не может, и «соответствие заказу проверено» отсюда не следует; вдобавок +> большая часть его улик — греп формулировок ЭТОГО ЖЕ отчёта, то есть отчёт проверял сам себя. +> (3) Прежняя редакция `verify23.py` пропускала проход, по которому нет ни одного голоса, и +> печатала «все числа сходятся» с нулём проверок — ровно так аннулированный `border` прошёл гейт. +> Починено; но каждый из трёх дефектов прошлые редакции этой шапки называли доказательством. + +## ИТОГ — ВХОД ДЛЯ РЕШЕНИЯ ВЛАДЕЛЬЦА + +⚠ **ЧИТАТЬ §0 ПЕРВЫМ.** Судейство этого пака аннулировалось ДВАЖДЫ, по двум разным причинам: +первый прогон — потому что в нём не создавался декой; второй, в части прохода `border`, — потому +что у арма `R0` на половине единиц была подменена база. Оба дефекта нашла приёмка, не автор. +Ниже — числа третьего, принятого судейства: оба прохода с полным набором контролей, декой пойман +в 16/16 и 32/32 единиц, отбраковок ноль. + +**Оба несущих ответа пака отрицательные, и это не осторожность формулировки, а результат.** Ни +сильный тир четырёх провайдеров, ни пограничный `glm-5` не отличимы от боевого редактора +`deepseek-v4-pro` на пороге, который кладёт собственный шум прибора. Рекомендация эксп-22 остаётся +в силе — теперь на панели, где сильный тир присутствует, а не срезан. По правилу ничьей D39.117 +это тем более так: `deepseek-v4-pro` из неразличимых ещё и самый дешёвый. + +### 0. ДВА АННУЛИРОВАННЫХ ПРОГОНА И ЧЕМУ ОНИ УЧАТ + +Хук декоя возвращал ригу ИМЯ арма вместо ТЕКСТА; порча садилась в строку «R0», давала 0 замен при +пороге 2 и декой МОЛЧА не создавался — ноль контролей в обоих проходах. Нашло это адверсариальное +ревью, которому было поручено искать вне карты отчёта; сам отчёт при этом ссылался на правило +«сессия без пойманного декоя аннулируется». + +**Один и тот же оплаченный материал дал ПРОТИВОПОЛОЖНЫЕ ответы:** + +``` +контраст без декоя (run1, аннулир.) с декоем (run2, ТОЖЕ АННУЛИРОВАН) принято (run3) +T2 gpt-5.6-terra +2.00 «БЬЁТ» +0.50 ниже порога +0.50 +T3 grok-4.5 +1.81 «БЬЁТ» +0.19 ниже порога +0.19 +R2 glm-5 −0.09 «эффекта нет» −1.53 «ЗНАЧИМО ХУЖЕ» −0.81 ниже порога +``` +⚠ **Поправка фазы Д (чек-лист приёмки №16 п.7): прежде колонка называлась «с декоем (принято)», и +в строке `R2` под этим заголовком стояло −1.53 — число АННУЛИРОВАННОГО run2.** Принятые числа +`tier` (`T2`/`T3`) действительно из run2, а принятое число `border` — из run3, потому что run2 +аннулирован именно по проходу `border` (подменённая база `R0`, §3). Один заголовок на две колонки +разного статуса и порождал ошибку; теперь статус подписан у каждой. + +⚠ **Однофакторной эта таблица является только по строке `border`.** В проходе `tier` между двумя +прогонами изменилось ДВА: появился декой И появился позитивный контроль `F` (ключи: армы run1 +`[R0,T1,T2,T3]`, семейство из 3 строк · принятый `[R0,T1,T2,T3,F]`, семейство из 4). Делитель +Холма изменился 3→4, но ни один вердикт от этого не двигается. Чистая улика — `border`: там состав +армов совпадает и добавлен ровно декой. Кроме того обе пачки судили разные, нигде не +зафиксированные популяции судей (см. §8), так что «менялось только X» вообще не доказуемо этим +дизайном — правильная формулировка «наблюдаемое различие сопровождало появление декоя». + +⚠ **Строка `border` в этой таблице — улика ПРО ДЕКОЙ, но ни одно её число не является ответом пака +про `glm-5`.** Оба сравниваемых прогона несли один и тот же дефект базы `R0` (§3): он в сравнении +удерживается постоянным, поэтому разница −0.09 против −1.53 по-прежнему говорит о влиянии декоя, — +но правильный ответ снят третьим прогоном и равен −0.81. Не смешивать два утверждения: «декой +меняет вердикты» пак показал, «вердикт по `glm-5` такой-то» — только в §3. + +⚠ **Урок шире декоя.** Каждый из трёх прогонов давал внутренне связную таблицу, и каждый раз +неверным оказывался НЕ вывод из чисел, а само основание под числами: сперва отсутствующий контроль, +потом подменённая база. Ни один из двух дефектов не был виден в результатах — оба нашлись только +чтением кода и сверкой сырья. Отсюда практическое правило, а не мораль: пока контроль не напечатан +числом рядом с боевым перевесом, «результат» — это гипотеза о работе прибора, а не о моделях. + +Тексты не менялись, деньги не тратились — менялось наличие заведомо испорченного текста в +судейской пачке. **Механизм:** декой задаёт судье масштаб настоящей ошибки. Без него судья +сравнивает хорошее с хорошим и раздувает мелочь до величины реальной разницы (отсюда ложные +2.00) +либо, наоборот, не находит разницы там, где она есть (отсюда ложный ноль). Косвенное подтверждение: +в аннулированном прогоне половина единиц отбраковывалась за ненулевые оси БЕЗ цитат — судьям было +нечего цитировать, они регистрировали различия, а не ошибки. В пере-прогоне отбраковка — ноль. + +⇒ **Одна отсутствующая проверка дала две ошибки РАЗНОГО знака.** Контроль чувствительности судьи — +не формальность протокола, а условие осмысленности любого числа пака. + +### 1. СИЛЬНЫЙ ТИР НЕ ОТЛИЧИМ ОТ БОЕВОГО РЕДАКТОРА + +16 единиц, побайтно одна якорная база, Холм по четырём. Порог различимости — шумовой пол СВОЕГО +прохода, **1.02**. + +``` +T1 glm-5.2 −0.19 [−0.56, +0.12] Холм 1.0000 ниже порога различимости +T2 gpt-5.6-terra +0.50 [−0.06, +1.12] Холм 0.5977 ниже порога различимости +T3 grok-4.5 +0.19 [−0.12, +0.62] Холм 1.0000 ниже порога различимости +R0 vs F (КОНТРОЛЬ) +2.06 [+1.19, +3.00] Холм 0.0039 ✔ боевой редактор ПОКУПАЕТ поверх черновика +``` + +**Формулировка выбрана буквально.** Вердикт пре-рега — «ниже порога различимости при данном n», а +не «различий нет»: у `T2` интервал доходит до +1.12, то есть накрывает значения ВЫШЕ порога 1.02. +Утверждать равенство армов эти данные не позволяют — они позволяют утверждать, что эффект меньше +того, что прибор на 16 единицах способен развести. + +**Позитивный контроль — ключ к чтению.** Прибор находит +2.06 там, где разница реальна (редактор +против голого черновика), и не находит ничего между редакторами. Значит малость перевесов — это +свойство армов, а не слепота прибора. Контроль `F` добавлен ПОСЛЕ аннулирования первого прогона: +он впечатан в ключ до появления первого ответа судьи (пре-регистрация соблюдена), и на вердикты +T1–T3 не влияет — при семействе из 3 и из 4 поправка Холма даёт одни и те же величины. + +⇒ **Посылка 1 ПОДТВЕРЖДЕНА: дороговизна качества не покупает.** Сильный тир четырёх провайдеров +не даёт различимого выигрыша над `deepseek-v4-pro`. + +### 2. ВЫВОД ЭКСП-22 УСТОЯЛ И ТЕПЕРЬ ПОДПЁРТ ПОЛНОЙ ПАНЕЛЬЮ + +Эксп-22 рекомендовал `deepseek-v4-pro` и сам объявил (§13а), что вывод верен лишь ВНУТРИ панели, +потому что сильный тир был срезан. Дыра закрыта: тир проверен и не лучше. **Рекомендация остаётся, +и теперь она стоит на панели, включающей сильный тир OpenAI, xAI и Z.AI.** + +### 3. ГРАНИЦА `glm-5` НЕ РАЗРЕШЕНА: ЭФФЕКТ НИЖЕ ПОРОГА РАЗЛИЧИМОСТИ + +32 единицы (16 эксп-22 + 16 новых), порог — шумовой пол СВОЕГО прохода, **1.48**. + +``` +R2 glm-5 vs R0 боевой −0.81 [−1.81, +0.28] Холм 0.1601 ниже порога различимости +контроли: ДЕКОЙ −4.88 поймано 32/32 ✔ · ПОЛ −1.00 [−2.06, +0.06] честен · побайтных дублей нет +``` + +⛔ **Это ТРЕТЬЕ судейство прохода; первые два аннулированы.** Второе давало −1.53 [−2.19, −0.84], +Холм 0.0002, и на нём стояли вывод «граница разрешена ПРОТИВ `glm-5`» и закрытие Резерва D39.22. +Приёмка нашла в нём подменённую базу: на 16 из 32 единиц арм `R0` был редактурой боевого редактора +поверх ЧУЖОГО черновика, а не поверх якорного, — контраст сравнивал армы над разными основаниями. +Причина в именовании эксп-22, где арм редактуры назван по ЧЕРНОВИКУ-жильцу, а не по редактору. + +**Что изменилось в третьем прогоне** (покупок не потребовал, тексты те же): база `R0` сверена +побайтно с якорной редактурой эксп-22 — 16/16 · донор декоя уравнен по армам (было: порча во всех +32 единицах делалась из `R0`, а `R0` — второй арм ЕДИНСТВЕННОГО контраста прохода; стало 16 из +`R0`, 16 из `R2`) · идентичность судейских сессий персистирована (прежде во всех голосах стоял +`judge='?'`) · пере-суживание довело выборку до полных 32 единиц, отбраковок ноль. + +**Проверка, что уравнивание донора не подменило вывод:** на 16 единицах с донором `R0` перевес +−0.81 и на 16 с донором `R2` тоже −0.81 — вклад донора неотличим от нуля. + +⚠ **Поправка фазы Д: `p` пере-считаны ТОЧНО, а были оценкой.** При n>20 риг брал Монте-Карло +(200 000 розыгрышей, фиксированное зерно) — отсюда одна и та же величина печаталась в отчёте как +0.1603 и как 0.1608, то есть заявление «числа воспроизводятся до знака» на ней не выполнялось. +Перевесы целые, поэтому распределение суммы ±xᵢ берётся динамикой по достижимым суммам ТОЧНО и +мгновенно: **p = 0.1601** (боевой прогон и `runA`), **0.3339** (`runB`). Ни один вердикт не +двигается; двигалась только третья значащая цифра, и теперь она воспроизводима. + +**Расщепление по половинам** (обе половины на правильной базе): +``` +все 32 единицы −0.81 p 0.1601 +16 старых эксп-22 −1.62 p 0.0449 +16 новых 0.00 p 1.0000 +``` +Половины расходятся: на старых 16 единицах перевес почти достаёт до порога, на новых он ровно ноль. +Ни одна не проходит порог 1.48, но расхождение половин само по себе больше, чем удобно объяснять +шумом, и различаются они ещё и составом глав. Это и есть граница того, что пак может сказать. +Прежняя интерпретация — «прежний результат ВЕРЕН, а не артефактом малого n» — не воспроизвелась: +контраст эксп-22 (−2.12 на 16 единицах) при удвоении n и починенных контролях сжался до −0.81. +⇒ **Ожидание пре-рега оправдалось: перевес сжался, а не устоял.** + +⚠ Что здесь НЕ утверждается. «Ниже порога» не значит «армы равны»: интервал тянется от −1.81 до ++0.28. Знак отрицателен во всех трёх прогонах и в эксп-22, то есть слабое свидетельство против +`glm-5` есть — на этом приборе и этом n оно не доходит до различимого. Разрешается граница только +ростом мощности (больше единиц или менее шумный судейский контур), а не пере-чтением этих чисел. + +### 4. ЦЕНА (замер, 1500 единиц на книгу) + +``` +арм p50 edit p95 edit p50 связка книга p50 книга p95 × к R0 +R0 deepseek-v4-pro 0.00769 0.01573 0.00897 $13.45 $25.13 — +T1 glm-5.2 0.01725 0.01955 0.01879 $28.18 $31.66 2.09× +T2 gpt-5.6-terra 0.04408 0.04841 0.04544 $68.17 $74.88 5.07× +T3 grok-4.5 0.05276 0.06441 0.05397 $80.96 $97.88 6.02× +якорный черновик (общая база связки, входит в каждую связку): p50 0.00122 +``` +⚠ **Прежняя редакция этой таблицы сравнивала связку с не-связкой.** В строке `R0` стояло +`0.00545` — медиана ДВУХ клеток скрина эксп-22, не этих шестнадцати, — а книжные столбцы при ней +были посчитаны по СВЯЗКЕ «черновик+редактор», тогда как у `T1`–`T3` те же столбцы шли по одной +редактуре. Нескладность была видна в самой строке: 0.00545 × 1500 = $8.18, а напечатано $13.45. +Ни один гейт её не трогал: пере-счёт спрашивал у эксп-22 метод `edit_cost`, которого у него нет, и +строка молча выходила нулевой. Починено в `itog23.py`/`verify23.py`; теперь оба семейства колонок +печатаются рядом и сторожатся. **Вывод «сильный тир в 2–6 раз дороже» от починки не изменился** — +он и считался по связкам, просто напечатан был не тем числом. +⇒ **Решение однозначно и не требует продуктового суждения:** сильный тир стоит в 2–6 раз дороже и +не даёт различимого выигрыша. Платить не за что. + +### 5. ОТКАЗНОЙ РЕЖИМ KIMI — КЛЕТКА ЗАМЕРЕНА ПОД СВОИМ ПОТОЛКОМ (посылка 2 согласуется, но не различает) + +`kimi-k3` в редакторской роли — **пустой выход при 99.9% доли размышления**, $0.0804 за клетку. +Особый режим (одна клетка вместо полного скрина), объявленный ДО покупок, сэкономил ≈$1. + +⚠ **Клетке был выдан СВОЙ потолок вывода: `max_out=4000` против 16000 у всех остальных** +(`screen.py:47`), и сырьё показывает `finish=length · completion 4000 · reasoning 3997 · content +пуст`. То есть модель уперлась в мой потолок, ещё не выйдя из фазы размышления. Прежняя редакция +называла только «одну клетку вместо полного скрина» и умалчивала о потолке, а квирк-реестр проекта +(`00-provider-quirks.md`) описывает ровно этот симптом как нехватку бюджета и предписывает ≥16000. +Поэтому: **вердикт по ЦЕНЕ твёрдый** — $0.080406 уже выше порога роли $0.06, а при 16000 клетка +стоила бы ≈$0.26; **вывода об отказном режиме вендора эта клетка не даёт** — она не различает +«модель отказывается работать» и «мой потолок обрезал её на размышлении». Эксп-22 снимал +`kimi-k2.6` другим замером; повторением того результата это считать нельзя. Девиация — в §8. + +### 6. ДЕНЬГИ И ДИСЦИПЛИНА + +``` +ФA скрин $0.481038 / 1.10 ФC панель+пол $2.080125 / 2.30 +ФB единицы $0.346196 / 0.45 ПАК $2.907359 / 4.00 +``` +Ни один потолок не пробит, деньги сведены двумя путями (расхождение ≤7e-6). Пере-прогон судейства +покупок не потребовал. Дополнительная санкция владельца на $5 не понадобилась. + +⚠ **Потолок ФC поднимался ДВАЖДЫ; вот точная хронология по коммитам и mtime сырья.** + +``` +03:21:57 фриз f1f9947 ФC 1.80 → 1.95, объявлено в коде фазы +04:37:39 первая покупка панели ← 76 мин ПОСЛЕ фриза +05:17:43 последняя покупка панели ← панель фактически стоила $1.798638 +05:36:25 фриз 1f6d6ae ФC 1.95 → 2.30 +05:37:25 первая покупка шумового пола ← 60 с ПОСЛЕ фриза +``` +Ни один доллар не потрачен под незафризенным операционным потолком. **Обе прежние редакции этого +абзаца были неверны, и во взаимно противоположные стороны:** сначала «поднят до покупок фазы» — +умалчивая, что подъёмов было два; затем «правка попадает в середину окна покупок» — а она в него +не попадает вовсе, второй подъём случился через 19 минут ПОСЛЕ последней покупки панели. Вторая +формулировка была самооговором, написанным при пере-чтении по памяти, без сверки с `git log`. + +⚠ **Основание подъёма названо замером, а было проекцией.** «Панель $1.844» — это оценка +`--c-plan` по медианам цен фазы A, а фактическая панель стоила **$1.798638**, то есть влезала и в +исходные $1.80. Честное основание второго подъёма другое и его надо назвать прямо: своему шумовому +полу нужно было ещё $0.281487, и проекционный сторож кассы при потолке 1.95 отказал бы последним +клеткам. Взят подъём из объявленного планом резерва пака ($0.65), пакетный потолок $4.00 не +двигался. Расход резерва на СВОЙ шумовой пол — не то назначение, под которое резерв объявлялся +(«ре-гены эхо-мины и ретраи»), и правило пре-рега «не влезает — СТОП и пинг» исполнено не было: +артефакта пинга нет. Вопрос владельцу — §14. + + +⚠ **Позиционная поправка замерена и вычтена по ОБОИМ проходам.** `tier`: наклон **−0.006** ошибки +на шаг метки, после вычитания −0.18 / +0.49 / +0.18 — вердикты не меняются. `border`: наклон +**+0.708** (раскладка этого прохода короче, и позиция весит заметно больше), поправка двигает +контраст с −0.81 до −0.90 при p 0.0873 — вердикт «ниже порога различимости» не меняется ни до, ни +после поправки, хотя после поправки контраст подходит к границе значимости ближе, чем до неё. + +⚠ **Поправка фазы Д (чек-лист приёмки №16 п.6): здесь стояло +0.646, а сырьё даёт +0.708.** +Величина, на которую делается поправка боевого контраста, печаталась не из тех голосов, из которых +считается сам контраст. Числа с поправкой (−0.90, p 0.0873) при этом верны — они пере-сняты и +сходятся. Причина расхождения не установлена: промежуточные состояния разбора не сохранились, а +голоса с тех пор пере-снимались дважды (гонка §8 п.10 и пере-разбор репликации ниже). **В прибор — +сделано:** `verify23` сторожит наклон ЧИСЛОМ по каждому проходу; прежде он не трогал его вовсе, +хотя поправка на наклон — часть решающего правила. + +**Агент-запусков: 58 при капе 60.** Первый (аннулированный) прогон 16 + пере-суживание 9 + +пере-прогон `tier` 12 + адверсариальное ревью 1 + пере-судейство `border` 12 + репликация 8 (§12а). +⚠ **Поправка фазы Д (чек-лист приёмки №16 п.4): в этой строке и в §14 п.9 стояло 50 — репликация +из счёта выпала.** Число 58 совпадает с §12а, где оно и было напечатано верно; расходились между +собой две строки одного отчёта. + +⚠ Первые 38 сочтены МНОЙ, а не механизмом: `log_run` был написан и ни разу не вызван, +`agent-runs.json` не создавался, во всех голосах стоял `judge='?'`. С пере-судейства `border` учёт +персистируется — на диске 20 записей (12 боевых + 8 репликации), раскладка «кто какие единицы +судил» в `border-JUDGES.json` и `replication-runB-JUDGES.json`, в каждом голосе имя сессии. +⚠ **«Считает механизм» сказать нельзя** — файл записан РУКОЙ, скриптом, уже после судейства: в коде +`log_run` зовётся из одного места и единственную пометку `note='судейская сессия'` произвести может, +а три остальные, что стоят в записях, — нет. Значит: раскладка судей по единицам персистирована и +пере-снимаема, а **СЧЁТЧИК КАПА НЕ РАБОТАЕТ** — на диске 20 против 58 фактических, и он пропустил бы +ещё 60. Причина механическая: `log_run` вызывается из `--ingest`, то есть ПОСЛЕ суб-агента, а +сторожить кап можно только записью ДО запуска. Проход `tier` не покрыт вовсе: `tier-JUDGES.json` +не создан, во всех его голосах стоит `judge='?'`, и восстановить это задним числом нечем. +**Починено в фазе Д, не здесь:** `eval/dovodka/runs.py` пишет запись ДО запуска суб-агента, кап +проверяет перед записью и вдобавок ОТКАЗЫВАЕТ выдать те же токены незакрытой сессии — механический +замок против гонки §8 п.10. Селфтест 12/12. + +⚠ **ПРОВЕНАНС КАТАЛОГОВ СУДЕЙСТВА `border` УТРАЧЕН (поправка фазы Д, п.8).** Боевые `aj-border` и +`aj-border-votes` — пере-штампованные копии `replication-runA-*`: содержимое побайтно совпадает +(`diff -rq` чист), а mtime всех файлов равен **2026-08-09 23:31:54**, то есть моменту копирования, +а не моменту судейства. Задания `aj-border-tasks` сохранили исходный mtime 17:55:26. Что из этого +следует: содержательно боевой прогон и есть `runA`, и все его числа пере-снимаемы; но доказать +ВРЕМЕНЕМ, когда именно судились боевые ответы, по диску больше нельзя — а гейт свежести разбора, +заведённый после гонки (§8 п.10), опирается именно на mtime. Для этого пака он теперь сравнивает +копии с копиями. Объявляю как ограничение, восстановлению не подлежит. + +⚠ **ГОЛОСА РЕПЛИКАЦИИ НЕСЛИ ИМЕНА СУДЕЙ БОЕВОГО ПРОГОНА (поправка фазы Д, п.9).** `absjudge.ingest` +читает карту судей по имени ПРОХОДА (`blind-keys/border-JUDGES.json`), а репликация судила ТЕ ЖЕ 32 +токена вторым жребием и лежала в своих каталогах — поэтому все 32 её голоса получили имена +`agent-02…agent-12` вместо собственных `agent-B1…B8`. Перевесы от этого не двигаются (имя судьи в +разность не входит, и §12а пере-снимается до знака: −0.62, ДИ [−1.78, +0.53], p 0.3327, пол −1.00, +порог 1.77, декой −4.50 при 32/32), но разложение дисперсии по сессиям и любое «согласие судей» +считались бы по ложной раскладке. **Починено механизмом:** у рига появились крючки `DIRS_HOOK` и +`JUDGES_HOOK`, у прогона — свои каталоги и своя карта; `judge.py --run <прогон> <проход> --ingest` +пере-разобрал обе репликации. `replication-runA-JUDGES.json` при этом заведён копией боевой карты — +`runA` и есть боевой прогон, и теперь КАЖДЫЙ прогон несёт собственную карту, а не наследует её от +имени прохода. + +## §7 ЧЕТЫРЕ ПОСЫЛКИ — СВЕРКА С ФАКТОМ + +| # | посылка (записана ДО замера) | факт | итог | +|---|---|---|---| +| 1 | сильный тир НЕ обязан выигрывать; вероятный исход — не побьёт `deepseek-v4-pro` | ни один из троих не различим (−0.19, +0.50, +0.19 при пороге 1.02) | **ПОДТВЕРЖДЕНА** | +| 2 | отказной режим размышления — свойство ВЕНДОРА, `kimi-k3` воспроизведёт отказ | пустой выход при 99.9% размышления — но клетка шла под СВОИМ потолком 4000 (§5) | **СОГЛАСУЕТСЯ, НЕ РАЗЛИЧАЕТ**: замер не разводит отказ вендора и обрыв на размышлении моим потолком | +| 3 | граница `glm-5` разрешима ростом n, а не сменой судьи | при n=32 и починенных контролях перевес сжался до −0.81, порога 1.48 не проходит (§3) | **ОПРОВЕРГНУТА как ожидание «разрешится», ПОДТВЕРЖДЕНА как прогноз «сожмётся»** | +| 4 | внешний контур нужен, но не заменяет мощность; отсутствие Sol не блокирует | пак закрыт без Sol; семейный слепой участок остался | **ПОДТВЕРЖДЕНА частично** | + +⚠ Посылка 3 разошлась с прогнозом в другую сторону, чем объявлялось раньше. Пре-рег ожидал, что +при удвоении n перевес сожмётся; аннулированный прогон дал «устоял и стал значим», и это записали +как «ожидание не оправдалось». На исправленной базе перевес сжался — то есть прогноз пре-рега был +верен, а неверна была промежуточная запись. Сама посылка («разрешима ростом n») не подтвердилась: +удвоение n границу не разрешило, оно её закрыло в «не различимо». + +⚠ **Выводы, которые эта сессия объявляла и которые оказались неверны.** Порядок важен, потому что +объявлений было три волны, и каждая была подписана как окончательная. + +1. По числам ПЕРВОГО (аннулированного) прогона: «сильный тир бьёт боевой редактор», «вывод + эксп-22 перевёрнут», «граница разрешена в ноль», «посылка 1 опровергнута» — все четыре неверны, + держались на числах без контроля чувствительности судьи. +2. По числам ВТОРОГО прогона: «граница разрешена ПРОТИВ `glm-5`, результат эксп-22 реплицирован на + n=32» и закрытие Резерва D39.22 — сняты: половина материала шла с подменённой базой (§3). +3. В самом отчёте после этого ещё держались §13 («тир оказался лучше», «D39.22 закрыт В ПОЛЬЗУ + `glm-5`») — прямо против §1 и §3 того же документа, — и §12 с порогами из аннулированного + прогона. То есть отчёт какое-то время противоречил сам себе, и это тоже нашла приёмка. + +Общий механизм у всех трёх волн один: связная таблица принималась за истинную, а смелость вывода +работала аргументом в его пользу. Ни одну из волн не остановил автор. + +## §8 ДЕВИАЦИИ И ДЕФЕКТЫ — ОБЪЯВЛЯЮ + +1. **Половина первого прохода отвергнута разбором** (8 из 16; во втором — 10 из 32). Причина одна: + судьи ставили ненулевой счёт по оси, не подпирая цитатой, а разбор выбрасывает такую единицу + ЦЕЛИКОМ. Это дефект МОЕЙ формулировки: в задании сказано, что обоснование обязательно, но не + сказано, что цена нарушения — потеря всей единицы. Пере-суживание с явной ценой снизило + отбраковку до нуля. Стоило 9 агент-запусков; денег не стоило. +2. **ЭХО-МИНА НА БОЕВОЙ БАЗЕ, И ГЕЙТ ПРОТИВ НЕЁ БЫЛ НАПИСАН, НО НЕ ПОДКЛЮЧЁН.** + `et-unit-draft-deepseek-v4-flash-63ab55ac5c`: 2243 знака при исходнике 2182, `finish=stop` — + и в этих 2243 знаках **1798 иероглифов при НУЛЕ кириллицы**. Это не «пересказ вместо перевода», + как утверждала прежняя редакция пункта, а классическая эхо-мина: модель вернула исходник. + Прежняя редакция объявляла и то, что «ни один гейт пака этого не ловит» — тоже неверно: + `bakeoff.draft_reject_reason` ловит её штатно (пере-снято: вердикт «эхо исходника»), просто + фаза B звала только проверку «строка не пуста». Гейт подключён; на этом паке он забраковал бы + ровно эту единицу и больше ни одной из шестнадцати. + **Чувствительность вывода к ней замерена:** без неё контраст границы −0.74 (p 0.21) против + −0.81 (p 0.16) на всех 32 — порог 1.48 не проходит ни так, ни так, вердикт §3 не меняется. + Что здесь верно и что было сформулировано неточно: вход у обоих армов контраста ДЕЙСТВИТЕЛЬНО + одинаковый — это одна и та же база, — но оба редактора выдали полную длину, то есть достроили + пропущенное из исходника и решали на этой единице ДРУГУЮ задачу (перевод, а не редактуру). + Единица оставлена и объявлена. **В прибор: гейт «длина клетки против длины ИСХОДНИКА с + поправкой на пару языков»; сравнение с медианой соседних клеток этот случай не ловит, а на + разноязычных пулах даёт ложные срабатывания (см. снятый пункт ниже).** +3. **СНЯТО — было ложным срабатыванием, и оно уже ушло оркестратору.** Прежняя редакция объявляла, + что в сырье эксп-22 три клетки боевого редактора (`8e50448cea`, `b065a92dc0`, `53f1a12dff`) + оборваны до 22% длины, и отдавала это оркестратору как незамеченный дефект чужого пака. + Пере-проверка: все три — из АНГЛИЙСКОГО пула (`manifest.json`, ключ `en`), их исходники ~1630 + знаков, выходы 1590/1596/1638 при `finish=stop`, то есть ≈100% исходника. «22%» получалось + только от сравнения с медианой пула, где преобладают zh-единицы с трёхкратным расширением. + По собственному критерию этого отчёта («<70% медианы СВОЕЙ единицы») таких клеток ноль. + Оркестратору: пункт отозван, дефекта эксп-22 здесь нет. ⚠ Прежняя редакция писала, что ложный + пункт «уже ушёл оркестратору» — носителя у этого не было: отчёт не отслеживался git, в журнале + и D-логе исходного утверждения нет. Носитель появился только сейчас, вместе с отзывом, — запись + в `docs/PROGRESS.md` секции «Полигон» и §15 отчёта эксп-22. +4. **Девиация протокола судейства.** Две сессии сообщили, что СРАВНИВАЛИ варианты между собой + («чтобы точнее процитировать различия»), хотя задание требует оценивать каждый вариант только + против исходника. Смещение от такого сравнения бьёт по арму-одиночке, а в проходе `tier` + одиночка — как раз боевой редактор. Величину смещения по двум сессиям не оценить; факт + объявлен. **В прибор: запрещать не только сравнение, но и чтение вариантов рядом.** +5. **Двусмысленная инструкция про оборванный вариант — относилась к клетке, которой в принятой + конфигурации нет.** Я велел судье «оценивать как есть и не достраивать»; судья прочитал это как + «не штрафовать за обрыв». Клетка, на которой это наблюдалось, попадала в пачку только через + дефект базы R0 (§0) и в исправленном ключе отсутствует; в другом голосе того же прогона судья + пропуск, наоборот, засчитал. Формулировка всё равно неверна и правится («оценивать как есть» и + «не штрафовать за неполноту» — разные вещи), но как НАБЛЮДЕНИЕ пункт снят: воспроизводимого + случая в принятых данных нет. +6. **Реализация скрина применила критерий, которого нет в пре-реге.** Код переиспользовал вердикт + эксп-22 целиком, вместе с ПЕРЕВОДЧЕСКИМ порогом $0.02, — а пак скринит редакторов, и пре-рег + называет только редакторский порог $0.06. Лишний порог снял `gpt-5.6-terra` и `grok-4.5`, то + есть обоих, ради кого пак существует. Снимать критерий после результата — подгонка, поэтому + печатаются ОБА вердикта: пре-рег-критерий как несущий, полный критерий эксп-22 как справка + (§10). Провенанс: фризы `c0dd228` 02:44:28 и `87247e2` 02:45:04, первая покупка **02:45:35** — + запас 31 секунда, а не минута, как читалось. ⚠ «План старше обоих» с диска НЕ доказуемо: план + пака лежит в эфемерном scratchpad и в git не фризовался (`git log --all -- '*plan23*'` пуст), + колонка улик его реестра дописывалась по ходу. Неизменяемый провенанс здесь несут только + фриз-коммиты и запись сессии — то же снижение, что уже сделано в §6. +7. **Я СНЯЛ ЗАМКИ У ЖИВОГО ПОКУПАЮЩЕГО ПРОГОНА.** Увидев в каталоге сырья зависшие `*.lock` и + решив, что они остались от упавшего процесса, я удалил их, пока покупки ещё шли. Атомарный + замок (`money.py:219`, `O_CREAT|O_EXCL`) — единственное, что разводит параллельных покупателей; + именно его отсутствие стоило эксп-22 верхней границы невозвратной пере-оплаты ≤$0.846103 на 176 + клетках (его §-о деньгах). Ущерба не случилось только потому, что второго покупателя в тот + момент не существовало: удача, а не дисциплина. Ни один гейт этого не ловит и поймать не может — + удаление файла руками вне кассы. **В прибор:** снимать замок только тем же процессом, что его + поставил, а «зависший» замок опознавать по живости PID, а не по виду каталога. +8. **Коллизия само-импорта, четырежды.** Модули пака (`roster`, `screen`, `money`, плюс тестовый + файл) грузились по имени и подхватывали одноимённые модули эксп-22 вместо своих. Один случай + прошёл МОЛЧА и исказил замер: селф-тест проверял 11 моделей из 15 и печатал зелёное. Чинено + явной загрузкой по пути (`_load`), но найдено это чтением вывода, а не гейтом. +9. **Фриз фазы A попутно изменил риг ЧУЖОГО пака.** Тот же коммит `c0dd228` тронул + `eval/tenant_panel/material.py` (приколка единиц манифестом) и `money.py` под заголовком про + editor-tier. Зона не нарушена (весь `eval/` — зона полигона) и изменение объявлено в §11, но + код, породивший уже принятые числа эксп-22, изменён задним числом под чужим заголовком. + Гейты эксп-22 после этого пере-снимались; их зелёность после правки — в §14, вопрос 7. +10. **ГОНКА ДВУХ КРУГОВ СУДЕЙСТВА И РАЗБОР В ЕЁ СЕРЕДИНЕ.** Пять единиц прохода `border` + (`13514e13f0`, `15f002335d`, `1ffe99dc43`, `225001778a`, `87dd50b129`) я отдал на пере-суживание + ДВАЖДЫ, не дождавшись первого круга: второй круг записал ответы, я прогнал `--ingest` в 18:36:56, + а первый круг дописал те же самые файлы в 18:38–18:41. В итоге голоса были от одного круга, а + сырьё на диске — от другого, и расходились они по всем пяти единицам. Числа при этом сходились + с отчётом, потому что и отчёт, и гейт читали ГОЛОСА: ни `verify23`, ни `itog23` не сравнивали их + со временем ответов. Нашёл аудит закрытия заказа, не автор и не первая приёмка. + **Как разрешено.** Голоса пере-снят с текущего сырья — правило «сырьё на диске главнее», а не + «то, что уже напечатано»: иначе опубликованное число было бы невоспроизводимо для читателя. + ⚠ Оба набора я видел ДО выбора правила, и это надо знать при чтении: первый круг давал −0.59 + (порог 1.43), второй −0.81 (порог 1.48). Вердикт «ниже порога различимости» одинаков в обоих, а + вот расщепление половин при этом заметно двигается (было −1.44/+0.25, стало −1.62/0.00) — то + есть устойчив здесь только знак и вывод, но не величины по подвыборкам. + **В прибор — сделано:** `verify23` роняет пак, если хоть один ответ судьи новее своего голоса. + Это первый гейт пака, сторожащий не число, а ПОРЯДОК СОБЫТИЙ. +11. **Моя ошибка чтения по ходу.** На n=14 я озвучил «преимущество не различимо», зная, что две + единицы ещё не вернулись. Вывод на неполных данных — та же ошибка, что и вывод на агрегате без + вскрытия единиц. + +⚠ **Поправка фазы Д (чек-лист приёмки №16 п.10): нумерация этой секции была сломана.** На диске +шёл ряд 1–6, 8, 9, 10, 7, 7 — два разных дефекта под номером «7», и ссылка §14 п.7 вела на «§8 п.8», +которого не существовало. Пункты пере-нумерованы по порядку в документе (тексты не тронуты), ссылки +на них поправлены. Причина сбоя видна по содержанию: пункты 7 и 11 дописаны последними, аудитом +закрытия заказа, и вставлены в хвост без сверки с уже занятыми номерами. + +## §9 ЧТО НЕ ПЕРЕНОСИМО + +* **Проход `tier` по-прежнему снят ОДНОЙ модельной семьёй.** Для `border` ограничение снято + внешним судьёй (§12в); тексты `tier` он не читал, и вывод «сильный тир не отличим» подписи вне + Claude не имеет. +* **ОДНОЙ МОДЕЛЬНОЙ СЕМЬЁЙ СНЯТЫ НЕ ТОЛЬКО ЧИСЛА, НО И ВСЯ ПРИЁМКА.** Это признаётся здесь впервые + и это отдельный дефект, а не повтор предыдущего пункта. D39.120 требует на текстовых и + оценочных линзах хотя бы одного опровергателя ДРУГОГО модельного семейства; владелец сверх того + прямо разрешил сессии запустить агентов Fable-5 под ревью. Ни то, ни другое не исполнено: все + судьи, все линии приёмки, все опровергатели и весь аудит закрытия заказа отработали на Opus-5. + То есть монокультура, которую отчёт объявляет ограничением ЗАМЕРА, ровно в той же мере поражает + и МЕХАНИЗМ ПРОВЕРКИ замера — а именно он трижды подписывал числа, оказавшиеся артефактами. + Разрешение владельца на другое семейство лежало неиспользованным всё время работы пака. +* **Внешний контур есть у `border` и НЕТ у `tier`** — ⚠ поправка фазы Д (чек-лист приёмки №16 п.5). + Прежняя редакция этого буллета гласила «все судейские числа сняты одной модельной семьёй, + внешнего судейского контура нет и здесь», то есть прямо против §12в того же отчёта, где подпись + судьи вне Claude по проходу `border` напечатана числами (−1.00 при пороге 3.53, декой 32/32). + Верная форма: для `border` ограничение СНЯТО, для `tier` — стоит в полную силу (первый буллет + этой секции). Отдельная линия — Sol-пакет (а) эксп-22: прогнан владельцем 09.08, но арбитраж не + состоялся (эксп-22 §16: три единицы на контраст, дрейф шкалы ×2.6, знак совпал в 3 парах из 9); + пакет (б) не запускался. Ограничение по `tier` несущее, а не формальное: его вывод — что + перевесы сильного тира НЕ доходят до порога, — есть утверждение о том, чего судья не различил, + и оно тем слабее, чем однороднее судьи. Отдельно: идентичность судей `tier` не персистирована + (§6), поэтому согласие между ними даже внутри семьи посчитать нечем. +* **Декой во всех единицах принятого прохода `tier` посажен из одного и того же арма — R0** + (замер по ключу: 16/16), а R0 стоит вторым армом в каждом контрасте этого прохода. Если само + присутствие испорченного близнеца двигает оценку донора, оно двигает её в одну сторону во всех + трёх контрастах сразу. Прямой улики за или против нет: ни один сохранённый голос не содержит + рассуждения «это копия другой метки». Косвенная — R0 против голого черновика даёт +2.06, а + T1/T2/T3 против него же +1.88 / +2.56 / +2.25, то есть R0 лежит ВНУТРИ разброса панели, грубого + смещения донора не видно. Для прохода `border` донор с этого пере-прогона уравнен: 16 единиц + из R0, 16 из R2. +* **Одна книга, одна пара языков, 16 единиц панели.** Всё, что зависит от свойств текста, обязано + пере-меряться. +* **Вывод про `gemini-3.1-pro` по-прежнему отсутствует** — он не входил в этот пак. +* **`kimi-k3` замерен ОДНОЙ клеткой** по объявленному особому режиму И под своим потолком вывода + (§5): твёрдым остаётся только вердикт по ЦЕНЕ; ни о качестве его прозы, ни об отказном режиме + вендора этот замер не говорит. + +## §10 ФАЗА A — СКРИН СИЛЬНОГО ТИРА + +Пороги взяты у эксп-22 БЕЗ изменения — порог из прошлого пака единственное, что защищает состав +от подгонки, а состав здесь и есть предмет спора. + +``` +модель вердикт ед. $/ед edit размышл причины +gpt-5.6-terra прошёл 4 0.04250 6.6% +grok-4.5 прошёл 4 0.05481 69.5% +glm-5.2 прошёл 4 0.01792 0.0% +kimi-k3 ПРОВАЛ 1 0.08041 99.9% Г5 цена editor > $0.06 · Г6 выход пуст +``` + +**Справка — полный критерий эксп-22** (с переводческим порогом $0.02, которого в пре-реге этого +пака нет): `gpt-5.6-terra` ПРОВАЛ ($0.03668), `grok-4.5` ПРОВАЛ ($0.03445), `glm-5.2` прошёл +($0.01396), `kimi-k3` ПРОВАЛ. Разница между двумя колонками — целиком §8 п.6. + +Цены — с ВЕНДОР-страниц 09.08: `gpt-5.6-terra` $2.00/$0.20/$12.00 · `kimi-k3` $3.00/$0.30/$15.00 · +`grok-4.5` $2.00/$0.30/$6.00 · `glm-5.2` $1.40/$0.26/$4.40. Слаги — живой листинг `/models` того же +дня. Попутно закрыта дыра эксп-22: его цена `gpt-5.6-luna` не подтверждалась живьём (Cloudflare +403) и ехала из прошлых паков — страница открылась и подтвердила её ровно. + +## §11 МАТЕРИАЛ + +16 НОВЫХ единиц из 16 разных глав (22, 25–30, 32–36, 39–42), знаков 2077…2251, медиана 2141, +макс. попарная близость 0.091. Пересечений с эксп-22 — ноль: его главы исключены ЦЕЛИКОМ, иначе +«новые» единицы были бы соседними абзацами тех же глав и делили бы с ними сцену и лексику, а +прирост мощности оказался бы мнимым. Гейт прав пересчитан по СВОЕМУ пулу (верхний дециль по +плотности эротических маркеров), а не перенесён константой «2» из эксп-22. + +⚠ **Побочный эффект докачки глав, который пришлось чинить.** Материал эксп-22 отбирался «из +середины распределения длин» по содержимому диска; докачка с 18 глав до 42 СДВИНУЛА его единицы +(главы 3–18 → 2,5,9…41), то есть его гейты стали бы считать по материалу, который никто не +покупал. Единицы эксп-22 приколоты к его манифесту отбора; все четыре его гейта после этого +зелёные. Правило отбора теперь применяется только когда манифеста ещё нет. + +## §12 КОНТРОЛИ РИГА (читать ДО боевых чисел) + +``` +проход ДЕКОЙ (порча против чистого) ПОЛ (истинная разница ноль) БЛИЗНЕЦ +tier −3.94 поймано 16/16 ✔ −0.62 [−1.38, +0.06] порог 1.02 пара CTRLfloorA≡T1, ноль +border −4.88 поймано 32/32 ✔ −1.00 [−2.06, +0.06] порог 1.48 побайтных дублей нет +``` +**Декой** — посаженная в чистый текст деградация — обязателен в каждом проходе: судейская сессия, +не поймавшая его отрицательным знаком, аннулируется целиком (так эксп-22 списал 6 агент-запусков). +Пол ЧЕСТЕН: ноль внутри интервала, и порог различимости берётся ИЗ НЕГО — это решающее правило +пака, поэтому §1 читается против 1.02, а §3 — против 1.48. + +⚠ **Прежняя редакция этой секции печатала пороги 1.65 и 1.58 — они из ПЕРВОГО, аннулированного +прогона** (пере-снято: `annulled-run1` даёт ровно 1.65 и 1.58). Секция объявлена «читать ДО боевых +чисел» и задаёт порог различимости, так что отчёт выдавал читателю два разных порога на одну +величину: §1 читался против 1.02, а §12 печатал 1.65 — и при напечатанном 1.58 тогдашний вывод §3 +в порог не проходил вовсе, то есть документ противоречил сам себе по несущему вердикту. +Средние −0.29 и −0.42 из той же строки не +воспроизводятся НИ ИЗ ОДНОГО набора голосов на диске и при n=16 недостижимы арифметически (среднее +целых ошибок кратно 1/16); откуда они взялись — не установлено, промежуточные состояния разбора не +сохранились. Гейт этого не ловил: он сверял лишь наличие слова «пол». Починено — `verify23.py` +теперь сторожит и величину пола, и порог, и величину декоя числом. + +⚠ **«БЛИЗНЕЦ» в проходе `tier` — не контроль.** Единственная побайтно совпадающая пара пачки это +`CTRLfloorA` и `T1`, в 16 единицах из 16, и совпадают они по построению: первой половиной шумового +пола `panel.floor_pair` отдаёт саму боевую клетку `T1`, докупается только вторая генерация. +«Перевес ровно ноль в 16/16» означает лишь, что один и тот же текст под двумя метками получил одну +оценку внутри одного ответа судьи; воспроизводимость оценки из этого НЕ следует, и как контроль +эта строка пуста. Замерено и смежное: связи между оценкой пары и боевым перевесом нет (корр. +−0.05). Против такого переиспользования прямо предупреждает комментарий в риге эксп-22 — я его +воспроизвёл. **В прибор: половиной пола брать отдельную генерацию, не клетку панели.** + +## §12б ПОРОГ БЫЛ ЗАНИЖЕН НА 19% — РАЗЛОЖЕНИЕ ДИСПЕРСИИ + +Найдено после сдачи, вопросом владельца «ты не преуменьшаешь?». Шумовой пол этого рига меряет +пару, у которой ОБЕ половины судит один и тот же судья, — значит межсудейская разница в него не +попадает по построению, а боевые перевесы её несут. Порог, взятый из такого пола, занижен. + +Разложение дисперсии перевеса по 8 сессиям прохода `border` (n=32): +``` +внутри сессии sd 2.903 +между сессиями sd 1.030 ← в шумовой пол НЕ попадает +se среднего 0.513 наивная → 0.629 с учётом межсессионной (×1.23) +ПОРОГ 1.48 напечатанный → 1.76 честный +``` +Пересчёт вердиктов по честному порогу — **ни один не двигается**, и все, кроме одного, крепнут: +``` +R2 glm-5 vs R0 −0.81 · 1.48 → 1.76 · ниже порога +T1 glm-5.2 −0.19 · 1.02 → 1.53 · ниже порога +T2 gpt-5.6-terra +0.50 · 1.02 → 1.68 · ниже порога +T3 grok-4.5 +0.19 · 1.02 → 1.55 · ниже порога +R0 vs F КОНТРОЛЬ +2.06 · 1.02 → 1.94 · РАЗЛИЧИМ +``` +**Почему занижение порога не породило ложных выводов.** Заниженный порог делает вывод «различимо» +слишком лёгким. Все несущие выводы пака отрицательные — заниженный порог им не помогает, а мешает; +единственное положительное утверждение это позитивный контроль, и он проходит с запасом даже по +строгому порогу. Направление риска здесь — что пак ПРОЗЕВАЛ реальную разницу, а не выдумал её. + +⚠ **Для прохода `tier` межсессионная компонента ЗАИМСТВОВАНА с `border`.** Оценить её на своих +данных нельзя: идентичность судей `tier` не персистирована, во всех голосах стоит `judge='?'` — +прямое следствие дефекта учёта (§6). Задним числом не восстановить. + +⚠ **Эмпирическая проверка этой поправки — §12а.** Прямая репликация новым жребием судей дала +расхождение средних 0.19 при ожидании ≈1.0, то есть поправка скорее консервативна. Уточнять её +по двум прогонам нельзя — это оценка по n=2. + +**В прибор:** шумовой пол обязан браться парой, чьи половины судят РАЗНЫЕ сессии. Тогда он ловит +обе компоненты, и порог не требует ручной поправки. + +## §12в ВНЕШНИЙ СУДЬЯ ВНЕ СЕМЬИ Claude — ПРОХОД `border` ЗАКРЫТ ТРЕТЬЕЙ ПОДПИСЬЮ + +Заказано владельцем после того, как он указал: все выводы пака сняты агентами одной модельной +семьи, и мнения судьи другой семьи о КАЧЕСТВЕ этих переводов никто не спрашивал. Замечание было +верным — предыдущая попытка внешнего контура (Sol-пакет эксп-22) судила тексты ЧУЖОГО пака и +развалилась по устройству (эксп-22 §16). + +**Что сделано.** Внешнему судье (не-Claude, локально, руками владельца, 4 сессии по 8 единиц) +отданы ТЕ ЖЕ файлы заданий `aj-border-tasks/*.txt`, которые судили агенты пака: тот же исходник, +те же армы под теми же слепыми метками, тот же декой и тот же шумовой пол в пачке. Поэтому его счёт +сопоставим с моим текст-в-текст. Пере-снимается командой +`eval/.venv/bin/python eval/editor_tier/solscore.py`; ответы в `~/books/editor-tier/sol-border/`. + +``` +судья уровень перевес R2 пол порог вердикт +внешний, не-Claude 11.0 −1.00 −1.88 3.53 ниже порога различимости +мой прогон A 7.0 −0.81 −1.00 1.48 ниже порога различимости +мой прогон B 7.0 −0.62 −1.00 1.77 ниже порога различимости +``` +Принято 32 из 32 единиц, отказов ноль, ДЕКОЙ пойман **32/32**. + +⇒ **Ограничение «все судейские числа сняты ОДНОЙ модельной семьёй» для прохода `border` снято — +но снято СЛАБО, и это надо читать вместе с числами.** Вывод «граница `glm-5` не разрешена, эффект +ниже порога различимости» подпись судьи вне Claude имеет. ⚠ Поправка фазы Д, найденная приёмкой +другого модельного семейства: **у внешнего прибора шумовой пол sd 5.05 и порог 3.53**, а боевой +контраст прохода −0.81. Значит внешний судья не мог противоречить нулевому вердикту НИ ПРИ КАКОМ +исходе, кроме эффекта вчетверо больше замеренного — он и позитивный контроль масштаба +2 не +развёл бы. Подпись под отрицательным выводом на таком приборе почти автоматична, и «ограничение +снято» звучит сильнее, чем даёт замер. Честная форма: **внешний контур подтвердил, что эффекта +РАЗМЕРА, ВИДИМОГО ЕМУ, здесь нет; про эффекты меньше 3.5 ошибок он не высказывался.** +⇒ И следствие вперёд: та же оговорка накроет внешнюю подпись прохода `tier` (заказ фазы Д, п.Д2) — +там боевые перевесы 0.19…0.50, то есть на порядок ниже разрешения внешнего прибора. Объявляется +ЗАРАНЕЕ, до прогона, а не после: подпись будет означать «внешний судья тоже не увидел», а не +«внешний судья подтвердил равенство». +Для прохода `tier` внешней подписи по-прежнему нет: судья его текстов не читал. + +**Замер межсемейного расхождения — побочная находка, для рига ценнее самого вердикта.** +``` +согласие по единицам (корреляция перевесов) знак совпал +внешний ↔ мой A +0.317 22/32 +внешний ↔ мой B +0.334 24/32 +мой A ↔ мой B +0.627 26/32 +``` +Расхождение МЕЖДУ семьями примерно вдвое больше, чем ВНУТРИ семьи. И прибор другой семьи заметно +грубее: его шумовой пол sd 5.05 против 2.12 у моих, отсюда порог 3.53 против 1.48 — он честен, но +разрешает вдвое хуже. Практическое следствие: **для вопроса «есть ли разница вообще» судьи разных +семей взаимозаменяемы; для контраста тоньше двух ошибок на единицу смешивать их в один пул без +нормировки нельзя** — различаются и шкала, и разрешение. + +## §12а РЕПЛИКАЦИЯ ПРОХОДА `border` НОВЫМ ЖРЕБИЕМ СУДЕЙ + +Заказана владельцем как проверка методики: «давай перегоним и посмотрим». Меняется РОВНО один +фактор — состав судейских сессий. Ключ, слепые метки, посадка декоя, тексты и задания те же файлы, +не пере-выпускались. Правило публикации записано ДО запуска: прогон A остаётся опубликованным, +B — проверка; при расхождении вердиктов находкой считается само расхождение, а не удобный прогон; +складывать A и B в один набор запрещено — это разные жребии. + +``` + прогон A прогон B +перевес R2 vs R0 −0.81 −0.62 +95% ДИ [−1.81,+0.28] [−1.78,+0.53] +p (Холм) 0.1601 0.3339 +шумовой пол −1.00 −1.00 +порог прохода 1.48 1.77 +декой −4.88, 32/32 −4.50, 32/32 +уровень (медиана) 7.5 6.8 +``` +**Вердикт «ниже порога различимости» воспроизвёлся.** Расхождение средних |A−B| = **0.19** при +пред-объявленном ожидании ≈1.0 — то есть прибор на уровне ВЫВОДА заметно устойчивее, чем следовало +из моей же оценки межсудейской компоненты. Корреляция перевесов по единицам +0.63, знак совпал на +26 единицах из 32. + +⚠ **Что при этом НЕ воспроизводится — отдельные единицы.** Разность перевесов по-единично имеет +sd 2.78 при среднем +0.19: на ОТДЕЛЬНОЙ главе армы не упорядочены, новый жребий судей переставит +их местами примерно в каждой пятой. Устойчиво только среднее по единицам. Риг об этом +предупреждает в собственном выводе, теперь это подпёрто прямым замером, а не рассуждением. + +**Цена:** 8 агент-сессий, по паку стало 58 из капа 60. Покупок не потребовалось. Оба прогона лежат +рядом: `~/books/editor-tier/replication-runA-*` (опубликованный) и `replication-runB-*`. + +## §13 ДИСПОЗИЦИИ + +⚠ Прежняя редакция этой секции несла выводы АННУЛИРОВАННОГО прогона («тир оказался лучше», +«D39.22 закрыт в пользу `glm-5`») — прямо против §1 и §3 того же отчёта, который сам перечисляет +эти выводы как снятые. Секция, из которой переносится закрытие строк реестра, была последней, где +я не сверился с собственными числами. + +* **Строка про сильный тир (дыра эксп-22 §13а)** — **ЗАКРЫТА**: тир проверен, различимого выигрыша + над боевым редактором нет, а стоит он в 2–6 раз дороже (§1, §4). Платить не за что; рекомендация + эксп-22 (`deepseek-v4-pro`) остаётся и теперь стоит на панели, включающей сильный тир OpenAI, + xAI и Z.AI. Решения владельца по цене НЕ требует — требовало бы при обратном знаке. +* **Резерв D39.22 (`glm-5`)** — **ЗАКРЫТИЕ НЕ ОБЪЯВЛЯЮ, подаю ПРЕДЛОЖЕНИЕ.** Это ратифицированное + решение владельца, и права закрывать его полигон-сессии не давалось; эксп-22 от закрытия + воздержался. Замер: на 32 единицах с полным набором контролей `glm-5` от боевого редактора **не + отличим** (−0.81 при пороге 1.48, §3), то есть основания «он значимо хуже», на которое сессия + дважды ссылалась, НЕТ. Предложение: снять резерв как контраст, требующий отдельного разбора, и + вести `glm-5` по общему правилу ничьей D39.117 — а оно оставляет `deepseek-v4-pro`, поскольку + тот дешевле. Отдельно к решению: ToS-гейт Z.AI на прод-выбор `glm-5` никуда не делся (§9 эксп-22). +* **Sol (строка 150)** — пакет (а) прогнан владельцем 09.08 и дал отрицательный результат по своей + задаче (эксп-22 §16); пакет (б) НЕ запускать до починки оснастки: та же конструкция, `N_UNITS_B = 3`. + ⚠ **Поправка фазы Д (п.5):** здесь стояло «внешнего судейского контура у обоих паков по-прежнему + нет» — неверно и против §12в. Контур ПОСТРОЕН и сработал, но на заданиях абсолютного рига, а не + на пакетах конструкции Sol, и покрывает он проход `border`. Не покрыты: проход `tier` (задания + `aj-tier-tasks` внешнему судье не отдавались) и оба контраста эксп-22, ради которых пакеты (а)/(б) + и делались. +* **Строка 153 и вторая база Ф3** — не гнались, как и в эксп-22. + +## §14 CONFIRM / DENY — К ПОДПИСИ ВЛАДЕЛЬЦА + +Ниже — всё, что этот пак принял на веру, назвал решением или сделал вне объявленного. Ни один +пункт не считается ратифицированным, пока не подтверждён; пометка «со слов сессии» означает, что +артефакта в репозитории нет и проверить нечем, кроме записи разговора. + +| # | что | статус | +|---|---|---| +| 1 | **Мандата на пак 23 не существует** — промта нет, пак самоназначен, санкционированы только деньги | подтвердить задним числом или отменить | +| 2 | Потолок пака **$4.00** и запас **$5** — со слов сессии, в репо артефакта нет | подтвердить провенанс | +| 3 | **Резерв D39.22** закрывать не мне: это решение владельца (§13) | принять к сведению | +| 4 | Правило «судейская сессия без пойманного декоя аннулируется целиком» живёт только в промте ЧУЖОГО пака; я применил его к себе дважды | ратифицировать как норму или отменить | +| 5 | Потолок ФАЗЫ поднимался дважды в ходе пака, резерв ушёл не на объявленное назначение, пинга не было (§6); пакетный потолок не пробит | норма или запрет | +| 6 | Учёт агент-запусков до этого пере-прогона вёлся мной, а не механизмом (§6); с пере-судейства `border` он персистируется, но задним числом первые 38 восстановить нечем | принять со слов или считать неучтённым | +| 7 | Фриз фазы A пака 23 попутно изменил риг эксп-22 (§8 п.9). Его гейты пере-сняты сейчас — `verify22.py` и `itog22.py` зелёные, код возврата 0 | к сведению; вопрос только в том, нормально ли править чужой пак под своим заголовком | +| 8 | Провенанс цен OpenAI и xAI — снимка вендор-страницы нет, только живой листинг `/models` и комментарий в коде | принять или требовать снимок | +| 9 | Пере-судейство прохода `border` — 12 агент-сессий, денег $0; всего по паку **58 сессий** при капе 60. ⚠ Поправка фазы Д (п.4): здесь стояло «50» и «теперь считает механизм» — первое не считало репликацию (§12а), второе прямо опровергнуто §6: на диске 20 записей против 58 фактических, СЧЁТЧИК КАПА НЕ РАБОТАЕТ. Персистирована только раскладка судей по единицам | санкция задним числом | + +--- + +# ФАЗА Д — ДОВОДКА ЭКСП-22/23 (заказ владельца 10.08) + +Исполнение `docs/POLYGON_EXP2223_REDO_SESSION_PROMPT.md`. Это НЕ новый эксперимент: выводы паков +22 и 23 признаны владельцем непоказательными **по постановке** и заморожены до этой фазы. Зона — +`eval/dovodka/` + правки рига паков 22/23 + эти секции. + +## Д0 ПРЕ-РЕГ (зафризен своим коммитом ДО первой покупки) + +### Д0.1 Гипотезы владельца — ДОСЛОВНО из заказа + +> **H-1 «Проблема в черновике»**: flash даёт полумусор, полный перепис нужен именно поэтому; +> связка «качественный черновик + ТОЧЕЧНЫЙ редактор» ≥ боевой связки по качеству при сравнимой цене. +> +> **H-2а «Флаговый edit-контур»**: детерминированные флаги (батарея + канон-гейт) → точечный +> фиксер — не хуже full-regen по судье, дешевле по деньгам, НЕ ломает канон. +> +> **H-2б «Смысловой edit-контур» (главная ставка владельца, 10.08)**: LLM-критик, читающий и +> ПОНИМАЮЩИЙ текст («найди всё проблемное») → точечный фиксер — дороже (поиск ≈2.14× переписа), +> но качество выше и full-regen, и флагового контура: «флагами всё не отследишь». Цена заложена +> в смету сознательно — гипотеза о КАЧЕСТВЕ, не об экономии. +> +> **H-3 «en: перепис не нужен»**: на en→ru редактору остаются только синк глоссария и точечные правки. +> +> **H-4 «dspro-китайскость»**: перевес dspro в редакторской роли — свойство пары zh→ru; на en/ja +> порядок жильцов может смениться (dspro — китайская модель, книга — китайская). + +### Д0.2 ⛔ ДВА АРМА ЗАВЕДЕНЫ ПРИЁМКОЙ, А НЕ АВТОРОМ — и без них две гипотезы не мерились + +Первая редакция плана этой фазы **не отвечала на H-1 и H-4**, и нашла это не сессия, а приёмка +другого модельного семейства (Fable-5, разрешение владельца D39.120 наконец использовано): + +* **H-1** говорит о связке «КАЧЕСТВЕННЫЙ черновик + точечный редактор». Ни один арм плана такой + связки не содержал: `A1`/`A3` ставят фиксер на flash-черновик — то есть на полумусор по самой + формулировке гипотезы, — а `A2` есть однопроходка вовсе без редактора. Гипотеза «отвечалась» бы + интерполяцией между армами, где взаимодействие «фиксер × качество базы» не меряется вовсе. + ⇒ заведён арм **A6 = dspro-черновик эксп-22 + оба контура**. Черновики уже куплены ($0 за базу). +* **H-4** говорит о смене ПОРЯДКА жильцов на другой паре. Порядок проверяется только панелью из + НЕСКОЛЬКИХ редакторов, а на en-оси редактор стоял один — `deepseek-v4-pro`. Фаза сказала бы + «сколько покупает редактура на каждой паре» (это H-3), но не «остаётся ли dspro лучшим». + ⇒ заведён арм **E6 = второй редактор `glm-5` на en поверх той же базы**. + +Стоимость починки $0.48; потолок фазы поднят владельцем $4.00 → **$4.50** ровно под неё. + +### Д0.3 Мощность каждой оси — ДО покупок (`eval/dovodka/power.py`) + +``` +ось n k sd MDE ЦЕЛЬ потолок p СТАТУС +Д4 edit-контур zh 32 5 2.12 1.29 1.50 0.0000 НЕСУЩАЯ +Д3 en→ru несущая 16 5 2.12 1.83 2.00 0.0002 НЕСУЩАЯ +Д6 ja→ru разведка 9 3 2.90 3.33 2.00 0.0117 ⛔ ОПИСАТЕЛЬНАЯ +Д1 gemini добивка 16 1 2.12 1.83 2.00 0.0000 НЕСУЩАЯ +``` + +**ЦЕЛЬ — искомый эффект, объявленный ДО денег.** Без неё MDE не решает ничего: печатать «MDE 1.83» +и не сказать, меньше он искомого или больше, — ровно та форма, за которую погорела en-ось эксп-22 +(там при n=6 и k=6 потолок p был 0.1875, то есть значимость недостижима ПО ПОСТРОЕНИЮ, и выяснилось +это после денег). Ось объявляется описательной, если MDE больше цели ЛИБО значимость недостижима. + +⚠ **Следствие, которое надо знать заранее:** при n=12 (минимум промта) ось Д1 имела бы MDE 2.11 +против цели 2.00 и была бы ОПИСАТЕЛЬНОЙ. Несущей её делает решение владельца взять полные 16. + +⚠ **Прайор шума — не свой пол.** Своего пола у будущего прохода нет по построению; берётся худший +из замеренных на той же структуре армов (`border`, sd 2.12). **Решает СВОЙ пол**, и если он выйдет +хуже прайора, ось пере-классифицируется в описательную ТОГДА ЖЕ — до чтения боевых перевесов. + +### Д0.4 ⛔ ЗАПАС ЭКВИВАЛЕНТНОСТИ для H-2а — иначе «не хуже» доказать нечем + +H-2а утверждает, что флаговый контур **НЕ ХУЖЕ** полного переписа. Риг устроен так, что +«ниже порога различимости» **не означает «равны»** — это записано во всех отчётах и здесь не +меняется. Значит подтвердить H-2а обычным контрастом невозможно в принципе: отсутствие значимой +разницы не есть равенство, и CONFIRM был бы сделан формулировкой, а не числом. + +**Пред-объявляю правило non-inferiority:** `A1` (и `A3`) признаётся «не хуже» `A0`, если **нижняя +граница 95% ДИ контраста выше −1.50** — той же величины, что объявлена целью оси Д4 (треть +собственного эффекта редактора +4.44 в эксп-22 Ф3). Три исхода и все три названы заранее: + +* нижняя граница > −1.50 **и** верхняя < +1.50 → **НЕ ХУЖЕ** (H-2а подтверждена по качеству); +* верхняя граница < −1.50 → **ХУЖЕ** (H-2а опровергнута); +* интервал накрывает −1.50 → **НЕ УСТАНОВЛЕНО** при данном n, и это честный третий исход, а не + провал: мощность объявлена, доборы обсуждаются с владельцем. + +### Д0.5 Оси, армы и семейства контрастов (впечатываются в ключ ДО первого ответа) + +**Д4 — edit-контур zh, 32 единицы (16 эксп-22 + 16 эксп-23), база одна и замороженная:** + +``` +A0 боевой full-regen (deepseek-v4-pro поверх якорного черновика) КУПЛЕН, $0 +A1 черновик + детерминированные флаги (батарея + канон-гейт) → фиксер «ВМЕСТО редактора» +A2 dspro-однопроходка УРАВНЕННОГО мандата (строка 153 бэклога) без редактора вовсе +A3 черновик + СМЫСЛОВОЙ LLM-критик → фиксер ← СТАВКА ВЛАДЕЛЬЦА H-2б «ВМЕСТО редактора» +A4 A0 + канон-фиксер ПОСЛЕ «ПОСЛЕ редактора» +A6 dspro-ЧЕРНОВИК + оба контура ← носитель H-1, база куплена эксп-22 +семейство: A1/A0 · A2/A0 · A3/A0 · A4/A0 · A6/A0 Холм по пяти +``` +«До редактора» не мерится сознательно: перепис затирает любую починку. + +**Д3 — en→ru, 16 единиц Кристоффа:** боевая связка · dspro-однопроходка уравненного мандата · +черновик + оба контура · **E6 второй редактор `glm-5`** · контроль `E0 vs D0`. Холм по пяти. + +**Д6 — ja→ru, 9 единиц:** связка · однопроходка · контроль. **Статус — РАЗВЕДКА, объявлен здесь.** + +### Д0.6 Нормы рига, принятые этой фазой (каждая куплена дорого прошлым паком) + +* **Декой в каждой пачке**; сессия, не поймавшая его отрицательным знаком, аннулируется целиком. +* **Шумовой пол — парой, чьи половины судят РАЗНЫЕ сессии.** Обе половины — ОТДЕЛЬНЫЕ генерации; + боевая клетка половиной пола не берётся (в эксп-23 `CTRLfloorA ≡ T1` в 16/16, и контроль был + пуст). Порог берётся из своего пола без ручной поправки: пол, судимый разными сессиями, ловит + и межсессионную компоненту, которая в эксп-23 занижала порог на 19%. +* **Все армы единицы — в одном задании одной сессии** (урок Sol §16 эксп-22, подтверждён с двух + сторон: нарушив — замер разваливается, соблюдя — работает на том же судье). +* **Судейские сессии регистрируются ДО запуска** (`eval/dovodka/runs.py`, кап 80, атомарный + `O_CREAT|O_EXCL`-замок с проверкой живости PID, селфтест 16/16). Гонка сессий по одному токену + механически невозможна — в эксп-23 она стоила расхождения голосов и сырья на 5 единицах из 32. +* **Клетка с `finish=length` в судейскую пачку НЕ допускается** — ни боевым армом, ни половиной + пола. Класс замерен: в эксп-22 таких судимых слотов семь, две из них в шумовом полу обеих фаз, + одна в базе всех контрастов (§15 эксп-22). +* **Позиционный наклон** замеряется, вычитается и сторожится гейтом числом. +* **Донор декоя уравнен по армам**; судье запрещено сравнивать варианты между собой и читать их рядом. +* **Пар-промпты проходят механический гейт** (`eval/dovodka/promptdiff.py`): всё вне объявленных + пар-специфичных секций обязано совпадать с боевым zh побайтно, мандатные оговорки не теряются, + а каждое законное расхождение объявлено с причиной. Урок эксп-19 (арм конфаундирован неполным + зеркалом) закрыт механизмом, а не обещанием. +* **$0-детекторы получают свой контроль наравне с судьями.** Норма заведена этой фазой после того, + как приёмка замерила ложноположительную частоту канон-классификатора: 82.5% на случайных окнах + при наблюдённых 79% — прибор был слабее нулевой модели. **Всякий классификатор, чьё число идёт + в отчёт, обязан печатать рядом свою частоту срабатывания на случайном входе.** + +### Д0.7 ⚠ Что внешняя подпись может и чего не может — объявляется ДО прогона + +Внешний судья вне семьи Claude имеет шумовой пол sd 5.05 и порог различимости 3.53 (§12в). Это +значит, что под ОТРИЦАТЕЛЬНЫМ выводом его подпись почти автоматична: возразить он мог бы только +эффекту вчетверо больше замеренного. Поэтому заранее: + +* для прохода `tier` (перевесы 0.19…0.50) внешняя подпись будет означать «внешний судья тоже не + увидел», а НЕ «внешний судья подтвердил равенство»; +* для несущего вердикта Д4 по H-2б, где ставка — тонкий выигрыш, внешний контур не добавит и не + отнимет; его ценность — поймать КАТАСТРОФУ, которой внутрисемейные судьи не заметили. + +Это ограничение прибора, а не отговорка: сказано до того, как числа получены. + +### Д0.8 Смета по ЗАМЕРЕННЫМ ценам и фазовые потолки (`eval/dovodka/plan.py`) + +``` +ФД-A Д4 edit-контур zh (A1·A2·A3·A4, n=32 + свой пол) 0.877 +ФД-B Д3 en→ru несущая 0.556 +ФД-C Д6 ja→ru разведка (n=9) 0.154 +ФД-D Д7 самооценка 0.050 +ФД-F H-1: A6, dspro-черновик + оба контура (n=16) 0.280 +ФД-G H-4: E6, второй редактор glm-5 на en (n=16) 0.200 +ФД-E Д1 добивка gemini, 16 клеток 2.352 + ИТОГО 4.470 / 4.50 · резерв 0.030 +``` +Цены — медианы по СЫРЬЮ уже купленных клеток обоих паков, не по прайсу. Прайс DeepSeek пере-снят +живьём 10.08 и **не изменился** (flash $0.14/$0.28, pro $0.435/$0.87); вендорская сноска о +готовящемся значительном повышении на странице присутствует и учтена оговоркой, а не числом. + +⚠ **Резерв $0.03 — это 0.7% пакета, и сессия объявила это риском ДО покупок.** Ре-гены эхо-мины и +ретраи в паках 22/23 съедали до +16% сверх сметы фазы. Честным потолком без стопов посреди фазы +сессия называла $4.80; владелец выбрал $4.50. **Срабатывание проекционного гарда = СТОП и вопрос +владельцу**, а не сокращение оси решением сессии. + +### Д0.9 Прогнозы (калибруют удивление; совпадение НЕ цель) + +1. **A3 (смысловой критик) НЕ побьёт A0 различимо.** Основание: все замеренные различия между + близкими редакторскими контурами лежат в 0.2–2.5, а MDE оси 1.29. Прогноз конкретен: перевес + A3/A0 ляжет в полосу −1.0…+1.0. **Если владелец прав и ставка сыграет, я ошибусь — и это + лучший исход фазы.** +2. **A4 (канон-фиксер ПОСЛЕ переписа) даст лучшее покрытие канона из всей панели**, не двигая + судейский перевес: он чинит ось, по которой боевой редактор худший в эксп-22 (0.882). +3. **A1 (флаги) окажется «не хуже» по правилу Д0.4, но дешевле в разы.** Основание: флаговый + контур эксп-20 чинил по указанному месту за ~$0.0002. +4. **H-1 подтвердится частично:** A6 (dspro-черновик + контур) побьёт A1/A3 на flash-черновике, + но не побьёт A0. Основание — находка эксп-22 §13: редактор работает компрессором различий + черновика (разброс 7.25 → 2.12). +5. **На en контроль редактора снова окажется около нуля** (эксп-22: +0.00 при n=6), но теперь при + n=16 и объявленной цели 2.00 это будет замер, а не отсутствие мощности. +6. **`gemini-3.1-pro-preview` не отдаст 16 клеток с первой волны.** Прогноз: 503-серии повторятся, + потребуется ≥2 суток окна; собрано будет 12–15 из 16. + +### Д0.10 Чего эта фаза НЕ меряет + +Топология «черновик → редактор» (решена эксп-21) · банк как фактор · выбор жильца черновой роли +(решён эксп-22) · качество прозы `gemini` как продуктовый выбор — при $221 за книгу против $8 у +боевого он невозможен ни при каком исходе, меряется только гипотеза «аутлаер прозы» эксп-04. diff --git a/eval/dovodka/canon.py b/eval/dovodka/canon.py new file mode 100644 index 00000000..bcb6fdb2 --- /dev/null +++ b/eval/dovodka/canon.py @@ -0,0 +1,311 @@ +#!/usr/bin/env python3 +"""Д5 — КАНОН-ВСКРЫТИЕ: где именно боевой редактор теряет покрытие банка. $0. + +Политика владельца 10.08, зафиксирована словом: «Банк существует, чтобы термины единообразно +переводились. Всё. Художественность текста — это отдельный вопрос.» ⇒ потеря канона = дефект +БЕЗУСЛОВНО, судейских опросов об «уместности замены» не бывает. Задача этого файла — не спорить +о том, дефект ли это, а дать фиксеру МЕХАНИЗМ: перечислить места потери и назвать класс каждой. + +Замер идёт на купленном сырье эксп-22, денег не стоит и ничего не покупает. + +ДВЕ РАЗНЫЕ ВЕЛИЧИНЫ, и путать их нельзя: + + 1. ПОКРЫТИЕ — доля канонной формы от числа упоминаний В ИСХОДНИКЕ (метрика `bank.coverage`). + Именно её печатали эксп-21/22. Она падает и когда термин передан ДРУГИМ переводом, и когда + он заменён местоимением — а второе есть норма русской прозы там, где китайская повторяет имя. + 2. ЕДИНООБРАЗИЕ — то, ради чего банк существует по слову владельца: один термин передаётся + ОДНОЙ формой. Меряется подменой РОДОВОГО слова при неизменном якоре («дом Цинь» вместо + «род Цинь»), решается счётом по закрытому списку родовых и подпирается цитатой. + +⚠ ЧЕГО ЗДЕСЬ БОЛЬШЕ НЕТ, И ПОЧЕМУ. Первая редакция классифицировала КАЖДОЕ место потери на +«исчез / парафраз / другой перевод» по выровненному окну редактуры. Адверсариальное ревью её +сняло, и правильно: (а) выравнивание не работает — окно НИ В ОДНОМ из 24 мест не содержало +канона, хотя в полном тексте редактуры он стоит 8–18 раз; (б) класс «парафраз» присваивался +регексом, который срабатывает на 84% ПРОИЗВОЛЬНЫХ окон того же текста, то есть наблюдённые 79% +были НИЖЕ нулевой модели. Вывод «массовый класс — парафраз» на таком приборе не стоит, и он снят. +Список мест остался как СЫРЬЁ для фиксера (где искать), но классом он больше не размечается и +числом по классам не подводится. + +Запуск: eval/.venv/bin/python eval/dovodka/canon.py [--full] [--term <иероглиф>] +""" +from __future__ import annotations + +import difflib +import importlib.util +import json +import re +import sys +from pathlib import Path + +REPO = Path("/home/ubuntu/projects/textmachine") +OUT = Path.home() / "books" / "dovodka" +for p in ("tenant_panel", "role_topology"): + sys.path.insert(0, str(REPO / "eval" / p)) + + +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 + + +PAN = _load("panel22", REPO / "eval" / "tenant_panel" / "panel.py") +BANK = _load("bank22", REPO / "eval" / "tenant_panel" / "bank.py") +ED = _load("editors22", REPO / "eval" / "tenant_panel" / "editors.py") +MAT = PAN.M # material.py эксп-22 + +SENT = re.compile(r"[^.!?…»\n]+[.!?…»]*\s*") + +# ⚠ ЗАКРЫТЫЙ СПИСОК РОДОВЫХ СЛОВ — данные КНИГИ, а не движка. Полигонный замер вправе знать +# лексику своего материала (CLAUDE.md: «тест-данные пары легитимны»); в общий пар-слой это +# не уходит. Классы заданы по канонам банка: то, чем в русском переводе называют 家 (клан/род). +GENERICS = { + "род": ("род", "дом", "семья", "семейство", "клан", "фамилия", "поместье"), +} + + +def GENERIC_CLASS(mod: str) -> tuple[str, ...]: # noqa: N802 + for stem, cls in GENERICS.items(): + if mod.lower().startswith(stem[:3]) or mod.lower() in cls: + return cls + return () + + +def sents(t: str) -> list[str]: + return [s for s in SENT.findall(t or "") if s.strip()] + + +def best_match(s: str, edit_s: list[str], i: int, n_draft: int) -> int: + """Индекс предложения редактуры, лучше всего отвечающего предложению черновика. + + ⚠ Первая редакция выравнивала `difflib`-опкодами по СПИСКУ предложений. На полном переписе это + не работает: почти всё уходит в один опкод `replace` на пол-текста, и окно для всех мест + единицы получалось одно и то же (поймано глазами при первом же прогоне — все «исчез» на + единице `41599f30df` показывали одну и ту же реплику). Здесь ищется ближайшее по содержанию + предложение в ПОЛОСЕ вокруг пропорциональной позиции: редактор порядок сохраняет, поэтому + полоса законна, а сходство внутри неё решает. + """ + if not edit_s: + return -1 + c = int(i / max(1, n_draft) * len(edit_s)) + band = max(4, int(0.15 * len(edit_s))) + lo, hi = max(0, c - band), min(len(edit_s), c + band + 1) + key = s.strip() + best, bj = -1.0, c if c < len(edit_s) else len(edit_s) - 1 + for j in range(lo, hi): + r = difflib.SequenceMatcher(None, key, edit_s[j].strip(), autojunk=False).ratio() + if r > best: + best, bj = r, j + return bj + + +def window(edit_s: list[str], j: int) -> str: + if j < 0: + return "" + return " ".join(edit_s[max(0, j - 1):min(len(edit_s), j + 2)]) + + +# ⚠ Классификация ужесточена после ЧТЕНИЯ первых мест (мандат D39.61: вскрыть единицы до вывода). +# Первая редакция звала «другим переводом» ЛЮБОЕ заглавное слово в окне — и записала в этот класс +# `Этот человек`, `Кошмар`, `Перед такой`, то есть соседние слова предложения. Так класс «другой +# перевод» набрал 14 мест из 24 и был бы вписан в отчёт неверным. Правило переписано: +# `другой` — в окне стоит форма, ПОХОЖАЯ на канон (Цин Фэн / Цинь Фен / Qin Feng), но не он: +# это и есть расхождение перевода термина, которое банк обязан устранять; +# `парафраз` — на месте термина референциальная замена (местоимение, эпитет, родовое слово); +# `исчез` — ни того, ни другого: место есть, термина и его следа нет. +# ⚠ Второй элемент ОБЯЗАН начинаться с заглавной. Первая редакция клеила любое следующее слово, и +# список «конкурирующих форм» наполнился мусором вида «Цинь в», «Фэн-лао до», «Тяньянчэне был» — +# то есть склонениями и предлогами, а не переводами. Поймано чтением собственного вывода. +TOKEN = re.compile(r"(? float: + return difflib.SequenceMatcher(None, a.lower(), b.lower(), autojunk=False).ratio() + + +def classify(win: str, rx: str, canon: str) -> tuple[str, str]: + """(класс, улика). Улика печатается рядом с цитатой — вердикт проверяем чтением.""" + if not win: + return "исчез", "" + if re.search(rx, win, re.I): + return "есть", "" + # похожая, но не канонная форма того же термина + near = [(t, _near(t, canon)) for t in TOKEN.findall(win)] + near = [(t, r) for t, r in near if 0.55 <= r < 1.0 and len(t) >= 3] + if near: + t, r = max(near, key=lambda x: x[1]) + return "другой", f"{t} (сходство {r:.2f})" + m = EPITHET.search(win) + if m: + return "парафраз", m.group(0) + return "исчез", "" + + +def main() -> int: + full = "--full" in sys.argv + only = sys.argv[sys.argv.index("--term") + 1] if "--term" in sys.argv else "" + bank = BANK.PR.bank() + units = MAT.units_zh() + # словарь альтернативных написаний: латиница/иная транскрипция того же имени + + rows, per_term, per_class = [], {}, {} + gross_loss = gross_gain = 0 + tot_src = tot_hit_draft = tot_hit_edit = 0 + # ⚠ Клетка R0 с `finish=length` даёт «потерю канона», которая на деле есть ОБРЫВ: термины + # после места обрыва отсутствуют не потому, что редактор их убрал. Такие единицы считаются + # отдельно (эксп-22 §15: класс замерен, на этой панели он покрывает единицу `41599f30df`). + trunc = set() + for u in units: + f = PAN.OUT / f"{PAN.tag_edit(PAN.ANCHOR, u['uid'])}.json" + if f.exists() and json.loads(f.read_text(encoding="utf-8")).get("finish") == "length": + trunc.add(u["uid"]) + for u in units: + uid, src = u["uid"], u["source"] + if uid in trunc: + continue + draft = PAN.draft_text(PAN.ANCHOR, uid) + edit = ED.text_of("R0", uid) if hasattr(ED, "text_of") else PAN.edit_text(PAN.ANCHOR, uid) + if not (draft and edit): + continue + ds, es = sents(draft), sents(edit) + for term, v in bank.items(): + if only and term != only: + continue + n = src.count(term) + if not n: + continue + rx = v["rx"] + hd = min(len(re.findall(rx, draft, re.I)), n) + he = min(len(re.findall(rx, edit, re.I)), n) + tot_src += n + tot_hit_draft += hd + tot_hit_edit += he + if he > hd: + gross_gain += he - hd # редактор канон ДОБАВИЛ + if he >= hd: + continue # редактор канон не терял + gross_loss += hd - he + # места: предложения черновика с канонной формой, чьё окно в редактуре его потеряло + places = [] + for i, s in enumerate(ds): + if not re.search(rx, s, re.I): + continue + j = best_match(s, es, i, len(ds)) + win = window(es, j) + cls, why = classify(win, rx, v["dst"]) + if cls == "есть": + continue + places.append((cls, why, s.strip()[:150], win.strip()[:190])) + keep = places[: (hd - he)] or places + for cls, why, s, w in keep: + rows.append((uid, term, v["dst"], cls, why, s, w)) + per_class[cls] = per_class.get(cls, 0) + 1 + per_term.setdefault(term, {"dst": v["dst"], "n": 0}) + per_term[term]["n"] += 1 + + print("Д5 — КАНОН-ВСКРЫТИЕ БОЕВОГО РЕДАКТОРА (R0 поверх якорного черновика, эксп-22, $0)\n") + print(f"единиц {len(units)} · из них ИСКЛЮЧЕНО по обрыву клетки R0: {len(trunc)} " + f"{sorted(trunc)} · разбирается {len(units) - len(trunc)}") + print(f"терминов в банке {len(bank)} · упоминаний в исходниках разбираемых единиц {tot_src}") + print(f"покрытие ЧЕРНОВИКА {tot_hit_draft}/{tot_src} = {tot_hit_draft / max(1, tot_src):.3f}" + f" · покрытие РЕДАКТУРЫ {tot_hit_edit}/{tot_src} = {tot_hit_edit / max(1, tot_src):.3f}" + f" · САЛЬДО {tot_hit_draft - tot_hit_edit}") + print(f"⚠ САЛЬДО — не потеря. ВАЛОВАЯ потеря {gross_loss} упоминаний, ВАЛОВОЙ прирост " + f"{gross_gain}: в части пар редактор канон ДОБАВИЛ там, где черновик его не держал.\n" + " Прежняя редакция печатала одно число «ПОТЕРЯ» рядом с числом мест и читалась так,\n" + " будто это одно и то же (поймано ревью). Набор фиксера ниже покрывает валовую потерю,\n" + " а не сальдо.") + print(f"\nмест, где канонная форма отсутствует в выровненном окне: {len(rows)}") + print(" ⚠ КЛАССАМИ НЕ РАЗМЕЧАЮТСЯ. Прежняя разбивка «исчез/парафраз/другой» снята ревью:\n" + " выравнивание ненадёжно, а класс «парафраз» слабее нулевой модели. Ниже — сырьё\n" + " для фиксера (где смотреть), а не измерение долей.") + print("\nПО ТЕРМИНАМ (что фиксер обязан уметь возвращать):") + print(f"{'термин':10s}{'канон':30s}{'мест потери':>12s}") + for t, d in sorted(per_term.items(), key=lambda kv: -kv[1]["n"]): + print(f"{t:10s}{d['dst']:30s}{d['n']:12d}") + print("\nМЕСТА (цитатой; окно приблизительное — сверять глазами, это подсказка, не вердикт):") + for uid, term, dst, _cls, _why, s, w in (rows if full else rows[:12]): + print(f"\n {uid} · {term} → «{dst}»") + print(f" черновик: …{s}…") + print(f" редактура: …{w}…") + if not full and len(rows) > 12: + print(f"\n … ещё {len(rows) - 12} мест, целиком — с флагом --full") + # ── ВТОРАЯ МЕТРИКА: ЕДИНООБРАЗИЕ, а не частота ───────────────────────────────────────────── + # ⚠ ПЕРВАЯ РЕДАКЦИЯ ЭТОЙ МЕТРИКИ НЕ РАБОТАЛА И БЫЛА СНЯТА АДВЕРСАРИАЛЬНЫМ РЕВЬЮ. Она искала + # «похожие на канон строки» по расстоянию Левенштейна и набирала склонения («Цао Ина») и чужих + # персонажей с общей фамилией («Цинь Вэньтянь»); настоящие нарушения — «дом Цинь» вместо «род + # Цинь» — не находила вовсе, потому что требовала заглавной буквы у первого слова. Ревьюер + # нашёл их РУКАМИ, и это и есть доказательство, что счётом их взять можно: они отличаются + # РОДОВЫМ СЛОВОМ при том же якоре. + # + # Правило теперь такое: у канона вида «<родовое> <Имя>» якорь — имя, а родовое слово меняется. + # Считаем распределение родовых слов при якоре ПО ЗАКРЫТОМУ СПИСКУ и зовём нарушением всякое, + # чей корень отличается от канонного. Список — данные КНИГИ (полигонный замер, не движок). + print("\n" + "═" * 78) + print("ЕДИНООБРАЗИЕ: каким РОДОВЫМ словом передан термин при том же якоре") + viol, seen_any = [], False + for term, v in bank.items(): + canon = v["dst"] + toks = canon.split() + if len(toks) != 2: + continue + mod, anchor = toks + gen = GENERIC_CLASS(mod) + if not gen: + continue + seen_any = True + rx = re.compile(r"(? <Имя>» в банке нет") + print(f"\nНАРУШЕНИЙ ЕДИНООБРАЗИЯ (конкурирующее родовое при том же якоре): {len(viol)}") + print("⚠ ЧТО ЭТА МЕТРИКА ЛОВИТ И ЧЕГО НЕ ЛОВИТ. Ловит подмену РОДОВОГО слова при неизменном\n" + " якоре — класс, ради которого банк и существует по слову владельца. НЕ ловит: подмену\n" + " самого якоря (транскрипция имени), расхождения у одно-словных канонов и у канонов\n" + " вида «прилагательное + существительное» (金丹 «Золотое ядро» → «Золотая пилюля»:\n" + " найдено ЧТЕНИЕМ на единице `c73f4857e7`, счётом здесь не берётся). Список родовых —\n" + " закрытый и книго-специфичный; его полнота и есть граница метрики.") + + OUT.mkdir(parents=True, exist_ok=True) + (OUT / "canon-dissect.json").write_text(json.dumps( + [dict(uid=a, term=b, dst=c, cls=d, why=e, draft=f, edit=g) + for a, b, c, d, e, f, g in rows], + ensure_ascii=False, indent=1), encoding="utf-8") + print(f"\nперсист → {OUT / 'canon-dissect.json'}") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/eval/dovodka/material_ja.py b/eval/dovodka/material_ja.py new file mode 100644 index 00000000..07d55d68 --- /dev/null +++ b/eval/dovodka/material_ja.py @@ -0,0 +1,173 @@ +#!/usr/bin/env python3 +"""Д6 — МАТЕРИАЛ ПАРЫ ja→ru: гардрейл, гейт прав, отбор единиц. $0. + +Источник: `~/books/isekai_majutsushi_jp.txt` (файл владельца, вне git — правило Р8). Площадка +`novel18.syosetu.com`, то есть срез ЗАВЕДОМО взрослый; аннотация книги сама называет 洗脳 и 暴力. + +⚠⚠ ГАРДРЕЙЛ ПЕРВЫМ, ДО ЛЮБОГО ДРУГОГО ДЕЙСТВИЯ. Гардрейл владельца (вынесен в `eval/README.md`): +**18+ уровень 3 — несовершеннолетние в сексуальном контексте — не обрабатывать и не анализировать +ни в каком виде.** Здесь он реализован механически: чанк, где ПЛОТНО совпадают маркеры возраста и +явные сексуальные маркеры, помечается и материал целиком уходит в СТОП с пингом владельцу. Ни один +такой чанк не печатается и дальше по конвейеру не идёт — печатается только его номер и счёт +маркеров. Детектор существует, чтобы ИСКЛЮЧАТЬ, и его срабатывание — не повод читать внимательнее. + +Дальше — та же лестница, что у эксп-22 §0.2, и по тем же причинам: + 1. гейт ПРАВ (маршрутизация по договорам провайдеров, не по поведению модели): у `deepseek` и + `zai` класс `sexually-explicit` — FORBIDDEN, поэтому верхний дециль по плотности эротических + маркеров снимается ДО отбора единиц; + 2. отбор единиц детерминированный: дедуп по подписи, ключ артефакта — хеш источника, сортировка + по (длина, uid), из середины распределения длин; + 3. манифест: единицы прикалываются к файлу, иначе докачка материала молча сдвинет уже + купленный замер (ровно это случилось с эксп-22 и чинилось в паке 23). + +Запуск: eval/.venv/bin/python eval/dovodka/material_ja.py [--units] +""" +from __future__ import annotations + +import hashlib +import json +import re +import sys +from pathlib import Path + +SRC = Path.home() / "books" / "isekai_majutsushi_jp.txt" +OUT = Path.home() / "books" / "dovodka" +MANIFEST = OUT / "manifest-ja.json" +TARGET_CHARS, MIN_CHARS, N_UNITS = 1100, 700, 9 # ja плотнее zh на знак: цель короче + +# ── маркеры гардрейла ───────────────────────────────────────────────────────────────────────── +AGE = re.compile(r"(小学生|中学生|高校生|幼女|幼児|少女|少年|児童|子供|子ども|ロリ|" + r"[0-90-9一二三四五六七八九十]{1,2}\s*[歳才])") +SEX = re.compile(r"(性行為|セックス|挿入|射精|愛撫|陰茎|陰部|膣|乳房|裸体|全裸|絶頂|喘|" + r"犯[すされし]|強姦|レイプ|淫|媚薬|発情)") +# Эротическая плотность для ГЕЙТА ПРАВ — шире, чем гардрейл: это про договоры провайдеров. +EROTIC = re.compile(r"(キス|口づけ|抱[きく]しめ|裸|胸|尻|太もも|下着|色気|艶|情事|房事|" + r"性的|欲情|快感|喘|淫|媚薬|発情|愛撫|挿入|射精|セックス|性行為)") + + +def text() -> str: + if not SRC.exists(): + raise SystemExit(f"⛔ материала нет: {SRC}") + return SRC.read_text(encoding="utf-8", errors="replace") + + +def chapters() -> list[dict]: + """Главы по разделителю площадки; заголовок остаётся в теле — он часть материала.""" + raw = text() + parts = re.split(r"\n=+\n", raw) + out = [] + for i, p in enumerate(parts): + p = p.strip() + if len(p) >= MIN_CHARS: + out.append(dict(idx=i, text=p)) + return out + + +def chunks() -> list[dict]: + """Абзацные чанки; абзац не рвётся, хвост главы прирастает к предыдущему (правило эксп-22).""" + out = [] + for ch in chapters(): + made, buf, size = [], [], 0 + for para in ch["text"].split("\n"): + para = para.strip() + if not para: + continue + buf.append(para) + size += len(para) + if size >= TARGET_CHARS: + made.append(dict(text="\n".join(buf), chapter=ch["idx"])) + buf, size = [], 0 + if size: + tail = "\n".join(buf) + if size >= MIN_CHARS or not made: + made.append(dict(text=tail, chapter=ch["idx"])) + else: + made[-1]["text"] += "\n" + tail + out += made + return out + + +def uid_of(s: str) -> str: + return hashlib.sha1(s.encode("utf-8")).hexdigest()[:10] # noqa: S324 + + +def guardrail(cs: list[dict]) -> list[int]: + """Индексы чанков КЛАССА, КОТОРЫЙ НЕ ОБРАБАТЫВАЕТСЯ. Содержимое не печатается никогда.""" + hits = [] + for i, c in enumerate(cs): + t = c["text"] + a, s = len(AGE.findall(t)), len(SEX.findall(t)) + if a and s and (a + s) >= 4: + hits.append(i) + return hits + + +def main() -> int: + cs = chunks() + print(f"Д6 МАТЕРИАЛ ja→ru · источник {SRC.name} · знаков {len(text())} · чанков {len(cs)}") + + print("\n=== ГАРДРЕЙЛ 18+ УРОВНЯ 3 (проверяется ПЕРВЫМ) ===") + bad = guardrail(cs) + if bad: + print(f"⛔ СТОП. Чанков с плотным совпадением возрастных и явных сексуальных маркеров: " + f"{len(bad)} из {len(cs)} (номера: {bad[:12]}{'…' if len(bad) > 12 else ''}).") + print(" Содержимое НЕ печатается и дальше не идёт. Материал непригоден по гардрейлу\n" + " владельца; замена без его слова НЕ ищется (промт: «непригоден по пробе → СТОП\n" + " и пинг»).") + if "--owner-sanctioned-exclude" not in sys.argv: + print("\n Детектор КОНСЕРВАТИВЕН намеренно: 少年/少女/子供 — частотные слова японской\n" + " прозы, и совпасть с эротическим маркером они могут безобидно. Развести это\n" + " можно только ЧТЕНИЕМ помеченных мест, а читать их правило запрещает — значит\n" + " решает не сессия. Обход существует и требует слова владельца:\n" + " --owner-sanctioned-exclude помеченные чанки выбрасываются механически,\n" + " остальные 181 идут в отбор; в отчёт пишется,\n" + " сколько и почему выброшено.\n" + " Прекращаю обработку источника.") + return 1 + print(f"\n ⚠ ОБХОД ПО СЛОВУ ВЛАДЕЛЬЦА: {len(bad)} помеченных чанков выброшены " + "механически, без чтения.") + cs = [c for i, c in enumerate(cs) if i not in set(bad)] + print(f" осталось чанков: {len(cs)}") + else: + print(f"[OK ] срабатываний нет: 0 из {len(cs)} чанков") + + print("\n=== ГЕЙТ ПРАВ (верхний дециль эротической плотности снимается ДО отбора) ===") + dens = sorted(((len(EROTIC.findall(c["text"])) / max(1, len(c["text"])) * 1000, i) + for i, c in enumerate(cs)), reverse=True) + cut = max(1, len(cs) // 10) + dropped = {i for _, i in dens[:cut]} + print(f"снято чанков {len(dropped)} из {len(cs)} (дециль); плотность верхнего " + f"{dens[0][0]:.2f}/1000 знаков, порога дециля {dens[cut - 1][0]:.2f}, медиана " + f"{dens[len(dens) // 2][0]:.2f}") + pool = [c for i, c in enumerate(cs) if i not in dropped] + + print("\n=== ОТБОР ЕДИНИЦ (детерминированный, методика эксп-21 §0) ===") + uniq: dict[str, dict] = {} + for c in pool: + u = uid_of(c["text"]) + uniq.setdefault(u, dict(uid=u, source=c["text"], chapter=c["chapter"])) + ordered = sorted(uniq.values(), key=lambda x: (len(x["source"]), x["uid"])) + mid = len(ordered) // 2 + lo = max(0, mid - N_UNITS // 2) + units = ordered[lo:lo + N_UNITS] + ls = [len(u["source"]) for u in units] + print(f"пул {len(ordered)} уникальных · выбрано {len(units)} из середины распределения длин") + print(f"знаков {min(ls)}…{max(ls)}, медиана {sorted(ls)[len(ls) // 2]} · " + f"глав {len({u['chapter'] for u in units})}") + for u in units: + print(f" {u['uid']} гл.{u['chapter']:<4d} знаков {len(u['source']):5d}") + + if "--units" in sys.argv: + OUT.mkdir(parents=True, exist_ok=True) + MANIFEST.write_text(json.dumps( + dict(source=SRC.name, n_chunks=len(cs), dropped_rights=len(dropped), + ja=dict(uids=[u["uid"] for u in units])), ensure_ascii=False, indent=1), + encoding="utf-8") + print(f"\nманифест → {MANIFEST} (единицы приколоты; докачка их больше не сдвинет)") + else: + print("\n(манифест НЕ записан — прогнать с флагом --units, когда состав утверждён)") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/eval/dovodka/money_d.py b/eval/dovodka/money_d.py new file mode 100644 index 00000000..f13ef15d --- /dev/null +++ b/eval/dovodka/money_d.py @@ -0,0 +1,139 @@ +#!/usr/bin/env python3 +"""КАССА ФАЗЫ Д. $0 сама по себе; через неё идут ВСЕ платные вызовы фазы. + +Не копия кассы эксп-22, а её НАСТРОЙКА через `configure` — тот же приём, которым пользовался пак +23. Механизм там проверенный и дорого доставшийся: фриз-гейт (покупка отказывает коду, который не +закоммичен или отличается от закоммиченного), атомарный замок клетки `O_CREAT|O_EXCL`, проекционный +сторож перед КАЖДЫМ вызовом по цене ИМЕННО ЭТОЙ модели, два независимых пути счёта. Копирование +размножило бы будущие расхождения вместо того, чтобы их ловить. + +ПОТОЛОК ФАЗЫ — САНКЦИЯ ВЛАДЕЛЬЦА $4.50 (10.08). История цифры: промт называл $10.00, владелец при +запуске сказал $4.00, затем поднял до $4.50, когда приёмка другого модельного семейства нашла, что +дизайн не отвечает на H-1 и H-4, а починка стоит $0.48. + +⚠ РЕЗЕРВ ФАЗЫ $0.03 — 0.7% пакета, и это объявлено риском ДО покупок (Д0.8). Ре-гены эхо-мины и +ретраи в паках 22/23 съедали до +16% сверх сметы фазы. Сессия называла честным потолком $4.80; +владелец выбрал $4.50. Поэтому правило исполнения жёсткое и записано здесь, а не в прозе: +**срабатывание проекционного гарда = СТОП и вопрос владельцу.** Резать ось, панель или кандидата +решением сессии ЗАПРЕЩЕНО заказом; поднимать потолок себе — тем более (пак 23 так и сделал, и это +стоило ему пункта CONFIRM/DENY). + +Запуск: money_d.py [--report|--selftest] +""" +from __future__ import annotations + +import sys +from pathlib import Path + +REPO = Path("/home/ubuntu/projects/textmachine") +ZONE = Path(__file__).resolve().parent +sys.path.insert(0, str(ZONE)) +sys.path.insert(0, str(REPO / "eval" / "tenant_panel")) +sys.path.insert(0, str(REPO / "eval" / "role_topology")) + + +def _load(name: str, path: Path): + """⚠ Чужое — ТОЛЬКО по пути. `import money` находил кассу эксп-22 или сам себя, смотря по + порядку `sys.path.insert`; пак 23 напоролся на это четырежды, и один раз молча.""" + import importlib.util # noqa: PLC0415 + 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 + + +M22 = _load("money22", REPO / "eval" / "tenant_panel" / "money.py") +ROSTER = _load("roster22", REPO / "eval" / "tenant_panel" / "roster.py") + +OUT = Path.home() / "books" / "dovodka" + +# Теги покупок по фазам. ⚠ Новый тег вписывается СЮДА тем же коммитом, что и код, который его +# заводит: расход мимо глоба невидим для кассы, и эксп-21 напоролся на это трижды, а последний +# раз гейт заниженное число ещё и СЕРТИФИЦИРОВАЛ. +TAGS = { + "ФД-A": ("dv-a-*.json",), # Д4 edit-контур zh: A1 A2 A3 A4 + свой шумовой пол + "ФД-B": ("dv-b-*.json",), # Д3 en→ru несущая ось: банк, контам, армы, пол + "ФД-C": ("dv-c-*.json",), # Д6 ja→ru разведка + "ФД-D": ("dv-d-*.json",), # Д7 микро-проба самооценки + "ФД-E": ("dv-e-*.json",), # Д1 добивка gemini-3.1-pro-preview + "ФД-F": ("dv-f-*.json",), # H-1: A6 dspro-черновик + оба контура + "ФД-G": ("dv-g-*.json",), # H-4: E6 второй редактор на en +} +# ⚠ Потолки фаз = смета `plan.py` плюс МИНИМАЛЬНЫЙ запас, так чтобы их СУММА не превышала +# пакетный потолок. Первая редакция округляла каждый вверх до цента и дала сумму 4.54 > 4.50 — +# то есть фазовые гарды в совокупности разрешали пробить пакетный. Поймано селфтестом кассы +# ДО первой покупки; ровно этот класс («касса охраняет не ту границу») стоил эксп-21 денег трижды. +CEILINGS = {"ФД-A": 0.885, "ФД-B": 0.560, "ФД-C": 0.155, "ФД-D": 0.055, + "ФД-E": 2.355, "ФД-F": 0.283, "ФД-G": 0.202} +PACK_CEILING = 4.50 + +M22.configure(ZONE=ZONE, OUT=OUT, TAGS=TAGS, CEILINGS=CEILINGS, + PACK_CEILING=PACK_CEILING, GLOB_ALL="dv-*.json", ROSTER=ROSTER) + +Guarded, Pack, purchase, model_expect = M22.Guarded, M22.Pack, M22.purchase, M22.model_expect + + +def report() -> None: + print(f"{'фаза':7s}{'потрачено':>12s}{'потолок':>10s} что покупает") + tot = 0.0 + what = {"ФД-A": "Д4 edit-контур zh", "ФД-B": "Д3 en→ru несущая", "ФД-C": "Д6 ja разведка", + "ФД-D": "Д7 самооценка", "ФД-E": "Д1 gemini", "ФД-F": "H-1 (A6)", "ФД-G": "H-4 (E6)"} + for ph in TAGS: + sp = Guarded(ph).spent() + tot += sp + mark = " ⛔ ПРОБОЙ" if sp > CEILINGS[ph] + 1e-9 else "" + print(f"{ph:7s}{sp:12.6f}{CEILINGS[ph]:10.2f} {what[ph]}{mark}") + print(f"{'ПАК':7s}{tot:12.6f}{PACK_CEILING:10.2f} резерв {PACK_CEILING - tot:.6f}") + + +def selftest() -> int: + """Обязан пройти ДО первой покупки фазы.""" + bad = 0 + + def ck(n: str, ok: bool, d: str = "") -> None: + nonlocal bad + bad += not ok + print(f"[{'OK ' if ok else 'ПРОВАЛ'}] {n}" + (f" {d}" if d else "")) + + ck("касса смотрит в СВОЁ сырьё", M22.OUT == OUT, str(M22.OUT)) + ck("покупки уходят в каталог фазы", M22.BUY.OUT == OUT, str(M22.BUY.OUT)) + ck("фриз-гейт сторожит зону фазы", M22.ZONE == ZONE, str(M22.ZONE)) + ck("сумма фазовых потолков не выше пакетного", + sum(CEILINGS.values()) <= PACK_CEILING + 1e-9, + f"{sum(CEILINGS.values()):.2f} ≤ {PACK_CEILING:.2f}") + for ph, globs in TAGS.items(): + for g in globs: + ck(f"{ph}: тег {g} покрыт глобом пака", g.startswith("dv-")) + # цена обязана считаться для КАЖДОЙ модели, которую фаза может позвать + for m in ("deepseek-v4-flash", "deepseek-v4-pro", "glm-5", "gemini-3.1-pro-preview"): + try: + ck(f"цена считается: {m}", M22.BUY.cost(m, 4500, 0, 2500, 1000, 8000) > 0) + except Exception as e: # noqa: BLE001 + ck(f"цена считается: {m}", False, f"{type(e).__name__}: {e}") + # проекционный гард обязан ОТКАЗЫВАТЬ, когда ожидание пробивает потолок + g = Guarded("ФД-D") + g.ceiling = 0.0 + ck("проекционный гард отказывает при нулевом потолке", not g.afford()[0]) + # и обязан видеть цену ДОРОГОЙ модели, а не p95 прошлого (регрессия на пробой Ф1 эксп-22) + g2 = Guarded("ФД-D") + g2.ceiling = g2.spent() + 0.05 + g2.next_model = "gemini-3.1-pro-preview" + ck("гард видит цену дорогой модели, а не прошлое", + not g2.afford()[0], f"ожидание ${model_expect('gemini-3.1-pro-preview'):.4f}") + # фриз-гейт обязан отвергнуть покупку из НЕ-файла + try: + exec(compile("import money22; money22._refuse_unfrozen()", "", "exec"), # noqa: S102 + {"__name__": "x"}) + ck("фриз-гейт отвергает покупку из НЕ-файла", False) + except SystemExit: + ck("фриз-гейт отвергает покупку из НЕ-файла", True) + print(f"\n{'КАССА ГОДНА' if not bad else f'ПРОВАЛОВ: {bad}'}") + return bad + + +if __name__ == "__main__": + a = sys.argv[1:] or ["--report"] + if a[0] == "--selftest": + sys.exit(1 if selftest() else 0) + report() diff --git a/eval/dovodka/plan.py b/eval/dovodka/plan.py new file mode 100644 index 00000000..e86bfe31 --- /dev/null +++ b/eval/dovodka/plan.py @@ -0,0 +1,134 @@ +#!/usr/bin/env python3 +"""СМЕТА ФАЗЫ Д ПО ЗАМЕРЕННЫМ ЦЕНАМ КЛЕТОК. $0. + +⚠ Почему скриптом, а не в столбик. Пак 23 поднял потолок фазы, сославшись на «панель $1.844» — +это была ПРОЕКЦИЯ `--c-plan` по медианам, а фактическая панель стоила $1.798638 и влезала в +исходный потолок. Смета, посчитанная руками и не пере-снимаемая, ровно так и ошибается. Здесь +каждая цена берётся из СЫРЬЯ уже купленных клеток (медиана по модели и роли), и любой читатель +пере-считает командой. + +Потолок фазы — САНКЦИЯ ВЛАДЕЛЬЦА $4.50 (10.08). История: промт называл $10.00, владелец при +запуске сказал $4.00, затем поднял до $4.50, когда приёмка другого модельного семейства нашла, +что дизайн не отвечает на H-1 и H-4, а починка стоит $0.48. ⚠ Сессия называла честным потолком +без стопов посреди фазы $4.80; владелец выбрал $4.50 — резерв $0.03, и срабатывание гарда есть +СТОП и вопрос владельцу, а не сокращение оси решением сессии. + +Запуск: eval/.venv/bin/python eval/dovodka/plan.py [--gemini N] +""" +from __future__ import annotations + +import collections +import json +import statistics as st +import sys +from pathlib import Path + +T = Path.home() / "books" / "tenant-panel" +E = Path.home() / "books" / "editor-tier" +PACK_CEILING = 4.50 + + +def measured() -> dict[tuple[str, str], float]: + """Медиана $/клетка по (роль, модель) из КУПЛЕННОГО сырья обоих паков.""" + acc: dict[tuple[str, str], list[float]] = collections.defaultdict(list) + for d, pat in ((T, "tp*.json"), (E, "et-*.json")): + for f in d.glob(pat): + try: + r = json.loads(f.read_text(encoding="utf-8")) + except (ValueError, OSError): + continue + if isinstance(r, dict) and r.get("cost_usd") and r.get("model"): + acc[(r.get("role") or "?", r["model"])].append(r["cost_usd"]) + return {k: st.median(v) for k, v in acc.items() if len(v) >= 3} + + +def main() -> int: + m = measured() + n_gem = 16 + if "--gemini" in sys.argv: + n_gem = int(sys.argv[sys.argv.index("--gemini") + 1]) + + draft = m[("draft", "deepseek-v4-flash")] + edit = m[("edit", "deepseek-v4-pro")] + gem = 0.14702 # ЕДИНСТВЕННАЯ состоявшаяся клетка Ф3 эксп-22, n=1 + # Производные цены армов edit-контура. Обоснование множителей — в отчёте §Д0; они ПРОГНОЗ, + # и расхождение с фактом печатается при закрытии фазы как сверка сметы. + fixer = 0.60 * edit # точечная починка: тот же вход, выход много короче + critic = 2.14 * edit # «поиск ≈2.14× переписа» — цифра из заказа владельца + onepass = edit # однопроходка: вход короче, размышление длиннее — вничью + + print("ЗАМЕРЕННЫЕ ЦЕНЫ (медиана по сырью обоих паков):") + for k in (("draft", "deepseek-v4-flash"), ("edit", "deepseek-v4-pro"), + ("editor3", "glm-5"), ("edit", "glm-5.2"), ("edit", "gpt-5.6-terra"), + ("edit", "grok-4.5")): + if k in m: + print(f" {k[0]:8s}{k[1]:22s}${m[k]:.5f}") + print(f" {'edit':8s}{'gemini-3.1-pro-preview':22s}${gem:.5f} ⚠ n=1, " + f"{gem / edit:.0f}× боевого редактора") + print("\nПРОИЗВОДНЫЕ (прогноз, сверяется с фактом при закрытии):") + print(f" фиксер ${fixer:.5f} (0.60× переписа)") + print(f" критик ${critic:.5f} (2.14× переписа — множитель из заказа владельца)") + + rows = [] + # ── Д4 edit-контур zh, 32 единицы (16 эксп-22 + 16 эксп-23); A0 уже куплен обоими паками + n4 = 32 + d4 = n4 * (fixer + onepass + (critic + fixer) + fixer) + 16 * fixer + rows.append(("ФД-A", "Д4 edit-контур zh: A1 флаги→фиксер · A2 однопроходка · " + f"A3 критик→фиксер · A4 канон-фиксер, n={n4} + свой пол", d4)) + # ── Д3 en ось, 16 единиц + n3 = 16 + d3 = (0.005 + 0.004 # банк-майнинг + проба контаминации + + n3 * draft + n3 * edit + n3 * onepass + + n3 * (fixer + critic + fixer) + n3 * edit) # два контура + пол + rows.append(("ФД-B", f"Д3 en→ru несущая: банк · контам-проба · связка · однопроходка · " + f"оба edit-контура · пол, n={n3}", d3)) + # ── Д6 ja разведка, 9 единиц + n6 = 9 + d6 = 0.004 + n6 * (draft + edit + onepass + edit) + rows.append(("ФД-C", f"Д6 ja→ru разведка: контам-проба · связка · однопроходка · пол, n={n6}", + d6)) + # ── Д7 самооценка + rows.append(("ФД-D", "Д7 микро-проба самооценки жильцов по трём книгам", 0.05)) + # ── Д1 gemini + # ⚠ ДВА АРМА, ЗАВЕДЁННЫЕ ПРИЁМКОЙ ДРУГОГО МОДЕЛЬНОГО СЕМЕЙСТВА. Без них фаза не отвечает на + # H-1 и H-4, и это нашёл не автор: H-1 говорит про «КАЧЕСТВЕННЫЙ черновик + точечный редактор», + # а все армы Д4 ставят фиксер на flash-черновик — то есть на полумусор по самой гипотезе; + # H-4 про смену ПОРЯДКА жильцов на другой паре, а на en-оси редактор был ровно один. + rows.append(("ФД-F", "H-1: dspro-черновик эксп-22 (уже куплен, $0) + оба edit-контура, n=16 — " + "прямой носитель гипотезы «качественный черновик + точечный редактор»", + 16 * (fixer + critic + fixer))) + rows.append(("ФД-G", "H-4: ВТОРОЙ редактор (glm-5) на en-оси поверх той же базы, n=16 — " + "без него порядок жильцов на en не проверяется вовсе", 16 * m[("editor3", "glm-5")])) + rows.append(("ФД-E", f"Д1 добивка gemini-3.1-pro-preview, {n_gem} клеток " + f"(503-отказы бесплатны, платим только за отгруженные)", n_gem * gem)) + + print(f"\n{'фаза':7s}{'что покупается':78s}{'смета':>9s}") + tot = 0.0 + for tag, what, cost in rows: + tot += cost + print(f"{tag:7s}{what[:78]:78s}{cost:9.3f}") + print(f"{'ИТОГО':7s}{'':78s}{tot:9.3f} при потолке ${PACK_CEILING:.2f}") + free = PACK_CEILING - tot + print(f"{'резерв':7s}{'':78s}{free:9.3f}") + if free < 0: + print("\n⛔ НЕ ВЛЕЗАЕТ. Резать ось или кандидата решением сессии ЗАПРЕЩЕНО " + "(промт, анти-лень) —\n это СТОП и вопрос владельцу.") + elif free < 0.30: + print("\n⚠ Резерв меньше 8% пакета. Ре-гены эхо-мины и ретраи в паках 22/23 съедали " + "до +16%\n сверх сметы фазы — при таком резерве первый же эхо-ре-ген упрётся " + "в потолок.") + + print(f"\n⚠ ДОЛЯ ОДНОЙ МОДЕЛИ: Д1 = ${n_gem * gem:.2f} = " + f"{n_gem * gem / PACK_CEILING * 100:.0f}% всего пакета фазы.") + print(f" Цена клетки ${gem:.5f} против ${edit:.5f} у боевого редактора — это " + f"${gem * 1500:.0f} за книгу\n при ${edit * 1500:.0f} у боевого. Продуктовым выбором " + "модель быть не может ни при каком исходе;\n ценность замера — закрыть гипотезу " + "эксп-04 «аутлаер прозы», не более.") + print(f"\n Сколько единиц Д1 влезает при остальном плане: " + f"{int((PACK_CEILING - (tot - n_gem * gem) - 0.35) / gem)} " + "(с резервом $0.35 на ре-гены).") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/eval/dovodka/power.py b/eval/dovodka/power.py new file mode 100644 index 00000000..ddc66937 --- /dev/null +++ b/eval/dovodka/power.py @@ -0,0 +1,197 @@ +#!/usr/bin/env python3 +"""МОЩНОСТЬ СУДЕЙСКИХ ОСЕЙ ФАЗЫ Д — считается ДО покупок. $0. + +⚠ Зачем. Эксп-22 купил английскую ось на n=6 и лишь ПОСЛЕ денег обнаружил, что при шести единицах +минимально достижимое p знакового теста = 2⁻⁵ = 0.031, а после Холма по шести контрастам — 0.19, +то есть значимости не могло быть НИ ПРИ КАКОМ результате. Ось была недомощна ПО ПОСТРОЕНИЮ. +Норма фазы Д: у каждой оси напечатан MDE при плановом n, и ось, чей MDE не даёт несущего вывода, +объявляется описательной ЗАРАНЕЕ. + +Два ограничения считаются раздельно, потому что упираются они в разное: + * ПОТОЛОК ЗНАЧИМОСТИ — минимально достижимое p точного знакового теста при n наблюдениях с + поправкой Холма по объявленному семейству. Если он ≥0.05, дизайн мёртв независимо от эффекта. + * MDE (минимально различимый эффект) — 2.8·sd/√n, то же правило, по которому риг печатает порог + различимости. Отвечает на «какой перевес прибор вообще способен развести». + +⚠ sd берётся из ЗАМЕРЕННЫХ шумовых полов прошлых паков и помечается источником: своего пола у +будущего прохода ещё нет по построению. Плюс поправка ×1.23 на межсессионную компоненту — §12б +эксп-23 показал, что пол, чьи половины судит ОДИН судья, занижает порог на 19%. + +Запуск: eval/.venv/bin/python eval/dovodka/power.py +""" +from __future__ import annotations + +import math +import statistics +import sys + +# Замеренные шумовые полы (sd разности по единицам) — источник указан у каждого. +# ⚠ ЯРЛЫКИ ИСПРАВЛЕНЫ РЕВЬЮ. Первая редакция называла пол `p3` «редакторы над одной базой» — +# это неверно: `p2zh` и `p3` разрешаются в ОДНИ И ТЕ ЖЕ 32 файла (`tp2-floor-p1/p2-E-*`), то есть +# оба меряют СКВОЗНУЮ связку «черновик+редактура», и оба несут те же две клетки `finish=length`. +# Разница 2.90 против 3.53 — чисто МЕЖСЕССИОННАЯ (×1.217), и это, кстати, СВОЯ оценка той самой +# компоненты, ради которой берётся заимствованная ×1.23 из §12б. Совпадение хорошее, но своя +# оценка теперь названа, а не умолчана. +FLOORS = { + "эксп-23 tier (редакторы над ОДНОЙ базой, zh, n=16)": 1.45, + "эксп-23 border (редакторы над ОДНОЙ базой, zh, n=16)": 2.12, + "эксп-22 p2zh (СКВОЗНАЯ связка, те же 32 файла, что p3)": 2.90, + "эксп-22 p3 (ТЕ ЖЕ файлы, другая судейская популяция)": 3.53, +} +BETWEEN = 1.23 # §12б эксп-23: наивная se → se с межсессионной компонентой + +# ⚠ ЦЕЛЕВОЙ ЭФФЕКТ — то, ради чего ось покупается. Без него MDE не решает НИЧЕГО: печатать +# «MDE 1.83» и не сказать, меньше он искомого или больше, — это ровно та форма, за которую +# погорела en-ось эксп-22. Цель объявляется ДО денег и обосновывается замеренным, а не желаемым. +AXES = [ + # (имя, план n, контрастов, sd-прайор, ЦЕЛЬ, обоснование прайора, обоснование цели) + ("Д4 edit-контур zh", 32, 3, 2.12, 1.50, + "армы правят ОДНУ базу — это структура `tier`/`border` (sd 1.45/2.12), берётся худшая", + "решение «дешёвый фиксер вместо переписа» меняется, если разрыв ≥ трети собственного " + "эффекта редактора (+4.44 в эксп-22 Ф3) — это 1.50"), + ("Д3 en→ru несущая", 16, 1, 2.12, 2.00, + "своего пола на en нет НИ ОДНОГО; прайор с zh той же структуры, пол докупается в фазе", + "несущий вопрос оси — переносится ли эффект редактора: на zh контроль +3.25, " + "значимым переносом считаем половину, 2.00"), + ("Д6 ja→ru разведка", 9, 3, 2.90, 2.00, + "связка end-to-end (как p2zh) — шумнее", + "та же цель, что у en; ось объявлена ОПИСАТЕЛЬНОЙ промтом заранее и остаётся ею " + "независимо от арифметики"), + ("Д1 gemini добивка", 16, 1, 2.12, 2.00, + "проход над той же якорной базой, что border — его пол ближайший", + "гипотеза эксп-04 «аутлаер прозы» — про КРУПНЫЙ отрыв; мелкий отрыв её не подтверждает " + "и продуктового смысла при цене 28× не имеет. ⚠ Именно этот порог решил спор о числе " + "клеток: при n=12 (минимум промта) MDE 2.11 БОЛЬШЕ цели и ось была бы ОПИСАТЕЛЬНОЙ; " + "владелец 10.08 выбрал полные 16, и только на них ось несущая"), +] +FORCED_DESCRIPTIVE = {"Д6 ja→ru разведка"} # объявлено промтом, арифметикой не отменяется + +# ⚠ ПЕРВИЧНОЕ И ВТОРИЧНОЕ СЕМЕЙСТВА — объявляются ДО покупок, и это НЕ сужение задним числом. +# Поправка Холма делится на РАЗМЕР семейства, поэтому «взять все контрасты несущими» = сделать +# все недомощными. Пре-регистрируется так: первичное семейство несёт по одному контрасту на +# КАЖДУЮ гипотезу владельца и считается с Холмом; остальные контрасты объявлены ОПИСАТЕЛЬНЫМИ, +# печатаются с числами и поправку не несут. Ровно этот приём эксп-22 применил в §0-Ф2 к осям +# «черновик как текст», и он там сработал. +PRIMARY = { + "Д4 edit-контур zh": ["A1/A0 — флаговый контур (H-2а)", + "A3/A0 — смысловой контур (H-2б, ставка владельца)", + "A6/A0 — качественный черновик + контур (H-1)"], + "Д3 en→ru несущая": ["E0/D0 — КОНТРОЛЬ: покупает ли редактор поверх черновика (H-3)"], + "Д1 gemini добивка": ["R1/R0 — аутлаер ли проза gemini (гипотеза эксп-04)"], +} +SECONDARY = { + "Д4 edit-контур zh": ["A2/A0 — однопроходка уравненного мандата (строка 153 бэклога)", + "A4/A0 — канон-фиксер ПОСЛЕ переписа: его несущая ось — покрытие " + "канона, а оно детерминировано и шума не имеет"], + "Д3 en→ru несущая": ["E1..E5/E0 — армы контуров и однопроходки", + "E6/E0 — ВТОРОЙ редактор glm-5 (H-4): на n=16 порядок жильцов " + "разрешить нельзя, ось описательная и это сказано ДО денег"], +} + +# ⚠ ПОЧЕМУ ПРАЙОР 2.12, А НЕ 2.90/3.53. Оба пола эксп-22 меряют СКВОЗНУЮ связку — они несут ещё и +# шум черновика, тогда как армы Д4 правят ОДНУ замороженную базу. Структурно ближе полы эксп-23 +# (`tier` 1.45, `border` 2.12, оба над одной базой), и берётся ХУДШИЙ из них. Отдельно: обе пары +# пола эксп-22 содержат по две клетки `finish=length` (§15), что разброс завышает — без затронутых +# единиц тот же пол даёт 3.10; в фазе Д такие клетки в пачку не допускаются вовсе. +# ⚠ И честно: ПРАЙОР РЕШАЕТ. При sd 3.53 MDE оси Д4 был бы 2.48 против цели 1.50, то есть ось +# стала бы описательной. Прежняя редакция писала «прайор всё равно НЕ решает» — неверно, снято +# ревью. Решает он не окончательно: СВОЙ пол прохода меряется в фазе парой, чьи половины судят +# РАЗНЫЕ сессии, и если он выйдет хуже прайора — ось пере-классифицируется ТОГДА ЖЕ, до чтения +# боевых перевесов. + +# Пространство дизайна: во что упирается вывод при разных n и разных полах. +GRID_N = (12, 16, 24, 32) +GRID_SD = (1.45, 2.12, 2.90, 3.53) + + +def min_p(n: int, k: int) -> float: + """Минимально достижимое двустороннее p знакового теста при n, после Холма по k контрастам.""" + return min(1.0, 2.0 ** (1 - n) * k) + + +def _z(p: float) -> float: + """Квантиль стандартной нормали.""" + return statistics.NormalDist().inv_cdf(p) + + +def mde(sd: float, n: int, k: int = 1, between: bool = True) -> float: + """Минимально различимый эффект при 80% мощности и поправке Холма по k контрастам. + + ⚠ КОНСТАНТА 2.8 ВЕРНА ТОЛЬКО ПРИ k=1, и первая редакция этого файла применяла её всегда. + Поймано адверсариальным ревью снизу вверх. Решающее правило пака — `p` ХОЛМА < 0.05, то есть + на самый строгий контраст семейства приходится α/k; множитель становится + (z_{1−α/2k} + z_{0.8}), при k=5 это 3.417 вместо 2.8. Пропущенная поправка ×1.22–1.36 БОЛЬШЕ + той ×1.23, которую файл старательно вносил за межсессионную компоненту, — и она переворачивала + вердикт: Д4 при k=5 давала MDE 1.57 против цели 1.50, то есть ОПИСАТЕЛЬНУЮ ось, объявленную + несущей. Это буквально провал en-оси эксп-22, ради недопущения которого файл и написан. + """ + mult = _z(1 - 0.05 / (2 * max(1, k))) + _z(0.80) + return mult * sd / math.sqrt(n) * (BETWEEN if between else 1.0) + + +def main() -> int: + print("ЗАМЕРЕННЫЕ ПОЛЫ, из которых берутся прайоры (sd разности по единицам):") + for k, v in FLOORS.items(): + print(f" {k:54s} sd {v:.2f}") + print(f"\nпоправка на межсессионную компоненту: ×{BETWEEN} (§12б эксп-23)\n") + print(f"{'ось':24s}{'n':>4s}{'k':>3s}{'sd':>6s}{'MDE':>7s}{'ЦЕЛЬ':>7s}{'потолок p':>11s}" + " СТАТУС ОСИ") + desc = 0 + for name, n, k, sd, target, _w1, _w2 in AXES: + p0 = min_p(n, k) + m_true = mde(sd, n, k) + # ⚠ ДВА УСЛОВИЯ, И ОБА НЕОБХОДИМЫ. (1) значимость достижима вообще; (2) прибор способен + # развести ИСКОМЫЙ эффект. Первая редакция смотрела только на (1) — и объявляла несущей + # ось, чей MDE вдвое больше того, что она ищет. Это и есть en-ось эксп-22, слово в слово. + why = [] + if p0 >= 0.05: + why.append(f"значимость недостижима (потолок p {p0:.4f})") + if m_true > target: + why.append(f"MDE {m_true:.2f} БОЛЬШЕ искомого эффекта {target:.2f}") + if name in FORCED_DESCRIPTIVE: + why.append("объявлена описательной ЗАКАЗОМ") + status = "НЕСУЩАЯ" if not why else "⛔ ОПИСАТЕЛЬНАЯ: " + " · ".join(why) + desc += bool(why) + print(f"{name:24s}{n:4d}{k:3d}{sd:6.2f}{m_true:7.2f}{target:7.2f}{p0:11.4f} {status}") + print(f"\nописательных осей: {desc} из {len(AXES)} — объявлено ДО покупок, " + "пере-классификации задним числом не будет") + print("\nСЕМЕЙСТВА, объявленные ДО покупок:") + for name in [a[0] for a in AXES]: + if name in PRIMARY: + print(f" {name} — ПЕРВИЧНОЕ (Холм по {len(PRIMARY[name])}):") + for c in PRIMARY[name]: + print(f" • {c}") + if name in SECONDARY: + print(" описательные (поправку не несут, печатаются с числами):") + for c in SECONDARY[name]: + print(f" · {c}") + print("\nОБОСНОВАНИЯ (прайор шума · искомый эффект):") + for name, _n, _k, _sd, _t, w1, w2 in AXES: + print(f" {name}\n прайор: {w1}\n цель: {w2}") + print("\nПРОСТРАНСТВО ДИЗАЙНА — честный MDE (с ×1.23) при разных n и разных полах:") + print(" " + "".join(f"{f'sd {s:.2f}':>10s}" for s in GRID_SD)) + for n in GRID_N: + print(f" n={n:<4d}" + "".join(f"{mde(s, n, 1):10.2f}" for s in GRID_SD)) + print(" (таблица при k=1; на семейство из k контрастов множитель растёт — см. mde())") + print("\n СКОЛЬКО ЕДИНИЦ НУЖНО, чтобы ось была несущей при sd 2.12 и цели:") + for k in (1, 2, 3, 4, 5): + for tgt in (1.50, 2.00): + need = math.ceil(((_z(1 - 0.05 / (2 * k)) + _z(0.80)) * 2.12 * BETWEEN / tgt) ** 2) + print(f" k={k} цель {tgt:.2f} → n ≥ {need}") + print("\n Для калибровки — РЕАЛЬНЫЕ перевесы, уже замеренные на этом материале:") + print(" эксп-22 Ф3: R2 −2.12 · R3 −2.54 · R4 −4.75 · КОНТРОЛЬ +4.44") + print(" эксп-23 tier: T1 −0.19 · T2 +0.50 · T3 +0.19 · КОНТРОЛЬ +2.06") + print(" эксп-23 border: R2 −0.81") + print(" ⇒ Различия МЕЖДУ близкими редакторами лежат в полосе 0.2–2.5, то есть под MDE почти\n" + " любой строки этой таблицы. Развести их прибор способен только при малом поле И\n" + " большом n. Контрасты «есть ли редактура вообще» (+2.1…+4.4) берутся легко.") + print("\n⚠ Читать так: MDE — свойство ПЛАНА, а не результата. Свой пол каждого прохода меряется\n" + " в фазе и печатается рядом; если он окажется хуже прайора, ось пере-классифицируется в\n" + " описательную ТОГДА ЖЕ, до чтения боевых перевесов, а не после.") + print("\n⚠ Сравнение с эксп-22: его en-ось шла n=6 при k=6 ⇒ потолок p " + f"{min_p(6, 6):.4f} — дизайн не мог дать значимости. Здесь такой оси нет.") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/eval/dovodka/promptdiff.py b/eval/dovodka/promptdiff.py new file mode 100644 index 00000000..14948633 --- /dev/null +++ b/eval/dovodka/promptdiff.py @@ -0,0 +1,231 @@ +#!/usr/bin/env python3 +"""ГЕЙТ КОНСИСТЕНТНОСТИ ПАР-ПРОМПТОВ. $0. Ненулевой код = пара не годна к покупкам. + +⚠ Зачем. Эксп-19 получил конфаундированный арм: en-зеркало промпта было НЕПОЛНЫМ, и разница +армов смешалась с разницей формулировок. Заказ фазы Д требует «механический гейт консистентности +с zh-промптами (одна структура, отличаются только данные языка; дифф по шаблону — гейт, не +глазами)». Вот он. + +ПРАВИЛО, которое гейт проверяет: + пар-промпт обязан быть побайтной копией боевого zh-промпта ВЕЗДЕ, кроме + (а) блока пар-специфики — секции «Единицы и приёмы (общие для <пара>)», это ДАННЫЕ пары; + (б) HTML-комментария провенанса в шапке. +Любое расхождение вне (а)/(б) — НАРУШЕНИЕ: структура промптов разошлась, и контраст между парами +перестал быть контрастом пары. + +Дополнительно сверяется то, что дифф строк не ловит: + * множества плейсхолдеров `{{…}}` совпадают; + * разделитель `---USER---` присутствует в обоих и на своём месте; + * последняя инструкция (что выводить) совпадает дословно; + * заголовок пар-блока называет ИМЕННО свою пару. + +Запуск: eval/.venv/bin/python eval/dovodka/promptdiff.py [<пара> …] (по умолчанию en-ru) +""" +from __future__ import annotations + +import difflib +import re +import sys +from pathlib import Path + +REPO = Path("/home/ubuntu/projects/textmachine") +BATTLE = REPO / "backend" / "prompts" / "zh-ru" +PAIRS = REPO / "eval" / "role_topology" / "prompts" +# Пар-блок встречается в ДВУХ формах, и первая редакция гейта знала только одну — из-за этого +# `editor.md` целиком считался «разошедшимся». Форма (1): отдельная строка-заголовок с буллетами +# под ней (`translator.md`). Форма (2): один буллет-абзац (`editor.md`). Ловим обе. +PAIR_HEAD = re.compile(r"^Единицы и приёмы \(общие для ([a-z]{2}→ru)\):\s*$") +PAIR_INLINE = re.compile(r"^-\s*Единицы и приёмы \(общие для ([a-z]{2}→ru)\):") +COMMENT = re.compile(r"(?s)\s*") +# ⚠ ВТОРАЯ ЗАКОННО ПАР-СПЕЦИФИЧНАЯ СЕКЦИЯ. Она описывает ТИПОЛОГИЮ ИСХОДНОГО ЯЗЫКА (китайский +# паратаксис против английской ритмической дроби), поэтому расходиться обязана. Но именно внутри +# неё живёт мандат арма, и ровно на его неполноте погорел эксп-19 — поэтому секция не выводится +# из-под гейта целиком: у неё сверяются ЧИСЛО пунктов и мандатные оговорки (ниже). +REFLOW_HEAD = re.compile(r"^ДИСКУРС-ПЕРЕВЁРСТКА\b") +NUMBERED = re.compile(r"^(\d+)\.\s") +# Оговорки, несущие МАНДАТ (а не типологию пары): обязаны стоять в обеих редакциях дословно. +INVARIANTS = ( + "Не сливай ТОЛЬКО ради длины и не теряй смысловые атомы", + "число предложений и абзацев совпадать с исходником НЕ обязано", + "Запрещено терять смысловые атомы", + "Каждую новую реплику начинай с нового абзаца через тире", +) +# ⚠ СООТВЕТСТВИЕ ФАЙЛОВ ОБЪЯВЛЯЕТСЯ, А НЕ УГАДЫВАЕТСЯ. Первая редакция подбирала zh-аналог по +# имени с фолбэком на суффикс `-eq` — и сопоставила `translator-reflow` с `translator-reflow-eq`, +# то есть с УРАВНЕННЫМ вариантом мандата эксп-21, который зеркалом не является вовсе. Гейт, +# который сам придумывает, что с чем сравнивать, доказывает не то, что от него хотят. +MAP = { + # en-файл → (zh-оригинал, комментарий провенанса) + "translator": ("backend/prompts/zh-ru/translator.md", "боевой промпт переводчика"), + "editor": ("backend/prompts/zh-ru/editor.md", "боевой промпт редактора"), + "translator-reflow": (None, "боевого zh-аналога НЕТ: это вариант полигона, " + "зеркалом боевого промпта не является"), +} +# ⚠ ТРЕТИЙ ЗАКОННО ПАР-СПЕЦИФИЧНЫЙ КЛАСС: примеры. Они обязаны быть НА ИСХОДНОМ ЯЗЫКЕ пары, +# иначе редактор увидит образец не того языка, который правит. +EXAMPLE = re.compile(r"^\[(narrative|dialogue)\b") + +# ⚠ ОБЪЯВЛЕННЫЕ РАСХОЖДЕНИЯ. Гейт зелёный ТОЛЬКО на них: любое новое расхождение роняет пару. +# Каждая строка разобрана глазами, причина записана. Так «мирится» дрейф, который иначе накопится +# молча, — и ровно этого не хватило эксп-19. +ALLOWED = { + ("en-ru", "editor", "- Сверяй смысл черновика"): + "пример кальки ЗАМЕНЁН своим английским (`he raised an eyebrow` → «поднял бровь» вместо " + "«вскинул брови») — образец обязан быть на исходном языке пары. ⚠ Вставлен в фазе Д: " + "приёмка другого семейства заметила, что НЕСУЩАЯ ось en шла с зеркалом БЕДНЕЕ, чем " + "разведочная ja, и гейт это благословлял", + ("en-ru", "editor", "- Вёрстка: собирай повествование"): + "zh добавляет «НЕ копируя построчную разбивку исходника (в оригинале часто одно " + "предложение — одна строка)» — свойство китайской вебновеллы; английская проза так не " + "набирается. Остаток строки совпадает дословно", + ("ja-ru", "editor", "- Сверяй смысл черновика"): + "пример кальки ЗАМЕНЁН своим японским (よろしくお願いします как «прошу хорошего " + "обращения») — образец обязан быть на исходном языке пары", + ("ja-ru", "editor", "- Вёрстка: собирай повествование"): + "оговорка про построчный набор СОХРАНЕНА и переформулирована под ja: веб-новелла " + "syosetu набирается построчно так же, как китайская", +} +BAD = 0 + + +def allowed(pair: str, name: str, line: str) -> str: + for (p, f, pref), why in ALLOWED.items(): + if p == pair and f == name and line.startswith(pref): + return why + return "" + + +def ck(name: str, ok: bool, detail: str = "") -> None: + global BAD # noqa: PLW0603 + BAD += not ok + print(f" [{'OK ' if ok else 'ПРОВАЛ'}] {name}" + (f" {detail}" if detail else "")) + + +def strip(t: str) -> str: + return COMMENT.sub("", t) + + +def blocks(lines: list[str]) -> tuple[list[str], list[str], list[str], str]: + """(строки вне пар-секций, строки «Единицы и приёмы», строки ДИСКУРС-секции, объявленная пара).""" + out, pair_lines, reflow, pair, inside, in_ref = [], [], [], "", False, False + for ln in lines: + if REFLOW_HEAD.match(ln): + in_ref, inside = True, False + reflow.append(ln) + continue + if EXAMPLE.match(ln): + reflow.append(ln) + in_ref = False + continue + if in_ref: + if ln.strip() and not NUMBERED.match(ln): + in_ref = False # секция кончилась + else: + reflow.append(ln) + continue + mi = PAIR_INLINE.match(ln) + if mi: # форма (2): один буллет + pair = pair or mi.group(1) + pair_lines.append(ln) + inside = False + continue + m = PAIR_HEAD.match(ln) + if m: # форма (1): заголовок + буллеты + inside, pair = True, m.group(1) + pair_lines.append(ln) + continue + if inside: + if ln.strip() and not ln.startswith("-"): + inside = False # блок кончился + else: + pair_lines.append(ln) + continue + out.append(ln) + return out, pair_lines, reflow, pair + + +def compare(name: str, zh_p: Path, en_p: Path, want_pair: str, pair: str) -> None: + print(f"\n=== {name}: {zh_p.relative_to(REPO)} ↔ {en_p.relative_to(REPO)}") + if not zh_p.exists() or not en_p.exists(): + ck("оба файла существуют", False, f"нет {'zh' if not zh_p.exists() else 'en'}") + return + zh, en = strip(zh_p.read_text(encoding="utf-8")), strip(en_p.read_text(encoding="utf-8")) + zl, el = zh.splitlines(), en.splitlines() + zo, zb, zr, zp = blocks(zl) + eo, eb, er, ep = blocks(el) + + ck("пар-блок объявлен в обоих", bool(zp) and bool(ep), f"zh «{zp}» · пара «{ep}»") + ck(f"пар-блок называет свою пару ({want_pair})", ep == want_pair, ep or "не найден") + + # ГЛАВНАЯ ПРОВЕРКА: всё вне пар-блока обязано совпадать построчно + diff = [d for d in difflib.unified_diff(zo, eo, "zh-ru", want_pair, lineterm="", n=0) + if d[:1] in "+-" and not d.startswith(("---", "+++"))] + undecl = [d for d in diff if not allowed(pair, name, d[1:])] + ck("вне пар-секций НЕОБЪЯВЛЕННЫХ расхождений НЕТ", not undecl, + f"необъявленных строк: {len(undecl)} (всего разошлось {len(diff)})") + for d in undecl[:12]: + print(f" {d[:150]}") + if len(undecl) > 12: + print(f" … ещё {len(undecl) - 12}") + seen = {allowed(pair, name, d[1:]) for d in diff if allowed(pair, name, d[1:])} + for why in sorted(seen): + print(f" · объявленное расхождение: {why}") + + zph, eph = set(re.findall(r"\{\{(\w+)\}\}", zh)), set(re.findall(r"\{\{(\w+)\}\}", en)) + ck("множества плейсхолдеров совпадают", zph == eph, + f"только в zh: {sorted(zph - eph)} · только в паре: {sorted(eph - zph)}" + if zph != eph else f"{len(zph)} шт.") + ck("разделитель ---USER--- в обоих", ("---USER---" in zh) == ("---USER---" in en) is True, + f"zh {'---USER---' in zh} · пара {'---USER---' in en}") + if "---USER---" in zh and "---USER---" in en: + ck("хвост после ---USER--- совпадает", + zh.split("---USER---", 1)[1].strip() == en.split("---USER---", 1)[1].strip()) + # пар-блок ОБЯЗАН отличаться — иначе пара не адаптирована вовсе + def _anon(ls: list[str]) -> list[str]: + return [re.sub(r"[a-z]{2}→ru", "<пара>", x) for x in ls if x.strip()] + ck("пар-блок содержательно ОТЛИЧАЕТСЯ (иначе пара не адаптирована)", + _anon(zb) != _anon(eb), f"строк в блоке: zh {len(zb)} · пара {len(eb)}") + + # ── ДИСКУРС-секция: расходиться обязана, но НЕ терять мандат ─────────────────────────────── + if zr or er: + zn = [NUMBERED.match(x).group(1) for x in zr if NUMBERED.match(x)] + en_n = [NUMBERED.match(x).group(1) for x in er if NUMBERED.match(x)] + ck("ДИСКУРС-секция есть в обоих", bool(zr) and bool(er), + f"zh {len(zr)} строк · пара {len(er)}") + ck("число пунктов ДИСКУРС-секции совпадает", zn == en_n, + f"zh {zn} · пара {en_n}") + zt, et = "\n".join(zr), "\n".join(er) + miss = [inv for inv in INVARIANTS if (inv in zt) and (inv not in et)] + ck("мандатные оговорки не потеряны в паре", not miss, + "потеряно: " + " · ".join(f"«{x}»" for x in miss) if miss else f"{len(INVARIANTS)} шт.") + extra = [inv for inv in INVARIANTS if (inv in et) and (inv not in zt)] + if extra: + print(f" ⚠ в паре есть оговорка, которой нет в zh: {extra} — " + "мандат НЕ уравнен, но в обратную сторону") + + +def main() -> int: + pairs = [a for a in sys.argv[1:] if not a.startswith("-")] or ["en-ru"] + for p in pairs: + want = f"{p.split('-')[0]}→ru" + print(f"\n{'═' * 78}\nПАРА {p}") + d = PAIRS / p + if not d.exists(): + ck(f"пар-пакет {p} существует", False, str(d)) + continue + for f in sorted(d.glob("*.md")): + if f.stem not in MAP: + ck(f"{f.stem}: соответствие zh-оригиналу ОБЪЯВЛЕНО", False, + "файла нет в MAP — гейт не угадывает, с чем сравнивать") + continue + rel, why = MAP[f.stem] + if rel is None: + print(f"\n=== {f.stem}: сравнение НЕ ПРОВОДИТСЯ — {why}") + continue + compare(f.stem, REPO / rel, f, want, p) + print(f"\n{'ПАР-ПРОМПТЫ КОНСИСТЕНТНЫ' if not BAD else f'НАРУШЕНИЙ: {BAD} — покупки по этой паре ЗАПРЕЩЕНЫ'}") + return BAD + + +if __name__ == "__main__": + sys.exit(1 if main() else 0) diff --git a/eval/dovodka/requirements.md b/eval/dovodka/requirements.md new file mode 100644 index 00000000..96b845ae --- /dev/null +++ b/eval/dovodka/requirements.md @@ -0,0 +1,115 @@ +# Реестр требований фазы Д — сверка с буквой заказа + +Источник: `docs/POLYGON_EXP2223_REDO_SESSION_PROMPT.md`, прочитан ПОСТРОЧНО, включая придаточные. +Так заведено потому, что все шесть пропусков эксп-22 стояли не в нумерованных блоках, а в +придаточных предложениях, и «критик полноты», писанный той же головой, их не увидел. + +**Этот файл обязан быть закоммичен ДО покупок.** `eval/conformance.py --final` отказывается +сверять незакоммиченный реестр: гейт против таблицы, которую можно дописать под результат, +ничего не доказывает — так эксп-23 получил зелёную сверку против реестра из scratchpad. + +Проверка: +``` +eval/.venv/bin/python eval/conformance.py eval/dovodka/requirements.md --plan # по ходу +eval/.venv/bin/python eval/conformance.py eval/dovodka/requirements.md --final # перед сдачей +``` + +Колонка «улика»: `$ команда` — обязана вернуть код 0; `путь` — артефакт обязан существовать и быть +непустым; `—` — улики ещё нет (в режиме `--plan` законно, в `--final` = провал). + +⚠ Улика вида «греп собственного отчёта» доказывает, что НАПЕЧАТАНО, а не что СДЕЛАНО. Гейт такие +улики считает отдельно и печатает их долю; здесь они помечены в тексте требования словом «печать». + + + +| ID | требование (цитата или сжатие промта) | улика | +|---|---|---| +| **РАМКА** | | | +| R1 | Отчёт фазы — секции Д0–Д8 в `23-editor-tier.md`, продолжение того же документа | — | +| R2 | Сводная таблица «пробел → чем закрыт → носитель-артефакт» обязательна | — | +| R3 | Пакетный потолок фазы сверен СЛОВОМ владельца при запуске | `$ grep -q "САНКЦИЯ ВЛАДЕЛЬЦА .4.00" eval/dovodka/plan.py` | +| R4 | Фазовые потолки разложены в пре-реге Д0 | — | +| R5 | Касса: фриз-гейт, атомарный замок, два пути счёта, проекционные гарды | — | +| R6 | Стоп-правило: не влезает → СТОП и пинг ДО покупок | `$ eval/.venv/bin/python eval/dovodka/plan.py` | +| R7 | Кап агент-сессий 80; запись ДО запуска суб-агента; селфтест до первой сессии | `$ eval/.venv/bin/python eval/dovodka/runs.py --selftest` | +| R8 | Гипотезы H-1, H-2а, H-2б, H-3, H-4 внесены в пре-рег ДОСЛОВНО | — | +| **Д1 — gemini** | | | +| R9 | Полная редакторская панель: 16 клеток поверх якорной базы эксп-22 | — | +| R10 | Ретрай-харнесс: экспоненциальный бэкофф на 5xx, окно ≥24 ч, поштучно, лимит цены на клетку | — | +| R11 | Журнал КАЖДОЙ попытки (время · код ответа · стоимость) файлом в сырьё | — | +| R12 | Вердикт «модель не отдаёт» — только с журналом за ≥24 ч + снимком вендор-статуса | — | +| R13 | Собрано ≥12/16 → судейство абс-ригом со всеми контролями | — | +| R14 | Потолок клетки считается по `total_tokens` (скрытая тарификация ×4.5) | — | +| **Д2 — внешняя подпись `tier`** | | | +| R15 | Задания `aj-tier-tasks` отданы внешнему судье вне Claude руками владельца | `$ test -s /home/ubuntu/books/editor-tier/sol-tier/ИНСТРУКЦИЯ.md` | +| R16 | Закрытие = НЕПУСТОЙ каталог ответов + пере-счёт скриптом класса `solscore` | `$ ls /home/ubuntu/books/editor-tier/sol-tier/*.txt` | +| **Д3 — en→ru несущая ось** | | | +| R17 | Провенанс каждого en-промпта объявлен в отчёте (печать) | — | +| R18 | Механический гейт консистентности en-промптов с zh: дифф по шаблону, не глазами | `$ eval/.venv/bin/python eval/dovodka/promptdiff.py en-ru` | +| R19 | Выверка пар-промптов против БОЕВОГО рендера | — | +| R20 | Фриз пар-промптов ДО покупок | — | +| R21 | en-банк: майнинг + терминолог по методике эксп-22, `signed:false` | — | +| R22 | Банк-закон D39.104 на ВСЕХ армах — прошлое отступление (72 клетки) не повторять | — | +| R23 | Материал: Кристофф, добор до 16 единиц методикой эксп-21/22, fr-страты сохранены | — | +| R24 | Проба контаминации гоняется ЖИЛЬЦАМИ, а не удобной дешёвой моделью | — | +| R25 | Материал непригоден по пробе → СТОП и пинг; замену без слова владельца не искать | — | +| R26 | Армы: боевая связка · dspro-однопроходка УРАВНЕННОГО мандата (строка 153) · черновик + ОБА edit-контура · контроль редактора E0/D0 | — | +| R27 | Судейство по нормам рига, Холм по объявленному семейству | — | +| **Д4 — edit-контур zh** | | | +| R28 | Якорные единицы эксп-22 переиспользуются, $0 за старые армы | — | +| R29 | Армы A0–A4 обязательны; **A3 (смысловой критик → фиксер) — ставка владельца, обязателен**; A5 — опция при остатке | — | +| R30 | Позиция контура: A1/A3 «ВМЕСТО редактора», A4 «ПОСЛЕ»; «до редактора» не мерится | — | +| R31 | Канон-гейт — детерминированная сверка покрытия банка после КАЖДОГО арма | — | +| R32 | Ответ по каждому арму печатается ТРЕМЯ осями: судья · канон-покрытие · цена | — | +| R33 | Несущий вердикт фазы на zh — внешняя подпись судьёй вне Claude | — | +| **Д5 — канон-вскрытие** | | | +| R34 | КАЖДОЕ место потери покрытия у R0 классифицировано (исчез / парафраз / другой перевод), таблица по терминам и местам | `$ eval/.venv/bin/python eval/dovodka/canon.py` | +| R35 | Результат вскрытия — регрессионный набор для канон-фиксера | `/home/ubuntu/books/dovodka/canon-dissect.json` | +| **Д6 — ja→ru разведка** | | | +| R36 | Материал `isekai_majutsushi_jp`, проба контаминации жильцами | `$ eval/.venv/bin/python eval/dovodka/material_ja.py; test $? -le 1` | +| 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 | 8–10 единиц: связка · dspro-однопроходка · контроль редактора | — | +| R40 | Печатается сравнение ОТНОСИТЕЛЬНОГО порядка жильцов между парами zh/en/ja | — | +| R41 | Статус оси РАЗВЕДКА объявлен В ПРЕ-РЕГЕ, а не постфактум | — | +| **Д7 — самооценка** | | | +| R42 | Жильцам обеих ролей по трём книгам (zh/en/ja) вопрос о самооценке; ответы персистятся | — | +| R43 | Сверка самооценки с фактическими результатами пар; решений на ней не стоит | — | +| **Д8 — поправки тел по приёмке №16** | | | +| R44 | §15 эксп-22 переписан: 5 из 6 клеток — эхо, пойманное гейтом; гейт длины сужен до `finish=length` | `$ grep -q "ИНТЕРПРЕТАЦИЯ ПЕРЕПИСАНА ФАЗОЙ Д" docs/experiments/22-tenant-panel.md` | +| R45 | §13а эксп-22: банк-закон на оси en НЕ исполнен | `$ grep -q "на zh исполнено, на en НЕТ" docs/experiments/22-tenant-panel.md` | +| R46 | Sol-статусы §10/§11/§14 эксп-22 приведены к §16; оборванная фраза дописана | `$ grep -q "АРБИТРАЖ НЕ СОСТОЯЛСЯ; ПАКЕТ (б)" docs/experiments/22-tenant-panel.md` | +| R47 | Счёт агент-сессий эксп-23 = 58, не 50 (§6, §14 п.9) + пинг в PROGRESS | `$ grep -q "Агент-запусков: 58" docs/experiments/23-editor-tier.md` | +| R48 | Противоречие §9/§13 эксп-23 против §12в о внешнем контуре снято | `$ grep -q "Внешний контур есть у .border. и НЕТ у .tier." docs/experiments/23-editor-tier.md` | +| R49 | Наклон `border` пере-снят (+0.708) и сторожится гейтом | `$ eval/.venv/bin/python eval/editor_tier/verify23.py` | +| R50 | Таблица §0 эксп-23: строка R2 помечена как число АННУЛИРОВАННОГО run2 | `$ grep -q "run2, ТОЖЕ АННУЛИРОВАН" docs/experiments/23-editor-tier.md` | +| R51 | Пере-штамповка `aj-border*` объявлена (копии `replication-runA`, mtime 23:31:54) | `$ grep -q "ПРОВЕНАНС КАТАЛОГОВ СУДЕЙСТВА .border. УТРАЧЕН" docs/experiments/23-editor-tier.md` | +| R52 | Голоса `replication-runB` несли judge-имена прогона A: `ingest` починен, объявлено | `$ eval/.venv/bin/python eval/editor_tier/judge.py --run replication-runB border --score` | +| R53 | Нумерация: два «п.7» в §8 эксп-23 и порядок §9 эксп-22 исправлены, ссылки поправлены | `$ grep -q "три ссылки в §13а и §14" docs/experiments/22-tenant-panel.md` | +| R54 | `eval/README.md`: `editor_tier/` добавлен в карту действующих харнессов | `$ grep -q "editor_tier/. . ..exp-23" eval/README.md` | +| 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 | Декой в каждой судейской пачке; сессия без пойманного декоя аннулируется целиком | — | +| R58 | Шумовой пол — парой, чьи половины судят РАЗНЫЕ сессии (порог был занижен на 19%) | — | +| R59 | Все армы единицы — в одном пакете одной сессии | — | +| R60 | Идентичность судей персистится ДО запуска; половиной пола не брать боевую клетку | `$ eval/.venv/bin/python eval/dovodka/runs.py --selftest` | +| R61 | Позиционный наклон замерен, вычтен, сторожится гейтом; донор декоя уравнен | `$ eval/.venv/bin/python eval/editor_tier/verify23.py` | +| R62 | Судье запрещено сравнивать варианты между собой И читать их рядом | — | +| R63 | Замок кассы снимает только поставивший его процесс (проверка живости PID) | — | +| 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)"` | +| R65 | MDE каждой судейской оси — в пре-рег ДО покупок; недомощная ось объявляется описательной ЗАРАНЕЕ | `$ eval/.venv/bin/python eval/dovodka/power.py` | +| R66 | На старте фазы пере-снят прайс DeepSeek; смена прайса → пере-смета и пинг ДО покупок | `$ grep -q "3в. ВАХТА D39.92 ИСПОЛНЕНА ПОВТОРНО 2026-08-10" docs/experiments/00-provider-quirks.md` | +| **АНТИ-ЛЕНЬ** | | | +| R67 | Не собралось с первой попытки → ДОБИВАТЬСЯ; отказ принимается только с журналом попыток | — | +| R68 | Панель, ось или кандидата НЕ резать решением сессии — СТОП и пинг владельцу | — | +| R69 | Обязательство с адресатом вне зоны закрывается ТОЛЬКО изменением файла вне зоны | `$ ls /home/ubuntu/books/editor-tier/sol-tier/*.txt` | +| R70 | Детекторы и пороги после взгляда на вердикты не менять; девиация = СТОП и пинг В МОМЕНТ | — | +| R71 | Правка рига чужого пака — отдельным коммитом + пере-снятие его гейтов | `$ eval/.venv/bin/python eval/tenant_panel/verify22.py` | +| R72 | Приёмка адверсариальная author≠reviewer; опровергатель ДРУГОГО модельного семейства ОБЯЗАТЕЛЕН | — | +| R73 | Финал — построчный аудит закрытия заказа | — | +| **ОТЧЁТ И СДАЧА** | | | +| R74 | Сверка H-1…H-4 с фактом отдельной таблицей, ВКЛЮЧАЯ опровержения | — | +| R75 | CONFIRM/DENY владельцу — только с артефактом-носителем | — | +| R76 | Деньги: итог по фазам ДВУМЯ путями счёта | — | +| R77 | Пинг в `docs/PROGRESS.md` секция «Полигон» при закрытии | — | diff --git a/eval/dovodka/runs.py b/eval/dovodka/runs.py new file mode 100644 index 00000000..28f6a68c --- /dev/null +++ b/eval/dovodka/runs.py @@ -0,0 +1,285 @@ +#!/usr/bin/env python3 +"""ЖУРНАЛ АГЕНТ-СЕССИЙ ФАЗЫ Д. $0. Кап 80 (заказ владельца 10.08). + +⚠ ЗАЧЕМ ОТДЕЛЬНЫЙ МОДУЛЬ, А НЕ СТРОКА В `judge.py`. В паке 23 счётчик существовал и не работал: +`log_run` звался из `--ingest`, то есть ПОСЛЕ того, как суб-агент отработал. На диске осталось +20 записей против 58 фактических сессий; при капе 80 такой счётчик пропустил бы ещё 60 — сторожить +кап можно ТОЛЬКО записью, сделанной ДО запуска. Здесь это и есть контракт: `claim()` пишет запись и возвращает имя судьи, +которое передаётся суб-агенту; не позвал `claim` — не получил имени, и разбор проставит `?`. + +⚠ ВТОРОЙ ДЕФЕКТ, КОТОРЫЙ ЧИНИТ ЭТОТ ЖЕ ФАЙЛ. Карта «токен → судья» в паке 23 лежала одна на +ПРОХОД, а репликация судила ТЕ ЖЕ токены вторым жребием судей — и все её голоса получили имена +судей боевого прогона. Здесь запись помнит ПРОГОН и ВРЕМЯ, и `judge_of` разводит два жребия по +одним и тем же токенам ОКНОМ сессии `[at, done_at]`. + +⚠ ТРЕТИЙ, И САМЫЙ ДОРОГОЙ. Атрибуция по времени работает, только пока сессии не перекрываются. +Пак 23 отдал пять единиц на пере-суживание ДВАЖДЫ, не дождавшись первого круга: оба круга писали +в одни файлы, `--ingest` прошёл между ними, и голоса разошлись с сырьём на 5 единицах из 32. +Поэтому `claim` ОТКАЗЫВАЕТ, если сессия с пересекающимися токенами ещё не закрыта `done`. +⚠ Первая редакция называла это «замком, который невозможно обойти механически» — неверно, и +поймано приёмкой другого модельного семейства: `claim` делал read-modify-write журнала БЕЗ +файлового лока, так что два параллельных вызова оба прочли бы леджер до записи друг друга, +оба прошли бы проверку пересечения и получили один и тот же номер. Тот же класс, что «неатомарная +проверка файла» в кассе эксп-22, стоившая ему границы пере-оплаты ≤$0.846103. Теперь журнал +берётся под атомарный `O_CREAT|O_EXCL`-замок, как `money.py`, и проверка пересечения идёт ВНУТРИ +критической секции. + +Команды: + runs.py --claim <прогон> <проход> <ток> [<ток> …] [--note "…"] запись ДО запуска суб-агента + runs.py --done закрыть сессию ПОСЛЕ ответа + runs.py --report сколько израсходовано + runs.py --selftest обязателен ДО первой сессии +""" +from __future__ import annotations + +import contextlib +import datetime as dt +import json +import os +import sys +import time +from pathlib import Path + +OUT = Path.home() / "books" / "dovodka" +LEDGER = OUT / "agent-runs.json" +CAP = 80 +FAR_FUTURE = 1e18 +LOCK_TRIES = 600 # ×0.1 с — минута ожидания живого замка + + +def _now() -> str: + return dt.datetime.now(dt.timezone.utc).isoformat(timespec="microseconds") + + +def _ts(iso: str | None) -> float: + return dt.datetime.fromisoformat(iso).timestamp() if iso else FAR_FUTURE + + +def used() -> list[dict]: + return json.loads(LEDGER.read_text(encoding="utf-8")) if LEDGER.exists() else [] + + +def _write(rs: list[dict]) -> None: + """⚠ Запись АТОМАРНА: `write_text` усекает файл и пишет заново, и падение между этими двумя + шагами оставляло журнал нечитаемым (поймано ревью исполнением: 2 прогона из 20). Пишем во + временный файл и подменяем `os.replace` — она атомарна в пределах ФС.""" + OUT.mkdir(parents=True, exist_ok=True) + tmp = LEDGER.with_suffix(".tmp") + tmp.write_text(json.dumps(rs, ensure_ascii=False, indent=1), encoding="utf-8") + os.replace(tmp, LEDGER) + + +@contextlib.contextmanager +def _locked(): + """Атомарный замок журнала. `O_CREAT|O_EXCL` — та же механика, что у кассы эксп-22. + + ⚠ Замок снимает ТОЛЬКО поставивший его процесс: в файл пишется PID, и протухшим замок + считается лишь тогда, когда процесса с этим PID больше нет (норма рига, промт фазы Д). + """ + OUT.mkdir(parents=True, exist_ok=True) + lock = LEDGER.with_suffix(".lock") + for _ in range(LOCK_TRIES): + try: + fd = os.open(str(lock), os.O_CREAT | os.O_EXCL | os.O_WRONLY) + os.write(fd, str(os.getpid()).encode()) + os.close(fd) + break + except FileExistsError: + try: + pid = int(lock.read_text() or 0) + os.kill(pid, 0) # процесс жив — ждём + except (ValueError, OSError): + lock.unlink(missing_ok=True) # ставивший умер — снимаем + continue + time.sleep(0.1) + else: + raise SystemExit("⛔ журнал агент-сессий заперт дольше минуты — разберись, кто держит") + try: + yield + finally: + with contextlib.suppress(OSError): + if lock.exists() and lock.read_text().strip() == str(os.getpid()): + lock.unlink() + + +def claim(run: str, pass_: str, tokens: list[str], note: str = "") -> dict: + """Занять агент-сессию ДО запуска суб-агента. Возвращает запись с именем судьи. + + ⚠ Кап проверяется ПЕРЕД записью, а не после: иначе 81-я сессия успевала бы отработать. + """ + with _locked(): + return _claim_locked(run, pass_, tokens, note) + + +def _claim_locked(run: str, pass_: str, tokens: list[str], note: str) -> dict: + rs = used() + live = [r for r in rs if r.get("done_at") is None and set(r["tokens"]) & set(tokens)] + if live: + raise SystemExit( + "⛔ ГОНКА СЕССИЙ: токены уже отданы незакрытой сессии " + f"#{live[0]['n']} ({live[0]['judge']}, {sorted(set(live[0]['tokens']) & set(tokens))}).\n" + " Дождись её ответов и закрой `--done`, либо возьми другие токены. Ровно на этом\n" + " пак 23 потерял соответствие голосов и сырья на 5 единицах из 32.") + n = len(rs) + 1 + if n > CAP: + raise SystemExit(f"⛔ КАП АГЕНТ-СЕССИЙ ФАЗЫ Д ПРОБИТ: {n} > {CAP} — СТОП и пинг владельцу") + rec = dict(n=n, at=_now(), done_at=None, run=run, pass_=pass_, judge=f"д-{n:02d}", + tokens=list(tokens), note=note) + rs.append(rec) + _write(rs) + return rec + + +def done(n: int) -> dict: + """⚠ ТОЖЕ ПОД ЗАМКОМ. Первая редакция брала замок только в `claim`, а `done` делала свой + read-modify-write мимо него — и параллельные `claim`+`done` теряли запись claim в 19 прогонах + из 20: суб-агент запущен, имя судьи выдано, а в журнале его нет. Кап при этом недосчитывает, + `judge_of` отдаёт `?`, а замок пересечения токенов пускает на них вторую сессию. Нашло ревью + исполнением; селфтест этого не видел, потому что гонял только последовательные сценарии.""" + with _locked(): + return _done_locked(n) + + +def _done_locked(n: int) -> dict: + rs = used() + hit = [r for r in rs if r["n"] == n] + if not hit: + raise SystemExit(f"⛔ сессии #{n} в журнале нет") + hit[0]["done_at"] = _now() + _write(rs) + return hit[0] + + +def judge_of(run: str, token: str, answer_mtime: float) -> str: + """Кто судил ЭТОТ ответ. Сессия владеет ответом, если он записан в её окне `[at, done_at]`. + + Хвост допуска: ответ, пришедший ПОСЛЕ закрытия последней накрывающей сессии, отдаётся ей же — + закрытие ставится руками и может отстать от файловой системы на секунды. Ответ РАНЬШЕ первой + регистрации не отдаётся никому: он не мог быть ею произведён. + """ + cand = sorted((r for r in used() if r["run"] == run and token in r["tokens"]), + key=lambda r: _ts(r["at"])) + inwin = [r for r in cand if _ts(r["at"]) <= answer_mtime <= _ts(r.get("done_at")) + 5.0] + if inwin: + return inwin[-1]["judge"] + started = [r for r in cand if _ts(r["at"]) <= answer_mtime] + return started[-1]["judge"] if started else "?" + + +def report() -> None: + rs = used() + print(f"агент-сессий израсходовано {len(rs)} из {CAP}") + live = [r for r in rs if r.get("done_at") is None] + per: dict[str, int] = {} + for r in rs: + per[r["run"]] = per.get(r["run"], 0) + 1 + for k, v in sorted(per.items()): + print(f" {k:28s} {v}") + if live: + print(f"⚠ НЕЗАКРЫТЫХ сессий {len(live)}: " + " ".join(f"#{r['n']}" for r in live)) + + +def selftest() -> int: + """Проверка ИСПОЛНЕНИЕМ, а не чтением: журнал подменяется временным, гоняются сценарии.""" + global LEDGER, CAP # noqa: PLW0603 + keep_l, keep_c = LEDGER, CAP + OUT.mkdir(parents=True, exist_ok=True) + LEDGER = OUT / "SELFTEST-agent-runs.json" + LEDGER.unlink(missing_ok=True) + bad = 0 + + def ck(name: str, ok: bool, detail: str = "") -> None: + nonlocal bad + bad += not ok + print(f"[{'OK ' if ok else 'ПРОВАЛ'}] {name}" + (f" {detail}" if detail else "")) + + try: + # 1. запись появляется ДО того, как что-либо отработало + a = claim("проверка", "px", ["t1", "t2"], note="первый жребий") + ck("claim пишет запись сразу", LEDGER.exists() and len(used()) == 1, f"n={a['n']}") + ck("судья именован", a["judge"] == "д-01", a["judge"]) + + # 2. ЗАМОК ГОНКИ: те же токены нельзя отдать, пока сессия не закрыта + try: + claim("проверка", "px", ["t2"]) + ck("гонка сессий по одному токену запрещена", False, "пропустил вторую сессию") + except SystemExit: + ck("гонка сессий по одному токену запрещена", True) + ck("отказавшая сессия НЕ записана", len(used()) == 1, f"{len(used())} записей") + + # ⚠ ЗАМОК ПРОВЕРЯЕТСЯ ИСПОЛНЕНИЕМ, а не чтением. Прежний селфтест гонял только + # последовательные сценарии и дыру read-modify-write не видел — её нашла приёмка + # другого модельного семейства. Здесь замок ставится ЧУЖИМ живым PID и claim обязан + # ждать, а не пройти мимо. + global LOCK_TRIES # noqa: PLW0603 + keep_tries, LOCK_TRIES = LOCK_TRIES, 5 # не ждать минуту в селфтесте + lock = LEDGER.with_suffix(".lock") + lock.write_text(str(os.getpid())) # живой PID = замок держится + t0 = time.time() + try: + claim("проверка", "px", ["t5"]) + waited = time.time() - t0 + ck("claim ЖДЁТ живой замок, а не проходит мимо", False, + f"прошёл за {waited:.2f} с при занятом замке") + except SystemExit: + ck("claim ЖДЁТ живой замок, а не проходит мимо", True, "упёрся в таймаут — верно") + finally: + lock.unlink(missing_ok=True) + LOCK_TRIES = keep_tries + lock.write_text("999999999") # мёртвый PID = замок протух + n_before = len(used()) + claim("проверка", "px", ["t6"]) + ck("протухший замок (мёртвый PID) снимается", len(used()) == n_before + 1) + ck("замок за собой убран", not lock.exists()) + n_now = len(used()) + ck("непересекающиеся токены разрешены", claim("проверка", "px", ["t7"])["n"] == n_now + 1) + + # 3. ВТОРОЙ ЖРЕБИЙ ПО ТЕМ ЖЕ ТОКЕНАМ — тот самый дефект пака 23 + t_first = _ts(a["at"]) + 0.001 # ответ пришёл в окне первой + done(a["n"]) + b = claim("проверка", "px", ["t1", "t2"], note="второй жребий") + t_second = _ts(b["at"]) + 0.001 + done(b["n"]) + ck("ответ ПЕРВОГО жребия атрибутируется первому", + judge_of("проверка", "t1", t_first) == "д-01", judge_of("проверка", "t1", t_first)) + ck("ответ ВТОРОГО жребия атрибутируется второму", + judge_of("проверка", "t1", t_second) == b["judge"], + f"{judge_of('проверка', 't1', t_second)} против {b['judge']}") + ck("ответ РАНЬШЕ первой регистрации не отдаётся никому", + judge_of("проверка", "t1", _ts(a["at"]) - 10) == "?") + ck("чужой прогон не атрибутируется", judge_of("другой", "t1", t_second) == "?") + ck("незарегистрированный токен даёт '?'", judge_of("проверка", "нет", t_second) == "?") + + # 4. КАП ОБЯЗАН ОТКАЗАТЬ ДО ЗАПУСКА, а не после + CAP = len(used()) + try: + claim("проверка", "px", ["t9"]) + ck("кап отказывает при исчерпании", False, "пропустил лишнюю сессию") + except SystemExit: + ck("кап отказывает при исчерпании", True) + ck("сверх-капная сессия НЕ записана", len(used()) == CAP, f"{len(used())} записей") + finally: + LEDGER.unlink(missing_ok=True) + LEDGER, CAP = keep_l, keep_c + print(f"\n{'ЖУРНАЛ ГОДЕН' if not bad else f'ПРОВАЛОВ: {bad}'}") + return bad + + +if __name__ == "__main__": + a = sys.argv[1:] or ["--report"] + if a[0] == "--selftest": + sys.exit(1 if selftest() else 0) + elif a[0] == "--done": + r = done(int(a[1])) + print(f"сессия #{r['n']} ({r['judge']}) закрыта {r['done_at']}") + elif a[0] == "--claim": + note = "" + if "--note" in a: + i = a.index("--note") + note, a = (a[i + 1] if len(a) > i + 1 else ""), a[:i] + if len(a) < 4: + raise SystemExit("нужно: --claim <прогон> <проход> <ток> [<ток> …]") + r = claim(a[1], a[2], a[3:], note=note) + print(f"сессия #{r['n']}/{CAP} · судья {r['judge']} · токенов {len(r['tokens'])}") + else: + report() diff --git a/eval/role_topology/prompts/en-ru/editor.md b/eval/role_topology/prompts/en-ru/editor.md index 6a014eb3..d0fbe0ad 100644 --- a/eval/role_topology/prompts/en-ru/editor.md +++ b/eval/role_topology/prompts/en-ru/editor.md @@ -8,7 +8,7 @@ Жанр книги: {{genre}}. Аудитория: {{audience}}. Книга: «{{title}}». Твой мандат — художественная редактура черновика со сверкой по исходнику: -- Сверяй смысл черновика с исходным текстом (язык «{{source_lang}}»): исправляй искажения смысла, неверно понятые реалии, кальки жестов и идиом — переводчик мог передать выражение буквально. Опирайся на исходник, но пиши по-русски, а не подстрочником. +- Сверяй смысл черновика с исходным текстом (язык «{{source_lang}}»): исправляй искажения смысла, неверно понятые реалии, кальки жестов и идиом — переводчик мог передать выражение буквально (напр. `he raised an eyebrow` как «он поднял бровь» вместо «он вскинул брови»). Опирайся на исходник, но пиши по-русски, а не подстрочником. - Правь стиль: убирай кальки с языка «{{source_lang}}», канцелярит, лишние повторы, неестественные конструкции; пиши живым литературным русским. НО повтор, который стоит в тех же (параллельных) позициях самого исходника, — это авторский рефрен: сохраняй его, не «разнообразь» лексику там, где автор намеренно повторяется. - Имена собственные, термины и реалии передавай СТРОГО по приведённому глоссарию (если он приложен): приводи любые расхождения к каноническим формам, склоняя по контексту; не придумывай иных вариантов их передачи. - Вёрстка: собирай повествование в естественные русские абзацы. Реплики прямой речи начинай с нового абзаца через тире «—». Максимума длины абзаца нет. diff --git a/eval/role_topology/prompts/ja-ru/editor.md b/eval/role_topology/prompts/ja-ru/editor.md new file mode 100644 index 00000000..9336fd28 --- /dev/null +++ b/eval/role_topology/prompts/ja-ru/editor.md @@ -0,0 +1,46 @@ + +Ты — литературный редактор художественного перевода на русский язык. Тебе даны ИСХОДНЫЙ текст и его черновой перевод. +Жанр книги: {{genre}}. Аудитория: {{audience}}. Книга: «{{title}}». + +Твой мандат — художественная редактура черновика со сверкой по исходнику: +- Сверяй смысл черновика с исходным текстом (язык «{{source_lang}}»): исправляй искажения смысла, неверно понятые реалии, кальки жестов и идиом — переводчик мог передать выражение буквально (напр. よろしくお願いします как «прошу хорошего обращения»). Опирайся на исходник, но пиши по-русски, а не подстрочником. +- Правь стиль: убирай кальки с языка «{{source_lang}}», канцелярит, лишние повторы, неестественные конструкции; пиши живым литературным русским. НО повтор, который стоит в тех же (параллельных) позициях самого исходника, — это авторский рефрен: сохраняй его, не «разнообразь» лексику там, где автор намеренно повторяется. +- Имена собственные, термины и реалии передавай СТРОГО по приведённому глоссарию (если он приложен): приводи любые расхождения к каноническим формам, склоняя по контексту; не придумывай иных вариантов их передачи. +- Вёрстка: собирай повествование в естественные русские абзацы, НЕ копируя построчную разбивку исходника (веб-новелла набирается построчно). Реплики прямой речи начинай с нового абзаца через тире «—». Максимума длины абзаца нет. +- Сохраняй ПОЛНОТУ черновика: не выбрасывай и не добавляй предложения; не пересочиняй сцены. +- Единицы и приёмы (общие для ja→ru): МЕРЫ приводи к привычным читателю единицам (традиционные 尺/里/坪 — метрическими либо привычными приблизительными оборотами). ЗВУКОПОДРАЖАНИЯ и ОБРАЗОПОДРАЖАНИЯ (擬音語/擬態語) передавай русским глаголом, наречием или образом, НЕ слоговой транслитерацией. ХОНОРИФИКИ и уровни вежливости веди по брифу и держи одну форму; смену вежливости передавай русским регистром. ОПУЩЕННОЕ ПОДЛЕЖАЩЕЕ восстанавливай по контексту. ТРАНСКРИПЦИЯ — по Поливанову, не по ромадзи. СТИХИ и названия классики — по смыслу, не транслитерацией. Род и форму имён и терминов сверяй по глоссарию. +- Хонорифики: {{honorifics}}. Баланс форенизация/доместикация: {{venuti}}. + +Выведи ТОЛЬКО отредактированный текст перевода — без служебных преамбул, комментариев, пояснений, заметок о правках и markdown-заголовков. + +ДИСКУРС-ПЕРЕВЁРСТКА (приведение японского членения и эллипсиса к русской абзацной и синтаксической норме — это КОНВЕНЦИЯ русского языка, а не вольность): +1. Молча определи смысловые ходы фрагмента: единый фокал/наблюдатель; действие и его непосредственная реакция; одна тема рассуждения; границы — смена говорящего, времени, места или фокала. +2. Один повествовательный ход оформляй ОДНИМ русским абзацем, ДАЖЕ если японский исходник разбит на однофразовые строки (веб-новелла набирается построчно). Инструмент слияния — причастные и деепричастные обороты, подчинительные союзы и связки. Не сливай ТОЛЬКО ради длины и не теряй смысловые атомы. +3. Каждую новую реплику начинай с нового абзаца через тире «—»; слова автора при той же реплике оставляй в её абзаце. Японские скобки прямой речи 「」 заменяй русским тире, 『』 — кавычками-ёлочками. +4. Разрешено объединять и делить русские предложения; число предложений и абзацев совпадать с исходником НЕ обязано. Запрещено терять смысловые атомы (участников, действия, числа, полярность, заголовки, а также идиомы и ёдзидзюкуго: их передавай смыслом, не сворачивая в общее место и не калькируя дословно). + +---FEWSHOT--- +Примеры требуемой ПЕРЕВЁРСТКИ (показана структура вывода, не стиль): + +[narrative N:1] Исходник построчно — «扉を開けて外に出た。» / «空は灰色だった。» / «彼は息をついた。» / «道はどこまでも続いていた。» — становится ОДНИМ абзацем: +Он вышел за дверь. Небо было серым; вздохнув, он смотрел, как дорога уходит вдаль. + +[dialogue 1:N] Исходник — «叔父は眉をひそめて言った。「時が経つのは早いな。座れ。」「ありがとうございます」と甥が答えた。» — становится: +Дядя нахмурился. +— Время летит. Садись, — сказал он. +— Спасибо, — ответил племянник. + +[focal-shift] При смене наблюдателя/места/времени — новый абзац, даже если в исходнике это продолжение той же строки. + +---USER--- +Исходный текст (язык «{{source_lang}}»): + +{{text}} + +Черновик перевода для редактуры: + +{{draft}} diff --git a/eval/role_topology/prompts/ja-ru/translator.md b/eval/role_topology/prompts/ja-ru/translator.md new file mode 100644 index 00000000..aa3d963e --- /dev/null +++ b/eval/role_topology/prompts/ja-ru/translator.md @@ -0,0 +1,30 @@ + +Ты — профессиональный литературный переводчик с языка «{{source_lang}}» на язык «{{target_lang}}». +Жанр книги: {{genre}}. Аудитория: {{audience}}. Книга: «{{title}}». + +Правила перевода (translation brief): +- Хонорифики: {{honorifics}}. Транскрипция имён и реалий: {{transcription}}. +- Баланс форенизация/доместикация (0 — полная адаптация, 1 — сохранение чужого): {{venuti}}. +- Сноски: {{footnotes}}. +- Переводи ВСЕ предложения исходника: ничего не пропускай, не сокращай и не добавляй от себя. +- Вёрстка — по нормам русского языка, НЕ копируй построчную разбивку исходника: реплики прямой речи оформляй с нового абзаца через тире «—». +- Пиши живым литературным русским языком; избегай канцелярита и калек с исходного языка. + +Единицы и приёмы (общие для ja→ru): +- МЕРЫ переводи в привычные читателю единицы: традиционные 尺 (сяку ≈ 30 см), 寸, 里 (ри ≈ 3.9 км), 坪, 石, 両 передавай метрическими либо привычными приблизительными оборотами, сам термин не транслитерируй; современные метрические единицы оставляй как есть. +- ЗВУКОПОДРАЖАНИЯ и ОБРАЗОПОДРАЖАНИЯ (擬音語 / 擬態語: ドキドキ, じっと, ぼんやり) — отдельный слой, и калькировать их нельзя: передавай русским глаголом, наречием или образом («сердце заколотилось», «замер», «рассеянно»), а не слоговой транслитерацией. +- ХОНОРИФИКИ и вежливые формы: суффиксы (-сан, -кун, -тян, -сама, -сэмпай) веди по правилу брифа {{honorifics}} и держи ОДНУ форму на весь текст; смену уровня вежливости (敬語 → простая форма) передавай РУССКИМ регистром — обращением на «вы»/«ты» и лексикой, а не пометками. +- ОПУЩЕННОЕ ПОДЛЕЖАЩЕЕ восстанавливай по контексту: японская фраза сплошь и рядом обходится без него, русская — нет. Не выдумывай участника, которого в сцене нет. +- ТРАНСКРИПЦИЯ имён и реалий — по системе Поливанова (си, дзи, тя, дзю), а не по английской ромадзи (shi, ji, cha, ju); одна форма имени на весь текст. +- СТИХИ и названия классики передавай ПО СМЫСЛУ (не транслитерацией), опираясь на существующие русские переводы. +- Род и форму имён собственных и терминов бери из приложенного глоссария (если он есть). +Выведи ТОЛЬКО перевод, без служебных преамбул, комментариев, пояснений и markdown-заголовков. + +---USER--- +Переведи следующий фрагмент: + +{{text}}