textmachine/docs/architecture/05-decisions-log.md

26 KiB
Raw Blame History

Журнал решений оркестратора — развязки перед Фазой 1 (2026-07-04)

Ответ на вопросы бэкенд-сессии (PROGRESS §«Вопросы от бэкенда», §«[НУЖНО РЕШЕНИЕ] перед Фазой 1») и полигона (think-режим). Каждое решение прошло адверсариальную панель из 3 критиков (research / экономика / исполнимость в коде Фазы 0); ни одно не отклонено, все уточнены. Это контракт Фазы 1 — бэкенд исполняет отсюда; при конфликте с буквой 01-decisions/02-mvp-plan — источник истины здесь (потом сольём в основные доки).

Статусы: принято · + принято с уточнением панели.


D1. Редактор C1 — монолингвальный. +

Решение: редактор Фазы 1 видит только черновик + одобренный глоссарий (dst-термины как target-констрейнты), не исходник. Убрать {{text}} из prompts/editor.md.

Обоснование: издательская норма (редактор не знает язык оригинала — research/07); билингвальный редактор в zh→ru якорится к исходнику и плодит кальки (главная translationese-болезнь); монолингвальный редактор к тому же дешевле (исходник не сидит на входе каждого edit-вызова).

Уточнения панели (обязательны):

  1. Честно: coverage-гейт НЕ контролирует верность. Он ловит только omission/дописывание по длине; same-length mistranslation (свап рода, «кто кого», неверная полисемия) проходит насквозь. Значит в Фазе 1 верность после черновика не контролирует ничто до билингвального судьи Фазы 2. Это допустимо (Фаза 1 — инфраструктурная веха), но записать прямо: приёмка Фазы 1 не содержит критерия верности — не маскировать это coverage-гейтом.
  2. Пилот Ф2.5: плечо C1 (draft→editor) меряется на win-rate только вместе с живым билингвальным судьёй — иначе C1 по собственному признанию полирует неверное в «гладко-неверное».
  3. Код: убрать {{text}} меняет SHA256 шаблона → resnapshot-гейт сработает громко (хорошо), но prompt_version: "v0-draft" в pipeline-c1.yaml станет врать (тот же лейбл на двух SHA). Поднять edit prompt_version при этой правке.

D2. Диспозиция плохого чанка — skip+flag+continue. +

Решение: finish=length → ретрай с бо́льшим max_tokens до капа, потом флаг; hard refusal/content_filter → флаг+продолжить; пустой оплаченный → skip+flag (не падение прогона); soft refusal (blacklist) → флаг. Флагнутый чанк не коммитит мусор/пустоту в TM.

Уточнения панели (обязательны):

  1. attempt этого НЕ несёт (переоценка в моём ответе). В коде Фазы 0 нет ни цикла по чанкам (chapter=1/chunk=0 захардкожены, runner.go:235), ни хранилища флагов, ни статуса «skipped/flagged»; любой ненормальный путь сейчас = SetJobStatus(failed)+return. Механика skip+flag+continue — net-new работа Фазы 1 (цикл + таблица флагов + колонка disposition, читаемая resume-путём). attempt — необходим, но не достаточен.
  2. Тегировать flag_reason. retry-flagged переатакует только length/empty на той же модели; content_filter/refusal детерминирован per-модель/канал — повтор на том же SFW-канале снова откажет и снова спишет деньги. В Фазе 1 refusal-флаги просто не переатаковать (деньги на гарантированный отказ не жечь); маршрутизация в канал B/эскалацию — Фаза 2.
  3. Детектор вырождения перед удвоением max_tokens. У дешёвого черновика finish=length чаще всего — repetition-loop; бо́льший max_tokens лишь оплачивает больше повтора (усугубляется неидемпотентными ретраями F3). Дешёвый n-gram-loop чек ПЕРЕД удвоением → при вырождении сразу флаг, без оплаты цикла.
  4. content_filter ненадёжен как ветка: OpenAI-совместимый адаптер отдаёт finish_reason сырым (httpllm.go), пробрасывают ли его DeepSeek/GLM/Kimi — не проверено. Реальная страховка — refusal-blacklist гейт, а не ветка по finish_reason. Полигону: замерить, эмитят ли провайдеры content_filter.
  5. Edit max_tokens считать от длины ЧЕРНОВИКА, не исходника (runner.go:283-287 сейчас от source). Для монолингвального редактора (D1), чей вход/выход ≈ русский черновик (~1.9× токенов исходника zh→ru), оценка от source мис-сайзит бюджет и ложно триггерит length-ретрай.

D3. Эскалация. +

Решение: канал A — DeepSeek V4 Pro → GLM-5.1 → апекс Gemini 3.1 Pro; канал B (18+) — Grok 4.3 основной пермиссивный тир + DeepSeek офиц. + локальная abliterated. Sonnet удалён (Anthropic — дорого); OpenAI остаётся (ключ есть) — GPT-5 nano альт черновика, GPT-5 mini кандидат в редакторы/second-opinion судьи.

Правка по xAI (04.07, вопрос владельца — xAI НЕ выведен): ретайрнулся только дешёвый Grok 4.1 Fast; сам провайдер жив, ключ есть. grok-4.3 ($1.25/$2.50) — средний тир, не «дорогой крайний». Роли Grok: (а) переводчик канала B (самый пермиссивный крупный API — стандарт R-rated); (б) судья/QA 18+ контента — не отказывается оценивать explicit, в отличие от Gemini (нужен нативный safety-off) и Claude (отказ). Ограничения: reasoning xAI аддитивный → канал B с thinking OFF, а think-ON на Grok — только после reasoning-буфера в EstimateUSD (D6.2). Промо xAI $150 за data-sharing: брать на выделенный NSFW/eval-аккаунт для PD/тест-корпуса и refusal/18+-бенчмарка (разблокирует висящую 18+ часть эксп.02) — но не гнать через data-sharing реальные/конфиденциальные клиентские книги (конфликт с ToS-гарантией и zero-retention режимом B2B, research/08).

Правка редактора (04.07, эксп.04 полигона — бейк-офф): дефолт-редактор Фазы 1 = grok-4.3. Слепой бейк-офф glm-4.6 / kimi-k2.6 / gemini-3.1-pro / grok-4.3 на 4 фрагментах (2 фронтир-судьи, сверка с каноническими ru-переводами, согласие независимо): grok-4.3 — ранг-1 верность, ранг-2 стиль, самый дешёвый+быстрый (~13с без reasoning) → лучший value «одной модели на роль». gemini-3.1-pro — лучшая проза → премиум-эскалация редактора. GLM-4.6 слабейший (опровергает допущение Р4 «редактор=GLM»; перемерить при GLM-5), Kimi худший value (~11k reasoning-ток./абзац, ранг-последний по верности) → оба сняты с дефолта редактора. Это закрывает вопрос бэкенда №2 (дефолт редактора). Оговорка: LLM-судьи на коротких фрагментах — провизорный дефолт Фазы 1; финальный выбор редактора/ядра — человеческий пилот Ф2.5. Р4 обновлён. Следствие: grok-4.3 стал центральной моделью (редактор SFW + канал B + судья 18+) — гигиена аккаунтов xAI на три использования (Р4), редактор с thinking OFF (ledger чист).

Уточнения панели (обязательны):

  1. Модели ОТСУТСТВУЮТ в конфиге. models.yaml знает только deepseek-v4-flash/glm-5/kimi-k2.6/local; deepseek-v4-pro/glm-5.1/gemini-3.1-pro/grok-4.3 и провайдеры gemini/xai/openaiне заведены, цепочки эскалации всё ещё [kimi-k2.6]-заглушки. Задача бэкенда: завести провайдеры+модели+цены, фактчекнуть слаг deepseek-v4-pro перед wiring (иначе 4xx).
    • ПОПРАВКА (принял корректировку бэкенда): «без новых адаптеров» было оптимистично. Провайдеры OpenAI-совместимы по эндпоинту, но не по телу запроса: текущий openAIRequest всегда шлёт temperature + max_tokens, а Kimi требует temperature: 1 (иначе 400), gpt-5-mini — max_completion_tokens без temperature + reasoning_effort: minimal, Gemini 3.1 Pro — обязательный thinking (thinking_budget: 0 → 400). Это не строчка в yaml, а per-model capability-слой в адаптере, и он нужен раньше D3: Kimi — уже альтернативный редактор Р4, edit-стадия на temp 0.4 даст 400 на первом вызове. → Capability-слой — фундаментальный элемент Фазы 1. Форма (согласована с принципом «модели/цены только в конфиге», Р4): каждая модель в models.yaml декларирует возможности — budget_field: max_tokens|max_completion_tokens, temperature: send|omit|locked:1, reasoning_control: none|extra_body_disable|effort_minimal|mandatory; адаптер строит тело запроса по ним. experiments/00-provider-quirks.md — эталонная таблица квирков.
  2. Премиум-бюджет $5/$30 УСТАРЕЛ — выведен из калибровки на Sonnet $3/$15. Апекс теперь Gemini 3.1 Pro ($2/$12 ≤200k) с обязательным thinking (reasoning биллится как output $12/M — тот же класс недоучёта, что сжёг vojo 3044%). Плюс batch 50% к inline-эскалации неприменим (последовательна из-за STM), только к судье/QA. Плюс Gemini = и апекс, и судья → эскалированный чанк платит Gemini дважды (перевод+ре-джадж), оба с форсированным reasoning. Полигону: пересчитать бюджет на фактическом Sonnet-free стеке с reasoning-as-output, batch только на судью. До пересчёта — $-потолок конфигурируемый (D-эконом ниже), дефолты $5/$30 держим как консервативную заглушку.

D4. Provider-fallback — НЕ добавлять для облачных draft/edit. +

Решение: авто-кросс-провайдерного фоллбека для облачных ролей нет (фоллбек драфта = разный стиль/термины по чанкам + model_id в ключе TM обнулит хиты на ре-ране). Провайдер лёг → durable pause → resume. Локальный→облако failover из vojo — только для local-ролей, channel-aware. Ночь — супервизор перезапускает tmctl.

Уточнения панели (обязательны):

  1. Channel-aware изоляция в коде НЕ обеспечена (дыра). FailoverConfig не имеет поля канала/пермиссивности — изоляция сейчас лишь doc-comment для вызывающего; сам failover в Фазе 0 не подключён. Хуже: ветка «fallback отсутствует → fail loud» сделает nil-panic (нет nil-guard, c.fallback.Complete разыменует nil), а путь «пустой local-контент → облако» — ровно тот сайлент-шип, о котором предупреждает заголовок, без guard. При подключении failover (Фаза 2): изоляцию канала B делать типом (поле permissive/channel) или явной fail-loud веткой на nil-fallback, а не комментарием.
  2. Формулировка «resume бесплатен» переусилена: техдолг F3 — retryable-но-оплаченные вызовы не идемпотентны, resume может переплатить прерванный in-flight (до MaxAttempts1). Точнее: «resume переоплачивает максимум прерванный in-flight, всё равно кратно дешевле mixed-model failover».
  3. Local→cloud failover для local-ролей канала A (экстракция глоссария, дешёвый судья-гейт) при OOM 8GB GPU молча превращает $0-вызовы в платные облачные — un-budgeted; пометить (телеметрия должна показывать, когда local-роль ушла в облако).

D5. Онгоинг Analyst — инкрементально. +

Решение: book-brief заморожен; новые главы — Analyst инкрементально расширяет резюме и ищет новые термины; approved-глоссарий/series-bible наследуются read-only, новые термины append, approved не ревизуются молча (research/07); разрешение gender=hidden-твиста — ручное событие.

Уточнения панели (обязательны):

  1. Проактивная эвристика gender=hidden. Онгоинг структурно теряет преимущество «книга целиком видит твист заранее»: инкрементальный Analyst не видит ещё не загруженных глав, поздний реверс-трап (女扮男装) → уже вмороженные главы закоммитили род. Митигация: когда Analyst в загруженных главах видит намеренную неоднозначность (омофонное ta / pro-drop без дизамбигуации), ставить gender=hidden и гнать безродовые конструкции (гайд ООН) до раскрытия, а не дефолтить в мужской (TACL-биас). Плюс ретро-патч-протокол «с тома N — так» с показом стоимости пере-перевода вмороженных глав, которые твист фальсифицировал.
  2. КРИТИЧНО (impl): snapshotID не покрывает память и сборку контекста. Сейчас хэширует только brief_hash + план стадий + defaults, не context-assembly (glossary_injection/stm_depth/overlap) и не инъектированный глоссарий/резюме/series-bible. Как только Фаза 1 начнёт инъектить память в msgs: изменённая память меняет request_hash (msgs в нём), но snapshotID не меняется → resnapshot-гейт не сработаетре-ран старой главы молча промахивается мимо чекпоинта и переоплачивает расходящимся переводом — ломается сам инвариант Р6, на который D5 опирается. Свернуть memory-state + context-assembly hash в snapshotID ДО того, как приедет инъекция Фазы 1. Это общий гейт Фазы 1 — держит и D1 (глоссарий редактору), и D5.

D6. Think-режим — не глобальный дефолт, но пилотить на переводчике. + (существенно переписано)

Решение: reasoning_effort per-роль, дефолт off/minimal для draft/edit — но по операционным причинам (эмпирика полигона: GLM ×3 таймауты, Gemini жрёт max_tokens, gpt-5-mini выжигает бюджет — 00-provider-quirks), НЕ потому что «reasoning вредит переводу».

Переписанное обоснование (панель отозвала мою посылку): прежняя «think ломает роль переводчика — пустой вывод» — отозванная находка: PROGRESS:230 исправляет её («пустота — артефакт тесного контекста, не зависание; с ctx 16k think завершается и реально чинит реалии: 羅生門→Радзёмон, чтение 朱雀»). DRT (research/03, arXiv:2412.17498) независимо показывает: long-CoT над метафорами/сравнениями поднимает литперевод — это роль ПЕРЕВОДЧИКА. Владелец был прав в гипотезе.

Уточнения (обязательны):

  1. Пилот Ф2.5 think-ON vs OFF по качеству включает draft/translator и editor (не только Terminologist+эскалацию, как я ошибочно сузил) — именно туда указывают и полигон, и DRT. Дефолт-off остаётся до результатов пилота.
  2. Скоуп think-ON эскалации — только subset-billing провайдеры (deepseek/zai/kimi, где reasoning ⊆ completion ≤ maxTokens, EstimateUSD покрывает). xAI/Grok исключить из think-ON пока EstimateUSD не резервирует явный reasoning-бюджет (additive reasoning xAI → «потолок пробьётся» ровно на премиум-пути канала B).
  3. Gemini-апекс/судья — форсированный think-ON (не чтит reasoning='off' → HTTP 400). Заложить его в бюджет как reasoning-as-output ($12/M), а не как opt-in. runner.go шлёт st.Reasoning — адаптеру Gemini off придётся молча глотать; этот кейс назвать явно.
  4. «×23 COGS» занижает многословных ризонеров: Kimi-k2.6 ~11k reasoning-ток./абзац = ×4…×20 на output этой роли — учесть в бюджете эскалации.

D7. Схема банка памяти v1 — скоуп. +

Решение (подтверждаю рекомендацию бэкенда с двумя правками):

  • v1 (store v2-миграция): база Р3 (src, dst, type, aliases, gender, speech, decl, since_ch/until_ch, status, note) + alias-граф (типы: имя/цзы/хао/прозвище/титул) + gender=hidden(until_ch) + translit_policy (западные имена через катакану — 04-unhappy §9) + first_person + series-bible-lite = gender + ты/вы.
  • Правка 1 (панель D5): ты/вы в series-bible-lite — журнал событий переходов, а не статичная матрица пар (переход ты↔вы — сюжетное событие, 12-ru-target §2). Поле genderс проактивной установкой hidden Analyst-ом на неоднозначности (D5.1).
  • Правка 2: nickname_translation оставить в v1 (это одна колонка + лок «перевести раз навсегда», 04-unhappy §1 относит в Фазу 1); дорогая только логика первичного перевода сильной моделью — её можно отложить, но поле держать.
  • Отложить в v1.1: ranked-set (инъективность ступеней), словарь эвфемизмов, число-словарь мер/разрядов (сам число-гейт — Фаза 1, а словарь конвертаций — v1.1).
  • Обязательно: сверка с «Следствиями для плана» 04-unhappy-paths; и snapshotID покрывает состояние памяти (D5.2) — иначе схема памяти без хеша в снапшоте молча ломает resume.

Q3/Q4 бэкенда — прямые ответы

  • Q3 (эскалационный дефолт RU-контура): см. D3 — не-Anthropic, дёшево-первым (DeepSeek Pro/GLM-5.1), апекс Gemini; Sonnet не дефолт (удалён). В будущем зарубежном контуре премиум-эскалация опциональна, но не сейчас.
  • Q4 (per-role provider-fallback): см. D4 — нет для облачных draft/edit; pause/resume; local→cloud только для local-ролей, channel-aware типом.

Требования Фазы 1, не покрытые 6 решениями (из «missed» панели) — в скоуп

  1. Runner без цикла по чанкам/главам — главный длинный шест (chapter/chunk захардкожены). Цикл + хранилище флагов + disposition-колонка — фундамент, на который опираются D2, D5, гейты, эскалация. Строить первым.
  2. snapshotID расширить memory-state + context-assembly hash ДО инъекции (см. D5.2) — общий гейт.
  3. ruby/фуригана-парсер в спеку импорта Фазы 1 (детерминированная часть, 04-unhappy-paths §4). Противоречие с планом:25 («epub v1 фуригану не сохраняет») разрешить: для ja→ru фуригана несёт авторские чтения имён → глоссарий-лок; молчаливая потеря подрывает приёмку ≥98% консистентности ja-книг. Извлекать чтения на инжесте (не сохранять инлайн-разметку, а выхватить ruby в глоссарий).
  4. Дешёвые детерминированные гейты Фазы 1 (были в 04-unhappy-paths §6/§9, но выпали из плана): линтер тире-диалогов (Розенталь §4752), ёфикатор, блок-лист транслит-междометий, число-гейт разрядов 万/億. Самый дешёвый и заметный читателю класс — «максимум воспринимаемого качества за минимум денег». Морфозависимые (канцелярит) остаются Фазой 2 (Python-сайдкар); эти четыре — детерминированные, Go, Фаза 1.
  5. Экономика инвалидации TM ([НУЖНО РЕШЕНИЕ] п.2в): смена параметров чанкера = полная инвалидация «как смена brief» с показом стоимости — да, подтверждаю (ключ TM от src-чанка, перечанкование by design инвалидирует; пользователю показывать $ пере-перевода). prompt_version и model_id в ключе (уже зафиксировано Р6) означают: правка промпта обнуляет TM-библиотеку, эскалация пишет новую запись — это осознанная плата за корректность, не баг.

Экономические открытые пункты (полигону, гейтят достоверность бюджета)

  • Пересчёт премиум-бюджета на Sonnet-free стеке с reasoning-as-output (D3.2).
  • Проброс cache-hit/batch RU-агрегаторами (уже в бэклоге полигона) — вся premium/cache-экономика посчитана для прямых ключей; в RU-контуре главный рычаг Р5 может испариться.
  • budget_usd: 0 в pipeline-c1.yaml — заглушка; $-потолок владелец задаёт по ценовому намерению, дефолты $5/$30 держим до пересчёта.