textmachine/docs/architecture/03-implementation-notes.md

266 lines
33 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Заметки реализации (сессия «Бэкенд», 2026-07-04)
Вердикт шага 0 — перевалидации решений v2 глазами реализатора перед Фазой 0.
Метод: собственное чтение донорского кода vojo + воркфлоу из 4 критиков (линзы: консистентность доков,
исполнимость в Go, сверка с донором построчно, онлайн-фактчек цен/API; ~940k токенов, находки ниже отобраны и объединены).
Формат: **подтверждаю** / **правки реализации** (фиксируются здесь, архитектурные доки не трогаю) /
**[НУЖНО РЕШЕНИЕ]** (продублировано в PROGRESS; с ревизии D31 — в `../archive/PROGRESS-2026-07-04-10.md`; все пункты развязаны D-логом).
## 1. Общий вердикт
Архитектура v2 **исполнима, кодить можно**. Противоречий, блокирующих Фазу 0, нет. Подтверждаю:
- Р1 (Go + порт vojo): донор сверен построчно — llm.go/httpllm.go/failover.go/pricing.go/telemetry.go/store.go
существуют, объёмы и механизмы соответствуют research/10 (retry-классификация 429/5xx vs терминальный 4xx,
типизированная `httpStatusError`, reserve/settle с TOCTOU-защитой, анти-flapping брейкер, «2xx с пустым
контентом = оплаченный вызов», priceFor никогда $0, 137 тест-функций).
- Р3/Р6 (SQLite, modernc.org/sqlite): FTS5 и WAL в pure-Go драйвере есть; чанк-чекпоинты и kill -9-тест
реализуемы (одна маленькая транзакция на LLM-вызов при вызовах в 10300 с — тривиальная нагрузка).
- Р5 (cache-раскладка): схема «стабильный префикс → волатильный хвост» совместима одновременно с Anthropic
`cache_control` (явные брейкпоинты, max 4) и DeepSeek-автокэшем (prefix-match с 0-го токена, блоки по 64
токена) — селективный глоссарий в хвосте кэш DeepSeek не ломает.
- Конфликты, разрешённые ревью v2 (кэш↔селективная инъекция, modernc↔sqlite-vec, интерим 18+, чанк-чекпоинты),
действительно согласованы по всей цепочке доков.
## 2. Упущения передавшей сессии, найденные валидацией (главное)
1. **В `Usage` донора нет cache-write**у Anthropic запись в кэш платная (×1.25 за 5m TTL / ×2 за 1h),
поле `usage.cache_creation_input_tokens`. Без него — систематический недоучёт расходов, тот же класс
ошибки, что уже оплаченный в vojo недоучёт reasoning-токенов 3044%. research/10 это предлагал (§3 п.1),
но в обязательства Фазы 0 в плане не попало. → В `Usage` добавляю `CacheCreationTokens`, в `ModelPrice`
`CacheWritePerM`; формула: `(promptcached)·in + cached·cacheRead + cacheCreation·cacheWrite + (completion+reasoning)·out`.
2. **Скрытый третий таймаут донора**: полигон знал про 45 с (нога failover) — но в транспорте httpllm.go:214
захардкожен ещё и per-attempt 60 с, а maxTry=3. С чанками 63278 с любая генерация >60 с была бы убита и
трижды бесплатно ретраена. → Все таймауты/ретраи — параметры клиента из конфига (профиль per-провайдер
per-роль), + чтение `Retry-After` на 429 (донор его игнорирует, backoff капится 8 с — молоток по
rate-limit'ам массового прогона).
3. **Прокси-ловушка глубже, чем в памяти проекта**: Go stdlib НЕ проксирует loopback никогда, так что
127.0.0.1 сам по себе безопасен; опасны **не-loopback локальные адреса** (WSL-gateway 172.x, как в самом
vojo). Все клиенты донора — `&http.Client{}` с `ProxyFromEnvironment`. → В адаптере локального провайдера
и его пробах явный `Transport{Proxy: nil}`.
4. **Grok 4.1 Fast мёртв** (retired 15.05.2026, слаги молча редиректятся на grok-4.3 c ДРУГОЙ ценой
$1.25/$2.50) — таблица Р4 в этой части устарела ещё до старта кода. Состав/цены — только configs/models.yaml
(Р4 это и предписывает), но канал B остаётся без «дешёвого Grok» — see [НУЖНО РЕШЕНИЕ].
5. **store.go — переписывание, а не «адаптация <20%»**: advisory-локов в SQLite нет, весь файл на pgx-API,
диалект Postgres (IDENTITY, JSONB, GREATEST, now()). Переносится семантика (reserve/settle/release,
миграции per-step, идемпотентные шаги, тесты как поведенческий контракт), не код. Оценку research/10
по этому файлу считать оптимистичной; трудоёмкость Фазы 0 это не ломает (семантика простая).
6. **Приёмка Фазы 0 требует больше, чем список работ Фазы 0**: `tmctl translate` на один чанк draft→edit
подразумевает мини-раннер, промпты Translator/Editor и сборку контекста. → Мини-раннер C1 — явный
деливерабл Фазы 0 (здесь зафиксирован).
## 3. Контракты Фазы 0 (обязательства реализации)
### 3.1 Детерминизм и request-hash (Р6)
Рендер промпта — **чистая функция** от `(snapshot джобы, закоммиченные чекпоинты предыдущих чанков, индекс
чанка)`. Запрещены timestamps/UUID в промпте; все выборки из памяти — с `ORDER BY`; все map сортируются по
ключу перед рендером. Проверяется unit-тестом «два рендера байт-в-байт идентичны».
`request_hash = sha256(book_id | chapter | chunk_index | stage | role | model+sampling | snapshot_id | sha256(rendered_messages))`.
Противоречие буквы Р6 («ключ: request-hash» vs «волатильные части исключаются») разрешаю так: хэш считается
от запроса, **отрендеренного из snapshot**, — волатильность заморожена снапшотом, исключать нечего.
### 3.2 Snapshot контекста джобы
Материализованная запись с собственным `snapshot_id`: `brief_hash`, версии промпт-шаблонов per-role,
model+sampling per-role, версия чанкера, зафиксированное состояние approved-глоссария (версия-счётчик),
style sheet, резюме книги/арок на момент старта джобы. **STM в snapshot не входит** — он детерминированно
пересобирается из чекпоинтов. Батч-подтверждение терминов (Фаза 1) применяется только на границе джоб.
### 3.3 Деньги: атомарность settle+checkpoint
Дыра «settle прошёл — kill -9 — чекпоинт не записан» = двойная оплата вызова. → **Settle и запись сырого
ответа — одна SQLite-транзакция в одном файле БД** (ledger и чекпоинты живут в проектной БД). Тогда
неустранимая потеря — только in-flight вызов (провайдер списал, ответ не дошёл) — это и есть честные
«≤1 вызов» приёмки. На старте процесса — recovery-проход: снять зависшие reserve джоб без чекпоинта.
### 3.4 Ключ TM (Фаза 1, фиксируется сейчас — влияет на схему store)
`tm_key = (lang_pair, brief_hash, sha256(нормализованный СЫРОЙ src-чанка))` — нормализация только
whitespace/Unicode NFC, **до** pre-replace терминов (иначе подтверждение термина инвалидирует TM через
чёрный ход, что прямо запрещено Р6). `prompt_version`, `model`, `glossary_version` — метаданные записи,
НЕ ключ. Открытые вопросы политики инвалидации — [НУЖНО РЕШЕНИЕ] п.2.
> **ПОПРАВКА (оркестратор ответил, PROGRESS/Р6 — источник истины).** Этот абзац устарел: оркестратор
> зафиксировал ключ TM `(chunk_src_hash, lang_pair, model_id, prompt_version, brief_hash,
> context_snapshot_hash)` — то есть **`model_id` и `prompt_version` ВХОДЯТ в ключ**, а не в метаданные
> (тюнинг промпта/смена модели легитимно инвалидируют TM; перечанкование — by design). Имплементеру Фазы 1:
> строить ключ TM по PROGRESS/Р6, не по букве этого абзаца. В Фазе 0 это неважно (cross-book TM не
> реализован — есть только checkpoint/resume request-hash, который `model`+`prompt_version` уже включает
> через snapshot).
### 3.5 Нейтральный интерфейс LLM (расширения к типам vojo)
- `Message.CacheBoundary bool` — конец стабильного блока префикса. Anthropic-адаптер вешает `cache_control`
на соответствующий блок (максимум 4 брейкпоинта); OpenAI-совместимые адаптеры игнорируют (автокэш по
префиксу). Единственная точка, где Р5 материализуется в коде, — раскладку задаёт сборщик контекста, не адаптеры.
- `Usage{Prompt, Cached, CacheCreation, Completion, Reasoning}` — семантика reasoning per-adapter
(xAI аддитивно, OpenAI-спека/ollama subset → 0, Anthropic thinking внутри output → 0) документируется в
каждом адаптере, как в доноре.
- `ReasoningEffort`: значение `"off"` — явное выключение thinking; маппится адаптером в провайдер-специфичный
флаг. Для нестандартных ручек (GLM `thinking.type=disabled`, Qwen `enable_thinking=false`) —
`extra_body` per-модель в models.yaml, мёрджится в JSON запроса OpenAI-совместимым адаптером
(эмпирика полигона: thinking GLM-4.6 — таймауты ×3, Gemini молча жрёт max_tokens на thinking).
- Стриминг в Фазе 0 НЕ реализуется, но интерфейс проектируется под него (второй метод в будущем не ломает
адаптеры); длинные генерации закрываются длинными attempt-таймаутами из профиля. См. [НУЖНО РЕШЕНИЕ] п.4.
- DeepSeek-адаптер парсит **оба** варианта cache-полей usage (`prompt_tokens_details.cached_tokens` и
`prompt_cache_hit_tokens`/`prompt_cache_miss_tokens`) — иначе эксперимент Фазы 0 по кэшу покажет ложный ноль.
- Ответ транспорта читается через `LimitReader` (у донора кап есть только в gemini-native).
- Failover-декоратор портируется, но состав пары primary/fallback задаётся снаружи per-роль
(позже per-канал: в канале B fallback = только пермиссивный провайдер или fail loudly — иначе пустой ответ
локальной abliterated молча уйдёт в облачную ногу, дыра в изоляции Р4).
### 3.6 SQLite-рецепт (modernc.org/sqlite)
- На файл БД два пула: write (`MaxOpenConns=1`, транзакции `BEGIN IMMEDIATE`) и read (N соединений).
- Прагмы на соединение: `journal_mode(WAL)`, `busy_timeout(5000)`, `foreign_keys(1)`, `synchronous(NORMAL)`
(для kill -9-приёмки достаточно; отключение питания — вне модели угроз MVP).
- Замена advisory-локов: сериализация check-and-reserve через write-пул + `BEGIN IMMEDIATE`.
- Регистр/леммы кириллицы нормализуются на стороне Go до записи (NOCASE в SQLite — ASCII-only);
сравнения в SQL — только по нормализованным колонкам.
- FTS5 в горячий путь v1 не тащить (unicode61 не сегментирует CJK; селективной инъекции хватает точных
ключей/алиасов по обычным индексам).
- Файлы БД — только на ext4 (WSL: не /mnt/c — WAL-локи ломаются на drvfs); задокументировать для установок.
### 3.7 Coverage-гейт v1 (Фаза 1, спека фиксируется сейчас)
Портировать классификацию исходов из `eval/refusal_bench.py` (hard_refusal / soft_refusal /
excision_suspect / ok) 1:1, не изобретать заново. Метрика — **символы без пробелов** (не токены!);
пороги per-пара из эксперимента 02: нижние zh<2.2 / ja<1.4 / en<0.70 (вырезание), верхние >4.2 / >2.6 / >1.4
(аномалия), sent_cov<0.75; минимальная длина применения (калибровка на 5002500 симв.). Сегментация класс
`[。!?.!?…]` со схлопыванием последовательностей терминаторов и поглощением закрывающих кавычек/скобок.
Отдельные ветки по `finish_reason`: `content_filter` = refusal; `length` = обрезание бюджетом ретрай с
бОльшим max_tokens, НЕ эскалация (реальный кейс Gemini из эксп. 02). Blacklist refusal-паттернов en/ru/zh/ja.
### 3.8 Конфиги (граница Р2 — закрытие 6 недоопределённостей)
Три файла: `book.yaml` (translation brief + ссылки) `configs/pipeline-c1.yaml` (ядро)
`configs/models.yaml` (модели/цены/таймауты, с датой проверки). В конфиг ядра входят: состав/порядок стадий,
рольмодель, версии промптов, пороги гейтов, **режим инъекции глоссария (selective | full_prefix)** (Р5
требует обе схемы), токен-бюджеты сборки контекста (инъекция, STM, overlap), лимит регенераций до эскалации,
**именованные эскалационные цепочки** (список моделей; channel-aware фильтр применяет раннер), fan-out N для
C2, cache TTL per-стадия. В models.yaml: цены (+cache write/read), профиль таймаутов/ретраев per-модель,
`extra_body`. Зашито в раннер: циклы по главам/чанкам, ветвление по гейтам, механика эскалации/ретраев,
семантика «эскалация re-gate флаг» (диаграмма pipeline.puml перегейтовку не рисует реализую re-gate
с ограничением попыток, иначе эскалированный вывод коммитится непроверенным).
### 3.9 Онлайн-факты (проверены 2026-07-04, источники — официальные доки)
- DeepSeek: `deepseek-chat`/`deepseek-reasoner` отключаются 24.07.2026 15:59 UTC (сейчас алиасы
`deepseek-v4-flash`); v4-flash: $0.14 in / $0.28 out / $0.0028 cache-hit, контекст 1M. Пиковых надбавок
для V4 официально НЕТ (только пресс-слух) в конфиг не закладывать, заложить переключаемость тарифа.
- Anthropic: cache write ×1.25 (5m) / ×2 (1h), read ×0.1; поля `cache_creation_input_tokens` /
`cache_read_input_tokens`; максимум 4 брейкпоинта; **минимальный кэшируемый префикс зависит от модели**
(Sonnet 5 1024 ток., Opus 4.8 1024, Haiku 4.5 4096) короче лимита кэш молча не создаётся;
thinking биллится как output (полный объём, даже при summarized-отображении). Sonnet 5 промо $2/$10 до
31.08.2026 (кэш write $2.50/5m, hit $0.20; batch $1/$5); Opus 4.8 $5/$25.
- GLM (Z.AI, международный): флагманы уже GLM-5.1/5.2 ($1.4/$4.4), GLM-5 $1/$3.2 (cached $0.2).
- Kimi: актуальны kimi-k2.6 ($0.95 in / $4.00 out / $0.16 cache-hit) и k2.7-code; API-домен теперь
platform.kimi.ai (301 с platform.moonshot.ai).
- xAI: grok-4.3 $1.25/$2.50 (1M контекст); Grok 4.1 Fast retired (см. §2 п.4).
- Gemini: 3.1 Pro (Preview) $2/$12 до 200k промпта, $4/$18 свыше; batch 50%. Эмпирика полигона: judge-роли
нужен нативный API (safety_settings через OpenAI-слой не передаются) нативный Gemini-адаптер планирую
Фазой 2 (в vojo есть донорское v1beta-лицо), в Фазе 0 закладываю только место в раскладке адаптеров.
Ловушки телеметрии кэша: TTL 5m может истекать между вызовами одной стадии (латентности чанков 63278 с +
гейты) TTL параметр стадии; телеметрия обязана показывать `cache_read=0 при повторных вызовах` (тихая
инвалидация). DeepSeek-кэш best-effort экономику считать только по фактическому usage.
## 4. Мелкие поправки к research/10 (для истории)
- request_log донора 46 колонок после v8, не 43; тест-функций 137, не «125+».
- «Table-driven тесты» преувеличение: основной стиль донора сценарные функции + httptest wire-контракты +
конкурентные тесты под `-race`; копирую именно этот стиль.
- Из config.go переносимы хелперы (~150200 строк: getenv/getSecret `*_FILE`/fail-fast «problems списком»/
Summary с redaction), а не ~400; YAML-слой конфигов целиком новый код (yaml.v3 в доноре только для Matrix).
- telemetry/trace: `safego` живёт в bot.go выносится в obs; reqInfo (sender/verbose) заменяется на ось
book/chapter/chunk/role; прайсеру нужен явный default-price якорь (у донора `Prices[XAIModel]`).
## 5. [НУЖНО РЕШЕНИЕ] — продублировано в PROGRESS (## Бэкенд)
1. **Премиум-бюджет Р5 «≤2× input-объёма»** после токен-калибровки неоднозначен (какой токенизатор, входит ли
output; эксп. 01: полный проход уже ~2.8×B o200k-input). Реализую как конфигурируемый $-потолок книги
(без зашитого «2×»); формулу должен уточнить владелец/оркестратор.
2. **Политика инвалидации TM** сверх brief_hash: (а) входит ли prompt_version в ключ (да = тюнинг промпта
обнуляет TM библиотеки; нет = TM молча переживает смену промптов); (б) модель в ключе или метаданных
(эскалация перезаписывает запись или дополняет?); (в) смена параметров чанкера = полная инвалидация
трактовать как «смену brief» с показом стоимости? Реализую (а) нет, (б) метаданные+дополняет, (в) да
но это меняет экономику пере-переводов, нужно подтверждение.
3. **Канал B без дешёвого Grok**: Grok 4.1 Fast retired; grok-4.3 $1.25/$2.50 (~9× дороже по input).
Основную модель канала B на Фазу 2 надо перевыбрать (кандидаты: open-weights через OpenRouter, DeepSeek,
локальная abliterated). Фазу 01 не блокирует (интерим-правило 18+ действует).
4. **Стриминг**: план относит к Фазе 3 «нужен только IDE-фронту», но эмпирика полигона требует его как
keep-alive транспорта для генераций 63278 с (иначе фейловер ложно роняет здоровую локалку). В Фазе 0
закрываю длинными таймаутами из профиля; предлагаю оркестратору перенести провайдерский стриминг
(не SSE-фронт!) в Фазу 12.
5. Оркестратору (не блокирует): цифры Р5 устарели ~2× против эксп. 01 (Sonnet-проход $46, не $92); задача
«проверка cache-hit агрегаторов» назначена и Фазе 0 бэкенда, и бэклогу полигона предлагаю отдать
полигону (у него харнесс сравнения usage), бэкенд потребит результат в конфиге; TMX в диаграммах не
помечен «(Фаза 3)»; морфодетектор/CometKiwi/NSFW-классификатор в диаграммах без фазовых пометок;
режим онгоинга отсутствует в диаграммах (и не определено, перегоняется ли Analyst при дозагрузке глав).
## 6. Селфревью Фазы 0 (агентное, 2026-07-04)
Финал Фазы 0: `/code-review` (8 finder-линз + верификация) + воркфлоу-критики (4 линзы задания: архитектура/
соответствие v2, корректность и деньги, идиоматичность Go, устойчивость к сбоям) с адверсариальной проверкой
каждой находки. Подтверждённые исправлены **двумя волнами** (коммиты `dc6ea8e` `a32ec2e` фиксы v2):
**Волна 1 (коммит a32ec2e):** общий retry-движок (устранена копипаста AnthropicOpenAI, кап Retry-After
`maxRetryAfterWait` , overflow-safe backoff); `BilledDecodeError` 2xx с нечитаемым телом консервативно
селтлит оценку, не возвращает резерв; пустой оплаченный ответ громкая остановка, не отравляет редактуру;
snapshot-pinning гейт с `--resnapshot`; flock-владение проектным файлом; цены локальных тегов $0; однопроходный
рендер; `temperature` без omitempty (Anthropic вообще не шлёт sampling 400); `attempt` в ключе и схеме.
**Волна 2 (эта):** snapshot-покрытие расширено (Defaults ratio/min, TitleBriefHash, модельный ExtraBody
правки конфига больше не инвалидируют чекпоинты в обход гейта); `PriceForResponse` неизвестный слаг ответа
книжится по ЗАПРОШЕННОЙ модели, не по дешёвому якорю (устранён недоучёт премиума); usage-кламп в `CostUSD`
(отрицательные числа не уменьшают committed); zero-usage платный 2xx settle оценки + WARN; жизненный цикл
статуса джобы (не остаётся `running` на отказе потолка/ошибке клиента); `CheckRunnable` fail-fast на
нереализованную механику (C2/C3, fan-out>1, роль judge — иначе `pipeline-c2.yaml` прогнал бы стадии линейно и
сжёг деньги Opus на мусор); `CheckKeys` — preflight API-ключей used-моделей (пустой ключ ловится до reserve, не
401-м после); request-hash — length-prefix полей + счётчик сообщений (устранена NUL-коллизия); нормализация
источника BOM/CRLF/NFC (детерминизм хеша между ОС/редакторами); LimitReader-усечение терминально (не ретраит
оплаченный >16 MiB); recoverReservations → stderr.
**Остаточный техдолг (осознанные ограничения Фазы 0, не блокеры):**
- **Дневной потолок — пер-книжный** (ledger в файле проекта одной книги): `day_usd` сбрасывается в UTC-полночь
и ограничивает burn-rate одной книги, но не «на оператора за день по всем проектам». Общий потолок оператора
потребует shared spend-файла — отложено (раскладка «файл БД на книгу» из Р3/Р6); пометить в UI при мультикниге.
- **Retryable-но-оплаченные вызовы не идемпотентны** (уточнено внешним ревью F3): 2xx с НЕусечённым
нечитаемым телом и чистый таймаут/обрыв до тела после серверного биллинга — retryable, поэтому цепочка
ретраев может оплатить **до `MaxAttempts1` лишних вызовов** (не «≤1», как я записал раньше — граница
занижена была). Потолок к ним слеп; краха нет, триггер редкий (повторный оплаченный сбой). Честный фикс —
idempotency-ключ на стороне провайдера (поддерживают не все), а не «сделать всё терминальным» (уронит
прогон на транзиентном сбое прокси). Обсудить с владельцем/оркестратором; отложено до Фазы 1.
- **finish=length с непустым обрезанным черновиком** (F4): течёт в edit с `OK=true` без `Degraded`-флага
(раннер ловит только пустоту). Асимметрия наблюдаемости — свернуть в редизайн обработки негодного вывода
Фазы 1 (та же семья, что «пустой ответ»): `Degraded:"truncated_length"` + WARN, или skip+flag по `attempt`.
- **`prices_checked` (120д) гейтит read-only `report` и $0-resume** (F6): они платных вызовов не делают, но
падают на устаревших ценах. Мелкая связанность — проверять свежесть цен только там, где будет платный вызов.
- **cache_ttl ↔ cache_write_per_m**: цена записи в кэш в models.yaml задаётся под ОДИН TTL (у нас 5m). Если
оператор поставит `cache_ttl: 1h`, реальная запись стоит ×2 (не ×1.25) — недоучёт. Боевой конфиг 5m совпадает;
при переходе на 1h требуется отдельная цена. Валидацию/вторую цену — Фаза 1 (когда включим 1h осознанно).
- **Гонка `Runner.clients` map**: ленивая инициализация клиентов не синхронизирована — безопасна в Фазе 0
(последовательный проход стадий), но параллельные чанки/fan-out Фазы 12 потребуют мьютекса/предзагрузки.
**Внешнее (независимое) ревью Фазы 0 (2026-07-04).** Отдельная сессия провела построчное чтение всего
`backend/` + исполняемую проверку приёмки + адверсариальный воркфлоу (22 агента). **Вердикт: Фаза 0 принята,
блокеров ноль**; двухволновое селфревью подтверждено закрывшим реальные вещи; детерминизм-машинерия признана
пропорциональной (не переинженерена). Диспозиция находок (F-нумерация ревью):
- **F1** (CheckKeys префлайтит эскалационные ключи) — **уже исправлено** до ревью (сужен до достижимых моделей:
стадии всегда, эскалация — только при включённом гейте; бэкенд заводится на 2 ключах).
- **F2** (local-оверрайды temp/max_tokens вне snapshot), **F5** (fail-open на `gates.enabled=true`),
**F7** (устаревший §3.4 vs подтверждённый ключ TM), **F8** (`*.db.lock` в .gitignore) — **исправлено этой
волной** (F2/F5 — тесты добавлены; F7 — поправка-сноска выше в §3.4).
- **F3/F4/F6** — в техдолг выше (обоснованно отложены).
- **Дизайн-вопросы к владельцу/оркестратору (D1D3):** **D1** — редактор C1 видит исходник (`{{text}}` в
editor.md); для Фазы 1+ сделать редактора монолингвальным (только `{{draft}}`), верность → coverage-гейт +
билингвальный судья (совпадает с ролевой моделью Р2); в Фазе 0 билингвальный редактор — защитимый интерим до
появления гейта/судьи, но `{{text}}` убрать, когда они приедут. **D2** — «пустой/негодный ответ = падение
всего прогона» неверно для Фазы 1 (много чанков); нужен per-chunk skip+flag (совпадает с Р4: ложное
срабатывание не должно убивать многочасовую джобу) — закроет механизм `attempt`. **D3** — изоляция канала B
в failover не принуждается кодом (пара primary/fallback задаётся вызывающим); при подключении failover
(Фаза 2) нужен структурный запрет Anthropic в канале B, иначе пустой ответ local-abliterated молча уйдёт в
Anthropic. Все три — Фаза 12, не блокеры Ф0.