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

101 KiB
Raw Blame History

ПАК-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:337Reserve:379client:410Complete: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 — правка одного файла достаточна, стендовой возни нет.

Политика лейбла (ран-конфиг, новый верхний блок): реестр, который даёт (а) допустимость лейбла, (б) действие, (в) цепочку хопов.

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:):

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>.chainescalation.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":false58883ff5…; 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. Данные: permissivecontent_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-228snapshot.go:413 в общем билдере обеих волн), поэтому лейбл в каноне пере-оплатил бы и ДРАФТ-волну.

Второе, независимое основание: backend/docs/D15.2-content-addressed-resume-spec.md:188brief_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).