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

108 KiB
Raw Blame History

ПАК-16 «слой-3 diff-редактор» — ДИЗАЙН-ПРЕДЛОЖЕНИЕ (ФАЗА 1, 25.07.2026)

Бэкенд-сессия по промту docs/BACKEND_PACK16_DIFF_EDITOR_SESSION_PROMPT.md (ревизия 2). Кода НЕ трогал — ни одной правки в backend/; всё ниже — предложение. СТОП на ратификацию.

⚠ ЧИТАТЬ §15 «РЕВИЗИЯ 2» ПЕРЕД РАТИФИКАЦИЕЙ. Тело §0§14 — дизайн в том виде, в каком он ушёл на адверсариальную панель. Панель (6 линз + скептик-верификация каждого возражения) вернула 4 выживших MAJOR, а замер, который я после этого воспроизвёл САМ, меняет рекомендуемый скоуп пака. Все поправки собраны в §15; расхождения тела с §15 разрешаются в пользу §15.

ЭХО-БЛОК (как я понял задачу — расхождение чините сразу)

  1. Скоуп: спроектировать (не строить) петлю «детерминированный флаг → адресный ремонт минимального спана дешёвым вызовом → детерминированный re-gate → при неудаче прежнее поведение».
  2. Мандат: решения мои и аргументированные; приоры оркестратора — приоры, а не предписания; north-star — ИЗМЕРЕННЫЙ вклад петли по классам, не вера.
  3. Инварианты «ровно так»: D2 не хард-гейт; лестница regenerate_before_escalateescalate_to не меняет семантику молча; $0-резюм драфта цел; repair-вызовы = новый класс (чекпойнт/RequestHash/леджер/маркер); no-repair путь байт-идентичен, golden зелён БЕЗ пере-капчера; пара/книга — данными, не Go; DeepSeek thinking никогда не off; книга и производные вне git.
  4. Не делать: канал B, копирование GPL/AGPL, пар/книго-литералы в Go, live-вызовы без санкции, расширение скоупа без пинга.
  5. Приёмка фазы 1: этот отчёт + адверсариальный селф-ревью 4 линзами, затем СТОП.
  6. Хвосты фазы 2 (строгий декод book.yaml и пар/models-слоёв, unknown-key-тест, zero-value-паники) — в паке, но после ратификации; пред-проверка стендовых book.yaml сделана уже сейчас (прил. B) — она read-only и ничего не стоила.

0. Метод: чем грунтован дизайн

Прочитан код (не заголовки): internal/checks/* целиком, internal/pipeline/{chunkrun,stagerun,waverun,escalation,disposition,snapshot,render,resume,runner,banknote,quality,export,status}.go, internal/config/{pipeline,pair,book,models}.go, internal/store/{ledger,requestlog,migrate}.go, internal/lang, промты prompts/zh-ru/*, пар-данные configs/langpacks/zh-ru/dc-checkers.txt, шиппинг-конфиги. Сверх этого прогнан воркфлоу из 7 независимых read-only инвентаризаций (позиции чекеров · класс «чинится чистым кодом» · money/resume/golden-поверхность нового класса вызовов · role/stage/wave-поверхность · стенд-аудит конфигов · объём дефектов на живых выходах rerun2 · прайор-арт) — все утверждения ниже с file:line; несогласия агентов с моими выводами разобраны явно (§10, §11).

Ключевой факт, определивший весь дизайн: сегодня типизированные факты чекеров вообще не возвращаются в контур. checks.RunCheapGates запускается ПОСЛЕ последовательности стадий, на уровне юнита (waverun.go:477), пишется в retrieval_state и больше ни на что не влияет. Санитайзер — единственная вердикт-ось (chunkrun.go:51), и он же — единственный, у кого уже есть полный цикл «нашёл → детерминированно починил → перепроверил» (chunkrun.go:60: SanitizeOutput(StripCosmetic(x)).Total()==0). Петля ремонта — это обобщение ровно этого цикла на классы, где детерминированной починки не существует.


D1. Единица ремонта + маппинг флаг→спан

Инвентаризация: кто даёт позиции, кто только факт

Полная таблица — прил. A. Сводка по 21 детерминированной проверке:

Класс Вход Позиция сейчас Позиция достижима аддитивно
DC1 时辰 src↔final нет (FindStringSubmatch ×2, checkers.go:132,140) да, обе стороныFindStringSubmatchIndex, та же семантика матча
DC2 千万/数十万 src↔final нет (Contains/MatchString, checkers.go:199-205) да — strings.Index/FindStringIndex
percent 成 src↔final src-матч есть, tgt нет (checkers.go:258-266) да
latin residue final рун-оффсеты уже считаются и выбрасываются (checkers.go:290-303) да, ноль работы
broken word final нет — text.TokenizeCyrillic лоуэркейсит и теряет оффсеты (runes.go:32-50) да, но нужен оффсет-сохраняющий проход
DC6 register final рун-индекс есть (checkers.go:225-232) да
санитайзер 1/2/6 final байт-оффсеты уже есть (sanitizer.go:247,276,617-634) да
санитайзер 4 (latin) final нет вообще слабо
coverage / length-collapse / number-drift / classify целый текст нет по природе (скаляр/порог) нет
glossary post-check final нет — дефект есть ОТСУТСТВИЕ dst-формы (mempostcheck.go:61-77) нет (нет чего локализовать)

В пакете checks уже есть словарь спанов: glossSpans возвращает [][2]int байт-диапазонов (sanitizer.go:554-570). Заводить новый тип «на вырост» не надо — это least-mechanism (§1 норматива).

Решение D1

  1. Единица ремонта = ПРЕДЛОЖЕНИЕ (одно, при необходимости с соседним) вокруг матча, а не строка и не абзац. Строку брать нельзя, потому что наш редактор легитимно реверстывает — это его прямой мандат (editor.md:15-19). Основание из research/22 цитирую ТОЧНО, без расширения: «punctuation add/drop + line-parity воюют с нашим editor-reflow (их VN = line-by-line без reflow; наш редактор легитимно сливает пунктуацию/абзацы) — брать нельзя вслепую» (22-domain-harness-survey.md:118, продублировано таблицей :181 — «Осознанно НЕТ»). То есть в наших доках зафиксирован конфликт СТРОЧНЫХ проверок с reflow; вывод «не якорить ремонт на индексах строк» — мой, выведенный из этого конфликта, а не цитата. Границы предложения берутся из общей данной lang.DefaultTerminators() (та же таблица, что у чанкера и coverage — internal/lang/data/terminator.txt), значит расширение спана пар-агностично.
  2. Новая ЧИСТАЯ функция в checks (детекция — работа этого пакета), а не поля в существующих Result-структурах:
// checks/repair.go
type RepairClass string // engine-идентификаторы: "dc1_time_units", "dc2_magnitude", …
type RepairCandidate struct {
    Class    RepairClass
    DstSpan  [2]int // байт-диапазон в FINAL — что заменяем
    SrcSpan  [2]int // байт-диапазон в SOURCE (нулевой для target-only классов)
    Detail   string // операторская диагностика (англ.-первичная, как у остальных)
}
func RepairCandidates(source, final string, cfg CheapGateConfig) []RepairCandidate

Почему НЕ дополнять CheapGateResult/SanitizerResult полями позиций: CheapGateResult сериализуется в retrieval_state.style_detail (chunkrun.go:113-117), PostcheckMiss — в postcheck_detail, и его golden-байты НЕ пусты (capture.golden:92, три тега без omitempty, mempostcheck.go:37-41). Отдельный тип держит новый механизм вне сериализуемых поверхностей полностью — риск байт-сдвига равен нулю по построению, а не по аккуратности. (Замечу для полноты: у CheapGateResult все теги omitempty и все golden-строки несут style_detail= ПУСТО, так что аддитив там был бы безопасен — но он не нужен.)

  1. ГАРД ОДНОЗНАЧНОСТИ (несущий): класс порождает кандидата, только если в юните ровно один матч с каждой релевантной стороны. Два 时辰 в источнике и одно «час…» в переводе — кандидата НЕТ, остаётся сегодняшний флаг. Это прямо закрывает главную дыру, найденную инвентаризацией: сегодняшние src↔dst-чекеры сопоставляют ПЕРВЫЙ матч с ПЕРВЫМ (checkers.go:132,140), то есть выравнивания у нас нет и его неоткуда взять. Прайор-арт формулирует то же правило: «неоднозначный якорь (≥2 матчей) обязан отвергать, а не молча патчить первый».
  2. Ремонт применяется к СЫРОМУ тексту завершившейся стадии (тому же, что видят чекеры), а не к экспортному: ExportNormalize и ApplyHeading накладываются позже (waverun.go:495-507), поэтому оффсеты, посчитанные на сыром тексте, остаются валидными ровно до места применения. Экспортный контракт применяется поверх отремонтированного текста как обычно.

Расхождение с приором D1

Приор: «дотипизация = позиции в Result-структуры аддитивными полями». Не согласен: позиции нужны только петле, а Result-структуры — персистентная поверхность. Отдельная чистая функция даёт то же самое, не касаясь сериализации (и не заставляя каждый чекер платить за поле, которым он не пользуется).


D2. Классы петли v1 vs flag-only vs «чинится чистым кодом»

Критерий отбора (мой, строже приорного): класс входит в LLM-петлю, только если выполнены ВСЕ четыре условия — (1) есть детерминированный кандидат-спан под гардом однозначности; (2) есть детерминированный ре-чек, который нельзя удовлетворить тривиально (не «слово исчезло», а «инвариант класса восстановлен»); (3) правильный вывод НЕ вычислим из байтов (иначе это класс (в)); (4) риск порчи хорошего текста снимается протоколом (см. NO-CHANGE ниже), а не верой.

Класс Диспозиция Аргумент
dc1_time_units (时辰) петля v1 Спан с обеих сторон получается даром; ре-чек строгий (ruNum == n && ruNum != 2n перестаёт выполняться только при реально изменённом числе); правильный вывод требует русской морфологии числительных + согласования («три часа»→«шесть часов», «около трёх часов»→«около шести часов») — это ровно отвергнутый класс «код≥локаль» (pymorphy отвергнут паком-13), значит код не годится, а LLM годится. Объём: 时辰 встречается 266 раз на книгу (прил. D).
dc2_magnitude (千万/数十万) петля v1, только с NO-CHANGE Ре-чек строгий (ok-суппрессор — регексп на верную величину). НО собственный заголовок чекера предупреждает: «千万 — это ещё и штатная ГИПЕРБОЛА, чья „тысячи“-передача в регистре» (checkers.go:186-188). Поэтому класс допускается в петлю ИСКЛЮЧИТЕЛЬНО с первоклассным ответом «без изменений»: решение «это гипербола» принимает модель, видя оба спана, а не наш регексп.
percent_scale (成) петля v1 Истинное значение чекер уже вычисляет (66%), ре-чек строгий (появилась процентная форма). Гард однозначности обязателен: сегодня fraction-cue ищется по всему юниту.
latin_residue петля v1 Оффсеты уже есть; ре-чек строгий (токен исчез И не появилось новых); вывод («again»→«снова») — перевод, кодом невычислим.
broken_word («войть», «-йть») петля v1 Детекция zero-FP по построению (структурная сигнатура); восстановление задуманного слова требует словаря/морфологии — отвергнуто политикой; спан = слово+предложение.
dc6_register («терем») flag-only Ре-чек тривиально удовлетворим: любая замена лексемы обнуляет счётчик, включая замену на плохую. Ре-чек, который ремонт может «сыграть», — не гейт. Регистр — стилевое суждение, его место в промте (north-star), не в петле.
glossary_miss flag-only v1 Ре-чек-то как раз хороший (появилась ли принятая dst-форма), но локализовать нечего: дефект — отсутствие; целевого спана не существует (mempostcheck.go:61-77). Кандидат v2, когда появится локатор (см. §12).
coverage/excision, length_collapse, number_drift, classify-классы flag-only Скалярные/пороговые по целому тексту; спана нет по природе; length/empty вообще ключуются на finish_reason, а не на тексте.
dialogue_dash, ё-консистентность, translit-interj flag-only Дефект чанк-глобальный (смешение стилей, пара «ё/е»), одиночный спан его не закрывает. Отдельно: дефис→тире соблазнительно чинить кодом, но это правка ЭКСПОРТНЫХ байт на no-repair пути → ломает инвариант байт-идентичности (§D4). Если владелец захочет — отдельным паком экспорт-контракта с санкцией на пере-капчер.
(в) чистым кодом — УЖЕ В ПРОДЕ, в петлю НЕ тащить заголовок главы: чанкер сам срезает исходный и рендерит «Глава N» (chunk.ApplyHeading) — это ПРОФИЛАКТИКА, модель номера не касается; markdown-### и контентless CJK-утечка: checks.StripCosmetic + ре-чек; глиф-уровень: recoverableFold, stripCombiningOnCyrillic в ExportNormalize.

Дискриминатор, который я принимаю (сформулирован адверсариальным агентом, и он точен): дефект чинится чистым кодом тогда и только тогда, когда правильный вывод есть функция от самих дефектных БАЙТОВ (глиф/разметка), а не от смысла источника. Все три прод-прецедента лежат по эту сторону; все остальные классы пере-прогона — по ту.

Протокол NO-CHANGE (несущий, из прайор-арта)

Наш же research зафиксировал APE-прецедент: на WMT19 en→ru ни одна APE-система не побила «ничего не делать» на сильном черновике, а «over-correction» — названная главная болезнь APE; LLM-редакторы правят и корректный вход. Поэтому:

  • Ответ модели «ничего менять не надо» — первоклассный выход, а не ошибка: движковый сентинел ⟦TM-NOCHANGE⟧ (идиома уже в проде — ⟦TM-BANK-v1⟧, banknote.go:28; это ENGINE-токен, не пар-данные, поэтому в Go он законен).
  • Всякий класс с известным риском ложного срабатывания (dc2 в первую очередь) допускается в петлю только под этим протоколом.
  • Отказ модели чинить = сегодняшнее поведение (флаг в наблюдаемости), плюс счётчик declined — это и есть измеримая цифра «сколько наши флаги были ложными», побочный и очень полезный продукт петли.

D3. Механика вызова

D3.1. Где живёт: СУБ-ШАГ финальной стадии, НЕ новая стадия

Инвентаризация role/stage/wave-поверхности (прил. A.2) даёт однозначный ответ. Новая стадия с ролью repair:

  • автоматически уезжает в edit-волну (разбиение бинарное: role=="translator" иначе EDIT — waverun.go:41-50, snapshot.go:195-204);
  • становится ФИНАЛЬНОЙisFinal уезжает с edit (stagerun.go:117), санитайзер начинает судить выход ремонта, а не редактора; экспортная строка тоже переезжает (export.go:173-177);
  • двигает edit-волновой снапшот (массив стадий входит в payload) → --resnapshot → пере-оплата edit-волны на КАЖДОЙ существующей книге, даже если ремонт выключен;
  • ломает read-back существующих книг: export → все юниты pending, qualityProcessedUnits 0, status → всё in_progress;
  • и главное — вешает книгу навсегда: status.go:307 считает expected = members·|draftStages| + |editStages| и требует ok == expected ТОЧНО; лишняя строка chunk_status = вечный in_progress.

Суб-шаг внутри runStage финальной стадии не трогает ничего из этого: волна та же, isFinal там же, лишних строк chunk_status/jobs нет, снапшот при выключенном гейте байт-идентичен. Цена суб-шага (его надо решать явно, и я решаю ниже): резюм не бесплатен «сам собой», промпт не фолдится «сам собой», телеметрия конфлатится с редактором, модель не попадает в reachableModels.

Точка встраивания — stagerun.go между разрешением эскалации и записью chunk_status (сейчас это строки 173-206). Там уже есть всё, что нужно, и ровно в нужной форме: ch.Text — источник юнита (склейка чистых членов, waverun.go:438), prev — черновик юнита, last.text — финальный текст, isFinal — признак шиппинг-стадии, job — для чекпойнта. Условие входа: isFinal && disposition == DispOK && gates.repair.enabled.

D3.2. Промпт: конвенция prompts/<пара>/repair/<класс>.md

Один файл на класс, в стандартном формате System ---USER--- (render.go:92-110), резолвится тем же promptConventionPath-идиомом, что и роли (pipeline.go:263,466), fail-loud с названным путём при включённом гейте. Почему так, а не один файл с секциями классов: нулевой новый механизм (загрузчик, сплиттер, SHA-фолд — всё уже есть), каждый класс версионируется отдельным PromptSHA256, редактура — работа редакторской команды, как и задумано для prompts/. Плейсхолдеры — только существующие: {{text}} = исходный спан (пусто для target-only классов, и тогда шаблон класса его просто не упоминает), {{draft}} = целевой спан. Новых полей RenderVars не требуется → байт-нейтрально для всех существующих шаблонов.

Пар-данность соблюдена: инструкция «时辰 = 2 часа» живёт в prompts/zh-ru/repair/dc1_time_units.md — это данные пары, как и уже вшитая строка editor.md:10. В Go — только идентификаторы классов и сентинел.

D3.3. Модель, потолки, бюджет

Конфиг (строгий декод pipeline уже включён — pipeline.go:331, так что опечатка = громкая ошибка):

gates:
  repair:
    enabled: false          # дефолт — как у coverage/sanitizer/banknote
    model: deepseek-v4-flash
    max_calls_per_unit: 2
    budget_usd: 0.50        # книго-широкий потолок класса
    classes: []             # пусто = ратифицированный дефолт-набор
  • Модель — дешёвая (flash-класс). Ценник (configs/models.yaml:119): вход $0.14/M, выход $0.28/M. Один ремонт ≈ 350 ток. промпта + ~100 ток. выхода ≈ $0.0001. При наблюдаемом объёме (прил. D) 5001000 вызовов на книгу ≈ $0.050.10 против COGS-цели $0.85 — приемлемо. Для контраста: LLM-судья как триггер стоил бы $204255 на книгу (замер стенда) — поэтому триггер обязан быть детерминированным, и это не стилевое предпочтение, а арифметика.
  • Валидация конфига при enabled: true (все — громкие ошибки на загрузке): модель существует в models.yaml; budget_usd > 0 (иначе петля не может выстрелить — это не «тихий ноль» как у эскалации, а осознанная девиация: гейт, который не может ничего сделать, — запрещённый класс, ср. pipeline.go:553-561); max_calls_per_unit > 0; для каждого включённого класса существует файл промпта; D13.6-гейт additive-reasoning применяется к repair-модели ровно как к escalate_to (pipeline.go:507-513) — иначе grok-модель в этой роли ослепила бы потолок трат.
  • reachableModels расширяется repair-моделью (runner.go:293-307) → её клиент строится в пре-компьют-пассе, CheckKeys требует её ключ, rateGuard работает. Без этого — громкая ошибка no pre-built client (runner.go:335). Это ровно та «третья модель-ось», о которой тривайр предупреждает в комментарии.
  • Эхо-мина: repair-вызов НЕ передаёт reasoning:"off" (поле не заводится вовсе) → провайдерский дефолт → на DeepSeek thinking остаётся ON. Гейт echoMineViolation работает на уровне модели (models.go:377), поэтому конфигурация мины невозможна и здесь. Ответ прогоняется через checks.StripThink перед использованием (thinking-модель может протечь блоком).

D3.4. Протокол одного ремонта

  1. cands := checks.RepairCandidates(ch.Text, last.text, cfg); фильтр по включённым классам; детерминированная сортировка (класс, затем начало спана); срез по max_calls_per_unit.
  2. Спан расширяется до предложения (lang.DefaultTerminators()); рендерится промпт класса; RequestHash{Role: "repair", Stage: st.Name, Model: repairModel, Attempt: <порядковый номер кандидата>, SnapshotID: <снапшот волны>, Messages: msgs} — коллизия с основной осью невозможна (роль различает), одинаковые сообщения дают один хеш, то есть повтор бесплатен и это корректно.
  3. reserve → call → settle+checkpoint тем же путём, что и все вызовы (переиспользование runAttempt с синтетической config.Stage, а не копия денежного пути — копия здесь была бы ровно тем классом дрейфа, против которого стоит foldModelWire). max_tokens считается от спана и проходит applyModelFloor для repair-модели.
  4. Гарды применения (до всякого re-gate; нарушение = ремонт отброшен, деньги записаны, текст исходный):
    • ответ ≠ пуст после StripThink+trim; finish == stop;
    • ответ == ⟦TM-NOCHANGE⟧ → «declined», текст не трогаем (это НЕ провал);
    • коридор длины: len(new) ∈ [0.5×, 2×] len(old) в рунах;
    • перевод строк в замене не появляется, если их не было в спане (иначе ремонт переверстает абзац);
    • множество CJK-рун замены ⊆ множества CJK-рун исходного спана (микро-эхо не проходит под 15%-порогом classify, поэтому нужен локальный гард);
    • применение — сплайс байтового диапазона; кандидаты применяются в порядке УБЫВАНИЯ смещения, чтобы оффсеты остальных не сдвигались.
  5. RE-GATE поверх собранного текста (все проверки — уже существующие чистые функции):
    • класс, породивший кандидата, больше не порождает кандидата;
    • RunCheapGates(...).Total() не вырос относительно до-ремонтного;
    • SanitizeOutput (если гейт включён) не стал грязным, если был чистым;
    • classify(...) остаётся ok;
    • membank.Postcheck не даёт НОВЫХ confirmed-промахов (см. D3.6 — почему это несущая проверка, а не украшение). Провал любой — отбрасываем ВСЕ ремонты юнита (атомарность: содержимое derived-чекпойнта детерминировано и не зависит от того, какие из ремонтов «частично прошли»); цена отката ≤ $0.0002.
  6. Успех → derived-чекпойнт (§D4) → final_hashsr.Text = отремонтированный текст, чтобы юнит-уровневые cheap-гейты (waverun.go:477) измеряли ПОСТ-ремонтное состояние. Диспозиция не меняется никогда.

D3.5. Loop-guard

Раунд ровно ОДИН (петли как таковой нет — один проход): отремонтированный текст повторно на ремонт не подаётся, даже если появился новый кандидат (он попадёт в наблюдаемость и в следующий прогон). Плюс жёсткий max_calls_per_unit. Плюс общий книжный потолок $. Возможности «ремонт породил дефект → ремонт ремонта» не существует по построению.

D3.6. Три интеграционные развилки, которые нельзя оставить «на потом»

(а) Глоссарный ре-чек и минимальное расширение сигнатуры. Ремонт подменяет спан прозы — а в спане может стоять каноническая форма термина. Пост-чек глоссария сегодня выполняется ПОСЛЕ возврата из стадии (waverun.go:465-473), то есть ремонт, сломавший канон, будет замечен, но уже как промах — а при включённом gates.glossary.postcheck_gate он бы ПЕРЕВЁЛ юнит в flagged с пустым экспортом, то есть ремонт своими руками уничтожил бы главу. (В шиппинг-конфигах гейт сейчас выключен — блок glossary: отсутствует в pipeline-c1.yaml/арм-ямлах, — но строить дизайн на «гейт всё равно выключен» нельзя.) Поэтому Postcheck входит в re-gate, а для этого runStage получает один дополнительный параметрinjected []membank.PickedEntry юнита. Он симметричен уже существующей прокидке injectionByRole через runStageSequence и меняет ровно два колл-сайта (runDraftChunk передаёт memSel.Injected, runEditUniteditSel.Injected). Альтернатива «перенести ремонт в runEditUnit, где выборка уже под рукой» отвергнута: там пришлось бы (1) вторично писать chunk_status, отобрав право на final_hash у того, кто его пишет, и (2) держать ВТОРУЮ копию логики для draft-only пути (runDraftChunk) — ровно тот класс «две независимые реализации одного правила», который пак-15 только что вылечил memberDrops().

(б) Банкнота и владение final_hash. На OK-пути final_hash уже может перенаправляться банкнотой (stagerun.go:184-190). Пересечение возможно только в draft-only пайплайне (банкнота — translator-only, banknote.go:167; ремонт — на финальной стадии), но оно возможно. Порядок ратифицирую явно: банкнота срезается первой (её очистка уже отражена в last.text), ремонт работает поверх очищенного текста, и final_hash в итоге указывает на derived-чекпойнт РЕМОНТА. Банкнотный derived-чекпойнт при этом остаётся записанным (он идемпотентен и контент-адресуем) — просто перестаёт быть финальным; это промежуточный артефакт, а не мусор. Никакого «кто последний, тот и прав» — порядок фиксирован кодом и покрывается тестом.

(в) Чекпойнт КАК ПАМЯТЬ о попытке ремонта. Отдельного «журнала попыток» не нужно: отклонённый (NO-CHANGE) и отвергнутый гардами ремонт на следующем прогоне порождает БАЙТ-ИДЕНТИЧНЫЙ запрос (тот же спан, тот же промпт, тот же снапшот, тот же порядковый номер) → попадает в свой чекпойнт → $0 и тот же исход. Резюм не переплачивает за неудачные ремонты по тому же механизму, по которому не переплачивает за неудачные попытки стадии.

Расхождения с приором D3

  • Приор: «1 раунд на чанк» — принято, но уточняю единицу: юнит (ремонтируется шиппинг-текст, а он в волновой модели per-unit), и добавляю потолок вызовов ВНУТРИ юнита.
  • Приор: «re-gate тем же чекером + полной cheap-сюитой» — принято и усилено: не только «не хуже по cheap», но и санитайзер + classify + локальные гарды применения, потому что cheap-сюита по построению не видит вреда, который может внести подмена спана (эхо в спане, переверстка, обрезка).

D3a. Позиция в лестнице эскалации

Факт, меняющий постановку вопроса: множества не пересекаются.

  • regenerate_before_escalate пере-атакует ТОЛЬКО ретраибельный поднабор {length, empty} (disposition.go:130-132, применяется stagerun.go:140).
  • escalate_to стреляет только на эскалируемых флагах {cjk_artifact, excision_suspect, loop_degenerate, hard/soft_refusal, content_filter} (disposition.go:142-149) и на роли editor запрещён конфигом (pipeline.go:528-531).
  • Ремонт срабатывает на состоянии, которого лестница не знает вообще: стадия завершилась ok, но шиппинг-текст несёт типизированный дефект.

Следствия:

  1. Формулировка приора «ремонт ПЕРВЫМ, дешевле полного re-gen» вакуумна: классы ремонта никогда не вызывали ни regenerate, ни эскалацию. Расхождение с приором фиксирую честно — и получаю инвариант СИЛЬНЕЕ запрошенного: двойная оплата одного дефекта невозможна по построению, а не по дисциплине.
  2. Провал ремонта НЕ триггерит regenerate — согласен с приором, и опять же по построению: цикл попыток к этому моменту завершён, диспозиция ok, пере-атаковать нечего.
  3. Семантика лестницы не меняется ни в одном байте: ни один существующий предикат (retryable/escalatable/disposition) не трогается. Ремонт — НЕ новая ступень лестницы, а новая ось: лестница отвечает «выход непригоден», ремонт — «выход пригоден, но в нём точечная ошибка».

D4. Wire / снапшот / resume / golden

Снапшот

type repairSnap struct {
    Enabled       bool              `json:"enabled"`
    Version       string            `json:"version"`                 // алгоритм: извлечение спана + гарды + re-gate
    Model         string            `json:"model"`
    ModelWire     json.RawMessage   `json:"model_wire,omitempty"`    // foldModelWire — та же функция, что для primary/escalate
    MaxCalls      int               `json:"max_calls_per_unit"`
    Classes       []string          `json:"classes"`                 // отсортировано
    PromptSHA256  map[string]string `json:"prompt_sha256"`           // класс → SHA файла промпта (json.Marshal сортирует ключи)
}
func (r *Runner) repairSnapshot() *repairSnap // nil, когда гейт выключен

Три несущих свойства:

  1. Указатель + omitempty (дисциплина banknoteSnap, snapshot.go:104-130), а НЕ форма {"enabled":false} как у coverage/sanitizer. Иначе добавление фичи пере-биллит каждую существующую книгу. Это прямое требование инварианта «no-repair путь байт-идентичен».
  2. Фолдится ТОЛЬКО в снапшот волны финальной стадии (finalStageWave(), snapshot.go:184-190): в 2-стадийном пайплайне — только edit-волна. Иначе включение ремонта сдвинуло бы и драфт-волновой снапшот и сломало $0-резюм драфта — прямое нарушение инварианта «$0-резюм драфта НЕ ломать». (В draft-only пайплайне финальная волна — драфт, туда и фолдится: правило одно, ветвления по книге нет.)
  3. SHA промптов ремонта фолдится явно. Суб-шаг не стадия, поэтому автоматики PromptSHA256 (snapshot.go:377) на него не распространяется, а msgsContentHash (render.go:284-309) покрывает только сообщения хост-стадии. Без явного фолда правка repair/dc1.md тихо переиспользовала бы чекпойнты — класс D5.2.

Стоимость включения (честно): enabling = --resnapshot = пере-оплата ТОЛЬКО edit-волны; драфт резюмится за $0. Это и есть «resnapshot ОДИН».

Resume

  • Repair-вызов — обычный оплаченный чекпойнт по своему request_hash → kill-9 между вызовом и записью теряет ≤1 вызов, повтор бесплатен (ledger.go:127-171 идемпотентен).
  • Отремонтированный ТЕКСТderived-чекпойнт по прецеденту commitSanitizedExport (stagerun.go:252-263) / commitBanknoteExport (banknote.go:182-208): id = "tm-repair-v1:" + sha256("tm-repair-v1\x00" + <reqHash терминальной попытки> + "\x00" + <отремонтированный текст>), контент-адресуемый, коллизия с hex-хешем реальной попытки невозможна, пишется ДО того, как на него сошлётся chunk_status.
  • chunk_status: диспозиция ok (не меняется), final_hash → derived. Ни одной новой строки (status.go:307expected := len(u.Members)*len(draftStages) + len(editStages), проверено чтением, — не ломается).
  • Резюм: быстрый путь stagerun.go:78-82 отдаёт resumeFromChunkStatus, тот читает final_hash и возвращает отремонтированный текст. Ремонт на резюме не повторяется и не оплачивается — $0 по построению. Для НЕудавшихся ремонтов (declined/rejected — текст не менялся, кандидат остался) работает второй механизм, D3.6(в): повторный запрос байт-идентичен → попадает в собственный чекпойнт → $0 и тот же исход.
  • Стоимость ремонта попадает в chunk_status.cost_usd юнита (честная сумма, как ретраи).

Golden

  • Гейт выключен → repairSnap == nil → payload снапшота байт-в-байт прежний → все request_hash прежние → capture.golden байт-идентичен. Пере-капчер НЕ нужен. Это проверяется исполнением на фазе 2 (TM_UPDATE_GOLDEN=1 + git status пуст), как в паке-15.
  • Путь ремонта пинится ОТДЕЛЬНОЙ фикстурой (свой testdata/repair/ + свой мок-провайдер + свой capture-файл), а не расширением capture.golden. Так инвариант «no-repair без пере-капчера» и требование «новый класс вызовов должен быть запинен» выполняются одновременно, без санкции на пере-капчер.
  • Вопрос на ратификацию: если оркестратор предпочитает единый golden с ячейкой ремонта — это санкционированный пере-капчер; я рекомендую отдельную фикстуру.

D5. Деньги и отчётность

Атрибуция денег НЕ требует миграции. checkpoints и request_log уже несут колонку role; repair-вызовы пишутся с role="repair" → книжная сумма = SELECT SUM(c.cost_usd) FROM checkpoints c JOIN jobs j ON c.job_id=j.id WHERE j.book_id=? AND c.role='repair' — тот же идиом, что EscalationSpentUSD (ledger.go:236-245). Это и есть запрошенный CostSource-маркер; изобретать второй не нужно. Категорически нельзя помечать repair-чекпойнты Escalation: true — они тихо съели бы escalation.budget_usd.

  • Потолок класса: repairBudgetRemains() по образцу escalationBudgetRemains() — мягкий пред-вызовный кап; админ через свой мьютекс repairMu по образцу escMu (runner.go:92, escalation.go:123), иначе N параллельных edit-воркеров каждый прочитают «бюджет есть» и перебор составит до N1 вызовов. С мьютексом перебор ≤ 1 вызова — как задокументировано у эскалации.
  • Флаг estimated (v10) продолжает работать как есть: repair-вызов, севший на нулевой usage, пометится тем же механизмом.

Счётчики качества — аддитивная миграция v11 (класс v7/v9: ALTER TABLE ... DEFAULT 0, прежние строки и wire не двигаются):

retrieval_state: n_repair_candidates, n_repair_calls, n_repair_applied, n_repair_declined, n_repair_rejected INTEGER DEFAULT 0
                 repair_detail TEXT DEFAULT ''   -- JSON: [{class, outcome, reason}], детерминированный порядок

Сводки:

  • QualityReport (quality.go): repair_applied/declined/rejected всего и по классам — это и есть north-star-метрика «измеренный вклад петли»; плюс defect_residual (кандидатов осталось после ремонта).
  • ChapterPassport (status.go:36): repair_applied, repair_failed рядом с StyleFlags — оператор видит, что глава чинилась.
  • declined (модель сказала «и так верно») — прямой замер ложных срабатываний наших чекеров, побочный продукт, которого у нас никогда не было.

D6. $0-валидация

Три эшелона, все бесплатные, ни один не требует live-санкции:

  1. Синтетические фикстуры в репо (internal/checks/testdata/repair/): по образцу дефект-классов, БЕЗ цитат книги (тест-данные пары легитимны, §0.3 норматива; книжный текст — нет). На них пинятся: извлечение спанов, гард однозначности (2 матча → нет кандидата), все гарды применения (пустой ответ, NO-CHANGE, длина вне коридора, перевод строки, CJK в замене), атомарный откат, детерминизм (два прогона байт-в-байт).
  2. Мок-провайдер (идиом golden_test.go: httptest + чистая функция от тела запроса) — сквозной прогон ремонта без сети и без денег, включая kill-9-резюм (второй прогон = $0, тот же derived-хеш).
  3. Стенд, RE-ONLY, $0. Ремонт-СКАН как read-only проекция (расширение QualityReport/export --pairs): пере-считать RepairCandidates над сохранёнными выходами rerun2 и выдать «класс → сколько кандидатов → сколько под гардом однозначности». Гейт при этом НЕ включается → снапшот не двигается → --resnapshot не нужен → ноль долларов. Данные для этого уже лежат готовыми: export-*.json несёт per-unit {source, final_text} (прил. D).

Порядок, который я рекомендую (north-star «измеренный вклад»): сначала $0-скан (эшелон 3) → он даёт остаточный объём по классам после уже вшитой профилактики (editor.md:10 про меры появился ПОСЛЕ прогона rerun2, значит стендовые цифры измеряют ДО-профилактическое состояние — см. §11, линза 5) → и только потом решение владельца о платном включении петли на реальном прогоне. Mini-live гейт — отдельное решение владельца, не в паке.


10. Сводка расхождений с приорами оркестратора

Приор Моё решение Аргумент
1 Дотипизация = аддитивные поля в Result-структурах Отдельный тип + чистая функция RepairCandidates Result-структуры сериализуются в стор (style_detail/postcheck_detail, у PostcheckMiss golden-байты не пусты) — новый механизм держим вне персистентной поверхности
2 В петлю: юнит-конверсия, число-масштабы, broken-word, latin-residue Принято + percent_scale; dc2 — только под NO-CHANGE Собственный заголовок чекера объявляет 千万 гиперболо-рискованным; APE-прецедент из нашего же research требует первоклассного «без изменений»
3 Ремонт ПЕРВЫМ в лестнице (дешевле re-gen) Формулировка вакуумна: множества классов не пересекаются retryable={length,empty}, escalatable={echo/excision/loop/refusal/filter}, ремонт — на ok-стадии. Инвариант выходит сильнее: двойная оплата невозможна по построению
4 Суб-шаг edit-стадии Принято, и обосновано измерением, а не удобством Новая стадия двигает isFinal, edit-снапшот, read-back существующих книг и вешает status на expected-арифметике
5 1 раунд на чанк 1 раунд на юнит + потолок вызовов внутри юнита + книжный $-потолок Шиппинг-единица в волновой модели — юнит
6 re-gate = чекер + cheap-сюита + санитайзер + classify + 5 локальных гардов применения cheap-сюита слепа к вреду, который вносит именно подмена спана
7 (в) «чинится чистым кодом» — отсортировать Отсортировано: всё, что можно, УЖЕ в проде; новых (в)-кандидатов, безопасных для инварианта байт-идентичности, нет Дефис→тире и т.п. правят экспортные байты на no-repair пути → отдельный пак с санкцией
8 Эскалация: budget 0 = выключено (по образцу) При enabled:true и budget_usd<=0громкая ошибка Гейт, который не может сработать, — запрещённый класс (pipeline.go:553-561); тихий ноль здесь дал бы «включил и не работает»

11. Адверсариальный селф-ревью дизайна (5 линз; 4 обязательные + 1 своя)

Линза 1 — детерминизм и resume. Атака: ремонт вносит недетерминизм в текст, который уже оплачен, и резюм расходится с живым прогоном. Ответ: весь ремонт — чистая функция от (текст, кандидаты, ответы моделей), ответы моделей чекпойнтятся по request_hash, результат фиксируется derived-чекпойнтом, а резюм ВООБЩЕ не входит в код ремонта (быстрый путь stagerun.go:78-82 отдаёт готовый текст). Порядок применения кандидатов детерминирован (сортировка + убывающие смещения). Итерация по map отсутствует. Остаточный риск: json.Marshal мапы PromptSHA256 в снапшоте — ключи сортируются стандартной библиотекой (тот же приём уже используется для extra_body, snapshot.go:237), риск снят. Найденные дыры (все три исправлены в дизайне, а не оставлены на стройку): (1) первая версия фолдила repairSnap в общий buildSnapshotID → сдвинула бы драфт-волновой снапшот и убила $0-резюм драфта → фолд только в волну финальной стадии; (2) не был определён владелец final_hash при совпадении с банкнотой (draft-only пайплайн) → порядок ратифицирован явно, D3.6(б); (3) неудавшийся ремонт выглядел как «повтор на каждом резюме» → механизм памяти найден и он бесплатный: чекпойнт запроса, D3.6(в).

Линза 2 — деньги. Атака: новый класс вызовов течёт мимо учёта/потолков или удваивает оплату. Ответ: вызов идёт тем же reserve→call→settle+checkpoint (не копия пути); книжный/дневной потолок работает автоматически; класс-потолок — отдельная сумма по role='repair', НЕ по escalation=1 (иначе съел бы премиум-бюджет); мьютекс держит перебор ≤1 вызова при параллельных воркерах; отброшенный ремонт всё равно записан как расход (честно). Порядок величин: $0.0001/вызов, ≤$0.10/книга против $0.85 COGS; LLM-триггер ($204255/книга) исключён по арифметике, а не по вкусу. Остаточный риск: резервация сайзится по min_max_tokens модели (для flash это 8000) → мгновенная резервация $0.0023 на вызов. При потолке книги это лишь временно «поджимает» ceiling; на settle возвращается фактическая цена. Отмечаю как известное свойство, не дефект.

Линза 3 — дисциплина D2. Атака: петля превращает наблюдаемость в гейт / протаскивает мусор в TM и экспорт. Ответ: ни один класс не меняет диспозицию — ни в одну сторону. Ремонт работает исключительно на disposition == ok; провал ремонта = ровно сегодняшнее состояние (текст как был, флаг в наблюдаемости). Мусор течь не может: любая подмена спана проходит 5 локальных гардов + 4 ре-чека, и при любом сомнении откатывается целиком. Честная поправка к формулировке промта: «при неудаче прежний flag+skip» точно описывает санитайзерные классы, но выбранные мной классы сегодня не диспозиции вовсеу них нет skip. Правильная формулировка для v1: «при неудаче — прежняя наблюдаемость». Это расхождение терминологии, а не дисциплины: инвариант «мусор не течёт в TM/экспорт» соблюдён строже (мы вообще не меняем судьбу чанка). Дыра, которую я НЕ закрываю в v1 и называю явно: самый дорогой сегодня дефект — санитайзерный СУБСТАНТИВНЫЙ (преамбула/хвостовой блок): юнит теряется целиком, экспорт пуст. Ремонт мог бы его спасти, но это меняло бы диспозицию flagged→ok, то есть трогало вердикт-ось. Дизайн v2 набросан в §12 — решает оркестратор.

Линза 4 — общность («заработает ли на паре, которой в репо ещё НЕТ, без правки Go?»). Ответ: да. Новая пара = (а) каталог prompts/<пара>/repair/*.md, (б) её dc-checkers.txt с паттернами классов, (в) при необходимости target-данные (broken_suffix). Go не трогается; отсутствие любого файла при включённом гейте — громкая ошибка с названным путём. В Go попадают только: идентификаторы классов (движковые), сентинел ⟦TM-NOCHANGE⟧ (движковый, идиома банкноты), алгоритм спанов/гардов/ре-чеков (общий). Единственная точка, где я НЕ добавляю общности и объясняю почему: класс latin_residue наследует существующий таргет-гейт isRuTarget (disposition.go:296) — потому что на →en-таргете «латинское слово в выходе» флагает всё подряд. Это ратифицированный target-aware гейт слоя 7, а не новая утечка: петля не заводит ни одного нового языкового предиката, а наследует гейт того чекера, который породил кандидата. Остаточный риск: расширение спана до предложения использует общую таблицу терминаторов — на паре без терминаторной разметки (напр. тайский без пробелов/точек) спан выродится в весь текст. Гард длины спана (доля от юнита) отсекает такой случай в «кандидата нет». Отмечено как известное ограничение, а не как скрытая поломка.

Линза 5 (моя) — «а не измеряем ли мы вчерашний день». Атака: классы петли выбраны по дефектам пере-прогона rerun2, но с тех пор часть дефектов закрыта ПРОФИЛАКТИКОЙ. Проверка исполнением (git, не память): строка «МЕРЫ приводи к привычным читателю единицам (时辰 = 2 часа … 六成六 = 66%, а не „6,6“)» в editor.md:10 пришла коммитом 2f91b04 от 2026-07-24 (пак-13, «release QA»); отчёт пере-прогона rerun2/RERUN_REPORT.md датирован 2026-07-24 00:09, а сами экспорты сделаны ДО него. То есть DC1 и percent_scale получили промпт-профилактику после того, как были сняты стендовые цифры: наблюдаемые «2 из 7 юнитов с неверным 时辰» измеряют ДО-профилактическое состояние. Следствие для дизайна (учтено в D6): сначала $0-скан остаточного объёма, потом платное включение. Петля, построенная под дефект, который уже вылечен промтом, — это ровно та «вера вместо замера», которую промт запрещает. Дизайн от этого не меняется (механизм общий), но порядок работ меняется, и я это фиксирую как рекомендацию к фазе 2.


12. Что НЕ в паке v1 (и почему) — кандидаты v2 с набросками

  1. Ремонт СУБСТАНТИВНЫХ санитайзерных классов (преамбула/хвостовой edit-блок). Сегодня они роняют юнит в пустой экспорт. Набросок безопасного механизма: ремонт «только удаление» — модель возвращает не новый текст, а границу удаляемого префикса/суффикса; Go проверяет детерминированно, что результат является суффиксом/префиксом исходного (то есть ремонт физически не может ничего дописать), и что санитайзер стал чист. Omission-safe по построению. Требует ратификации: меняет диспозицию.
  2. glossary_miss. Ре-чек отличный, локатора нет. Нужен либо src→dst-локатор, либо приём «переспросить только предложение с сработавшим src-ключом». Прайор-арт (LinguaGacha, retranslation-by-keyword) — ровно про это; у нас уже есть src-позиции ключей в матчере.
  3. Детерминированные (в)-фиксы экспортного контракта (дефис→тире и родня) — только отдельным паком с санкцией на пере-капчер golden.
  4. Дифф-формат вместо спан-замены. Наш research фиксирует, что цифры search/replace (EM 0.95/0.94/0.68) получены на КОДЕ, русской прозы никто не мерил, а арм Q4b никогда не гонялся (11-implementation-plan.md:882 — парковка). Поэтому v1 умышленно берёт самую узкую форму диффа — «один спан ↔ одна замена» с детерминированным Go-применением, без диалекта диффа как такового. Это осознанно СЛАБЕЕ, чем «diff-редактор» в названии пака: полноценный search/replace-протокол — гипотеза, требующая замера, а не дефолта.

13. Вопросы к ратификации (решения не мои)

  1. Класс-набор v1: утверждаю ли 5 классов (dc1, dc2, percent, latin_residue, broken_word) — или dc2 исключить как гиперболо-рискованный даже под NO-CHANGE? ⚠ Отвечать по §15.1: замер показал, что на прод-арме эти пять классов дают 1 настоящий дефект и 1 ложное срабатывание (именно dc2-гипербола) на 7 юнитов.
  2. Golden: отдельная фикстура пути ремонта (моя рекомендация) — или санкция на пере-капчер capture.golden с ячейкой ремонта?
  3. Порядок работ фазы 2 — ГЛАВНЫЙ ВОПРОС ПАКА, теперь с цифрами (§15.1): строим в паке-16 всё $0-е (позиционная детекция + скан-поверхность + два пар-данных бага-фикса ru_hours_re/shichen_re), а платный вызов кладём за enabled: false до замера остатка на пост-профилактическом прогоне — или строим петлю целиком и меряем на первом платном прогоне?
  4. Субстантивный санитайзерный ремонт (§12.1) — в этот пак под «только удаление», или в следующий?
  5. v11-миграция счётчиков (retrieval_state, аддитивно) — санкционирована?
  6. Расширение сигнатуры runStage одним параметром (injected []membank.PickedEntry, D3.6(а)) ради полного re-gate — принимается? Отказ означает, что глоссарный ре-чек из петли выпадает и ремонт сможет тихо сломать канон термина.

Приложение A. Инвентаризация (сжато; полные file:line — в теле)

A.1 Позиции чекеров. Уже материализуют позиции: санитайзер-преамбула (sanitizer.go:247), edit-мета (:276), CJK-утечка (:617-634), glossSpans [][2]int (:554-570), latin-residue (checkers.go:290-303), DC6 (checkers.go:225-232), translit-interj (cheapgates.go:350-357), число-токены (regressionguard.go:97-128). Позиция достижима без смены семантики: DC1/DC2/percent/万億 (замена FindStringSubmatch...Index, ContainsIndex). Позиция недостижима по природе: coverage, length-collapse, classify-пороги, glossary post-check. Особый случай: broken-word и ё идут через text.TokenizeCyrillic, который лоуэркейсит и теряет оффсеты (runes.go:32-50) — нужен отдельный оффсет-сохраняющий проход.

A.2 Role/stage/wave-поверхность. Ветвления по роли: waverun.go:41-50, snapshot.go:184-204, chunkrun.go:76,96-99, stagerun.go:106, banknote.go:167, pipeline.go:466-473,528-531. По индексу: stagerun.go:88-91,117, export.go:173-177, quality.go:210-213. По количеству: status.go:307 (expected — самый опасный), quality.go:185-207, runner.go:293-326, models.go:488-492, snapshot.go:374-411.

A.3 Чек-лист «новый класс оплаченных вызовов». 14 пунктов; несущие: единственные два места RequestHash (stagerun.go:296, escalation.go:105) — третье обязано зеркалить набор полей; JSONOnly хешируется, но не доезжает на wire (render.go:245+273 vs stagerun.go:411-417) — ловушка, которую наш дизайн обходит (JSON-режим не используем); опт-ин фичи фолдятся ТОЛЬКО указателем+omitempty; лишняя строка chunk_status вешает status.

Приложение B. Стенд-аудит (пред-проверка санкционированного строгого декода book.yaml)

Вердикт: внесхемных ключей НЕТ ни в одном book-образном yaml. Проверено 13 файлов на стенде (acceptance/book.yaml, rerun/book.yaml, rerun2/{book,book-dspro,book-mistral}.yaml, 8 якорей exp15/anchors/*.yaml) + 2 в репо (example/book.yaml, testdata/golden/book.yaml) + configs/models.yaml + 3 пар-конфига + инлайн-литералы book.yaml в 9 Go-тестах. Флип KnownFields(true) на LoadBook — нулевая миграция для всего, что существует сегодня. Строгость сегодня: только LoadPipeline (pipeline.go:331-332); LoadBook (book.go:103), LoadModels (models.go:176), LoadPair (pair.go:69) — обычный yaml.Unmarshal.

Побочная находка (хвост владельцу, НЕ бэкенду): легаси-конфиги стенда acceptance/pipeline-acceptance.yaml и rerun/pipeline-rerun.yaml уже сегодня не проходят строгий LoadPipeline — несут снятые context.stm_depth/overlap_tokens и retired prompt:. Трио rerun2/pipeline-*.yaml чистое (мигрировано на prompt_override). То есть повторный прогон СТАРЫХ стендовых конфигов упадёт громко на загрузке — это ожидаемое поведение пака-15, но владельцу полезно знать заранее.

Приложение C. Прайор-арт (идеи; ни строки чужого кода)

  • AiNiee ProofreadTask (AGPL-3.0): триггер — флаг чекера, НЕСУЩИЙ ТИП (error_type); действие — однострочный proofread (ProofreadTask.py:250 по research/22, спот-чек оркестратора подтверждал цитату). Что именно получает модель, политика ретраев и наличие ре-чека — в нашей документации НЕ зафиксировано (UNVERIFIED), поэтому эти детали я НЕ переносил в дизайн.
  • LinguaGacha retranslation-by-keyword: после правки глоссария пере-переводятся ТОЛЬКО строки с термином. Мотив идентичен нашему; наш аналог — кандидат v2 (§12.2). Оговорка: их 3-классовый чекер описан у нас по deepwiki (AI-ген), доказательная сила слабая; сильный, код-верифицированный аналог — GalTransl (8 категорий + per-category redrive).
  • Эмпирика диффов — вся из КОДА: Diff-XYZ generation EM 0.95/0.94/0.68 (search-replace) vs 0.77/0.82/0.23 (unified); aider format-compliance 92.998.2% у среднего тира ⇒ 29% ответов малформед — у нас это закрыто гардами применения + откатом. Переносимость на русскую прозу нашим research прямо названа НЕизмеренной.
  • Предупреждения, учтённые в дизайне: APE над сильным черновиком не бьёт «ничего не делать» (WMT19 en→ru) ⇒ NO-CHANGE как первоклассный выход + замер против do-nothing; «negative optimization» у явных critique-артефактов; чекеры GalTransl на пунктуацию/паритет строк ВОЮЮТ с нашим reflow (22:118, 22:181) ⇒ вывод «строковые якоря запрещены» — мой, из этого конфликта, а не цитата из research/22; неоднозначный якорь обязан отвергать, а не патчить первый матч.
  • Дисциплина цитирования: отчёт прошёл собственную проверку на ОВЕР-АТРИБУЦИЮ (известный класс провала при лендинге research/N). Одна нашлась и исправлена — см. D1.1: формулировка про строчные якоря была подана как цитата research/22, а является моим выводом из зафиксированного там конфликта.

Приложение D. Объём и деньги (живые выходы rerun2, read-only, книга вне git)

Срез: 5 глав = 7 юнитов, источник 14 545 zh-символов, экспорты 4246k ru-символов на арм.

Сигнал dspro glm mistral
Хард-флаги (все — sanitizer_stripped, CJK-утечка) 3/7 юнитов (42.9%) 0 0
Глоссарные промахи (судья) 1 3 5
«час»-формы в выходе / из них НЕВЕРНЫХ (时辰≠час) 3 / 2 2 / 2 3 / 1
latin-residue / «-йть» в теле экспорта 0 / 0 1 / 1 0 / 0
markdown-заголовки / CJK в теле 0 / 0 0 / 0 0 / 0

Ground truth среза: 时辰 ×3, 千万 ×1, 数十万 ×1.

Книжный масштаб — ПЕРЕ-МЕРЕН МНОЙ ИСПОЛНЕНИЕМ (агент-инвентаризатор дал близкие, но не совпавшие числа; беру свои): guzhenren-utf8.txt = 7 778 990 символов (не 7 994 664); глав-节 = 2 106 на начале строки / 2 131 вхождений (не 2 283; 第N章 в книге всего 29 — книга размечена 节, что и отражает cjk-section.txt). При наблюдаемых ~2 078 исходных символах на юнит это ≈3 7003 900 юнитов (диапазон агента 3 2004 000 включает истину, но шире). Частоты сигнатур классов, пере-считанные grep'ом по всей книге: 时辰 ×266 (совпало с агентом), 千万 ×234, 数十万 ×62, [цифра]成 ×708.

Вывод от этой поправки не меняется: объём ремонт-кандидатов — сотни, не десятки тысяч; порядок стоимости петли сохраняется.

Три вывода, влияющие на решения:

  1. Доминирующий «дефект» книги уже чинится кодом: CJK-утечка = 42.9% юнитов у dspro, и StripCosmetic её снимает автоматически. Проблема не в починке, а в том, что ~1 4001 700 юнитов на книгу останутся ПОМЕЧЕННЫМИ для человека — при таком объёме флаг перестаёт быть сигналом. Это находка о ПОЛИТИКЕ флагов, не о ремонте; пингую оркестратору отдельно (§14).
  2. LLM-триггер экономически невозможен: судейский скрин $0.0637/юнит → $204255/книга при цели COGS $0.85. Триггер обязан быть детерминированным.
  3. Данные для $0-скана готовы: export-*.json = {book_id, total_units, …, chunks[{chapter, chunk_idx, disposition, final_text, source}]} — per-unit источник И финал, пере-чанкинг не нужен.

13-bis. Хвосты первого касания (байт-нейтральные, тем же паком) — диспозиции

Промт называет четыре; фиксирую, что с каждым будет, чтобы ни один не «растворился»:

Хвост Диспозиция Что уже проверено сейчас
Строгий декод book.yaml (САНКЦИОНИРОВАН) делаем; риска нет Пред-проверка выполнена: внесхемных ключей нет ни в одном стендовом/репозиторном book.yaml (прил. B) → флип KnownFields(true) в LoadBook (book.go:103) — нулевая миграция
Строгий декод пар/models-слоёв делаем тем же приёмом LoadModels (models.go:176) и LoadPair (pair.go:69) сегодня не строгие; внесхемных ключей в configs/models.yaml и в трёх configs/pairs/*.yaml нет; на стенде своих models.yaml/pairs/ вообще НЕТ (все книги смотрят в репо)
Unknown-key-тест пинит ИМЯ ключа делаем Сегодняшний TestUnknownConfigKeyIsRejected проверяет факт отказа, но не то, что сообщение называет опечатанный ключ — ужесточаем ассерт
Zero-value-паники PickedEntry/Bank гард ИЛИ документирование — решаю на стройке по факту чтения обоих типов Не диктую заранее: выбор между гардом и package-doc зависит от того, достижимо ли нулевое значение с живого колл-сайта; отчёт фазы 2 назовёт выбор и причину

Все четыре — байт-нейтральные (только error-пути, доки и тесты), то есть не трогают ни wire, ни снапшот, ни golden.

14. Пинги оркестратору (вне скоупа пака, замечено попутно)

  1. Политика флагов при книжном масштабе (прил. D, вывод 1): косметический авто-стрип помечает ~40% юнитов; на 3 000+ юнитов это шум, а не сигнал. Стоит решить: остаётся ли sanitizer_stripped флагом для человека, или становится чистой телеметрией.
  2. Док-дрейф: 12-go-style-notes.md §0.4 всё ещё описывает retired-форму prompts: {<пара>: файл}, которую пак-15 заменил конвенцией prompts/<пара>/<роль>.md (D39.23). Норматив надо поправить, иначе следующая сессия сошлётся на снятый механизм.
  3. Легаси-конфиги стенда (прил. B) упадут на строгом декоде — ожидаемо, но лучше знать до прогона.

Статус: ФАЗА 1 ЗАВЕРШЕНА. Кода не тронуто (git status по backend/ чист). Жду ратификации перед фазой 2.


§15. РЕВИЗИЯ 2 — результаты адверсариальной панели и вытекающие поправки

Что прогнано. Дизайн (в виде §0§14) отдан 6 независимым атакующим линзам — детерминизм/резюм · деньги · дисциплина D2/вердикт-ось · общность движка · доказательная база («стоит ли вообще строить») · критик полноты против текста промта. Каждое возражение уровня BLOCKER/MAJOR затем передано ОТДЕЛЬНОМУ скептику с установкой «по умолчанию возражение неверно — опровергни его по коду». Из поднятого выжило 4 MAJOR (13 верификаций). Сверх панели я воспроизвёл исполнением сам пять фактов — их и держу как несущие, потому что агентам верить на слово нельзя (правило проекта), а тут расхождения были у обеих сторон.

15.1. ГЛАВНОЕ: замер сменил рекомендуемый скоуп пака

Панель заметила, что вход для «$0-скана» (эшелон 3 моего же D6) лежал готовым, и прогнала его. Я воспроизвёл скан независимо (порт пяти детекций строго по dc-checkers.txt + алгоритму checkers.go, над rerun2/export-*.json, где на юнит лежат И источник, И финал):

Арм Юнитов dc1 dc2 percent latin broken ИТОГО кандидатов
deepseek-v4-pro (РАТИФИЦИРОВАННЫЙ прод-редактор, D39.22) 7 1 1 0 0 0 2
glm-5 (резервный арм) 7 1 0 1 1 1 4
mistral (ИСКЛЮЧЁН, D39.20) 7 1 2 1 0 0 4

И один из двух кандидатов прод-арма — ЛОЖНОЕ СРАБАТЫВАНИЕ. Проверено глазами по тексту: источник 杀了千万人的性命, перевод «загубил тысячи и тысячи жизней» — это штатная ГИПЕРБОЛА, ровно тот случай, о котором предупреждает заголовок самого чекера (checkers.go:186-188). То есть на единственном арме, который имеет значение, пять предложенных классов дают 1 настоящий дефект и 1 ложный на 7 юнитов.

Три дополнительных факта, каждый пере-мерен мной:

  1. Флагманский класс слабее, чем я написал. 时辰 встречается 266 раз на книгу, но shichen_re требует цифру/числительное перед 时辰, а самая частая форма — 半个时辰, 84 из 266 (32%) — не матчится вообще; всего срабатываний паттерна 120, а не 266. Мой §D3.3/прил. D завышали детектируемую поверхность в 2.2 раза.
  2. Профилактика уже стоит НА ОБЕИХ стадиях и никогда не мерилась. Не только editor.md:10, но и translator.md:13 — и там прямо назван пропущенный случай: «时辰 = 2 часа (三个时辰 ≈ шесть часов, 半个时辰 ≈ час)». Обе строки пришли коммитом 2f91b04 (2026-07-24 03:19), а отчёт пере-прогона датирован 2026-07-24 00:09 — то есть профилактика легла через ~3 часа ПОСЛЕ снятия цифр, на которых я строил выбор классов.
  3. У DC1 живое ЛОЖНОЕ СРАБАТЫВАНИЕ в пар-данных. ru_hours_re = (…|три|…)\s+час не имеет правой границы слова, поэтому «три части» матчится как «три час» (проверено исполнением на 5 пробах). Сегодня это безвредно (наблюдаемость), но АКТУАТОР на этом паттерне перепишет «три части» → «шесть часов» — и такая правка ПРОЙДЁТ ре-гейт.

Вывод, который я обязан доложить, даже приняв «фаза 1 принята в текущем виде»: строить платную LLM-петлю под эти цифры сейчас — это вера, а не замер, то есть ровно то, что north-star промта запрещает. Механизм спроектирован полностью и ратифицируем как есть; но порядок работ рекомендую сменить:

Рекомендуемый скоуп пака-16 (ревизия): всё, что стоит $0, — строим; платный вызов — за выключенным гейтом до замера остатка. А именно: (1) checks.RepairCandidates (позиционная детекция, вердикт-нейтральная); (2) read-only скан-поверхность (измеряет остаток по классам за $0 на любой уже прогнанной книге); (3) два пар-данных бага-фикса: правая граница в ru_hours_re и слепота shichen_re к 半个时辰 — оба $0, оба чинят СЕГОДНЯШНЮЮ наблюдаемость, оба обязательны ДО того, как класс станет актуатором; (4) машинерия ремонт-вызова по §D3D5 с поправками §15.2 — под enabled: false, включение отдельным решением владельца после того, как скан на пост-профилактическом прогоне покажет остаточный объём.

Это НЕ отказ от петли и не сужение задачи по своей инициативе — это перестановка платного шага за замер, и решение остаётся за оркестратором (вопрос §13.3 теперь имеет цифры).

15.2. Выжившие возражения и поправки к дизайну

(A) MAJOR — ре-чек всех пяти классов удовлетворяется УДАЛЕНИЕМ. Проверено мной по коду: удали спан — lintTimeUnits не найдёт ru_hours_re (checkers.go:140-142), lintMagnitudeScale не найдёт fire-word (:199-200), lintPercentScale не найдёт fraction-cue (:266), lintLatinResidue не найдёт токен (:305), lintBrokenWord не найдёт слово (:353) — все вернут 0. Это ровно тот дисквалификатор, по которому я сам отверг dc6. Мой коридор длины 0.52× не спасает: удаление короткого токена из предложения его не нарушает. Поправка — ре-гейт становится ПОЗИТИВНЫМ, а не «отсутствие флага»:

  • dc1: в замене ОБЯЗАН найтись hours-оборот, и его число обязано равняться 2n (утверждение, а не молчание);
  • dc2: ok-суппрессор обязан СМАТЧИТЬСЯ (qianwan_ok_re/shushiwan_ok_re), а не просто «fire-word исчез»;
  • percent: в замене обязана быть процентная форма со значением, равным вычисленному pct;
  • latin/broken: дефектный токен исчез И число словных токенов замены ≥ (исходное 1) — анти-удаление;
  • глобально: каждое АРАБСКОЕ число исходного спана обязано присутствовать в замене, кроме числа-цели класса.

(B) MAJOR — счётчики в retrieval_state обнулялись бы на каждом резюме. persistRetrievalState собирает строку с нуля (chunkrun.go:106-112), UpsertRetrievalState перезаписывает ВСЕ перечисленные колонки (glossary.go:256-274), драфт-волна выполняется ДО edit-волны и зовёт запись безусловно (waverun.go:371-375) — а ремонт на резюме не повторяется и переписать счётчики некому. Поправка — миграция v11 ОТМЕНЯЕТСЯ, счётчики выводятся из durable-артефактов, которые уже есть: calls = число чекпойнтов с role='repair' для юнита; declined = те, чей ответ равен сентинелу; applied = final_hash несёт префикс tm-repair-v1:; rejected = остальные. Это self-healing на резюме и ровно доктрина репозитория («recomputed deterministically each run»). Ноль новых колонок, ноль миграции.

(C) MAJOR — перекрывающиеся спаны. Гард однозначности — ПО КЛАССУ, поэтому два кандидата разных классов в одном (или соседнем) предложении дают перекрытие всегда, а не в углу; сплайс по убыванию смещения тогда режет уже изменённую строку → в худшем случае паника по границам среза или разрез посреди руны — в оплаченном прогоне. Поправка: после расширения до предложения кандидаты проверяются на пересечение; при пересечении остаётся первый в детерминированном порядке, остальные ОТБРАСЫВАЮТСЯ (не чинятся). Перед сплайсом — явное утверждение непересечения.

(D) MAJOR — отказ резервации по потолку убил бы книгу. runAttempt возвращает ошибку с errReserveCeiling (stagerun.go:373-376); во всём репозитории её ловит РОВНО одно место — escalation.go:137, и его доктрина дословно: опциональный хоп деградирует, а не роняет книгу. Ремонт опциональнее того хопа, но стоит ДО записи chunk_status, поэтому проброс ошибки потерял бы строку уже оплаченной успешной редактуры, и так на каждом резюме. Поправка: ремонт ловит errReserveCeiling и деградирует в «ремонта не было»; любая другая инфра-ошибка по-прежнему падает громко.

(E) MAJOR-половина — classify над ответом ремонта НУЖЕН, а не вреден. Скептик развернул возражение: модель, ответившая «Не могу помочь…», проходит ВСЕ пять моих гардов (непусто, finish=stop, коридор длины, без переводов строк, CJK-подмножество) и была бы вклеена в прозу; classify ловит это как soft_refusal (disposition.go:247-249 + lang/data/refusal.txt). Поправка: ответ ремонта прогоняется через classify (и, если гейт включён, через санитайзер) как шестой гард.

(F) Уточнения, принятые без спора:

  • Хеш ремонта — ПОЛНАЯ позиционная ось (BookID/Chapter/ChunkIdx/Attempt/Stage/Role/Model/SnapshotID/Messages), как в runAttempt; формулировку «одинаковые сообщения → один хеш → бесплатный дедуп» из D3.4 снимаю: это не фича, а следствие, и подавать её как фичу опасно.
  • Бюджет обходится чекпойнтом: уже оплаченный ремонт реплеится независимо от остатка бюджета (escalation.go:99-114 — иначе резюм после исчерпания бюджета отдал бы ДРУГИЕ байты).
  • buildSnapshotID не знает волны (snapshot.go:219) — фолд «только в волну финальной стадии» требует явного третьего параметра, который передаёт snapshotIDForWave. Механизм назван, а не подразумевается.
  • style_check_version — обязательство доказательством. Позиционная детекция не смеет двигать вердикт ни одного существующего чекера; CheapGateVersion НЕ бампится, и это доказывается исполнением (golden байт-идентичен + счётчики на фикстурном корпусе не сдвинулись). Если хоть один счётчик поехал — это громкий бамп и пере-капчер, то есть провал дизайна, а не «мелочь».
  • budget_usd в снапшот НЕ фолдится (скептик показал: эскалационный бюджет тоже не фолдится, а влияет на результат сильнее; фолд доллара пере-биллил бы книгу при правке потолка).
  • Мягкий кап зависит от планировщика — да, но это ратифицированное свойство уже шиппящегося кап-механизма (escalation.go:48-55 + мьютекс, пиненный TestWaveEscalationBudgetSerializedUnderParallelism), а не новый класс. Формулировку линзы 1 «весь ремонт — чистая функция» смягчаю: чист сам ремонт; ПОРЯДОК допуска под исчерпанным бюджетом — нет, и это наследуемое, задокументированное свойство.
  • Деньги: thinking-токены. DeepSeek считает thinking внутри completion_tokens (subset), а он обязан быть ВКЛЮЧЁН — значит выход ремонта больше «ста токенов». Честная вилка: $0.00010.0006 за вызов, то есть $0.050.30 на книгу при сотнях вызовов (было «$0.050.10» — занижено).
  • D13.6 для repair-модели невозможно применить к блоку без ключа: либо gates.repair.reasoning_max_tokens, либо запрет additive-провайдера в этой роли. Выбираю запрет (least mechanism): repair — дешёвая роль, ей нечего делать на xAI; попытка = громкая ошибка на загрузке.
  • Формулировка про диспозиции уточнена. «Выбранные классы сегодня не диспозиции» верно для CHEAP-гейтов, но у санитайзера есть СУБСТАНТИВНЫЕ однофамильцы (LatinInsert/BrokenWord, sanitizer.go:146-147,161-162), и он включён во всех шиппинг-конфигах. Противоречия в механизме нет (ремонт входит только на DispOK, то есть видит лишь текст, который санитайзер пропустил), но фраза была неточной.
  • Общность — честная граница. «Новая пара = только данные» верно для ДОБАВЛЕНИЯ пары. Но три вещи привязаны к target=ru в Go до этого пака и независимо от него: гейт isRuTarget живёт на колл-сайтах (waverun.go:476, chunkrun.go:51), text.TokenizeCyrillic кириллице-специфичен (runes.go:41), и множитель «时辰 = ×2» захардкожен (checkers.go:148, при том что dc-checkers.txt объявляет алгоритм generic). Ремонт этого не добавляет, но делает АКТУАТОРОМ — поэтому фиксирую как унаследованный долг и как ограничение: класс работает на новой ПАРЕ, но не на новом ТАРГЕТЕ без правки Go.
  • Fail-loud промпта — только для применимых классов: файл repair/<класс>.md требуется, если класс включён И у пары есть данные этого класса; иначе пара была бы обязана заводить промты под чужие дефекты.

15.3. Дополнения к таблице расхождений §10 (панель нашла пропуски)

Приор/инвариант промта Что я делаю Почему это надо было объявить
9 Конвенция промптов prompts/<пара>/<роль>.md (ратифицирована D39.23) prompts/<пара>/repair/<класс>.md Ремонт — не роль стадии; ключ — класс дефекта. Это ОТКЛОНЕНИЕ от ратифицированной конвенции, и в §10 его не было
10 D3: «src-спан + draft-спан + типизированная ошибка → исправление» Тип ошибки доезжает до модели как ВЫБОР ФАЙЛА промпта, а не как интерполированная строка Диагностика чекеров англо-первичная (checkers.go:150 и др.); вставлять её в русский промпт — языковая утечка. Но это именно отклонение от буквы D3, а не её исполнение
11 «no-repair путь байт-идентичен» Доказывается не только repairSnap == nil, но и обязательством не двигать CheapGateVersion Позиционная детекция трогает checkers.go, который версионируется и фолдится в ОБЕ волны

15.4. Что панель НЕ смогла сломать (проверено скептиками, держится)

Форма петли: суб-шаг вместо стадии · derived-чекпойнт с namespace-id · отсутствие новых строк chunk_status · фолд указателем+omitempty · атрибуция денег через role='repair' без миграции · вывод D3a о непересечении множеств классов (двойная оплата невозможна по построению) · отказ от фолда budget_usd · мьютекс-кап по образцу эскалации. Ни одно из этих решений не пало.

15.5. Итог фазы 1

Дизайн ратифицируем — с поправками §15.2 (они внутри дизайна, переархитектуры не требуют) и с открытым вопросом о скоупе §15.1, который теперь подкреплён цифрами, а не мнением. Кода по-прежнему не тронуто.


§16. ДЕЛЬТА К §15 — ПОЛНЫЙ РЕЗУЛЬТАТ ПАНЕЛИ (пришёл ПОСЛЕ ратификации D39.24)

§15 выше не редактирую — он ратифицирован в коммите 811f561 и остаётся как есть. Здесь дельта. Скоуп D39.24 этой дельтой НЕ отменяется (все новые находки касаются АКТУАТОРА, который ратифицирован под enabled: false); меняются два пункта содержания, помеченные ⇒ОРКЕСТРАТОРУ.

Что произошло. §15 писался по ЧАСТИЧНОМУ результату панели: на тот момент было доступно 13 верификаций и 4 выживших возражения. Полный прогон: 48 возражений BLOCKER/MAJOR → 48 скептик-верификаций → 13 выживших (3 BLOCKER, 9 MAJOR, 1 сведён в MINOR; 35 опровергнуто). Три BLOCKER'а и четыре MAJOR'а в §15 не попали.

16.1. ⚠ Моя поправка §15.2(A) НЕДОСТАТОЧНА и частично НЕВЕРНА (3 BLOCKER)

Ре-чек всех пяти классов срабатывает на ОТСУТСТВИЕ, а не на восстановленный инвариант — это §15.2(A) поймал. Но панель показала три вещи, которых (A) не закрывает:

  1. DC1 снимает флаг при ЛЮБОМ ruNum != n (checkers.go:148-149): «три часа» → «пять часов» — новое НЕВЕРНОЕ число — ре-гейтится ЗЕЛЁНЫМ; вычисленное expectedHours тут же выбрасывается.
  2. Позитивный пост-инвариант ruNum == 2n НЕ СТРОИТСЯ на сегодняшних пар-данных. ru_hour — 9 форм (dc-checkers.txt:19-27), ru_hours_re косвенных падежей не покрывает: КОРРЕКТНАЯ починка «шести часов» / «тремя часами» / «6 ч.» даёт hm == nil и была бы ОТВЕРГНУТА, а неверная «пять часов» — принята.
  3. DC2 и percent снимаются ещё дешевле: dc2 — конъюнкция, достаточно убрать «тысяч» ИЛИ вставить «миллион» где угодно в юните (checkers.go:198-203); percent возвращает 0 при ЛЮБОМ «%» в юните (:263), так что «6%» вместо «66%» проходит.

⇒ОРКЕСТРАТОРУ (i): пункт (3) ратифицированного скоупа (фиксы пар-данных) — это не улучшение наблюдаемости, а ПРЕДУСЛОВИЕ любого будущего актуатора: позитивный пост-инвариант формулируется на ДАННЫХ (расширенные формы единиц), а не в Go. Класс dc1 до этого фикса — детектор, не актуатор.

16.2. ⚠ У dc2 и percent_scale нет целевого якоря в принципе (MAJOR)

Их fire-предикаты — Contains по ВСЕМУ юниту (checkers.go:199-205, :263-266); позиционной связи src↔dst нет ни в одном. qianwan_fire_word = голая подстрока «тысяч», матчит «тысячелетие»/«тысячи лет»; при ~6 000 ru-символах на юнит одно НЕСВЯЗАННОЕ «тысяч…» — норма, а не край. И мой «гард однозначности» — это КАРДИНАЛЬНОСТЬ, а не выравнивание: один матч слева + один справа не доказывают соответствия (формулировка D1.3 «прямо закрывает главную дыру» была переоценкой — признаю).

⇒ОРКЕСТРАТОРУ (ii): дефолт-набор классов при будущем включении сужается — dc2 и percent_scale выбывают (нет якоря; и именно dc2 дал единственное ЛОЖНОЕ срабатывание на прод-арме, §15.3). Первыми актуаторами становятся latin_residue и broken_word: у них токен-уровневые оффсеты уже вычисляются (checkers.go:290-303), а детекция zero-FP по построению.

16.3. Поправки, которые я вношу в СТРОЙКУ без изменения скоупа

  • (F) repairMu НЕ вводится. Оценка «перебор ≤1» достижима только удержанием лока ЧЕРЕЗ провайдерский вызов (escalation.go:118-124), а обоснование эскалации дословно опирается на РЕДКОСТЬ. Ремонт — 1327% юнитов; deepseek-v4-flash не имеет rate_limit (models.yaml:117-125), значит мьютекс стал бы единственным ограничителем с лимитом 1, а под локом лежит транспортный ретрай (attempt_s:240 × 3) — один 429-шторм заморозил бы всех воркеров. При $0.50 против $0.0001/вызов кап и так не связывает. → бюджет читается без сериализации, перебор документируется как ≤ (workers1).
  • (C+) непересечение спанов проверяется ДО оплаты (иначе платим за неприменимый вызов), тайбрейк по классу задан явно, перед сплайсом — утверждение непересечения И валидности UTF-8 (панель показала путь к панике по границам среза и к разрезу посреди руны в оплаченном прогоне).
  • числовая слепота: NumberDrift живёт за RegressionEnabled, которого нет НИ В ОДНОМ шиппинг-конфиге, поэтому в «cheap-суита не выросла» этот член структурно нулевой — числовой инвариант обязан быть класс-специфичным утверждением значения, а не «множество чисел сохранилось» (моё первое предложение было равенством множеств и убивало как раз число-центричные классы).

16.4. Что панель НЕ сломала (устояло под скептиками)

Суб-шаг вместо стадии · derived-чекпойнт с namespace-id · ноль новых строк chunk_status · фолд указателем+omitempty только в волну финальной стадии · деньги через role без миграции · D3a о непересечении множеств классов · отказ фолдить budget_usd · счётчики без миграции (§15.2 B).