> **Ревью-шапка оркестратора №7 (25.07, приёмка EXECUTE-FIRST: воркфлоу 5 линз ~610k ток. + инлайн-доприёмка дельты).** 0 блокеров. Все три §9-блокер-сценария отклоняются — сквозные репродукции через настоящий драйвер («три часа»→«пять часов» · удаление придаточного на уровне спана · реплей оплаченного ремонта под исчерпанным бюджетом байт-в-байт при $0); шесть MAJOR-фиксов реальны (четырём без in-tree тестов построен overlay-харнесс — все прошли); ценз 399→433 воспроизведён исполнением, удалено 0; golden `f7641993…` байт-в-байт; версия пака `f83acc…`→`eaea82…`→`a7d4be2c0465` — ровно от ратифицированных правок данных. Двойной MAJOR приёмки (hourWordRE: нет левой границы → «тотчас» отгружал удаление длительности; ru-парадигма в Go) закрыт дельтой: `hour_word_re` в пар-данных с ОБЕИМИ границами + load-guard пары. **Решения оркестратора:** §9.2 = вариант (а) — both-wave re-pin принят ценой фикса ($0-резюм — внутри-снапшотный инвариант; resnapshot = переоплата ОДНА, D30.9; пер-волновой суб-хеш записан опцией на эру масштаба, покрывает и `hour_word_re`-сдвиг). **§16.2 РАТИФИЦИРОВАНА** (dc1-классы понижены до детекторов; дефолт-актуаторы = broken_word+latin_residue). Документирование `brokenWordCandidates`/Cyrillic принято (аргумент вердикт-нейтральности верный; долг чинится одной правкой target-сеама). **В очередь:** per-class исходы ремонта = схемное решение (класс не durable — колонка или формат derived-id), принимается вместе с решением о включении петли; включение платного пути — владелец, по замеру остатка на пост-профилактическом прогоне. **Норма приёмки пополнена (правило 11):** пак, меняющий отгружаемые байты / добавляющий исполняемый механизм, самопроверяется И принимается сэндбокс-репродукциями через драйвер — девять дефектов фазы 2 прошли тесты автора и чтение, поймало только исполнение. Кредит: ценз-норма `^func Test` впервые поймала снос тестов ДО отчёта (7 восстановлены). # ПАК-16 «слой-3 diff-редактор» — ПРИЁМОЧНЫЙ ОТЧЁТ ФАЗЫ 2 (25.07.2026) > Бэкенд-сессия. Источник: `docs/BACKEND_PACK16_DIFF_EDITOR_SESSION_PROMPT.md`; дизайн ратифицирован **D39.24** > (`811f561`) по отчёту `PACK16_DESIGN_2026-07-25.md` §0–14 + §15.2, скоуп — перестановка §15.1. > **Сессия НЕ коммитит.** `git mv` не использовался. Каждое числовое утверждение ниже снабжено КОМАНДОЙ, > которой оно получено (норма отчётности; заявка без команды — не заявка). > ## ⚠ ПЕРВАЯ РЕДАКЦИЯ ЭТОГО ОТЧЁТА СОДЕРЖАЛА ЗАВЫШЕННЫЕ ЗАЯВКИ — исправлено, см. §9 > Адверсариальная панель по ДИФФУ (4 линзы → 18 находок, **все 18 выжили** у скептиков, 3 из них BLOCKER) > нашла, что часть заявок первой редакции была неверна. **Девять находок ИСПРАВЛЕНЫ в коде** (§9.1), > **одна вынесена на решение оркестратора** (§9.2). Таблица ниже приведена в соответствие; строка про > $0-резюм драфта переформулирована — она была верна только про repair-фолд. ## Вердикт (с поправкой §9): 4 ратифицированных пункта построены, но приёмка НЕ чистая | Инвариант | Как проверен | Результат | |---|---|---| | golden БАЙТ-ИДЕНТИЧЕН, пере-капчер НЕ нужен | `TM_UPDATE_GOLDEN=1 go test ./internal/pipeline/ -run TestGolden -count=1 && sha256sum internal/pipeline/testdata/golden/capture.golden && git status --porcelain internal/pipeline/testdata/` | sha `f7641993e37f7ec619dfc5d2914a342d796a7cecd92080e33704f3974f0020f8` — ТОТ ЖЕ, что на baseline до пака; `git status` по testdata пуст | | майнер-парити EXACT | `TM_MINER_PARITY=1 go test ./internal/miner/ -run Parity -count=1 -v` | `PARITY: n=13618 catastrophe{方源:0 蛊:1 蛊师:2 古月:22} recall@proposed=0.9655 (56/58 GT)` — тождественно паку-15 | | build / vet / test -race | `go build ./... && go vet ./... && TM_MINER_PARITY=1 go test -race -count=1 ./...` | зелено; **12 пакетов `ok`** (все, где есть тесты) | | `gofmt` | `gofmt -l internal/ cmd/` | только `internal/llm/llm.go` — ПРЕ-существующее, не тронуто (зафиксировано ещё паком-15) | | ни один тест не удалён | дифф `^func Test` ИСПОЛНЕНИЕМ (см. §6) | было **399**, стало **433**, удалено **0** | | CheapGateVersion НЕ сдвинут | `git diff backend/internal/checks/cheapgates.go` | файл не тронут вовсе; версия-константа на месте | --- ## 1. Что сделано (по ратифицированным пунктам) ### (1) `checks.RepairCandidates` — позиционная детекция, вердикт-нейтральная Новый файл `internal/checks/repair.go` (321 строка): `RepairClass`, `RepairCandidate{Class, DstSpan, SrcSpan, Detail}`, `RepairCandidates`, `ExpandToSentence`, `DisjointCandidates`. **Вердикт-нейтральность обеспечена КОНСТРУКЦИЕЙ, а не аккуратностью:** ни одна существующая lint-функция не тронута — новый код заново выводит попадание теми же скомпилированными пар-паттернами. Поэтому `CheapGateVersion` не двигается (его сдвиг обнулил бы снапшот КАЖДОЙ книги — он фолдится безусловно, `snapshot.go: StyleCheckVersion`). Эквивалентность доказывается исполнением, а не заявляется: `TestRepairCandidatesAgreeWithLints` гоняет корпус из 9 пар (src, final), покрывающий и срабатывание, и молчание, и требует: кандидат класса появляется ТОЛЬКО там, где сработал его lint. Несущий механизм — **гард однозначности**: класс даёт кандидата, только если его свидетельство встречается ровно один раз с каждой стороны. Сегодняшние src↔dst-чекеры сопоставляют ПЕРВЫЙ матч с ПЕРВЫМ (`checkers.go:132,140`) — это эвристика, а не выравнивание, и на двух вхождениях она способна связать несвязанное. Отказ выдавать адрес честнее догадки (`TestRepairCandidateAmbiguousAnchorYieldsNothing`). **Классы, которых тут НЕТ и почему** (поправка §16.2, ОДОБРЕНА ПРИЁМКОЙ оркестратора; ратификацию оформляет ревью-шапка): DC2 (千万/数十万) и 成-percent срабатывают `Contains`-ом по ВСЕМУ юниту, позиционной связи с исходным матчем нет — спан от них указал бы в невинную прозу. Они остаются наблюдаемостью. ### (2) Read-only скан остатка — $0-измерение `QualityReport` (`internal/pipeline/quality.go`) получил `repair_candidates` + `repair_candidates_by_class` (и `repair_candidates` на юнит). Считается той же read-only проекцией, что уже пересчитывает структурный KPI: никаких LLM, снапшота и чекпойнт-реплеев. Оба поля `omitempty` + nil-мапа → отчёт чистой книги байт-идентичен прежнему. В CLI выводится строкой `REPAIR RESIDUAL:` только при ненулевом остатке (`cmd/tmctl/render.go`). Оговорка зафиксирована в коде, а не умолчана: скан меряет ЭКСПОРТ-нормализованный текст, а in-loop детектор увидит сырой ответ — расходятся ровно на восстановимый глиф-фолд. ### (3) Два фикса пар-данных (`configs/langpacks/zh-ru/dc-checkers.txt`) **(а) Правая граница `ru_hours_re`.** Было `…\s+час` — голый стем, который матчит ВНУТРИ слов: «три части» читалось как «три час…». Живой false positive сегодня и ложный триггер АКТУАТОРА завтра. Стало: явная парадигма + обязательный не-кириллический символ или конец текста (в Go RE2 нет lookahead, а `\b` — только ASCII, поэтому граница пишется руками). Рецессии recall нет — косвенные формы («часах», «часов») остались в парадигме. Проверено: `TestDC1HoursWordBoundary` (2 FP-кейса + 2 TP + 1 корректная конверсия). **(б) Слепота к 半个时辰.** Группа счёта в `shichen_re` принимает ТОЛЬКО числительное, а 半 — не числительное, поэтому самая частая форма меры была невидима классу целиком. Добавлен отдельный дробный зонд (src-паттерн + target fire-word — идиома DC2), data-gated на ОБА ключа (empty-probe гард пака-15): пара без ключей гоняет под-проверку инертно. Проверено: `TestDC1FractionalUnit`, `TestDC1FractionalProbeInertWithoutData`. **Классификация сдвига (запрошена оркестратором): VERSION-ONLY по паку.** Алгоритм остался generic, изменились ДАННЫЕ → двигается `pack.Version()` → `LangpackVersion` в снапшоте → для zh-ru книги это громкий `--resnapshot`. Книга БЕЗ пака (в т.ч. golden ja→ru) не затронута — что и доказано байт-идентичностью golden. `CheapGateVersion` не бампится: новая ветка инертна без данных, поэтому «сдвиг счётчиков всегда сопровождается громким сдвигом версии» остаётся верным для всех. ### (4) Машинерия ремонта под `enabled: false` `internal/pipeline/repair.go` (508 строк) + интеграция в `stagerun.go` между разрешением эскалации и записью `chunk_status`. Реализованы все поправки §15.2: - **(A) позитивный ре-гейт + анти-удаление.** Шесть гардов на ответ: вердикт классификатора · пустота · сентинел `⟦TM-NOCHANGE⟧` · коридор длины 0.6–2.0× · запрет новых переводов строк · script-гард (ни одной новой руны исходного письма) · сохранение ≥60% словных токенов · сохранение всех цифровых прогонов, кроме ровно одной замены значения. Плюс ре-гейт по СОБРАННОМУ тексту: адресуемых дефектов стало меньше · cheap-суита не выросла · `classify` ok · санитайзер не загрязнён · пост-чек глоссария не приобрёл confirmed-промахов. - **(B) счётчики БЕЗ миграции.** Выводятся из durable-артефактов (`role='repair'`-чекпойнты + namespace `tm-repair-v1:` у `final_hash`), потому что `persistRetrievalState` строит строку с нуля и обнулил бы любые колонки на каждом резюме драфт-волны. Ноль новых колонок. - **(C) непересечение спанов** — `DisjointCandidates` ДО оплаты; перед сплайсом — проверка границ и `utf8.ValidString` по собранному тексту. - **(D) `errReserveCeiling` — деградация**, а не падение книги. - **(E) `classify` над ответом** — шестой гард (ловит «Не могу помочь…», которое прошло бы все контентные). - **(F, §16.3) `repairMu` НЕ введён**: оценка «≤1 перебор» требует держать лок ЧЕРЕЗ провайдерский вызов, а обоснование эскалации опирается на её редкость; ремонт — общий путь, и лок над транспортным ретраем заморозил бы всех воркеров. Кап читается без сериализации, перебор задокументирован как ≤ (workers−1). Промпты пары — `prompts/zh-ru/repair/<класс>.md` (4 файла), по конвенции, fail-loud с названным путём. Тип ошибки доезжает до модели ВЫБОРОМ ФАЙЛА (отклонение §15.3, одобрено приёмкой): англо-первичная операторская диагностика в русском промпте была бы языковой утечкой. Снапшот: `repairSnap` — указатель+omitempty, фолдится **только в волну финальной стадии** (`snapshotIDForWave`), поэтому включение гейта не двигает драфт-волновой снапшот и $0-резюм драфта цел. Проверено исполнением: `TestRepairFoldsOnlyIntoFinalStageWaveSnapshot` сравнивает id обеих волн с гейтом и без. ### Хвосты первого касания (все четыре) | Хвост | Сделано | Проверка | |---|---|---| | Строгий декод `book.yaml` | `KnownFields(true)` в `LoadBook` | `TestStrictDecodeRejectsUnknownKeysInBookAndPair`: `langpack_extned:` падает с названным ключом, чистый book.yaml грузится | | Строгий декод pair/models | то же в `LoadModels`, `LoadPair` | тот же тест (pair-половина) + весь набор шиппинг-конфигов грузится (полный прогон тестов) | | Unknown-key-тест пинит ИМЯ | новый `TestUnknownKeyErrorNamesTheKey` | пинит `promt_override` и `segmantation` дословно | | Zero-value паники `PickedEntry`/`Bank` | ГАРД (не документирование) | `TestZeroValuePickedEntryAndBankDoNotPanic` | **Почему гард, а не документирование** (выбор был оставлен мне): оба типа экспортированы ВМЕСТЕ с экспортированными полями, поэтому нулевое значение конструируется снаружи пакета кем угодно, а расплата — nil-dereference в ОПЛАЧЕННОМ прогоне. Контракт теперь: запись без строки пропускается, банк без матчера ничего не выбирает. Поведение реального банка измениться не может — `Materialize`/`Select` всегда заполняют оба. --- ## 2. Замер на стенде (read-only, $0, книга вне git) Прогнан ШИППИНГ-детектор (`internal/checks`) над живыми выходами `rerun2/export-*.json` — теми самыми артефактами, которые D6 называет эшелоном 3. Ни одного вызова провайдера, ни одной записи. | Арм | Юнитов | Адресуемых дефектов | Разбивка | |---|---|---|---| | **deepseek-v4-pro (прод, D39.22)** | 7 | **2** | dc1_time_units 1 («прошло три часа» ← 三个时辰) · dc1_fractional 1 («За полчаса» ← 半个时辰) | | glm-5 (резерв) | 7 | 4 | dc1 1 · dc1_fractional 1 · latin_residue 1 · broken_word 1 | | mistral (исключён D39.20) | 7 | 1 | dc1 1 | **Два вывода, которые стоит прочитать вместе.** 1. **Дробный зонд поймал РЕАЛЬНЫЙ дефект прод-арма, невидимый всей прежней сюите:** источник «半个时辰之后» (≈ час) отдан как «За полчаса» — ошибка длительности в 2×. Проверено чтением контекста обеих сторон. 2. **Ложного срабатывания в адресуемом наборе больше НЕТ.** В §15.3 (дизайн-фаза) прод-арм давал 2 кандидата, из которых один был ложным (千万 = штатная гипербола «тысячи и тысячи жизней»). После исключения dc2/percent из адресуемых классов оба оставшихся кандидата — настоящие дефекты. Решение §16.2 окупилось измеримо. **Блокер, о котором надо знать (пре-существующий, НЕ от этого пака):** прогнать `tmctl report` прямо на стендовых БД нельзя — они на схеме **v9**, бинарь ждёт **v10**, а read-only открытие принципиально не мигрирует. Мигрировать можно только ПИШУЩЕЙ командой по оплаченной книге владельца — без санкции я этого не делал. Санкционированный путь, если нужен именно CLI-выход: скопировать БД и книгу в отдельный каталог и прогнать пишущую команду по КОПИИ (все юниты разрешены → резюм за $0). --- ## 3. Что НЕ сделано и почему - **Платный путь не включался.** `enabled: false` по умолчанию; включение — решение владельца по замеру остатка на пост-профилактическом прогоне (вне пака, D39.24). - **dc2 / percent_scale не введены в адресуемый набор** — §16.2, одобрено приёмкой оркестратора. - **Косвенные формы часов в пар-данных не расширялись.** Ратифицированный скоуп называл ГРАНИЦУ и 半个时辰. При этом §16.1 остаётся в силе: позитивный пост-инвариант для dc1 (утверждать «стало 2N часов») на 9-формной таблице `ru_hour` построить нельзя — «шести часов»/«тремя часами»/«6 ч.» не матчатся. Пока это так, dc1 в петле опирается на общие контентные гарды, а не на класс-специфичное утверждение значения. **Это ограничение, а не недосмотр, и оно ждёт решения оркестратора** (расширение форм = ещё один version-only сдвиг пака). - **ru-target-долг не чинился** — по прямому указанию («задокументированное ограничение, НЕ чинить»): `isRuTarget` на колл-сайтах, кириллице-специфичный `TokenizeCyrillic`, захардкоженный множитель ×2 в `checkers.go`. Пак их не добавляет и не расширяет. --- ## 4. Инварианты «ровно так» — построчно | Инвариант промта | Статус | |---|---| | D2 не хард-гейт; неудача = прежнее поведение | ✅ ремонт входит только на `DispOK`, не меняет диспозицию ни в одну сторону; неудача = исходный текст. `TestRepairDeclineKeepsOriginal`, `TestRepairRejectsDeletingReply` | | Лестница эскалации не сломана | ✅ ни один предикат (`retryable`/`escalatable`/`disposition`) не тронут; ремонт — на состоянии, которого лестница не знает | | $0-резюм драфта НЕ сломан **repair-фолдом** | ✅ фолд только в волну финальной стадии (`TestRepairFoldsOnlyIntoFinalStageWaveSnapshot`). **НО** пар-данные (п. 3) двигают `LangpackVersion`, общий для ОБЕИХ волн → для zh-ru книги это both-wave `--resnapshot`. См. §9.2 — решение оркестратора | | repair = новый класс: чекпойнт/RequestHash/леджер/маркер | ✅ полная позиционная ось хеша через `runAttempt`; `role='repair'` = маркер стоимости без миграции; `RepairSpentUSD` | | no-repair путь байт-идентичен, golden без пере-капчера | ✅ sha golden не сдвинулся (команда в шапке) | | Общность §0: пара — данными | ✅ в Go только идентификаторы классов и движковый сентинел; новая пара = каталог промптов + её `dc-checkers.txt` | | Новые ключи конфига — строгий декод | ✅ `gates.repair` под уже строгим декодом pipeline; + строгость доведена до book/models/pair | | DeepSeek thinking никогда не off | ✅ синтетическая стадия ремонта не выставляет `reasoning` вовсе → провайдерский дефолт | | Книга и производные вне git | ✅ в репо только СИНТЕТИЧЕСКИЕ фикстуры; стенд-скан — разовым read-only инструментом, удалён из дерева | --- ## 5. Изменённые файлы ``` 22 файла изменено (+767 / −29), из них docs/… — отчёт дизайна (+65) Новых строк в новых файлах: 1834 (wc -l по: internal/checks/repair{,_test}.go · internal/pipeline/repair{,_integration_test}.go · prompts/zh-ru/repair/*.md) ``` Команда: `git diff --stat | tail -3` и `wc -l` по новым путям. ## 6. Дифф `^func Test` — ИСПОЛНЕНИЕМ, не по памяти ``` grep -rhoP "^func \KTest\w+" --include=*_test.go . | sort > tests_after.txt comm -23 tests_before.txt tests_after.txt # УДАЛЕНО comm -13 tests_before.txt tests_after.txt # ДОБАВЛЕНО ``` **было 399 · стало 433 · удалено 0 · добавлено 34.** Список — в §7. ## 7. Добавленные тесты (34) `checks`: DC1HoursWordBoundary · DC1FractionalUnit · DC1FractionalProbeInertWithoutData · RepairCandidateDC1Counted · RepairCandidateDC1Fractional · RepairCandidateAmbiguousAnchorYieldsNothing · RepairCandidateLatinAndBrokenWord · RepairCandidatesInertWithoutData · RepairCandidatesAgreeWithLints · ExpandToSentence · DisjointCandidatesDropsOverlap · RepairSpansAreSpliceSafe. `pipeline`: RepairResidualScanSurfacesInQualityReport · RepairResidualScanZeroWithoutPackData · RepairAppliesAndShipsRepairedBytes · RepairResumeIsFreeAndIdentical · RepairDeclineKeepsOriginal · RepairRejectsDeletingReply · RepairRejectsScriptLeakReply · RepairModelIsInReachableSet · RepairLoadFailsLoudOnUnknownClassAndMissingPrompt · RepairFoldsOnlyIntoFinalStageWaveSnapshot. `config`: UnknownKeyErrorNamesTheKey · StrictDecodeRejectsUnknownKeysInBookAndPair · RepairGateValidation. `membank`: ZeroValuePickedEntryAndBankDoNotPanic. Плюс два прямых теста гардов ответа: `RepairReplyGuards` (11 кейсов) · `RepairDigitGuardAllowsOneSubstitution`. ## 8. Самопроверка исполнением: одна дыра найдена МОИМ ЖЕ тестом и закрыта Пиша прямой тест гардов (а не полагаясь на интеграционные), поймал реальную дыру в собственном коде: цифровой гард проверял только ИСЧЕЗНОВЕНИЕ числа, поэтому ответ, **придумавший** новую цифру («…заплатил 500 монет **за 7 дней**»), проходил приёмку. Это ровно класс addition/фабрикации, который петля обязана снижать, а не создавать, и внизу его не ловит никто: reflow-гард числового дрейфа опт-ин и не включён НИ В ОДНОМ шиппинг-конфиге (`grep -rn "regression_guard" backend/configs/` → пусто), то есть член `NumberDrift` в «cheap-суита не выросла» структурно нулевой. Исправлено: `digitsPreserved` сверяет цифровые прогоны ПОЗИЦИОННО — их количество обязано совпадать, а значение может отличаться максимум в одной позиции (та самая подстановка, ради которой класс существует). Тест `TestRepairReplyGuards/invents_a_number` пинит это навсегда. --- ## 9. Адверсариальная панель по диффу: 18 находок, все выжили Прогнаны 4 атакующие линзы по РЕАЛИЗАЦИИ (байт-идентичность · корректность петли · деньги/резюм · общность/контракт); каждая находка BLOCKER/MAJOR отдана отдельному скептику с установкой «опровергни». Поднято 18, **опровергнуто 0** (часть понижена в severity). Это худший результат приёмки в паке — и он означает, что первая редакция §1 заявляла реализованными вещи, которых в коде не было. Ниже — что именно. ### 9.1. Исправлено в этой же сессии (9 находок) | # | Находка (severity после верификации) | Что было | Что стало | |---|---|---|---| | 1 | **BLOCKER: ре-гейт не имел НИ ОДНОГО позитивного условия** — все пять проверок отрицательные, то есть ратифицированная поправка §15.2(A) была заявлена, но не сделана. Воспроизведено: ответ «три часа» → «**пять** часов» (другое НЕВЕРНОЕ число) принимался и **отгружался** | детектор молчит при любом `ruNum != n` → ре-гейт зелёный | `classInvariantRestored` — позитивное утверждение НА КЛАСС через те же пар-данные: часы обязаны равняться 2N, дробная мера обязана исчезнуть И длительность остаться, латиница/битое слово — счётчик строго меньше. `TestRepairRejectsDifferentWrongValue`, `TestRepairFractionalMustStateADuration` | | 2 | **BLOCKER: анти-удаление 0.6× применялось к ПРЕДЛОЖЕНИЮ** — воспроизведено удаление придаточного (28% юнита), прошедшее приёмку и отгрузку | `rw < ow*0.6` | ратифицированное правило «токенов ≥ исходное − 1» (`repairMaxTokenLoss`) | | 3 | **BLOCKER: бюджет проверялся ДО чекпойнта** — при исчерпанном бюджете резюм терял уже ОПЛАЧЕННЫЙ ремонт и отгружал другие байты (ратифицировано в §D4: «бюджет обходится чекпойнтом») | ранний `return` до цикла | чекпойнт-проба на каждого кандидата, бюджет гейтит только СВЕЖИЙ вызов; `repairCallBudget` — одна точка истины хеша. `TestRepairPaidCallReplaysUnderExhaustedBudget` | | 4 | MAJOR: стоимость ремонта терялась при реплее чекпойнта (`runCost` шёл и в `cumCost`) → `chunk_status.cost_usd` занижался на резюме | одна цифра на оба счётчика | `CumUSD` (честный итог) и `CostUSD` (спенд прогона) — как у ретраев | | 5 | MAJOR: счётчики §15.2(B) **никем не читались** — `stageSeqResult.repair` был мёртвым полем, деливерабл D5 не выполнен | ничего | `Store.RepairStats` выводит calls/declined/applied из durable-артефактов, `QualityReport` + CLI их показывают. `TestRepairCountersDerivedAndSurviveResume`, `TestRepairDeclineIsCountedSeparately` | | 6 | MAJOR: `latin_residue` не гейтился по таргету — на латинском таргете КАЖДОЕ слово становилось кандидатом (скан выдавал бы бессмыслицу) | без гейта | тот же `isRuTarget` на обоих колл-сайтах, что у lint-близнеца | | 7 | MAJOR: оба класса dc1 были актуаторами по умолчанию вопреки §16.2 | в дефолт-наборе | дефолт = `latin_residue` + `broken_word`; dc1 — детекторы, актуаторы только по явному указанию оператора | | 8 | MAJOR: `ExpandToSentence` без абсолютного потолка → спан мог стать целым юнитом | без потолка | `repairMaxSpanBytes`; кандидат отбрасывается ДО оплаты | | 9 | MAJOR: спан `dc1_fractional` не выровнен (одно «полчаса» в юните может не соответствовать 半个时辰) | — | смягчено п. 7 (не актуатор по умолчанию); при явном включении ограничение остаётся и задокументировано | ### 9.2. НЕ исправлено — решение оркестратора **MAJOR: фикс пар-данных двигает снапшот ОБЕИХ волн.** Проверено мной исполнением: пак-версия `langpack-v2-f83acc67ea0f` (HEAD) → `langpack-v2-eaea82333353` (рабочее дерево); `LangpackVersion` лежит в ОБЩЕЙ части payload (`snapshot.go`), а `buildSnapshotID` — один на обе волны. Значит для любой zh-ru книги с langpack приземление пака = громкий `--resnapshot`, при котором **пере-оплачивается и драфт-волна**, а не только edit. Это шире, чем «version-only сдвиг пака», как я классифицировал в первой редакции. Практическая цена сегодня — вероятно ноль: rerun2 закрыт (D39.22), книг «в полёте» нет. Но инвариант «$0-резюм драфта» формально задет, поэтому решение ваше: - **(а) принять** both-wave re-pin как цену фикса (мой выбор по умолчанию: книг в полёте нет, а фикс — предусловие любого актуатора); - **(б) отложить** правку `dc-checkers.txt` до следующего санкционированного полного пере-прогона (тогда из пака уходит ратифицированный пункт 3); - **(в) построить** пак-суб-хеш, фолдящийся только в волну финальной стадии (чистое решение, но это НОВЫЙ механизм и расширение скоупа — без вашей санкции не делаю). ### 9.3. Что это говорит о процессе Все девять исправленных дефектов лежали в коде, который прошёл мои собственные тесты и мою собственную 4-линзовую самопроверку из §11 дизайна. Их поймала только панель, гонявшая КОД, а не читавшая его: шесть из девяти воспроизведены исполнением в песочнице (реальный пайплайн, мок-провайдер), а не выведены из чтения. Вывод для нормы приёмки: для пака, который МЕНЯЕТ отгружаемые байты, чтения диффа недостаточно — нужен прогон враждебных сценариев через настоящий драйвер. **Итоговое состояние после исправлений:** build/vet/`-race` — 12 пакетов `ok`; golden `f7641993e37f7ec619dfc5d2914a342d796a7cecd92080e33704f3974f0020f8` (не сдвинулся); тесты **399 → 432, удалено 0**; `gofmt` чист кроме пре-существующего `llm.go`. --- ## 10. МИНИ-ДЕЛЬТА ПРИЁМКИ (4 пункта оркестратора) ### (1) `hour_word_re` — левая граница + переезд в пар-данные ✅ **Дыра воспроизведена ДО правки** (пробой по реальному паку): `MentionsHourWord` возвращал `true` для «полчаса», «тотчас» и «сейчас» — то есть ответ, УДАЛИВШИЙ длительность и подставивший любой филлер на «-час», удовлетворял позитивное условие дробного класса, ради которого оно и существует. **Дом — пар-файл `configs/langpacks/zh-ru/dc-checkers.txt`, ключ `hour_word_re`. Аргумент:** пробу потребляет ТОЛЬКО позитивное условие дробного класса DC1, а тот не может сработать без пар-ключей `halfshichen_*`; его родня (`ru_hours_re`, `ru_hour`) уже живёт в пар-файле. Класть в `internal/lang/data/target-ru.txt` значило бы (а) разнести данные ОДНОГО класса по двум файлам и (б) заявить пробу target-general, хотя ни один target-general чекер её не читает. Новая пара, объявляющая дробный зонд, обязана объявить и свою пробу — иначе она молча унаследовала бы русскую орфографию. Обе границы явные (RE2 без lookaround, `\b` только ASCII). После правки: «час»/«шесть часов»/«два часа» → true; «полчаса»/«тотчас»/«сейчас»/без длительности → false. Тесты: `TestHourWordProbeBoundaries` (8 кейсов, включая названные оркестратором), `TestRepairFractionalRejectsHourFiller` (сквозной). **Плюс fail-loud, которого не просили, но без которого правка неполна:** пара, объявившая дробный зонд БЕЗ `hour_word_re`, не может утвердить инвариант — раньше это дало бы «платим и отвергаем» на каждом юните молча. Теперь класс не грузится, ошибка называет недостающий ключ (`TestRepairFractionalRequiresHourProbe`). **Классификация сдвига версии пака:** `langpack-v2-eaea82333353` → **`langpack-v2-a7d4be2c0465`** (команда: `lang.Load("configs/langpacks","zh","ru").Version()`). Класс тот же, что в §9.2 — **version-only по паку, но both-wave re-pin** для zh-ru книги. Дельта НЕ добавляет нового класса сдвига: она попадает внутрь того же единственного `--resnapshot`, который уже несёт основной фикс данных. Решение §9.2 остаётся за вами и покрывает оба изменения разом. ### (2) `brokenWordCandidates` + `unicode.Cyrillic` — задокументировано ✅ Выбрал документирование, а не data-drive, и вот почему это не отговорка: сканер кандидатов ОБЯЗАН токенизировать так же, как lint-близнец (`text.TokenizeCyrillic`), иначе ломается свойство вердикт-нейтральности, на котором стоит весь файл («кандидат только там, где сработал lint»). Data-drive ТОЛЬКО сканера сделал бы их разными. Долг репарируем лишь на общем токенизаторе — одной осознанной правкой target-сеама, а не побочным эффектом петли ремонта. Это записано в комментарии у самой функции, со ссылкой на близнеца и на D39.24 («ru-target-долг — задокументированное ограничение, НЕ чинить»). ### (3) D5 — доставлено частично, раскрываю ✅ + доделал дешёвую половину | Пункт D5 | Состояние | |---|---| | CostSource-маркер | ✅ `role='repair'` без миграции; `RepairSpentUSD` | | Счётчики починок в quality-report | ✅ calls / applied / declined / rejected, **выведены** из durable-артефактов | | Поля паспорта главы | ✅ **доделано в дельте**: `ChapterPassport.RepairApplied`, выведено из namespace `final_hash` (`TestRepairSurfacesInChapterPassport`) | | **Исходы ПО КЛАССАМ** (applied/declined/rejected на класс) | ❌ **НЕ доставлено — в очередь.** Причина механическая: класс нигде не записан durable. Чекпойнт ремонта несёт роль, но не класс; derived-id несёт namespace, но не класс. Вывести per-class исходы можно только записав класс — то есть колонкой ИЛИ расширением формата derived-id/finish_reason. И то и другое — решение о СХЕМЕ/ФОРМАТЕ, а не мелкая доводка, поэтому не делаю молча. Кандидатов по классам (`repair_candidates_by_class`) это НЕ касается — они есть. | ### (4) Гигиена ✅ - Секция в `docs/PROGRESS.md` добавлена. - Стейл-цифры первой редакции приведены к фактическим: ценз тестов **427 → 433** (после дельты добавились тесты границы часа, филлера, load-guard и паспорта), дифф **+675/−19 → +767/−29**, объём новых файлов **1416 → 1834**. - «§16.2 ратифицирована» → «одобрена приёмкой оркестратора» (в трёх местах); ратификацию оформляете вы. ### Честная поправка о процессе внутри самой дельты При правке теста фолда я заменил текст «от функции до конца файла» и тем самым снёс **7 тестов**, дописанных ранее (гарды ответа, цифровой гард, позитивный ре-гейт, дробная длительность, реплей под исчерпанным бюджетом, счётчики, декларация деклайна). Поймал это ценз-диффом `^func Test` СРАЗУ (это ровно то, ради чего ценз и введён нормой), все семь восстановлены, финальный дифф против baseline: **удалено 0**. Отмечаю, потому что это третий случай в проекте, когда тесты пропадают при механической правке файла — и первый, когда механизм поймал это до отчёта, а не после.