101 KiB
ПАК-17, ФАЗА 1 — ДИЗАЙН: generic content-labels × provider-capabilities (канал B «вживую»)
Ревью-шапка оркестратора №8 (25.07, приёмка пропорциональная — инлайн-пасс исполнением, без воркфлоу; РАТИФИЦИРОВАНО
D39.26). Принято В РЕДАКЦИИ §14: тело §0–13 читать ТОЛЬКО через неё (при расхождении побеждает §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 (BriefHashbook-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) при лейблованном переводчике mistralmax_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: только дизайн B1–B6 + адверсариальный селф-ревью; ни строчки кода; отчёт → СТОП → ратификация.
- Инвариант №1 (архитектурный): в движке НЕТ понятия «18+/adult/канал B». Механизм generic: content-label книги (данные книги) × capability провайдера (данные models.yaml) × политика цепочки (конфиг). «adult» — ЗНАЧЕНИЕ данных.
- Инвариант №2: refusal-обработка причино-агностична — детектим ФАКТ отказа (D12), причина = операторская диагностика; путь один: регенерация → эскалация по цепочке лейбла → flag+skip (D2).
- Инвариант №3 (жёсткая линия): L3 не строим, не анализируем;
eval/data/refusal_corpus/*не открывал (проверяемо: ни одного чтения оттуда). - Инвариант №4: live-вызовы с 18+ контентом — только по санкции владельца; дизайн и будущие тесты — на mock-провайдере и СИНТЕТИЧЕСКИХ безобидных фикстурах.
- Инвариант №5: канал A байт-идентичен — golden без пере-капчера, парити EXACT; adult-механика включается ТОЛЬКО явной маркировкой.
- Инвариант №6: capability-исключения (DeepSeek/Anthropic без adult) — СТРОКИ ДАННЫХ, не Go-литералы; гарантия «label ⊄ capabilities → провайдер не получает контент» компилируемая/тестируемая, fail-loud.
- Мысленный тест генеральности: второй лейбл (archaic-register) добавляется ДАННЫМИ, без правки Go.
- Не делать: L3-классификатор · live 18+ без санкции · реальный 18+ текст в репо · пар/книго/провайдер-литералы в Go · расширение скоупа без пинга.
- Канал вопросов: конфликт промта с кодом → пинг оркестратору через владельца (в §5/§6 ниже — 4 таких пинга и 3 вопроса владельцу).
§1. Метод и что верифицировано исполнением
Метод — образец пака-16 (D39.24): эхо → инвентаризации → дизайн → адверсариальные линзы → ревизия отдельным слоем.
- Онбординг: CLAUDE.md →
docs/README.md→ D-лог целиком по несущим блокам (D2/D3/D4/D12/D14/D19/D22/D39.20–D39.25) →architecture/12-go-style-notes.md§0 → нормы промтов §7–11. - Личное чтение кода (не по памяти):
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. - Семь параллельных read-only инвентаризаций (снапшот/деньги/отчётность/транспорт/тесты/конфиги/refusal-поток), каждая с требованием
file:line+ цитата; их выводы ниже перепроверены точечно мной. - Ключевой численный факт проверен МОИМ исполнением (не агентским): 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>.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 несовпадений, найденных инвентаризацией:
escalationOutcomeдержит РОВНО ОДНУ попытку (escalation.go:73-76), аstagerun.go:164-166суммирует толькоesc.fb.*⇒ хопы 1..N-1 были бы оплачены и НЕ учтены вchunk_status.cost_usd. → срез + суммирование.- Ось 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 = инвалидация всех чекпойнтов). escMuдержитсяdefer-ом до конца функции (escalation.go:123-124) ⇒ при обходе мьютекс накрыл бы ВСЕ N вызовов и сериализовал волну. → лок/анлок на итерацию.reachableModelsзнает толькоst.Model ∪ st.EscalateTo ∪ repair(runner.go:303-321) ⇒ клиент для хопа ≥2 не собран иr.clientпадает громко посреди волны (runner.go:347-351). → расширить (это и есть «третья модельная ось», о которой предупреждает комментарийrunner.go:301-302).- Снапшот фолдит РОВНО ОДНУ эскалационную модель (
snapshot.go:317-335) ⇒ правка capability хопа №3 была бы ТИХИМ false-hit. → массив фолдов хопов через указатель/omitempty (дисциплинаbanknoteSnap,snapshot.go:108-112), nil у книг без лейблов ⇒ байт-идентичность канала A. chunk_status.escalation_model— одна строка, и сегодня в неё пишется модель, которую ПОПРОБОВАЛИ, а не которая ОТВЕТИЛА (stagerun.go:167). → писать ответившую; путь обхода выводить из чекпойнтов (escalation=1+model_requested) — прецедент пака-16 «счётчики из durable-чекпойнтов, миграция ОТМЕНЕНА» (store/ledger.go:99-100).Escalationsв read-моделях — булев счётчик на юнит (status.go:226-228,338-341) ⇒ обход из 3 хопов отчитается как 1. → оставить как есть, число хопов выводить (omitempty), см. B6.- Предикат авторитетности написан под один
fb(stagerun.go:168) → правило терминального выбора «первый OK». - Валидация способности смотрит только
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):
- каждый лейбл книги имеет запись в
content_policy(иначе громко); action: terminalу любого лейбла книги → отказ запуска, ни одного вызова (generic-механизм под политику входа D22.7);∀ m ∈ reachableModels(): L ⊆ content_labels(m)— сообщение называет модель, провайдера и НЕДОСТАЮЩИЙ лейбл;- дедуп цепочки (B3-R п.2) и запрет хопа в праймари той же стадии;
- конфликт двух лейблов на одной стадии (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):
- Шапка
tmctl report/status: резолвнутые лейблы книги + таблица маршрутизации (стадия → модель под лейблом) — чистая проекция конфига, $0, без схемы. - QualityReport:
Hops int(число фактических хопов, выводится из чекпойнтовescalation=1) — закрывает искажение «булевEscalations= 1 при обходе» (B3-R п.7). Всё omitempty. - Атрибуция денег без миграции: новый 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легитимен ровно потому, что это НОВЫЙ вызов). - Паспорт главы (
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 попутно)
pipeline.go:331-332— «Adult … never a hash input» неверно (Ф6, проверено численно). Комментарий уезжает вместе сCheckAdultChannel.models.yaml:37иconfigs/pipeline-c1.yaml:62— «вход редактора — русский черновик, эхо-класс вырожден» устарело против D30.1 (редактор билингв,prompts/zh-ru/editor.md:35-37). Правка комментариев + NOTE о неизмеренной экспозиции (§4).configs/pipeline-c1.yaml:7-8— «channel-aware фильтр цепочек (Anthropic вне 18+)» описывает несуществующий механизм и провайдера, удалённого из стека (models.yaml:70). Переписать под лейблы.escalation.go:15— «Channel B (18+) Ф2 extends EXACTLY this file (permissive chains)» — после пака формулировка станет generic.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 (для разметки владельцем «ровно так» / «реши сам»)
- Данные:
permissive→content_labels: [adult](3 строки models.yaml) + отставнойpermissiveгромко;content_policyв шиппинг-конфиги (пустой/отсутствующий = сегодняшнее поведение). - Книга:
content_labels(lower+sort, omitempty-хвост канона), отставнойadult:(true → громко), канон заморожен. - Резолвер
labelsFor(chunk)(v1 — набор книги) + пер-стадийныйlabel_models+ резолв ДО рендера снапшота. - Валидация (5 проверок §6 рубеж 1) + удаление
CheckAdultChannel. - Ассерт
clientFor(model, labels)+ расширениеreachableModels. - B3-R: обход цепочки с 9 закрытыми формами (или B3-A по решению владельца).
- Снапшот: массив фолдов хопов через указатель/omitempty;
stageSnap.Model= резолвнутая модель. - Отчётность: шапка отчёта,
Hops,SpendByModel. - Тесты: негативный load, рантайм-ассерт до резервации, сквозняк refusal→хоп→OK→ре-гейт, сквозняк «хоп тоже отказал», генеральность на СИНТЕТИЧЕСКОМ лейбле, второй лейбл только данными.
- Хвосты общности из петель: Detail-строки чекеров из данных · множитель ×2
lintTimeUnitsв ключ данных. - Док-правки §10 (5 расхождений).
Инварианты фазы 2: golden байт-идентичен (без пере-капчера) · парити EXACT ·
-race· сэндбокс-репродукции через настоящий драйвер (правило 11) · дифф^func TestИСПОЛНЕНИЕМ · сессия не коммитит.
§14. ПОСТ-ПАНЕЛЬНАЯ РЕВИЗИЯ (отдельный слой; тело §0–13 НЕ переписано — метод пака-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:100vs:51). gates.repair.modelдля лейблованной книги обязана быть способной И не-аддитивной (pipeline.go:606-610) ⇒ из способных остаются толькоmistral-large-2512(исключён D39.20 как несущий) иlocal-qwen3-8b; grok падает на загрузке. Поле глобальное и фолдится (snapshot.go:143) ⇒ правка под лейблованную книгу инвалидирует ремонт канала A.
Исправленная конструкция (закрывает все шесть одним ходом): РЕЗОЛВ → ЗАТЕМ ВАЛИДАЦИЯ → ЗАТЕМ ВСЁ ЧИТАЕТ РЕЗОЛВНУТОЕ.
- Лейблы книги приходят в
LoadPipeline(path, models, pair, labels)— тем же способом, каким уже приходитpair(один прод-вызовrunner.go:137, 22 тестовых). Это сохраняет доктрину «все проблемы конфига одним списком» (models.go:6-7), которую вариант «второй метод после загрузки» ломает. - Резолв заполняет
yaml:"-"-поля стадии (ResolvedModel,ResolvedHop) — установившаяся конвенция резолвнутых полей (pipeline.go:145PromptPath,:217PromptsDir). - ВСЕ существующие гарды пере-прогоняются над резолвнутым: «хоп ≠ праймари», аддитивный буфер, роль-скоуп D12, capability-инвариант,
CheckKeys,reachableModels, фолд снапшота, рантайм. - «Достижимое» для лейблованной книги =
{резолвнутые модели стадий} ∪ {хоп(и), выбранные ЛЕЙБЛОМ} ∪ {repair-модель};st.EscalateToиchains.defaultпод лейблом НЕДОСТИЖИМЫ по построению. Дедуп применяется ТОЛЬКО к цепочкам, выбранным политикой лейблов книги —chains.defaultне валидируется (согласовано с Q5). - Резолв обязан работать и на 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. Принятые точечные фиксы дизайна (проверены по коду)
- Реестр политик — УПОРЯДОЧЕННЫЙ СПИСОК, не map. Тело оставило неопределённым случай ДВУХ активных лейблов на одной книге (две цепочки и два
max_hopsна один вызов), а map-порядок в Go не хранится — то есть Go выбирал бы итерацией. Форма:content_policy: [{label: …, action: route, chain: …}, …], правило «первая объявленная политика, претендующая на стадию, выигрывает», громкая ошибка при равном притязании. Эффективный список хопов — упорядоченное объединение по активным лейблам с дедупом ПОСЛЕ резолва (иначеchains.adult:[grok]+chains.archaic:[grok]дают два идентичных хеша ⇒ второй хоп — бесплатный реплей первого). - Пер-модельный оверрайд способности —
*[]string, и только СУЖАЕТ. Копирование дисциплиныcapabilities(«пусто = наследуй»,models.go:419-441) на список означает, что модель НЕ МОЖЕТ вычесть лейбл провайдера:content_labels: []неотличим от «не задано» ⇒ модель наследует способность, ⊆-инвариант зелен, контент уходит к явно исключённой модели. Форма: указатель (nil = наследовать, непустой указатель на пустой список = «ни одного»), плюс правило «модельный набор ⊆ провайдерского» (расширять нельзя) и load-тест на вычитание. - Разные имена ключей для двух сторон (панель права: один ключ конфлатит «свойство контента» и «разрешение получателя»): книга —
content_labels:, провайдер/модель —accepts_labels:. Инвариант читаетсяcontent_labels(книга) ⊆ accepts_labels(модель). - Go-имя отставного ключа — без слова adult. Буква D39.25 («ни один Go-идентификатор … не именуется adult/18+/каналом B») требует: поле
LegacyContentFlag boolс ТЕГАМИyaml:"adult"/json:"adult"(тег = замороженный ключ данных, идентификатор — generic). То же для канона. - Две РАЗНЫЕ семантики отставки — намеренно, с причиной в коде.
permissiveвmodels.yaml— громко ПРИ НАЛИЧИИ (один файл репо, пак его и миграцирует).adult:в book.yaml — громко только наtrue,false/отсутствие терпимо: ключ несут 13 файлов вне git (проверено мной: 5 book.yaml стенда + 8exp15/anchors/*.yaml) +example/book.yaml:9. Тело называло 5 — недоучёт, исправлен; и он усиливает вывод: терпимость обязательна, а не удобна. ОбязателенLoadBook-тест на все три формы (нет /false/true), потому что golden-фикстура ключа не несёт вовсе и ветку терпимости не упражняет. - Ассерт: место оставляю, тест исправляю. Панель требовала перенести ассерт в самое начало
runAttempt(доGetCheckpoint). Отклоняю: реплей чекпойнта провайдеру ничего не отправляет (ToS-экспозиции нет), а перенос отнял бы $0-реплей уже оплаченной работы при правке данных, которую загрузка и так ловит громко. Принимаю тестовую часть: негативный тест обязан гонять СВЕЖИЙ вызов (без чекпойнта) и ассертить идентичность ОШИБКИ, а не дельтуspend— иначе он зелен вакуумно (на реплееspendтоже не двигается). - Второй путь на провод существует в дереве — в тестах.
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), чтобы новый путь физически не мог не объявить контент. - Мёртвый failover — решить, а не «отметить».
failover.go:114,130,148пересылают ТОТ ЖЕreqдругому клиенту, а собственная шапка файла (:19-24) описывает именно канало-B-опасность. Ассерт на строке модели декоратор не покрывает. Варианты: удалить файл в этом паке ЛИБО потребовать набор лейблов в конструкторе (компиль-энфорс). Второе — прямая разрядка ратифицированного D4 п.1 («изоляцию канала B делать ТИПОМ … а не комментарием»). Рекомендую второе; удаление — санкционированная альтернатива. - Честность границ гарантии (в D-лог, не в код): механизм не утверждает ничего про НЕОБЪЯВЛЕННЫЙ контент (лейблы — данные владельца) и про eval-сторону (
eval/*.pyзовут провайдеров сами, вне движка). Инвариант формулируется как «egress прогонов ДВИЖКА». - Тест-гард на шиппинг-конфиги расширить: существующие
LoadPipeline-тесты шиппинг-конфигов (§14.2) дополнить лейблованным сценарием, иначе лейбл-ветка валидации остаётся непокрытой на реальных данных.
§14.6. Скоуп: что УХОДИТ из пака-17 (пинг оркестратору — норма 5, не тихая девиация)
- §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 семантика остаётся как есть. - §13 п.10 (два нита общности из петель: Detail-строки чекеров из данных · множитель ×2
lintTimeUnitsв данные) — УСЛОВНО. Множитель живёт в чекере (checks/checkers.go:174expectedHours := 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-нуть тоже нельзя: это правка версионируемого правила без сигнала. - Открытая петля не пака-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). - 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.1–14.2): резолв до валидации, «достижимое» лейбл-зависимо, лейблы вне brief-канона. Скоуп B3 сокращён до одного хопа из цепочки (§14.3), два хвоста вынесены/условны (§14.6). Q1 остаётся «книга», но по аргументу гарантии, а не данных (§14.4). Вопросы владельцу §9 остаются в силе; Q4 моя рекомендация теперь B3-A′.
§14.8. Четыре добора из последней линзы (все проверены мной по коду)
Stage.Channel+ его enum + блок D4.1 обязаны получить диспозицию В ТОМ ЖЕ диффе, чтоpermissive(HIGH). Тело §13 распорядилось тремя точками из шести (Ф1) —permissive,adult:,CheckAdultChannel— аStage.Channel(pipeline.go:166), его enum (:549-553) и D4.1-изоляция (:569-576) остались без судьбы. Опасность конкретна: после отставкиpermissiveproviderPermissive(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) — иначе они остаются семантически ложными.- Убрать
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-каноне). - 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). - Поправка к Ф5/M6 (MED, моя ошибка):
chains.adultнесут 5 конфигов, не 4, и пятый указывает на НЕспособного провайдера:configs/pipeline-c2.yaml:66adult: [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 (Q1–Q6, где Q4 теперь = B3-A′) и §14 (три отмены, два переворота, скоуп-выносы §14.6).