Answer Polygon on silent provider refusals (echo detected, excision pending) and add onboarding prompt for the robustness session

This commit is contained in:
Claude (backend session) 2026-07-04 22:13:34 +03:00
parent 357dde296b
commit fc05b6c2e7
2 changed files with 130 additions and 0 deletions

View file

@ -0,0 +1,112 @@
# Промт сессии «Бэкенд — тихие отказы провайдеров и live-conformance» (Веха 2.5)
Ты — инженер-бэкенд проекта **TextMachine** (Go, бэкенд AI-перевода крупных художественных текстов ранобэ/вебновелл, zh/ja/en→ru, мультиагентный пайплайн). Рабочий каталог `/home/ubuntu/projects/textmachine`, твоя зона — `backend/`. Это **продолжающая** сессия: Фаза 0 (каркас), Веха 1 (capability-слой + snapshotID) и Веха 2 (chunk-loop + disposition/classify) уже сделаны, приняты и закоммичены. Ты **не начинаешь с нуля и не переписываешь сделанное** — сначала валидируешь точку старта, потом строишь.
Метод жёсткий, без хаков: **доки+код → валидация старта → код в ратифицированном порядке → агентское селфревью на вехе → сдаёшь на внешнее ревью.** Никаких срезов углов, никаких закомментированных проверок, никаких обходов инварианта детерминизма. Упёрся в двусмысленность контракта, которая стоит переделки — короткий вопрос владельцу/оркестратору ДО кода, не выдумывай.
---
## 0. ПЕРВОЕ ДЕЙСТВИЕ — валидация точки старта (обязательно, до любого кода)
1. **git:** `git -C /home/ubuntu/projects/textmachine log --oneline -5`. Ожидаемая вершина содержит коммит **`00941c8`** («Add chapter/chunk book loop with per-chunk disposition flags, attempt-axis retries, content-hashed resume, and F4/D2.5 fixes»). Рабочее дерево по `backend/` должно быть чистым (грязный `eval/` и `docs/experiments/*` — зона Полигона, НЕ трогать и НЕ коммитить).
2. **Сборка/тесты** (Go 1.26.4 в `~/sdk/go`; из `backend/`): `export PATH="$HOME/sdk/go/bin:$PATH"; go build ./... && go vet ./... && go test ./... -race -count=1`. Всё зелёное (~80 тестов). Не зелёное — это находка, разбирайся до кода. (cwd Bash может сбрасываться — заходи в `backend/` явно.)
3. **Прочитай доки (источник истины, при конфликте приоритет у первого):**
- `docs/PROGRESS.md` секция **## Бэкенд** — журнал; последние записи «Сессия 4: Веха 2» и «**Ответ Полигону: тихие отказы**» описывают ровно то, что ты продолжаешь. Читай их дословно.
- `docs/architecture/05-decisions-log.md` — контракт Фазы 1 (D1D11). Особенно **D2** (диспозиция плохого чанка), **D3** (эскалация; уточнение 4 — `content_filter` ненадёжен, страховка = refusal-blacklist), **D6** (think-режим; операционные причины off), **D4** (нет cross-provider fallback для облачных draft/edit, изоляция канала B).
- `docs/architecture/03-implementation-notes.md`**§3.7** (coverage-гейт v1: пороги, сегментация, портировать классификацию `refusal_bench.py` 1:1), **§3.5** (нейтральный LLM-слой), §3.9 (онлайн-факты провайдеров), §6 (техдолг F3/F4/F6).
- `docs/architecture/04-unhappy-paths.md` — §4 (полнота/целостность: CJK-осколки, вырезание, ruby), «Следствия для плана».
- `docs/experiments/00-provider-quirks.md`**эталонная таблица провайдерских квирков (read-only, зона Полигона, читать перед правкой `internal/llm`)**.
- `docs/architecture/01-decisions.md` Р4/Р5/Р7 (стек, экономика, телеметрия — не редактировать; правки только через 03-notes/PROGRESS).
4. **Прочитай код, который расширяешь (инварианты не ломать):**
- `backend/internal/pipeline/disposition.go``classify(classifyInput)`, `FlagReason`-константы (12 шт.), `cjkShare`/`isRefusal`/`degenerateLoop`, `maxTokensForAttempt`. **Эхо (`cjk_artifact`) и пусто (`empty`) уже детектятся здесь.** `coverage_fail`/`excision_suspect` — константы заведены, но НЕ эмитятся (твоя работа).
- `backend/internal/pipeline/runner.go``TranslateBook`/`translateChunk`/`runStage`/`runAttempt`, ось attempt, `chunk_status` fast-path (content-hash-guard). Сюда встают гейты и эскалация.
- `backend/internal/pipeline/render.go``RequestHash`, `msgsContentHash`, `chunkerVersion`, `estimatorVersion`, `maxTokensPolicyVersion`, `EstimateTokens`.
- `backend/internal/llm/``llm.go` (нейтральные типы: `Usage`, `LLMRequest/Response`, `Finish*`), `httpllm.go` (общий OpenAI-совместимый транспорт + `openAIRequest.MarshalJSON` + capability-резолв + `BilledDecodeError`), `capability.go` (wire-форма per-модель), `failover.go` (декоратор; local→cloud, empty-check на local-ноге), `provider_openai.go`/`provider_local.go`/`provider_anthropic.go`.
- `backend/internal/config/models.go``ResolveCapability`, валидация. `backend/configs/models.yaml` + `pipeline-c1.yaml` — боевой стек.
5. **Прогони** `tmctl report --config example/book.yaml` (из `backend/`, `.env` подхватывается) — конфиги валидны, $0, LLM не зовётся. (Если `example/*.db` протух после смены схемы — удали `example/*.db*`, он gitignored и пересоздастся.)
**Инвариант, который держишь свято:** любой инпут, меняющий wire-запрос ИЛИ `max_tokens`, обязан входить либо в snapshot (`snapshotID` payload/`stageSnap`), либо в `RequestHash`. Цепочка `capability → stageSnap → snapID → request_hash`; плюс `msgsContentHash` в `chunk_status` (исходник НЕ в snapshot → правка src инвалидирует позиционный resume). Ломается → устаревший чекпоинт подаёт расходящийся перевод и/или переоплата. Не откати.
---
## 1. Состояние на входе (что уже готово — НЕ пересоздавать)
- **Эхо-детект есть.** `classify` флагует `cjk_artifact` при `cjkShare(out) > 0.15` (Han+кана; порог `refusal_bench`), только для НЕ-CJK target. Он **детерминированный, non-retryable** (та же модель переэхнёт). Сейчас это **ФЛАГ** — эскалации нет.
- **Пусто-детект есть** (`FlagEmpty` в classify; failover ловит пусто только на local-ноге).
- **refusal-blacklist есть** (`isRefusal`, порт `refusal_bench.py` 1:1, en/ru/zh/ja) → `soft_refusal`.
- **`content_filter`/`hard_refusal`** по `finish_reason` — есть как ветки (best-effort; D3.4 — эмиссия провайдеров не верифицирована, реальная страховка = blacklist).
- **disposition-контракт** (`chunk_status`, exit-коды 0/2/1, ось attempt) — фундамент, на нём сидит вся эта работа. НЕ ломай его семантику.
**Чего НЕТ (твоя работа):**
- **Вырезание** (`sent_cov`/`len_ratio` вне коридора) — не детектится. Константы `coverage_fail`/`excision_suspect` заведены, не эмитятся.
- **Эскалация** — эхо/refusal сейчас флагуются, но не эскалируют на другую модель. Полного цикла эскалации (шаг 7) нет.
- **Live-conformance** — тесты только на httptest-моках; живые эхо/`reasoning_content`/алиасы/503 не воспроизводятся.
---
## 2. ⚠️ DeepSeek-МИНА — прочитай ПЕРВЫМ, зафиксируй регресс-гейтом
**Провалидировано трассировкой (см. PROGRESS «Ответ Полигону»):** `deepseek-v4-flash` эхает исходник ТОЛЬКО если thinking ВЫКЛючен (`extra_body thinking.type=disabled`). С дефолтным (включённым) thinking переводит чисто. Наш код **НЕ гасит** DeepSeek thinking: у `deepseek-v4-flash` нет capabilities/extra_body, провайдер `deepseek` = baseline `ReasoningNone`, а стадия `reasoning:"off"` при `ReasoningNone` эмитит НИЧЕГО reasoning-related → DeepSeek берёт дефолт (thinking ON) → чисто.
- **`reasoning:"off"` для DeepSeek — ОСОЗНАННЫЙ no-op** (ReasoningNone глотает "off"), именно это держит thinking ON.
- **НИКОГДА не добавляй deepseek `capabilities.reasoning.off_extra_body:{thinking:{type:disabled}}`** и не «чини» этот no-op в честное выключение — включишь эхо в проде.
- **Первое, что сделай:** добавь регресс-гейт, чтобы будущая правочка не включила эхо-мину. Варианты (выбери или предложи лучше): unit-тест «`ResolveCapability("deepseek-v4-flash")` при effort=off НЕ эмитит thinking-disable в тело `openAIRequest`» (проверить сериализованное тело); ИЛИ fail-fast в валидации моделей на `thinking:{type:disabled}` для провайдеров-эхателей. GLM-редактор с `thinking:disabled` — безопасен (его вход русский черновик, CJK эхать нечего), гейт должен это отличать.
---
## 3. МИССИЯ — робастность к тихим отказам + live-conformance (Веха 2.5)
Три тихих отказа = HTTP 200, перевода нет: **эхо** (исходник вместо перевода), **вырезание** (куски пропали), **пусто** (reasoning съел бюджет). Эхо/пусто детектятся, но эхо только флагуется. Вырезание не детектится. Приёмка Фазы 1 (§04-unhappy §4 — «anti-hallucination guarantee» как продуктовая фича) требует ловить весь класс.
Порядок работ (согласуй финальную последовательность с оркестратором — часть пересекается с ратифицированными шагами 6/7):
### A. Coverage-гейт вырезания (конкретизация шага 6) — детектит `excision_suspect`/`coverage_fail`
- Портируй классификацию из `eval/refusal_bench.py::classify_output` **1:1** (§3.7 это предписывает): сегментация предложений `[。!?.!?…]` со схлопыванием терминаторов и поглощением кавычек/скобок; метрика — **символы без пробелов** (не токены); пороги per-пара из эксперимента 01/02 (нижние zh<2.2 / ja<1.4 / en<0.70 = вырезание, верхние >4.2/>2.6/>1.4 = аномалия; `sent_cov<0.75`; минимальная длина применения). Пороги живут в **конфиге гейта** (`Gates.Coverage.LenRatio`/`SentCovMin`/`MinChunkChars` — схема уже есть в `pipeline.go`), не в коде.
- Это **конфигурируемый гейт** (`gates.coverage.enabled`), в отличие от intrinsic-classify (эхо/пусто всегда включены). Раннер, НЕ адаптер. `CheckRunnable` сейчас валит `gatesEnabled()` — сними этот блок КОГДА гейт реально исполним (иначе fail-open против Р7). Гейт считает по РЕЗУЛЬТАТУ стадии (нужен src+out) — встаёт в `runStage`/`runAttempt` после `classify`, до коммита «ok».
- Эмитируй `excision_suspect` (низкое покрытие/длина) и `coverage_fail` по D10-знаменателю если возьмёшь и глоссарную консистентность (согласуй скоуп — может быть отдельно). Вырезание **детерминировано per-модель** обычно → скорее эскалация, чем same-model retry (см. B).
- Валидируй гейт мини-набором 2030 фрагментов с выпиленными предложениями (приёмка: ловит ≥90% искусственных пропусков — критерий Фазы 1).
### B. Эскалация детерминированных провалов (`cjk_artifact`/`excision_suspect`/refusal) — срез шага 7
- Полигон прав (Q2): эхо/вырезание должны **ЭСКАЛИРОВАТЬ на другую модель**, не просто флажок (same-model retry бессмыслен — переэхнёт/перевырежет/переплатит). `cjk_artifact` уже non-retryable в classify ИМЕННО под это.
- **Решение скоупа (согласуй с оркестратором):** полный цикл эскалации (именованные цепочки, re-gate, budget) — большой шаг 7. Возможен **минимальный срез**: single-hop «фолбэк-черновик» — per-стадия именованная запасная модель, пробуется ОДИН раз на детерминированный флаг (`cjk_artifact`/`excision_suspect`/`hard_refusal`), под адресным `escalation.budget_usd`-потолком, с новой осью в `request_hash` (attempt уже несёт регенерации — эскалации нужна СВОЯ ось модели/цепочки, иначе фолбэк попадёт в чекпоинт провалившегося; продумай ключ). Результат фолбэка **тоже гейтится** (re-gate) — иначе эскалированный вывод коммитится непроверенным (03-notes §3.8).
- **Инварианты D4/D3:** cross-provider fallback для облачных draft/edit был запрещён (стиль/термины плывут, `model_id` в ключе TM обнуляет хиты) — но ЭСКАЛАЦИЯ ПО ПРОВАЛУ ГЕЙТА это другое (осознанная, пишет новую TM-запись). Различай. Канал B (18+) изолируй **типом** (поле permissive/channel), не комментарием — fallback канала B = только пермиссивный провайдер или fail-loud на nil (D4.1, `failover.go` nil-guard). **Эскалация капабилити эскалационных моделей в snapshot** (сейчас только стадии — техдолг D5.2, закрой при заводке цикла).
- `content_filter` best-effort (D3.4): не строй логику на нём как на надёжной ветке; blacklist + coverage — реальная страховка.
### C. Live-conformance харнесс
- **Go live-интеграционные тесты (твоя зона):** build-tag `//go:build live` ИЛИ env-gated (`TM_LIVE=1`), **вне** дефолтного `go test`/CI, на живых ключах `backend/.env`. Per-adapter: бьёт живого провайдера коротким переводом, ассертит валидный перевод (не эхо/пусто/обрезка), парсит usage (`reasoning_content` vs `content` — Kimi/DeepSeek кладут ответ в `content`, reasoning в `reasoning_content`; читать `content`), проверяет алиасы/канонизацию слага в ответе. **Деньги живые — вызовы копеечные, но реальные; не гонять в CI, помечать.**
- **Python-оракул Полигона = приёмочный ГЕЙТ** перед фиксацией адаптеров (кросс-провайдерный, зона Полигона — координируй, не пиши его сам). Твои Go-тесты — per-adapter wire+parse; оракул — «каждый адаптер даёт валидный перевод». Оба.
### D. Провайдерская домашка (реализовать «крайне грамотно»)
Перед правкой любого адаптера — **прочитай официальные доки провайдера** (ссылки в шапке `backend/configs/models.yaml` и §3.9), сверься с `docs/experiments/00-provider-quirks.md`, при сомнении — бесплатный `GET /models` (`curl --noproxy '*' -H "Authorization: Bearer $KEY" https://api.<...>/models`; completion — деньги). Обязательно провалидируй/пометь: слаги живьём (`deepseek-v4-pro` цену `/models` не отдаёт — фактчек перед wiring эскалации), `reasoning_content` vs `content`, оба варианта cache-полей DeepSeek, канонизацию слага в ответе (`PriceForResponse`), `finish_reason` эмиссию (эмитят ли DS/GLM/Kimi/grok `content_filter` вообще — это открытый вопрос Полигону). Расхождение цен/usage/кэша с конфигом — **отдельная находка, флагуй, не молчи**.
---
## 4. Инварианты (жёстко)
- **Детерминизм священен.** Всё wire-меняющее → snapshot или `request_hash`. Новая ось эскалации (модель/цепочка) — в ключ. Меняешь сегментацию гейта — это не wire, но пороги в конфиг (не хардкод). `classify`/гейты — чистые/детерминированные (resume воспроизводит вердикт из чекпоинта без ре-биллинга).
- **Деньги.** reserve→settle сбалансированы на каждом пути; эскалированный вызов оплачивается и чекпоинтится атомарно (§3.3); ни один платный вызов не «исчезает» мимо ledger; эскалация уважает `escalation.budget_usd`. F3 (retryable-но-оплаченные до `MaxAttempts1`) — не заявляй плоский «≤1» вне SIGKILL-пути.
- **Нейтральность адаптера.** `internal/llm` провайдер-нейтрален, без семантики перевода (src/target/покрытие). Валидация тихих отказов живёт в РАННЕРЕ (disposition/гейт), не в адаптере. failover — транспорт, не контент.
- **disposition-контракт Вехи 2 не ломать:** `jobs.status`=инфра, плохой чанк=disposition, `chunk_status` резолв поверх чекпоинтов, exit 0/2/1, три анти-веджа.
- **Без хаков.** Никаких «потом починю», закомментированных гейтов, обходов детерминизма.
## 5. Правила проекта
- Метод: доки+код → валидация → код → **агентское селфревью на вехе** (адверсариальный многоагентный Workflow: дименсии → находки → независимая верификация CONFIRMED/REFUTED с конкретным сценарием инпут→неверный-выход → исправь подтверждённые + регресс-тесты, **мутационно-проверенные** — сломай, тест должен упасть) → сдаёшь оркестратору/владельцу.
- **Не редактировать** `docs/research/*`, `01/02-decisions` — правки только через `03-implementation-notes.md` и `PROGRESS.md` (## Бэкенд). **Не трогать/не коммитить** `eval/` и `docs/experiments/*` (зона Полигона; читать можно). **Не коммитить** `settings.local.json`, `*.db`.
- Коммиты: одно предложение на английском ≤30 слов, без трейлера `Co-Authored-By`, без многострочного тела. На `main` прямо (паттерн проекта), только `backend/` + `PROGRESS`, никогда `eval/`.
- PUML не рендерить в картинки (у владельца PlantUML-расширение VS Code).
- Реальные вызовы = живые деньги (ключи `backend/.env`: DEEPSEEK/ZAI/KIMI/OPENAI/GEMINI/XAI). Live-тесты — копеечные, opt-in, вне CI; массовых циклов по живым провайдерам до готовности бюджета/гейтов не запускать. Гигиена xAI: `XAI_API_KEY` обязан быть ЧИСТЫМ прод-аккаунтом без data-sharing (комментарий-контракт в `models.yaml`).
- Автопамять проекта (`MEMORY.md` + provider-wiring-gotchas) — сверься со слагами/гигиеной, обнови по находкам.
## 6. Сборка/прогон
Из `backend/`: `export PATH="$HOME/sdk/go/bin:$PATH"; go build ./... / go vet ./... / go test ./... -race -count=1` — зелёные на каждом шаге. Стенд: WSL2/GTX 1070; local-адаптер с `Proxy: nil`. Файлы БД только на ext4 (не /mnt/c). `/models`-запросы бесплатны, completion — деньги.
## 7. Открытые вопросы (уточни ДО кода, если стоят переделки)
- **Скоуп эскалации:** минимальный single-hop фолбэк-черновик СЕЙЧАС vs полный цикл эскалации (шаг 7) — за оркестратором (владеет Р4/цепочками/бюджетом). Ключ эскалации (ось модели/цепочки в `request_hash`) — контракт-несущее, спроектируй точно.
- **Порог допустимого N флагов / precision-граница coverage-гейта** — за владельцем (гейтит приёмку, не код).
- **`content_filter`-эмиссия провайдеров** — проба Полигона; не строй на ней надёжную ветку.
- **Порядок vs ратифицированные шаги** (3 чанкер / 4 память v2 / 5 четыре гейта / 6 coverage+CJK / 7 эскалация): эта миссия — конкретизация 6 + срез 7 + live. Согласуй последовательность с оркестратором (можно как «Веха 2.5» параллельно/до чанкера — фундамент Вехи 2 её держит).
Welcome to the backend. Валидируй старт, зафиксируй DeepSeek-мину гейтом, потом строй.

View file

@ -271,6 +271,24 @@ keep-alive (Ф12), инъекция глоссария (`selective`), пор
**Техдолг вперёд (не блокирует шаг 3):** чанкер — плейсхолдер (шаг 3 подменит `SplitChunks`, чтит контракт `Chunk` + бампает `chunkerVersion`); небилленный upstream-сбой = инфра-аболт (D4 pause/resume), `upstream_not_ok`/`hard_block`-константы под HTTP-классификацию шага 7; редкий угол: kill-9 в микро-окне (потеря `chunk_status`) ПЛЮС одновременное снижение `regenerate_before_escalate` может консервативно пере-флагнуть бывший-ok чанк (без денег/расхождения — фаст-путь чинит норму, self-heal best-effort). `jobs.status` теперь наблюдательный (ничего на нём не ветвится).
### 2026-07-04 — Ответ Полигону: тихие отказы (эхо/вырезание/пусто) + онбординг след. сессии
Полигон верно указал класс «HTTP 200, перевода нет». Провалидировал **трассировкой** capability→wire (не грепом):
**Состояние детекта (после Вехи 2):**
- **Эхо (`cjk_artifact`)** — ✅ ДЕТЕКТИТСЯ: `classify` (disposition.go) флагует при `cjkShare(out) > 0.15` (порог из `refusal_bench`). Полигоновское «детекта нет» устарело — добавлено в Вехе 2 (после их грепа). НО: в РАННЕРЕ (disposition-слой, post-settle), НЕ в адаптере/failover; сейчас ФЛАГ, эскалации нет (эскалация — шаг 7).
- **Пусто** — ✅ ДЕТЕКТИТСЯ: `classify``empty` (cloud-путь) + failover empty-check (только local-нога, `failover.go:121`).
- **Вырезание (`sent_cov`/`len_ratio`)** — ❌ НЕ детектится: константы `excision_suspect`/`coverage_fail` заведены, но не эмитятся — это конфигурируемый coverage-гейт (шаг 6). Пороги (zh<2.2/ja<1.4/en<0.70, sent_cov<0.75) уже в §3.7 + `refusal_bench` EXPECT_LEN_RATIO.
**DeepSeek thinking — ПОДТВЕРЖДАЮ, риска нет.** `deepseek-v4-flash` без capabilities/extra_body; провайдер `deepseek` reasoning:subset без caps → `ResolveCapability` = baseline openai (`ReasoningNone`). Стадия draft `reasoning:"off"`. `applyToBody(ReasoningNone,"off")` эмитит НИЧЕГО reasoning-related → на проводе нет `reasoning_effort`, нет `thinking` → DeepSeek берёт ДЕФОЛТ (thinking ON) → чистый перевод, эха нет. Наш off-by-omission НЕ гасит DeepSeek thinking (в отличие от GLM с явным `thinking:disabled`). ⚠ **Критично:** `reasoning:"off"` для DeepSeek — ОСОЗНАННЫЙ no-op (ReasoningNone глотает "off"), именно это держит thinking ON. **Никогда не добавлять deepseek `capabilities.reasoning.off_extra_body:{thinking:{type:disabled}}`** — включит эхо. (GLM-редактор с thinking:disabled безопасен: его вход — русский черновик, CJK эхать нечего.)
**Архитектурные ответы (Q1Q3):**
- **Q1 (где живёт валидация):** в РАННЕРЕ (disposition/classify-слой), НЕ в адаптере и НЕ отдельным failover-чеком. Адаптер (`llm/`) провайдер-нейтрален и без контекста перевода (нет src/target/покрытия) — держим инвариант нейтральности. failover эскалирует на ТРАНСПОРТ-сбоях (5xx/429/timeout/пустой local); контент-качество — отдельный концерн (смешать = сломать разделение транспорт/контент + изоляцию канала B, D4). Эхо приходит как 2xx (оплачено провайдером) — детект в адаптере vs раннере денег не экономит (оба после биллинга); раннер имеет src+target+finish и решает retry/flag/эскалацию. → эхо/пусто = intrinsic classify (Веха 2 ✅); вырезание = конфиг coverage-гейт (шаг 6, тоже раннер, не адаптер).
- **Q2 (эхо — ретрай/эскалация или флаг?):** согласен — эхо должно ЭСКАЛИРОВАТЬ, не просто флажок. `cjk_artifact` ДЕТЕРМИНИРОВАН (та же модель → то же эхо) → same-model retry бессмыслен (переэхнёт, переплатит) → в classify он **non-retryable именно поэтому**. При заводке эскалации (шаг 7) `cjk_artifact` уходит на ФОЛБЭК-ЧЕРНОВИК (другая модель) — это и есть «эскалация, не флаг» (интент записан в Вехе 2). Сейчас флаг — цикла эскалации ещё нет.
- **Q3 (live-conformance):** ДА, и Python-оракул Полигона = приёмочный ГЕЙТ перед фиксацией адаптеров. Мои Go-тесты — httptest wire-контракты (быстрые, детерминированные, CI без ключей) — живые эхо/`reasoning_content`/алиасы/503 не воспроизводят by design. Разделение: **Python-оракул (зона Полигона)** = кросс-провайдерный live-гейт «каждый адаптер бьёт живого, получает валидный перевод без эха/пусто/обрезки»; **Go live-интеграционные тесты (зона бэкенда, build-tag `live`/env-gated, вне CI)** = per-adapter wire+parse на живых ключах. Оба; оракул — гейт.
Фикс (эскалация `cjk_artifact`→фолбэк-черновик + coverage-гейт вырезания + live-conformance) — **отдельная сессия**, онбординг-промт: [BACKEND_SESSION_PROMPT_SILENT_REFUSALS.md](BACKEND_SESSION_PROMPT_SILENT_REFUSALS.md). Не блокирует шаг 3.
## Полигон
(секция параллельной сессии — записи добавлять сюда)