# ПАК-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_escalate`→`escalate_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-структурах: ```go // 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=` ПУСТО, так что аддитив там был бы безопасен — но он не нужен.) 3. **ГАРД ОДНОЗНАЧНОСТИ (несущий):** класс порождает кандидата, **только если в юните ровно один матч с каждой релевантной стороны**. Два 时辰 в источнике и одно «час…» в переводе — кандидата НЕТ, остаётся сегодняшний флаг. Это прямо закрывает главную дыру, найденную инвентаризацией: сегодняшние src↔dst-чекеры сопоставляют ПЕРВЫЙ матч с ПЕРВЫМ (`checkers.go:132,140`), то есть выравнивания у нас нет и его неоткуда взять. Прайор-арт формулирует то же правило: «неоднозначный якорь (≥2 матчей) обязан отвергать, а не молча патчить первый». 4. **Ремонт применяется к СЫРОМУ тексту завершившейся стадии** (тому же, что видят чекеры), а не к экспортному: `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`, `quality` → `ProcessedUnits 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`, так что опечатка = громкая ошибка): ```yaml 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) 500–1000 вызовов на книгу ≈ **$0.05–0.10** против COGS-цели $0.85 — приемлемо. Для контраста: LLM-судья как триггер стоил бы **$204–255 на книгу** (замер стенда) — поэтому **триггер обязан быть детерминированным**, и это не стилевое предпочтение, а арифметика. - Валидация конфига при `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_hash` → `sr.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`, `runEditUnit` — `editSel.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 ### Снапшот ```go 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" + + "\x00" + <отремонтированный текст>)`, контент-адресуемый, коллизия с hex-хешем реальной попытки невозможна, пишется ДО того, как на него сошлётся `chunk_status`. - `chunk_status`: диспозиция `ok` (не меняется), `final_hash` → derived. **Ни одной новой строки** (`status.go:307` — `expected := 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-воркеров каждый прочитают «бюджет есть» и перебор составит до N−1 вызовов. С мьютексом перебор ≤ 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-триггер ($204–255/книга) исключён по арифметике, а не по вкусу. *Остаточный риск:* резервация сайзится по `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`, `Contains`→`Index`). Позиция недостижима по природе: 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.9–98.2% у среднего тира ⇒ 2–9% ответов малформед — у нас это закрыто гардами применения + откатом. Переносимость на русскую прозу нашим 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-символов, экспорты 42–46k 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 700–3 900 юнитов** (диапазон агента 3 200–4 000 включает истину, но шире). Частоты сигнатур классов, пере-считанные grep'ом по всей книге: **时辰 ×266** (совпало с агентом), **千万 ×234**, **数十万 ×62**, `[цифра]成` ×708. Вывод от этой поправки не меняется: объём ремонт-кандидатов — сотни, не десятки тысяч; порядок стоимости петли сохраняется. **Три вывода, влияющие на решения:** 1. **Доминирующий «дефект» книги уже чинится кодом:** CJK-утечка = 42.9% юнитов у dspro, и `StripCosmetic` её снимает автоматически. Проблема не в починке, а в том, что ~1 400–1 700 юнитов на книгу останутся ПОМЕЧЕННЫМИ для человека — при таком объёме флаг перестаёт быть сигналом. Это находка о ПОЛИТИКЕ флагов, не о ремонте; пингую оркестратору отдельно (§14). 2. **LLM-триггер экономически невозможен:** судейский скрин $0.0637/юнит → $204–255/книга при цели 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) машинерия ремонт-вызова по §D3–D5 с поправками §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.5–2× не спасает: удаление короткого токена из предложения его не нарушает. **Поправка — ре-гейт становится ПОЗИТИВНЫМ, а не «отсутствие флага»:** - 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.0001–0.0006 за вызов**, то есть **$0.05–0.30 на книгу** при сотнях вызовов (было «$0.05–0.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`), а обоснование эскалации дословно опирается на РЕДКОСТЬ. Ремонт — 13–27% юнитов; `deepseek-v4-flash` не имеет `rate_limit` (`models.yaml:117-125`), значит мьютекс стал бы единственным ограничителем с лимитом 1, а под локом лежит транспортный ретрай (`attempt_s:240 × 3`) — один 429-шторм заморозил бы всех воркеров. При $0.50 против $0.0001/вызов кап и так не связывает. → бюджет читается без сериализации, перебор документируется как ≤ (workers−1). - **(C+) непересечение спанов** проверяется ДО оплаты (иначе платим за неприменимый вызов), тайбрейк по классу задан явно, перед сплайсом — утверждение непересечения И валидности UTF-8 (панель показала путь к панике по границам среза и к разрезу посреди руны в оплаченном прогоне). - **числовая слепота:** `NumberDrift` живёт за `RegressionEnabled`, которого нет НИ В ОДНОМ шиппинг-конфиге, поэтому в «cheap-суита не выросла» этот член структурно нулевой — числовой инвариант обязан быть класс-специфичным утверждением значения, а не «множество чисел сохранилось» (моё первое предложение было равенством множеств и убивало как раз число-центричные классы). ## 16.4. Что панель НЕ сломала (устояло под скептиками) Суб-шаг вместо стадии · derived-чекпойнт с namespace-id · ноль новых строк `chunk_status` · фолд указателем+omitempty только в волну финальной стадии · деньги через `role` без миграции · D3a о непересечении множеств классов · отказ фолдить `budget_usd` · счётчики без миграции (§15.2 B).