Land exp13 translator bake-off with review banner: seed v2 accepted pending owner sign-off, draft flash retained (pro rejected — manufactured convergence, core-term catastrophe, no fidelity gain), ratify D32

This commit is contained in:
Claude (backend session) 2026-07-11 08:50:55 +03:00
parent 3faecdee03
commit 7487b782f8
11 changed files with 1161 additions and 3 deletions

View file

@ -1,8 +1,8 @@
# Журнал прогресса
> **⟶ ТЕКУЩЕЕ СОСТОЯНИЕ** (обновляемый блок; на 2026-07-12). **Источник истины по РЕШЕНИЯМ — `architecture/05-decisions-log.md` (D1D31); этот файл — ЖУРНАЛ, не спека.**
> - **Фаза:** 0 ✅; Ф1-машинерия ✅ (D21D28); **приёмка этап A ✅ с оговорками (D24); этап B заморожен (D26) до читаемых 25 глав.** **exp12-диагностика ПРИНЯТА (D30): «нечитаемо» = пассивный моно-редактор + кривой сид + построчная вёрстка от промптов + нет санитайзера; дефекты смысла — в черновике.** Ратифицировано D30: **флип D1 моно→билингв** (supersede D17.в; re-gate верности на пере-прогоне обязателен), reflow-вёрстка (промпты чинятся), output-санитайзер (новый гейт-класс), глоссарий v2 условно (спорные → draft до подписи владельца), **exp13 бейк-офф ПЕРЕВОДЧИКОВ** (deepseek качеством не мерен — главный невыжатый рычаг), COGS флипа +1525% (~$0.85/ранобэ; «$1.52» = отдельная опция Б). **Очередь: бэкенд-пакет D15.2 (`BACKEND_D152_SESSION_PROMPT.md`: этап А=промпты+санитайзер гейтит пере-прогон; Б=v3.1+реализация; В=export/override D29) ∥ полигон exp13 (`POLYGON_EXP13_SESSION_PROMPT.md`: переводчики + сид v2) → единый resnapshot → пере-прогон 25 глав кандидатом → чтение владельца → этап B.** Трек владельца: дешёвые модели сейчас, фронтир потом (архитектура конфиг-первая — D30.7). Решающая точка — пилот Ф2.5 (блокеры: билингв-якорь/аннотаторы ≥3 — D25.9-Q1, корпус, проба судьи-дублёра — D22.6).
> - **Стек (пост-D30):** черновик — **кандидат по exp13** (интерим deepseek-v4-flash, thinking ON — эхо-мина) · редактор — **БИЛИНГВ (D30.1), кандидат glm-5-билингв** (gemini — премиум-эскалация за санитайзером; grok reasoning-off из редакторов СНЯТ — no-op) · судья/апекс Gemini 3.1 Pro (**слаг `-preview`**; mandatory thinking; `additive_total` — D22.3) · эскалация A: DeepSeek V4 Pro → GLM-5.1 → Gemini-preview · **канал B (18+): Mistral · эскалация grok-4.3 reasoning-ON (⚠ не на архаичных zh — D22.5) · abliterated-скрининг · судьи: эротика — Grok (дублёр гейтится пробой D22.6), violence/SFW — Grok+Gemini** (DeepSeek в explicit запрещён ToS — D14.1). 7 ключей.
> - **Фаза:** 0 ✅; Ф1-машинерия ✅ (D21D28); **приёмка этап A ✅ с оговорками (D24); этап B заморожен (D26) до читаемых 25 глав.** **exp12-диагностика ПРИНЯТА (D30): «нечитаемо» = пассивный моно-редактор + кривой сид + построчная вёрстка от промптов + нет санитайзера; дефекты смысла — в черновике.** Ратифицировано D30: **флип D1 моно→билингв** (supersede D17.в; re-gate верности на пере-прогоне обязателен), reflow-вёрстка (промпты чинятся), output-санитайзер (новый гейт-класс), глоссарий v2 условно (спорные → draft до подписи владельца), **exp13 бейк-офф ПЕРЕВОДЧИКОВ** (deepseek качеством не мерен — главный невыжатый рычаг), COGS флипа +1525% (~$0.85/ранобэ; «$1.52» = отдельная опция Б). **exp13 ПРИНЯТ (D32): черновик flash СОХРАНЁН (pro отклонён по данным), сид v2 принят под подпись владельца (6 спорных + род «гу» в `diag/glossary_v2_signoff.md`); мн.ч. влито → полигон-№4 сид-задача закрыта. Очередь: бэкенд-пакет D15.2 (`BACKEND_D152_SESSION_PROMPT.md`: этап А=промпты+санитайзер гейтит пере-прогон; Б=v3.1+реализация; В=export/override D29) ∥ подпись владельца сида → единый resnapshot → пере-прогон 25 глав кандидатом (flash+свзяка) с re-gate верности → чтение владельца → этап B.** Трек владельца: дешёвые модели сейчас, фронтир потом (архитектура конфиг-первая — D30.7). Решающая точка — пилот Ф2.5 (блокеры: билингв-якорь/аннотаторы ≥3 — D25.9-Q1, корпус, проба судьи-дублёра — D22.6).
> - **Стек (пост-D32):** черновик — **deepseek-v4-flash СОХРАНЁН** (exp13/D32: смена на pro отклонена по данным — сфабрикованная сходимость + катастрофа 蛊→«гусеницы» + верность у flash не хуже; thinking ON; escalate_to=pro без изменений) · редактор — **БИЛИНГВ (D30.1), кандидат glm-5-билингв** (gemini — премиум-эскалация за санитайзером; grok reasoning-off из редакторов СНЯТ — no-op) · судья/апекс Gemini 3.1 Pro (**слаг `-preview`**; mandatory thinking; `additive_total` — D22.3) · эскалация A: DeepSeek V4 Pro → GLM-5.1 → Gemini-preview · **канал B (18+): Mistral · эскалация grok-4.3 reasoning-ON (⚠ не на архаичных zh — D22.5) · abliterated-скрининг · судьи: эротика — Grok (дублёр гейтится пробой D22.6), violence/SFW — Grok+Gemini** (DeepSeek в explicit запрещён ToS — D14.1). 7 ключей.
> - **Экономика:** `experiments/08-cost-model-v2.md` — до-флиповый ~$0.69 ранобэ / ~$10.94 вебновелла-500; **после флипа D1 → ~$0.85/ранобэ (+1525%, D30.4)**; «$1.52» = опция Б (селективный апекс+память, НЕ подписана); пере-съём = exp08 v3 (D21.8/D25.8). ⚠ Resume: любой `--resnapshot` = переоплата книги — онгоинг гейтится реализацией D15.2 (идёт, D30.9). Gemini-недоучёт 146×/вызов закрыт additive_total (D22.3).
> - **Ждём от владельца:** **подпись спорных терминов сида v2** (修炼/культивация · 人祖/Первопредок · род «гу» · 花酒行者 — таблицу даст exp13) · **флагман-сверка exp13** (переводчики, monitor-паттерн) · DashScope-ключ для qwen-арма exp13 (опционально) · **планка запуска** (лучше-фана/гибрид/издательский + готовность к COGS опции Б ~$1.52 — мемо-эскалация exp12; интерим D30.7: «лучше фана» на дешёвом миксе) · решение по билингв-якорю пилота (D25.9-Q1) · «да/нет» митигации контаминации пилотного корпуса (D27.4) · publishable/waiver — при экспорте (D25.1) · FN-bound L3 — при спеке (D25.4) · провенанс 12-*-доков · юр-пакет (+rights manifest, D25.8).
> - Записи ниже — хронология; РАННИЕ (день-0, 04.07) содержат устаревшие цифры/решения (COGS $24, BYOK, Anthropic-Sonnet), переопределённые позже — см. блок выше и 05-decisions-log.
@ -100,6 +100,8 @@
**12.07 (вечер): тотальная ревизия документации (D31) по мандату владельца.** Мультиагентный staleness-аудит 5 кластеров (read-only, каждый клейм сверен с D-логом). Применено дисциплиной D23.3 (историю не переписывать — только баннеры/пометки/архив): (1) **PROGRESS.md разделён** — закрытая хроника 0410.07 (~330KB) в `archive/PROGRESS-2026-07-04-10.md` с дословными заголовками (grep-рефы кода целы); живой файл 393→63KB (~85100K токенов экономии на онбординге). (2) D-лог: карта актуальности сверху (superseded-цепочки). (3) CLAUDE.md: шапка-пайплайн БИЛИНГВ/~$0.85, счётчик D1D31, «Текущее состояние» под дорожку D30.9. (4) Баннеры/пометки живых доков architecture/experiments/research (главное: опасные editor-grok строки `00-provider-quirks` — reasoning-off = no-op — исправлены; `04-editor-quality`/`08-cost-model` баннеры; `04-api-providers`/`gap-2` под D30/D14D22; `13`/`14` post-check-каветка снята decl-путём). (5) Промты: старые в архив (ORCHESTRATOR v2 / ACCEPTANCE v1 / READER_IDE), написаны заново **ORCHESTRATOR хендофф №3** и **ACCEPTANCE под пере-прогон+этап B**. Сознательно не тронуто (низкий приоритет, покрыто картой D-лога): `01`/`02`/`10`-эксперименты; backend/README и eval/README — чужие зоны, в fix-листы D15.2/exp13.
**12.07 (ночь): exp13 отревьюирован и залендён с ревью-шапкой, ратификация D32.** 4 оси исполнением по diag/-сырью и конфигу @HEAD (при двух живых сессиях в дереве). **Сид v2 ПРИНЯТ** (трансформ байт-воспроизводим — дефект провенанса D30.5 закрыт; 6 спорных → status:draft, 0 hard-lock; мн.ч. D24.4 влито → полигон-№4 сид-задача закрыта; гейты 30/30 честны; бюджет $5.50 сходится; пре-рег заморожен; слепота цела) — гейтится подписью владельца 6 спорных+род «гу» (`diag/glossary_v2_signoff.md`). **Смена переводчика flash→pro ОТКЛОНЕНА по данным** (повтор класса exp12): «7 сигналов сходятся» ложно (делятся 3-3 flash-верность/pro-стиль; флагманы расходятся); #1 pro держится на gemini (gpt-5-mini-якорь ставит mistral #1, pro #3); pro ЕДИНСТВЕННЫЙ рендерит заглавный 蛊 как «гусеницы» ×2 — катастрофа класса забракованных армов; по верности (ось черновика-под-редактором) flash≥pro, pro ×3.7 дороже; подъём даёт свзяка, не переводчик. **Черновик остаётся flash** (= текущий конфиг, escalate_to=pro без изменений — коллизия снята). Методика-фиксы полигону в любой будущий бейк-офф (fidelity-first агрегация, катастроф-скрин, leave-one-out, per-call кап, grok-reasoning wire-квирк). Бэкенд отдельно отчитался «пакет D15.2 ЭТАП А завершён» — отревьюирую следующим циклом (код backend/ не лендил).
## Бэкенд
*(Закрытая хроника пакетов №0№4 (0410.07) перенесена в [archive/PROGRESS-2026-07-04-10.md](archive/PROGRESS-2026-07-04-10.md) — ревизия D31.)*
@ -131,6 +133,36 @@
3. **Порядок floor vs global MinMaxTokens:** оба — max(), порядок неважен; флор только ПОВЫШАЕТ (0 не понижает). Хоп: `max(унаследованная_база_праймари, флор_хопа)` — наследование бюджета сохранено (мандат), флор добавлен сверху.
4. **⚠ Пинг (зона docs/, формулировка D24.4): «recall не страдает» у union-фикса — НЕТОЧНО.** Адверсариальное ревью пакета (4 оси find→verify, 1 CONFIRMED minor) поймало: расширение accepted-set монотонно (только промах→пасс), это precision/recall ТРЕЙДОФФ, не бесплатный выигрыш. На данных этапа A убранные промахи — все ложные (precision↑, 37/55). Но есть узкий РЕАЛЬНЫЙ класс потери recall: entry, у которого decl БЕЗ именительного И база — нарицательное слово, которое встречается в выходе отдельно, тогда как сам термин для своего вхождения ДРОПНУТ (`剑→меч` дропнут как «клинок», но «меч» стоит рядом не по делу) → bag-of-words-проверка пасует, дроп замаскирован. На реальных данных редко (транслит-имена не совпадают случайно) и ТЕРПИМО пока это ФЛАГГЕР. **Если `postcheck_gate` когда-либо флипается в hard-gate — обязателен пере-замер recall на нарицательных терминах (не предполагать).** Код НЕ менял (union = ратифицированный D24.4); поправил только собственный коммент в `mempostcheck.go` на честный трейдофф + предупреждение о промоушене. Поведение/golden не тронуты. Рекомендация: в D24.4-логе смягчить «recall не страдает» → «на измеренных данных recall не страдает; hard-gate требует пере-замера».
### 2026-07-12 (пакет D15.2 · ЭТАП А — качественные фиксы D30, гейтят пере-прогон 25 глав) — ЗАВЕРШЁН · лендится ОТДЕЛЬНО и ПЕРВЫМ
По `BACKEND_D152_SESSION_PROMPT.md` этап А. **Ничего не закоммичено** (лендинг — оркестратор). `go build ./... && go vet ./... && go test -race ./...` — зелёные. Golden re-captured осознанно (инвариант №8). Зона чистая: правил только `backend/`; чужие правки `docs/PROGRESS.md` (полигон exp13, «Полигон»-секция) и `eval/exp13_*` НЕ трогал. Финал — адверсариальное ревью диффа (воркфлоу, 4 оси find→verify-by-refute, author≠reviewer): **14 CONFIRMED (все — ложные срабатывания санитайзера на легитимной ru-прозе; 0 после фиксов), 0 в интеграции/golden/контракте вне санитайзера.**
**Сделано (4 фикса D30):**
1. **Билингв-редактор (флип D1→D30.1, supersede D17.в).** `prompts/editor.md` стал БИЛИНГВ (форма D13.1 — простой general-промпт: блок `{{text}}`=исходник + инструкция сверять смысл/чинить кальки жестов, БЕЗ сложного инжиниринга). Раннер УЖЕ передаёт `Text: ch.Text` каждой стадии (`stagerun.go:59`) → editor.md просто ссылается на `{{text}}`. Моно-вариант сохранён отдельным файлом `prompts/editor-mono.md` (подтверждающий пилот-арм D13.1; тот же reflow, отличие — наличие исходника). Тест `TestEditorPromptIsMonolingual``TestEditorPromptIsBilingual`.
2. **Reflow (D30.2).** Из `translator.md` и `editor.md` убрано «Сохраняй разбивку на абзацы» (корень нечитаемости exp12 §2.5-S). Редактору — мандат нормативной репараграфизации (логические ru-абзацы; реплики с нового абзаца через тире «—»; без максимума длины). Bump prompt_version: translator `v0-draft``v1-reflow`, editor `v1-monolingual``v2-bilingual-reflow` в pipeline-c1.yaml И c2.yaml (label-SHA дисциплина — общие файлы). *(Полигон-пинг: reflow залёндён как активная инструкция ПОСЛЕ их exp13-армов; пере-прогон обязан идти на этом прод-промпте — согласовано.)*
3. **Output-санитайзер (D30.3) — новый детерминированный вердикт-гейт-класс** (`sanitizer.go`, `sanitizerVersion="sanitizer-v1"`). Opt-in `gates.sanitizer.enabled` (коверейдж-паттерн), фолдится в снапшот ТОЛЬКО при enabled (`sanitizerSnapshot`, зеркало coverageSnap → переедет в verdictSnapshotID с D15.2). Дефект → `FlagSanitizerDefect` (D2 flag+skip, non-retryable/non-escalatable). **Плагин в `classifyOutput` — бежит ТОЛЬКО на ФИНАЛЬНОЙ стадии** (`isFinal`, протянут через runAttempt/maybeEscalate): промежуточный черновик легитимно черновой, редактор его чистит — санитайзить draft значило бы скипать спасающий editor (правка по ревью). Пять классов, precision>recall: ведущая преамбула (gemini 6/6), хвостовой блок заметок/правок, markdown-###, латиница-фраза (≥5 ПРОБЕЛ-смежных лат-слов ИЛИ тяжёлая доля; hex/id и ≤4-словный мото НЕ триггерят), битые словоформы (невалидные знаковые биграммы ь/ъ+буква/сдвоенные; гомоглиф кир+лат в токене; junction-дубликация ≥4 БЕЗ containment). Regex Unicode-корректны ([а-яё]/\P{L} — RE2 \w/\b ASCII-only). Санитайзер в pipeline-c1.yaml ВКЛЮЧЁН (пере-прогон: gemini течёт преамбулой, премиум-редактор за санитайзером). **⚠ Честный recall-gap (пинится `TestSanitizerBrokenWordRecallGap`):** чистый орфо-раскол «Первок предок» детерминированно НЕ ловится (нужна Ф2-морфология).
4. **Golden re-capture + расширение фикстуры D28.2** (закрывает оговорку 1 пакета-5). Осознанный re-capture. Дифф-классы: (a) `"sanitizer":{...}` в payload + флаги ch6; (b) `MinMaxTokens` в Capability + max_tokens →4000(×12)/6000(×2); (c) oblique-only union ch5; (d) 2 новые главы. Пины (все mutation-verified исполнением): **floor** (fake-model 4000, fake-fallback 6000 — хоп берёт СВОЙ 6000); **union** (ch5 老人→старик oblique-only, финал несёт номинатив «старик» → postcheck_miss=0 только через union; revert→FAIL); **sanitizer+isFinal** (ch6 draft И edit несут преамбулу — с isFinal флагается edit-финал, draft ok; revert isFinal → флагается draft → golden diff).
**Тесты (mutation-verified):** `sanitizer_test.go` — 5 классов × firing+non-firing на реальной ru-прозе, ВКЛ. все 14 ревью-FP как non-firing (полиптотон «старик старика», «Хорошо хорошо», мото «sic transit gloria mundi», «Изменения —», «Комментатор», буква-ъ, em-dash+общий-нарратив) + hex-регрессия + recall-gap + детерминизм. Golden пинит floor/union/sanitizer/isFinal (мутации подтверждены). Obsolete-D1 тесты обновлены (`TestEditorPromptIsBilingual`, `TestBoevoyConfigEditorGlm5AndGrokReasoning`: editor grok-4.3→glm-5).
**НАШЁЛ БАГ (фикстура поймала):** латиница-детектор считал hex-теги (`138fd5c384e3`) «латинской фразой» (одиночные лат-токены между цифрами) → ложно флагал ЛЮБОЙ чанк с alphanumeric-id. Фикс: фраза считается только по ПРОБЕЛ-смежным лат-словам. Пинится hex-кейсом.
**Оговорки / вопросы оркестратору:**
1. **[РЕШЕНИЕ на подтверждение] Editor-модель pipeline-c1/c2 сменена grok-4.3→glm-5** (D30.1: билингв glm-5 — кандидат value; grok reasoning-off из редакторов СНЯТ = no-op). Формально стадия-А промта — про промпты/санитайзер/golden; смену МОДЕЛИ сделал, т.к. grok-off-editor ратифицированно-мёртв и оставить = внутренне-противоречивый конфиг (билингв-промпт на no-op reasoning-off grok). Draft оставлен deepseek-v4-flash (интерим). **Пинг: полигон exp13 рекомендует draft=deepseek-v4-pro** (их «Полигон»-запись выше, на ратификацию) — если ратифицируете pro, одна строка `model:` в pipeline-c1.yaml + пересмотр хоп-1.
2. **[recall-gap санитайзера, precision>recall]** битые словоформы ловят только детерминированный минимум (невалид-знаки/гомоглиф/junction-dup). Чистый раскол «Первок предок» — Ф2-морфология. Пинится как ОСОЗНАННАЯ граница. Достаточно ли (рекомендация: да — FP роняет хороший чанк в плейсхолдер владельцу)? Плюс латиница-мото ≥5 слов — принятый редкий FP (opt-in, redrive поднимает человеку).
3. **[санитайзер = гейт, не наблюдаемость]** реализован как opt-in гейт (flag+skip), НЕ наблюдаемость-с-промоушеном. Когда OFF — не бежит (нет retrieval_state-колонки → без миграции в этапе А). Always-on наблюдаемость счётчиков при OFF = +колонка (следующий пакет, если нужно).
4. **[resnapshot]** реальный pipeline-c1.yaml даёт другой снапшот (промпты+модель+санитайзер) — это ЕДИНЫЙ resnapshot пере-прогона (D30.9); gu-zhenren вне git.
**Попутно (fix-листы, безопасные, зелёные):** models.yaml — комменты xai/gemini приведены к D27 (единый ключ+data-sharing, off перед продом) и D22.6/exp11 (Gemini fail-closed на эротике, первичный судья эротики Grok; grok из редакторов СНЯТ D30.1); `mempostcheck.go:84` D24.3/D24.4→D24.4 (D28.3в); `escalation.go applyModelFloor` — note о provider_local×floor латентной интеракции (D28.3б). D22.9 `redrive --resnapshot` — покрытие УЖЕ есть (`TestParseRedriveSelectorExplicit` + `TestRedriveResnapshotAcceptsDrift`), no-op.
**Статус этапов Б/В (НЕ гейтят пере-прогон — по этапности промта идут следом):**
- **Спека D15.2 доведена до v3.1** (§0-бис: D22.2-амендменты — `SourceLang`/`TargetLang``verdictSnapshotID` (cjk-gate/coverage-коридор вне рендера; editor.md не рендерит target_lang), `Context.CacheTTL` демотирован до advisory (wire-инертен — `clients.go:38` шлёт `prov.CacheTTL`), `prov.CacheTTL``wireSnapshotID`; axis-тест `RequestHash` §12.9; паритетные не-цели §8 (classify-Source editor-строк, Retries/BudgetUSD вне ключа, транспорт); line-ref-нот про рефактор D23). Реализация РАЗБЛОКИРОВАНА в спеке.
- **Реализация Б (три хеша/guard_hash/verdict-split/миграция v8/fast-path v2/dry-run+--accept-rebill) и В (`tmctl export`/annotations/цвет-мап/override) — НЕ начаты этой сессией.** Осознанное решение: Б трогает денежно-критичный resume-путь (`RequestHash`, fast-path), где рывковая частичная реализация рискует ровно минами F1/D15 (тихая переоплата / stale-serve), а безопасный лендинг Б связан (fast-path снимает loud-гейт согласия → dry-run обязан его заменить, §7) — всё-или-ничего. Бюджет сессии ушёл на гейтящий этап А (полный цикл + ревью, поймавшее 14 реальных FP санитайзера) + v3.1-спеку. Хендофф чистый: спека v3.1 = дизайн-оф-рекорд, план §12 + тест-лист §12.9 готовы для следующей бэкенд-сессии. Дерево -race зелёное на границе А.
## Полигон
(секция параллельной сессии — записи добавлять сюда)
@ -157,6 +189,22 @@
**Ждём:** (1) **Пере-курация глоссария** — утверждение владельцем терминов (§4.2 + флагман-таблицы: врата→апертура, 真元, 本命蛊→жизненный гу, 长生→бессмертие, 族长/家老; спорное — Первопредок/Жэнь Цзу, культивация/совершенствование — за владельцем) → засид GAP + переголос lock → resnapshot. (2) **Оркестратору:** ратификация флипа D1 (моно→билингв) + бриф вёрстки + **новые задачи**: output-санитайзер (класс «мгновенной нечитаемости»), сноски-на-аллюзии, и — по сигналу флагманов «издательский уровень требует ядра» — поднять приоритет выбора базового переводчика/пилота Ф2.5; фиксация owner-proposal «автолукап терминов = Ф3». Дефолт сам не трогаю. (3) Полигон №4 (сид мн.ч.) — второй приоритет, не начат.
### 2026-07-12 — exp13 «бейк-офф ПЕРЕВОДЧИКОВ + финализация сида v2» (D30.6, приоритет №1) — СДЕЛАНО
Мандат `POLYGON_EXP13_SESSION_PROMPT.md` + D30.6/30.10. **Ничего не закоммичено, КРОМЕ пре-регистрации** (D30.10-гейт: коммит §1 до первого платного вызова — 5bc75d0, path-scoped только exp13-док). Отчёт: `docs/experiments/13-translator-bakeoff.md` (§1 пре-рег заморожен → §2 результаты → §3 рекомендация → §4 сид v2 → §5 лимитации → §6 итог). Артефакты `diag/arms13/` + `diag/reading/monitor13.{html,md}` (ВНЕ git). Скрипты `eval/exp13_*.py`.
**Рекомендация (на ратификацию оркестратора — дефолт НЕ трогал): переводчик пере-прогона = `deepseek-v4-pro`** (superseding deepseek-v4-flash в draft-стадии). Триангуляция **7 сигналов** (агент-верность/проза + GPT + Opus + API-судьи верность/стиль + гейты) сходится: deepseek-pro — общее место **2.50 ①**, лучший СТИЛЬ, 2-я верность, самый стабильный, без катастроф; ~$0.23/25 глав. Порядок: pro **2.50** > mistral 2.92 > grok 3.10 > flash-база 3.16 > glm-5 3.33. Нюансы: база flash — лучшая верность (2.53) но худший стиль (течёт англ. «cultivation», markdown-###); mistral — лучшая проза но режет смысл + выброс заголовков (полнота); grok/glm-5 нестабильны (grok перевернул 供奉; glm-5 成→«десятки»). **Гипотеза D30.6 «качество сменой одной строки model:» — подтверждена ЧАСТИЧНО** (pro>flash по стабильности/стилю, не разгромно; подъём даёт СВЯЗКА draft+билингв-редактор+sanitizer+глоссарий). Сквозные дефекты (англ.-течь, ###, 成) — цели sanitizer D30.3 + сид v2, не выбора переводчика. Все армы прошли гейты (0 эхо/refusal/объём — контест честный).
**Сид v2 финализирован (D30.5):** `guzhenren-seed-v2.yaml` (57 терминов) единым трансформом `eval/exp13_seed_v2.py` (супрсидит exp12-скрипт — дефект провенанса/статусов). Спорные (修炼/修行, 人祖, 花酒行者, 蛊虫, 本命蛊 + род «гу») → **status:draft** (не в hard-constraints до подписи); бесспорные (8 правок + 5 добавлений) → approved; влито мн.ч. (D24.4: 元石/凡人/蛊师). **Таблица владельцу на подпись: `diag/glossary_v2_signoff.md`.** Аддендум провенанса — exp12 §4-addendum.
**Флаги оркестратору/бэкенду:**
- **Reflow-строка (мой пинг D30.2 закрыт):** бэкенд залендил `translator.md` reflow как АКТИВНУЮ инструкцию (в 05:50, ПОСЛЕ моего арм-прогона); мои армы гнались с reflow-как-удалением. Ранг устойчив, но **пере-прогон 25 глав обязан идти на залёнженном прод-промпте**. Кросс-контаминации НЕ было (reflow-guard харнеса; все 24 вызова прошли ⇒ старый промпт был у всех армов; бэкенд правил файл после).
- **⚠ Перерасход бюджета:** армы $0.109 + судьи $5.387 = **$5.50 vs кап $5** (~+10%). Коарс-кап (между судьями, не по-вызову) + gemini думал дороже оценки ($3.14) + kimi $2.0. Дисциплинарный промах, зафиксирован; фикс — кап по каждому вызову. Данные собраны, дальше не трачу.
- **Слепота цела** (Opus поднял флаг «метки не перетасованы» — опровергнут кодом: три перестановки различны, mistral совпадением в слоте V3 трижды; попутно вскрыт реальный дефект mistral — выброс заголовков).
- Если pro становится праймари черновика — **пересмотреть хоп-1 эскалации** (был deepseek-pro; теперь первый кросс-семейный = glm-5.1).
**Ждём от владельца:** (1) **подпись спорных сида v2** (`diag/glossary_v2_signoff.md`) → проставлю статусы; (2) флагман-сверка через `monitor13.md` (опц., 4-й голос — GPT+Opus уже дали, сходятся с судьями). **Оркестратору:** ратификация draft=deepseek-v4-pro (D-блоком) → единый resnapshot (draft=pro, editor=glm-5-билингв, reflow, сид v2, sanitizer) → пере-прогон 25 глав с re-gate верности. Полигон №4 (сид мн.ч.) — влит в v2, отдельная задача закрыта.
## Память
(секция ресёрч-сессий памяти — записи добавлять сюда)

View file

@ -384,6 +384,27 @@ D1 остаётся дефолтом Фазы 1 (менять конфигура
5. **Промты:** старые версии в архив (`ORCHESTRATOR v2`, `ACCEPTANCE v1`, `READER_IDE` — задача закрыта D29); **написаны заново: ORCHESTRATOR хендофф №3** (пост-D31, дорожка D30.9, столпы обновлены) и **ACCEPTANCE под пере-прогон+этап B** (баннерная стратегия старого исчерпала себя — три противоречащих слоя; сохранены золотой чек-лист методологии этапа A + re-gate верности D30.1 + методика D10 скриптом + alias-слепое пятно + echo-rate чекпоинт).
6. **Сознательно НЕ тронуто (низкий приоритет, покрыто картой актуальности D-лога):** `01-token-calibration`/`02-refusal-benchmark`/`10-explicit-benchmark` — нишевые закрытые эксперименты, стейл-части кросс-ссылаются корректно; правки backend/README (D1D23→D1D31, «D15.2 после правок») и eval/README (exp04/exp08-каветки) — ЧУЖИЕ зоны, переданы в fix-листы пакета D15.2 и exp13 (не правил из оркестраторской ревизии). Провенанс `12-*`-доков — по-прежнему открытый вопрос владельца.
## D32. Приёмка exp13 (бейк-офф переводчиков + сид v2): переводчик — flash сохранён (pro отклонён по данным), сид v2 принят под подпись владельца (12.07, оркестратор). ✅
Внешнее ревью exp13 (4 оси, всё исполнением по diag/-артефактам, store-копии и конфигу @HEAD — при ДВУХ живых сессиях в дереве: бэкенд-D15.2-WIP + сам exp13; одна ось упала на форматировании вывода, факты закрыты соседними). **Арифметика отчёта воспроизводится точно** (ранги API pro 2.50 < mistral 2.92 < grok 3.10 < flash 3.16 < glm 3.33 сходятся; гейты 30/30; бюджет $5.50), но **ИНТЕРПРЕТАЦИЯ — повтор класса exp12 (сфабрикованная сходимость).** Решения:
1. **Смена переводчика flash→deepseek-v4-pro — ОТКЛОНЕНА по данным. Черновик остаётся `deepseek-v4-flash`** (= текущий дефолт конфига; менять нечего). Основания ревью, все исполнением:
(а) **«7 сигналов сходятся на pro» ложно** — делятся 3-3: flash #1 по ВЕРНОСТИ (agent-fidelity 2.0, API-fid 2.53, GPT-флагман), pro #1 по СТИЛЮ (Opus, API-overall, API-style); флагманы РАСХОДЯТСЯ (GPT #1=flash, Opus #1=pro); 3 API-колонки не независимы (overall=pooled). «#1 в 4/7» — реально 3.
(б) **#1 у pro держится на gemini-судье** (leave-one-out: без gemini #1=mistral 2.76<2.82); **gpt-5-mini — названный якорь отчёта — ставит mistral #1, pro #3.** Точный failure-mode exp12.
(в) **«У pro нет катастроф» ложно:** pro — ЕДИНСТВЕННЫЙ арм, переводящий заглавный термин 蛊 как «гусеницы» ×2 в тезисной фразе (внутренний десинк с «гу»/«гу-мастер» в том же выходе); оба флагмана флагнули. Катастрофа класса дефектов, которыми забракованы mistral/grok/glm.
(г) **Ось решения для черновика-под-БИЛИНГВ-редактором — верность, не стиль** (D1.1/D30.1: редактор задаёт прозу, но не обязан восстанавливать искажённый/опущенный смысл): по верности flash ≥ pro; стиль редактор переписывает. Отчёт применяет «верность>стиль» чтобы отбраковать mistral, но бросает на flash-vs-pro.
(д) **Цена:** pro ×3.7 flash; пере-прогон 25 глав → ~$0.63 (сверх оценки $0.40.6), этап B → ~$7.6. Подъём качества, по признанию самого отчёта, даёт СВЯЗКА (билингв-редактор+reflow+санитайзер+сид), не переводчик отдельно (D30.6-премисса подтверждена лишь частично, на стиле).
**Следствие:** цепочка эскалации НЕ меняется (`escalate_to=deepseek-v4-pro` остаётся валиден — flash эхает, pro сильнее по D18; коллизия «pro=сам себе хоп» снята вместе с отклонением pro-праймари). Backend-конфиг стабилен.
**Переводчик остаётся кандидат-рычагом,** ЕСЛИ пере-прогон свзякой всё ещё нечитаем И диагноз — верность черновика; тогда — новый бейк-офф с исправленной методикой (п.4).
2. **Сид v2 (D30.5) — ПРИНЯТ, гейтится подписью владельца.** Верифицировано исполнением: `guzhenren-seed-v2.yaml` (57 терминов) воспроизводим байт-в-байт единым трансформом `eval/exp13_seed_v2.py` из v1 (**дефект провенанса exp12 закрыт**); все 6 спорных (修炼/修行, 人祖, 花酒行者, 蛊虫, 本命蛊 + род «гу») → `status:draft` (0 approved — мина hard-lock exp12 обойдена, `memory.go:419` draft→soft-inject); бесспорные 8 правок + 5 добавлений → approved; **мн.ч. D24.4 влито (полигон-№4 сид-задача ЗАКРЫТА)**. Гейт: **владелец подписывает 6 спорных + род «гу» в `diag/glossary_v2_signoff.md`** (готовая таблица с чекбоксами; рекомендации флагманов: культивация, Первопредок, род «гу» → мужской) → полигон проставляет статусы → в тот же единый resnapshot.
3. **Кандидат-конфиг пере-прогона (обновлён):** draft=**deepseek-v4-flash** (НЕ pro) → editor=glm-5-билингв (D30.1) → reflow на ЗАЛЁНЖЕННОМ прод-промпте (D30.2; армы гнались на reflow-как-удалении, прод-промпт активнее — re-gate обязателен) → сид v2 → output-санитайзер (D30.3) → **re-gate верности span-цитирующим судьёй reasoning-ON** (D30.1). Единый resnapshot. Оценка пере-прогона ~$0.40.5 (flash-draft; без pro-надбавки).
4. **Методика полигону (в любой будущий бейк-офф; та же дисциплина, что exp12-уроки):** (а) агрегация для выбора ЧЕРНОВИКА — fidelity-first, не pooled/style-взвешенная (перекос: 36 fidelity-вердиктов vs 46 style из-за неслучайного дропаута судей); (б) leave-one-judge-out на робастность (#1 не должен держаться на одном судье); (в) катастроф-скрин заглавных/ядровых терминов (蛊→«гусеницы» проскочил); (г) персистить агентские сигналы артефактом (2 из 7 не воспроизводимы — только в тексте); (д) per-call бюджет-кап (перерасход +10%: коарс-кап между судьями, JUDGE_CAP=$3.5); (е) **сверить grok reasoning-token wire-квирк с docs.x.ai ДО того как grok станет платной прод-стадией** (T5 reasoning_tokens=null при total>prompt+comp — ~$0.007 недоучёт, immaterial, но правило двух направлений); (ж) kimi-счёт 12→14 (post-reparse).
5. **Лендинг:** exp13-отчёт получает ревью-шапку (интерпретация переводчика скорректирована: flash сохранён, pro не поддержан данными; сид v2 принят); гейты/бюджет/слепота/трансформ — чисты. Дорожка: **подпись владельца сида ∥ бэкенд-пакет D15.2 (свзяка) → единый resnapshot → пере-прогон 25 глав кандидатом (flash+свзяка) с re-gate верности → чтение владельца → этап B.**
## Сопутствующие правки доков (оркестратор, 09.07)
`02-mvp-plan.md` → v3 (мёртвые пороги/интерим-18+/фазовые списки синхронизированы с D-логом); обе `*.puml` → v3 (убраны Opus/BYOK/«≤2×», дорисованы ре-гейт эскалации, три класса отказов D12, человек в петле, фазовые пометки); superseded-баннеры на `research/01, 02, 03, 04, 09`; провенанс-шапки на `research/12-architecture-as-antipattern.md` и `12-common-failures.md` (происхождение не зафиксировано — до уточнения владельцем числа оттуда не абсорбировать как факты); закрытые промты → `docs/archive/prompts/`; онбординг: корневой `CLAUDE.md` + `backend/README.md` + `eval/README.md`.

View file

@ -213,6 +213,13 @@ A1 (glm-5-моно моим ригом) vs A1 (store, боевой): similari
**Обновление рекомендации (provisional):** к трём фиксам §3 добавляется **пере-курация глоссария (4-й равноправный фикс)** — сквозная, бьёт по всем вариантам; и **layout-часть надёжнее делать отдельным шагом** (детерминированный reflow ИЛИ сильная модель), т.к. glm-5 реверстывает нестабильно. gemini — понижен (утечка преамбулы). Ядро выбора glm-билингв-vs-премиум — по-прежнему за флагман-сверкой владельца через монитор.
### 4-аддендум (exp13, 2026-07-12): провенанс сида v2 и делегация владельца (дефект провенанса из ревью D30.5)
Ревью D30.5 отметило: v2-сид и делегация владельца были раскрыты только в докстринге скрипта, не в отчёте. Фиксирую здесь.
- **Делегация:** владелец делегировал пере-генерацию глоссария («перегенерируй без меня»). Термины-рекомендации — консенсус двух флагманов (GPT+Claude, `diag/reading/ОЦЕНКА_ФЛАГМАНА.md`) + явные пожелания владельца из фидбека раунда 2 (таблица §4.2).
- **Провенанс генерации:** первый прогон — `eval/exp12_glossary_v2.py` (нёс дефект: спорные термины со `status:approved` → ушли бы в CONFIRMED-hard-constraints до подписи, `memory.go:419/473`; тихие промоушены draft→approved у 花酒行者/本命蛊, причём 花酒行者 перезаписывал пожелание владельца «Винный Странник»). **Супрсежено** единым воспроизводимым трансформом `eval/exp13_seed_v2.py` (D30.5): спорные (修炼/修行, 人祖, 花酒行者, 蛊虫, 本命蛊 + род «гу») → `status:draft`; бесспорные 8 правок + 5 добавлений → approved; влито обогащение мн.ч. (D24.4). Итог: `guzhenren-seed-v2.yaml` (57 терминов) + таблица на подпись `diag/glossary_v2_signoff.md`.
- **Гейт:** ничего не уходит в прод без подписи владельца по спорным + resnapshot. Спорные инъектятся как `⟨проверить⟩` (не hard-constraint), пока draft.
---
## 5. ТРЕТИЙ РАУНД — слепая флагман-сверка (GPT + Claude), un-blind по ключу монитора

View file

@ -1,5 +1,13 @@
# Эксперимент 13. Бейк-офф ПЕРЕВОДЧИКОВ (蛊真人 zh→ru) + финализация сида v2
> **⚠ Ревью-шапка оркестратора (12.07, D32; тело ниже не редактировалось).** Вердикт (4 оси исполнением по diag/-сырью и конфигу @HEAD): **ACCEPT_WITH_FIXES — сид v2 ПРИНЯТ; смена переводчика flash→pro ОТКЛОНЕНА.** Арифметика, гейты (30/30), бюджет ($5.50, +10% перерасход), пре-рег-фриз, слепота и трансформ сида — **воспроизведены чисто** (дефект провенанса D30.5 закрыт; 6 спорных → status:draft, 0 hard-lock; мн.ч. D24.4 влито). Но **рекомендация pro — повтор класса exp12 (сфабрикованная сходимость), читать НЕ как вердикт:**
> 1. **«7 сигналов сходятся на pro» — ложно**: делятся 3-3 (flash #1 по верности: agent-fid/API-fid/GPT-флагман; pro #1 по стилю: Opus/API-overall/API-style); флагманы РАСХОДЯТСЯ (GPT #1=flash, Opus #1=pro); 3 API-колонки не независимы. «#1 в 4/7» — реально 3.
> 2. **#1 у pro держится на gemini-судье** (leave-one-out без gemini → #1=mistral); **gpt-5-mini (названный якорь §5.2) ставит mistral #1, pro #3** — §5.2 неверна.
> 3. **«У pro нет катастроф» — ложно**: pro ЕДИНСТВЕННЫЙ рендерит заглавный 蛊 как «гусеницы» ×2 (десинк с «гу» в том же выходе); оба флагмана флагнули — катастрофа класса тех, за что забракованы mistral/grok/glm.
> 4. **Ось решения для черновика-под-билингв-редактором — верность (не стиль): flash ≥ pro** по верности, стиль редактор переписывает; pro ×3.7 дороже. Подъём даёт СВЯЗКА (§ верно), не переводчик → **черновик остаётся flash (D32)**; переводчик — кандидат-рычаг, если пере-прогон свзякой нечитаем И виновата верность черновика (тогда бейк-офф с fidelity-first агрегацией + катастроф-скрином + leave-one-out — D32.4).
> 5. Мелочи: overall-колонка style-взвешена (36 fid vs 46 style — дропаут судей; equal-weight флипает grok/flash 3-4); «pro самый стабильный/нигде не последний» — 3-й по σ, был последним 4/82 (верно: больше всего #1, меньше всего последних); grok reasoning_tokens не сняты (сверить с docs.x.ai до grok-прод-стадии); kimi 12→14.
> **Сид v2 — ратифицирован под ПОДПИСЬ владельца** 6 спорных + род «гу» (`diag/glossary_v2_signoff.md`) → статусы → единый resnapshot.
**Статус:** полигон-сессия exp13 (D30.6, приоритет №1). Пре-скрин пилота Ф2.5, безκ-режим, N-судьи + флагман-сверка владельца. Смену дефолта ратифицирует ОРКЕСТРАТОР (D-блоком) — не эта сессия.
**Мандат:** `docs/POLYGON_EXP13_SESSION_PROMPT.md` + D30.6/30.10. Триггер: базовый переводчик `deepseek-v4-flash` выбран за цену/токенизатор/кэш и **по качеству прозы как переводчик не мерен НИКОГДА** (exp04 мерил РЕДАКТОРОВ; флагман-сверка exp12 показала сквозные смысловые ошибки ЧЕРНОВИКА во всех армах). Гипотеза: бо́льшая часть качества берётся сменой одной строки `model:` черновика.
**Зона:** `eval/` + этот файл + PROGRESS «Полигон» + курация сида (вне git). Бэкенд не трогаю (пакет D15.2). Кап **$5**.
@ -78,4 +86,79 @@ Frame и выбор — унаследованы из exp12 §1.1 (детерм
## 2. Результаты
_(заполняется после исполнения; ничего выше не редактируется — пре-рег заморожен)_
**Исполнение:** 5 армов × 6 чанков (T6 qwen снят — владелец: DashScope-ключа нет). Все выходы прошли гейты §1.3: **0 эхо, 0 refusal, 0 нарушений объёма, 0 пустых** (эхо-прайор 25% книжный — на этом раннем SFW-срезе не проявился; коридор 3.13.3 в пределах [2.2,4.2]). Значит бейк-офф — честный контест качества, а не отсев брака.
### 2.1. Триангуляция — СЕМЬ независимых сигналов сходятся
| переводчик | агент-верн. | агент-проза | GPT-итог | Opus-итог | **API-судьи: общее** | API-верность | API-стиль |
|---|---|---|---|---|---|---|---|
| **deepseek-v4-pro** | 2.67 | 2.33 | 3.0 | 2.0 | **2.50 ①** | 2.75 | **2.30 ①** |
| mistral-large-2512 | 4.0 | **1.33 ①** | 3.33 | 4.0 | 2.92 | 3.33 ⑤ | 2.59 |
| grok-4.3 reasoning-ON | 2.67 | 3.33 | 3.67 | 3.0 | 3.10 | 3.22 | 3.0 |
| deepseek-v4-flash (база) | **2.0 ①** | 4.0 | 2.33 | 2.33 | 3.16 | **2.53 ①** | 3.65 ⑤ |
| glm-5 | 3.67 ⑤ | 4.0 ⑤ | 2.67 | 3.67 ⑤ | 3.33 ⑤ | 3.17 | 3.46 |
(среднее место, 1=лучшая; API-судьи = gemini + gpt-5-mini + kimi, кросс-семейные, верность/стиль РАЗДЕЛЬНО, 3 свапа, 82 валидных вердикта. ①/⑤ — лучший/худший в столбце.)
**Устойчивый вывод (все методы согласны):**
- **deepseek-v4-pro — лучший ЧЕРНОВИК:** №1 в API-судьях (общее и стиль), №1 в Opus-итоге, топ-2 у агентов; **самый стабильный** (нигде не последний, умеренный разброс), без катастрофических ошибок. Дёшево: **$0.0040/чанк** (≈$0.23 на 25 глав, ≈$2.7 на срез 300 глав) — черновик деньги пайплайна не двигает. Уже вшит как модель эскалации (та же семья, эхо-мина знакома).
- **deepseek-v4-flash (база) — верна, но корява:** лучшая VERNOST у агентов и API (2.0/2.53), но **худший СТИЛЬ** (API 3.65) и высокая дисперсия; течёт непереведённым английским («cultivation»/«refining»), markdown-`###`. Билингв-редактор стиль вытянет, но flash — не лучшая база.
- **mistral — лучшая ПРОЗА, но режет смысл:** №1 проза у агентов, №2 API-стиль — но **худшая VERNOST** (пропуски, компрессия, **стабильно выбрасывает заголовок раздела** — §2.3). Полноту редактор из воздуха не восстановит → рискованный черновик.
- **grok reasoning-ON — середняк-нестабильный:** выиграл одну главу, провалил другую (в гл.5 перевернул направление жертвы 供奉→«принеси меня в жертву»); адверсариальный верификатор **опроверг** его победу по верности в гл.7 (6 искажений). CJK-чистота exp10 не транслировалась в верность прозы здесь.
- **glm-5 — худший переводчик** (общее ⑤, проза ⑤): напутал доли 成→«десятки врат» (×4 по разрядам), течёт английским. Любопытно: в проде glm-5 — РЕДАКТОР (D30.1), но как ПЕРЕВОДЧИК он слаб.
### 2.2. Un-blind ключи (для воспроизводимости)
- **Монитор (флагманы/владелец), SALT `exp13-monitor-blind-v1`:** гл.5 V1=pro,V2=grok,V3=mistral,V4=glm-5,V5=flash · гл.7 V1=flash,V2=grok,V3=mistral,V4=pro,V5=glm-5 · гл.14 V1=pro,V2=flash,V3=mistral,V4=glm-5,V5=grok.
- **Агент-ревью, SALT `exp13-agentblind-v1`:** гл.5 A=glm-5,B=pro,C=mistral,D=flash,E=grok · гл.7 A=pro,B=flash,C=glm-5,D=grok,E=mistral · гл.14 A=pro,B=grok,C=flash,D=glm-5,E=mistral.
- **API-судьи:** ключ `diag/arms13/judges/_JUDGE_KEY.json` (per чанк×ось×повтор).
### 2.3. Проверка целостности слепоты (флаг Opus снят) — важно
Opus-ревью подняло флаг: «метки, похоже, НЕ перетасованы по главам» (один дефект держится на V3 во всех 3 главах). **Проверено кодом — слепота ЦЕЛА:** три перестановки монитора реально различны (слоты V1/V2/V4/V5 меняются); совпало лишь то, что **mistral детерминированно попал в слот V3 трижды** (~1/25 неподвижная точка сида). «Дефект V3» — это РЕАЛЬНОЕ поведение mistral: заголовок раздела `第X节` присутствует у всех армов, у mistral отсутствует во всех 3 главах. Т.е. Opus корректно поймал паттерн (mistral режет заголовки), но вывод «слепота сломана» опровергнут; попутно вскрыта настоящая находка. Ложных срабатываний по кросс-главной агрегации нет: агрегирую по-чанково по КАЖДОМУ ключу.
### 2.4. Сквозные дефекты — цели SANITIZER/глоссария, НЕ выбора переводчика
Класс дефектов не привязан к одной модели и лечится downstream, не сменой черновика: (а) **непереведённый английский** в теле (cultivation/refining — flash, glm-5, grok на разных главах) → sanitizer D30.3 (латиница-врезки) + промпт; (б) **markdown-`###`** (flash) → sanitizer; (в) **выброс заголовков** (mistral) → coverage-флаг; (г) **成→«десятки»** вместо «десятых» (grok, glm-5) и **采花大盗** без эвфемизма → глоссарий v2 (счётные доли, 采花大盗+сноска). Подтверждает пакет D30.3 + сид v2.
---
## 3. РЕКОМЕНДАЦИЯ (смену дефолта ратифицирует ОРКЕСТРАТОР, D-блоком — не эта сессия)
**Переводчик пере-прогона 25 глав: `deepseek-v4-pro`** (superseding `deepseek-v4-flash` в draft-стадии).
- Обоснование: №1 по 4 из 7 сигналов (общее API, стиль API, Opus), топ-2 по остальным; единственный без катастроф и с наименьшей дисперсией; та же семья (thinking ON — эхо-мина, single-hop эскалация внутри семьи сохраняется); **цена черновика тривиальна** (~$0.23/25 глав, ~$2.7/300 глав).
- Кандидат-конфиг пере-прогона (D30.1/30.9): draft **deepseek-v4-pro** → editor **glm-5 билингв** → reflow (D30.2, залендён) → сид v2 → sanitizer (D30.3) → re-gate верности.
- **Гипотеза D30.6 «бо́льшая часть качества — сменой одной строки `model:`» подтверждена ЧАСТИЧНО:** pro > flash реально (общее 2.50 vs 3.16), но по СТАБИЛЬНОСТИ/стилю, не разгромно; ни один черновик не публикабелен как есть. Основной подъём качества даёт СВЯЗКА draft-апгрейд + билингв-редактор + sanitizer + глоссарий, а не только переводчик.
- **Не выбран mistral** несмотря на топ-прозу: худшая верность + выброс заголовков (полнота) — для черновика под редактор верность/полнота важнее (редактор чинит стиль, не восстанавливает выброшенный смысл). Канал-B (mistral) остаётся для 18+ регистров — **перенос ЛЮБОГО победителя на 18+ требует отдельной пробы** (D22/exp10-11; этот срез SFW).
**Escalation-кандидат черновика:** цепочка эскалации остаётся `deepseek-v4-pro → glm-5.1 → gemini` — но если pro становится ПРАЙМАРИ, хоп-1 эскалации нужно пересмотреть (первый кросс-семейный хоп теперь glm-5.1). Флаг оркестратору/бэкенду.
---
## 4. Финализация сида v2 (D30.5) — ВЫПОЛНЕНО
Единый воспроизводимый трансформ `eval/exp13_seed_v2.py` (супрсидит exp12_glossary_v2.py — дефект провенанса/статусов). Итог: **`guzhenren-seed-v2.yaml` (57 терминов)** + **таблица на подпись `diag/glossary_v2_signoff.md`**.
- **Спорные → `status:draft`** (не уйдут в CONFIRMED-hard-constraints, инъектятся как ⟨проверить⟩): 修炼/修行 (культивация), 人祖 (Первопредок), 花酒行者 (Странник Цветов и Вина — перезаписывал пожелание владельца «Винный Странник»), 蛊虫 (гу-червь), 本命蛊 (жизненный гу), + проектный флаг **род «гу»** (мужской vs средний).
- **Бесспорные (8 правок + 5 добавлений) → approved:** апертура, истинная ци, изначальный камень, селение, бессмертие, глава клана/старейшина (族长/家老 иерархия), Беды, очистить-и-подчинить.
- **Мн.ч. (D24.4):** плюрал-формы в 元石/凡人/蛊师 (закрывает inflection_gap post-check).
- **Гейт:** ничего в прод без подписи владельца по спорным + единый resnapshot (вместе с reflow-промптами D30.2 и sanitizer D30.3).
- Провенанс раскрыт: аддендум exp12 §4-addendum.
---
## 5. Лимитации и дисциплина (честно)
1. **⚠ Перерасход бюджета:** армы $0.109 + судьи $5.387 = **$5.50 против капа $5** (~+10%). Причина: хард-стоп проверялся МЕЖДУ судьями (перед kimi спенд $3.39 < $4 → запустился), а kimi добавил $2.0; gemini думал дороже оценки ($3.14). **Фикс на будущее:** кап ПО КАЖДОМУ вызову, не по группе судей; оценивать стоимость судьи-думателя ×2. Данные собраны, дальше не трачу.
2. **Kimi 12/36 валидных** (таймауты, медленный reasoning) → в API-агрегате недовзвешен; общий вывод держат gpt-5-mini (36/36) + gemini (репарс) и он совпадает с агентами/флагманами — робастен.
3. **Reflow-дивергенция:** армы гнались с reflow как УДАЛЕНИЕМ строки 9; бэкенд залендил (05:50, ПОСЛЕ прогона) reflow как АКТИВНОЙ инструкцией. Ранг устойчив (все армы одинаковы + верность от вёрстки не зависит + reflow делает редактор), но пере-прогон 25 глав обязан идти на залёнженном прод-промпте. Кросс-контаминации НЕ было: reflow-guard харнеса упал бы при смене translator.md в ходе прогона — все 24 вызова прошли.
4. **N=1 срез, LLM+агент-судьи** — предварительно до вебновелл-пилота (D25.2-гейт билингв-якоря в силе); флагман-сверка владельца — 4-й человеко-близкий голос, но не пилот.
5. Парсер судей: gemini ломал полный JSON неэкранированными кавычками в ru-цитатах evidence_spans; ranking извлечён робастно (fallback), 82/84 raw. Span-цитаты у судей ЕСТЬ (в raw), в агрегат идёт ранжирование.
---
## 6. Deliverable — итог
- **Рекомендация переводчика:** `deepseek-v4-pro` (цена ~$0.23/25 глав) — на ратификацию оркестратора.
- **Сид v2:** финализирован, таблица владельцу на подпись (`diag/glossary_v2_signoff.md`).
- **Артефакты:** `diag/arms13/` (выходы, гейты, судьи, summary.json, costs); `diag/reading/monitor13.{html,md}` + ключ (слепая флагман-сверка владельца); `eval/exp13_*.py` (воспроизводимо).
- **Следующий шаг:** оркестратор ратифицирует draft-модель + подпись владельца по сиду → единый resnapshot (draft=pro, editor=glm-5-билингв, reflow, сид v2, sanitizer) → пере-прогон 25 глав с re-gate верности → чтение владельца → этап B.

108
eval/exp13_aggregate.py Normal file
View file

@ -0,0 +1,108 @@
#!/usr/bin/env python3
"""exp13 — агрегация судейских вердиктов + гейтов + стоимости в сводку (§2). $0.
Средняя позиция (1=лучшая) по осям и суммарно; разброс (leak-устойчивость); согласие судей;
эхо-rate/объём из gates.json; суммарная стоимость из costs.jsonl + judge_costs.jsonl.
Запуск: eval/.venv/bin/python eval/exp13_aggregate.py
Выход: печать + diag/arms13/summary.json
"""
from __future__ import annotations
import json, statistics as st
from collections import defaultdict
from pathlib import Path
ARMS = Path("/home/ubuntu/books/gu-zhenren/diag/arms13")
LEGEND = {"T1": "deepseek-v4-flash", "T2": "mistral-large-2512", "T3": "glm-5",
"T4": "deepseek-v4-pro", "T5": "grok-4.3 reasoning-ON", "T6": "qwen-flash"}
def load_jsonl(p):
if not p.exists():
return []
return [json.loads(l) for l in p.read_text(encoding="utf-8").splitlines() if l.strip()]
def positions(verdicts, axis=None, judge=None):
"""arm -> list of positions (1=best)."""
pos = defaultdict(list)
for v in verdicts:
if axis and v["axis"] != axis:
continue
if judge and v["judge"] != judge:
continue
r = v["arm_ranking"]
for i, arm in enumerate(r):
pos[arm].append(i + 1)
return pos
def summarize(pos):
out = {}
for arm, ps in pos.items():
out[arm] = {"mean": round(st.mean(ps), 3), "n": len(ps),
"std": round(st.pstdev(ps), 3) if len(ps) > 1 else 0.0}
return out
def table(title, pos):
print(f"\n== {title} ==")
rows = sorted(summarize(pos).items(), key=lambda kv: kv[1]["mean"])
print(f"{'arm':<5} {'model':<24} {'mean_pos':>8} {'std':>6} {'n':>4}")
for arm, s in rows:
print(f"{arm:<5} {LEGEND.get(arm, arm):<24} {s['mean']:>8} {s['std']:>6} {s['n']:>4}")
return {arm: s for arm, s in rows}
def main():
verdicts = load_jsonl(ARMS / "judges" / "verdicts.jsonl")
print(f"валидных вердиктов: {len(verdicts)}")
judges = sorted({v["judge"] for v in verdicts})
axes = sorted({v["axis"] for v in verdicts})
summary = {"n_verdicts": len(verdicts), "judges": judges, "axes": axes}
summary["overall"] = table("ОБЩЕЕ (обе оси, все судьи)", positions(verdicts))
for axis in axes:
summary[f"axis_{axis}"] = table(f"ось: {axis}", positions(verdicts, axis=axis))
# согласие судей (по каждой оси, средняя позиция у каждого судьи)
summary["per_judge"] = {}
for judge in judges:
summary["per_judge"][judge] = {}
for axis in axes:
summary["per_judge"][judge][axis] = summarize(positions(verdicts, axis=axis, judge=judge))
# гейты
gates = {}
gp = ARMS / "gates.json"
if gp.exists():
g = json.loads(gp.read_text(encoding="utf-8"))["per_arm"]
print("\n== ГЕЙТЫ ==")
print(f"{'arm':<5} {'echo_rate':>9} {'refusal':>7} {'volBad':>6} {'empty':>5} {'han_avg':>8} {'vol_avg':>7}")
for arm in sorted(g):
a = g[arm]
print(f"{arm:<5} {a['echo_rate']:>9} {a['refusal']:>7} {a['vol_bad']:>6} {a['empty']:>5} {a['han_avg']:>8} {a['vol_avg']:>7}")
gates[arm] = {k: a[k] for k in ("echo_rate", "refusal", "vol_bad", "empty", "han_avg", "vol_avg")}
summary["gates"] = gates
# стоимость
arm_costs = load_jsonl(ARMS / "costs.jsonl")
judge_costs = load_jsonl(ARMS / "judge_costs.jsonl")
per_arm_cost = defaultdict(float)
for r in arm_costs:
per_arm_cost[r["arm"]] += r.get("cost", 0) or 0
arm_total = sum(per_arm_cost.values())
judge_total = sum((r.get("cost", 0) or 0) for r in judge_costs)
print("\n== СТОИМОСТЬ ==")
print(f"{'arm':<5} {'model':<24} {'$/6chunks':>10} {'$/chunk':>9}")
for arm in sorted(per_arm_cost):
c = per_arm_cost[arm]
print(f"{arm:<5} {LEGEND.get(arm, arm):<24} {c:>10.4f} {c/6:>9.5f}")
print(f"\nармы: ${arm_total:.4f} судьи: ${judge_total:.4f} ИТОГО: ${arm_total+judge_total:.4f} (кап $5)")
summary["cost"] = {"per_arm": {k: round(v, 5) for k, v in per_arm_cost.items()},
"arm_total": round(arm_total, 4), "judge_total": round(judge_total, 4),
"grand_total": round(arm_total + judge_total, 4)}
(ARMS / "summary.json").write_text(json.dumps(summary, ensure_ascii=False, indent=1), encoding="utf-8")
print("\nsummary → diag/arms13/summary.json")
if __name__ == "__main__":
main()

51
eval/exp13_blind_stage.py Normal file
View file

@ -0,0 +1,51 @@
#!/usr/bin/env python3
"""exp13 — слепая выкладка для АГЕНТ-ревью (ultracode, триангуляция к API-судьям).
Для каждой главы: source_zh.txt + A..E.txt (армметка перемешаны СВОЕЙ солью на главу).
Агенты ранжируют A..E, не зная моделей; un-blind по _BLIND_KEY.json (держать закрытым).
Выход: diag/arms13/blind/ch{5,7,14}/{source_zh,A..E}.txt + diag/arms13/blind/_BLIND_KEY.json
"""
from __future__ import annotations
import json, random
from pathlib import Path
DIAG = Path("/home/ubuntu/books/gu-zhenren/diag")
ARMS = DIAG / "arms13"
BLIND = ARMS / "blind"
chunks = json.loads((DIAG / "chunks.json").read_text(encoding="utf-8"))
CH = [5, 7, 14]
CHUNKS = {5: [0, 1], 7: [0, 1], 14: [0, 1]}
SALT = "exp13-agentblind-v1"
def arms_present():
return sorted([p.name for p in ARMS.iterdir() if p.is_dir() and p.name.startswith("T")])
def arm_text(arm, ch):
return "\n\n".join((ARMS / arm / f"{ch}_{ci}.txt").read_text(encoding="utf-8").strip() for ci in CHUNKS[ch])
def source_text(ch):
return "\n\n".join(chunks[f"{ch}/{ci}"]["source"] for ci in CHUNKS[ch])
def main():
arms = arms_present()
key = {}
for ch in CH:
order = arms[:]
random.Random(f"{SALT}-{ch}").shuffle(order)
labels = {chr(65 + i): arm for i, arm in enumerate(order)} # A..E
key[str(ch)] = labels
d = BLIND / f"ch{ch}"; d.mkdir(parents=True, exist_ok=True)
(d / "source_zh.txt").write_text(source_text(ch), encoding="utf-8")
for lab, arm in labels.items():
(d / f"{lab}.txt").write_text(arm_text(arm, ch), encoding="utf-8")
(BLIND / "_BLIND_KEY.json").write_text(json.dumps({"salt": SALT, "map": key}, ensure_ascii=False, indent=1), encoding="utf-8")
print(f"blind stage → {BLIND} ({len(arms)} меток A..{chr(64+len(arms))} × {len(CH)} глав)")
for ch in CH:
print(f" гл.{ch}: {key[str(ch)]}")
if __name__ == "__main__":
main()

62
eval/exp13_gates.py Normal file
View file

@ -0,0 +1,62 @@
#!/usr/bin/env python3
"""exp13 — гейты на выходах армов (пре-рег §1.3): эхо-rate / refusal / объём-коридор / полнота.
Читает diag/arms13/<ARM>/<ch>_<ci>.txt + diag/chunks.json (исходники zh). $0 (детерминированно).
Выход: diag/arms13/gates.json + печать таблицы.
"""
from __future__ import annotations
import json, re
from pathlib import Path
DIAG = Path("/home/ubuntu/books/gu-zhenren/diag")
OUT = DIAG / "arms13"
TARGETS = [(5, 0), (5, 1), (7, 0), (7, 1), (14, 0), (14, 1)]
LEN_BOUNDS = (2.2, 4.2) # zh→ru приёмки
REFUSAL = re.compile(r"(?i)\b(я не могу|не буду переводить|as an ai|i cannot|i'm sorry|извините, но я|не могу выполнить|против правил)\b")
HAN = re.compile(r"[一-鿿㐀-䶿]")
def han_ratio(t: str) -> float:
ns = [c for c in t if not c.isspace()]
if not ns:
return 1.0
return sum(1 for c in ns if HAN.match(c)) / len(ns)
def main() -> None:
chunks = json.loads((DIAG / "chunks.json").read_text(encoding="utf-8"))
arms = sorted([p.name for p in OUT.iterdir() if p.is_dir()])
rows, per_arm = [], {}
for arm in arms:
agg = {"arm": arm, "n": 0, "echo": 0, "refusal": 0, "vol_bad": 0, "empty": 0,
"han_sum": 0.0, "vol_sum": 0.0}
for ch, ci in TARGETS:
key = f"{ch}/{ci}"
p = OUT / arm / f"{ch}_{ci}.txt"
src = chunks[key]["source"]
if not p.exists() or not p.read_text(encoding="utf-8").strip():
agg["empty"] += 1; agg["n"] += 1; continue
t = p.read_text(encoding="utf-8").strip()
hr = han_ratio(t)
vr = len(t) / max(1, len(src))
echo = hr > 0.10
ref = bool(REFUSAL.search(t))
volbad = not (LEN_BOUNDS[0] <= vr <= LEN_BOUNDS[1])
agg["n"] += 1; agg["han_sum"] += hr; agg["vol_sum"] += vr
agg["echo"] += echo; agg["refusal"] += ref; agg["vol_bad"] += volbad
rows.append({"arm": arm, "chunk": key, "han": round(hr, 4), "vol": round(vr, 2),
"echo": echo, "refusal": ref, "vol_bad": volbad, "chars": len(t)})
n = max(1, agg["n"] - agg["empty"])
agg["han_avg"] = round(agg["han_sum"] / n, 4)
agg["vol_avg"] = round(agg["vol_sum"] / n, 2)
agg["echo_rate"] = round(agg["echo"] / max(1, agg["n"]), 3)
per_arm[arm] = agg
(OUT / "gates.json").write_text(json.dumps({"per_arm": per_arm, "rows": rows}, ensure_ascii=False, indent=1), encoding="utf-8")
print(f"{'arm':<5} {'n':>2} {'echo':>5} {'refus':>5} {'volBad':>6} {'empty':>5} {'han_avg':>8} {'vol_avg':>7}")
for arm in arms:
a = per_arm[arm]
print(f"{arm:<5} {a['n']:>2} {a['echo']:>5} {a['refusal']:>5} {a['vol_bad']:>6} {a['empty']:>5} {a['han_avg']:>8} {a['vol_avg']:>7} echo_rate={a['echo_rate']}")
if __name__ == "__main__":
main()

258
eval/exp13_judges.py Normal file
View file

@ -0,0 +1,258 @@
#!/usr/bin/env python3
"""exp13 — судейская панель (пре-рег §1.4). Span-цитирующая, reasoning-ON, оси РАЗДЕЛЬНО,
кросс-семейная (ни один арм не из семей судей), 3 повтора со свапом, parse-чек полноты.
Панель: J1 gemini-3.1-pro-preview (mandatory thinking), J2 gpt-5-mini (reasoning_effort=medium),
J3 kimi-k2.6 (reasoning-ON, temp=1). Каждый вызов = ранжирование ВСЕХ армов по ОДНОЙ оси на
одном чанке; JSON {ranking, evidence_spans}. Слепые метки V1..VN, свап на (чанк×ось×повтор).
Параллель внутри судьи; хард-чек спенда между судьями (кап $3.5 судьям). Персист usage/cost.
Запуск: eval/.venv/bin/python eval/exp13_judges.py [--repeats 3]
Выход: diag/arms13/judges/raw/*.txt, judges/verdicts.jsonl, judges/_JUDGE_KEY.json, judge_costs.jsonl
"""
from __future__ import annotations
import json, os, re, sys, time, random, urllib.request, urllib.error
from concurrent.futures import ThreadPoolExecutor
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent))
from refusal_bench import load_env_file
load_env_file()
DIAG = Path("/home/ubuntu/books/gu-zhenren/diag")
ARMS = DIAG / "arms13"
JOUT = ARMS / "judges"
TARGETS = [(5, 0), (5, 1), (7, 0), (7, 1), (14, 0), (14, 1)]
REPEATS = 3
JUDGE_CAP = 3.5
PRICES = {"gemini-3.1-pro-preview": (2.0, 12.0), "gpt-5-mini": (0.25, 2.0), "kimi-k2.6": (0.95, 4.0)}
AXES = {
"fidelity": "ВЕРНОСТЬ исходнику: точность смысла; отсутствие искажений, инверсий смысла, пропусков, отсебятины и добавлений. Стиль игнорируй.",
"style": "СТИЛЬ русской художественной прозы: живость и читаемость, отсутствие канцелярита и калек с китайского, естественный синтаксис и ритм (школа Норы Галь). Верность НЕ оценивай.",
}
# judge_type: gemini | openai | kimi — разная wire-форма (провайдер-квирки models.yaml)
JUDGES = {
"J1_gemini": dict(model="gemini-3.1-pro-preview", base="https://generativelanguage.googleapis.com/v1beta/openai",
keyenv="GEMINI_API_KEY", jtype="gemini"),
"J2_gpt5mini": dict(model="gpt-5-mini", base="https://api.openai.com/v1",
keyenv="OPENAI_API_KEY", jtype="openai"),
"J3_kimi": dict(model="kimi-k2.6", base="https://api.moonshot.ai/v1",
keyenv="KIMI_API_KEY", jtype="kimi"),
}
def arms_present() -> list[str]:
return sorted([p.name for p in ARMS.iterdir() if p.is_dir() and p.name.startswith("T")])
def arm_text(arm: str, ch: int, ci: int) -> str:
p = ARMS / arm / f"{ch}_{ci}.txt"
return p.read_text(encoding="utf-8").strip() if p.exists() else ""
def build_labels(arms: list[str], ch: int, ci: int, axis: str, rep: int) -> dict:
order = arms[:]
random.Random(f"exp13-judge-{ch}-{ci}-{axis}-{rep}").shuffle(order)
return {f"V{i+1}": arm for i, arm in enumerate(order)}
def build_messages(source: str, labels: dict, ch: int, ci: int, axis: str) -> list[dict]:
n = len(labels)
sys_p = ("Ты — строгий литературный судья художественного перевода китайской вебновеллы "
"(сянься/культиваторка) на русский язык. Тебе дан ИСХОДНИК (zh) и {n} переводов под "
"слепыми метками. Оцени и РАНЖИРУЙ переводы ТОЛЬКО по одной оси:\n\n{axis}\n\n"
"Верни СТРОГО валидный JSON без текста вокруг:\n"
'{{"ranking": ["метки от ЛУЧШЕЙ к ХУДШЕЙ, все {n}"], '
'"evidence_spans": [{{"label": "Vx", "quote": "цитата ≤15 слов из перевода или исходника", '
'"note": "чем эта деталь хороша/плоха по оси"}}]}}\n'
"Каждой метке — минимум один span с конкретной цитатой (не пересказ). "
"ranking должен содержать ровно метки V1..V{n}, без пропусков и повторов.").format(n=n, axis=AXES[axis])
body = [f"ИСХОДНИК (zh):\n\n{source}\n"]
for lab in [f"V{i+1}" for i in range(n)]:
body.append(f"=== {lab} ===\n{arm_text(labels[lab], ch, ci)}\n")
body.append(f"Ранжируй {n} переводов по оси «{axis}». Только JSON.")
return [{"role": "system", "content": sys_p}, {"role": "user", "content": "\n".join(body)}]
def call(spec: dict, messages: list[dict], timeout: int = 300):
key = os.environ.get(spec["keyenv"], "")
if not key:
return None, f"no key {spec['keyenv']}", {}
body = {"model": spec["model"], "messages": messages}
jt = spec["jtype"]
if jt == "gemini":
body.update(max_tokens=8000, temperature=0.3) # mandatory thinking (не шлём reasoning)
elif jt == "openai":
body.update(max_completion_tokens=8000, reasoning_effort="medium") # gpt-5: no temp, max_completion_tokens
elif jt == "kimi":
body.update(max_tokens=16000, temperature=1) # kimi: temp force 1, floor 16000
req = urllib.request.Request(spec["base"].rstrip("/") + "/chat/completions",
data=json.dumps(body).encode(),
headers={"Content-Type": "application/json", "Authorization": f"Bearer {key}"})
try:
with urllib.request.urlopen(req, timeout=timeout) as r:
data = json.loads(r.read())
except urllib.error.HTTPError as e:
return None, f"HTTP {e.code}: {e.read()[:200].decode('utf-8','replace')}", {}
except Exception as e:
return None, f"ERR {str(e)[:160]}", {}
ch = data["choices"][0]
text = (ch.get("message", {}) or {}).get("content") or ""
usage = data.get("usage", {}) or {}
usage["finish_reason"] = ch.get("finish_reason", "")
return text.strip(), (None if text.strip() else f"empty|finish={ch.get('finish_reason')}"), usage
def cost_of(model: str, usage: dict) -> float:
inp = usage.get("prompt_tokens", 0) or 0
comp = usage.get("completion_tokens", 0) or 0
total = usage.get("total_tokens", 0) or 0
pin, pout = PRICES.get(model, (0, 0))
if model == "gemini-3.1-pro-preview": # additive_total: thinking только в total
reason = max(0, total - inp - comp)
return (inp * pin + (comp + reason) * pout) / 1_000_000
return (inp * pin + comp * pout) / 1_000_000 # subset: comp уже включает reasoning
def parse_ranking(text: str, labels: list[str]):
"""Достаём ranking робастно; parse-чек полноты set(ranking)==set(labels).
Судьи (gemini) ломают полный JSON неэкранированными кавычками в ru-цитатах evidence_spans
ranking-массив при этом чистый, извлекаем его напрямую (fallback). Spans best-effort."""
t = text.strip()
if "```" in t:
m = re.search(r"```(?:json)?\s*(.*?)```", t, re.S)
if m:
t = m.group(1).strip()
obj = None
mo = re.search(r"\{.*\}", t, re.S)
if mo:
for cand in (mo.group(0), mo.group(0).replace("\n", " ")):
try:
obj = json.loads(cand)
break
except Exception:
obj = None
rank = obj.get("ranking") if isinstance(obj, dict) else None
if not isinstance(rank, list):
rm = re.search(r'"ranking"\s*:\s*(\[[^\]]*\])', t)
if rm:
try:
rank = json.loads(rm.group(1))
except Exception:
rank = None
if not isinstance(rank, list) or set(rank) != set(labels):
return None, f"incomplete:{rank}", obj
return rank, None, obj
def run_judge(jname: str, spec: dict, arms: list[str], chunks: dict, keymap: dict, spent_box: list):
tasks = [(ch, ci, axis, rep) for ch, ci in TARGETS for axis in AXES for rep in range(REPEATS)]
raw_dir = JOUT / "raw"; raw_dir.mkdir(parents=True, exist_ok=True)
verdicts, jcosts = [], []
def one(t):
ch, ci, axis, rep = t
labels = keymap[f"{ch}/{ci}"][axis][str(rep)]
src = chunks[f"{ch}/{ci}"]["source"]
msgs = build_messages(src, labels, ch, ci, axis)
text, err, usage = call(spec, msgs)
c = cost_of(spec["model"], usage)
tag = f"{jname}_{ch}_{ci}_{axis}_r{rep}"
rec = {"judge": jname, "model": spec["model"], "chunk": f"{ch}/{ci}", "axis": axis,
"rep": rep, "cost": round(c, 6),
"usage": {k: usage.get(k) for k in ("prompt_tokens", "completion_tokens", "total_tokens", "finish_reason")}}
if not text:
rec["error"] = err
return rec, None, tag, ""
(raw_dir / f"{tag}.txt").write_text(text, encoding="utf-8")
lab_list = [f"V{i+1}" for i in range(len(labels))]
rank, perr, obj = parse_ranking(text, lab_list)
if rank is None:
rec["parse_error"] = perr
return rec, None, tag, text
# маппим метки → армы
arm_rank = [labels[l] for l in rank]
v = {"judge": jname, "chunk": f"{ch}/{ci}", "axis": axis, "rep": rep,
"arm_ranking": arm_rank, "evidence_spans": obj.get("evidence_spans", [])}
return rec, v, tag, text
with ThreadPoolExecutor(max_workers=5) as ex:
for rec, v, tag, _ in ex.map(one, tasks):
jcosts.append(rec)
if v:
verdicts.append(v)
spent_box[0] += rec["cost"]
return verdicts, jcosts
def reparse():
"""Пере-парсит raw-файлы робастным парсером в verdicts.jsonl (без API-вызовов)."""
keymap = json.loads((JOUT / "_JUDGE_KEY.json").read_text(encoding="utf-8"))
pat = re.compile(r"^(J\d+_[a-z0-9]+)_(\d+)_(\d+)_(fidelity|style)_r(\d+)$")
verdicts, ok, bad = [], 0, 0
for f in sorted((JOUT / "raw").glob("*.txt")):
m = pat.match(f.stem)
if not m:
continue
jname, ch, ci, axis, rep = m.groups()
labels = keymap[f"{ch}/{ci}"][axis][rep]
lab_list = [f"V{i+1}" for i in range(len(labels))]
rank, err, obj = parse_ranking(f.read_text(encoding="utf-8"), lab_list)
if rank is None:
bad += 1
continue
ok += 1
verdicts.append({"judge": jname, "chunk": f"{ch}/{ci}", "axis": axis, "rep": int(rep),
"arm_ranking": [labels[l] for l in rank],
"evidence_spans": obj.get("evidence_spans", []) if isinstance(obj, dict) else []})
with (JOUT / "verdicts.jsonl").open("w", encoding="utf-8") as f:
for v in verdicts:
f.write(json.dumps(v, ensure_ascii=False) + "\n")
print(f"reparse: {ok} валидных, {bad} невалидных → verdicts.jsonl")
def main():
if "--reparse" in sys.argv:
reparse()
return
if "--repeats" in sys.argv:
globals()["REPEATS"] = int(sys.argv[sys.argv.index("--repeats") + 1])
chunks = json.loads((DIAG / "chunks.json").read_text(encoding="utf-8"))
arms = arms_present()
print(f"армы в панели: {arms} (repeats={REPEATS})")
JOUT.mkdir(parents=True, exist_ok=True)
# ключ (label→arm) фиксируем ЗАРАНЕЕ для всех судей — свап на (чанк×ось×повтор)
keymap = {}
for ch, ci in TARGETS:
keymap[f"{ch}/{ci}"] = {}
for axis in AXES:
keymap[f"{ch}/{ci}"][axis] = {}
for rep in range(REPEATS):
keymap[f"{ch}/{ci}"][axis][str(rep)] = build_labels(arms, ch, ci, axis, rep)
(JOUT / "_JUDGE_KEY.json").write_text(json.dumps(keymap, ensure_ascii=False, indent=1), encoding="utf-8")
all_v, all_c, spent_box = [], [], [0.0]
cfp = ARMS / "judge_costs.jsonl"
for jname, spec in JUDGES.items():
if spent_box[0] > JUDGE_CAP:
print(f"!! судейский кап ${JUDGE_CAP} достигнут (${spent_box[0]:.2f}) — {jname} пропущен"); continue
t0 = time.time()
v, c = run_judge(jname, spec, arms, chunks, keymap, spent_box)
with cfp.open("a", encoding="utf-8") as f:
for rec in c:
f.write(json.dumps(rec, ensure_ascii=False) + "\n")
ok = sum(1 for x in c if "error" not in x and "parse_error" not in x)
print(f"{jname}: {ok}/{len(c)} валидных, {time.time()-t0:.0f}s, спенд-накопл ${spent_box[0]:.3f}")
all_v += v; all_c += c
with (JOUT / "verdicts.jsonl").open("w", encoding="utf-8") as f:
for v in all_v:
f.write(json.dumps(v, ensure_ascii=False) + "\n")
print(f"\nвсего валидных вердиктов: {len(all_v)} судейский спенд: ${spent_box[0]:.4f}")
if __name__ == "__main__":
main()

186
eval/exp13_monitor.py Normal file
View file

@ -0,0 +1,186 @@
#!/usr/bin/env python3
"""exp13 — слепой «монитор ПЕРЕВОДЧИКОВ»: оригинал zh + все армы-переводчики, СЛЕПО.
Для флагман-сверки владельца (пре-рег §1.4): оригинал+вариант на копирование во Fable 5/5.6,
+ место под любительский перевод. Имена моделей скрыты; армы под нейтральными метками,
перемешаны СВОЕЙ перестановкой в каждой главе (свап). Ключ отдельный файл
`_МОНИТОР13_КЛЮЧ.json`, не открывать до вынесения оценки. Локальный HTML (текст книги ВНЕ git/хостинга).
Выход: diag/reading/monitor13.html + diag/reading/_МОНИТОР13_КЛЮЧ.json
"""
from __future__ import annotations
import html, json, random
from pathlib import Path
DIAG = Path("/home/ubuntu/books/gu-zhenren/diag")
ARMS = DIAG / "arms13"
chunks = json.loads((DIAG / "chunks.json").read_text(encoding="utf-8"))
CH = [(5, "смешанная (нарратив+диалог)"), (7, "диалого-плотная"), (14, "экспозиция/описание")]
CHUNKS = {5: [0, 1], 7: [0, 1], 14: [0, 1]}
SALT = "exp13-monitor-blind-v1"
LEGEND = {
"T1": "deepseek-v4-flash (текущий базовый черновик)",
"T2": "mistral-large-2512",
"T3": "glm-5",
"T4": "deepseek-v4-pro",
"T5": "grok-4.3 reasoning-ON",
"T6": "qwen-flash",
}
MD_PROMPT = """# Слепая оценка ЧЕРНОВИКОВ перевода (蛊真人, zh→ru) — задача для модели
Ты эксперт по художественному переводу китайских вебновелл-культиваторок (сянься/уся) на русский. Ниже 3 главы. В КАЖДОЙ: китайский ОРИГИНАЛ и 5 анонимных вариантов её русского ЧЕРНОВОГО перевода (метки V1V5). Это черновики РАЗНЫХ моделей-переводчиков (редактура будет позже) оценивай именно черновой перевод. Метки перемешаны В КАЖДОЙ ГЛАВЕ ПО-РАЗНОМУ: «V1» в главе 5 и в главе 7 это РАЗНЫЕ модели, не связывай метки между главами. Не пытайся угадать, какая модель за какой меткой суди только текст.
Для КАЖДОЙ главы сравни варианты по трём осям, СТРОГО сверяясь с оригиналом:
1. **ВЕРНОСТЬ смыслу (главное):** точность; отсутствие искажений, ИНВЕРСИЙ смысла, пропусков, отсебятины, перепутанных субъектов. Особо отмечай «smooth-wrong» гладко по-русски, но смысл искажён.
2. **КАЧЕСТВО русской прозы:** живость, читаемость, отсутствие канцелярита и калек, естественный синтаксис/ритм.
3. **ЖАНР/ТЕРМИНОЛОГИЯ:** корректность реалий культивации (культивация, секты, ранги, гу-термины), уместность имён/титулов.
**Вывод по каждой главе:** (a) ранг V1V5 от лучшего к худшему (приоритет верность); (b) по каждому варианту 24 примера (цитата + что не так/хорошо относительно оригинала); (c) список smooth-wrong искажений (вариант цитата верный смысл).
**Сводно в конце:** какая метка в какой главе лучший черновик; есть ли модель, стабильно лучшая во всех 3 главах; дотягивает ли хоть один черновик до чтения без спотыкания и какой ближе к публикабельному.
---
"""
def arms_present():
return sorted([p.name for p in ARMS.iterdir() if p.is_dir() and p.name.startswith("T")])
def build_markdown(arms, key_map):
out = [MD_PROMPT]
for ch, reg in CH:
out.append(f"\n# ГЛАВА {ch}{reg}\n")
out.append("## Оригинал (zh)\n")
out.append(source_text(ch))
for i in range(len(arms)):
lab = f"V{i+1}"
out.append(f"\n## Вариант {lab}\n")
out.append(arm_text(key_map[str(ch)][lab], ch))
return "\n".join(out)
def arm_text(arm, ch):
parts = []
for ci in CHUNKS[ch]:
p = ARMS / arm / f"{ch}_{ci}.txt"
parts.append(p.read_text(encoding="utf-8").strip() if p.exists() else "(нет)")
return "\n\n".join(parts)
def source_text(ch):
return "\n\n".join(chunks[f"{ch}/{ci}"]["source"] for ci in CHUNKS[ch])
CSS = """
:root{--bg:#faf9f7;--fg:#1c1a17;--card:#fff;--bd:#e3ddd3;--mut:#8a8177;--acc:#3e5c7a}
@media(prefers-color-scheme:dark){:root{--bg:#0f1317;--fg:#e8e3d8;--card:#171e25;--bd:#2a333b;--mut:#8494a0;--acc:#78a6c9}}
*{box-sizing:border-box}body{margin:0;background:var(--bg);color:var(--fg);font:16px/1.6 Georgia,"Noto Serif",serif}
.wrap{max-width:1180px;margin:0 auto;padding:24px}
h1{font-size:24px;margin:0 0 6px}.sub{color:var(--mut);font-size:14px;margin:0 0 20px;line-height:1.5}
.chap{margin:34px 0 8px;font-size:20px;border-bottom:2px solid var(--acc);padding-bottom:6px}
.src{background:var(--card);border:1px solid var(--bd);border-radius:10px;padding:14px 16px;margin:12px 0 20px}
.src .lbl{color:var(--acc);font-weight:700;font-size:13px;letter-spacing:.03em;text-transform:uppercase}
.grid{display:grid;grid-template-columns:1fr;gap:14px}
.card{background:var(--card);border:1px solid var(--bd);border-radius:10px;overflow:hidden}
.hd{display:flex;align-items:center;gap:10px;padding:11px 14px;border-bottom:1px solid var(--bd)}
.hd .t{font-weight:700;font-size:15px}
.body{padding:6px 16px 14px;white-space:pre-wrap;font-size:15.5px}
.body.zh{font-family:"Noto Sans CJK SC","Microsoft YaHei",sans-serif;font-size:15px}
button{font:inherit;font-size:12.5px;background:var(--acc);color:#fff;border:0;border-radius:6px;padding:5px 11px;cursor:pointer}
button.ghost{background:transparent;color:var(--acc);border:1px solid var(--acc)}
.slot{border:2px dashed var(--bd);border-radius:10px;padding:16px;color:var(--mut);font-size:14px;margin-top:14px}
.toolbar{display:flex;gap:8px;flex-wrap:wrap;margin:8px 0 0}
.note{background:var(--card);border:1px solid var(--bd);border-left:3px solid var(--acc);border-radius:8px;padding:10px 14px;font-size:13.5px;color:var(--mut);margin:16px 0}
"""
FLAGSHIP_PROMPT = (
"Ты — эксперт по художественному переводу китайских вебновелл-культиваторок (сянься/уся) на русский. "
"Ниже — КИТАЙСКИЙ ИСХОДНИК одной главы и несколько АНОНИМНЫХ вариантов её русского ЧЕРНОВОГО перевода "
"(метки V1, V2, …). Это черновики РАЗНЫХ моделей; редактура будет позже — оценивай именно черновой перевод. "
"Не пытайся угадать модель — суди только текст.\\n\\n"
"Сравни варианты по трём осям, СТРОГО сверяясь с исходником:\\n"
"1) ВЕРНОСТЬ смыслу (главное): точность; отсутствие искажений, ИНВЕРСИЙ смысла, пропусков, отсебятины, "
"перепутанных субъектов. Особо отмечай «smooth-wrong» — гладко по-русски, но смысл искажён.\\n"
"2) КАЧЕСТВО русской прозы: живость, читаемость, отсутствие канцелярита и калек, естественный синтаксис/ритм.\\n"
"3) ЖАНР/ТЕРМИНОЛОГИЯ: корректность реалий культивации, уместность имён/титулов.\\n\\n"
"Ответ: (a) РАНГ вариантов от лучшего к худшему (приоритет — верность); (b) по каждому 24 конкретных "
"примера (цитата + что не так/хорошо относительно zh); (c) отдельный список smooth-wrong (вариант → цитата → "
"верный смысл); (d) вердикт: дотягивает ли ХОТЬ ОДИН до чтения без спотыкания и какой ближе к публикабельному черновику."
)
JS = """
var PROMPT=%r;
function esc(id){var e=document.getElementById(id);return e?e.textContent:''}
function copyOne(vid){navigator.clipboard.writeText(esc(vid));flash(event.target)}
function copyPair(sid,vid){navigator.clipboard.writeText('=== ОРИГИНАЛ (zh) ===\\n'+esc(sid)+'\\n\\n=== ПЕРЕВОД ===\\n'+esc(vid));flash(event.target)}
function copyPrompt(){navigator.clipboard.writeText(PROMPT);flash(event.target)}
function copyChapter(sid,vids){
var parts=[PROMPT,'\\n\\n===== ИСХОДНИК (zh) =====\\n'+esc(sid)];
for(var i=0;i<vids.length;i++){parts.push('\\n\\n===== V'+(i+1)+' =====\\n'+esc(vids[i]))}
navigator.clipboard.writeText(parts.join(''));flash(event.target)
}
function flash(b){var o=b.textContent;b.textContent='✓ скопировано';setTimeout(function(){b.textContent=o},1400)}
""" % FLAGSHIP_PROMPT
def main():
arms = arms_present()
key_map = {}
out = ['<div class="wrap">']
out.append('<h1>蛊真人 — слепой монитор ПЕРЕВОДЧИКОВ (exp13)</h1>')
out.append('<p class="sub">Оригинал + варианты ЧЕРНОВОГО перевода (разные модели-переводчики) по 3 главам, '
'СЛЕПО: имена моделей скрыты, метки перемешаны по-своему в каждой главе («V3» в гл.5 и гл.7 — РАЗНЫЕ модели). '
'Это ЧЕРНОВИКИ (без редактуры) — сравнивай ВЕРНОСТЬ смыслу и качество русской прозы. '
'Кнопка «копировать оригинал+перевод» кладёт пару в буфер для флагмана (Fable 5/5.6) слепо. '
'Ключ — <code>_МОНИТОР13_КЛЮЧ.json</code> рядом, НЕ открывай до вынесения оценки.</p>')
out.append('<div class="note">🎯 <b>Как гонять флагмана (GPT/Claude), слепо:</b> у каждой главы жми '
'«📋 копировать ГЛАВУ целиком» — в буфер лягут ГОТОВЫЙ ПРОМТ + оригинал + все версии V1…V5. '
'Вставь одним куском в GPT/Claude и отправь. Флагман не увидит имён моделей. '
'<button onclick="copyPrompt()">копировать только промт</button></div>')
for ch, reg in CH:
order = arms[:]
random.Random(f"{SALT}-{ch}").shuffle(order)
labels = {f"V{i+1}": arm for i, arm in enumerate(order)}
key_map[str(ch)] = labels
out.append(f'<h2 class="chap">Глава {ch}{html.escape(reg)}</h2>')
sid = f"src{ch}"
vids_js = "[" + ",".join(f"'v{ch}_{i+1}'" for i in range(len(arms))) + "]"
out.append(f'<div class="src"><div class="lbl">Оригинал (zh)</div>'
f'<div class="toolbar">'
f'<button onclick="copyChapter(\'{sid}\',{vids_js})">📋 копировать ГЛАВУ целиком (промт + оригинал + все версии) — для флагмана</button>'
f'<button class="ghost" onclick="copyOne(\'{sid}\')">только оригинал</button></div>'
f'<div class="body zh" id="{sid}">{html.escape(source_text(ch))}</div></div>')
out.append('<div class="grid">')
for i, (lab, arm) in enumerate(labels.items(), 1):
vid = f"v{ch}_{i}"
out.append('<div class="card">')
out.append(f'<div class="hd"><span class="t">{lab}</span></div>')
out.append(f'<div class="toolbar" style="padding:8px 16px 0">'
f'<button onclick="copyPair(\'{sid}\',\'{vid}\')">копировать оригинал+перевод</button>'
f'<button class="ghost" onclick="copyOne(\'{vid}\')">только перевод</button></div>')
out.append(f'<div class="body" id="{vid}">{html.escape(arm_text(arm, ch))}</div>')
out.append('</div>')
out.append('</div>')
out.append('<div class="slot"> Любительский перевод этой главы: добавляй сам при загрузке во флагман.</div>')
out.append('<div class="note">Это ЧЕРНОВИКИ переводчиков (стадия draft), НЕ финал: downstream их чинит '
'билингвальный редактор (D30.1). Задача монитора — выбрать САМЫЙ ВЕРНЫЙ и живой ЧЕРНОВИК. '
'Имена моделей скрыты, чтобы ярлык не искажал восприятие. Расшифровка — в закрытом ключе.</div>')
out.append('</div>')
out.append(f'<style>{CSS}</style><script>{JS}</script>')
(DIAG / "reading").mkdir(parents=True, exist_ok=True)
(DIAG / "reading" / "monitor13.html").write_text("\n".join(out), encoding="utf-8")
(DIAG / "reading" / "monitor13.md").write_text(build_markdown(arms, key_map), encoding="utf-8")
(DIAG / "reading" / "_МОНИТОР13_КЛЮЧ.json").write_text(
json.dumps({"salt": SALT, "map": key_map, "arms": {a: LEGEND.get(a, a) for a in arms}}, ensure_ascii=False, indent=1),
encoding="utf-8")
print(f"монитор ({len(arms)} армов) → diag/reading/monitor13.html + monitor13.md (самодостаточный)")
for ch in [5, 7, 14]:
print(f" гл.{ch}: {[key_map[str(ch)][f'V{i+1}'] for i in range(len(arms))]}")
if __name__ == "__main__":
main()

160
eval/exp13_seed_v2.py Normal file
View file

@ -0,0 +1,160 @@
#!/usr/bin/env python3
"""exp13 — ФИНАЛИЗАЦИЯ сида v2 (D30.5). Единый воспроизводимый трансформ v1 → v2,
СУПЕРСИДИТ exp12_glossary_v2.py (дефект провенанса/статусов из ревью D30.5):
спорные термины status:DRAFT (не approved!): 修炼/修行 (культивация), 人祖 (Первопредок),
род «гу»-зависимые (本命蛊 жизненный гу, 蛊虫 гу-червь), 花酒行者 (перезаписывал пожелание владельца);
бесспорные правки/добавления (D30.5: 11 правок + 8 добавлений минус спорные) approved;
откат тихих промоушенов draftapproved (花酒行者, 本命蛊 остаются draft);
обогащение мн.ч. (D24.4/полигон-4): плюрал-формы в decl.forms счётных 元石/凡人/蛊师;
gu-gender: НЕ флипаю молча (рекоменд. флагманов «мужской») проектное решение владельца, в sign-off.
Вход: guzhenren-seed.yaml. Выход: guzhenren-seed-v2.yaml + diag/glossary_v2_signoff.md. ВНЕ git. НЕ в прод без подписи+resnapshot.
"""
from __future__ import annotations
import yaml
from pathlib import Path
BOOK = Path("/home/ubuntu/books/gu-zhenren")
SRC = BOOK / "guzhenren-seed.yaml"
OUT = BOOK / "guzhenren-seed-v2.yaml"
SIGNOFF = BOOK / "diag" / "glossary_v2_signoff.md"
DRAFT = "draft" # спорное — до подписи владельца
OK = "approved"
# src -> (new_dst, forms|None, status, contested_reason|None, rationale)
CHANGES = {
"空窍": ("апертура", ["апертуры", "апертуре", "апертуру", "апертурой", "апертуре"], OK, None,
"оба флагмана — топ-проблема: «врата» ломают грамматику («одни врата»); «апертура» = фан-вики жанра"),
"开窍": ("открытие апертуры", ["открытия апертуры", "открытию апертуры", "открытием апертуры", "открытии апертуры"], OK, None,
"согласовано с 空窍→апертура; 开窍大典 = обряд открытия апертуры"),
"真元": ("истинная ци", ["истинной ци", "истинной ци", "истинную ци", "истинной ци", "истинной ци"], OK, None,
"оба: НЕ «истинная энергия» (физикализм); ци — жанровая норма; отделить от 元气"),
"元海": ("море истинной ци", ["моря истинной ци", "морю истинной ци", "морем истинной ци", "море истинной ци"], OK, None,
"согласовано с 真元→истинная ци"),
"元石": ("изначальный камень", ["изначального камня", "изначальному камню", "изначальный камень", "изначальным камнем", "изначальном камне"], OK, None,
"«первокамень» → «двадцать первокамень» читается как «двадцать первый»; согласован с рядом 元"),
"古月山寨": ("селение Гуюэ", ["селения Гуюэ", "селению Гуюэ", "селением Гуюэ", "селении Гуюэ"], OK, None,
"«стан» = воинский лагерь; 山寨 здесь — укреплённое селение/деревня"),
"白家寨": ("селение Бай", ["селения Бай", "селению Бай", "селением Бай", "селении Бай"], OK, None, "согласовано"),
"熊家寨": ("селение Сюн", ["селения Сюн", "селению Сюн", "селением Сюн", "селении Сюн"], OK, None, "согласовано"),
# --- СПОРНЫЕ → DRAFT ---
"本命蛊": ("жизненный гу", ["жизненного гу", "жизненному гу", "жизненного гу", "жизненным гу", "жизненном гу"], DRAFT,
"род «гу» + выбор рендеринга", "оба: НЕ «гу Жизни» (читается как ВИД). СТАТУС, не вид. Согласование зависит от рода «гу»"),
"花酒行者": ("Странник Цветов и Вина", ["Странника Цветов и Вина", "Страннику Цветов и Вина", "Странника Цветов и Вина", "Странником Цветов и Вина", "Страннике Цветов и Вина"], DRAFT,
"перезаписывает пожелание владельца «Винный Странник»", "флагманы: сохранить 花 (=блуд; 采花大盗). Владелец предпочёл «Винный Странник» — за ним"),
"蛊虫": ("гу-червь", ["гу-червя", "гу-червю", "гу-червя", "гу-червём", "гу-черве"], DRAFT,
"род «гу»", "«гу-тварь» пейоративно; «гу-червь»/«гу». Согласование зависит от рода «гу»"),
}
# новые термины: dict(src,dst,type,forms|None,invariant,status,contested_reason|None,note)
ADDITIONS = [
dict(src="长生", dst="бессмертие", type="term", forms=["бессмертия", "бессмертию", "бессмертие", "бессмертием", "бессмертии"],
status=OK, contested=None, note="chángshēng; вечная жизнь/бессмертие — сквозная тема; НЕ «долголетие»"),
dict(src="族长", dst="глава клана", type="title", forms=["главы клана", "главе клана", "главу клана", "главой клана", "главе клана"],
status=OK, contested=None, note="zúzhǎng; глава клана — НАД старейшинами; черновик путал со «старейшиной». ⚠ консистентность род/клан — см. sign-off"),
dict(src="家老", dst="старейшина", type="title", forms=["старейшины", "старейшине", "старейшину", "старейшиной", "старейшине"],
status=OK, contested=None, note="jiālǎo; старейшина клана; развести с 族长"),
dict(src="困境", dst="Беды", type="term", forms=["Бед", "Бедам", "Беды", "Бедами", "Бедах"],
status=OK, contested=None, note="kùnjìng; стая зверей в легенде о Первопредке; имя стаи"),
dict(src="炼化", dst="очистить и подчинить", type="term", forms=[], invariant=True,
status=OK, contested=None, note="liànhuà; очистить-и-подчинить живого гу; НЕ «переплавить». Кратко «очистить»"),
# --- СПОРНЫЕ → DRAFT ---
dict(src="修炼", dst="культивация", type="term", forms=["культивации", "культивации", "культивацию", "культивацией", "культивации"],
status=DRAFT, contested="лит.альтернатива «совершенствование» (флагманы для издания)", note="xiūliàn; культивация/культивировать"),
dict(src="修行", dst="культивация", type="term", forms=["культивации", "культивации", "культивацию", "культивацией", "культивации"],
status=DRAFT, contested="как 修炼", note="xiūxíng; практика культивации; согласовано с 修炼"),
dict(src="人祖", dst="Первопредок", type="name", forms=["Первопредка", "Первопредку", "Первопредка", "Первопредком", "Первопредке"],
status=DRAFT, contested="альт-имя «Жэнь Цзу» (Палладица)", note="Rénzǔ; говорящее имя (Праотец человечества). НЕ «Рен Зу» (англ.релей-ошибка)"),
]
# обогащение мн.ч. (D24.4): src -> плюрал-формы (append к decl.forms), закрывает inflection_gap
PLURALS = {
"元石": ["изначальные камни", "изначальных камней", "изначальным камням", "изначальными камнями", "изначальных камнях"],
"凡人": ["смертные", "смертных", "смертным", "смертными"],
"蛊师": ["гу-мастеров", "гу-мастерам", "гу-мастерами", "гу-мастерах"],
}
GU_FLAG = ("РОД «гу»: сид держит НЕСКЛОНЯЕМЫЙ СРЕДНИЙ; оба флагмана рекомендуют МУЖСКОЙ («этот гу», как «жук»; "
"согласуется с «винный червь — он»). Меняет согласование во ВСЕХ гу-именах — НЕ флипаю молча. "
"Жизненный гу/гу-червь даны в мужском как рекомендация под status:draft. Решение — за владельцем.")
def main():
data = yaml.safe_load(SRC.read_text(encoding="utf-8"))
terms = data["terms"]
changed, added, plural_done, rollbacks = [], [], [], []
for t in terms:
s = t.get("src")
if s in CHANGES:
new_dst, forms, status, contested, rat = CHANGES[s]
old_dst, old_status = t.get("dst"), t.get("status")
t["dst"] = new_dst
if forms is not None:
t["decl"] = {"invariant": False, "forms": list(forms)}
t["status"] = status
tag = "DRAFT⚑" if status == DRAFT else "OK"
t["note"] = f"[v2:{tag}] {rat}. (было: «{old_dst}»). " + t.get("note", "")
changed.append((s, old_dst, new_dst, status, contested, rat))
if old_status == DRAFT and status == DRAFT and contested:
rollbacks.append((s, "оставлен draft (тихий промоушен НЕ применён)"))
if s in PLURALS:
forms = t.setdefault("decl", {}).setdefault("forms", [])
for f in PLURALS[s]:
if f not in forms:
forms.append(f)
t["note"] = t.get("note", "") + " [v2: +мн.ч. D24.4]"
plural_done.append((s, t["dst"], PLURALS[s]))
for a in ADDITIONS:
entry = {"src": a["src"], "dst": a["dst"], "type": a["type"],
"decl": {"invariant": a.get("invariant", False), "forms": list(a.get("forms", []))},
"since_ch": 0, "status": a["status"],
"note": f"[v2-NEW:{'DRAFT⚑' if a['status']==DRAFT else 'OK'}] {a['note']}"}
terms.append(entry)
added.append((a["src"], a["dst"], a["status"], a.get("contested"), a["note"]))
OUT.write_text(yaml.dump(data, allow_unicode=True, sort_keys=False, width=200), encoding="utf-8")
# --- sign-off table для владельца ---
contested_changes = [(s, o, n, c) for s, o, n, st, c, _ in changed if st == DRAFT]
contested_adds = [(s, d, c) for s, d, st, c, _ in added if st == DRAFT]
lg = ["# Глоссарий 蛊真人 — v2 · таблица на ПОДПИСЬ владельца (D30.5)", "",
f"Изменено: {len(changed)} · Добавлено: {len(added)} · Обогащено мн.ч.: {len(plural_done)} · Всего терминов: {len(terms)}",
"Провенанс: единый трансформ `eval/exp13_seed_v2.py` из v1-сида; рекомендации — консенсус флагманов "
"(GPT+Claude, diag/reading/ОЦЕНКА_ФЛАГМАНА.md) + пожелания владельца. **НЕ в прод без подписи + resnapshot.**",
"Статусы: **approved** = применяю (бесспорное); **draft⚑** = НЕ уходит в CONFIRMED-hard-constraints до твоей подписи (memory.go:419/473).", "",
"## A. Спорные — на решение (сейчас status:draft, инъектятся как ⟨проверить⟩)", "",
"| src | вариант в v2 | альтернатива / причина спора | твоё решение |", "|---|---|---|---|"]
for s, o, n, c in contested_changes:
lg.append(f"| {s} | **{n}** (было «{o}») | {c} | ☐ принять / ☐ альтернатива |")
for s, d, c in contested_adds:
lg.append(f"| {s} | **{d}** | {c} | ☐ принять / ☐ альтернатива |")
lg += ["", f"- **Род «гу»** (проектное, влияет на согласование всех гу-имён): {GU_FLAG}", ""]
lg += ["## B. Бесспорные — применены (status:approved)", "",
"| src | было | стало | почему |", "|---|---|---|---|"]
for s, o, n, st, c, rat in changed:
if st == OK:
lg.append(f"| {s} | {o} | **{n}** | {rat} |")
for s, d, st, c, note in added:
if st == OK:
lg.append(f"| {s} | — (новый) | **{d}** | {note} |")
lg += ["", "## C. Обогащение мн.ч. (D24.4/полигон-№4 — закрывает inflection_gap post-check)", "",
"| src | dst | добавленные формы |", "|---|---|---|"]
for s, d, forms in plural_done:
lg.append(f"| {s} | {d} | {', '.join(forms)} |")
lg += ["", "## D. Прочие открытые флаги сида (уже draft в v1, не трогаю без тебя)",
"- 花酒行者/Винный Странник, 甲乙丙丁/разряд А–Г, 蛊虫/гу-червь, 本命蛊/жизненный гу, 南疆/Наньцзян,",
" 一气金光虫/Золотосветный червь, 青丝蛊/гу Чёрных волос, 留影存声蛊 (длинное имя), 沈嬷嬷/матушка Шэнь.",
"- ⚠ Консистентность **род/клан**: 族长→«глава клана» (новый) vs 四代族长→«Четвёртый глава рода» (v1). Унифицировать? ☐ клан ☐ род"]
SIGNOFF.write_text("\n".join(lg), encoding="utf-8")
print(f"v2 seed → {OUT}")
print(f"sign-off → {SIGNOFF}")
print(f"изменено {len(changed)} (спорных→draft: {len(contested_changes)}), "
f"добавлено {len(added)} (спорных→draft: {len(contested_adds)}), мн.ч. {len(plural_done)}, всего {len(terms)}")
print("Спорные (draft):", [s for s, *_ in contested_changes] + [s for s, *_ in contested_adds])
if __name__ == "__main__":
main()

174
eval/exp13_translate.py Normal file
View file

@ -0,0 +1,174 @@
#!/usr/bin/env python3
"""exp13 — бейк-офф ПЕРЕВОДЧИКОВ (платно, кап $5). Пре-регистрация §1.2 заморожена.
Faithful draft-стадия: swap ТОЛЬКО модели переводчика. Layout повторяет прод draft-стадию
(chunkrun.go: translatorInjection = renderGlossaryBlock; render.go MessagesWithInjection):
system(translator.md reflow-edition, brief-filled) system(инъекция «src dst») user(перевод).
Инъекция и исходник из приёмочного store (V1-сид), ИДЕНТИЧНЫ по чанку у всех армов.
translator.md строится в коде с УДАЛЕНИЕМ строки 9 «Сохраняй разбивку» (ратифиц. D30.2;
пакет бэкенда ещё не залендён прод-файл не трогаю).
T1 deepseek-v4-flash из store ($0). T6 qwen только с DashScope-ключом (env QWEN_API_KEY|DASHSCOPE_API_KEY).
Запуск: eval/.venv/bin/python eval/exp13_translate.py [--dry]
Выход: diag/arms13/<ARM>/<ch>_<ci>.txt + diag/arms13/results.json + diag/arms13/costs.jsonl
"""
from __future__ import annotations
import json, os, sys, time, urllib.request, urllib.error
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent))
from refusal_bench import load_env_file
load_env_file()
DIAG = Path("/home/ubuntu/books/gu-zhenren/diag")
OUT = DIAG / "arms13"
TRANSLATOR_MD = Path("/home/ubuntu/projects/textmachine/backend/prompts/translator.md")
CAP_USD = 5.0
TARGETS = [(5, 0), (5, 1), (7, 0), (7, 1), (14, 0), (14, 1)]
# brief-vars из acceptance/book.yaml (D18)
VARS = {"source_lang": "zh", "target_lang": "ru", "genre": "вебновелла",
"audience": "взрослые читатели вебновелл", "title": "蛊真人",
"honorifics": "keep", "transcription": "palladius", "venuti": "0.6",
"footnotes": "minimal"}
# строка D30.2-reflow: удаляем ровно её (ратифицированный снос «Сохраняй разбивку»)
REFLOW_DROP = "Сохраняй разбивку на абзацы и прямую речь как в оригинале."
PRICES = { # $/1M in/out (models.yaml, prices_checked 2026-07-10)
"deepseek-v4-flash": (0.14, 0.28), "deepseek-v4-pro": (0.435, 0.87),
"mistral-large-2512": (0.5, 1.5), "glm-5": (1.0, 3.2), "grok-4.3": (1.25, 2.50),
}
ARMS = {
# T1 — из store, генерации нет (см. main); прайс для справки
"T2": dict(model="mistral-large-2512", base="https://api.mistral.ai/v1",
keyenv="MISTRAL_API_KEY", extra={}, max_tokens=8000),
"T3": dict(model="glm-5", base="https://api.z.ai/api/paas/v4",
keyenv="ZAI_API_KEY", extra={"thinking": {"type": "disabled"}}, max_tokens=8000),
"T4": dict(model="deepseek-v4-pro", base="https://api.deepseek.com/v1",
keyenv="DEEPSEEK_API_KEY", extra={}, max_tokens=8000), # thinking ON = default
"T5": dict(model="grok-4.3", base="https://api.x.ai/v1",
keyenv="XAI_API_KEY", extra={}, max_tokens=8000), # reasoning ON = default low
}
# T6 qwen — гейт на ключ; DashScope OpenAI-совместимый слой
QWEN = dict(model="qwen-flash", base="https://dashscope-intl.aliyuncs.com/compatible-mode/v1",
keyenv="DASHSCOPE_API_KEY", extra={}, max_tokens=8000)
def render(tpl: str) -> str:
for k, v in VARS.items():
tpl = tpl.replace("{{%s}}" % k, v)
return tpl
def translator_system() -> str:
raw = TRANSLATOR_MD.read_text(encoding="utf-8")
sys_part = raw.split("---USER---")[0].rstrip()
lines = [ln for ln in sys_part.split("\n") if REFLOW_DROP not in ln]
dropped = sys_part.count(REFLOW_DROP)
if dropped != 1:
raise SystemExit(f"reflow-guard: ожидал 1 вхождение строки D30.2, нашёл {dropped} — translator.md изменился, сверься с оркестратором")
return render("\n".join(lines))
def build_messages(source: str, inj: str) -> list[dict]:
msgs = [{"role": "system", "content": translator_system()}]
if inj.strip():
msgs.append({"role": "system", "content": inj})
msgs.append({"role": "user", "content": f"Переведи следующий фрагмент:\n\n{source}"})
return msgs
def call(spec: dict, messages: list[dict], timeout: int = 300):
key = os.environ.get(spec["keyenv"], "")
if not key:
return None, f"no key {spec['keyenv']}", {}
body = {"model": spec["model"], "messages": messages,
"temperature": 0.3, "max_tokens": spec["max_tokens"], **spec["extra"]}
req = urllib.request.Request(spec["base"].rstrip("/") + "/chat/completions",
data=json.dumps(body).encode(),
headers={"Content-Type": "application/json", "Authorization": f"Bearer {key}"})
try:
with urllib.request.urlopen(req, timeout=timeout) as r:
data = json.loads(r.read())
except urllib.error.HTTPError as e:
return None, f"HTTP {e.code}: {e.read()[:200].decode('utf-8','replace')}", {}
except Exception as e:
return None, f"ERR {str(e)[:160]}", {}
ch = data["choices"][0]
text = (ch.get("message", {}) or {}).get("content") or ""
usage = data.get("usage", {}) or {}
usage["finish_reason"] = ch.get("finish_reason", "")
return text.strip(), (None if text.strip() else f"empty|finish={ch.get('finish_reason')}"), usage
def cost_of(model: str, usage: dict) -> float:
inp = usage.get("prompt_tokens", 0) or 0
comp = usage.get("completion_tokens", 0) or 0
reason = usage.get("reasoning_tokens", 0) or 0 # grok additive шлёт отдельно; subset=0
pin, pout = PRICES.get(model, (0, 0))
return (inp * pin + (comp + reason) * pout) / 1_000_000
def main() -> None:
dry = "--dry" in sys.argv
chunks = json.loads((DIAG / "chunks.json").read_text(encoding="utf-8"))
blocks = json.loads((DIAG / "blocks.json").read_text(encoding="utf-8"))
OUT.mkdir(parents=True, exist_ok=True)
costs_fp = OUT / "costs.jsonl"
results, spent = [], 0.0
arms = dict(ARMS)
qkey = os.environ.get("DASHSCOPE_API_KEY") or os.environ.get("QWEN_API_KEY")
if qkey:
os.environ["DASHSCOPE_API_KEY"] = qkey
arms["T6"] = QWEN
print("T6 qwen: DashScope-ключ найден — включаю")
else:
print("T6 qwen: ключа нет — пропускаю (не блокируюсь)")
# T1 — из store (deepseek-v4-flash draft), $0
t1 = OUT / "T1"; t1.mkdir(exist_ok=True)
for ch, ci in TARGETS:
(t1 / f"{ch}_{ci}.txt").write_text(chunks[f"{ch}/{ci}"]["draft"], encoding="utf-8")
print("T1 deepseek-v4-flash: 6 чанков из store ($0)")
for arm, spec in arms.items():
adir = OUT / arm; adir.mkdir(exist_ok=True)
for ch, ci in TARGETS:
key = f"{ch}/{ci}"
src = chunks[key]["source"]
inj = blocks[key]["bilingual_glossary"]
outp = adir / f"{ch}_{ci}.txt"
if outp.exists() and outp.read_text(encoding="utf-8").strip():
print(f" [{arm} {key}] cached"); continue
msgs = build_messages(src, inj)
if dry:
approx_in = sum(len(m["content"]) for m in msgs) // 3
print(f" [{arm} {key}] model={spec['model']} msgs={len(msgs)} ~in={approx_in}tok src_chars={len(src)}")
continue
if spent > CAP_USD * 0.8:
print(f"!! near cap (${spent:.3f}) — stop"); break
t0 = time.time()
text, err, usage = call(spec, msgs)
c = cost_of(spec["model"], usage)
spent += c
rec = {"arm": arm, "model": spec["model"], "chunk": key, "cost": round(c, 6),
"usage": {k: usage.get(k) for k in ("prompt_tokens", "completion_tokens", "reasoning_tokens", "total_tokens", "finish_reason")}}
with costs_fp.open("a", encoding="utf-8") as f:
f.write(json.dumps(rec, ensure_ascii=False) + "\n")
if not text:
print(f" [{arm} {key}] ERROR: {err} (${spent:.3f})")
results.append({**rec, "error": err}); continue
outp.write_text(text, encoding="utf-8")
results.append({**rec, "chars": len(text)})
print(f" [{arm} {key}] {time.time()-t0:.0f}s {len(text)}ch ${c:.4f} cum=${spent:.3f} finish={usage.get('finish_reason')}")
if not dry:
(OUT / "results.json").write_text(json.dumps(results, ensure_ascii=False, indent=1), encoding="utf-8")
print(f"\nTOTAL arm spend: ${spent:.4f} (cap ${CAP_USD})")
if __name__ == "__main__":
main()