textmachine/docs/archive/reports/PACK17_DESIGN_2026-07-25.md

344 lines
101 KiB
Markdown
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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.

# ПАК-17, ФАЗА 1 — ДИЗАЙН: generic content-labels × provider-capabilities (канал B «вживую»)
> **Ревью-шапка оркестратора №8 (25.07, приёмка пропорциональная — инлайн-пасс исполнением, без воркфлоу; РАТИФИЦИРОВАНО `D39.26`).** **Принято В РЕДАКЦИИ §14: тело §013 читать ТОЛЬКО через неё** (при расхождении побеждает §14).
> Пере-выведено мной, не с чужих слов: **M1 ре-деривирован независимо** — все пять дайджестов посимвольно (`c2021b1b…cab0df` = `capture.golden:6`; omitempty-хвост при пустых лейблах байт-в-байт тот же); **фантом-гард D-ссылок чист** (D2/D3/D4.1/D6.2/D12/D13.6/D14.1/D15.2/D19/D19.1/D20.2-Q1,Q2/D20.4/D22.5-22.7/D25.4/D30.1/D39.20-25 существуют и говорят заявленное); спот-чеки подтвердили §14.1 (`BriefHash` book-global в общем билдере обеих волн при пер-стадийном `stageSnap`), §14.2 (сырое `reachableModels`; `chains.default[0]` = `escalate_to` драфта), §14.3 (`escMu`-пин «перерасход ≤ 1 хоп»; `Waves.Workers` дефолт 1), §14.5.6 (`GetCheckpoint:337` → `Reserve:379` → `client:410` → `Complete:442`), §14.6.1 (golden пинит `esc_model` на ОТКАЗАВШЕМ хопе), §14.6.2 (`style_check_version` фолдится безусловно), 13 файлов вне git с `adult:`.
> **Ноль опровержений §14; три добора оркестратора (в D39.26):** (A) единый лейбл «adult» схлопывает ратифицированную violence-развязку D14 п.1 (DeepSeek допустим на violence) ⇒ вокабуляр лейблов — решение владельца; (B) `reasoning:"off"` — тихий выход из аддитивного гейта (`pipeline.go:541` внешним условием) И из эхо-гейта (`echoes_when_thinking_off` только у deepseek, обобщение D19.1 п.2 в данные не доехало) ⇒ закрытие обязательно; (C) рейт-гард молча деградирует в «неограниченно» (`ratelimit.go:92-97`) при лейблованном переводчике mistral `max_concurrency:2`.
> **Неточности, выводов не меняющие:** манифестное «`accept-rebill` грепом пусто» — 9 попаданий, все в `backend/docs/D15.2-*.md` (в коде/CLI действительно нет); «23 сайта `LoadPipeline`» включает строку определения.
> **Скоуп-выносы приняты:** `escalation_model`=ответившая (вердикт-изменение, отдельная ратификация) · два нита общности — условно (байт-нейтральная форма + доказательство исполнением) · D22.7 паком НЕ снимается · `--accept-rebill` (D20.2-Q2) — открытая петля. **Разметка фазы 2 — в промте пака-17.**
> **Статус: ДИЗАЙН-ОТЧЁТ, СТОП ПЕРЕД РАТИФИКАЦИЕЙ.** Сессия: бэкенд, 2026-07-25. Промт: `docs/BACKEND_PACK17_CHANNEL_B_SESSION_PROMPT.md` (фаза 1). Кода НЕ трогал — дерево чистое (`git status --porcelain` пусто на старте и на сдаче, кроме этого файла). Ратифицированная база: D39.25 (директива владельца: в движке нет понятия 18+) · D22.7 (L3 = политика входа) · D19 (переводчик канала B = Mistral) · D12/D2 (лестница отказов) · D4.1 (изоляция типом) · D15.2 §13-Q3 (первый рантайм-потребитель обязан назначить слой снапшота).
## §0. Эхо-блок (что я понял — ДО работы)
1. **Скоуп фазы 1:** только дизайн B1B6 + адверсариальный селф-ревью; ни строчки кода; отчёт → СТОП → ратификация.
2. **Инвариант №1 (архитектурный):** в движке НЕТ понятия «18+/adult/канал B». Механизм generic: content-label книги (данные книги) × capability провайдера (данные models.yaml) × политика цепочки (конфиг). «adult» — ЗНАЧЕНИЕ данных.
3. **Инвариант №2:** refusal-обработка причино-агностична — детектим ФАКТ отказа (D12), причина = операторская диагностика; путь один: регенерация → эскалация по цепочке лейбла → flag+skip (D2).
4. **Инвариант №3 (жёсткая линия):** L3 не строим, не анализируем; `eval/data/refusal_corpus/*` не открывал (проверяемо: ни одного чтения оттуда).
5. **Инвариант №4:** live-вызовы с 18+ контентом — только по санкции владельца; дизайн и будущие тесты — на mock-провайдере и СИНТЕТИЧЕСКИХ безобидных фикстурах.
6. **Инвариант №5:** канал A байт-идентичен — golden без пере-капчера, парити EXACT; adult-механика включается ТОЛЬКО явной маркировкой.
7. **Инвариант №6:** capability-исключения (DeepSeek/Anthropic без adult) — СТРОКИ ДАННЫХ, не Go-литералы; гарантия «label ⊄ capabilities → провайдер не получает контент» компилируемая/тестируемая, fail-loud.
8. **Мысленный тест генеральности:** второй лейбл (archaic-register) добавляется ДАННЫМИ, без правки Go.
9. **Не делать:** L3-классификатор · live 18+ без санкции · реальный 18+ текст в репо · пар/книго/провайдер-литералы в Go · расширение скоупа без пинга.
10. **Канал вопросов:** конфликт промта с кодом → пинг оркестратору через владельца (в §5/§6 ниже — 4 таких пинга и 3 вопроса владельцу).
## §1. Метод и что верифицировано исполнением
Метод — образец пака-16 (D39.24): эхо → инвентаризации → дизайн → адверсариальные линзы → ревизия отдельным слоем.
1. Онбординг: CLAUDE.md → `docs/README.md` → D-лог целиком по несущим блокам (D2/D3/D4/D12/D14/D19/D22/D39.20D39.25) → `architecture/12-go-style-notes.md` §0 → нормы промтов §711.
2. Личное чтение кода (не по памяти): `internal/config/{pipeline,models,book}.go`, `internal/pipeline/{disposition,escalation,stagerun,snapshot,waverun,runner,repair,clients}.go`, `configs/models.yaml`, `configs/pipeline-c1.yaml`, `prompts/zh-ru/editor.md`.
3. Семь параллельных read-only инвентаризаций (снапшот/деньги/отчётность/транспорт/тесты/конфиги/refusal-поток), каждая с требованием `file:line` + цитата; их выводы ниже перепроверены точечно мной.
4. **Ключевой численный факт проверен МОИМ исполнением** (не агентским): brief-канон golden-книги даёт ровно записанный в фикстуре `brief_hash` — см. §7 манифест, строка M1. Это фундамент §B5.
## §2. Инвентаризация: 8 фактов, на которых стоит дизайн
**Ф1. Сегодняшняя adult-поверхность в Go — ровно 6 точек, все load-time.** `config.Book.Adult` (`backend/internal/config/book.go:32`) · `config.Stage.Channel` `""|sfw|adult` (`pipeline.go:166`) · `config.Provider.Permissive` (`models.go:60`) + `Models.providerPermissive` (`models.go:271-276`) · `Pipeline.CheckAdultChannel` (`pipeline.go:336-346`) · enum-валидация канала (`pipeline.go:549-553`) · D4.1-изоляция типом (`pipeline.go:569-576`). Единственный вызов из движка — `runner.go:147`, тоже load-time.
**Ф2. Рантайм-влияния на выбор модели у книги НЕТ вообще.** Выбор модели 100% статичен из конфига: `r.client(...)` вызывается только с `model ∈ {st.Model, st.EscalateTo, Gates.Repair.Model}` (`runner.go:299-323`, `stagerun.go:410`). Единственное книжное свойство, меняющее поведение рантайма, — `TargetLang`, и оно гейтит ГЕЙТЫ (`isRuTarget`/`isCJKTarget`) и выбирает ДАННЫЕ (`lang.*For`), но никогда модель. `Stage.Channel` имеет НОЛЬ рантайм-читателей.
**Ф3. Единственная точка-удавка на провода — одна, и она уже есть.** `(*Runner).runAttempt` (`stagerun.go:326`) — единственный вызов `r.client(model)` (`stagerun.go:410`) и единственный `client.Complete(...)` во всём продовом коде (`stagerun.go:442`). Через неё проходят ВСЕ три класса вызовов: праймари (`stagerun.go:131`), эскалационный хоп (`escalation.go:132`), ремонт (`repair.go:389`). Failover-декоратор (`internal/llm/failover.go`) — мёртвый код: `NewFailoverClient` не имеет ни одного вызывающего вне своих тестов. Следствие: один клиент = один провайдер = одна модель, второго пути на провод нет.
**Ф4. Ремонтный путь ТЕРЯЕТ стадийный канал.** `repair.go:386`: `rst := config.Stage{Name: st.Name, Role: roleRepair, Model: model}` — синтетическая стадия копирует только Name/Role/Model, `Channel` отбрасывается. Любой ассерт, ключёванный на `st.Channel`, слеп на ремонтных вызовах; ассерт на `(лейблы книги/чанка, model)` — не слеп. Это решает форму B4.
**Ф5. `escalation.chains` — мёртвые данные рантайма.** Единственные потребители: валидация существования моделей (`pipeline.go:578-584`) и префлайт ключей под бюджетом (`models.go:507-518`). Исполняется ТОЛЬКО single-hop через `stage.escalate_to` (`escalation.go:90,132`). При этом `chains.adult` во всех 4 шиппинг-конфигах = **[grok-4.3], ОДИН элемент** (`configs/pipeline-c1.yaml:101`, `pipeline-arm-{glm,mistral,deepseek-pro}.yaml`). То есть «мульти-хоп по цепочке лейбла» сегодня по данным = один хоп.
**Ф6. `Book.Adult` фолдится в снапшот транзитивно — и документация это ОТРИЦАЕТ (док↔код).** `Adult bool json:"adult"` внутри канона `BriefHash()` (`book.go:221,228`) → `snap.BriefHash` (`snapshot.go:338,413`) → оба волновых snapshotID → `Request.SnapshotID` в каждом `RequestHash` (`render.go:247,271-274`). А комментарий `pipeline.go:331-332` утверждает: «Adult stays wire/verdict-NEUTRAL (no snapshot layer … never a hash input)» — **неверно**. Проверено численно (§7/M1): убрать `"adult":false` из канона = `c2021b1b…``58883ff5…`, то есть обязательный `--resnapshot` и пере-оплата КАЖДОЙ книги.
**Ф7. Байт-стабильность brief-канона — это защита денег следующего платного прогона, а не косметика.** На стенде 5 book.yaml, все с `adult: false`; `rerun2/book-dspro.yaml` и `book-mistral.yaml` по шапке «IDENTICAL to book.yaml (same book_id + all BriefHash fields, so the shared paid draft resumes at $0)». D39.20 зафиксировал $0-резюм драфта на трёх армах, а пак-16 оставил ОТКРЫТУЮ петлю (замер остатка ремонта на пост-профилактическом прогоне, D39.24 §15.1) — она поедет на следующем платном прогоне тех же книг. Сдвиг `BriefHash` = потеря $0-резюма драфта.
**Ф8. Тестовая поверхность adult = 2 теста, оба load-only, исполняемого adult-пути нет.** `config_test.go:37-64` (`CheckAdultChannel`, чистые in-memory структуры) и `runner_test.go:1552-1572` (`NewRunner`+`Close`, `TranslateBook` НЕ вызывается). НЕ покрыто: ветка «escalate_to должен быть permissive» (`pipeline.go:573-575`), enum-валидация канала, `providerPermissive` напрямую, любой хоп на adult-стадии, refusal→хоп→УСПЕХре-гейт (все «хоп вылечил» тесты гоняют `cjk_artifact`), `soft_refusal`→хоп, `content_filter`→хоп. Ни одна фикстура не ставит `adult: true`. Мока на Go-шве не существует вообще: подделка живёт на HTTP (`httptest`, `runner_test.go:60-76`), ответы ключуются ПО ТЕЛУ запроса (имя модели / `max_tokens` / маркер в источнике).
## §3. B1 — Схема content-labels и гранулярность
### Вариант 1 (рекомендуемый): лейблы книги + capability провайдера + реестр политик в ран-конфиге
**Данные книги** (`book.yaml`): `content_labels: [<label>…]` — свободные строковые значения; на загрузке lower-case + сортировка (ровно дисциплина `StyleAllowlist`, `book.go:188-194`), пустой по умолчанию.
**Данные модели/провайдера** (`models.yaml`): `content_labels: [<label>…]` на ПРОВАЙДЕРЕ + опциональный пер-модельный оверрайд (слоение «провайдер → модель», как у `capabilities`, `models.go:327-338`). Одно и то же имя ключа с двух сторон читается как сам механизм: `labels(книга) ⊆ content_labels(модель)`.
- **Почему НЕ переиспользуем существующий блок `capabilities:`**: он про ФОРМУ ПРОВОДА (budget_field/temperature/reasoning, `models.go:79-88`). Смешать «что модель принимает по контенту» с «как собрать тело запроса» — гарантированный футган на ревью. Имена разные, слоение одинаковое.
- Миграция данных: `permissive: true` (3 строки — `models.yaml:67,105,113`) → `content_labels: [adult]`. Ключ `permissive` становится ОТСТАВНЫМ и падает ГРОМКО при наличии (дисциплина отставных ключей `Stage.LegacyPrompt`, `pipeline.go:134-142`). Стендовые книги указывают `models:` на репозиторный `configs/models.yaml` — правка одного файла достаточна, стендовой возни нет.
**Политика лейбла** (ран-конфиг, новый верхний блок): реестр, который даёт (а) допустимость лейбла, (б) действие, (в) цепочку хопов.
```yaml
content_policy: # движок знает ФОРМУ, не значения
adult: { action: route, chain: adult } # route → маршрутизация по данным
# prohibited: { action: terminal } # будущее значение: книга не обрабатывается вовсе
```
- `action: route|terminal`. `terminal` = загрузка книги падает ГРОМКО, ни одного вызова (это generic-механизм под политику ВХОДА D22.7 — без всякого классификатора).
- Лейбл книги БЕЗ записи в реестре = громкая ошибка загрузки. Иначе «лейбл, о котором ран-конфиг не слышал» тихо означал бы «маршрутизируй дефолтами» — ровно класс тихой подмены, который репо уже отвергает (`pipeline.go:360-364`).
**Пер-стадийный оверрайд модели** (там же, где `model:`/`escalate_to:`):
```yaml
stages:
- name: draft
role: translator
model: deepseek-v4-flash # путь без лейблов — не двигается
label_models: { adult: mistral-large-2512 } # D19: переводчик канала B
```
Почему на стадии, а не отдельной таблицей: пер-стадийный фолд снапшота уже существует (`stageSnap`, `snapshot.go:278-336`), валидация уже пер-стадийная, eager-build клиентов уже пер-стадийный (`runner.go:299-323`) — оверрайд ложится в три готовых места без новой сущности.
**Разрешение конфликтов без приоритетов.** Go-map порядок не хранит, поэтому «приоритет по порядку объявления» нереализуем честно. Вместо приоритетов — ГРОМКИЙ отказ на загрузке: если фактический набор лейблов книги даёт для ОДНОЙ стадии два разных оверрайда, это ошибка конфига (набор лейблов книги известен на загрузке — проверка полная, не эвристическая). Альтернатива «упорядоченный список политик» отмечена как расширение, если владелец захочет приоритеты.
**Гранулярность v1 — КНИГА.** Резолвер объявляется как `labelsFor(ch chunk.Chunk) []string` (шов), но в v1 возвращает набор книги.
Аргумент (не догадка): (а) источника данных для пер-главных лейблов в v1 нет — авто-детекция вне скоупа по L3-линии, а ручная разметка 1000+ глав неоперабельна; (б) любая правка лейблов — это `--resnapshot` при ЛЮБОЙ гранулярности (лейбл влияет на модель, модель фолдится пер-стадийно), так что пер-главность не дешевле; (в) добавление пер-главности потом = данные + один фолд, без переархитектуры.
**Цена v1, честно:** книга с редкими explicit-главами едет ПЕРЕСТИКОМ вся — весь объём через permissive-провайдера. Это прямо задевает качество и COGS (см. B2). Вариант «диапазоны глав» (`{adult: {chapters: ["12-14","80-95"]}}`) операбелен руками и стоит немного — но это спекулятивный спрос; вынесен вопросом владельцу (§6/Q1).
**Мысленный тест второго лейбла (archaic-register).** Нужны ровно три правки ДАННЫХ: `content_policy.archaic-register: {action: route, chain: archaic}` · `escalation.chains.archaic: [<модель>]` · `content_labels: [archaic-register]` у способного провайдера (+ при желании `label_models`). Go не трогается. **Механическая проверка генеральности в фазе 2:** сквозной тест гоняет СИНТЕТИЧЕСКОЕ значение лейбла (не «adult») — если движок хоть где-то знает слово «adult», такой тест красный.
### Вариант 2 (отклонён): лейбл как перечисление в Go (`type ContentLabel string` + константы)
Даёт валидацию значений компилятором и автодополнение. Отклонён: прямо нарушает D39.25 (значение = данные), и второй лейбл потребовал бы правки Go — мысленный тест провален.
### Вариант 3 (отклонён): оставить `Stage.Channel`, переименовав значения
Минимум правок, но сохраняет «канал» как ОСЬ КОДА (стадия объявляет свой канал), тогда как директива требует ось ДАННЫХ КОНТЕНТА (книга/глава несёт свойство). Дополнительно: `Channel` теряется на ремонтном пути (Ф4) — то есть ось стадии структурно не годится для гарантии B4.
**Риски B1:** (а) два места, где живут лейблы (книга + models.yaml), рассинхронизируются — закрыто требованием B4 (все достижимые модели обязаны нести все лейблы книги, иначе загрузка падает); (б) свободные строки допускают опечатку — закрыто: опечатанный лейбл не объявлен ни одним провайдером → громко; (в) реестр политик добавляет верхний блок в ран-конфиг (строгий декод `KnownFields`, `pipeline.go:366`) — старые конфиги без блока валидны, пока книга без лейблов.
## §4. B2 — Маршрутизация ролей под лейблом
**Переводчик = `mistral-large-2512`** — D19 п.1, «ровно так»: `label_models: {adult: mistral-large-2512}` на draft-стадии. Провайдер уже несёт способность (`models.yaml:105`), цена $0.5/$1.5 (`models.yaml:220`), не reasoning-модель ⇒ эхо-мины нет, есть rate-guard `max_concurrency: 2` (`models.yaml:226`) — под волновым параллелизмом это уже учтено.
**Редактор — открытый вопрос, разбираю по данным.** Способность сегодня несут только `xai`, `mistral`, `local` (`models.yaml:67,105,113`). Значит кандидатов ровно три, и:
- `deepseek-v4-pro` (текущий интерим-редактор, D39.22) **НЕЛЬЗЯ**: ToS DeepSeek §3.4(5) прямо запрещает sexually explicit (D14 п.1, верифицировано по первоисточнику). Это данные, не код.
- `mistral-large-2512`**отвергаю как несущий редактор**: D39.20 исключил mistral именно в этой роли (анти-корреляция гладкость↔верность доказана трижды; критические смысловые аварии вне гейтов). Годится как дешёвый фолбэк-хоп, не как несущий.
- `local-qwen3-8b` (abliterated) — скрининг/дешёвый фолбэк (D19 п.1: 8/10), не несущий редактор.
- **`grok-4.3` — рекомендация, ИНТЕРИМ.** Аргументы: (1) бейк-офф exp04 кормил моделей БИЛИНГВАЛЬНО и дал grok ранг-1 по верности / ранг-2 по стилю (D3 «правка редактора»), а сегодняшний редактор — ровно билингв (D30.1; `prompts/zh-ru/editor.md:35-37` подставляет `{{text}}` = исходник); (2) это ратифицированный первичный судья эротики (D22.6), т.е. на этом контенте он мерян как не-отказывающий; (3) единственный из способных, кто вообще претендует на «несущий».
**Цена и ловушки — тоже честно:** $1.25/$2.50 (`models.yaml:183`) — это ×~2.9 по output к dspro ($0.435/$0.87, `models.yaml:131`); провайдер `xai` биллит reasoning АДДИТИВНО (`models.yaml:66`), а D30.1 снял reasoning-off из редакторских ролей ⇒ стадия обязана объявить `reasoning_max_tokens > 0`, иначе загрузка падает по D6.2/D13.6 (`pipeline.go:541-547`) — это не опция, а гейт.
**Замера НЕТ:** билингв-редактор на лейблованном контенте не мерян никем (exp04 — короткие SFW-фрагменты, exp10/11 мерили ПЕРЕВОДЧИКОВ и СУДЕЙ). Выбор интерим до замера; замер — довесок к следующему платному прогону (полигон), как и вопросы итерации №2.
- **`glm-5` — естественный дешёвый кандидат, но требует ДАННЫХ, которых у нас нет.** Способность выдаётся только после проверки ToS/AUP Z.AI по первоисточнику (правило двух направлений, `experiments/00-provider-quirks.md`); угадывать ToS запрещено. Если проверка положительна — это ОДНА строка данных (`content_labels: [adult]` у `zai`) и переключение `label_models`, без правки Go.
**Эскалация под лейблом** — цепочка лейбла (`chains.adult: [grok-4.3]`, `pipeline-c1.yaml:101`). Внимание на коллизию: если редактор = grok-4.3, то grok в цепочке ПЕРЕВОДЧИКА остаётся корректен (разные стадии), но валидация обязана запрещать хоп в модель, равную праймари той же стадии (сегодня запрет есть только для `escalate_to`, `pipeline.go:557-558`).
**Судья** — вне пака (eval-сторона, проба-гейт D22.6).
**Риск-заметка (NOTE, не задача пака).** Обоснование «редактору эхо-класс не страшен, вход — русский черновик» в `models.yaml:37` (про GLM) и `configs/pipeline-c1.yaml:62` (про dspro) **устарело против D30.1**: редактор билингв и ВИДИТ плотный CJK (`prompts/zh-ru/editor.md:35-37`). Гейт `echoMineViolation` (`models.go:385-401`) прикрывает только провайдеров с `echoes_when_thinking_off` (сегодня — DeepSeek), у которого thinking всё равно ON. Для выбора редактора под лейблом это значит: у любого reasoning-off/эхо-склонного редактора экспозиция эха НЕ измерена, а D19.1/D22.5 показали, что на архаичном плотном zh даже reasoning-ON не спасает. Ловит эхо-гейт вниз по потоку (`disposition.go:210-214`), но платно.
## §5. B3 — Refusal-политика цепочки (GENERIC, причино-агностичная)
**Что НЕ меняется (семантику молча не двигаем).** Лестница D12/D2 остаётся: ретраим на той же модели только `{length, empty}` (`disposition.go:130-132`); детерминированные контент-провалы (`cjk_artifact`, `excision_suspect`, `loop_degenerate`, `hard_refusal`, `soft_refusal`, `content_filter`) эскалируются (`disposition.go:142-149`); всё остальное — flag+skip. **Отказ по эротике/политике/чему угодно уже сегодня идёт одним путём** — классификатор ловит ФАКТ (finish_reason=refusal / blacklist-совпадение на аномально коротком выходе), причина живёт только в диагностике (`chunk_status.detail`, `request_log.degraded`). Новых классов «18+ отказ» не вводим; `refusal.txt` не переписываем.
**Что добавляется:** источник хопов становится ЦЕПОЧКОЙ ЛЕЙБЛА для лейблованных стадий; путь без лейблов не двигается вовсе.
### Рекомендация B3-R: обход цепочки, включённый только лейблом
- `content_policy.<label>.chain``escalation.chains.<name>` (упорядоченный список). Для стадии, у которой лейбл активен, цепочка ЗАМЕЩАЕТ `escalate_to` целиком (один источник истины на вызов); пустая цепочка у лейблованной стадии = громкая ошибка загрузки.
- Обход: пока флаг эскалируем И остались хопы И бюджет допускает — следующий хоп, ре-гейт (`classifyOutput` уже гоняется на выходе хопа, `stagerun.go:168`), первый OK (или `FlagSanitizerStripped`) — авторитетен; цепочка исчерпана → остаётся флаг праймари → flag+skip (D2). Потолки: длина цепочки + существующий `escalation.budget_usd` + (рекомендую) `content_policy.<label>.max_hops` как явный кноб политики.
- Отказ по потолку USD на хопе — деградирует, как сегодня (`escalation.go:133-143`), но ОСТАНАВЛИВАЕТ обход, а не пропускает один хоп.
- **Обязательные к закрытию формы (иначе это латентная мина, а не механизм)** — 9 несовпадений, найденных инвентаризацией:
1. `escalationOutcome` держит РОВНО ОДНУ попытку (`escalation.go:73-76`), а `stagerun.go:164-166` суммирует только `esc.fb.*` ⇒ хопы 1..N-1 были бы оплачены и НЕ учтены в `chunk_status.cost_usd`. → срез + суммирование.
2. Ось request-hash: у `Request` нет поля хопа (`render.go:235-249`), хоп отличается только `Model` при `Attempt: 0` (`escalation.go:106`). Цепочка, повторяющая модель (или совпадающая с праймари/ретраем), даёт КОЛЛИЗИЮ хеша, а `checkpoints.request_hash` — PRIMARY KEY (`store/migrate.go:46`) ⇒ тихий реплей чужого хопа. → **валидация дедупа цепочки на загрузке** (члены различны, ≠ праймари стадии, ≠ `escalate_to`); НЕ вводить `Hop` в `RequestHash` (это `tm-request-v2`→v3 = инвалидация всех чекпойнтов).
3. `escMu` держится `defer`-ом до конца функции (`escalation.go:123-124`) ⇒ при обходе мьютекс накрыл бы ВСЕ N вызовов и сериализовал волну. → лок/анлок на итерацию.
4. `reachableModels` знает только `st.Model st.EscalateTo repair` (`runner.go:303-321`) ⇒ клиент для хопа ≥2 не собран и `r.client` падает громко посреди волны (`runner.go:347-351`). → расширить (это и есть «третья модельная ось», о которой предупреждает комментарий `runner.go:301-302`).
5. Снапшот фолдит РОВНО ОДНУ эскалационную модель (`snapshot.go:317-335`) ⇒ правка capability хопа №3 была бы ТИХИМ false-hit. → массив фолдов хопов через указатель/omitempty (дисциплина `banknoteSnap`, `snapshot.go:108-112`), nil у книг без лейблов ⇒ байт-идентичность канала A.
6. `chunk_status.escalation_model` — одна строка, и сегодня в неё пишется модель, которую ПОПРОБОВАЛИ, а не которая ОТВЕТИЛА (`stagerun.go:167`). → писать ответившую; путь обхода выводить из чекпойнтов (`escalation=1` + `model_requested`) — прецедент пака-16 «счётчики из durable-чекпойнтов, миграция ОТМЕНЕНА» (`store/ledger.go:99-100`).
7. `Escalations` в read-моделях — булев счётчик на юнит (`status.go:226-228,338-341`) ⇒ обход из 3 хопов отчитается как 1. → оставить как есть, число хопов выводить (omitempty), см. B6.
8. Предикат авторитетности написан под один `fb` (`stagerun.go:168`) → правило терминального выбора «первый OK».
9. Валидация способности смотрит только `st.Model`/`st.EscalateTo` (`pipeline.go:569-576`) ⇒ маршрут по цепочке её обходил бы. → закрывается формулировкой B4 (все достижимые модели).
**Почему всё-таки B3-R, а не «сделаем позже».** `chains.adult` = один элемент (Ф5), поэтому обход сегодня исполняет ≤1 хоп: риск-поверхность мала, а механизм становится честным и `chains` перестаёт быть мёртвыми данными (репо уже наказан за мёртвые кнобы: снятые STMDepth/OverlapTokens, `pipeline.go:96-99`). При этом пп. 2 и 5 обязательны ДАЖЕ для одного хопа, если цепочка вообще становится источником.
### Альтернатива B3-A (санкционированный минимум): пер-лейбловый ОДИН хоп
`label_escalate_to: {adult: <model>}` на стадии; ноль изменений формы снапшота, ноль риска по оси хеша. Дешевле примерно вдвое. Минус: `chains` остаются мёртвыми, и при росте цепочки нужна вторая миграция дизайна. Если владелец хочет минимальный пак — беру A.
### Вопрос владельцу (несущий): редактор под лейблом ПРИКОЛОЧЕН, значит его отказ терминален
D12 разрешает `escalate_to` только на роли translator, и это проверяется на загрузке (`pipeline.go:563-565`; причина — дрейф стиля, 2605.13368). Для лейблованной книги это значит: **отказ РЕДАКТОРА = flag+skip, без хопа**а буквальное чтение B3 («отказ любой причины → следующий хоп») этого не даёт. Варианты: (i) сохранить D12 (моя рекомендация — контракт, риск дрейфа стиля реален, отказ редактора на permissive-модели ожидаемо редок); (ii) разрешить хоп редактора ТОЛЬКО внутри цепочки того же лейбла и ТОЛЬКО для refusal-класса. Молча не выбираю — §6/Q3.
## §6. B4 — Механическая гарантия маршрутизации
**Формулировка инварианта (одна, сильная):** *для книги с набором лейблов L каждая модель, которую прогон может вызвать, обязана нести все лейблы из L.*
Это ToS-гигиена в терминах D39.25: провайдер без способности не должен ПОЛУЧИТЬ контент — независимо от того, откажет ли он. Формулировка «все достижимые модели» строго сильнее старой D4.1-проверки (`pipeline.go:569-576`), потому что покрывает и цепочку, и ремонтную модель, и стадии без оверрайда.
**Рубеж 1 — загрузка (fail-loud, в общий список проблем `LoadPipeline`/`openRunner`):**
1. каждый лейбл книги имеет запись в `content_policy` (иначе громко);
2. `action: terminal` у любого лейбла книги → отказ запуска, ни одного вызова (generic-механизм под политику входа D22.7);
3. `∀ m ∈ reachableModels(): L ⊆ content_labels(m)` — сообщение называет модель, провайдера и НЕДОСТАЮЩИЙ лейбл;
4. дедуп цепочки (B3-R п.2) и запрет хопа в праймари той же стадии;
5. конфликт двух лейблов на одной стадии (B1) — громко.
`CheckAdultChannel` (`pipeline.go:336-346`) при этом ИСЧЕЗАЕТ как отдельная сущность: п.3 закрывает её задачу целиком и без слова «adult» (сегодня она лишь требует наличия какой-нибудь adult-стадии — слабее: не проверяет, что ВСЕ вызовы способны).
**Рубеж 2 — рантайм-ассерт в точке-удавке.** В `runAttempt` (`stagerun.go:326`) ДО `Reserve` (то есть деньги даже не резервируются): `labelsFor(ch) ⊆ content_labels(model)`, иначе громкая инфра-ошибка (книга durable-паузится, как на любой инфра-ошибке, D4) — это ассерт программной/конфигурационной ошибки, а НЕ контентный вердикт, поэтому не флаг.
**Форма, которую рекомендую (компиль-энфорс, а не дисциплина):** заменить `r.client(model)` на `r.clientFor(model, labels)` (`runner.go:344-352`) — тогда НИ ОДИН будущий путь на провод не сможет получить клиента, не объявив, какой контент он отправляет. Это ровно стиль репо: `buildClients` уже громко падает на неперечисленной модели, а `foldModelWire` был вынесен, чтобы третья модельная ось не расползлась копипастой (`snapshot.go:488-494`). Ассерт обязан ключеваться на данных книги/чанка, а не на стадии — из-за Ф4 (ремонт теряет `Channel`).
**Тесты — обязательны оба рубежа (правило 11: сквозняк через настоящий драйвер, не чтение диффа):**
- негативный load-тест: книга с лейблом + модель без способности → `NewRunner` падает, сообщение называет модель и лейбл;
- рантайм-ассерт: конструируем ситуацию, обходящую загрузку (пост-конструкционный флип поля, прецедент `repair_integration_test.go:336`) → ассерт стреляет ДО резервации (проверяем: `spend` не двинулся);
- позитивный сквозняк: лейблованная книга с синтетическим лейблом идёт через mock-провайдера, отказ на праймари → хоп по цепочке → OK → ре-гейт → `DispOK`; и «хоп тоже отказал» → flag+skip, edit skipped, exit 2. **Этот сценарий сегодня НЕ покрыт ни для одного refusal-класса** (Ф8) — то есть тест закрывает и старую дыру.
**Риски B4:** (а) `reachableModels` — единственный источник истины о «достижимом»; если новая ось вызовов появится и его не расширит, инвариант обойдут — прикрыто рантайм-ассертом (второй рубеж) и громким `clientFor`; (б) ассерт на горячем пути — стоимость O(|L|) сравнений строк на вызов, шум нулевой рядом с сетевым вызовом; (в) `local`-провайдер несёт способность и $0 — соблазн «прогнать лейблованное локально» существует и легитимен (D19: abliterated-скрининг), но качество 8b как несущего редактора не годится (B2).
## §7. B5 — Wire / снапшот / резюм / фикстуры
**Р1. Лейблы получают ЯВНЫЙ слой снапшота — это требование контракта, а не выбор.** D15.2 §13-Q3 ратифицировано: «Первый рантайм-потребитель `Adult` (если появится) обязан ЯВНО вернуть его в `wireSnapshotID`/`verdictSnapshotID`». Пак-17 и есть тот первый потребитель. Назначение: (а) НАБОР ЛЕЙБЛОВ едет в `BriefHash` (wire-ось, транзитивно во оба волновых снапшота, `snapshot.go:413`); (б) РЕШЕНИЕ маршрутизации едет через `stageSnap.Model`, который становится РЕЗОЛВНУТОЙ моделью (routing применяется ДО рендера снапшота) — то есть в снапшоте лежит модель, которую реально позовут, как и сегодня по смыслу; (в) цепочка хопов — массивом фолдов (B3-R п.5). Отдельный фолд самих `content_labels(модель)` НЕ нужен: их эффект целиком захвачен резолвнутой моделью + её wire-фолдом, а изъятие способности падает ГРОМКО на загрузке (никогда не «тихо перемаршрутизируем»).
**Р2. Байт-идентичность канала A достигается ровно одним способом — и он проверен численно.** Канон `BriefHash` (`book.go:214-228`) сегодня рендерит `"adult":false` СЕДЬМЫМ полем. Мои замеры (M1):
| состояние канона | brief_hash | следствие |
|---|---|---|
| сегодня | `c2021b1b…cab0df` (совпадает с `testdata/golden/capture.golden:6`) | — |
| поле убрано | `58883ff5…b39822` | `--resnapshot` для КАЖДОЙ книги, пере-капчер golden, потеря $0-резюма стенда |
| `content_labels: null` на месте поля | `29a3031d…8c87b` | то же |
| omitempty-хвост, лейблов нет | **байт-в-байт `c2021b1b…`** | ноль пере-оплаты, golden не трогается |
| omitempty-хвост, `["adult"]` | `0e2f3111…c9d2c` | лейблованная книга — другой бриф (корректно и громко) |
**Рекомендация:** `adult:` становится ОТСТАВНЫМ ключом книги (`LegacyAdult bool yaml:"adult"`, дисциплина `Stage.LegacyPrompt`, `pipeline.go:134-142`): значение `true` = громкая ошибка миграции, называющая `content_labels: [adult]`; `false`/отсутствие — терпимо (это ровно то, что стоит в 5 стендовых book.yaml и в `example/book.yaml:9`, а стенд вне git ⇒ терпимость экономит владельцу правки и не даёт ложного «книга не грузится»). В каноне поле остаётся на своём 7-м месте с постоянным `false` (заморожённая раскладка brief-канона, документируется как таковая), а `content_labels []string json:"content_labels,omitempty"` дописывается В КОНЕЦ. Условия байт-идентичности (все обязательны): порядок полей не меняется · тег не переименовывается · новое поле с `omitempty` · `LoadBook` НЕ дефолтит лейблы ни во что непустое (в отличие от `Encoding`/`YoPolicy`, `book.go:172-187`).
**Альтернатива:** просто выкинуть поле и классифицировать сдвиг как «version-only». Отвергаю: цена — потеря $0-резюма драфта на стендовых книгах (Ф7), а открытая петля пака-16 (замер остатка) поедет именно на них.
**Р3. Резюм.** Лейблованные вызовы — обычные чекпойнты: `Model` уже в `RequestHash` (`render.go:271-274`), поэтому смена маршрута = честный промах чекпойнта на новую модель, а не ложное попадание. Правка лейбла = `--resnapshot` (через `BriefHash`) — громко и с пере-оплатой; операционное правило: **лейблы объявляются ДО первого платного вызова** (в v1 это ручное действие владельца при заводке книги). Пер-главность этого не улучшает (см. B1).
**Р4. Ремонт (пак-16) под лейблом.** `gates.repair.model` попадает в `reachableModels` (`runner.go:319-321`) ⇒ автоматически подпадает под инвариант B4. Отдельно отмечаю существующую асимметрию фолда: `repairSnap.ModelWire` берёт только capability (`snapshot.go:171`), без провайдерской тройки и `extra_body`, в отличие от праймари/эскалации — не задача пака, но если ремонт когда-нибудь поедет под лейблом с другой моделью, эту асимметрию надо закрыть.
**Р5. Фикстуры — только синтетика.** Реального 18+ текста в репо не появляется. Отказ моделируется маркером-прокси ровно так, как это уже делает golden: `goldenRefusalMarker` → «Не могу помочь с этим фрагментом.» + `finish_reason=refusal` (`golden_test.go:87-89`). Значение лейбла в тестах — СИНТЕТИЧЕСКОЕ (проверка генеральности, B1); отдельный дата-тест утверждает, что шиппинг-`models.yaml` объявляет `adult` ровно у трёх провайдеров, которые сегодня несут `permissive` — это утверждение о данных, без контента.
**Р6. Что фолдить НЕ надо** (чтобы не переплатить впустую): `content_policy` целиком (реестр может содержать лейблы, которых у книги нет) · `max_hops`/бюджеты (деньги — не wire и не вердикт, прецедент `BudgetUSD` не фолдится, `snapshot.go` repairSnap) · `content_labels` провайдеров (Р1).
## §8. B6 — Отчётность
**Дёшево сейчас (ноль миграций, дисциплина omitempty — «книга, не использовавшая фичу, байт-идентична прошлому», `quality.go:79-84`):**
1. **Шапка `tmctl report`/`status`:** резолвнутые лейблы книги + таблица маршрутизации (стадия → модель под лейблом) — чистая проекция конфига, $0, без схемы.
2. **QualityReport:** `Hops int` (число фактических хопов, выводится из чекпойнтов `escalation=1`) — закрывает искажение «булев `Escalations` = 1 при обходе» (B3-R п.7). Всё omitempty.
3. **Атрибуция денег без миграции:** новый read-only агрегат `SpendByModel(bookID)` (`GROUP BY model_requested` — таких запросов сегодня нет ни одного, `store/*.go`), а сопоставление «какие модели лейбл-маршрутизированы» делается на стороне конфига. Даёт ответ «сколько стоил лейблованный маршрут», не трогая ни `spend`, ни ceiling-арифметику.
**Чего делать НЕ надо:** (а) не переиспользовать `Checkpoint.Escalation` (конфликт с `escalation.budget_usd`); (б) НЕ вводить синтетическую роль под лейбл — `Role` есть ось `RequestHash` (`render.go:241`), новая роль = новый хеш = новый ПЛАТНЫЙ вызов, а лейбл-маршрутизация обязана сохранить роль `translator`/`editor` (прецедент `roleRepair` легитимен ровно потому, что это НОВЫЙ вызов).
4. **Паспорт главы** (`ChapterPassport`, `status.go:35-55`) — сегодня менять не нужно: он агрегирует диспозиции и деньги, а «через какого провайдера шла глава» выводится из `chunk_status`+`request_log` при необходимости.
**В очередь (не этот пак):** пер-хоповая телеметрия пути (`[m1,m2,m3]`) как отдельная проекция · колонка лейблов на `chunk_status` (миграция v11 — не стоит, выводимо) · пометка в экспорте/ридере — это фронт по D39.25.
## §9. Открытые петли и вопросы владельцу (диспозиция обязательна — норма 8)
| # | Вопрос | Почему решает владелец | Моя рекомендация |
|---|---|---|---|
| Q1 | Гранулярность v1: книга или диапазоны глав? | Определяет качество/COGS: книга-целиком гонит ВЕСЬ объём через permissive-тир (в т.ч. редактора) | книга (нет источника данных для глав; правка лейбла — `--resnapshot` при любой гранулярности) |
| Q2 | Редактор под лейблом: grok-4.3 интерим? | Деньги (×~2.9 output к dspro + аддитивный reasoning-буфер) против качества; замера нет | grok-4.3 ИНТЕРИМ; glm-5 — только после проверки ToS Z.AI по первоисточнику (данные, не код) |
| Q3 | Отказ РЕДАКТОРА под лейблом: терминален (D12 pinned) или хоп внутри цепочки лейбла? | Дрейф стиля против потери юнита | сохранить D12 (терминален) |
| Q4 | Скоуп B3: обход цепочки (B3-R) или пер-лейбловый один хоп (B3-A)? | Объём пака | B3-R (цепочка = 1 элемент сегодня ⇒ риск мал, `chains` перестают быть мёртвыми) |
| Q5 | `chains.default` (3 модели) остаётся мёртвым для канала A? | Сделать живым = изменение денег канала A и не байт-идентично | оставить мёртвым в этом паке; отдельное решение |
| Q6 | Нужно ли значение `action: terminal` уже сейчас (нет данных под него)? | Политика входа — человеческий уровень | построить механизм (он тривиален), значение не заводить |
## §10. Находки-побочки (док↔код; чинятся в фазе 2 попутно)
1. `pipeline.go:331-332` — «Adult … never a hash input» **неверно** (Ф6, проверено численно). Комментарий уезжает вместе с `CheckAdultChannel`.
2. `models.yaml:37` и `configs/pipeline-c1.yaml:62` — «вход редактора — русский черновик, эхо-класс вырожден» **устарело против D30.1** (редактор билингв, `prompts/zh-ru/editor.md:35-37`). Правка комментариев + NOTE о неизмеренной экспозиции (§4).
3. `configs/pipeline-c1.yaml:7-8` — «channel-aware фильтр цепочек (Anthropic вне 18+)» описывает несуществующий механизм и провайдера, удалённого из стека (`models.yaml:70`). Переписать под лейблы.
4. `escalation.go:15` — «Channel B (18+) Ф2 extends EXACTLY this file (permissive chains)» — после пака формулировка станет generic.
5. `stagerun.go:167` пишет в `escalation_model` ПОПРОБОВАННУЮ модель, не ответившую (B3-R п.6) — латентная неточность телеметрии уже сегодня, чинится тем же диффом.
## §11. Адверсариальный селф-ревью (5 линз промта)
**Линза 1 — L3-линия и гардрейлы.** Классификатор не проектируется и не упоминается как задача; `refusal_corpus` не открывался; реальный 18+ текст в репо/фикстуры не заводится (Р5). Механизм `action: terminal` — это ОТКАЗ ВХОДА по данным, а не автоматическая классификация уровня, т.е. он усиливает L3-политику, не подменяя её (D22.7 п.г остаётся невыполненным намеренно). Риск, который вижу: `terminal` может быть прочитан как «мы умеем детектить L3» — поэтому в §9/Q6 предлагаю построить механизм, но НЕ заводить значение.
**Линза 2 — маршрутизация fail-loud.** Инвариант «все достижимые модели несут все лейблы книги» строго сильнее D4.1-проверки; ассерт стоит в единственной точке-удавке, доказанной греп-исчерпанием (Ф3), и ключуется на данных книги, а не на стадии (Ф4 — иначе ремонт слеп). Слабое место: `reachableModels` как источник истины — прикрыто вторым рубежом и `clientFor`. Второе слабое место: `local`-провайдер способен и $0 (см. риск B4в).
**Линза 3 — байт-точность канала A.** Единственный путь без пере-оплаты найден и проверен численно (Р2, M1); все прочие варианты канона двигают хеш. Условия байт-идентичности выписаны в виде проверяемого списка; фазе 2 предписан прямой тест исполнением: `sha256` фикстуры golden не меняется и `git status` по `testdata/` пуст (как в приёмке пака-16). Опасность, которую держу на виду: любое непреднамеренное дефолтирование лейблов (например «sfw» по умолчанию) немедленно ломает байт-идентичность — поэтому пустой набор обязан оставаться пустым.
**Линза 4 — деньги и резюм.** Ни одной новой оси `RequestHash` (осознанный отказ от `Hop` — это v3 и инвалидация всех чекпойнтов, B3-R п.2). Обход цепочки не сбрасывает retry-бюджет (D12) и наследует пре-хоповый софт-кап; отмечено, что при обходе перерасход деградирует с «≤1 хоп» до «≤1 цепочка» — поэтому `max_hops` вынесен в политику явным кнобом. Учёт стоимости всех хопов — п.1 обязательного списка (иначе оплаченные хопы не попали бы в `chunk_status.cost_usd`). Атрибуция — без миграций (B6.3). $0-резюм стенда защищён (Ф7/Р2).
**Линза 5 — общность §0.** Ни одного нового Go-идентификатора/ветки с именем adult/18+/канал B: значения приходят строками из данных, движок сравнивает множества. Мысленный тест второго лейбла проходит на трёх правках данных (B1) и получает МЕХАНИЧЕСКУЮ проверку — сквозной тест на синтетическом значении лейбла. Остаточная честность: слово «adult» останется в (а) отставном ключе книги `yaml:"adult"`/`json:"adult"` (ровно чтобы миграция была громкой и канон не сдвинулся) и (б) данных `models.yaml`/`content_policy`. Первое — цена денег (Р2), второе — по директиве и есть данные. Считаю это соответствием, а не исключением, но это ровно тот пункт, который владельцу стоит ратифицировать явно.
**Что мой дизайн НЕ закрывает (честно):** судью 18+ (eval) · авто-детекцию лейблов · L3 · ja→ru §B5 · унаследованный ru-target-долг (D39.24 §15.3 — ограничение остаётся) · измеренный выбор редактора под лейблом (интерим до замера).
## §12. Манифест «заявление = команда» (норма 7; приёмка ре-ранит)
| # | Заявление | Команда |
|---|---|---|
| M1 | brief-канон golden = `c2021b1b…cab0df`; удаление `"adult":false``58883ff5…`; omitempty-хвост при пустых лейблах → байт-в-байт тот же хеш | `python3 -c` с каноном из `book.go:214-228` (см. §7 таблицу) + `sed -n '6p' backend/internal/pipeline/testdata/golden/capture.golden` |
| M2 | `escalation.chains` не имеет рантайм-потребителей | `grep -rn "Escal.Chains\|Escal\." --include='*.go' backend/internal \| grep -v _test.go` |
| M3 | единственный `client.Complete` в проде — `stagerun.go:442`; единственный `r.client(``stagerun.go:410` | `grep -rn "\.Complete(" --include='*.go' backend \| grep -v _test.go` ; `grep -rn "r\.client(" --include='*.go' backend \| grep -v _test.go` |
| M4 | failover — мёртвый код | `grep -rn "NewFailoverClient\|FailoverConfig" --include='*.go' backend \| grep -v _test.go` |
| M5 | `permissive: true` ровно у 3 провайдеров | `grep -n "permissive" backend/configs/models.yaml` |
| M6 | `chains.adult` = один элемент во всех шиппинг-конфигах | `grep -rn "adult:" backend/configs/` |
| M7 | ни один шиппинг-конфиг не объявляет `channel:`; `adult: true` нет нигде | `grep -rnE "^\s*channel:" backend/configs backend/internal/pipeline/testdata` ; `grep -rnE "^\s*adult:\s*true" .` |
| M8 | adult/channel/permissive трогают ровно 2 теста | `grep -rn "adult\|Adult\|Permissive\|Channel: \"" --include='*_test.go' backend \| grep -v banknote \| grep -v miner` |
| M9 | `Stage.Channel` не фолдится в снапшот | `grep -n "Channel" backend/internal/pipeline/snapshot.go` |
| M10 | база тестов 433 (`^func Test`, греп — фаза 2 обязана пере-считать ИСПОЛНЕНИЕМ) | `grep -rn "^func Test" backend \| wc -l` |
| M11 | редактор билингв: подставляется исходник | `grep -n "{{text}}\|Исходный текст" backend/prompts/zh-ru/editor.md` |
| M12 | дерево чистое, сессия не коммитила | `git status --porcelain` |
## §13. Список работ фазы 2 (для разметки владельцем «ровно так» / «реши сам»)
1. Данные: `permissive``content_labels: [adult]` (3 строки models.yaml) + отставной `permissive` громко; `content_policy` в шиппинг-конфиги (пустой/отсутствующий = сегодняшнее поведение).
2. Книга: `content_labels` (lower+sort, omitempty-хвост канона), отставной `adult:` (true → громко), канон заморожен.
3. Резолвер `labelsFor(chunk)` (v1 — набор книги) + пер-стадийный `label_models` + резолв ДО рендера снапшота.
4. Валидация (5 проверок §6 рубеж 1) + удаление `CheckAdultChannel`.
5. Ассерт `clientFor(model, labels)` + расширение `reachableModels`.
6. B3-R: обход цепочки с 9 закрытыми формами (или B3-A по решению владельца).
7. Снапшот: массив фолдов хопов через указатель/omitempty; `stageSnap.Model` = резолвнутая модель.
8. Отчётность: шапка отчёта, `Hops`, `SpendByModel`.
9. Тесты: негативный load, рантайм-ассерт до резервации, сквозняк refusal→хоп→OK→ре-гейт, сквозняк «хоп тоже отказал», генеральность на СИНТЕТИЧЕСКОМ лейбле, второй лейбл только данными.
10. Хвосты общности из петель: Detail-строки чекеров из данных · множитель ×2 `lintTimeUnits` в ключ данных.
11. Док-правки §10 (5 расхождений).
Инварианты фазы 2: golden байт-идентичен (без пере-капчера) · парити EXACT · `-race` · сэндбокс-репродукции через настоящий драйвер (правило 11) · дифф `^func Test` ИСПОЛНЕНИЕМ · сессия не коммитит.
---
# §14. ПОСТ-ПАНЕЛЬНАЯ РЕВИЗИЯ (отдельный слой; тело §013 НЕ переписано — метод пака-16, D39.24)
> Прогнано 11 адверсариальных агентов: 5 предписанных линз (L3/гардрейлы · маршрутизация-fail-loud · байт-точность · деньги/резюм · общность §0) + 6 скептиков с мандатом «ОПРОВЕРГНИ ПО КОДУ». Вердикты: 4× ACCEPT_WITH_FIXES, 2× REJECT, 2× REFUTED, 1× PARTIALLY_REFUTED (по одному скептику ответ не пришёл — его клейм и так трижды проверен: мой замер + две независимые ре-деривации всех пяти дайджестов M1). **Три решения тела ОТМЕНЯЮ, два рекомендации ПЕРЕВОРАЧИВАЮ.** Ниже — только то, что я проверил сам; клеймы агентов, оказавшиеся неверными, помечены как отклонённые.
## §14.1. ОТМЕНА №1 (CRIT, несущая): лейблы НЕ идут в `BriefHash`
Тело (§7/Р1а, Р2) предлагало вести набор лейблов omitempty-хвостом brief-канона. **Это противоречит ратифицированному D20.2-Q1 — с тем же самым рабочим примером.** `docs/architecture/05-decisions-log.md:245`: «**Q1 — пер-стадийная гранулярность wireSnapshotID, ДА** (живой довод: вероятная смена редактора glm-5→**grok-4.3** при book-global пере-оплатила бы весь translator-бэклог)». Пак-17 — ровно этот сценарий: лейбл маршрутизирует РЕДАКТОРА на grok-4.3. `BriefHash` book-global (`book.go:214-228``snapshot.go:413` в общем билдере обеих волн), поэтому лейбл в каноне пере-оплатил бы и ДРАФТ-волну.
Второе, независимое основание: `backend/docs/D15.2-content-addressed-resume-spec.md:188``brief_hash` в v2 **задропан**`content_hash`, и «ЛЮБОЕ brief-поле, потребляемое ВНЕ рендера, обязано получить явную судьбу»; два ратифицированных прецедента этого же класса назначены явно «**verdictSnapshotID** (не через `brief_hash`)» (`:227` YoPolicy, `:228` StyleAllowlist). Лейблы ни в один промпт не рендерятся ⇒ `content_hash` их не поймает ⇒ «через BriefHash» — это ровно тот путь, который §13-Q3 исключает, с отложенным окном тихой потери.
**Исправленное назначение слоя (это и есть разрядка §13-Q3):** набора лейблов в хешах НЕТ; вся его wire-судьба — **РЕЗОЛВНУТАЯ модель стадии + фолд её wire-формы, пер-стадийно и пер-волново** (`stageSnap`, `snapshot.go:278-336`; фильтр стадий по волне `snapshot.go:253-262`). Следствия, которые это покупает: лейбл, маршрутизирующий только редактора, двигает ТОЛЬКО edit-волновой снапшот — оплаченный драфт резюмится за $0 (ровно дисциплина repair-фолда, `snapshot.go:224-227`: «Folding it into both would move the DRAFT-wave snapshot … destroying the $0 draft resume»). Провенанс набора лейблов — НЕ хеш, а шапка отчёта (B6.1). Тихой расходимости не возникает: единственные эффекты лейбла вне резолвнутой модели — load-time (реестр, `terminal`, capability), а они падают ГРОМКО на загрузке.
**Что из Р2 ОСТАЁТСЯ в силе:** заморозка `"adult":false` на 7-й позиции канона (байт-идентичность, M1) — она теперь ЕДИНСТВЕННОЕ, что делается с каноном, и omitempty-хвост не нужен вовсе. Условия байт-идентичности из §7 сохраняются + **шестое условие** (находка панели): поля `stageSnap.{EscalateTo,EscalateCapability,EscalateExtra,EscalateProviderTemp,EscalateProviderMaxTok,EscalateProviderModel}` сохраняют имя, тег, позицию и правило заполнения `if st.EscalateTo != ""` (`snapshot.go:323-335,463-472`); любой фолд хопов — строго АДДИТИВНЫЙ хвост через указатель/omitempty. Иначе «унификация оси хопов» вынесет 4 отрендеренных ключа из payload golden-фикстуры (`testdata/golden/pipeline.yaml:11` объявляет `escalate_to: fake-fallback`) и двинет оба снапшота.
## §14.2. ОТМЕНА №2 (CRIT, конструктивная): резолв маршрутизации ДО валидации; «reachable» становится ЛЕЙБЛ-ЗАВИСИМЫМ
Тело (§6 рубеж-1 п.3 + §13 п.5 «расширить `reachableModels`») задаёт инвариант над СЫРЫМ `reachableModels()` (`runner.go:311-314` безусловно добавляет `st.Model` и `st.EscalateTo`). Проверил: для лейблованной книги на шиппинг-конфиге это **ложное падение загрузки**`deepseek-v4-flash`/`deepseek-v4-pro` (`configs/pipeline-c1.yaml:36,51,63`) не несут способности (`models.yaml:26` без флага), хотя под лейблом они замещены. Три агента сошлись на этом независимо; я подтверждаю по коду. Плюс тот же дефект имеет ещё пять проявлений, каждое проверено:
- **Гейт аддитивного reasoning обходится** (`pipeline.go:541-547` смотрит только `st.Model`/`st.EscalateTo`): `edit{model: deepseek-v4-pro (subset), reasoning_max_tokens: 0, label_models:{adult: grok-4.3}}` грузится, а в рантайме `AdditiveReasoningTokens(grok,…,0)=0` (`models.go:298-305`) ⇒ резерв не видит reasoning ⇒ потолок книги слепнет. Это ровно дыра D6.2/D13.6 — и она открывается на РЕКОМЕНДОВАННОМ мной редакторе.
- **Коллизия request_hash выживает** заявленный дедуп: все существующие «та же модель» гарды сравнивают КОНФИГОВЫЙ `st.Model` (`pipeline.go:557-558`). При `label_models:{adult: X}` и хопе в `X` хоп строит ТОТ ЖЕ хеш (`Attempt: 0`, тот же stage/role/temp/reasoning/snapshot/msgs, `escalation.go:105-109`) ⇒ `GetCheckpoint` ПОПАДАЕТ ⇒ «хоп» бесплатно реплеит собственный отказ праймари, `stagerun.go:164` двойным счётом добавляет `esc.fb.cumCost` в `chunk_status.cost_usd`, `Escalated=true` пишется, а отказ на лейблованной книге НЕ пере-маршрутизируется. `checkpoints.request_hash` — PRIMARY KEY (`store/migrate.go:46`), то есть это тихий реплей, не ошибка.
- **`CheckKeys`** (`models.go:507-518`) под `budget_usd>0` требует ключи ВСЕХ цепочек — для лейблованной книги это ключи цепочек, которые она не пройдёт.
- **Дедуп цепочки, заданный неограниченно**, отвергает все 4 шиппинг- и 3 стендовых конфига (`chains.default` содержит `deepseek-v4-pro`, равный `escalate_to` драфта: `pipeline-c1.yaml:100` vs `:51`).
- **`gates.repair.model`** для лейблованной книги обязана быть способной И не-аддитивной (`pipeline.go:606-610`) ⇒ из способных остаются только `mistral-large-2512` (исключён D39.20 как несущий) и `local-qwen3-8b`; grok падает на загрузке. Поле глобальное и фолдится (`snapshot.go:143`) ⇒ правка под лейблованную книгу инвалидирует ремонт канала A.
**Исправленная конструкция (закрывает все шесть одним ходом):** **РЕЗОЛВ → ЗАТЕМ ВАЛИДАЦИЯ → ЗАТЕМ ВСЁ ЧИТАЕТ РЕЗОЛВНУТОЕ.**
1. Лейблы книги приходят в `LoadPipeline(path, models, pair, labels)` — тем же способом, каким уже приходит `pair` (один прод-вызов `runner.go:137`, 22 тестовых). Это сохраняет доктрину «все проблемы конфига одним списком» (`models.go:6-7`), которую вариант «второй метод после загрузки» ломает.
2. Резолв заполняет `yaml:"-"`-поля стадии (`ResolvedModel`, `ResolvedHop`) — установившаяся конвенция резолвнутых полей (`pipeline.go:145` `PromptPath`, `:217` `PromptsDir`).
3. ВСЕ существующие гарды пере-прогоняются над резолвнутым: «хоп ≠ праймари», аддитивный буфер, роль-скоуп D12, capability-инвариант, `CheckKeys`, `reachableModels`, фолд снапшота, рантайм.
4. «Достижимое» для лейблованной книги = `{резолвнутые модели стадий} {хоп(и), выбранные ЛЕЙБЛОМ} {repair-модель}`; `st.EscalateTo` и `chains.default` под лейблом НЕДОСТИЖИМЫ по построению. Дедуп применяется ТОЛЬКО к цепочкам, выбранным политикой лейблов книги — `chains.default` не валидируется (согласовано с Q5).
5. Резолв обязан работать и на READ-путях: `status`/`export` пере-считывают `snapshotIDForWave` для дрейф-детекции (`export.go:274`), значит без резолва их проекция разошлась бы с translate.
**Отклоняю** сопутствующий клейм панели «ни один тест не грузит шиппинг-конфиги, дефект всплыл бы только на платном прогоне»: грузят — `prompt_pack_test.go:178` (c1) и `:232` (все пять), `echo_mine_test.go:196` (c1/c2), `:245` (армы). Неограниченный дедуп упал бы в CI, а не на прогоне. Фикс остаётся, страшилка — нет.
## §14.3. ПЕРЕВОРОТ РЕКОМЕНДАЦИИ №1 (Q4): пак-17 берёт ОДИН хоп из цепочки лейбла, обход цепочки — отложен
Тело рекомендовало B3-R (полный обход). Панель довела список обязательных к закрытию форм с 9 до **≥15** (добавлены: гейт аддитивного reasoning над членами цепочки · `CheckKeys` над резолвнутым набором · `budget_usd: 0` как молчаливый выключатель всего механизма · взаимодействие пер-хопового реплея с бюджетом · роль-скоуп D12 · коллизия «две метки → две цепочки с общим членом»). Плюс `escMu`: тело предлагало лок на итерацию — **отменяю**, это ломает ратифицированную гарантию «перерасход ≤ 1 хоп», запинённую тестом `waverun_test.go:333` (бюджет 0.001 < один хоп 0.00182, `workers: 4`, ассерт равенства ровно одному хопу). При этом `Waves.Workers` по умолчанию 1 (`pipeline.go:414-415`) и ни один шиппинг-конфиг не объявляет `waves:` то есть оптимизируемая конкуренция вне тестов не существует.
**Исправленная рекомендация B3-A (минимальная, честная, без мёртвых данных):** источник хопа под лейблом `escalation.chains.<name>`, но исполняется **ОДИН хоп = `chain[0]`**; `len(chain) > 1` = ГРОМКАЯ ошибка загрузки «мульти-хоп не построен» (никакого тихого усечения). Сегодня `chains.adult: [grok-4.3]` ровно один элемент во всех 4 шиппинг- и 3 стендовых конфигах, поэтому данные уже совместимы. Что это даёт: ноль изменений ФОРМЫ снапшота (существующего одиночного `Escalate*`-фолда достаточно) · ноль риска по оси хеша сверх §14.2 · сохранённая гарантия `escMu` (лок как сегодня) · `chains` перестают быть мёртвыми данными. Полный обход остаётся спроектированным 5 + 6 добавленных форм) и ждёт данных, которые его требуют.
**Дополнительно обязательно (находка панели, принята):** для книги, чей лейбл даёт `action: route`, требовать на загрузке `escalation.budget_usd > 0`, называя лейбл и цепочку. Иначе (шиппинг-дефолт `budget_usd: 0`, `pipeline-c1.yaml:106`) все проверки зелены, хопов НОЛЬ, первый отказ = flag+skip, и лейблованные юниты теряются молча класс «гейт, который не может выстрелить», который репо уже отвергает для coverage/repair.
## §14.4. ПЕРЕВОРОТ РЕКОМЕНДАЦИИ №2 (Q1): аргумент за книжный уровень был неверным; вывод остаётся, но по ДРУГОЙ причине
Тело обосновывало книжный уровень «нет источника данных». **Опровергнуто:** глава-диапазоны установившаяся идиома книжных данных (`internal/seed/seed.go:35-36` `until_ch`, потребляется `internal/membank/memory.go:507-511`), то есть ручная разметка арок операбельна. Второй аргумент тела правка лейбла = пере-оплата при любой гранулярности») **тоже опровергнут** после §14.1 пер-стадийный фолд делает пере-оплату локальной. И COGS-аргумент панели реален: лейблованная книга целиком едет по цене permissive-тира (draft mistral $0.5/$1.5 против flash $0.14/$0.28; editor grok $1.25/$2.50 + аддитивный reasoning против dspro $0.435/$0.87 `models.yaml:119,131,183,220`).
**Но вывод я сохраняю, и вот НЕСУЩИЙ аргумент, которого не было ни в теле, ни у панели:** автоматического скрина контента НЕТ и не будет в этом паке (L3-линия). Поэтому при пер-главной разметке ToS-гарантия «провайдер без способности не ПОЛУЧИТ такой контент» деградирует до точности ручного чтения владельцем каждой главы: одна пропущенная сцена = лейблованный контент ушёл к не-способному провайдеру ровно то, что D39.25 запрещает. При книжном уровне утечка структурно невозможна. То есть выбор не «дорого против дешёво», а **«гарантия против стоимости»**, и это решение владельца (Q1), а не моё.
Дополнительно (находка панели, принята): B1(в) «пер-главность потом = данные + один фолд» **неверно** снапшот считается ОДИН раз на волну до пер-чанкового цикла (`waverun.go:104,114,150,163`), поэтому пер-главная маршрутизация требует снять фолд модели с book-global оси (карта лейблмодель вместо скаляра) = переархитектура фолда, не «один фолд». Резолвер v1 объявляется КНИЖНЫМ (`labelsFor()` без аргумента, с ассертом «в v1 книго-константа»), чтобы не обещать формой того, чего механизм не держит.
## §14.5. Принятые точечные фиксы дизайна (проверены по коду)
1. **Реестр политик — УПОРЯДОЧЕННЫЙ СПИСОК, не map.** Тело оставило неопределённым случай ДВУХ активных лейблов на одной книге (две цепочки и два `max_hops` на один вызов), а map-порядок в Go не хранится то есть Go выбирал бы итерацией. Форма: `content_policy: [{label: …, action: route, chain: …}, …]`, правило «первая объявленная политика, претендующая на стадию, выигрывает», громкая ошибка при равном притязании. Эффективный список хопов упорядоченное объединение по активным лейблам с дедупом ПОСЛЕ резолва (иначе `chains.adult:[grok]` + `chains.archaic:[grok]` дают два идентичных хеша второй хоп бесплатный реплей первого).
2. **Пер-модельный оверрайд способности — `*[]string`, и только СУЖАЕТ.** Копирование дисциплины `capabilities` пусто = наследуй», `models.go:419-441`) на список означает, что модель НЕ МОЖЕТ вычесть лейбл провайдера: `content_labels: []` неотличим от «не задано» модель наследует способность, -инвариант зелен, контент уходит к явно исключённой модели. Форма: указатель (nil = наследовать, непустой указатель на пустой список = «ни одного»), плюс правило «модельный набор провайдерского» (расширять нельзя) и load-тест на вычитание.
3. **Разные имена ключей для двух сторон** (панель права: один ключ конфлатит «свойство контента» и «разрешение получателя»): книга `content_labels:`, провайдер/модель `accepts_labels:`. Инвариант читается `content_labels(книга) ⊆ accepts_labels(модель)`.
4. **Go-имя отставного ключа — без слова adult.** Буква D39.25 ни один Go-идентификатор не именуется adult/18+/каналом B») требует: поле `LegacyContentFlag bool` с ТЕГАМИ `yaml:"adult"`/`json:"adult"` (тег = замороженный ключ данных, идентификатор generic). То же для канона.
5. **Две РАЗНЫЕ семантики отставки — намеренно, с причиной в коде.** `permissive` в `models.yaml` громко ПРИ НАЛИЧИИ (один файл репо, пак его и миграцирует). `adult:` в book.yaml громко только на `true`, `false`/отсутствие терпимо: ключ несут **13 файлов вне git** (проверено мной: 5 book.yaml стенда + 8 `exp15/anchors/*.yaml`) + `example/book.yaml:9`. Тело называло 5 **недоучёт, исправлен**; и он усиливает вывод: терпимость обязательна, а не удобна. Обязателен `LoadBook`-тест на все три формы (нет / `false` / `true`), потому что golden-фикстура ключа не несёт вовсе и ветку терпимости не упражняет.
6. **Ассерт: место оставляю, тест исправляю.** Панель требовала перенести ассерт в самое начало `runAttempt` (до `GetCheckpoint`). **Отклоняю:** реплей чекпойнта провайдеру ничего не отправляет (ToS-экспозиции нет), а перенос отнял бы $0-реплей уже оплаченной работы при правке данных, которую загрузка и так ловит громко. **Принимаю тестовую часть:** негативный тест обязан гонять СВЕЖИЙ вызов (без чекпойнта) и ассертить идентичность ОШИБКИ, а не дельту `spend` иначе он зелен вакуумно (на реплее `spend` тоже не двигается).
7. **Второй путь на провод существует в дереве — в тестах.** `BuildClient` экспортирован (`clients.go:15`) и `live_conformance_test.go:70,76` вызывает `Complete` против БОЕВОГО `configs/models.yaml`. Мой манифест M3 с `grep -v _test.go` структурно слеп к нему. Фиксы: (а) механический гард-тест `grep` по `\.Complete(` вне `internal/llm` и вне `stagerun.go` обязан быть ПУСТ, включая тесты; (б) требовать лейблы в подписи сема (`clientFor`/`BuildClient`), чтобы новый путь физически не мог не объявить контент.
8. **Мёртвый failover — решить, а не «отметить».** `failover.go:114,130,148` пересылают ТОТ ЖЕ `req` другому клиенту, а собственная шапка файла (`:19-24`) описывает именно канало-B-опасность. Ассерт на строке модели декоратор не покрывает. Варианты: удалить файл в этом паке ЛИБО потребовать набор лейблов в конструкторе (компиль-энфорс). Второе прямая разрядка ратифицированного D4 п.1 изоляцию канала B делать ТИПОМ а не комментарием»). Рекомендую второе; удаление санкционированная альтернатива.
9. **Честность границ гарантии (в D-лог, не в код):** механизм не утверждает ничего про НЕОБЪЯВЛЕННЫЙ контент (лейблы данные владельца) и про eval-сторону (`eval/*.py` зовут провайдеров сами, вне движка). Инвариант формулируется как «egress прогонов ДВИЖКА».
10. **Тест-гард на шиппинг-конфиги расширить**: существующие `LoadPipeline`-тесты шиппинг-конфигов 14.2) дополнить лейблованным сценарием, иначе лейбл-ветка валидации остаётся непокрытой на реальных данных.
## §14.6. Скоуп: что УХОДИТ из пака-17 (пинг оркестратору — норма 5, не тихая девиация)
1. **§13 п.6 «писать в `escalation_model` ОТВЕТИВШУЮ модель» ВЫНОСИТСЯ.** Проверено: golden пинит `esc_model="fake-fallback"` на хопе, который ОТКАЗАЛ (`capture.golden:24,59,151,186`; мок отказывает и на праймари, и на хопе `golden_test.go:87-89`). Смена семантики даёт `esc_model=""` на 4 строках `TestGoldenDeterminism` красный, и маскировка это не скрывает (`maskGoldenLines` маскирует только payload-строки и 12-hex, `golden_test.go:366-381`) то есть это ВЕРДИКТ-изменение, требующее санкционированного пере-капчера. Держу отдельным ратифицируемым изменением; в паке-17 семантика остаётся как есть.
2. **§13 п.10 (два нита общности из петель: Detail-строки чекеров из данных · множитель ×2 `lintTimeUnits` в данные) УСЛОВНО.** Множитель живёт в чекере (`checks/checkers.go:174` `expectedHours := n * 2`), который версионируется `CheapGateVersion` (`cheapgates.go:68`), а тот фолдится в оба волновых снапшота **безусловно, без omitempty** (`snapshot.go:399,432`; в фикстуре `capture.golden:3`). Плюс если данные лечь в langpack двигается `pack.Version()` (`lang/langpack.go:159-162,218`), а его фолд (`langpack_version`) присутствует у СТЕНДОВЫХ книг (они объявляют `langpack_root`) `--resnapshot` и потеря $0-резюма драфта, на котором поедет замер остатка пака-16. **Условие включения в пак:** нит берётся только в БАЙТ-НЕЙТРАЛЬНОЙ форме (данные в пар-конфиге + фолд ТОЛЬКО при отличии от дефолта, дисциплина указателя/omitempty) и с доказательством ИСПОЛНЕНИЕМ (выход чекеров байт-идентичен до/после над корпусом). Иначе отложить. Молча «bump-нуть версию» нельзя: это пере-оплата всех книг; молча НЕ bump-нуть тоже нельзя: это правка версионируемого правила без сигнала.
3. **Открытая петля не пака-17 (записать, чтобы не потерялась):** ратифицированный D20.2-Q2 механизм согласия на пере-оплату флаг `--accept-rebill[=usd]` с порогом `min($0.50, 5%×ProjectedBookUSD)` (`05-decisions-log.md:245`) **в коде отсутствует** (`grep -rn "accept-rebill\|AcceptRebill" backend/` пусто); `--resnapshot` пере-пиннит без суммы (`stagerun.go:52-61`).
4. **D22.7 — восстановить формулировку.** Тело дважды называет `action: terminal` «generic-механизмом под политику входа D22.7». Точнее: D22.7 пер-чанковый ingest-скрин с человеческим ack и redrive только по явному ack (`05-decisions-log.md:276`), а `terminal` book-level отказ ВХОДА, механизм СЛАБЕЕ. И главное: там же ратифицировано «ОБЯЗАТЕЛЕН до первой erotica-книги в проде» этот предусловие пак-17 НЕ снимает и обязано стоять отдельной строкой гейта, иначе оптовая ратификация пака прочитается как разрешение на первую erotica-книгу.
## §14.7. Итоговый вердикт по дизайну (мой, после панели)
**Механизм данных (B1) и денежный кейстоун (M1) выстояли** все пять дайджестов ре-деривированы тремя независимыми путями. **Назначение слоя снапшота (B5) и формулировка энфорсмента (B4/B6-рубеж-1) переписаны** 14.114.2): резолв до валидации, «достижимое» лейбл-зависимо, лейблы вне brief-канона. **Скоуп B3 сокращён** до одного хопа из цепочки 14.3), два хвоста вынесены/условны 14.6). **Q1 остаётся «книга», но по аргументу гарантии, а не данных** 14.4). Вопросы владельцу §9 остаются в силе; Q4 моя рекомендация теперь B3-A.
## §14.8. Четыре добора из последней линзы (все проверены мной по коду)
1. **`Stage.Channel` + его enum + блок D4.1 обязаны получить диспозицию В ТОМ ЖЕ диффе, что `permissive` (HIGH).** Тело §13 распорядилось тремя точками из шести (Ф1) `permissive`, `adult:`, `CheckAdultChannel` а `Stage.Channel` (`pipeline.go:166`), его enum (`:549-553`) и D4.1-изоляция (`:569-576`) остались без судьбы. Опасность конкретна: после отставки `permissive` `providerPermissive` (`models.go:271-276`) возвращает false ВСЕГДА, и любой выживший `channel: adult` (в репо и на стенде их нет проверено M7 + мой греп стендовых `pipeline*.yaml`, но конфиги стенда вне git) упал бы с требованием ключа, который сам теперь падает громко = операторский тупик. Фикс: отставить `Channel` (громкий retired-ключ, называющий `content_labels`), удалить enum и блок D4.1 тем же диффом; и внести в §13.9 **переписывание/удаление двух существующих adult-тестов** (`config_test.go:37-64`, `runner_test.go:1552-1572`) иначе они остаются семантически ложными.
2. **Убрать `prohibited` из примера реестра (HIGH).** Значения `adult`/`prohibited` в моём примере 3) воспроизводят два из ТРЁХ ратифицированных выходов будущего L3-классификатора (`05-decisions-log.md:310`, D25 п.4: «тройной выход `adult/prohibited/uncertain`, terminal uncertainty, human-confirm»), а мой набор действий (`route|terminal`) не выражает ни `uncertain`, ни ack-семантику. Это приглашение прочитать пак как «мы умеем детектить L3». Фикс: в примере только `route` на синтетическом значении; отдельной строкой записать, что будущий L3-скрин подключается НАЗВАННЫМ пер-лейбловым хуком политики (набор действий открыт), а НЕ через `terminal`, и что пер-чанковые вердикты классификатора не могут жить в книжном поле (после §14.1 это уже структурно так: лейблы не в brief-каноне).
3. **Load-time отказы не должны убивать $0 read-only контракт D20.4 (HIGH).** `CheckAdultChannel` стоит в БЕЗУСЛОВНОЙ части `openRunner` (`runner.go:147-152`), а `CheckKeys` за `forWrite` (`:153-157`), потому что `status`/`report`/`export` делают 0 вызовов. Если новые отказы (терминальный лейбл, недостача способности) сядут безусловно, то книга, помеченная после оплаты, перестанет быть даже ЧИТАЕМОЙ уже оплаченную работу нельзя ни осмотреть, ни выгрузить, а единственный выход снять лейбл, то есть тихий обход. Фикс: **резолв маршрутизации — всегда** (read-путь пере-считывает снапшот, `export.go:274`), а ОТКАЗЫ за `forWrite`; на read-пути то же самое становится громким предупреждением + флагом в шапке отчёта (B6.1).
4. **Поправка к Ф5/M6 (MED, моя ошибка):** `chains.adult` несут **5** конфигов, не 4, и пятый указывает на НЕспособного провайдера: `configs/pipeline-c2.yaml:66` `adult: [kimi-k2.6]` (kimi без способности) при этом сам файл помечен «⚠ ИНТЕРИМ-ЗАГЛУШКА, НЕ контракт» (`:62`) и НЕ исполняем: `CheckRunnable` отвергает core C2 (`pipeline.go:307-312`). Для правила «len(chain)==1» это безразлично (длина 1 у обоих), для capability-инварианта важно: проверка обязана применяться к цепочкам, выбранным политикой лейблов ИСПОЛНЯЕМОГО конфига, иначе C2-заглушка блокирует загрузку. Числа Ф5/M6 читать с этой поправкой.
**Дополнения к манифесту §12** (приёмка ре-ранит): `sed -n '245p' docs/architecture/05-decisions-log.md` (D20.2-Q1/Q2) · `sed -n '188p;227,228p' backend/docs/D15.2-content-addressed-resume-spec.md` (brief_hash задропан; прецеденты «не через brief_hash») · `grep -rln "^adult:" /home/ubuntu/books/ | wc -l` (13 файлов вне git) · `grep -rn "LoadPipeline(" --include='*.go' backend | wc -l` (23 сайта: 1 прод + 22 теста) · `grep -rn "\.Complete(" --include='*.go' backend | grep -v '/internal/llm/'` (второй сем `live_conformance_test.go`) · `grep -n "escalate_to\|esc_model" backend/internal/pipeline/testdata/golden/capture.golden | head` (пин `esc_model` на отказавшем хопе) · `grep -rn "accept-rebill\|AcceptRebill" backend/` (пусто петля §14.6.3) · `grep -rn "adult:" backend/configs/` (5 файлов с `chains.adult`, c2 kimi) · `grep -rnE "^[[:space:]]*channel:" backend/configs /home/ubuntu/books/gu-zhenren/*/pipeline*.yaml` (пусто `Stage.Channel` мёртв и в репо, и на стенде).
**СТОП.** Дизайн-фаза закрыта; кода не тронуто (`git status --porcelain` показывает ровно один новый файл этот отчёт). Жду ратификации §9 (Q1Q6, где Q4 теперь = B3-A) и §14 (три отмены, два переворота, скоуп-выносы §14.6).