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

636 lines
108 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# ПАК-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) 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_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" + <reqHash терминальной попытки> + "\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-воркеров каждый прочитают «бюджет есть» и перебор составит до 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`, `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.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).