textmachine/docs/archive/reports/PACK13_QA_REPORT_2026-07-24.md

22 KiB
Raw Permalink Blame History

Отчёт бэкенд-сессии: ПАК-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:228 readOptional + :244 parseHeading; пары без файла — инертны и байт-стабильны (не ре-биллятся), с файлом — байты фолдятся в pack.Version() (громкий --resnapshot), битый — fail-loud.
  • Детект/стрип/рендер (алгоритм в движке, читает данные): internal/pipeline/chunker.go:129 stripHeading, :173 matchHeaderLine, :160 applyHeading, Chunk.Heading. Чанкер детектит первый заголовок, СТРИПАЕТ маркер из ch.Text (модель не рендерит заголовок), субтитр сохраняется как тело, «Глава N» рендерится детерминированно.
  • Прокидка: runner.go headingRule(); 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:187 lintPercentScale — 成-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:219 lintLatinResidue — цельное латинское слово в ru-выводе (again). Флаг all-lowercase латинского токена ≥3 (утечка прозы — строчная; любая заглавная = бренд/имя/акроним iPhone/Google/Suzuki/II → пропуск), без цифр (id/хеш пропускается), не римская цифра, не в allowlist. Ужесточён по адверсариал-ревью: первая версия (any-case) ложно срабатывала на брендах/URL — теперь строчная-только (цена: заглавная утечка / URL-хост не ловятся — принято для тихого observability-сигнала).
  • checkers_zh_ru.go:279 lintBrokenWord — ТОЛЬКО общее правило: слово на «-йть» (невозможное окончание в русском). Без книго-списков, без словаря. Ловит войть. лять/Вперди/глава клан — не детектируются без морфологии (честно, см. §директива).
  • Существующий санитайзер-класс битых слов (невалидные знаковые биграммы, гомоглифы) ортогонален, работает.

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: editor glm-5deepseek-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_rootheadingRule=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-заметки (НЕ дефекты рантайма, НЕ книго-специфика — язык-общее):
    1. Парсер CJK-цифр (chunker.go isHeadingNumeral/parseSectionNumeral) захардкожен в движке, не в langpack. Принято: это ОБЩИЙ китайско-языковой алгоритм (не пер-книга, не пер-пара — цифры универсальны), пер-парные данные (marker/units/template) в langpack. Как майнер-алгоритмы. Residual: значения цифр в langpack — чище на будущее (как отмечено в шапке checkers_zh_ru.go про code-const паки).
    2. matchHeaderLine требует unit-руну после числа (CJK-конвенция 第N章). Пара с заголовком «маркер+число без юнита» (en «Chapter N») потребует расширения правила/матчера. Документированное ограничение текущего CJK-скоупа.
    3. Чекеры gated по isRuTarget (target=ru), не по source=zh — они КОРРЕКТНО общие (source-контент-gated / target-общие): ja→ru дефект 千万 тоже поймается. Поведение верное; только шапка-комментарий файла слегка over-claim «zh-specific» (существующий код, не мой; моё честно помечено «general»).

Приёмка

  • 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-гард, сид-правки.