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

28 KiB
Raw Permalink Blame History

ПАК-17, ФАЗА 2 — СТРОЙКА: generic content-labels × provider-capabilities (исполнено)

Ревью-шапка оркестратора №8 (25.07, приёмка EXECUTE-FIRST, инлайн; ПРИНЯТ и залендён — D39.27). 0 блокеров. Каждый пункт манифеста ре-ранен мной: build/vet чисты · -race 12 пакетов ok · golden f7641993…4f0020f8 = HEAD-версия фикстуры байт-в-байт (git show HEAD:… | sha256sum), testdata/ чист · парити EXACT n=13618 … recall 0.9655 (56/58) · ценз проверен ДИФФОМ ИМЁН: 433→451, удалены ровно 8 объявленных · -shuffle=on зелёный · шов на провод один + именованный аллоулист. Рубеж-2 — три МОИХ мутации (не из списка сессии), все пойманы именованными тестами: лейблы обратно в канон BriefHash → красный TestEditorOnlyLabelLeavesTheDraftWaveUntouched (инвариант D39.26 п.1 запинён СЛЕДСТВИЕМ — снапшот драфт-волны — что сильнее ассерта хешей) · отключение лейбл-проверки в clientFor → красные TestLabelledRuntimeAssertPrecedesMoney/TestClientForRefusesUnknownModel · безусловный отказ маршрутизации → красный TestLabelledReadPathStaysAlive. Подтверждено по коду: достижимость лейбл-зависима (ReachableModels из резолвнутых) · r.client( больше не существует · порядок GetCheckpoint:337 → clientFor:374 → Reserve:390 · добор C закрыт именованным тестом рейт-гардов · отчётность вся omitempty · шиппинг-конфиги без единой поведенческой правки (только комменты + 3 снятых permissive) · стенд чист (0 файлов с permissive:/channel: вне git; все 5 книг ссылаются на репозиторный models.yaml) ⇒ отставки операционку не ломают. Диспозиции пингов §3: форма добора B — ФИКСИРУЮ форму 2 (аргумент про echoMineViolation × control: effort проверен; цена = консервативное пере-резервирование) · семантика хопа (§3.2) — ратифицирована как следствие D39.26 п.1 · labelsFor() подписью — принято (сильнее рантайм-ассерта) · хвосты общности — «отложено-с-записью». Ответ владельца по вокабуляру: ПАРА violence/sexually-explicit (сохраняет D14 п.1 и dspro-редактора) ⇒ дельта следующего касания: третье значение action — assert-only (route требует chain+бюджет+label_models, terminal не обрабатывает — чистого capability-ассерта в enum нет, а пара без него невыразима данными). Q2 (редактор) отложен с записью — связывает только explicit-книги, которых нет. Фикс-лист: стейл-коммент Stage.ReasoningMaxTokens (pipeline.go:189-194) про «ignored … reasoning-off».

Статус: ФАЗА 2 ИСПОЛНЕНА, СДАНА НА ПРИЁМКУ. Сессия НЕ коммитила (дерево: 25 modified + 2 new + 2 staged-deleted; git mv не применялся — удаления объявлены в §7). База: D39.26 + §«ФАЗА 2» промта docs/BACKEND_PACK17_CHANNEL_B_SESSION_PROMPT.md + дизайн-отчёт PACK17_DESIGN_2026-07-25.md в редакции §14. Инварианты фазы держатся все: golden байт-идентичен БЕЗ пере-капчера · парити EXACT · -race зелёный · сквозняки через настоящий драйвер · цензы исполнением.

§0. Эхо-блок (что делал)

  1. Механизм: книга объявляет content_labels (данные) × провайдер/модель — accepts_labels (данные) × ран-конфиг — УПОРЯДОЧЕННЫЙ реестр content_policy; движок только сравнивает множества.
  2. Порядок: РЕЗОЛВ маршрутизации → ВАЛИДАЦИЯ → все читатели (валидация, eager-клиенты, рейт-гарды, префлайт ключей, снапшот, рантайм) видят РЕЗОЛВНУТОЕ.
  3. Хеши: лейблы не входят ни в один; их wire-судьба = резолвнутая модель стадии, пер-стадийно и пер-волново. Канон brief ЗАМОРОЖЕН.
  4. Эскалация под лейблом: ОДИН хоп = chain[0], len>1 — громко; budget_usd>0 обязателен; escMu не тронут.
  5. Гарантия: каждая достижимая модель несёт все лейблы книги — рубеж 1 (загрузка, за forWrite) + рубеж 2 (рантайм-ассерт в runAttempt ДО Reserve, через clientFor(model, labels)).
  6. Отставки одним диффом: permissive · adult: true · Stage.Channel+enum+блок D4.1 · CheckAdultChannel; failover.go удалён.
  7. Данные владельца НЕ прибиты: вокабуляр лейблов и редактор под лейблом ждут ответа — fail-closed до него, ответ = строка данных.
  8. Не делал: L3 · live 18+ · реальный 18+ текст · мульти-хоп · авто-детекция · судья · вынесенные хвосты.

§1. Что построено (файл → что изменилось)

Данные (конфиг-слой)

  • internal/config/book.gocontent_labels: []string (trim+lower+dedupe+sort на загрузке); отставной adult: как LegacyContentFlag (громко только на true, false/отсутствие терпимо — ключ несут 13 файлов вне git); канон BriefHash заморожен: 7-е поле рендерится константой retiredContentSlot=false, лейблов в каноне НЕТ.
  • internal/config/models.goaccepts_labels *[]string на провайдере и модели (nil = наследовать, указатель на пустой = «ни одного»); модельный набор только СУЖАЕТ (молчащий провайдер = «ничего», иначе одна строка на модели выдала бы разрешение, которого у эндпойнта нет); отставной permissive (громко при ЛЮБОМ значении); AcceptsLabels/MissingLabels/ProviderOf; ThinksOnWire (резолвнутая wire-форма); canonicalLabel — каждое АВТОРСКОЕ написание лейбла обязано быть каноническим (иначе fail-closed молча).
  • internal/config/pipeline.gocontent_policy []LabelPolicy (action: route|terminal, chain), Stage.label_models, резолвнутые ResolvedModel/ResolvedHop (yaml:"-"), отставной channel:; resolveContentRouting + checkContentRouting + ReachableModels (ЕДИНОЕ определение достижимости); ContentRoutingError (книго-зависимые отказы, применяются по пути).
  • configs/models.yamlpermissive: true ×3 УБРАН; вместо значений — комментарий, почему строка пуста (вокабуляр = решение владельца, D39.26 добор A) с двумя формами ответа.
  • configs/pipeline-c1.yaml — закомментированный пример реестра на СИНТЕТИЧЕСКОМ значении + требования загрузки; шапка/цепочки/редактор-комменты переписаны.

Движок

  • runner.golabelsFor() (книго-константа), clientFor(model, labels) (компиль-энфорс: клиента нельзя получить, не объявив контент), reachableModels делегирует в конфиг, отказы за forWrite + предупреждение на read-пути, sourceEchoExposure (эхо-половина добора B).
  • stagerun.go — праймари = ResolvedModel; клиент резолвится ДО Reserve (ассерт срабатывает раньше денег; ветка релиза резервации на этой ошибке больше не нужна).
  • escalation.go — хоп = ResolvedHop (для книги без лейблов это ровно escalate_to).
  • snapshot.gostageSnap.Model и хоп-фолд берут резолвнутые модели; ни одного нового поля (существующие Escalate* сохранили имя/тег/позицию/правило заполнения).
  • resume.go/waverun.go — атрибуция резолвнутой моделью.
  • internal/store/ledger.goEscalationHops + SpendByModel (read-only агрегаты, БЕЗ миграции).
  • status.go/quality.go/cmd/tmctl/render.go — провенанс лейблов и денег (всё omitempty: книга без лейблов байт-идентична прошлому).
  • Удалено: internal/llm/failover.go + failover_test.go.

§2. Три решения, которые я принимал сам (и аргументы)

(1) Форма закрытия добора B — гейт НЕЗАВИСИМО от off (вторая из двух ратифицированных форм). Первая форма (echoes_when_thinking_off: true у xai) требует снять у grok off-выключатель, иначе models.yaml вообще не грузится (echoMineViolation бьёт по control: effort), — а этот выключатель запинён инвариантом с ДЕНЕЖНЫМ обоснованием (echo_mine_test.go: без него grok думает по дефолту, additive). То есть форма 1 = отдать за эхо-гарантию деньги и сломать пин, чего сессия решать не вправе. Поэтому: аддитивный гейт больше не смотрит ни на строку reasoning, ни на провод — буфер обязан объявить ЛЮБАЯ стадия на аддитивном провайдере, и AdditiveReasoningTokens резервирует его так же (гейт и резервация не могут разойтись по одному вызову). Подавленный вызов лишь ПЕРЕ-резервирует: потолок ужимается, слепым не становится. Эхо-ось при этом тоже перестала молчать, но НЕ гейтом: sourceEchoExposure громко предупреждает, когда стадия отправляет ИСХОДНИК модели, чей провод гасит thinking (generic: без имён провайдеров и лейблов; на шиппинг-пути не стреляет, на glm-арме стреляет — ровно та неизмеренная экспозиция, которую отрицал устаревший коммент D30.1).

(2) Где живут резолвнутые поля и как лейблы доезжают до runAttempt. Поля — на стадии, yaml:"-" (конвенция PromptPath); лейблы приходят в LoadPipeline тем же швом, что pair (доктрина «все проблемы конфига одним списком»), а до провода едут ПАРАМЕТРОМ clientFor(model, labels) — единственный способ получить клиента. Ключевание ассерта на данных книги/чанка, а не на стадии, обязательно: ремонтный путь строит СИНТЕТИЧЕСКУЮ стадию и стадийное поле потерял бы.

(3) Объём B6. Шапка status/report (лейблы + таблица маршрутизации), EscalationHops (булев escalated хопы не считает), SpendByModel (GROUP BY model_requested). Ни миграций, ни синтетической роли (Role — ось RequestHash: новая роль = новый ПЛАТНЫЙ вызов).

§3. Отклонения и пинги оркестратору (норма 5 — не тихие девиации)

  1. Добор B закрыт ВТОРОЙ формой, не первой (§2.1). Цена: любая стадия на аддитивном провайдере теперь обязана объявить reasoning_max_tokens даже при off — одна строка конфига, громко на загрузке. Пере-резервирование консервативно. Прошу зафиксировать выбор формы явно.
  2. Семантика хопа под лейблом уточнена: цепочка лейбла подменяет escalate_to ТОЛЬКО на стадии, которую лейбл реально пере-маршрутизирует (label_models). Причина — ратифицированная D39.26 п.1: безусловная подмена двигала ДРАФТ-волновой снапшот (в нём фолдится escalate_to) при лейбле, который маршрутизирует только редактора, то есть пере-оплачивала оплаченную волну. Стадия, которую лейбл не трогает, сохраняет свой фолбэк — и если тот не вправе получить контент, прогон падает по ИМЕНИ модели, а не молча подменяет её. Тест TestEditorOnlyLabelLeavesTheDraftWaveUntouched пинит именно ратифицированное обещание.
  3. Хвосты общности (Detail-строки чекеров · множитель ×2) — ОТЛОЖЕНЫ, как позволял промт. Причина проверена: множитель живёт в чекере, версионируемом CheapGateVersion, а тот фолдится в оба волновых снапшота безусловно; данные в langpack двигают pack.Version(), который у СТЕНДОВЫХ книг присутствует ⇒ --resnapshot и потеря $0-резюма драфта под замер остатка пака-16. Байт-нейтральной формы (пар-конфиг + фолд только при отличии от дефолта) в этом паке не строил — это отдельная работа с доказательством исполнением.
  4. labelsFor() «ассерт книго-константы» реализован ПОДПИСЬЮ, а не рантайм-проверкой: функция не принимает чанк, поэтому пер-главная маршрутизация физически невыразима; в комменте названо, что для неё пришлось бы снять фолд модели с пер-волнового снапшота. Если владелец хотел исполняемый ассерт — скажите, добавлю.
  5. internal/llm/llm.go не отформатирован gofmt — это состояние HEAD, не моё (проверено: git show HEAD:… | gofmt -l). Не правил, чтобы не носить в диффе чужой шум.

§4. Ревью ИСПОЛНЕНИЕМ своего кода (мандат 12.07) — и что оно нашло

Прогнал 6 адверсариальных ревьюеров ПО ИСПОЛНЕНИЮ (компliance по 15 пунктам · байт-идентичность · гарантия маршрутизации · деньги · общность · честность тестов), с мутациями в копии дерева. Нашли 8 существенных дефектов; все исправлены, каждое исправление проверено мутацией (мутация возвращает дефект → тест краснеет):

# Найдено Исправление Мутация-доказательство
1 CRIT (тесты). Лейбл-фикстура была ТОЖДЕСТВЕННЫМ отображением (label-модель = конфиговая), поэтому откат ResolvedModel/ResolvedHop в stagerun/escalation не краснел — несущая половина пака была не покрыта фикстура маршрутизирует в ДРУГИЕ слаги (fake-label/fake-labelhop); ассерты на ТЕЛО запроса (мок канонизирует model в ответе, поэтому modelActual свидетелем быть не может) st.ResolvedModel→st.Model: 2 красных · ResolvedHop→EscalateTo: 3 красных
2 HIGH (D39.26 п.1). Безусловная подмена хопа двигала ДРАФТ-снапшот при лейбле «только редактор» ⇒ пере-оплата оплаченной волны подмена только на пере-маршрутизированных стадиях (§3.2) восстановление безусловной подмены → TestEditorOnlyLabelLeavesTheDraftWaveUntouched красный
3 HIGH (гарантия). Модель могла ВЫДАТЬ лейбл, которого нет у провайдера, если у провайдера набор не объявлен (nil) — а сегодня он не объявлен ни у одного nil провайдера = «ничего»; правило «модельный ⊆ провайдерского» безусловно && prov.AcceptsLabels != nilTestAcceptsLabelsNarrowingAndWidening красный
4 HIGH (снапшот). Откат хоп-слота фолда не краснел ассерт "escalate_to":"fake-labelhop" в payload ss.EscalateTo = st.EscalateTo → красный
5 MED. Два активных route-лейбла с РАЗНЫМИ головами цепочек — вторая никогда не выстрелит, тихое усечение упорядоченное объединение голов + дедуп + громко при >1 отключение проверки → TestContentRoutingRefusals красный
6 MED. Гард шва .Complete( не видел cmd/ и зависел от ИМЕНИ переменной ctx; исключал себя по basename корень = модуль, regexp \.Complete\(, needle собран из частей, stagerun.go исключён по ПУТИ пакета (позитивный контроль ревьюера: файл-обход ловится)
7 MED. EscalationHops/SpendByModel и провенанс в StatusReport — ноль тестов TestRoutingMoneyProvenanceIsDerived + ассерты в read-path-тесте обнуление агрегата: 8 красных · снятие блока провенанса: 1 красный
8 MED. Написание лейбла: книга нормализуется, а accepts_labels/политика/label_models сравнивались побайтно ⇒ fail-closed, но с враждебной диагностикой; плюс порядок сообщений зависел от итерации map canonicalLabel для всех авторских мест + сортировка ключей снятие проверки → TestLabelSpellingIsCanonicalEverywhere красный

Плюс мелкое, тоже исправлено: тавтологический errors.Is(nil, …) удалён · basePipeline разведён (голова цепочки ≠ escalate_to, иначе «подменяет» непроверяемо) · переформулирована завышенная проза в TestShippingConfigsUnderLabels · os.Unsetenvt.Setenv · тест перестал пинить ДАННОЕ значение chains.adult (ответ владельца по вокабуляру ломал бы CI) · добавлен TestNoPrivilegedLabelValue (тривью на «движок выучил значение»: книга с adult против реестра, который знает только синтетический лейбл, обязана быть отвергнута — привилегированное значение делает тест красным) · сняты 4 устаревших против D30.1 коммента «вход редактора — русский черновик» (репо + арм-конфиги) · retiredAdultSlotretiredContentSlot, канон-поле AdultRetiredSlot (тег json:"adult" заморожен — байт-нейтрально).

Что ревью НЕ опровергло: байт-идентичность (все четыре кейстоуна ре-деривированы независимо), единственность шва .Complete( в проде, отсутствие вокабуляра/редактора в конфигах, все скоуп-выносы чисты.

§5. Манифест «заявление = команда» (приёмка ре-ранит; все цифры — из моего исполнения)

# Заявление Команда Результат
M1 сборка и vet чисты go build ./... && go vet ./... без вывода
M2 вся сьюта зелёная под -race go test -race -count=1 ./... 12 пакетов ok, 0 FAIL
M3 golden БАЙТ-ИДЕНТИЧЕН без пере-капчера sha256sum internal/pipeline/testdata/golden/capture.golden f7641993e37f7ec619dfc5d2914a342d796a7cecd92080e33704f3974f0020f8ТОТ ЖЕ, что baseline пака-16
M4 фикстуры golden не тронуты git status --porcelain backend/internal/pipeline/testdata/ пусто (0 строк)
M5 парити EXACT TM_MINER_PARITY=1 go test ./internal/miner/ -run Parity -count=1 -v PARITY: n=13618 catastrophe{方源:0 蛊:1 蛊师:2 古月:22} recall@proposed=0.9655 (56/58 GT) — тождественно пакам 15/16
M6 ценз тестов ИСПОЛНЕНИЕМ: было 433, стало 451 (+26 добавлено, 8 удалено) grep -rn "^func Test" backend | wc -l · git grep -c "^func Test" HEAD -- 'backend/**/*_test.go' (сумма) · diff списков имён 451 / 433; удалены 6 failover + TestCheckAdultChannel + TestRunnerChannelBRequiresPermissive
M7 новые тесты не зависят от порядка go test -count=1 -shuffle=on -run 'Content|Label|Retired|Shipping|Accepts|Routing|Privileged|Egress|Echo' ./internal/config/ ./internal/pipeline/ ok / ok
M8 в движке не осталось ветвления по типу контента grep -rn "CheckAdultChannel|providerPermissive|Stage.Channel|\.Channel\b" --include='*.go' backend только отставные Legacy*-поля и их сообщения
M9 вокабуляр и редактор НЕ прибиты grep -rnE "^\s*(accepts_labels|content_policy)" backend/configs/ ни одной раскомментированной строки
M10 шов на провод один grep -rn "\.Complete(" --include='*.go' backend | grep -v '/llm/' stagerun.go + live_conformance_test.go (именованный аллоулист) + сам гард-тест
M11 мутации возвращают дефекты (ревью §4) копия дерева в /tmp, 9 мутаций, go test ./internal/config/ ./internal/pipeline/ каждая даёт ≥1 красный (таблица §4)
M12 сессия не коммитила git status --porcelain | wc -l 29 записей, HEAD не двигался

§6. Открытые петли / на владельце

  1. Вокабуляр лейблов (единый adult против violence/sexually-explicit — D39.26 добор A): до ответа accepts_labels пусты, любая лейблованная книга падает громко, называя модель/провайдера/лейбл. Ответ = 13 строки данных в models.yaml + запись реестра в ран-конфиге.
  2. Редактор под лейблом (Q2): кандидаты только из способных; grok-4.3 интерим по аргументам §4 дизайна, но теперь ОБЯЗАН объявить reasoning_max_tokens (следствие §2.1); glm-5 — одна строка данных ТОЛЬКО после сверки ToS/AUP Z.AI по первоисточнику.
  3. Гранулярность = книга (Q1): лейблованная книга едет по цене permissive-тира целиком.
  4. D22.7 паком НЕ снят: пер-чанковый L3-скрин остаётся предусловием первой erotica-книги в проде; action: terminal — book-level отказ ВХОДА по объявленным данным, механизм СЛАБЕЕ, и значение под него не заведено.
  5. Форма закрытия добора B — прошу явной фиксации (§3.1).
  6. Хвосты общности — отложены с причиной (§3.3).
  7. --accept-rebill (D20.2-Q2) по-прежнему отсутствует в коде — петля не пака-17.

§7. Удаления и переносы (объявляю)

  • git rm backend/internal/llm/failover.go backend/internal/llm/failover_test.go (ратифицировано D39.26 п.10: вызывающих не было, файл пересылал ТОТ ЖЕ запрос другому провайдеру — ровно то, что лейбл запрещает; nil-panic-дыра D4 п.1). Удалённые тесты объявлены в цензе M6.
  • git mv не применялся.
  • Новые файлы: backend/internal/config/content_labels_test.go, backend/internal/pipeline/content_routing_test.go.