Fix packs 22/23 rigs and reports per acceptance 16 and three adversarial review lines: exact sign test, word-boundary bank regex, judge attribution, coverage guard

This commit is contained in:
Claude (backend session) 2026-08-10 07:05:55 +03:00
parent d1b46984b5
commit b1780b8d9d
24 changed files with 2278 additions and 50 deletions

View file

@ -2,13 +2,13 @@
> **⟶ ТЕКУЩЕЕ СОСТОЯНИЕ** (на 2026-08-09, голова D39.126; хендофф №15→№16 — D39.124/125, докс-пак 167 исполнен — D39.126). Оркестраторов ДВА (решение владельца 07.08): этот — движок/платформа/фронт/доки; параллельный (РОЛЬЮ, без номера — счётчик один, D39.112 п.6) — приёмка полигона. Одновременно не запускаются; CURRENT-STATE ведут оба, чужие строки не трогают. Норма изоляции панелей после инцидента отката чужих файлов — D39.113, гардрейлы в CLAUDE.md.
> - **Эра №15 закрыта — семь приёмок, все ПРИНЯТЫ и залендены**; лента, коммиты и разборы — D39.109123 (D-лог) и реестр нот `architecture/05-decisions-index.md`; снимок прежних бюллетеней этой шапки — архив-слайс `-08-02-04`.
> - **ЖИВОЕ:** полигон эксп-22 «панель жильцов» — ЗАКРЫТ 09.08, ЖДЁТ ЛЕНДИНГА (8 CONFIRM/DENY на владельце; секция «Полигон» ниже; в дереве замечен незаявленный эксп-23 editor_tier — спросить при лендинге) · фронт: S4 ГЕЙЧЕН словом владельца об оболочке → сначала «S3.5 фикс-пак» (D39.119 п.4) · активных промтов бэкенда/платформы НЕТ — следующие по слову владельца: **эмиттер** (103 + деньги шва; сессии перед стартом читать PD-95/105/79/99/107/60/61 + D39.106 + строку 165) · **платформа**: стоп/резюм-ручки + эскроу (136).
> - **Открыто на владельце:** прогон Sol-пакетов руками (150) · кредиты xAI (арбитр grok + судья 18+) · Приложение А контракта (148: реконструкция с баннером или заново) · продуктовое слово «остановлена: лимиты» (В-3) · К-13/Ф-31 (paused_reason один — «свой потолок» и «нет кредита» неразличимы) · В-4/Ф-30 (глава без заголовка) · фри-тир PD-104 (ноль на бете против автогранта $5 с суточным потолком) · порог 116 · мини-проба флора 44 (одобрена, промт не выдан) · подпись денежного шага 46 · лист В-3+К-6 · авто-резюм paused (вопрос платформы) · запуск S3.5/эмиттера/следующего платформенного пака.
> - **ЖИВОЕ:** полигон эксп-22/23 — дерево ЗАЛЕНДЕНО 10.08 с ревью-шапками приёмки №16 (числа подтверждены независимым пересчётом; **выводы НЕ ратифицированы** — владелец 10.08 признал постановку неполной), **фаза Д заказана**: промт `docs/POLYGON_EXP2223_DOVODKA_SESSION_PROMPT.md`, запуск по слову владельца (санкция потолка $10.00 при запуске) · фронт: список претензий владельца ПРИШЁЛ 09.08 (24 замечания) — промт S3.5 ВЫДАН (`frontend/docs/S35_SESSION_PROMPT.md`), запуск по слову владельца, S4 гейчен до приёмки S3.5 (D39.119 п.4) · активных промтов бэкенда/платформы НЕТ — следующие по слову владельца: **эмиттер** (103 + деньги шва; сессии перед стартом читать PD-95/105/79/99/107/60/61 + D39.106 + строку 165) · **платформа**: стоп/резюм-ручки + эскроу (136).
> - **Открыто на владельце:** запуск фазы Д полигона (слово + санкция потолка $10.00) · Приложение А контракта (148: реконструкция с баннером или заново) · продуктовое слово «остановлена: лимиты» (В-3) · К-13/Ф-31 (paused_reason один — «свой потолок» и «нет кредита» неразличимы) · В-4/Ф-30 (глава без заголовка) · фри-тир PD-104 (ноль на бете против автогранта $5 с суточным потолком) · порог 116 · мини-проба флора 44 (одобрена, промт не выдан) · подпись денежного шага 46 · лист В-3+К-6 · авто-резюм paused (вопрос платформы) · запуск S3.5/эмиттера/следующего платформенного пака.
> - **Сделано (сводно)** — снимок ленты закрытых эр в [archive/PROGRESS-2026-08-04-09.md](archive/PROGRESS-2026-08-04-09.md); детали — D-лог и слайсы.
> - **Курс:** ОБЩНОСТЬ ✅ → КАЧЕСТВО БАНКА ✅ → ПАКЕТ-ЧЕКЕРОВ ✅ (D39.5978) → **ФРОНТ-ЭРА** (D39.81100: зоны живые, контракт API ратифицирован) → **шов/платформа/движковые блокеры построены** (D39.106123). Хвосты курса живут строками: 16 (edit-волна не гонялась НИ РАЗУ — держит и оси голоса 13б/24) · 46 (дизайн заморожен D39.92/93, промт ждёт выдачи) · coldrun-b фаза C заморожена чекпойнтом легитимно (D39.86; эталон денег/поведения — coldrun-a, read-only); развилка 0731 решена и исполнена (D39.87/91, код `553f1a3`).
> - **Горизонт (D39.62/67, освежён D39.95):** **ДОБОР ИДЕАЛА** (первым прогоном: оси голоса 24 · авто-режим · цена 16; жильцы ролей решаются ДО прогона эксп-22 — строка 149 · веса K1K12 13а · вне-претрейн чекпоинт 55; остаток арбитража банка = рецензент спорных кластеров при ре-пробе 74 — D39.102) → ВТОРАЯ ПАРА живьём (ja→ru; преп 81) → МАСШТАБ → пилот Ф2.5 (гейт резюме-строки 80; строки 6268, 85) → Ф3 ридер-IDE (6971). **Стоячие:** ToS-триггер 25.10 · Ш-2 до go1.27 (⚠ + x/text Unicode 17 тем же тулчейном — строка 119, реестр §Б-108 справочника якорей) · платные прогоны разблокированы (проба провода — D39.97, конфиги 112 залендены).
> - **Стек (полная карта роль→модель→конфиг→квирки — [STACK.md](STACK.md), D39.126):** draft deepseek-v4-flash thinking-ON `low` **⚠0731** → терминолог (та же модель) → editor deepseek-v4-pro БИЛИНГВ ИНТЕРИМ (топология ПОДТВЕРЖДЕНА при неразличимости жильцов — D39.117; закон-блок ОБЯЗАТЕЛЕН — строка 134; glm-5 резерв; вахта маппинга эффорта pro — §Б-108) → судья gemini (Ф2, в движке НЕ построен — строка 33); канал B Mistral+grok; ~$0.85/ранобэ (D30.4, пере-калибровка при следующем платном прогоне). ⚠ **ВЕСЬ банковый контур (банкнота+терминолог+классификатор) в shipping-c1 НЕ включён** — жив ран-локальным конфигом книги (строка 140; сверка STACK.md 09.08 — факт шире прежней декларации «одна банкнота»); эскалация в shipping за `budget_usd: 0` (STACK.md §примечания).
> - **ЕДИНЫЙ БЭКЛОГ — секция «Бэклог» ниже** (одна таблица, единственный трекер; каждая петля обязана иметь диспозицию: решено / отложено-с-записью / отклонено; ведёт оркестратор). **СЧЁТ ОЧЕРЕДИ на 09.08 (скриптом по таблице — `python3 docs/scripts/counts.py`; обновлять при каждом лендинге):** всего **132** строки · зона бэкенд **67** строго / **89** широко (⚠ колонки счётчик читает С КОНЦА — испр. D39.116) · **блокеров очереди 0**, платные прогоны разблокированы · «скоро» **32** (перечень — грепом по таблице, рукописный список снят D39.126) · гейт-строки эксп-22: 55·153 (плюс 150 — руки владельца); строка 5 — остаток гейчен ре-пробой 74 (D39.102 п.2); остальное «когда-нибудь». ⚠ Счёт — НИЖНЯЯ граница долга, не потолок (разбор — легенда таблицы ниже).
> - **ЕДИНЫЙ БЭКЛОГ — секция «Бэклог» ниже** (одна таблица, единственный трекер; каждая петля обязана иметь диспозицию: решено / отложено-с-записью / отклонено; ведёт оркестратор). **СЧЁТ ОЧЕРЕДИ на 10.08 (скриптом по таблице — `python3 docs/scripts/counts.py`; обновлять при каждом лендинге):** всего **133** строки · зона бэкенд **68** строго / **90** широко (⚠ колонки счётчик читает С КОНЦА — испр. D39.116) · **блокеров очереди 0**, платные прогоны разблокированы · «скоро» **32** (перечень — грепом по таблице, рукописный список снят D39.126) · гейт-строки эксп-22: 55·153 (плюс 150 — руки владельца); строка 5 — остаток гейчен ре-пробой 74 (D39.102 п.2); остальное «когда-нибудь». ⚠ Счёт — НИЖНЯЯ граница долга, не потолок (разбор — легенда таблицы ниже).
> - Архивы хроники: `archive/PROGRESS-2026-07-04-10.md` (D31) · `-10-13` (D39.6-гигиена) · `-13-25` (стройка паков 1116, rerun2) · **`-25-31` (паки 1720 · мини-прогон · полигон-пакеты 58 · ToS · холодный прогон; D39.2658)** · **`-08-01-02` (сессии №9/№10: общность · качество банка · coldrun-b · открытие фронта/платформы; D39.5990, срез D39.105)** · **`-08-02-04` (сессии №11№13: ручки эффорта · стандарты · контракт API · платформа P0 · банк-арбитраж; D39.91105 + снимок шапки эры №15)**. Записи ниже — живой хвост (№14+, эра D39.106+).
## Бэклог (ЕДИНЫЙ, собран 26.07, актуализация 04.08 D39.99/D39.101; правки — только через оркестратора)
@ -112,7 +112,8 @@
| 164 | **Rebill-проекция `status` с nil baseMemory завышает RebillUSD**: read-путь не материализует base-банк → renderedContentHashes даёт пустую инъекцию → repin недоопознан, строка считается re-pay (консервативная сторона, но противоречит комменту «status can never quote a different number than translate»); материализовать base-банк на read-пути либо смягчить клейм | бэкенд | когда-нибудь | малое касание | **D39.122**, приёмка (охотник №1); preexisting |
| 165 | **Различимые exit-коды tmctl: потолочная остановка и graceful-stop** (сегодня оба = exit 1 ⇒ платформа отдаёт `failed` и на штатный стоп, и на исчерпание потолка — PD-113 платформы; после поправки формулы потолка (D39.123, PD-158) стоп по потолку = ровно исчерпание холда, цена неразличимости выросла; вскрыто приёмкой раннера на РЕАЛЬНОМ tmctl — фейк пробы умирал от сигнала) | бэкенд | скоро (вместе с эмиттером 103) | бэкенд-пак эмиттера | D39.123 |
| 166 | **Движковая поверхность оценки $/глава** (ставка-константа платформы $0.03 из exp08 v2 — временная мера беты, П-10 зоны платформы; честная оценка по книге/паре — движковая) | бэкенд | когда-нибудь | отдельный пак | D39.123 |
| 167 | **Докс-пак по аудиту D39.125 — ЯДРО ИСПОЛНЕНО (D39.126):** реестр всех нот `architecture/05-decisions-index.md` · counts.py `--check` полнота реестра + `--lint` якорей · разгрузка топ-10 строк · входы 15-money-path/16-events-emitter/STACK.md · К-статусы сведены · 09-target/product-requirements освежены. **Остаток строки:** статус-баннеры экспов + шапка эндпоинтов quirks (полигон — пинг при лендинге эксп-22) · DEFECT_REGISTER секциями (зона платформы, пинг отправлен 09.08) | доки/полигон/платформа | скоро (остаток — лендинг эксп-22) | пинги зонам | D39.125, **D39.126** |
| 167 | **Докс-пак по аудиту D39.125 — ЯДРО ИСПОЛНЕНО (D39.126):** реестр всех нот `architecture/05-decisions-index.md` · counts.py `--check` полнота реестра + `--lint` якорей · разгрузка топ-10 строк · входы 15-money-path/16-events-emitter/STACK.md · К-статусы сведены · 09-target/product-requirements освежены. **Остаток строки:** статус-баннеры экспов + шапка эндпоинтов quirks (передано полигону промтом фазы Д, §Д8 п.12) · DEFECT_REGISTER секциями (зона платформы, пинг отправлен 09.08) | доки/полигон/платформа | скоро (остаток — фаза Д полигона) | пинги зонам | D39.125, **D39.126** |
| 168 | **Состав волн пайплайна = пар-конфиг (данные), не общий хардкод**: вопрос владельца 10.08 «нормально ли под китайский строить весь бэкенд и конфиги?» — жильцы ролей и промпты уже конфиг/langpacks, но ТОПОЛОГИЯ волн (перепис · точечный edit-контур · без второго прохода) одна на всех пар; если фаза Д покажет разных победителей по парам (H-3/H-4: en-черновик+edit против zh-связки), состав волн уезжает в пар-конфиг данными; ревью-вопрос общности §0 `12-go-style-notes.md` | бэкенд | когда-нибудь (после данных фазы Д) | замер (фаза Д) → решение владельца → малое касание | слово владельца 10.08, промт фазы Д |
| 49 | **Этапы Б+В спеки D15.2** (content-addressed resume / `guard_hash` — D39.31 сознательно не строил) ; этап В = tmctl export-контракт D29.1 (annot-v1 · цвет-мап+rollup · политика красных fail-closed) + операторский протокол-документ релиза гендер-твиста (D5.1): сам механизм УЖЕ построен — until_ch-правка → точечный re-edit со сметой и $0-пере-пином (`repin.go`/`rebill.go`, D39.42 п.5/D39.45) | бэкенд | скоро (ПОДТЯНУТА D39.81: annot-v1 = источник замечаний для фронта — критический путь подключения) | отдельное решение (annot-половина — по контракту 14, D39.99) | D39.34(4), D33 п.5, D39.81 |
| 49а | **ALTER-шаги миграций v8v14 не идемпотентны вопреки шапке `migrate.go:9-11`** (полу-применённая БД не сходится; счётчик версий скрывает) — находка критика полноты пака-19 | бэкенд | когда-нибудь | отдельное решение | D39.56, PACK19_BUILD §6.9 |
| 50 | F3-остаток идемпотентности | бэкенд | когда-нибудь | отдельное решение | D39.34(4) |
@ -190,13 +191,30 @@
## Полигон
**ФАЗА Д «ДОВОДКА ЭКСП-22/23» — ИДЁТ (10.08).** Заказ `docs/POLYGON_EXP2223_REDO_SESSION_PROMPT.md`, пре-рег Д0 в `docs/experiments/23-editor-tier.md`, код `eval/dovodka/`. Потолок фазы **$4.50** словом владельца (промт называл $10.00 → $4.00 → $4.50 под починку дизайна). Покупок на момент записи ноль.
⚠ **Три поправки к уже сданным числам, которые оркестратору надо знать при ратификации.**
1. **Счёт агент-сессий эксп-23 = 58, не 50** — репликация §12а выпадала из счёта в §6 и §14 п.9; обе строки поправлены. Счётчик капа в паке 23 НЕ работал (на диске 20 записей против 58 фактических): `log_run` звался из `--ingest`, то есть после суб-агента. В фазе Д починено механизмом — `eval/dovodka/runs.py` пишет ДО запуска, атомарный замок с проверкой живости PID, селфтест 16/16.
2. **`p` эксп-23 пере-считаны ТОЧНО.** При n>20 риг брал Монте-Карло, и одна величина печаталась как 0.1603 и как 0.1608. Перевесы целые ⇒ точный ответ берётся динамикой мгновенно: **0.1601** (боевой прогон и runA), **0.3339** (runB). Вердикты не двигаются.
3. **Регекс банка ловил корень в СЕРЕДИНЕ чужого слова** («Го-РОд Цинчжоу» под канон «род Цинь») — 30 ложных из 2113 = 1.4%. Починено границей слова; **печатные числа покрытия НЕ изменились** (кап `min(hits, n)` их поглощал), и в `verify22` заведён сторож покрытия — прежде эти 18 чисел не сторожил никто.
**Адверсариальное ревью трёх линий (снизу вверх · сверху вниз · приёмка Fable-5 — разрешение D39.120 наконец использовано).** 25 находок, 4 блокера, все подтверждены исполнением. Крупнейшие: MDE в пре-реге не нёс поправку Холма ⇒ две оси были объявлены несущими, будучи недомощными по построению (тот же класс, что en-ось эксп-22); `done()` журнала шла мимо замка и теряла 19 записей `claim` из 20; крючки судейского рига протекали между паками (эксп-22 молча читал каталоги эксп-23); счётчик «сам-себя» в `conformance.py` не срабатывал никогда. Всё исправлено до покупок.
**Пробел заказа, найденный приёмкой другого семейства:** дизайн фазы не отвечал на H-1 и H-4. Заведены армы A6 (dspro-черновик + контуры) и E6 (второй редактор на en); владелец поднял потолок под них. **H-4 остаётся ОПИСАТЕЛЬНОЙ** — при n=16 порядок жильцов не разрешается, объявлено до денег.
**Оркестратору — мёртвая ссылка в его зоне:** `docs/README.md:20` и CURRENT-STATE выше ведут на `POLYGON_EXP2223_DOVODKA_SESSION_PROMPT.md`; файла нет, промт называется `..._REDO_...`. В отчётах 22/23 ссылка поправлена.
**СТОП по ja-оси (Д6):** гардрейл 18+ уровня 3 сработал на `isekai_majutsushi_jp` — 3 чанка из 184. Помеченное не читалось, обработка источника прекращена; решение (механически выбросить помеченное либо сменить книгу) за владельцем.
**ЭКСП-22 «ПАНЕЛЬ ЖИЛЬЦОВ» — ЗАКРЫТ, ГОТОВ К ЛЕНДИНГУ (09.08).** Отчёт `docs/experiments/22-tenant-panel.md`, харнесс `eval/tenant_panel/`. Гейты код 0: `itog22.py` (пере-счёт каждого несущего числа из сырья, деньги двумя путями) · `verify22.py` (85 проверок) · `eval/conformance.py` (сверка с буквой заказа — новый зонный инструмент) · `verify_report.py` эксп-21 не задет. Пак $3.409743 при потолке $6.50, 40 агент-запусков из 60, контроли чисты во всех четырёх проходах.
**Гейт `eval/conformance.py` в строке выше засчитан ошибочно.** Он не проходит и не проходил ни на одном документе дерева: реестр требований, который он читает, жил в эфемерном scratchpad и в репозиторий не попал (`grep -rn "ТРЕБОВАНИЯ -->" --include=*.md .` — ноль). «Сверка с буквой заказа» пере-снятию не подлежит; на неё опираться нельзя. Найдено аудитом 09.08.
**Результаты — в блоке ИТОГ отчёта, здесь не дублируются.** Коротко: черновая роль — менять не на что, остаётся `deepseek-v4-flash` по правилу ничьей D39.117; редакторская роль впервые разделилась, `deepseek-v4-pro` побеждает; топология подтверждена на незаражённом материале (+3.25 и +4.44 против +2.50/+2.81 эксп-21); на пару en не переносится.
**Три ограничения вывода — читать вместе с ИТОГом.** (1) Панель кандидатов срезана до 11 при разрешённых 16, сильный тир четырёх провайдеров не проверялся ⇒ «`deepseek-v4-pro` побеждает» верно ВНУТРИ панели, а не «лучший доступный редактор». (2) Контраст `glm-5` 2.12 стоит между двумя порогами (2.03 пред-объявлен, 2.47 собственный пол прохода) ⇒ вопрос о Резерве D39.22 не решён. (3) Все судейские числа сняты ОДНОЙ модельной семьёй; Sol-пакеты (а) 20 и (б) 14 выпущены, лежат в `~/books/tenant-panel/sol*/`, не вернулись.
**Три ограничения вывода — читать вместе с ИТОГом.** (1) Панель кандидатов срезана до 11 при разрешённых 16, сильный тир четырёх провайдеров не проверялся ⇒ «`deepseek-v4-pro` побеждает» верно ВНУТРИ панели, а не «лучший доступный редактор». (2) Контраст `glm-5` 2.12 стоит между двумя порогами (2.03 пред-объявлен, 2.47 собственный пол прохода) ⇒ вопрос о Резерве D39.22 не решён. (3) Все судейские числа сняты ОДНОЙ модельной семьёй. **Sol-пакет (а) прогнан владельцем 09.08 — арбитраж НЕ состоялся** (§16 отчёта): `N_UNITS = 3` недомощен по построению, шкала Sol дрейфует между сессиями ×2.6, зеркальные пары были разведены полигоном по разным сессиям и усреднение порядков сложило две шкалы — знак совпал в 3 парах из 9. Уцелело: декой пойман 2/2, позиционное смещение замерено (4.61, стабильно). Пакет (б) НЕ запускать до починки оснастки.
**Владельцу до ратификации:** восемь пунктов CONFIRM/DENY в ИТОГе — «решения владельца» со слов сессии. Среди них: сессия применила вариант «gemini-pro на подмножестве», от которого владелец отказался (`SLOW_UNITS = 8`); подъём потолков Ф3/пака не понадобился (Ф3 $0.63 при исходных $1.60). Диспозиции строк 55/150/151/152/153 — в отчёте.
**Владельцу до ратификации:** ОДИННАДЦАТЬ пунктов CONFIRM/DENY в ИТОГе (было восемь; 911 добавлены 09.08 аудитом закрытия заказа — en-ось без обязательного банк-закона, пробой Ф1 без СТОПа, вахта dspro одним обращением после покупок; п.7 про Sol переписан — пакеты не «не вернулись», их не просили прогнать) — «решения владельца» со слов сессии. Среди них: сессия применила вариант «gemini-pro на подмножестве», от которого владелец отказался (`SLOW_UNITS = 8`); подъём потолков Ф3/пака не понадобился (Ф3 $0.63 при исходных $1.60). Диспозиции строк 55/150/151/152/153 — в отчёте.
**ИНЦИДЕНТ САМОПРОВЕРКИ — ОРКЕСТРАТОРУ, ко входу по D39.120 (09.08).** Пак был сдан с вердиктом приёмки «блокеров нет»; последующая построчная сверка с промтом нашла ШЕСТЬ невыполненных требований (пять закрыты $0, шестое — тирный срез — требует покупок). Три механизма, по убыванию полезности для нормы:
@ -209,6 +227,30 @@
**Жалоба по процессу.** Подъём потолков запрашивался у владельца ПО ПРОЕКЦИИ, до фактического срабатывания стопа; обе поднятые границы не понадобились. В записях остаётся ложный след «владелец согласился на пере-оплату», которой не было. ⇒ Спрашивать при срабатывании стопа, не при прогнозе.
**ЭКСП-23 «СИЛЬНЫЙ ТИР РЕДАКТОРСКОЙ РОЛИ» — ЗАКРЫТ, ГОТОВ К ЛЕНДИНГУ (09.08). ⚠ ЗАКАЗА НА ЭТОТ ПАК НЕ БЫЛО.** Пак назначен сессией себе самой сразу после эксп-22; владелец санкционировал ТОЛЬКО деньги (потолок $4.00), промта не существует. Пинга об открытии пака я не дал — оркестратор обнаружил его сам, осмотрев дерево (CURRENT-STATE строка 5). Это и есть пропуск, отдельно от результатов. Отчёт `docs/experiments/23-editor-tier.md`, харнесс `eval/editor_tier/`, сырьё `~/books/editor-tier/`. Потрачено **$2.907359** при потолке $4.00, дополнительная санкция $5 не понадобилась; 50 агент-сессий из капа 60.
**Что закрывает.** Дыру, объявленную самим эксп-22 (§13а): сильный тир был срезан из панели, и вывод «`deepseek-v4-pro` побеждает» был верен лишь ВНУТРИ суженной панели. Теперь тир проверен — `gpt-5.6-terra`, `grok-4.5`, `glm-5.2` поверх ТОЙ ЖЕ якорной базы на тех же 16 единицах. **Ни один не отличим** от боевого редактора (+0.50 / +0.19 / 0.19 при пороге 1.02), при цене в 26 раз выше. Позитивный контроль «редактор против голого черновика» +2.06 (Холм 0.0039) показывает, что прибор не слеп. Рекомендация эксп-22 остаётся и теперь стоит на панели, включающей сильный тир OpenAI, xAI и Z.AI. Ограничение (2) из блока эксп-22 выше — СНЯТО.
**Резерв D39.22 (`glm-5`).** На 32 единицах (16 эксп-22 + 16 новых) контраст **0.81 [1.81, +0.28], Холм 0.16 при пороге 1.48 — ниже порога различимости.** Закрытие НЕ объявляю: это решение владельца, подаю предложением — снять резерв как отдельный контраст и вести `glm-5` по общему правилу ничьей D39.117, которое оставляет `deepseek-v4-pro` как более дешёвого. ToS-гейт Z.AI на прод-выбор не отменяется. Ограничение (2) блока эксп-22 про «2.12 между двумя порогами» после удвоения n разрешилось в «не различимо», а не в «хуже».
**Главный результат пака — методический, и он про приборы, а не про модели.** Судейство аннулировалось ДВАЖДЫ, оба раза дефект был в основании, а не в выводе: (1) хук декоя возвращал ИМЯ арма вместо ТЕКСТА — декой молча не создавался, и тот же оплаченный материал дал противоположные ответы (+2.00 «бьёт» против +0.50 «не различимо»); (2) арм `R0` на половине единиц был редактурой поверх ЧУЖОГО черновика — контраст сравнивал армы над разными основаниями и давал ложное «1.53, значимо хуже». Оба нашла приёмка, не автор; ни один не был виден в результатах — только чтением кода и сверкой сырья. Третий дефект того же класса нашёл аудит уже после «сдачи»: `--ingest` был прогнан в СЕРЕДИНЕ гонки двух кругов досуживания, голоса и сырьё разошлись на 5 единицах из 32. ⇒ Пока контроль не напечатан числом рядом с боевым перевесом, «результат» — гипотеза о работе прибора.
**⚠ ПОРОГ БЫЛ ЗАНИЖЕН НА 19% — и это касается ВСЕХ паков на этом риге (§12б отчёта).** Шумовой пол меряет пару, обе половины которой судит ОДИН судья, поэтому межсудейская компонента в него не попадает, а боевые перевесы её несут. Разложение по 8 сессиям: внутри сессии sd 2.903, между сессиями sd 1.030, порог 1.48 → **1.76**. Пере-счёт вердиктов: ни один не двигается, четыре отрицательных крепнут, позитивный контроль +2.06 проходит и по строгому порогу 1.94. Направление риска — пак мог ПРОЗЕВАТЬ разницу, а не выдумать её. Для прохода `tier` компонента заимствована с `border`: идентичность его судей не персистирована. **В норму рига: пол брать парой, чьи половины судят РАЗНЫЕ сессии.**
**Репликация новым жребием судей (заказана владельцем, §12а отчёта).** Тот же ключ, те же тексты, новая восьмёрка судей: перевес 0.62 против 0.81, вердикт «ниже порога» воспроизвёлся, расхождение средних **0.19** при ожидании ≈1.0. Корреляция по единицам +0.63, знак совпал 26/32. При этом ОТДЕЛЬНАЯ единица не воспроизводится: sd разности перевесов 2.78 — новый жребий переставляет армы примерно в каждой пятой главе. Устойчиво только среднее. Цена 8 сессий, покупок нет; по паку 58 из капа 60.
**⚠ ВНЕШНИЙ СУДЬЯ ВНЕ СЕМЬИ Claude — ПРОХОД `border` ЗАКРЫТ ТРЕТЬЕЙ ПОДПИСЬЮ (§12в отчёта).** Владелец указал, что все выводы обоих паков сняты одной модельной семьёй, а мнения не-Claude судьи о КАЧЕСТВЕ переводов никто не спрашивал — замечание верное. Внешнему судье отданы ТЕ ЖЕ файлы заданий, что судили агенты пака. Принято 32/32, декой 32/32, перевес **1.00 при его пороге 3.53** против моих 0.81 и 0.62 — вердикт «ниже порога различимости» совпал у всех троих. ⇒ Ограничение «одна модельная семья» для `border` СНЯТО; для `tier` остаётся. Пере-снимается `eval/editor_tier/solscore.py`. Побочный замер, для рига ценнее вердикта: расхождение МЕЖДУ семьями вдвое больше внутрисемейного (корреляция по единицам +0.32 против +0.63), и внешний прибор вдвое грубее (пол sd 5.05 против 2.12). ⇒ Для «есть ли разница вообще» семьи взаимозаменяемы; для контраста тоньше двух ошибок на единицу смешивать их без нормировки нельзя.
**Починки прибора (переносимы в следующие паки).** `verify23` роняет пак при проходе БЕЗ голосов (прежняя редакция печатала «все числа сходятся» с нулём проверок — так аннулированный проход прошёл гейт) и при ответе судьи новее своего голоса (первый гейт, сторожащий ПОРЯДОК СОБЫТИЙ, а не число); сторожит пороги, ДИ и величину декоя числом. `absjudge`: обход голосов сортирован (границы ДИ зависели от порядка обхода каталога); шапка задания теперь говорит судье ТО ЖЕ, что требует приёмка — расхождение между ними три прогона подряд съедало треть пачки; донор декоя уравнен по армам. `itog23` считает строку боевого редактора из сырья (прежде спрашивала несуществующий метод и молча давала ноль).
**⚠ ОРКЕСТРАТОРУ, ко входу по D39.120 — ВТОРОЙ ИНЦИДЕНТ ПРИЁМКИ, класс новый.** Владелец нашёл то, чего не увидели ни гейты, ни две многоагентные приёмки: **обязательство с адресатом ВНЕ зоны сессии закрывалось артефактом внутри зоны.** Образец — Sol: промт эксп-22 (стр.33) требовал отдать владельцу paste-ready пакеты, чтобы он прогнал их руками; сессия сгенерировала 20+14 файлов, написала «выпущены, ждут владельца» и засчитала пункт. Каталоги `answers/` пусты — ноль вернулось, потому что ноль было запрошено. Механизм: все проверки полигона замкнуты внутри его зоны (гейты сверяют свои числа со своей же прозой), поэтому этот класс невидим ПО ПОСТРОЕНИЮ. ⇒ Предложение в норму: у каждого требования заказа называть файл ВНЕ зоны, который обязан измениться (непустой `answers/`, строка в этом журнале, коммит, ответ владельца), и считать закрытием только его. Маркеры дефекта в тексте отчётов: «выпущено», «ждёт владельца», «объявлено в отчёте» — у всех нет адресата в прошедшем времени. Пакеты Sol владельцу переданы 09.08.
**Владельцу до ратификации — девять пунктов CONFIRM/DENY в §14 отчёта.** Ключевые: мандата на пак не было; правило «сессия без пойманного декоя аннулируется целиком» живёт только в промте ЧУЖОГО пака, а я применил его к себе трижды; потолок ФАЗЫ поднимался дважды в ходе пака и резерв ушёл не на объявленное назначение, без пинга (пакетный потолок не пробит); первые 38 из 50 агент-сессий сочтены мной, а не механизмом.
**⚠ ЛЕНДИНГУ ЭКСП-22.** Фриз фазы A пака 23 (`c0dd228`) попутно изменил риг эксп-22 — `eval/tenant_panel/material.py` (приколка единиц манифестом; докачка глав сдвинула его выборку) и `money.py`. Гейты эксп-22 после этого пере-сняты: `verify22.py` и `itog22.py` код 0. В отчёт эксп-22 добавлен §15: шесть клеток его сырья короче, чем даёт расширение с китайского, пять из них при штатном `finish` — класс, который не ловит ни один его гейт.
**Запись оркестратора №16, 10.08 — ПРИЁМКА ЭКСП-22/23 ПРОВЕДЕНА, дерево залендено, фаза Д заказана.** Верификация: 11 агентов, 105 проверок — все несущие числа обоих паков воспроизведены из сырья МИМО харнесса (деньги до цента, провенанс до секунд, leave-one-out по судейским сессиям устойчив), фальсификаций нет; находки приёмки и чек-лист поправок тел — §Д8 промта фазы Д (крупнейшая: интерпретация §15 эксп-22 опровергнута — 5 из 6 клеток суть пойманные гейтом эхо-первые-попытки, судились ре-гены). Поправки к пингу выше (чужой текст не редактирую): сессий по паку 23 = **58**, не 50; декой-правило сессия применяла к себе **дважды**, не трижды; тир-абзац закрывает ограничение **(1)**, не (2) — Резерв D39.22 сессией как раз не решён, отдан владельцу. **Владелец 10.08:** санкции/лимиты/мандат эксп-23 подтверждены задним числом; постановка признана неполной ⇒ **выводы обоих паков НЕ ратифицированы**, заморожены до фазы Д (добивка gemini ретрай-харнессом · внешняя подпись `tier` · несущие оси en/ja · edit-контур — ставка владельца · канон-вскрытие с вопросом судье · поправки тел). Декой-правило и обязательный кросс-семейный опровергатель приёмки одобрены владельцем — в D-ноту при ратификации. Гейт `conformance.py` аннулирован подтверждённо (реестр жил в scratchpad); в фазе Д реестры — только в git.
*(Закрытые блоки полигона — экспы 18/19/20, эксп-21, банк-арбитраж — в [archive/PROGRESS-2026-08-04-09.md](archive/PROGRESS-2026-08-04-09.md), D39.125.)*
(секция параллельной сессии — записи добавлять сюда)

View file

@ -8,6 +8,28 @@
## Эндпоинты и модели (OpenAI-совместимые, если не указано иное)
> ✅ **ЖИВОЙ ЛИСТИНГ `/models` ПЕРЕ-СНЯТ 2026-08-10** (полигон, фаза Д; $0, `eval/role_topology/
> list_models.py`, снимок `~/books/role-topology/models-live.json`). Все семь ключей отвечают.
> Счёт слагов: **deepseek 2 · zai 8 · kimi 12 · openai 124 · gemini 59 · xai 10 · mistral 53.**
> Дельты к колонке «на 2026-07-04», которую таблица ниже НЕ переписывает (она датирована):
> * **zai** — в листинге сверх записанных: `glm-4.5`, `glm-4.5-air`, `glm-4.6`, `glm-5-turbo`
> (то есть все восемь). `glm-4.7-flash`/`glm-4.7-flashx` в `/models` по-прежнему НЕТ — их видно
> только на прайс-странице (находка эксп-22 §0.5, в ростер не берутся).
> * **kimi** — появился **`kimi-k3`** (эксп-23 его замерил: пустой выход при 99.9% размышления
> и $0.0804 за клетку, но под СВОИМ потолком 4000 — вывода об отказном режиме вендора нет).
> * **openai** — линейка ушла далеко вперёд `gpt-5-mini`: живы `gpt-5.4`(+`-2026-03-05`), `gpt-5.5`,
> и тройка **`gpt-5.6-luna` · `gpt-5.6-terra` · `gpt-5.6-sol`**. Первые два меряны паками 22/23.
> * **gemini** — **`models/gemini-3.1-pro-preview` В ЛИСТИНГЕ ЖИВ** (важно: эксп-22 не получил от
> него панели по ПРОВОДУ, а не по отсутствию слага); рядом `gemini-3.5-flash`, `gemini-3.6-flash`,
> `gemini-3.1-flash-lite`. Вся 2.5-серия ещё в листинге — shutdown объявлен на 16.10.2026.
> * **xai** — `grok-4.3` и `grok-4.5` живы, плюс три слага-обёртки `grok-4.20-0309-*`.
> * **mistral** — `mistral-large-latest`/`-2512`, `mistral-medium-latest` живы.
>
> ⚠ **«Слаг живой» ≠ «модель та же»** (урок катовера DeepSeek 31.07, D39.61) — листинг доказывает
> только доступность имени. Цены DeepSeek пере-сняты в тот же день и НЕ изменились (flash
> $0.14/$0.28 cache-hit $0.0028 · pro $0.435/$0.87 cache-hit $0.003625); сноска вендора о
> **готовящемся значительном повышении** на прайс-странице ПРИСУТСТВУЕТ и на 10.08.
| Провайдер | base_url | Модель (на 2026-07-04) | env-ключ |
|---|---|---|---|
| DeepSeek | `https://api.deepseek.com/v1` | **`deepseek-v4-flash`** + `deepseek-v4-pro` — и это **ВЕСЬ** листинг (live 25.07.2026). **Катовер ИСПОЛНЕН:** `deepseek-chat`/`deepseek-reasoner` сняты не только из листинга, но и из реестра — `GET /v1/models/deepseek-chat`**404 `Model Not Found`**, алиас-оговорка календаря на них больше НЕ распространяется. Цены пост-катовера НЕ изменились (25.07: flash $0.14/$0.28 cache-hit $0.0028 · pro $0.435/$0.87 cache-hit $0.003625) — `models.yaml` актуален. Эхает zh — см. ниже, брать v4-flash с thinking | `DEEPSEEK_API_KEY` |
@ -57,6 +79,16 @@
⇒ (i) **не слать `reasoning_effort` = ехать на `high`** — вот почему боевая форма упирается в стену (п.10); (ii) на `deepseek-v4-pro` ручка `low` НЕ РАБОТАЕТ (маппится в `high`); рычаг бюджета размышления существует ТОЛЬКО у flash — **строка В СИЛЕ**: её отзыв от 05.08 сам ОТОЗВАН адверсариальным ревью того же дня (см. 3б), замер §3б признан недиагностичным; (iii) `none` в OpenAI-формате у DeepSeek не существует вовсе (колонка `reasoning.effort:"none"` — Anthropic-формат), запись 04.07 «`none` → 400» в силе.
**3в. ВАХТА D39.92 ИСПОЛНЕНА ПОВТОРНО 2026-08-10 (полигон, фаза Д; $0, `curl` HTTP 200).**
Таблица маппинга 3а на `guides/thinking_mode` **побайтно та же** (снята разбором ячеек `<td>`:
`low`→low/high · `high`→high/high · `xhigh`→high/**max** · `max`→max/max), и сноска (3)
*«We will update the actual mapped effort of deepseek-v4-pro in early August 2026»* **всё ещё на
странице** — то есть «early August» уже прошёл, а дока не обновлена. Change Log свежее
**31.07.2026** записей не имеет. ⇒ Расхождение доки с замером 05.08 (звено «б»: `xhigh` на pro
ведёт себя как отдельный уровень НАД `high`, а не как `max`) **сохраняется и не объяснено
вендором**; статус `low` на pro — по-прежнему «НЕ УСТАНОВЛЕНО». Следующий триггер вахты: любая
новая запись Change Log ЛИБО исчезновение сноски (3).
**3б. ⚠⚠ ЧАСТИЧНО ОТОЗВАНО В ДЕНЬ ПУБЛИКАЦИИ. Звено про `low` НЕДЕЙСТВИТЕЛЬНО; звено про `xhigh` в силе.** (полигон, живой замер 2026-08-05, ревизия того же дня по адверсариальному ревью — `19-editor-contract-q4b.md` §6.2). Исполнение вахты D39.92 (реестр загейченных триггеров, строка 108: «дата > 05.08 ⇒ пере-проба маппинга + сверка changelog»). Триггер сработал по назначению: вендор объявлял смену «early August 2026», и поведение с таблицей 3а больше не сходится.
**Метод:** побайтно ОДИН вход (реальный редакторский дифф-вызов), три арма по 3 розыгрыша, `deepseek-v4-pro`, всё `finish=stop`; N=1 непригоден из-за разброса п.2, решает разделение диапазонов. Харнесс — `eval/editor_contract/effort_probe.py`, сырьё `~/books/gu-zhenren/bank-arbitration/eff-*.json`, $0.044163.

View file

@ -1,5 +1,11 @@
# Эксперимент 01. Токен-калибровка на реальных текстах
> ⚠ **СТАТУС (баннер фазы Д, 10.08): ЗАКРЫТ, фактура жива, ГРАНИЦА объявлена.** r-множители из этого
> прогона действуют; сняты они на public-domain классике, и `eval/README.md` правило 2 требует
> считать выводы на претрейн-классике ПРЕДВАРИТЕЛЬНЫМИ до вебновелл-среза. Денежная модель, которая
> ими пользуется, — `08-cost-model-v2.md` (там же датировка цен).
Дата прогона: 2026-07-04. Статус: **завершён** (public-domain калибровка; уточнение на реальных вебновеллах — по мере поступления файлов владельца).
## TL;DR

View file

@ -1,5 +1,12 @@
# Эксперимент 02. Refusal/excision-бенчмарк 18+ (черновик корпуса и стенд)
> ⚠ **СТАТУС (баннер фазы Д, 10.08): ЗАКРЫТ, но код отчёта — ЖИВОЙ КОНТРАКТ.** `split_sentences`/
> `classify_output` из `eval/refusal_bench.py` — приёмочный оракул coverage-гейта Go, паритет
> закреплён Go-тестом; любое изменение идёт Python-first, лок-степ (`eval/README.md`, карта).
> Правовая (ToS) сторона 18+ живёт НЕ здесь, а в `00-provider-quirks.md` (таблица прав, ре-чек
> 31.07, следующий 25.10) — это ДРУГАЯ ось, чем поведение моделей в этом отчёте.
Дата: 2026-07-04. Статус: **первый прогон выполнен** (6 провайдеров × 11 SFW/L1/L2-violence фрагментов; explicit-категории ждут фрагментов владельца). Основание: `docs/research/11-gap-2.md` §8 и Выводы п.5; `01-decisions.md` Р4.
## Результаты первого прогона (2026-07-04)

View file

@ -1,5 +1,11 @@
# Эксперимент 03. Локальный стенд: скорость, память, качество
> ⚠ **СТАТУС (баннер фазы Д, 10.08): ЗАКРЫТ.** Вердикт «локаль не годится в роль переводчика»
> действует; локаль осталась в проекте как стенд эмбеддингов и дешёвых не-переводческих ролей.
> Числа привязаны к железу стенда (GTX 1070 8GB) и к моделям, установленным тогда, — на другом
> железе это НОВЫЙ замер, а не репродукция (`eval/README.md`, правило 5).
Дата: 2026-07-04. Задача 3 полигона. Основание: [research/06-local-models.md](../research/06-local-models.md).
Скрипты: `eval/local_bench.py` (dense-модели через ollama), `eval/llama_moe_bench.sh` (MoE через llama.cpp). Сырые переводы: `eval/data/local_bench*/`.

View file

@ -1,5 +1,11 @@
# Эксперимент 06. Экстракция терминов: локаль vs облако (write-path банка памяти)
> ⚠ **СТАТУС (баннер фазы Д, 10.08): ЗАКРЫТ, ГЛАВНОЕ ЧИСЛО ПОД ОГОВОРКОЙ.** recall 0.981.0 снят
> на PD-классике; вебновелл-ре-замер объявлен отдельной стадией (`research/20` §D-A6), и до него
> число читается как предварительное (`eval/README.md`, карта + правило 2). Действующая линия
> банк-майнинга — `research/20` и `16-bank-mining.md`, не этот отчёт.
Дата: 2026-07-04. Закрывает дыру **G1** реестра рисков (`architecture/06`: «качество популяции глоссария не измерено»). Скрипт: `eval/extract_bench.py`; сырьё: `eval/data/extract_bench/results.json`.
**Зачем.** Горячий путь банка памяти модель НЕ использует (детерминированный матч). Локаль касается банка в 2 точках: эмбеддинги (exp там же → bge-m3) и **экстракция кандидатов в глоссарий из сырого текста** (bootstrap write-path). Полигон exp03 мерил локаль как ПЕРЕВОДЧИКА (вердикт: не годится). Экстракция — другая, более лёгкая задача; здесь меряем её отдельно и **верифицированно**.

View file

@ -1,5 +1,11 @@
# Эксперимент 07. Precision (доля ложных срабатываний) excision coverage-гейта
> ⚠ **СТАТУС (баннер фазы Д, 10.08): ЗАКРЫТ, ОДИН ВЫВОД НЕ ПЕРЕНОСИТСЯ.** Precision гейта (0 FP/57)
> действует. А вывод «`content_filter` мёртв» на Gemini 3.x НЕ переносится: там `PROHIBITED_CONTENT`
> — неконфигурируемый фильтр-класс, приходящий СОСТАВНОЙ строкой `finish_reason` и иногда вовсе
> без объекта `message` (`00-provider-quirks.md`, D22.6/D22.8а + дополнение эксп-21).
**Дата:** 2026-07-05 · **Зона:** полигон · **Гейтит:** боевой флип `gates.coverage.enabled:true` (D12/Q4).
## TL;DR

View file

@ -1,5 +1,11 @@
# Эксперимент 10 — 18+ рука (explicit/dark) на 蛊真人 (D14.4, последний critical)
> ⚠ **СТАТУС (баннер фазы Д, 10.08): ЗАКРЫТ (эмпирика поведения).** Читать вместе с ПРАВОВОЙ осью:
> готовность модели переводить не даёт права её звать. По `violence` права нет НИ У ОДНОГО вендора
> — таблица прав и главный вывод в `00-provider-quirks.md`. Состав канала B с тех пор менялся
> (D19.1/D22.4); брать отсюда модели по именам без сверки с квирками нельзя.
**Дата:** 2026-07-09 · **Зона:** полигон · **Гейт:** D14.4 (18+ эмпирика — единственный critical ревью 07.09) + D14.2 (Mistral как фолбэк канала B) + D3 (Grok = канал B + судья 18+).
Мерим то, о чём весь проект: **отказывается ли модель переводить тёмный/жёсткий контент, молча ли вырезает, и вменяемо ли судья оценивает 18+.** Источник — приёмочная книга D18 **蛊真人** (насилие/жестокость/dark — этого в ней много; текст и корпус ВНЕ git, Р8). **Модели — БОЕВОЙ канал B** (grok-4.3, Mistral-кандидат, локальная abliterated), DeepSeek — КОНТРОЛЬ.

View file

@ -1,5 +1,12 @@
# Эксперимент 11 — эротика-рука 18+ канала B (D14.4, закрытие последнего critical)
> ⚠ **СТАТУС (баннер фазы Д, 10.08): ЗАКРЫТ (эмпирика поведения).** Та же оговорка, что у эксп-10:
> право отдельно от поведения. Ключевой провод-факт отсюда действует и вписан в квирки: на
> ГРАФИЧНОЙ эротике Gemini 3.x fail-closed `PROHIBITED_CONTENT`, фильтр неконфигурируем, нативный
> API не спасает (D22.6) ⇒ судья эротики — только Grok. Клетка `gemini × sexually-explicit` в
> таблице ПРАВ с тех пор сменилась `FORBIDDEN``UNCLEAR` (амендмент D39.32 п.2).
**Дата:** 2026-07-10 · **Зона:** полигон · **Гейт:** D14.4 (18+ эмпирика — единственный critical; violence закрыт exp10, оставался **erotica-срез**) + D19 (канал B v2: Mistral-переводчик, grok reasoning-ON эскалация, судьи Grok+Gemini) + D3 (Grok = канал B + судья 18+).
Сестринский прогон к [`10-explicit-benchmark.md`](10-explicit-benchmark.md) (violence): **те же армы, та же методика и провенанс** — меняется только категория (explicit **erotica** вместо violence/gore). Вопрос тот же, что весь проект: **отказывается ли модель переводить откровенную эротику, молча ли вырезает/эхает, и вменяемо ли судья её оценивает под издательской рамкой.**

File diff suppressed because it is too large Load diff

View file

@ -28,6 +28,24 @@
| `providers.json` | базовые URL/слаги для зондов | квирки — `docs/experiments/00-provider-quirks.md`, читать ПЕРЕД любым вызовом |
| `promptlang/` | Р6+P4 (закрыт D39.61): язык промптов · анти-эхо wiring · верность митигаций; действующая редакция выводов = §12 отчёта `docs/archive/reports/POLYGON_PROMPTLANG_ANTIECHO_2026-07-31.md` | `slugcheck.py`, `wire_probe.py` (сырой провод), `run_arms.py`, `spend.py` (два пути счёта денег — прогонять оба), `judge_*.py` |
### Действующие харнессы паков 1823 и фазы Д (добавлено эксп-22 08.08 — в карте их не было)
| Папка | Эксперимент | Что внутри и чем полезно дальше |
|---|---|---|
| `bank_arbitration/` | exp-16/18 + research/24 | `editor_wire_probe.py` — рендер боевых промптов (`render`, `strip_comments`, `editor_block`, `HEADER_LAW_PLAIN`/`HEADER_LAW_CLAUSE`), окна и посадки; `inject_probe.py` — глоссарий-блок переводчика и `render_translator`. **Это фундамент всех последующих паков**, ломать нельзя |
| `editor_contract/` | exp-19 (дифф-контракт Q4b) | `probe.py`, `apply.py` (аппликатор, самотест 15/15), `effort_probe.py` (вахта маппинга эффорта DeepSeek) |
| `editor_harness/` | exp-20 (контракт редактора × модели) | `probe.py` — сборка сообщений по контрактам (`full`/`diff-*`/`locate`/`direct`), en-зеркала промптов |
| `role_topology/` | **exp-21 (топология ролей)** | `absjudge.py` — абсолютный судейский риг (слепые метки · декой · пол · обоснование цитатой · семейство в ключе · Холм), `battery.py` — 11 детерминированных проверок, `buy.py` — касса с проекционным гардом, `bakeoff.py`/`pair_en.py` — армы zh/en, `itog.py`/`verify_report.py` — образец гейтов чисел, `ruff.toml` — линтер зоны |
| `tenant_panel/` | **exp-22 (панель жильцов)** | `material.py` — добыча и отбор невиданного среза, `contam.py` — проба контаминации, `bank.py` — банк книги + метрика покрытия канона, `roster.py` — ростер и цены с провенансом, `money.py` — касса с фриз-гейтом (покупка отказывает незакоммиченному коду) и `configure()` для пере-настройки под другой пак, `judge.py` — тот же `absjudge`, настроенный на другую панель, `sol.py` — пакеты внешнему судье (⚠ конструкция признана негодной, §16 отчёта) |
| `editor_tier/` | **exp-23 (сильный тир редактора и граница `glm-5`)** | `screen.py` — скрин сильного тира, `panel.py` — панель редактур поверх якорной базы, `judge.py` — два прохода (`tier`/`border`) + режим `--run <прогон>` для ОТДЕЛЬНЫХ прогонов того же прохода (репликация), `money.py` — касса эксп-22 через `configure`, `itog23.py`/`verify23.py` — пере-счёт и гейт чисел (сторожат пол, порог, декой, наклон и СВЕЖЕСТЬ разбора), `solscore.py` — счёт ответов ВНЕШНЕГО судьи вне семьи Claude |
| `dovodka/` | **фаза Д (доводка 22/23, заказ владельца 10.08)** | `runs.py` — журнал агент-сессий: запись ДО запуска суб-агента, кап 80, замок против ГОНКИ сессий по одному токену (в паке 23 гонка стоила соответствия голосов и сырья на 5 единицах) |
> ⚠ **Две мины импорта, на которые наступает каждый новый пак.**
> (1) Модуль `probe.py` существует И в `editor_contract/`, И в `editor_harness/` — какой из них
> подхватится, решает ПОРЯДОК `sys.path.insert` в вызывающем файле. Проверять фактический путь
> (`probe.__file__`), а не догадываться.
> (2) Сырьё паков живёт в `~/books/<пак>/`; путь зашит константой. Если каталог переехал —
> харнесс молча видит пустоту и печатает «голосов нет» вместо ошибки.
### 18+ / корпус-билдеры
| Скрипт | Эксперимент | Заметки |
|---|---|---|

View file

@ -25,9 +25,22 @@
проходящая улика, а не что улика доказывает именно это требование: связь «требование улика»
устанавливает автор таблицы, и подменить её ничего не мешает. Это сдвигает работу с «вспомнить всё
из прозы» на «объявить улику заранее» но не отменяет чтения.
**ДВА ДЕФЕКТА, НАЙДЕННЫЕ ПРИЁМКОЙ НА ПАКЕ 23, ЧИНЯТСЯ ЗДЕСЬ** (фаза Д, 10.08):
1. **Реестр жил в эфемерном scratchpad.** `--final` печатал зелёное против таблицы, которой в
репозитории нет: читатель пере-снять её не мог, а автор мог править её после результата. Теперь
в режиме `--final` источник ОБЯЗАН быть отслежен git и совпадать с закоммиченным (тот же
фриз-гейт, что у кассы `tenant_panel/money.py`). Обход `TM_ALLOW_UNFROZEN=1`, громко.
2. **Отчёт проверял сам себя.** Большая часть улик была грепом формулировок ТОГО ЖЕ документа, то
есть «в тексте написано, что сделано» выдавалось за «сделано». Механически отличить «улика на
печать» от «улики на дело» нельзя, поэтому инструмент их СЧИТАЕТ и печатает долю: улика,
упоминающая сам файл реестра или файл отчёта, помечается `сам-себя` и выносится в итог
отдельной строкой. Ноль таких улик не требуется требуется, чтобы читатель видел их число.
"""
from __future__ import annotations
import os
import subprocess
import sys
from pathlib import Path
@ -36,6 +49,22 @@ REPO = Path("/home/ubuntu/projects/textmachine")
MARK = "<!-- ТРЕБОВАНИЯ -->"
def frozen(src: Path) -> tuple[bool, str]:
"""Реестр отслежен git и совпадает с закоммиченным? Это и есть его фриз."""
try:
rel = str(src.resolve().relative_to(REPO))
except ValueError:
return False, f"реестр вне репозитория: {src}"
r = subprocess.run(["git", "-C", str(REPO), "ls-files", "--error-unmatch", rel], # noqa: S603,S607
capture_output=True, text=True)
if r.returncode:
return False, f"реестр НЕ в git: {rel} — гейт проверял бы файл, которого нет у читателя"
d = subprocess.run(["git", "-C", str(REPO), "diff", "--quiet", "HEAD", "--", rel]) # noqa: S603,S607
if d.returncode:
return False, f"реестр отличается от закоммиченного: {rel}"
return True, ""
def rows(md: str) -> list[tuple[str, str, str]]:
"""Строки таблицы требований: (ID, требование, улика)."""
if MARK not in md:
@ -97,19 +126,46 @@ def main() -> int:
if not src:
print("не указан файл")
return 1
md = (REPO / src if not src.startswith("/") else Path(src)).read_text(encoding="utf-8")
bad = pend = 0
path = REPO / src if not src.startswith("/") else Path(src)
md = path.read_text(encoding="utf-8")
bad = pend = selfref = 0
print(f"СВЕРКА С БУКВОЙ ЗАКАЗА: {src} [{mode[2:]}]\n")
# ⚠ ФРИЗ РЕЕСТРА. Гейт против незакоммиченной таблицы ничего не доказывает: её можно дописать
# ПОД результат, и читатель этого не увидит. Пак 23 прошёл `--final` против реестра, лежавшего
# в сессионном scratchpad, — и «соответствие заказу проверено» из этого не следовало.
if mode == "--final":
ok, why = frozen(path)
if not ok:
if os.environ.get("TM_ALLOW_UNFROZEN") == "1":
print(f"⚠ ФРИЗ-ГЕЙТ РЕЕСТРА ОТКЛЮЧЁН (TM_ALLOW_UNFROZEN=1): {why}\n")
else:
print(f"{why}\n зафризь реестр коммитом ДО сверки (промт фазы Д, нормы рига)")
return 1
# Улика, которая грепает сам реестр/отчёт, доказывает «напечатано», а не «сделано».
# ⚠ Первая редакция искала имя как ОТДЕЛЬНОЕ слово с запретом «/» слева — то есть любая
# улика с ПУТЁМ (а иначе улику не пишут) была невидима, и строка «сам-себя: 0» печаталась
# всегда. Поймано ревью исполнением. Ищем подстроку имени, плюс имена ОТЧЁТОВ фазы: улика,
# грепающая отчёт, доказывает «напечатано», а не «сделано».
selfnames = {path.name, *(Path(a).name for a in args if a.endswith(".md"))}
selfnames |= {f.name for f in (REPO / "docs" / "experiments").glob("2[23]-*.md")}
for rid, req, ev in rows(md):
st, detail = check(ev)
if st == "ОЖИДАЕТ" and mode == "--final":
st = "ПРОВАЛ"
detail = "улики нет, а пак сдаётся"
mark = ""
if any(n in ev for n in selfnames):
selfref += 1
mark = " [сам-себя]"
bad += st == "ПРОВАЛ"
pend += st == "ОЖИДАЕТ"
print(f"[{st:8s}] {rid:6s} {req[:78]}" + (f"{detail}" if detail else ""))
print(f"[{st:8s}] {rid:6s}{mark} {req[:70]}" + (f"{detail}" if detail else ""))
total = len(rows(md))
print(f"\nтребований {total} · ожидают {pend} · провалов {bad}")
print(f"улик вида «греп своего же документа» ({'сам-себя'}): {selfref} из {total}"
+ (" ⚠ они доказывают, что НАПЕЧАТАНО, а не что СДЕЛАНО" if selfref else ""))
if bad:
print("СВЕРКА НЕ ПРОЙДЕНА")
return 1 if bad else 0

199
eval/editor_tier/itog23.py Normal file
View file

@ -0,0 +1,199 @@
#!/usr/bin/env python3
"""ПЕРЕ-СЧЁТ ВСЕХ НЕСУЩИХ ЧИСЕЛ ПАКА ИЗ СЫРЬЯ. $0.
Число, которое нельзя пере-снять командой из репозитория, ничем не отличается от числа по памяти.
Здесь каждый блок ИТОГа печатается заново из персистированных артефактов, деньги двумя
независимыми путями (касса и пере-счёт из сырых `usage` × прайс).
Запуск: eval/.venv/bin/python eval/editor_tier/itog23.py
"""
from __future__ import annotations
import json
import statistics as st
import sys
from pathlib import Path
REPO = Path("/home/ubuntu/projects/textmachine")
ZONE = Path(__file__).resolve().parent
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 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
JU = _load("judge_et", ZONE / "judge.py")
PAN, SCR, MONEY, MAT = JU.PAN, JU.PAN.SCR, JU.MONEY, JU.MAT
ROSTER = SCR.ROSTER
OUT = MONEY.OUT
def h(t: str) -> None:
print(f"\n═══ {t} ═══")
def material() -> None:
h("МАТЕРИАЛ")
old, new = JU.P22.M.units_zh(), MAT.units_new()
print(f" единиц эксп-22 (приколоты манифестом) {len(old)} · глав "
f"{sorted(u['chapter'] for u in old)}")
ln = sorted(len(u["source"]) for u in new)
print(f" НОВЫХ единиц {len(new)} · глав {sorted(u['chapter'] for u in new)}")
print(f" знаков {ln[0]}{ln[-1]} медиана {ln[len(ln) // 2]} · "
f"макс. близость {JU.P22.M._pairwise_max_sim(new):.3f}") # noqa: SLF001
print(f" снято гейтом прав из своего пула: {MAT.rights_dropped(set()) or ''}"
if False else f" пересечений с эксп-22: "
f"{len({u['uid'] for u in new} & {u['uid'] for u in old})}")
def screen() -> None:
h("ФАЗА A — СКРИН СИЛЬНОГО ТИРА (пороги эксп-22, не свои)")
rs = SCR.result()
print(f" {'модель':16s}{'вердикт':>10s}{'ед.':>4s}{'$/ед edit':>11s}{'размышл':>9s} причины")
for m, r in rs.items():
print(f" {m:16s}{r['verdict']:>10s}{r['n']:4d}{(r['price_editor'] or 0):11.5f}"
f"{r['reasoning_share']:9.1%} {'; '.join(r['why'])}")
print(f" прошли: {[m for m, r in rs.items() if r['verdict'] == 'прошёл']}")
print(" СПРАВКА (полный критерий эксп-22, с переводческим порогом, которого нет в пре-реге):")
for m, r in rs.items():
print(f" {m:16s}{r['verdict_full22']:>10s} $/ед transl "
f"{(r['price_translator'] or 0):.5f}")
def judged(pass_: str) -> None:
h(f"СУДЕЙСТВО, проход {pass_}")
JU.setup(pass_)
import absjudge as AJ # noqa: PLC0415
if not AJ._by_unit(): # noqa: SLF001
print(" голосов нет")
return
AJ.controls()
print()
AJ.score()
def positional() -> None:
h("ПОЗИЦИОННЫЙ НАКЛОН — ЗАМЕРЕН И ВЫЧТЕН (требование ЗАКОНа)")
for pass_ in JU.PASSES:
JU.setup(pass_)
import absjudge as AJ # noqa: PLC0415
if not AJ._by_unit(): # noqa: SLF001
continue
print(f" --- {pass_} ---")
AJ.position()
print()
def money_two_ways() -> None:
h("ДЕНЬГИ — ДВА НЕЗАВИСИМЫХ ПУТИ")
led = {ph: MONEY.Guarded(ph).spent() for ph in MONEY.TAGS}
def indep(rec: dict) -> float:
m = rec.get("model")
if m not in ROSTER.CANDIDATES:
return rec.get("cost_usd", 0.0)
_, _, pin, pc, pout, _t, _s = ROSTER.CANDIDATES[m]
pt, cd = rec.get("prompt_tokens", 0), rec.get("cached_tokens", 0)
out = ROSTER.billed_output(m, rec.get("completion_tokens", 0),
rec.get("reasoning_tokens", 0),
rec.get("total_tokens", 0), pt)
return (pt - cd) / 1e6 * pin + cd / 1e6 * pc + out / 1e6 * pout
tot2 = {ph: 0.0 for ph in led}
empty = []
for ph, globs in MONEY.TAGS.items():
for g in globs:
for f in OUT.glob(g):
r = json.loads(f.read_text(encoding="utf-8"))
tot2[ph] += indep(r)
if r.get("cost_usd", 0) > 0 and not (r.get("content") or "").strip():
empty.append((f.name, r["cost_usd"]))
print(f" {'фаза':6s}{'касса':>12s}{'пере-счёт':>12s}{'расхождение':>13s}{'потолок':>10s}")
for ph in MONEY.TAGS:
mark = " ⛔ ПРОБОЙ" if led[ph] > MONEY.CEILINGS[ph] + 1e-9 else ""
print(f" {ph:6s}{led[ph]:12.6f}{tot2[ph]:12.6f}{led[ph] - tot2[ph]:13.6f}"
f"{MONEY.CEILINGS[ph]:10.2f}{mark}")
print(f" {'ПАК':6s}{sum(led.values()):12.6f}{sum(tot2.values()):12.6f}"
f"{sum(led.values()) - sum(tot2.values()):13.6f}{MONEY.PACK_CEILING:10.2f}")
if empty:
t = sum(c for _n, c in empty)
print(f" оплачено за ПУСТОЙ выход: {len(empty)} вызовов ${t:.5f} "
f"({t / max(1e-9, sum(led.values())) * 100:.1f}% пака)")
for n, c in sorted(empty, key=lambda x: -x[1]):
print(f" {n} ${c:.5f}")
def _cost(f: Path) -> float:
return json.loads(f.read_text(encoding="utf-8")).get("cost_usd", 0.0) if f.exists() else 0.0
def economics() -> None:
"""⚠ Строка R0 считается ИЗ СЫРЬЯ эксп-22, а не через `hasattr(P22,"edit_cost")`.
Прежняя редакция спрашивала у эксп-22 метод, которого у него нет (есть только `draft_cost`),
строка молча выходила нулевой и отфильтровывалась, а в отчёт попадали числа, снятые где-то ещё
и друг с другом не сходившиеся (0.00545 × 1500 = $8.18, напечатано $13.45). Поймано приёмкой.
Колонки печатаются ДВУМЯ семействами: редактура отдельно и СВЯЗКА черновик+редактор сравнивать
связку одного арма с редактурой другого нельзя, а именно это отчёт и делал.
"""
h("ЦЕНА РЕДАКТОРСКОЙ КЛЕТКИ, p50 И p95 ПО ЕДИНИЦАМ")
us = JU.P22.M.units_zh()
T22 = JU.P22.OUT
base = [_cost(T22 / f"{JU.P22.tag_draft(PAN.ANCHOR, u['uid'])}.json") for u in us]
rows = [("R0 " + PAN.BATTLE,
[_cost(T22 / f"{JU.P22.tag_edit(PAN.ANCHOR, u['uid'])}.json") for u in us])]
for i, m in enumerate(PAN.survivors(), start=1):
rows.append((f"T{i} {m}", [_cost(OUT / f"{PAN.tag_c(m, u['uid'])}.json") for u in us]))
def q(cs: list[float], p: int) -> float:
s = sorted(c for c in cs if c > 0)
return s[min(len(s) - 1, -(-p * len(s) // 100) - 1)] if s else 0.0
print(f" {'арм':26s}{'ед.':>5s}{'p50 edit':>10s}{'p95 edit':>10s}"
f"{'p50 связка':>12s}{'книга p50':>12s}{'книга p95':>12s}")
for name, cs in rows:
nz = [c for c in cs if c > 0]
if not nz:
continue
link = [c + b for c, b in zip(cs, base, strict=True) if c > 0 and b > 0]
print(f" {name:26s}{len(nz):5d}{st.median(nz):10.5f}{q(nz, 95):10.5f}"
f"{(st.median(link) if link else 0):12.5f}"
f"{(st.median(link) if link else 0) * 1500:12.2f}{q(link, 95) * 1500:12.2f}")
print(f" якорный черновик (общая база связки): p50 {st.median([b for b in base if b > 0]):.5f}"
if any(base) else " якорного черновика в сырье нет")
def agent_runs() -> None:
h("АГЕНТ-ЗАПУСКИ")
for p in JU.PASSES:
v = OUT / f"aj-{p}-votes"
t = OUT / f"aj-{p}-tasks"
print(f" {p:8s} заданий {len(list(t.glob('*.txt'))) if t.exists() else 0} · "
f"голосов принято {len(list(v.glob('*.json'))) if v.exists() else 0}")
def main() -> None:
print("ИТОГ ПАКА «СИЛЬНЫЙ ТИР» — ВСЕ НЕСУЩИЕ ЧИСЛА ИЗ СЫРЬЯ ($0)")
material()
screen()
economics()
for p in JU.PASSES:
try:
judged(p)
except Exception as e: # noqa: BLE001
print(f"\n═══ СУДЕЙСТВО {p} ═══\n не считается: {type(e).__name__}: {e}")
positional()
money_two_ways()
agent_runs()
print("\nВСЁ ВЫШЕ ПЕРЕ-СНЯТО ИЗ СЫРЬЯ.")
if __name__ == "__main__":
main()

View file

@ -15,6 +15,7 @@
"""
from __future__ import annotations
import hashlib
import json
import sys
from pathlib import Path
@ -47,7 +48,8 @@ PASSES = ("tier", "border")
def arm_map(pass_: str) -> dict[str, str]:
if pass_ == "tier":
return {"R0": PAN.BATTLE, **{f"T{i}": m for i, m in enumerate(PAN.survivors(), start=1)}}
return {"R0": PAN.BATTLE, **{f"T{i}": m for i, m in enumerate(PAN.survivors(), start=1)},
"F": PAN.ANCHOR} # голый якорный черновик — база позитивного контроля
return {"R0": PAN.BATTLE, "R2": PAN.BORDER}
@ -58,8 +60,13 @@ def arms(pass_: str) -> tuple[str, ...]:
def family(pass_: str) -> list[tuple[str, str, str]]:
"""Семейство объявляется ДО выпуска и впечатывается в ключ."""
if pass_ == "tier":
return [(a, "R0", f"редактор сильного тира {a} против боевого над ТОЙ ЖЕ базой")
for a in arms(pass_) if a != "R0"]
fam = [(a, "R0", f"редактор сильного тира {a} против боевого над ТОЙ ЖЕ базой")
for a in arms(pass_) if a not in ("R0", "F")]
# ⚠ ПОЗИТИВНЫЙ КОНТРОЛЬ, обещанный планом и пропущенный первой редакцией: покупает ли
# боевой редактор что-нибудь поверх голого черновика. Без него проход не отличает
# «армы равны» от «судья ничего не различает».
fam.append(("R0", "F", "КОНТРОЛЬ: покупает ли боевой редактор поверх якорного черновика"))
return fam
return [("R2", "R0", "пограничный glm-5 против боевого, n удвоено до 32")]
@ -78,8 +85,14 @@ def text_of(pass_: str):
# единица новая → тексты фазы B; единица эксп-22 → его же клетки
if u["uid"] in new_uids:
return PAN.text_b(arm, u["uid"])
return P22.edit_text(PAN.BATTLE if arm == "R0" else PAN.BORDER, u["uid"]) \
if arm == "R0" else _ed22(u["uid"])
# ⚠ R0 — боевой редактор поверх ЯКОРНОГО черновика. В именовании эксп-22 арм
# редактуры назван по ЧЕРНОВИКУ-жильцу, а не по редактору: `edit_text("dspro")` дало
# бы редактуру поверх ЧЕРНОВИКА dspro, то есть ДРУГУЮ базу, и контраст сравнивал бы
# армы над разными основаниями. Поймано приёмкой. Направление смещения НЕ замерено —
# прежний комментарий утверждал «против R2» без замера, утверждение снято.
return P22.edit_text(PAN.ANCHOR, u["uid"]) if arm == "R0" else _ed22(u["uid"])
if arm == "F":
return PAN.P22.draft_text(PAN.ANCHOR, u["uid"])
return PAN.text_c(arm, u["uid"])
return hook
@ -103,17 +116,67 @@ def floor_of(pass_: str):
def decoy_base(slots: dict) -> str:
for a in ("R0", "T1", "R2"):
if a in slots and (slots[a] or "").strip():
return a
return next(iter(slots))
"""⚠ Возвращать ТЕКСТ, а не имя арма. Первая редакция возвращала имя — риг сажал порчу в
строку «R0», получал 0 замен при пороге 2 и МОЛЧА не создавал декой: ноль контролей в обоих
проходах. Это ровно тот отказ, которым эксп-22 потерял проход en, и он воспроизвёлся здесь.
ДОНОР ПОРЧИ УРАВНОВЕШЕН ПО АРМАМ. Вторая редакция брала R0 всегда (замер: 16/16 и 32/32), а
R0 стоит вторым армом в КАЖДОМ контрасте обоих проходов: если присутствие испорченного близнеца
вообще двигает оценку донора, оно двигает её в одну сторону во всех контрастах сразу, и в
`border` где контраст один опровергнуть это нечем. Донор раздаётся РОВНО: единицы
прохода нумеруются по хешу, донор берётся по остатку номера судье не предсказуем, по проходу
поделён поровну (чистый хеш давал 22/10 на 32 единицах, перекос остался бы). Принятый проход
`tier` отсужен ДО этой правки, его декой во всех 16 единицах взят из R0 объявленное
ограничение, см. отчёт §12.
"""
battle = [a for a in ("R0", "T1", "T2", "T3", "R2") if (slots.get(a) or "").strip()]
if not battle:
return next(t for t in slots.values() if (t or "").strip())
rank = _DONOR_RANK.get(hashlib.sha1(slots[battle[0]].encode("utf-8")).hexdigest(), 0) # noqa: S324,E501
return slots[battle[rank % len(battle)]]
def setup(pass_: str) -> None:
_DONOR_RANK: dict[str, int] = {}
def _rank_units(pass_: str) -> None:
"""Номер единицы для ровной раздачи донора. Ключ — хеш текста ПЕРВОГО боевого арма: он же
считается в `decoy_base`, куда номер единицы не передаётся. Порядок по хешу, а не по главе:
соседние главы не должны систематически получать одного донора."""
_DONOR_RANK.clear()
hook = text_of(pass_)
keyed = []
for u in units(pass_):
for a in ("R0", "T1", "T2", "T3", "R2"):
t = hook(a, u) or ""
if t.strip():
keyed.append(hashlib.sha1(t.encode("utf-8")).hexdigest()) # noqa: S324
break
for i, k in enumerate(sorted(keyed)):
_DONOR_RANK[k] = i
def setup(pass_: str, run: str = "") -> None:
"""`run` — имя ОТДЕЛЬНОГО прогона того же прохода (репликация). Пустое = боевой прогон.
Заведено поправкой фазы Д (чек-лист приёмки 16, п.9). Репликацию гоняли, копируя каталоги
руками: имя каталога выводилось только из `PASS`, поэтому разбор шёл под боевым именем и
проставил голосам репликации имена судей БОЕВОГО жребия то есть ровно то, что репликация
и меняет. Здесь у прогона свои каталоги и своя карта судей, а ключ/соль/метки остаются
общими (иначе это была бы не репликация, а другой замер).
"""
_rank_units(pass_)
AJ.configure(OUT=OUT, KEYS=KEYS, PASS=pass_, ARMS=arms(pass_), ARMS_2B=(),
FAMILY=family(pass_), TEXT_HOOK=text_of(pass_),
UNITS_HOOK=lambda: units(pass_), FLOOR_HOOK=floor_of(pass_),
DECOY_HOOK=decoy_base, SRC_LANG="китайском")
DECOY_HOOK=decoy_base, SRC_LANG="китайском",
DIRS_HOOK=None, JUDGES_HOOK=None)
if run:
jmap = json.loads((OUT / f"{run}-JUDGES.json").read_text(encoding="utf-8"))
AJ.configure(
DIRS_HOOK=lambda: (OUT / f"aj-{pass_}-tasks", OUT / f"{run}-answers",
OUT / f"{run}-votes"),
JUDGES_HOOK=lambda tok, _mtime: jmap.get(tok, "?"))
KEYS.mkdir(parents=True, exist_ok=True)
@ -161,9 +224,24 @@ if __name__ == "__main__":
elif a[0] == "--emit":
setup(a[1])
AJ.emit()
elif a[0] == "--run":
# Разбор/счёт ОТДЕЛЬНОГО прогона: --run <прогон> <проход> [--ingest|--score|--controls]
setup(a[2], run=a[1])
what = a[3] if len(a) > 3 else "--score"
if what == "--ingest":
AJ.ingest()
elif what == "--controls":
AJ.controls()
else:
AJ.score()
elif a[0] == "--ingest":
setup(a[1])
AJ.ingest()
# ⚠ Раздача судей персистируется ЗДЕСЬ. `log_run` был написан и ни разу не вызван: во всех
# голосах пака стоит `judge='?'`, согласие судей посчитать нечем, а «агент-запусков 38» —
# счёт по памяти. Токены сессии передаются списком: --ingest <проход> <ток> <ток> …
if len(a) > 2:
print(f"запуск #{log_run(a[1], a[2:], note='судейская сессия')} записан")
elif a[0] == "--score":
setup(a[1])
AJ.controls()

View file

@ -42,6 +42,7 @@ PR = _load("prompts22", TP / "prompts.py")
P22 = _load("panel22", TP / "panel.py")
MAT = _load("material_et", ZONE / "material.py")
SCR = _load("screen_et", ZONE / "screen.py")
import bakeoff as BO # noqa: E402
MONEY = SCR.MONEY
ROSTER = SCR.ROSTER
@ -90,6 +91,13 @@ def cmd_b_run() -> None:
if not d.strip():
print(f" {u['uid']}: черновика нет — редактуры пропущены")
continue
# ⚠ ГЕЙТ БРАКА ЧЕРНОВИКА БЫЛ НАПИСАН И НЕ ПОДКЛЮЧЁН. Фаза проверяла только «строка пуста»,
# поэтому эхо-мина прошла на боевую базу: `63ab55ac5c` — 1798 ханьцзы и НОЛЬ кириллицы при
# `finish=stop`. Отчёт из-за этого объявлял класс «не ловится ни одним гейтом» — неверно:
# `draft_reject_reason` его ловит, звать было некому. Поймано аудитом закрытия заказа.
if (why := BO.draft_reject_reason(d)):
print(f" {u['uid']}: черновик забракован ({why}) — редактуры НЕ покупаются")
continue
law = PR.law_block(PR.terms_in(u["source"]))
for m in (BATTLE, BORDER):
te = tag_b("edit", m, u["uid"])
@ -180,7 +188,15 @@ def cmd_floor_run() -> None:
def floor_pair(uid: str):
"""(генерация 1, генерация 2) самого дешёвого арма панели."""
"""(генерация 1, генерация 2) самого дешёвого арма панели.
ДЕФЕКТ, ОБЪЯВЛЕННЫЙ И НЕ ПОЧИНЕННЫЙ В ЭТОМ ПАКЕ: первая половина пары это САМА боевая
клетка `T1`, а не отдельная генерация. Следствие ровно одно, и оно не в числах пола (пол
считается по РАЗНОСТИ двух генераций и остаётся честным), а в контроле «БЛИЗНЕЦ»: он ловит
побайтные дубли, и здесь единственным дублем оказывается пара `CTRLfloorA``T1` в 16/16
то есть контроль сравнивает `T1` с самим собой и ничего не проверяет. Чинить надо ПОКУПКОЙ
отдельной первой генерации, поэтому в этом паке дефект только объявлен (отчёт §12).
"""
import json # noqa: PLC0415
a = text_c("T1", uid)
f = MONEY.OUT / f"{tag_floor(uid, 2)}.json"

View file

@ -0,0 +1,118 @@
#!/usr/bin/env python3
"""ПЕРЕ-СЧЁТ ГОЛОСОВ ВНЕШНЕГО СУДЬИ (не-Claude) ПО ПРОХОДУ border. $0.
Судья другой семьи отсуживал ТЕ ЖЕ файлы заданий, что и агенты-судьи пака (`aj-border-tasks/`), и
клал ответы в `sol-border/`. Поэтому его счёт сопоставим с моим текст-в-текст, а не «другой замер».
Разбор здесь СВОЙ, а не `absjudge.ingest`: тот пишет в боевой каталог голосов и затёр бы прогон
пака. Правила приёмки ответа те же каждая ненулевая ось названа и подпёрта цитатой.
Запуск: eval/.venv/bin/python eval/editor_tier/solscore.py
"""
from __future__ import annotations
import json
import math
import random
import re
import statistics as st
import sys
from pathlib import Path
OUT = Path.home() / "books" / "editor-tier"
# Проход задаётся аргументом: `border` (исполнен 09.08) и `tier` (заказан фазой Д, п.Д2).
PASS = "border"
KEY = OUT / "blind-keys" / f"{PASS}-KEY.json"
ANSW = OUT / f"sol-{PASS}"
AXES = ("ВЕРНОСТЬ", "ТЕРМИН", "ЯЗЫК", "ФОРМА")
SEED = 20260809
def votes() -> tuple[dict, list]:
key = json.loads(KEY.read_text(encoding="utf-8"))
ok, bad = {}, []
for f in sorted(ANSW.glob("*.txt")):
k = key.get(f.stem)
if not k:
bad.append((f.stem, "токена нет в ключе"))
continue
txt, unit, fail = f.read_text(encoding="utf-8"), {}, None
for lab, meta in k.items():
ax = {}
for a in AXES:
m = re.search(rf"^{lab}-{a}[ \t]*:[ \t]*(\d+)[ \t]*$", txt, re.M)
if m:
ax[a] = int(m.group(1))
if len(ax) != len(AXES):
fail = f"не разобран блок {lab}"
break
wm = re.search(rf"^{lab}-ПОЧЕМУ[ \t]*:[ \t]*(.*?)(?=^\S+-(?:{'|'.join(AXES)}|ПОЧЕМУ)"
rf"[ \t]*:|\Z)", txt, re.M | re.S)
why = (wm.group(1) if wm else "").strip()
miss = [a for a in AXES if ax[a] and not re.search(rf"(?<!\w){a}(?!\w)", why.upper())]
if miss:
fail = f"ось без обоснования: {'/'.join(miss)}"
break
if any(ax.values()) and not re.search(r"\"“„].+?[»\"”]", why):
fail = "обоснование без цитаты"
break
unit[meta["arm"]] = dict(errors=sum(ax.values()), decoy_from=meta.get("decoy_from"))
if fail:
bad.append((f.stem, fail))
continue
ok[next(iter(k.values()))["uid"]] = unit
return ok, bad
def sign_p(m: list[float]) -> float:
"""Точный при малом n, иначе сид-Монте-Карло: воспроизводимость важнее последней цифры."""
rnd, s, hits, trials = random.Random(SEED), sum(m), 0, 200_000 # noqa: S311
for _ in range(trials):
if abs(sum(x * rnd.choice((1, -1)) for x in m)) >= abs(s) - 1e-12:
hits += 1
return (hits + 1) / (trials + 1)
def main() -> int:
global PASS, KEY, ANSW # noqa: PLW0603
args = [a for a in sys.argv[1:] if not a.startswith("-")]
if args:
PASS = args[0]
KEY, ANSW = OUT / "blind-keys" / f"{PASS}-KEY.json", OUT / f"sol-{PASS}"
print(f"проход {PASS} · ключ {KEY.name} · ответы {ANSW}")
if not ANSW.exists() or not list(ANSW.glob("*.txt")):
print(f"ответов внешнего судьи нет в {ANSW}")
return 1
ok, bad = votes()
print(f"принято единиц {len(ok)} · отвергнуто {len(bad)}")
for t, r in bad:
print(f"{t}: {r}")
dec = [u[u["CTRLdecoy"]["decoy_from"]]["errors"] - u["CTRLdecoy"]["errors"] for u in ok.values()
if "CTRLdecoy" in u and u["CTRLdecoy"].get("decoy_from") in u]
flo = [u["CTRLfloorA"]["errors"] - u["CTRLfloorB"]["errors"] for u in ok.values()
if "CTRLfloorA" in u and "CTRLfloorB" in u]
# Семейство прохода: у `border` один контраст, у `tier` — три плюс позитивный контроль.
fam = ([("R2", "R0")] if PASS == "border"
else [("T1", "R0"), ("T2", "R0"), ("T3", "R0"), ("R0", "F")])
pairs = [(a, b, [u[b]["errors"] - u[a]["errors"] for u in ok.values() if a in u and b in u])
for a, b in fam]
pairs = [(a, b, m) for a, b, m in pairs if len(m) >= 3]
if not (dec and flo and pairs):
print("контролей или боевых армов не хватает")
return 1
caught = sum(1 for x in dec if x < 0)
thr = 2.8 * st.pstdev(flo) / math.sqrt(len(flo))
print(f"\n ДЕКОЙ {st.mean(dec):+.2f} · поймано {caught}/{len(dec)}"
+ ("" if caught == len(dec) else " ⛔ сессия внешнего судьи аннулируется"))
print(f" ПОЛ {st.mean(flo):+.2f} · n={len(flo)} sd={st.pstdev(flo):.2f} → ПОРОГ {thr:.2f}")
for a, b, m in pairs:
print(f" {a} vs {b} n={len(m)} {st.mean(m):+.2f} p={sign_p(m):.4f} "
f"{'РАЗЛИЧИМ' if abs(st.mean(m)) > thr else 'ниже порога различимости'}")
battle = {x for a, b, _ in pairs for x in (a, b)}
lv = [r["errors"] for u in ok.values() for a, r in u.items() if a in battle]
print(f" уровень (медиана боевых клеток) {st.median(lv):.1f}")
return 0 if caught == len(dec) else 1
if __name__ == "__main__":
sys.exit(main())

View file

@ -0,0 +1,189 @@
#!/usr/bin/env python3
"""ВЕРИФИКАТОР ЧИСЕЛ ОТЧЁТА ПАКА. $0. Ненулевой код возврата = отчёт не сдаётся.
`itog23.py` пере-считывает числа из сырья; этот файл сверяет их с ТЕКСТОМ отчёта. Разделение
нужно потому, что «число посчиталось» и «в отчёте напечатано именно оно» разные утверждения.
ЧЕГО ЭТОТ ГЕЙТ НЕ ДЕЛАЕТ. Он сторожит, что число ПРИСУТСТВУЕТ в тексте, а не что предложение
вокруг числа истинно. Приёмка эксп-22 нашла ровно этот класс: «все перевесы выше порога 2.47» при
всех сходящихся числах. Пороговые и сравнительные утверждения проверяются только чтением; за
соответствие ЗАКАЗУ отвечает отдельный инструмент `eval/conformance.py`.
"""
from __future__ import annotations
import json
import re
import statistics as sst
import sys
from pathlib import Path
REPO = Path("/home/ubuntu/projects/textmachine")
ZONE = Path(__file__).resolve().parent
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 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
IT = _load("itog23", ZONE / "itog23.py")
JU, PAN, SCR, MONEY, MAT = IT.JU, IT.PAN, IT.SCR, IT.MONEY, IT.MAT
REPORT = REPO / "docs" / "experiments" / "23-editor-tier.md"
BAD = 0
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 _norm(x: str) -> str:
"""Пробелы не значимы; типографский минус U+2212 и ASCII-дефис — один знак."""
return re.sub(r"\s+", "", x).replace("", "-").replace("", "-")
def has(text: str, s: str) -> bool:
return _norm(s) in _norm(text)
def main() -> int:
if not REPORT.exists():
print("отчёта нет")
return 1
t = REPORT.read_text(encoding="utf-8")
print("=== МАТЕРИАЛ ===")
new = MAT.units_new()
ck(f"новых единиц {len(new)}", has(t, f"{len(new)} нов") or has(t, f"{len(new)} НОВ"))
ck("главы новых единиц напечатаны",
all(has(t, str(c)) for c in sorted({u["chapter"] for u in new})[:3]))
print("\n=== ФАЗА A ===")
for m, r in SCR.result().items():
ck(f"{m}: вердикт {r['verdict']} назван", has(t, m))
if r["price_editor"]:
ck(f"{m}: цена редактора {r['price_editor']:.5f}", has(t, f"{r['price_editor']:.5f}"))
ck("оба вердикта скрина различены (пре-рег и полный критерий эксп-22)",
has(t, "пре-рег") and has(t, "переводческ"))
print("\n=== ЦЕНА ===")
us = JU.P22.M.units_zh()
T22 = JU.P22.OUT
def cost(f: Path) -> float:
return json.loads(f.read_text(encoding="utf-8")).get("cost_usd", 0.0) if f.exists() else 0.0
base = [cost(T22 / f"{JU.P22.tag_draft(PAN.ANCHOR, u['uid'])}.json") for u in us]
# ⚠ R0 сторожится наравне с панелью. Прежняя редакция шла по `survivors()` = T1..T3, и строка
# боевого редактора — та, против которой читается ВЕСЬ вывод о цене, — не проверялась ничем.
arms = [("R0", [cost(T22 / f"{JU.P22.tag_edit(PAN.ANCHOR, u['uid'])}.json") for u in us])]
arms += [(f"T{i}", [cost(MONEY.OUT / f"{PAN.tag_c(m, u['uid'])}.json") for u in us])
for i, m in enumerate(PAN.survivors(), start=1)]
for name, cs in arms:
nz = [c for c in cs if c > 0]
if not nz:
continue
ck(f"{name}: p50 редактуры {sst.median(nz):.5f}", has(t, f"{sst.median(nz):.5f}"))
link = [c + b for c, b in zip(cs, base, strict=True) if c > 0 and b > 0]
if link:
ck(f"{name}: книга по СВЯЗКЕ ${sst.median(link) * 1500:.2f}",
has(t, f"{sst.median(link) * 1500:.2f}"))
print("\n=== СВЕЖЕСТЬ РАЗБОРА ===")
# ⚠ ГОЛОС СТАРШЕ СВОЕГО ОТВЕТА = РАЗБОР ГНАЛСЯ В СЕРЕДИНЕ ГОНКИ. Поймано аудитом: два круга
# досуживания писали в ОДНИ И ТЕ ЖЕ файлы ответов, `--ingest` прошёл между ними, и 5 единиц из
# 32 остались с голосами от одного круга при ответах от другого. Числа при этом пере-считывались
# из голосов и сходились с отчётом — то есть ни один прежний гейт этого не видел.
for pass_ in JU.PASSES:
votes, answ = MONEY.OUT / f"aj-{pass_}-votes", MONEY.OUT / f"aj-{pass_}"
if not votes.exists() or not answ.exists():
continue
stale = [a.stem for a in answ.glob("*.txt")
if (votes / f"{a.stem}.json").exists()
and a.stat().st_mtime > (votes / f"{a.stem}.json").stat().st_mtime + 1]
ck(f"{pass_}: голоса не старше ответов судей", not stale,
f"устарели {len(stale)}: {' '.join(sorted(stale)[:6])}" if stale else "")
print("\n=== СУДЕЙСТВО ===")
for pass_ in JU.PASSES:
v = MONEY.OUT / f"aj-{pass_}-votes"
# ⚠ ПРОХОД БЕЗ ГОЛОСОВ РОНЯЕТ ГЕЙТ. Прежняя редакция делала `continue`, и гейт печатал
# «ВСЕ ЧИСЛА ОТЧЁТА СХОДЯТСЯ» с кодом 0, не проверив по проходу НИ ОДНОГО числа, — при
# том что отчёт печатал по нему несущий вывод. Ровно этим прошёл аннулированный border.
if not v.exists() or not list(v.glob("*.json")):
ck(f"{pass_}: голоса прохода на диске", False, "нет ни одного — числа не пере-снимаемы")
continue
JU.setup(pass_)
import absjudge as AJ # noqa: PLC0415
data = AJ._by_unit() # noqa: SLF001
ck(f"{pass_}: единиц отсужено {len(data)}", len(data) >= 3, f"{len(data)}")
for a, b, _w in JU.family(pass_):
m = AJ._margins(a, b) # noqa: SLF001
if len(m) < 3:
continue
ck(f"{pass_}: контраст {a} vs {b} = {sst.mean(m):+.2f} напечатан",
has(t, f"{sst.mean(m):+.2f}") or has(t, f"{sst.mean(m):.2f}"))
lo, hi = AJ.BO.boot_ci(m)
ck(f"{pass_}: ДИ {a} vs {b} [{lo:+.2f}, {hi:+.2f}] напечатан",
has(t, f"[{lo:+.2f},{hi:+.2f}]"))
dec = [d[d["CTRLdecoy"].get("decoy_from", "")]["errors"] - d["CTRLdecoy"]["errors"]
for d in data.values()
if "CTRLdecoy" in d and d["CTRLdecoy"].get("decoy_from", "") in d]
# Декой — условие осмысленности пака по его же §0, поэтому сторожится ЧИСЛОМ, а не словом.
ck(f"{pass_}: ДЕКОЙ есть в каждой единице", len(dec) == len(data), f"{len(dec)}/{len(data)}")
if dec:
ck(f"{pass_}: декой пойман {sum(1 for x in dec if x < 0)}/{len(dec)}",
all(x < 0 for x in dec))
ck(f"{pass_}: величина декоя {sst.mean(dec):+.2f} напечатана",
has(t, f"{sst.mean(dec):+.2f}"))
flo = [d["CTRLfloorA"]["errors"] - d["CTRLfloorB"]["errors"]
for d in data.values() if "CTRLfloorA" in d and "CTRLfloorB" in d]
if flo:
sd = sst.pstdev(flo)
thr = 2.8 * sd / (len(flo) ** 0.5)
# Порог различимости — решающее правило пака: печатать его надо ИМЕННО из своего пола.
ck(f"{pass_}: ПОРОГ {thr:.2f} напечатан", has(t, f"{thr:.2f}"))
ck(f"{pass_}: пол {sst.mean(flo):+.2f} напечатан", has(t, f"{sst.mean(flo):+.2f}"))
# ⚠ НАКЛОН СТОРОЖИТСЯ ЧИСЛОМ (поправка фазы Д, чек-лист приёмки №16 п.6). В теле стояло
# +0.646, а текущее сырьё даёт +0.708: величина, на которую делается ПОПРАВКА боевого
# контраста, печаталась не из тех голосов, из которых считается сам контраст. Ни один
# прежний гейт наклон не трогал — сторожились только перевес, ДИ, пол, порог и декой.
xs, ys = [], []
for d in data.values():
mx = sst.mean([r["slot"] for r in d.values()])
my = sst.mean([r["errors"] for r in d.values()])
for r in d.values():
xs.append(r["slot"] - mx)
ys.append(r["errors"] - my)
if sum(x * x for x in xs) > 0:
slope = sum(a * b for a, b in zip(xs, ys, strict=True)) / sum(x * x for x in xs)
ck(f"{pass_}: НАКЛОН {slope:+.3f} напечатан", has(t, f"{slope:+.3f}"),
f"n={len(xs)}")
print("\n=== ДЕНЬГИ ===")
tot = 0.0
for ph in MONEY.TAGS:
sp = MONEY.Guarded(ph).spent()
tot += sp
if sp <= 0:
continue
ck(f"{ph}: расход ${sp:.6f} напечатан", has(t, f"{sp:.6f}"))
if sp > MONEY.CEILINGS[ph] + 1e-9:
ck(f"{ph}: ПРОБОЙ объявлен", has(t, "ПРОБОЙ") or has(t, "пробой"))
ck(f"расход пака ${tot:.6f} напечатан", has(t, f"{tot:.6f}"))
ck("подъём потолка фазы C из резерва объявлен", has(t, "2.30") and has(t, "резерв"))
print(f"\n{'ВСЕ ЧИСЛА ОТЧЁТА СХОДЯТСЯ' if not BAD else f'РАСХОЖДЕНИЙ: {BAD}НЕ СДАЁТСЯ'}")
return BAD
if __name__ == "__main__":
sys.exit(1 if main() else 0)

View file

@ -60,7 +60,15 @@ KEYS = OUT / "blind-keys"
def _dirs():
"""Каталоги прохода. ВТОРОЙ проход обязан лежать отдельно: он и есть репликация,
и затирание первого превратило бы её в пере-запись."""
и затирание первого превратило бы её в пере-запись.
`DIRS_HOOK` заведён приёмкой 16 (чек-лист фазы Д, п.9). Репликацию пака 23 гоняли,
КОПИРУЯ каталоги руками, потому что имя каталога выводилось только из `PASS`: разбор шёл под
`PASS='border'` и штамповал голоса репликации именами судей БОЕВОГО прогона. Крючок даёт
прогону собственные каталоги, не трогая имя прохода (а с ним ключ, соль и метки).
"""
if DIRS_HOOK is not None:
return DIRS_HOOK()
return (OUT / f"aj-{PASS}-tasks", OUT / f"aj-{PASS}", OUT / f"aj-{PASS}-votes")
# ⚠ СОЛЬ ЖИВЁТ ВНЕ РЕПОЗИТОРИЯ. Адверсариальное ревью 07.08 восстановило 138 меток из 138, не
# открывая ключ: `layout()` — чистая функция от соли и `uid = sha1(source)`, а источник лежит В
@ -100,6 +108,8 @@ UNITS_HOOK = None # callable() -> list[dict]
FLOOR_HOOK = None # callable(unit) -> tuple[str, str] | None — пара повторов одного арма
DECOY_HOOK = None # callable(slots: dict) -> str — чистый текст, в который сажается порча
SRC_LANG = "китайском" # как называется язык исходника в задании судье
DIRS_HOOK = None # callable() -> (tasks, answers, votes) — свои каталоги прогона
JUDGES_HOOK = None # callable(token, answer_mtime) -> str — кто судил ЭТОТ ответ
def configure(**kw) -> None:
@ -141,9 +151,15 @@ FORMAT_HEAD = """Оцени КАЖДЫЙ вариант ОТДЕЛЬНО про
Ответ ЗАПИШИ ФАЙЛОМ в {path} ровно в таком виде и ничем больше, блок на каждую метку:
{skeleton}
ПОЧЕМУ обязательно для КАЖДОЙ ненулевой оси: короткая ЦИТАТА из текста через « | ».
Число без цитаты не принимается и на диск не попадает: именно из-за отсутствия цитат показания
прошлых судей оказались непроверяемыми. Ноль по оси обоснования не требует.
ПОЧЕМУ обязательно для КАЖДОЙ ненулевой оси, и ось надо НАЗВАТЬ. Единственный принимаемый вид
имя оси заглавными, двоеточие, цитаты в «ёлочках» через « | »; перечисляй только ненулевые оси:
Т1-ПОЧЕМУ: ВЕРНОСТЬ: «цитата» | «ещё цитата». ЯЗЫК: «цитата». ФОРМА: «цитата».
Разбор ищет имя оси буквально. Цитаты без имени оси не принимаются, и единица выбрасывается
ЦЕЛИКОМ вся работа по ней пропадает; так уже терялась треть пачки в трёх прогонах подряд, потому
что это требование стояло в приёмке, но не в задании. Ноль по оси обоснования не требует; нечем
подпереть число цитатой ставь ноль, а не число без цитаты.
"""
@ -293,8 +309,19 @@ def ingest() -> None:
# ⚠ ИДЕНТИЧНОСТЬ СУДЬИ. Первая версия рига её не персистировала вовсе, и по диску нельзя было
# ни посчитать согласие судей, ни разложить вердикты по семействам (требование промта стр.79).
# Поймано адверсариальным ревью 07.08. Карта раздачи лежит рядом с ключом и судье не даётся.
# ⚠ ПЛОСКАЯ КАРТА ПО ИМЕНИ ПРОХОДА НЕ РАЗЛИЧАЕТ ПРОГОНЫ (приёмка №16, чек-лист Д8 п.9).
# Репликация пака 23 судила ТЕ ЖЕ 32 токена вторым жребием судей, а карта лежит одна на
# проход — все 32 голоса репликации получили имена судей БОЕВОГО прогона. Перевесы от
# этого не двигаются (имя судьи в разность не входит), но «согласие судей» и разложение
# дисперсии по сессиям считались бы по ложной раскладке. Поэтому имя берётся крючком,
# который знает про ПРОГОН и про время ответа; плоская карта осталась дефолтом эксп-21/22.
jf = KEYS / f"{PASS}-JUDGES.json"
judges = json.loads(jf.read_text(encoding="utf-8")) if jf.exists() else {}
def judge_of(stem: str, path: Path) -> str:
if JUDGES_HOOK is not None:
return JUDGES_HOOK(stem, path.stat().st_mtime)
return judges.get(stem, "?")
VOTES.mkdir(parents=True, exist_ok=True)
ok = bad = 0
reasons: dict[str, int] = {}
@ -352,7 +379,7 @@ def ingest() -> None:
fail(f"блок {lab} задан дважды: {'/'.join(dup)}", f.stem)
break
rows[lab] = dict(**meta, **ax, why=why, errors=sum(ax.values()),
slot=int(lab[1:]), judge=judges.get(f.stem, "?"))
slot=int(lab[1:]), judge=judge_of(f.stem, f))
else:
(VOTES / f"{f.stem}.json").write_text(
json.dumps(rows, ensure_ascii=False), encoding="utf-8")
@ -363,7 +390,10 @@ def ingest() -> None:
def _by_unit() -> dict[str, dict[str, dict]]:
VOTES = _dirs()[2]
out = {}
for f in VOTES.glob("*.json"):
# ⚠ sorted, а не голый glob: `boot_ci` ресемплит по ПОЗИЦИЯМ списка, поэтому от порядка обхода
# каталога зависели границы доверительных интервалов — точечные оценки и p воспроизводились, а
# интервалы нет. Поймано приёмкой пака 23.
for f in sorted(VOTES.glob("*.json")):
rows = json.loads(f.read_text(encoding="utf-8"))
uid = next(iter(rows.values()))["uid"]
out[uid] = {r["arm"]: r for r in rows.values()}

View file

@ -482,19 +482,50 @@ def boot_ci(xs: list[float], reps: int = 5000, alpha: float = 0.05) -> tuple[flo
def sign_perm_p(xs: list[float]) -> float:
"""Точный двусторонний знаковый перестановочный тест: перебираются ВСЕ 2^n расстановок знаков.
"""ТОЧНЫЙ двусторонний знаковый перестановочный тест. $0.
Заведён 07.08 по адверсариальному ревью. Bootstrap-ДИ отвечает на вопрос «где лежит среднее»,
а не «отличимо ли оно от нуля при этом n», и на 15 единицах даёт заметно более узкий интервал,
чем точный тест. Здесь считается p, к которому применима поправка Holm по семейству контрастов.
"""
n = len(xs)
if n == 0 or n > 20:
if n == 0:
return float("nan")
obs = abs(sum(xs))
hits = sum(1 for mask in range(1 << n)
if abs(sum(x if mask >> i & 1 else -x for i, x in enumerate(xs))) >= obs - 1e-12)
return hits / (1 << n)
if n <= 20:
hits = sum(1 for mask in range(1 << n)
if abs(sum(x if mask >> i & 1 else -x for i, x in enumerate(xs))) >= obs - 1e-12)
return hits / (1 << n)
# ⚠ n > 20: ПОЛНЫЙ ПЕРЕБОР 2^n НЕПОДЪЁМЕН, НО ТОЧНЫЙ ОТВЕТ ВСЁ РАВНО ЕСТЬ. Первая редакция
# брала здесь Монте-Карло с фиксированным зерном; ревью исполнением показало, что этого не
# нужно: перевесы — ЦЕЛЫЕ числа (разности счётчиков ошибок), поэтому распределение суммы ±xᵢ
# считается динамикой по достижимым суммам за миллисекунды и ТОЧНО. Цена приближения была
# видна в отчёте: одна и та же величина печаталась как 0.1603 и 0.1608 (se Монте-Карло
# ≈0.0008), то есть заявление «числа воспроизводятся до знака» на ней не выполнялось.
# Дробные значения (их у нас нет, но контракт функции шире) уходят на прежний Монте-Карло.
scaled = [x * 2 for x in xs] # полуцелые тоже берём точно
if all(abs(v - round(v)) < 1e-9 for v in scaled):
ints = [abs(int(round(v))) for v in scaled]
target = abs(sum(scaled)) - 1e-9
# dp[s] = число расстановок знаков, дающих сумму s (сдвиг на total для неотрицательности)
total = sum(ints)
dp = [0] * (2 * total + 1)
dp[total] = 1
for v in ints:
nxt = [0] * (2 * total + 1)
for s, c in enumerate(dp):
if c:
nxt[s + v] += c
nxt[s - v] += c
dp = nxt
hits = sum(c for s, c in enumerate(dp) if abs(s - total) >= target)
return hits / (1 << len(xs))
import random # noqa: PLC0415
rnd = random.Random(20260809) # noqa: S311
trials = 200_000
hits = sum(1 for _ in range(trials)
if abs(sum(x if rnd.getrandbits(1) else -x for x in xs)) >= obs - 1e-12)
return (hits + 1) / (trials + 1)
def cmd_score() -> None:

View file

@ -139,6 +139,14 @@ def _rx(dst: str) -> str:
влияет), объявляется как девиация. Первая версия усекала только слова от 6 знаков, и короткие
склоняемые слова канона («род Цинь») не находились в форме «рода Цинь» метрика покрытия
занижалась бы на всех армах одинаково, но занижалась.
ГРАНИЦА СЛОВА В НАЧАЛЕ поправка фазы Д (адверсариальное ревью 10.08). Без неё усечённый
корень ловился В СЕРЕДИНЕ чужого слова: канон «род Цинь» матчился на «Го-РОд Цинчжоу» и
«Б-РОвь Цинь», канон «род Су» на «Пилюлю Кровавой Су». Замер: **30 ложных попаданий из
2113 = 1.4%**; кросс-термовых коллизий (одна форма подходит под ДВА канона) нет ни одной.
Влияние на печатные числа: покрытие всех армов падает на 0.0030.004 то есть В ОДНУ сторону
у всех, и ни один относительный вывод эксп-22 не двигается. Числа в отчёте пере-сняты.
`(?<![^\W\d_])` = «слева не буква»: `\b` здесь не годится, он пропускает начало после цифры.
"""
parts = []
for raw in dst.split():
@ -149,7 +157,7 @@ def _rx(dst: str) -> str:
parts.append(w[:-1] + r"\w*")
else:
parts.append(w)
return r"\s+".join(parts)
return r"(?<![^\W\d_])" + r"\s+".join(parts)
# ⚠ РУЧНЫЕ ПРАВКИ КАНОНА. Пусто = терминолог принят как есть. Каждая строка — решение СЕССИИ,

View file

@ -13,6 +13,7 @@
from __future__ import annotations
import json
import math
import statistics as st
import sys
from pathlib import Path
@ -110,10 +111,54 @@ def battery_canon() -> None:
f"{f'{k}/{s}':>14s}{(k / s if s else 0):>10.3f}")
def battery_canon_f3() -> None:
"""Та же батарея по армам Ф3. Прежде §6.2 отчёта считалась ВНЕ дерева: шапка обещала, что
пере-считывается каждое число, а таблица Ф3 под пере-счёт не попадала клейм был шире гейта."""
h("Ф3 БАТАРЕЯ И ПОКРЫТИЕ КАНОНА ПО АРМАМ РЕДАКТОРОВ (детерминированно, шума нет)")
import editors as ED # noqa: PLC0415
zh, cards = M.units_zh(), B.load_cards()
print(f" {'арм':30s}{'ед.':>5s}{'ханьцзы':>9s}{'типогр':>8s}{'потеря':>8s}"
f"{'канон':>14s}{'покрытие':>10s}{'$/ед':>10s}")
pan = ED.panel()
for i, model in enumerate(pan):
arm = f"R{i}"
rows, k, s, costs = [], 0, 0, []
for u in zh:
t = ED.text_of(arm, u["uid"])
if not t.strip():
continue
rows.append(B.run_unit(B.Unit(source=u["source"], draft=t, final=t, cards=cards)))
kk, ss = BANK.coverage(u["source"], t)
k += kk
s += ss
f = OUT / f"{ED.tag(model, u['uid'])}.json"
if f.exists():
costs.append(json.loads(f.read_text(encoding="utf-8")).get("cost_usd", 0.0))
if rows:
print(f" {arm + ' ' + model:30s}{len(rows):5d}"
f"{sum(r['cjk_leak']['han_chars'] for r in rows):9d}"
f"{sum(r['typography']['violations'] for r in rows):8d}"
f"{sum(r['numbers']['lost_n'] for r in rows):8d}"
f"{f'{k}/{s}':>14s}{(k / s if s else 0):>10.3f}"
f"{(st.median(costs) if costs else 0):10.5f}")
print(" ⚠ Клетки с ПУСТЫМ содержимым в счёт не идут — именно поэтому у части армов ед. < 16;")
print(" оплаченные пустые клетки перечислены в блоке ДЕНЬГИ.")
def _p95(xs: list[float]) -> float:
"""p95 по ЕДИНИЦАМ, ближайший ранг. При n=16 это 16-я по возрастанию, то есть максимум:
честнее так и сказать, чем интерполяцией изображать точность, которой при таком n нет."""
if not xs:
return 0.0
s = sorted(xs)
return s[min(len(s) - 1, math.ceil(0.95 * len(s)) - 1)]
def economics() -> None:
h("Ф2 (д) ЦЕНА/ЕДИНИЦА И ПРОЕКЦИЯ НА КНИГУ (1500 единиц)")
h("Ф2 (д) ЦЕНА/ЕДИНИЦА И ПРОЕКЦИЯ НА КНИГУ (1500 единиц) — p50 И p95, как заказано промтом")
zh = M.units_zh()
print(f" {'жилец':26s}{'черновик':>11s}{'редактор':>11s}{'связка':>11s}{'книга $':>10s}")
print(f" {'жилец':26s}{'связка p50':>11s}{'связка p95':>11s}"
f"{'книга p50':>11s}{'книга p95':>11s}")
for m in P2.tenants():
dc = [P2.draft_cost(m, u["uid"]) for u in zh]
ec = []
@ -123,11 +168,49 @@ def economics() -> None:
ec.append(json.loads(f.read_text(encoding="utf-8")).get("cost_usd", 0.0))
if not dc:
continue
d50, e50 = st.median(dc), (st.median(ec) if ec else 0.0)
print(f" {m:26s}{d50:11.5f}{e50:11.5f}{d50 + e50:11.5f}{(d50 + e50) * 1500:10.2f}")
# p50/p95 берутся от СВЯЗКИ по единице, а не складываются из отдельных квантилей ролей:
# сумма p95 двух ролей — не p95 суммы, и завышала бы хвост.
pair = [d + e for d, e in zip(dc, ec, strict=False)] if ec else dc
print(f" {m:26s}{st.median(pair):11.5f}{_p95(pair):11.5f}"
f"{st.median(pair) * 1500:11.2f}{_p95(pair) * 1500:11.2f}")
print(" ⚠ DeepSeek объявил на прайс-странице 08.08 повышение цен — проекция едет с оговоркой")
def drafts_as_text() -> None:
h("Ф2 (а) ЧЕРНОВИК КАК ТЕКСТ — ОПИСАТЕЛЬНАЯ ОСЬ ЗАКАЗА (поправки не несёт, вердиктов не даёт)")
import statistics as sst # noqa: PLC0415
for pass_ in ("p2zh", "p2en"):
JU.setup(pass_)
import absjudge as AJ # noqa: PLC0415
if not AJ._by_unit(): # noqa: SLF001
continue
fl = {"p2zh": 2.03, "p2en": None}[pass_]
print(f" --- {pass_} --- порог различимости "
f"{('%.2f' % fl) if fl else 'своего пола нет (пол Ф2 снят на zh)'}")
for a, bb, _w in JU.family_desc(pass_):
m = AJ._margins(a, bb) # noqa: SLF001
if not m:
continue
mean = sst.mean(m)
mark = ("ниже порога различимости" if fl and abs(mean) < fl else
"выше порога" if fl else "")
print(f" {a} vs {bb} n={len(m):2d} {mean:+6.2f} {mark}")
print(" ⚠ Знак ПОЛОЖИТЕЛЬНЫЙ = черновик жильца лучше якорного. Ось описательная: пре-рег Ф2")
print(" сузил решающее семейство до финалов, поэтому Холм по ней НЕ считается.")
def positional() -> None:
h("ПОЗИЦИОННЫЙ НАКЛОН — ЗАМЕРЕН И ВЫЧТЕН (требование ЗАКОНа промта)")
for pass_ in ("p2zh", "p2en", "p3", "dva"):
JU.setup(pass_)
import absjudge as AJ # noqa: PLC0415
if not AJ._by_unit(): # noqa: SLF001
continue
print(f" --- {pass_} ---")
AJ.position()
print()
def judged(pass_: str) -> None:
h(f"СУДЕЙСТВО, проход {pass_}")
JU.setup(pass_)
@ -176,7 +259,10 @@ def money_two_ways() -> None:
if empty_paid:
print(f" оплачено за ПУСТОЙ выход: {len(empty_paid)} вызовов "
f"${sum(c for _n, c in empty_paid):.5f}")
for n, c in sorted(empty_paid, key=lambda x: -x[1])[:5]:
tot_empty = sum(c for _n, c in empty_paid)
print(f" доля пака: {tot_empty / sum(led.values()) * 100:.1f}% "
f"(${tot_empty:.5f} из ${sum(led.values()):.6f})")
for n, c in sorted(empty_paid, key=lambda x: -x[1]):
print(f" {n} ${c:.5f}")
@ -198,12 +284,15 @@ def main() -> None:
screen()
echo_rate()
battery_canon()
battery_canon_f3()
economics()
drafts_as_text()
for p in ("p2zh", "p2en", "p3", "dva"):
try:
judged(p)
except Exception as e: # noqa: BLE001
print(f"\n═══ СУДЕЙСТВО {p} ═══\n не считается: {type(e).__name__}: {e}")
positional()
money_two_ways()
agent_runs()
print("\nВСЁ ВЫШЕ ПЕРЕ-СНЯТО ИЗ СЫРЬЯ. Чисел без пути воспроизведения в отчёте быть не должно.")

View file

@ -79,6 +79,22 @@ def family(pass_: str) -> list[tuple[str, str, str]]:
return [("D_", "A", "однопроходка без мандата против связки (сырьё эксп-21)")]
def family_desc(pass_: str) -> list[tuple[str, str, str]]:
"""ОПИСАТЕЛЬНОЕ семейство — ось (а) заказа: черновик КАК ТЕКСТ, до редактора.
Пре-рег Ф2 сузил РЕШАЮЩЕЕ семейство до финалов (решение владельца принимается по тому, что
уедет в книгу), и эти контрасты поправку Холма не несут и вердиктов не определяют. Но
печатать их обязательно: тот же пре-рег требует печатать перевес ниже пола ЧИСЛОМ, а не
замалчивать, а промт заказал эту ось явным пунктом. Молчание о купленном и отсуженном
замере та самая ловушка «след отчёта», от которой пак и защищается.
"""
if pass_ in ("p2zh", "p2en"):
ts = P2.tenants()
return [(f"D{i}", "D0", f"черновик жильца {i} КАК ТЕКСТ против якорного черновика")
for i in range(1, len(ts))]
return []
def arms(pass_: str) -> tuple[str, ...]:
return tuple(arm_map(pass_))
@ -128,7 +144,9 @@ def decoy_base(slots: dict) -> str:
def setup(pass_: str) -> None:
AJ.configure(OUT=OUT, KEYS=KEYS, PASS=pass_, ARMS=arms(pass_), ARMS_2B=(),
AJ.configure(DIRS_HOOK=None, JUDGES_HOOK=None, # крючки — глобали ОБЩЕГО рига:
# не сбросишь — соседний пак прочитает чужие каталоги молча (ревью 10.08)
OUT=OUT, KEYS=KEYS, PASS=pass_, ARMS=arms(pass_), ARMS_2B=(),
FAMILY=family(pass_), TEXT_HOOK=text_of(pass_),
UNITS_HOOK=lambda: units(pass_), FLOOR_HOOK=floor_of(pass_),
DECOY_HOOK=decoy_base,

View file

@ -82,10 +82,49 @@ def pick_units(a: str, b: str) -> list[dict]:
return [u for _g, u in scored[:N_UNITS]]
# ⚠ РАМКА ВНЕШНЕГО СУДЬИ. Sol работает в UI-подписке владельца, а не по API: у него нет ни ключа,
# ни жёсткого формата ответа, ни запрета на веб. Владелец 09.08: «он пипец как любит посмотреть
# туда, куда его не просят». Три риска, каждый обесценивает пакет: узнавание книги вместо сверки с
# исходником (контаминация — тот самый класс, ради которого искали невиданный срез); собственный
# вариант перевода вместо счёта ошибок; разметка и преамбулы, ломающие построчный разбор.
FRAME = """РАМКА ЗАДАНИЯ — прочитай целиком до того, как начнёшь.
Это слепой замер, а не консультация. От тебя нужен РОВНО ОДИН артефакт: блок строк с числами.
Материал оплачен и одноразовый; всё, что сверх формата, ломает машинный разбор и обесценивает его.
ЧЕГО ДЕЛАТЬ НЕЛЬЗЯ:
* НЕ искать ничего вовне этого сообщения ни в сети, ни в памяти: ни книгу, ни автора, ни главу,
ни чужие переводы. Если ты узнал произведение не опирайся на это ни одним словом. Единственный
эталон ИСХОДНИК ниже. Узнавание вместо сверки с исходником делает замер недействительным.
* НЕ предлагать свой перевод, не переписывать, не редактировать и не «улучшать» варианты.
* НЕ гадать, кто автор вариантов человек, модель, какая именно, какой вариант «основной».
Порядок вариантов перемешан жребием и не значит ничего.
* НЕ оценивать варианты друг через друга. Каждый считается ОТДЕЛЬНО против исходника: вопрос не
«какой лучше», а «сколько ошибок в этом».
* НЕ добавлять заголовки, таблицы, markdown-разметку, жирный шрифт, преамбулы и рекомендации.
* НЕ задавать уточняющих вопросов и не просить контекст: его не будет, отвечай по тому, что дано.
* НЕ менять имена строк ответа и не переставлять их разбор ищет их буквально, посимвольно."""
FORMAT = """ФОРМАТ ОТВЕТАРОВНО ЭТИ ДЕВЯТЬ СТРОК, КАЖДАЯ С НОВОЙ, БЕЗ РАЗМЕТКИ И БЕЗ ТЕКСТА МЕЖДУ НИМИ:
В1-ВЕРНОСТЬ: <число>
В1-ТЕРМИН: <число>
В1-ЯЗЫК: <число>
В1-ФОРМА: <число>
В2-ВЕРНОСТЬ: <число>
В2-ТЕРМИН: <число>
В2-ЯЗЫК: <число>
В2-ФОРМА: <число>
СТИЛЬ: <В1 или В2 или НИЧЬЯ где живее и естественнее русская проза>
Пояснения и разбор можно дать, но ТОЛЬКО ПОСЛЕ этих девяти строк, отдельным абзацем. Ничего перед
ними и ничего между ними. Если по какой-то оси ошибок нет ставь 0, а не пропуск и не прочерк."""
def _packet(src: str, v1: str, v2: str, tok: str) -> str:
axes = J.RUBRIC.split("---", 1)[0].strip() if "---" in J.RUBRIC else J.RUBRIC.strip()
return "\n".join([
f"ПАКЕТ {tok}", "",
J.RUBRIC.split("---", 1)[0].strip() if "---" in J.RUBRIC else J.RUBRIC.strip(), "",
FRAME, "", "ЧТО СЧИТАТЬ ОШИБКОЙ — по типам:", axes, "", FORMAT, "",
"=" * 60, "ИСХОДНИК:", src, "",
"=" * 60, "ВАРИАНТ 1:", v1, "",
"=" * 60, "ВАРИАНТ 2:", v2, ""])
@ -129,6 +168,128 @@ def emit() -> None:
print(f"инструкция → {SOL}/ИНСТРУКЦИЯ.md")
# ── ПАКЕТ (б): слепые пары ФИНАЛИСТОВ Ф2/Ф3 ────────────────────────────────────────────────
# Заказан промтом «по готовности Ф2/Ф3». Смысл иной, чем у пакета (а): (а) арбитрирует спор
# ДВУХ платных судей эксп-21, а (б) выносит наружу решение, которое внутри пака вынесла ОДНА
# модельная семья. Все 40 судейских сессий — один и тот же судья в разных контекстах, и это
# объявлено непереносимым (§11 отчёта). Sol — единственный контур другой семьи.
PAIRS_B = [
# (проход, арм A, арм B, зачем)
("p3", "R0", "R2", "редактор: рекомендованный против ПОГРАНИЧНОГО (на нём висит Резерв D39.22)"),
("p2zh", "E0", "E2", "черновик: финал поверх якоря против финала поверх лучшего соперника"),
]
N_UNITS_B = 3
def _no_openai(models: list[str]) -> None:
"""Семейный запрет симметричен (ЗАКОН промта): Sol не судит клетки OpenAI-моделей."""
import roster as R # noqa: PLC0415
bad = [m for m in models if "openai.com" in R.CANDIDATES.get(m, ("",))[0]]
if bad:
raise SystemExit(f"семейный запрет: Sol не судит клетки OpenAI — {bad}")
def pick_units_b(pass_: str, a: str, b: str) -> list[dict]:
"""Единицы, где ВНУТРИСЕМЕЙНЫЙ судья был увереннее всего.
Для арбитража это правильнее, чем брать спорные: если внешний судья не воспроизводит перевес
там, где свой был максимально уверен, под вопросом сам перевес, а не его величина.
"""
import judge as JU # noqa: PLC0415
JU.setup(pass_)
import absjudge as AJ2 # noqa: PLC0415
per = []
for uid, d in AJ2._by_unit().items(): # noqa: SLF001
if a in d and b in d:
per.append((abs(d[b]["errors"] - d[a]["errors"]), uid))
per.sort(key=lambda x: (-x[0], x[1]))
byid = {u["uid"]: u for u in _units_b()}
return [byid[uid] for _g, uid in per[:N_UNITS_B] if uid in byid]
def _units_b() -> list[dict]:
import material as M # noqa: PLC0415
return M.units_zh()
def _text_b(pass_: str, arm: str, uid: str) -> str:
import judge as JU # noqa: PLC0415
return JU.text_of(pass_)(arm, {"uid": uid})
def emit_b() -> None:
"""Выпуск пакета (б). $0: ничего не покупается, только сборка уже купленных клеток."""
import judge as JU # noqa: PLC0415
out = MONEY.OUT / "sol-b"
out.mkdir(parents=True, exist_ok=True)
(out / "answers").mkdir(parents=True, exist_ok=True)
key: dict[str, dict] = {}
n = 0
for pass_, a, b, why in PAIRS_B:
_no_openai([JU.arm_map(pass_).get(a, ""), JU.arm_map(pass_).get(b, "")])
for u in pick_units_b(pass_, a, b):
ta, tb = _text_b(pass_, a, u["uid"]), _text_b(pass_, b, u["uid"])
if not ta.strip() or not tb.strip():
continue
for order in (0, 1):
tok = hashlib.sha1( # noqa: S324
f"{_salt()}|b|{pass_}|{a}|{b}|{u['uid']}|{order}".encode()).hexdigest()[:10]
v1, v2 = (ta, tb) if order == 0 else (tb, ta)
(out / f"{tok}.txt").write_text(_packet(u["source"], v1, v2, tok), encoding="utf-8")
key[tok] = dict(contrast=f"{a}v{b}", pass_=pass_, why=why,
first=a if order == 0 else b, second=b if order == 0 else a,
uid=u["uid"], order=order, kind="боевой")
n += 1
# тот же обязательный декой: сессия, не поймавшая посаженную деградацию, аннулируется
u0 = _units_b()[0]
clean = _text_b("p3", "R0", u0["uid"])
planted, k = AJ._plant(clean) # noqa: SLF001
if k >= 2:
for order in (0, 1):
tok = hashlib.sha1(f"{_salt()}|b-decoy|{u0['uid']}|{order}".encode()).hexdigest()[:10] # noqa: S324
v1, v2 = (clean, planted) if order == 0 else (planted, clean)
(out / f"{tok}.txt").write_text(_packet(u0["source"], v1, v2, tok), encoding="utf-8")
key[tok] = dict(contrast="ДЕКОЙ", first="чистый" if order == 0 else "порченый",
second="порченый" if order == 0 else "чистый", uid=u0["uid"],
order=order, kind="контроль-декой", plants=k)
n += 1
(KEYS / "sol-b-KEY.json").write_text(json.dumps(key, ensure_ascii=False, indent=1),
encoding="utf-8")
(out / "ИНСТРУКЦИЯ.md").write_text(_instr_b(len(key)), encoding="utf-8")
print(f"пакет (б): {n} файлов → {out} · ключ → {KEYS}/sol-b-KEY.json (владельцу НЕ давать)")
def _instr_b(n: int) -> str:
return f"""# Пакеты для Sol — ПАКЕТ (б), финалисты эксп-22
Всего пакетов: **{n}**. Правила те же, что в пакете (а): один пакет один НОВЫЙ чат, ответ
копируется дословно в `answers/<тот же токен>.txt`, порядок вариантов внутри пакета не менять,
какая модель какой вариант писала не сообщать.
## Чем этот пакет отличается от (а)
Пакет (а) разрешал спор двух платных судей эксп-21. Здесь спора нет здесь есть **решение,
которое целиком вынесла ОДНА модельная семья**: все 40 судейских сессий эксп-22 один и тот же
судья в разных контекстах. Он мог систематически предпочитать один стиль русской прозы другому,
и изнутри пака это неотличимо от истины. Sol единственный судейский контур другой семьи.
Поэтому важны именно эти пары:
* **редактор рекомендованный против пограничного** на этом контрасте держится вопрос о
Резерве D39.22, и внутри пака он прошёл на грани (перевес 2.12 при пороге прохода 2.47);
* **финал поверх якорного черновика против финала поверх лучшего соперника** крупнейший
перевес фазы 2.
Единицы отобраны те, где внутренний судья был увереннее всего: если внешний судья не
воспроизводит перевес там, где свой был максимально уверен, под вопросом сам перевес.
## Что будет с ответами
Возврат не блокер: не вернётся так и будет записано. Вернётся числа лягут рядом с
внутренними, и расхождение станет находкой, а не поводом переписать вывод.
"""
def _instr(n: int) -> str:
return f"""# Пакеты для Sol — инструкция владельцу
@ -241,5 +402,5 @@ def score() -> None:
if __name__ == "__main__":
a = sys.argv[1:] or ["--emit"]
fn = {"--emit": emit, "--ingest": ingest, "--score": score}.get(a[0])
fn = {"--emit": emit, "--emit-b": emit_b, "--ingest": ingest, "--score": score}.get(a[0])
fn() if fn else print(__doc__)

View file

@ -8,6 +8,12 @@
Правило: гейт сторожит НЕСУЩИЕ числа те, на которых стоит решение владельца. Число, которого
гейт не проверяет, в ИТОГе стоять не должно.
ЧЕГО ЭТОТ ГЕЙТ НЕ ДЕЛАЕТ, чтобы клейм не был шире прибора. Он проверяет, что число ПРИСУТСТВУЕТ
в тексте отчёта, а не что оно стоит в нужном месте и не что предложение вокруг него истинно.
Приёмка нашла ровно этот класс: «все перевесы выше порога 2.47» все числа напечатаны и сходятся,
а утверждение о них ложно. Пороговые и сравнительные предложения гейт не сторожит; их проверяет
только чтение.
"""
from __future__ import annotations
@ -36,9 +42,16 @@ def ck(name: str, ok: bool, detail: str = "") -> None:
print(f"[{'OK ' if ok else 'ПРОВАЛ'}] {name}" + (f" {detail}" if detail else ""))
def _norm(x: str) -> str:
"""Нормализация ПЕРЕД сравнением: пробелы не значимы, а типографский минус U+2212 и
ASCII-дефис один и тот же знак. Второе заведено гейтом-же: он пометил девять расхождений
там, где число совпадало, а отличался ЗНАК-СИМВОЛ. Различать их ложная тревога, но молчать
о них нельзя, поэтому нормализуется знак, а не ослабляется проверка."""
return re.sub(r"\s+", "", x).replace("\u2212", "-").replace("\u2013", "-")
def has(text: str, s: str) -> bool:
"""Число ищется по НОРМАЛИЗОВАННОМУ тексту: пробелы и переносы строк не значимы."""
return re.sub(r"\s+", "", s) in re.sub(r"\s+", "", text)
return _norm(s) in _norm(text)
def main() -> int:
@ -79,6 +92,25 @@ def main() -> int:
for m in P2.tenants():
ck(f"жилец {m} назван в отчёте", has(t, m))
# ⚠ ПОКРЫТИЕ КАНОНА СТОРОЖИТСЯ ЧИСЛОМ (поправка фазы Д). Таблицы (г) и §6.2 несут по 18
# чисел покрытия, и до сих пор их не проверял никто: правка регекса банка прошла бы молча.
# Поймано ревью, когда в `bank._rx` нашлась ловля корня В СЕРЕДИНЕ чужого слова.
print("\n=== ПОКРЫТИЕ КАНОНА ===")
us = M.units_zh()
for label, get in (("D0", lambda u: P2.draft_text(P2.ANCHOR, u["uid"])),
("E0", lambda u: P2.edit_text(P2.ANCHOR, u["uid"]))):
k = s = 0
for u in us:
o = get(u) or ""
if not o:
continue
a, b = BANK.coverage(u["source"], o)
k += a
s += b
if s:
ck(f"покрытие канона {label} = {k / s:.3f} напечатано", has(t, f"{k / s:.3f}"),
f"{k}/{s}")
print("\n=== ДЕНЬГИ ===")
for ph in ("Ф0", "Ф1", "Ф2", "Ф3"):
sp = MONEY.Guarded(ph).spent()
@ -92,6 +124,66 @@ def main() -> int:
pack = MONEY.Pack().spent()
ck(f"расход пака ${pack:.6f} напечатан", has(t, f"{pack:.6f}"))
print("\n=== ОСЬ (а): ЧЕРНОВИК КАК ТЕКСТ ===")
import statistics as sst # noqa: PLC0415
for pass_ in ("p2zh", "p2en"):
votes = MONEY.OUT / f"aj-{pass_}-votes"
if not votes.exists() or not list(votes.glob("*.json")):
continue
import judge as JU0 # noqa: PLC0415
JU0.setup(pass_)
import absjudge as AJ0 # noqa: PLC0415
for a, bb, _w in JU0.family_desc(pass_):
m = AJ0._margins(a, bb) # noqa: SLF001
if not m:
continue
if pass_ == "p2zh": # решающая печать — zh; en описателен и в ИТОГ не вынесен
ck(f"{pass_}: {a} vs {bb} = {sst.mean(m):+.2f} напечатан",
has(t, f"{sst.mean(m):+.2f}"))
print("\n=== ПОЗИЦИОННЫЙ НАКЛОН (требование ЗАКОНа промта) ===")
ck("наклон объявлен замеренным по всем проходам", has(t, "0.078") and has(t, "0.469"))
ck("поправленное значение довеска напечатано", has(t, "-1.65") or has(t, "1.65"))
print("\n=== ЦЕНА p95 ===")
import itog22 as IT # noqa: PLC0415
zh = M.units_zh()
for m in P2.tenants():
dc = [P2.draft_cost(m, u["uid"]) for u in zh]
ec = []
for u in zh:
f2 = MONEY.OUT / f"{P2.tag_edit(m, u['uid'])}.json"
if f2.exists():
ec.append(json.loads(f2.read_text(encoding="utf-8")).get("cost_usd", 0.0))
if not dc:
continue
pair = [d + e for d, e in zip(dc, ec, strict=False)] if ec else dc
ck(f"{m}: связка p95 {IT._p95(pair):.5f}", has(t, f"{IT._p95(pair):.5f}")) # noqa: SLF001
print("\n=== ПУСТОЙ ВЫХОД И ГРАНИЦА ДВОЙНОЙ ОПЛАТЫ ===")
empty = []
for globs in MONEY.TAGS.values():
for g in globs:
for f3 in MONEY.OUT.glob(g):
r3 = json.loads(f3.read_text(encoding="utf-8"))
if r3.get("cost_usd", 0) > 0 and not (r3.get("content") or "").strip():
empty.append(r3["cost_usd"])
if empty:
ck(f"оплачено за пустой выход ${sum(empty):.5f} напечатано", has(t, f"{sum(empty):.5f}"))
ck(f"{len(empty)} пустых вызовов объявлено", has(t, f"{len(empty)} вызов"))
# граница двойной оплаты обязана быть ВЫВОДИМОЙ: расход Ф2 до атомарного замка
import subprocess # noqa: PLC0415
at = subprocess.run(["git", "log", "--format=%at", "-1", "fd3e522"],
capture_output=True, text=True, cwd=REPO).stdout.strip()
if at:
pre = 0.0
for g in MONEY.TAGS["Ф2"]:
for f4 in MONEY.OUT.glob(g):
if f4.stat().st_mtime < int(at):
pre += json.loads(f4.read_text(encoding="utf-8")).get("cost_usd", 0.0)
ck(f"верхняя граница двойной оплаты ${pre:.6f} напечатана", has(t, f"{pre:.6f}"),
"граница = расход Ф2 ДО атомарного замка, в худшем случае оплаченный дважды")
print("\n=== СУДЕЙСТВО ===")
import judge as JU # noqa: PLC0415
rs = JU.runs_used()