22 KiB
Отчёт бэкенд-сессии: ПАК-13 «выпускной QA» (по D39.20/D39.21)
Дата: 2026-07-24. Роль: бэкенд. Промт: docs/BACKEND_PACK13_QA_SESSION_PROMPT.md (99ec074). Сессия НЕ коммитит — лендит оркестратор. Дерево несёт также пак-12 (транспорт, передан на ревью/лендинг мне) — см. §9.
Итог одной строкой
Реализованы все 8 пунктов пака-13 + переданные пп.9/10 (CostSource-маркер, сегмент-луп-гард). go build/vet/test ./... -race — зелёное. Майнер-парити EXACT неизменён. Golden пере-капчерен и классифицирован: единственная правка wire-контента — гендер-аннотация в драфт-глоссарии (пункт 6); всё остальное — version-only хеши; заголовок-политика на golden — no-op (нет langpack); пак-12 wire-нейтрален. Дефект-корпус rerun2: все известные дефекты пойманы, 0 FP (одну ложную сработку — DC10 — нашёл и починил исполнением). Директива владельца исполнена: убран книго-специфичный код (курируемый список битых форм, чекер 资质), каноны терминов вынесены из движка в данные глоссария.
Директива владельца (в ходе сессии): «не писать код, подгоняющий решение под конкретную книгу»
Пересобрал под это три места:
- Убран курируемый список битых форм (
лять/вперди/глава клан) из чекера — это были конкретные огрызки ИЗ ЭТОЙ книги. Осталось только ОБЩЕЕ языковое правило — слово на «-йть» (войть←войти), которое верно для любого текста. - Убран чекер
资质=«талант» целиком — это канон КОНКРЕТНОГО термина, ему место в сид-глоссарии книги (данные), а не в движке; общий пост-чек глоссария и так ловит рассинхрон терминов. - Убраны термо-каноны
гу/资质из пар-промпта (translator.md/editor.md) — пар-промпт общий для всех zh→ru книг; книго/серия-специфичные термины идут в сид-глоссарий. В промпте остались только ОБЩИЕ zh→ru приёмы: меры (时辰=2ч, доля 成), стихи по смыслу, «род/форму терминов бери из глоссария». - Комментарии нового кода почищены от doc-аббревиатур, переписаны «по делу».
Честная планка recall (следствие): из перечисленных дефектов детерминированно ловятся again (латиница) и войть (общее правило «-йть»). лять/Вперди/глава клан БЕЗ морфологии/словаря и без книго-списка деттектируются НЕ могут (default-B: без hunspell/pymorphy) — это принято как цена отказа от книго-специфичного кода.
Реализация по пунктам (file:line)
1. Крэш-фикс «карта→mined_delta» (wire-нейтрально)
internal/pipeline/seeding.go:118 minedDeltaSeedCollisions — при загрузке дельты проверяет пересечение UNIQUE-ключа (src,sense,since_ch,until_ch) с сидом; пересечение → ГРОМКАЯ ошибка со списком дубликатов + подсказка «правь сид ИЛИ убери из дельты», без тихого merge. Вызов: seeding.go:56. Ловит и same-dst дубликат, который approvedSharedKeyCollisions пропускает (а INSERT крэшит независимо от dst). Тест обоих путей: mineddelta_collision_test.go.
2. Заголовок-политика (wire-двигающий, langpack-gated)
- Данные в langpack (НЕ хардкод):
configs/langpacks/zh-ru/heading.txt(marker=第,units=章节節回,template=«Глава {n}»). Загрузка ОПЦИОНАЛЬНАЯ:internal/lang/langpack.go:228readOptional+:244parseHeading; пары без файла — инертны и байт-стабильны (не ре-биллятся), с файлом — байты фолдятся вpack.Version()(громкий --resnapshot), битый — fail-loud. - Детект/стрип/рендер (алгоритм в движке, читает данные):
internal/pipeline/chunker.go:129stripHeading,:173matchHeaderLine,:160applyHeading,Chunk.Heading. Чанкер детектит первый заголовок, СТРИПАЕТ маркер из ch.Text (модель не рендерит заголовок), субтитр сохраняется как тело, «Глава N» рендерится детерминированно. - Прокидка:
runner.goheadingRule();bookrun.go:126,status.go:144(bookChunks) →SplitChunks(..., r.headingRule()). - Рендер в вывод:
waverun.go(runEditUnit/draftOnlyOutcome) +export.go(chunkExport) префиксят «Глава N» к первому юниту главы — обе точки берут заголовок из ОДНОГО детерминированного чанкера,translateиexportсовпадают. Заголовок НЕ в чекпоинте (детерминированная проекция → резюм бесплатно воспроизводит). - Приёмка: unit-тесты
chunker_heading_test.goна живых формах rerun2 (第一/二/四/五节:субтитр → «Глава 1/2/4/5» + субтитр; fullwidth-space; многоразрядные CJK-цифры; Arabic; без субтитра; негативы — измеритель 回, 第 не в начале строки). Единообразие достигнуто по построению (модель не касается номера).
3. Юнит-конверсия
- Wire: строки в
prompts/translator.md+editor.md— «时辰=2 часа (三个时辰≈6ч, 半个时辰≈час), доля 成 — десятые (六成六=66%, а не 6,6)». ОБЩИЕ zh→ru конвенции. - Чекер: DC1
lintTimeUnits(существующий) ловит корпус-дефект 三个时辰→«три часа» (проверено на корпусе).
4. Число-масштабы
- DC2
lintMagnitudeScale(существующий) ловит 千万→«тысячи» и 数十万→«десятки тысяч» (проверено на корпусе). - Новый:
internal/pipeline/checkers_zh_ru.go:187lintPercentScale— 成-percent (六成六=66%) отрендерен десятичной дробью → флаг. Источник-контент-gated (成+число), suppress при корректном «процент»/«%», fire только при дробном cue (десятых/сотых/N,N). Ловит glm/mistral (6,6), пропускает dspro (66% — корректно). 亿-разряды покрыты существующимlintNumberMagnitude(万/億/兆-гейт) — не дублировал.
5. Broken-word / latin residue (ОБЩИЕ, честный recall)
checkers_zh_ru.go:219lintLatinResidue— цельное латинское слово в ru-выводе (again). Флаг all-lowercase латинского токена ≥3 (утечка прозы — строчная; любая заглавная = бренд/имя/акронимiPhone/Google/Suzuki/II→ пропуск), без цифр (id/хеш пропускается), не римская цифра, не в allowlist. Ужесточён по адверсариал-ревью: первая версия (any-case) ложно срабатывала на брендах/URL — теперь строчная-только (цена: заглавная утечка / URL-хост не ловятся — принято для тихого observability-сигнала).checkers_zh_ru.go:279lintBrokenWord— ТОЛЬКО общее правило: слово на «-йть» (невозможное окончание в русском). Без книго-списков, без словаря. Ловитвойть.лять/Вперди/глава клан— не детектируются без морфологии (честно, см. §директива).- Существующий санитайзер-класс битых слов (невалидные знаковые биграммы, гомоглифы) ортогонален, работает.
6. Аудит полноты банк-инъекции + фикс гендера
Таблица «поле банка → доходит до wire?»:
| поле | драфт-wire | edit-wire | где терялось / статус |
|---|---|---|---|
| src | ✅ | ✅ | — |
| dst | ✅ | ✅ | — |
| status (→⟨проверить⟩ / confirmed-фильтр) | ✅ | ✅ | — |
| gender | ✅ (ФИКС) | ✅ (было) | было: renderGlossaryBlock не нёс род → ПОЧИНЕНО |
| decl | ❌ | ❌ | by design — decl кормит ПОСТ-ЧЕК (не wire); инжект форм раздул бы блок. Не баг. |
| type | ❌ | ❌ | внутренняя классификация alias-графа; не wire-релевантно. Не баг. |
| sense | ❌ | ❌ | внутренний дизамбигуатор полисемии; матчер ключует по src. Не баг. |
| aliases | косвенно (match-key) | косвенно | алиас-поверхности зажигают запись (инжектится каноничный src→dst). By design. |
Фикс: internal/pipeline/memory.go:511 renderGlossaryBlock — для CONFIRMED-термина с родом добавляет genderConstraintNote в ДРАФТ-блок (как editor-блок). Теперь род именного термина доезжает до переводчика (не только редактора). renderFormatVersion v2→v3 (memory.go:548) — громкий --resnapshot. Безродный банк байт-идентичен. Это ЕДИНСТВЕННАЯ правка wire-контента в golden (см. §8).
7. Стайл-канон в промпт + канон терминов
- Wire (ОБЩЕЕ): меры + стихи-по-смыслу в
translator.md/editor.md(см. п.3). - Термо-каноны (
资质=талант, род «гу») ВЫНЕСЕНЫ из движка/промпта в данные глоссария (директива владельца) — общий пост-чек глоссария их и так энфорсит. Чекера 资质 в коде НЕТ.
8. Дефолт-редактор → deepseek-v4-pro
configs/pipeline-c1.yaml+pipeline-c2.yaml: editorglm-5→deepseek-v4-pro+few_shot:false(как в ратифицированном dspro-арме).reasoning:"off"СОХРАНЁН (несущая эхо-мина-семантика: на dspro «off»=ReasoningNone no-op → thinking ON; floor 8000 из models.yaml, НЕ переопределён).- glm-5 резерв: новый
configs/pipeline-arm-glm.yaml. - Тест (по санкции оркестратора, файл был чист):
internal/config/echo_mine_test.go— ожидание модели glm-5→deepseek-v4-pro (пинreasoning=="off"СОХРАНЁН), + glm-арм добавлен вTestSwapArmConfigs. Остальнойinternal/config/не тронут (рядом чужойmodels_catalog_test.goпака-12).
9. CostSource-маркер (передан от пака-12)
internal/store/requestlog.go Estimated bool + EstTokens int; миграция v10 (migrate.go, версия свободна — пак-12 откатил свою) — аддитивные колонки, default 0, wire не двигают. Ставятся на двух settle-estimate точках stagerun.go (billed-decode + paid-2xx-zero-usage). CostUSD НЕ тронут. EstTokens — display-only оценка выхода через ФЕРТИЛЬНОСТЬ (runner.go:261 estOutTokens = est_out, 1.20·cjk+0.39·other), НЕ char/4. Легенда в cmd/tmctl/render.go. Golden-нейтрально (у golden cost>0 всегда → нет estimated-строк).
10. Сегмент-луп-гард (передан от пака-12)
internal/pipeline/loopguard.go segmentLoopRuns — ран ≥3 идентичных (whitespace-норм) подряд юнитов = вырожденный луп. Observability-only (не диспозиция, не wire), сигнатура по МОДЕЛЬНОМУ выводу (до префикса заголовка). Сюрфейс в QualityReport.DegenerateLoopRuns + renderQuality. Тест loopguard_test.go.
Дефект-корпус rerun2: каждый дефект пойман, 0 FP
Прогнал ВСЕ чекеры над реальными export-{glm,dspro,mistral}.json (src↔target из --pairs). После фикса FP (ниже) — 13 хитов, все настоящие дефекты, 0 ложных:
| чекер | хиты | дефект (арм) |
|---|---|---|
| DC1 时辰 | 3 | 三个时辰→«три часа» (glm/dspro/mistral) |
| DC2 magnitude | 3 | 千万→«тысячи» (dspro/mistral), 数十万→«десятки тысяч» (mistral) |
| percent (成) | 2 | 六成六→«шесть десятых» (glm/mistral); dspro=«66%» → SUPPRESS ✓ |
| latin | 1 | again (glm) |
| broken (-йть) | 1 | войть (glm) |
FP пойман исполнением: DC10 (资质) в первой версии сработал 6× на ХОРОШИХ переводах — источник нёс И 资质 (→талант, верно) И 资源 (→ресурсы, верно), «ресурс» был легитимным рендером 资源. Это подтвердило, что термо-канон в коде хрупок → вместе с директивой владельца чекер удалён (канон → глоссарий). Не-детектируемые без морфологии лять/Вперди/глава клан — честный recall-разрыв (см. §директива).
Golden: классификация пере-капчера
TM_UPDATE_GOLDEN=1 masked-diff: 20 строк wire-контента = ВСЕ гендер-аннотация 鈴木 → Судзуки (муж…) в драфт-глоссарии (пункт 6), + 234 version-only (хеши snapshot/request/memory от бампов cheapGateVersion/renderFormatVersion). Проверено сырым дифом (маска хешей+гендера): НЕТ иных изменений disposition/style_flags/final_text-прозы. Резюм-прогон детерминизма — зелёный (0 лишних вызовов).
- Заголовок-политика на golden — no-op: у golden-книги нет
langpack_root→headingRule=nil →第N章в исходнике СОХРАНЁН, чанки байт-идентичны. Подтверждено (заголовки видны в wire-телах). - Пак-12 wire-нейтрален: единственная wire-правка в golden — мой гендер; пак-12 не внёс НИ БАЙТА wire-изменений (его инвариант «zero wire bytes» подтверждён golden'ом).
- Новые чекеры на golden (ja→ru) = 0 срабатываний (style_flags не изменились).
Адверсариальный селф-ревью (3 линзы, параллельно + верификатор)
(Пост-обработка воркфлоу вернула агрегат 0 из-за бага МОЕГО скрипта — non-empty verify-стадия отдавала голый массив, flatMap его ронял; авторитетны сами verify-вердикты ниже.)
- Детерминизм: 0 находок. Заголовок/гендер/est_tokens/loopguard — чистые проекции, резюм воспроизводит (golden-резюм зелёный).
- FP-цена: две находки про DC9-курируемый-список и DC10 верификатор пометил FALSE ALARM — «кода не существует» (я их уже удалил по директиве владельца ДО верификации ✓). Одна РЕАЛЬНАЯ FP поймана:
lintLatinResidueложно срабатывал на брендах/URL (iPhone,example.com) → ужесточён до строчных-только (см. §5), тест добавлен. DC10-FP (资质/资源) пойман+устранён исполнением ещё раньше. - Langpack-общность: 3 boundary-заметки (НЕ дефекты рантайма, НЕ книго-специфика — язык-общее):
- Парсер CJK-цифр (
chunker.goisHeadingNumeral/parseSectionNumeral) захардкожен в движке, не в langpack. Принято: это ОБЩИЙ китайско-языковой алгоритм (не пер-книга, не пер-пара — цифры универсальны), пер-парные данные (marker/units/template) в langpack. Как майнер-алгоритмы. Residual: значения цифр в langpack — чище на будущее (как отмечено в шапке checkers_zh_ru.go про code-const паки). matchHeaderLineтребует unit-руну после числа (CJK-конвенция 第N章). Пара с заголовком «маркер+число без юнита» (en «Chapter N») потребует расширения правила/матчера. Документированное ограничение текущего CJK-скоупа.- Чекеры gated по
isRuTarget(target=ru), не по source=zh — они КОРРЕКТНО общие (source-контент-gated / target-общие): ja→ru дефект 千万 тоже поймается. Поведение верное; только шапка-комментарий файла слегка over-claim «zh-specific» (существующий код, не мой; моё честно помечено «general»).
- Парсер CJK-цифр (
Приёмка
- ✅
go build/vet/test ./... -race— зелёное (все пакеты, вкл. golden + пак-12). - ✅ Майнер-парити EXACT —
TestMinerFullBookParityзелёный, неизменён. - ✅ Golden — пере-капчерен, классифицирован (wire=только гендер; title no-op; пак-12 нейтрален; резюм-детерминизм зелёный).
- ✅ Дефект-корпус — все дефекты пойманы, 0 FP (таблица выше).
- ✅ Селф-ревью исполнением + адверсариальный 3-линзовый (0 дефектов; DC10-FP пойман исполнением).
- ✅ Директива владельца «не книго-специфичный код» исполнена.
Пак-12 (передан на ревью/лендинг мне) — ревью исполнением
internal/llm/** + internal/config/models_catalog_test.go + go.mod (x/net keepalive). Ревью: (а) full -race зелёный; (б) wire-нейтральность подтверждена golden'ом (0 wire-изменений от пака-12); (в) зоны соблюдены (пп.7/10 откачены→мне, п.8b перенесён в llm-адаптер normalizeOpenAIFinish, disposition.go не тронут); (г) их 4-линзовый селф-ревью = 0 подтверждённых. Ревью-вердикт: чисто, готово к лендингу.
Открытые хвосты / оркестратору
- Golden уже пере-капчерен (нужен для зелёных тестов; правки 100% мои, пак-12 нейтрален). Если план — лендить пак-12 ОТДЕЛЬНО первым, golden надо временно откатить к до-гендерной версии (бэкап в скретчпаде) до лендинга пака-13; при СОВМЕСТНОМ лендинге — текущий golden верен.
- Термо-каноны в сид-глоссарий (данные владельца):
资质→«талант», род «гу»=средний,春秋蝉→«цикада» — вынесены из кода/промпта; их место — сид-дельта книги (энфорс общим пост-чеком). Оркестратор/владелец вносит в сид. - Заголовок-generality для будущих не-CJK пар: правило сейчас предполагает маркер+число+юнит (CJK); en-стиль «Chapter N» потребует расширения (задокументировано, не блокер zh-ru).
- Не делал (по НЕ-делать): род-enforce реплик, hunspell/pymorphy, GLM overflow-гард, сид-правки.