textmachine/docs/archive/reports/PACK19_BUILD_2026-07-30.md

28 KiB
Raw Blame History

ПАК-19 «голос и состояние» — ФАЗА 2 (стройка). Отчёт бэкенд-сессии, 2026-07-30

Аддендумы получены релеем: директива фазы 2 (оркестратор №9, 30.07) + D39.55 + ревью-шапка PACK19_DESIGN_2026-07-30.md. Сессия НЕ коммитит; git mv не было.

Ревью-шапка (оркестратор №9, 31.07): ПРИНЯТО И ЗАЛЕНДЕНО, D39.56. Короткая приёмка по норме 30.07: полный сьют -race -count=1 перегнан оркестратором — зелёный, vet чист; repin.go/snapshot.go/testdata не тронуты ни строкой (диффы пусты — запреты (д)/(и) и «golden байт-идентичен» подтверждены исполнением); записка D21.10 в migrate.go сверена — фиксирует стабильный ключ (src, sense) и неснятую заморозку. Диспозиции пингов §7: (1) ЦЕЛЕВАЯ атрибуция принята — выравнивания src↔tgt в движке нет, скоуп не менялся; (2) speech-cue.txt zh НЕ шипить до решения владельца — бэклог 13б; (3) golden не расширять — верно по директиве, счётчики голоса = кандидат следующего ратифицированного пере-капчера; (4) флаггер утечки вне гейта — принят с записью (стоячий механизм C1, retrieval_state вне хешей); (5) записка сверена. Находка критика полноты §6.9 (ALTER-миграции не идемпотентны вопреки шапке) — бэклог 49а. Первое измерение осей — включение gates.voice на холодном мини-прогоне (бэклог 24).

Гейты: go build · go vet ./... · gofmt -l чист по всем моим файлам (флагнут только internal/llm/llm.go — предсуществующий нит бэклога-53, мной не тронут) · полный сьют -race -count=1 зелёный · golden: capture байт-идентичен (git diff --stat testdata/ пуст) · мутации 23/23 красные · диффстат +672/71, 16 файлов + 7 новых.


1. Эхо-блок

  1. Шесть пунктов директивы построены; инъекции нет — кода за §3.V3 не написано.
  2. Три сущности = отдельные таблицы, фолд — внутрь memory_version (запрет (д) соблюдён).
  3. Фолд условный и сегодня НИКОГДА не пишется ⇒ хеш каждой книги байт-идентичен, golden пуст.
  4. (в) закрыт компиляцией, а не фильтром: voice/address — отдельные Go-типы.
  5. Утечка считается только по несработавшему-по-окну при СРАБОТАВШЕМ ключе; sticky-реджекты (ж) в счёт не входят.
  6. Регистр — абстрактный (informal/formal), поверхности — целевые данные.
  7. gates.voice не фолдится; VoiceCheckVersion едет с прогоном и в отчёт.
  8. speech-cue.txt для zh НЕ шипнут — его байты двигают LangpackVersion. Решение владельцу (§7).
  9. Одно отклонение от буквы дизайна (механизм атрибуции — целевой, не исходный) — не молчу, §7 пинг 1.
  10. Не делаю: инъекцию · Annotator · слой-2 · ru-target долг · платные прогоны · коммиты.

2. Что построено (по пунктам директивы)

# Пункт Как сделано Улика
1 Схема трёх типов + сид + одна транзакция + условный фолд миграция v13 (voice_profiles, address_pairs), v14 (4 колонки retrieval_state); ReplaceBank заменяет ВСЕ три типа в одной tx; BankInput/MaterializeBank/ComputeVersionScopedIn store/migrate.go v13/v14 · store/glossary.go:ReplaceBank · membank/memory.go:ComputeVersionScopedIn
1а speech → deprecated, first_person → поле профиля колонка помечена в схеме; self_ref живёт в voice_profiles migrate.go:184 · store/voice.go
2 Флаггер спойлер-утечки Bank.SpoilerLeaks(rejected, output) поверх Selection.Rejected + dstFormPresent; НОЛЬ схемных изменений membank/mempostcheck.go
3 Ты/вы-флаггер internal/checks/voice.go, оси AC в Total(), ось D — отдельное поле; свой гейт gates.voice, версия не фолдится checks/voice.go · config/pipeline.go:VoiceGate
4 Данные V5 5 новых целевых категорий в target-ru.txt; опциональный ИСХОДНЫЙ speech-cue.txt (формат+загрузчик+тест) lang/data/target-ru.txt · lang/langpack.go:parseSpeechCue
5 Инъекция не проводится BankInput.InjectVoice не имеет конфиг-ключа и всегда false pipeline/seeding.go (комментарий у bankIn)
6 Фикс-строка D39.54 комментарий поля SHA256 → «canonical (comment-stripped)» pipeline/render.go

Что НЕ построено сознательно: колонка reveal_ch и фаза на алиасах (D39.55 сократил скоуп; записка D21.10 в migrate.go переписана и объясняет, почему две строки с непересекающимися окнами уже выражают pre/post).


3. Девять «РОВНО ТАК» — исполнение

Требование Исполнено Улика / мутация
(а) бюджет инъекции в потолке/reserve §5 отдельным абзацем
(б) критик полноты §6 отдельной секцией
(в) Materialize не пускает голос в AC/инъекцию сильнее фильтра — компиляцией: store.VoiceProfile/AddressPair не присваиваемы в []store.GlossaryEntry, матчер видит только BankInput.Rows M23 (искусственно вливаю профили в rows) → красный TestVoiceProfileNeverReachesTheMatcherOrTheInjection
(г) порядок «исключение скоупом ДО подсчёта условия» фолд собирает inScope в строчном цикле, затем voice-цикл фильтрует ПЕРЕД hasVoice = true M3 (переставить) → красный TestVoiceFoldRespectsTheMinedScopeBeforeCountingIt
(д) свой ключ снапшота запрещён ни одного нового ключа payload; repin.go не тронут ни строкой git diff по repin.go пуст; golden payload не двигается
(е) address_pair по стабильному ключу колонки speaker_src/speaker_sense/addressee_src/addressee_sense; glossary.id нигде не участвует store/voice.go, migrate.go v13
(ж) sticky-carry, отклонённый окном → Rejected одна строка в Select M4 → красный TestStickyCarryBlockedByWindowIsRecordedAsRejected
(з) включение инъекции = полный --resnapshot InjectVoice входит в memory_version ⇒ флип двигает ОБА wave-снапшота; bank-only на флипе не обещаю M1/M2 → красные TestVoiceRowsDoNotMoveTheBankVersionWhileUninjected / TestInjectedVoiceRowsMoveTheBankVersion
(и) v1 переиспользует GlossaryTokenBudget отдельного voice-бюджета не заведено; repin.go:186 не тронут
опц. линт «gap в окнах одного src» VoiceWindowGaps, WARN при сиде, никогда не фатал TestUnknownVoiceCharactersAndWindowGaps

Уточнение по (ж), которое важнее самой правки. Ветка недостижима через прод-драйвер: precomputeSticky сбрасывает окно на каждой границе главы (wave.go:82-85), поэтому carry всегда приходит в ТУ ЖЕ главу, где сработал, и spoilerBlocked(e, chapter) даёт тот же ответ. То есть это починка на границе ЭКСПОРТИРОВАННОГО API, а не смена поведения — и именно поэтому golden остался байт-идентичным. Тест бьёт по Select напрямую.


4. Найдено и починено ИСПОЛНЕНИЕМ (то, чего дизайн не видел)

  1. Отсчёт исходных реплик двоил. “来吧。”方源说道。他走了。“不。” давал 2 атрибутированные реплики на одном кие: окно «до открывающей кавычки» дотягивалось до кия ПРЕДЫДУЩЕЙ реплики. Исправлено потреблением: кий атрибутирует не более одной реплики (countSourceReplies, consumed). Мутация M13 возвращает дефект → красный.
  2. Две мои же мутации выжили в первом кругеобе оказались реальными дырами тестов, не формальностью:
    • M12 (снять предусловие «на сцене ровно двое»): мой «третий персонаж» стоял В АТРИБУЦИИ, поэтому кейс на самом деле проверял неоднозначную атрибуцию, а не эвристику адресата. Перенёс третье имя в НАРРАТИВ + добавил утверждение Attributed == 1, чтобы кейс не мог снова уехать не на то правило.
    • M14 (убрать инертный путь без T/V-данных): мой «инертный» кейс не имел и маркеров реплик, так что T/V-гард был замаскирован гардом сегментации. Добавлен кейс «цель РАЗМЕЧАЕТ диалог, но T/V-различения не имеет» (en-подобная цель) — он изолирует именно T/V-гард. Обе дыры закрыты, второй круг: 23/23 красные.
  3. Тест апгрейда схемы был бы вакуумным без премис-проверки: добавил два SELECT, которые ОБЯЗАНЫ упасть на старой БД до Open — иначе тест «апгрейда» ничего не доказывает.

5. Бюджет инъекции и денежный потолок — закрытие REFUTED-1 (ровно так (а))

Сегодня вопрос закрыт ПО ПОСТРОЕНИЮ, и это надо сказать прямо, а не выдать за проектную заслугу: инъекции нет, InjectVoice не имеет конфиг-ключа, ни один байт голоса не попадает в msgs, поэтому промпт-оценка не меняется, EstimateTokens (render.go:349) считает те же строки, ledger.EstimateUSD получает тот же promptEst, и Store.Reserve (stagerun.go:450-452) резервирует ту же сумму. Потолок не ослеплён, потому что резервировать нечего.

Что механизм обязан будет сделать, когда инъекция поедет (записываю, чтобы полигон и будущая сессия не искали это заново):

  • Инъекция — ОТДЕЛЬНОЕ system-сообщение (render.go:245), и promptEst считается по СОБРАННЫМ msgs, а не по шаблону. Значит voice-блок попадёт в оценку автоматически, БЕЗ правки резервации — это свойство того, что оценка живёт после сборки сообщений, а не до неё. Проверяемое утверждение, не обещание: если добавить блок и не тронуть stagerun.go, резерв вырастет.
  • Ослепить потолок может ровно один сценарий: блок, добавленный в msgs ПОСЛЕ вычисления promptEst. В текущем коде такого пути нет (сборка → хеш → оценка → резерв, один порядок в stagerun.go), и сохранить этот порядок — обязательное «ровно так» пака инъекции.
  • Порядок величины (пере-мер §V6 дизайна по действующему прайсу): драфт-слот ≈ $0.042/книга-500 при 300 ток./чанк и $0.14/M; редакторский ≈ $0.065 при $0.435/M. Резерв на вызов растёт на ≤$0.0002 — на три порядка ниже флора min_max_tokens, то есть в потолок не упирается.
  • gates.terminology.budget_usd-подобный отдельный потолок голосу НЕ нужен: инъекция не является отдельным КЛАССОМ ВЫЗОВОВ (у неё нет своих чекпойнтов), она удорожает существующие вызовы, а их уже накрывает книжный потолок.

6. Критик полноты — закрытие REFUTED-2 (ровно так (б))

Спрашиваю себя: что в этом паке НЕ покрыто, НЕ измерено или измерено слабее, чем звучит.

  1. Оси B и D держатся на данных, которых на стенде нет. Профилей и реестра ни у одной стендовой книги нет, gates.voice выключен, speech-cue.txt не шипнут. Значит на реальном тексте флаггер сегодня даёт ровно ноль срабатываний — и это НЕ «чисто», а «не измерено». Первое настоящее измерение — холодный мини-прогон (бэклог-24 уже держит строку «срабатывания голос-флаггера»); до него точность осей не известна.
  2. Ось A может ловить цитату. Реплика, внутри которой персонаж ЦИТИРУЕТ чужие слова с другим регистром, засчитается противоречием. Вето по множественности этого не снимает. Класс не закрыт и не обойдён — назван.
  3. Ось B узка по построению. Она срабатывает только когда в реплике есть ОБЩЕЕ местоимение и нет заявленного самоназвания. Реплика, где служанка не называет себя вовсе, — молчание, а не сигнал. Это осознанный precision-over-recall, но «доля сохранённых маркеров» из research/15 этим НЕ меряется.
  4. Атрибуция ловит только ту форму имени, что есть в decl. Мой же тест это и показал: без «Фан Юаню» неоднозначная атрибуция читается как однозначная. Значит качество осей B/C/D — функция ПОЛНОТЫ склонений в сиде, а не только кода. Для полигона это прямое следствие: неполный decl тихо снижает Attributed, а не флаги.
  5. Сегментация реплик — построчная. Реплика, разорванная переносом строки внутри одного хода, разъедется на две; реплика без маркера (косвенная речь) не видна вовсе.
  6. SourceReplies меряет исходник, но сопоставить с целевым Replies нельзя — выравнивания нет. Это два независимых счётчика; читать разницу как «потеряли реплики» без ручной сверки нельзя.
  7. Схема есть, потребителя-инъекции нет. register, lexicon_markers, exemplars, brightness, address_default, form, closeness хранятся, валидируются и (при включённой инъекции) хешируются — но сегодня их не читает НИКТО. Это ратифицированное состояние (D21 п.2), но честное имя ему — «зарезервировано», а не «работает».
  8. Не измерено: холодный старт голоса. Как поведёт себя флаггер на книге, где банк пуст, а профили есть, — не проверено ни фикстурой, ни прогоном.
  9. Пре-существующее, не моё, но рядом: ALTER-шаги миграций (v8v14) не идемпотентны, вопреки шапке migrate.go:9-11, которая обещает сходимость полу-применённой БД. Счётчик версий это скрывает. Мой апгрейд-тест показывает, что путь v12→v14 работает; про полу-применённый шаг не утверждаю ничего.

7. Пинги оркестратору

  1. Отклонение от буквы дизайна, названное вслух: атрибуция ЦЕЛЕВАЯ, а не исходная. Фаза 1 набросала исходный пре-гейт «имя + 说/道 у кавычки». При стройке выяснилось то, что на бумаге не видно: кий называет говорящего в ИСХОДНИКЕ, а проверять надо реплику в ЦЕЛИ, и выравнивания src↔tgt в движке нет — перенести спикера через границу нечем. Целевой стороне выравнивание не нужно: хвост атрибуции русской тире-строки уже называет спикера словами, которые банк умеет матчить (speech_verb + decl-формы). Поэтому оси AD работают на целевой стороне, а исходный speech-cue.txt построен и используется ТОЛЬКО как знаменатель SourceReplies. Скоуп не расширен и не сужен; изменился механизм внутри пункта.
  2. Решение владельца: шипить ли configs/langpacks/zh/speech-cue.txt. Файл опциональный, формат и загрузчик готовы, но его байты входят в Version() пакета (langpack.go:188-191) ⇒ LangpackVersion (snapshot.go:376) ⇒ пере-покупка ОБЕИХ волн всех zh-книг. Цена ≠ ноль, польза = один знаменатель. Поэтому не шипнул: тот же аргумент, которым terminology.txt не шипят при совпадении с дефолтом (langpack.go:245-247). Пока файла нет, SourceReplies = 0 = «не измерено».
  3. golden сознательно НЕ расширен новыми счётчиками. Директива требует пустой маскированный дифф; добавление полей в capture — это ратифицированный пере-капчер, которого в директиве нет. Новые счётчики закрыты сквозными тестами через настоящий драйвер. Если оркестратор хочет их в golden — это отдельная строка «сдвинутой оси».
  4. Флаггер утечки НЕ гейтится gates.voice (сознательно): спойлер-окно — стоячий механизм безопасности C1, а не опциональный замер. Следствие: у книги с окнами счётчик n_spoiler_leaks начнёт заполняться без всякого включения. На провод и на снапшот это не влияет (retrieval_state пересчитывается каждый прогон), но это ИЗМЕНЕНИЕ наблюдаемого поведения без флага — называю строкой, а не прячу.
  5. Записка D21.10 в migrate.go переписана (была «reserved, not code»): теперь она фиксирует, что два типа построены, третий не нужен, и почему. Это зона бэкенда, но записка была ссылкой D-лога — сверьте формулировку.

8. Сдвинутые оси (п.6 «ровно так»)

Одна, и она нулевая по байтам: memory_version получил УСЛОВНОЕ слагаемое, которое при InjectVoice=false не пишется. Проверено исполнением с двух сторон — байт-идентичность (TestVoiceRowsDoNotMoveTheBankVersionWhileUninjected, плюс сверка с ComputeVersion старой формы) и golden байт-в-байт. Ни одна книга не двигается этим паком.

Наблюдаемое поведение, изменившееся без флага: n_spoiler_blocked теперь считает и sticky-реджекты (ветка недостижима в проде, §3), n_spoiler_leaks заполняется всегда (§7 пинг 4). Обаretrieval_state, самозалечивающиеся, вне хешей.


9. Мутации (23/23 красные)

Мутация Убита тестом
M1 фолд игнорирует условие инъекции (фолдит всегда) TestVoiceRowsDoNotMoveTheBankVersionWhileUninjected
M2 voice-строки не фолдятся даже при инъекции TestInjectedVoiceRowsMoveTheBankVersion
M3 тег ставится ДО скоуп-фильтра (нарушение (г)) TestVoiceFoldRespectsTheMinedScopeBeforeCountingIt
M4 sticky-реджект снова тихо дропается TestStickyCarryBlockedByWindowIsRecordedAsRejected
M5 утечка считает и sticky-реджекты (скоуп расширен) TestSpoilerLeakFlagger
M6 утечка не проверяет присутствие рендеринга TestSpoilerLeakFlagger
M7 ось A без вето по множественности TestVoiceAxisATVContradictionInOneReply
M8 ось B срабатывает при ПРИСУТСТВУЮЩЕМ самоназвании TestVoiceAxisBSelfReferenceFlattened
M9 ось D втянута в Total() TestVoiceAxisDIsAnIndicatorOutsideTheCount
M10 атрибуция принимает неоднозначного спикера TestVoiceAttributionRequiresOneSpeakerAndASpeechVerb
M11 атрибуция без глагола речи то же
M12 ось D без предусловия «на сцене ровно двое» TestVoiceAxisDIsAnIndicatorOutsideTheCount
M13 кий атрибутирует несколько реплик TestVoiceSourceReplyDenominator
M14 флаггер работает без целевых T/V-данных TestVoiceFlaggerIsInertWithoutTargetData
M15 цитата принимается за реплику (нет глагола/вето) TestVoiceQuotedRepliesNeedASpeechVerbAndRespectTheInnerVeto
M16 драйвер игнорирует гейт TestVoiceFlaggerIsSilentWhenTheGateIsOff
M17 гард «персонажа нет в банке» обезврежен TestSeedRefusesAVoiceProfileForAnUnknownCharacter
M18 пересекающиеся окна профиля принимаются TestBankSeedRefusesMalformedVoiceSections
M19 ReplaceBank не пишет пары TestVoiceContentRoundTripsThroughTheBankReplace
M20 проекция игнорирует окно профиля TestVoiceProjectionRespectsWindows
M21 setVoiceState не пишет в строку TestVoiceFlaggerReachesTheStoreAndTheReport
M22 противоречие регистров в сиде принимается TestBankSeedRefusesMalformedVoiceSections
M23 содержимое профиля вливается в глоссарные строки TestVoiceProfileNeverReachesTheMatcherOrTheInjection

Харнесс: scratchpad/pack19/mutate.py (правка → именованный тест → восстановление файла; .mutbak не остались, git status чист).


10. Тесты (26 новых ^func Test, исполнено)

checks/voice_test.go 9 · membank/memvoice_test.go 10 · pipeline/voicerun_test.go 5 · store/voice_test.go 2. Сквозные — через настоящий драйвер (TranslateBook с мок-провайдером): флаггер доезжает до retrieval_state и QualityReport и НЕ флагает юнит; гейт выключен ⇒ ноль байт; утечка всплывает без гейта; сид с профилем неизвестного персонажа останавливает прогон ДО вызовов; голос/пары round-trip через ReplaceBank. Схема: апгрейд БД v12→v14 с премис-проверкой + полная замена трёх типов.


11. Что осталось владельцу/оркестратору

  • решение по speech-cue.txt для zh (§7 пинг 2);
  • включение gates.voice на холодном мини-прогоне — первое настоящее измерение точности осей (§6 п.1);
  • инъекция — отдельное решение после полигон-эксперимента D21 п.2; при включении см. §5.

СТОП. Не коммичу.