28 KiB
ПАК-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.txtzh НЕ шипить до решения владельца — бэклог 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. Эхо-блок
- Шесть пунктов директивы построены; инъекции нет — кода за §3.V3 не написано.
- Три сущности = отдельные таблицы, фолд — внутрь
memory_version(запрет (д) соблюдён). - Фолд условный и сегодня НИКОГДА не пишется ⇒ хеш каждой книги байт-идентичен, golden пуст.
- (в) закрыт компиляцией, а не фильтром: voice/address — отдельные Go-типы.
- Утечка считается только по несработавшему-по-окну при СРАБОТАВШЕМ ключе; sticky-реджекты (ж) в счёт не входят.
- Регистр — абстрактный (
informal/formal), поверхности — целевые данные. gates.voiceне фолдится;VoiceCheckVersionедет с прогоном и в отчёт.speech-cue.txtдля zh НЕ шипнут — его байты двигаютLangpackVersion. Решение владельцу (§7).- Одно отклонение от буквы дизайна (механизм атрибуции — целевой, не исходный) — не молчу, §7 пинг 1.
- Не делаю: инъекцию · 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, оси A–C в 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. Найдено и починено ИСПОЛНЕНИЕМ (то, чего дизайн не видел)
- Отсчёт исходных реплик двоил.
“来吧。”方源说道。他走了。“不。”давал 2 атрибутированные реплики на одном кие: окно «до открывающей кавычки» дотягивалось до кия ПРЕДЫДУЩЕЙ реплики. Исправлено потреблением: кий атрибутирует не более одной реплики (countSourceReplies,consumed). Мутация M13 возвращает дефект → красный. - Две мои же мутации выжили в первом круге — обе оказались реальными дырами тестов, не формальностью:
- M12 (снять предусловие «на сцене ровно двое»): мой «третий персонаж» стоял В АТРИБУЦИИ, поэтому кейс на самом деле проверял неоднозначную атрибуцию, а не эвристику адресата. Перенёс третье имя в НАРРАТИВ + добавил утверждение
Attributed == 1, чтобы кейс не мог снова уехать не на то правило. - M14 (убрать инертный путь без T/V-данных): мой «инертный» кейс не имел и маркеров реплик, так что T/V-гард был замаскирован гардом сегментации. Добавлен кейс «цель РАЗМЕЧАЕТ диалог, но T/V-различения не имеет» (en-подобная цель) — он изолирует именно T/V-гард. Обе дыры закрыты, второй круг: 23/23 красные.
- M12 (снять предусловие «на сцене ровно двое»): мой «третий персонаж» стоял В АТРИБУЦИИ, поэтому кейс на самом деле проверял неоднозначную атрибуцию, а не эвристику адресата. Перенёс третье имя в НАРРАТИВ + добавил утверждение
- Тест апгрейда схемы был бы вакуумным без премис-проверки: добавил два
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 (ровно так (б))
Спрашиваю себя: что в этом паке НЕ покрыто, НЕ измерено или измерено слабее, чем звучит.
- Оси B и D держатся на данных, которых на стенде нет. Профилей и реестра ни у одной стендовой книги нет,
gates.voiceвыключен,speech-cue.txtне шипнут. Значит на реальном тексте флаггер сегодня даёт ровно ноль срабатываний — и это НЕ «чисто», а «не измерено». Первое настоящее измерение — холодный мини-прогон (бэклог-24 уже держит строку «срабатывания голос-флаггера»); до него точность осей не известна. - Ось A может ловить цитату. Реплика, внутри которой персонаж ЦИТИРУЕТ чужие слова с другим регистром, засчитается противоречием. Вето по множественности этого не снимает. Класс не закрыт и не обойдён — назван.
- Ось B узка по построению. Она срабатывает только когда в реплике есть ОБЩЕЕ местоимение и нет заявленного самоназвания. Реплика, где служанка не называет себя вовсе, — молчание, а не сигнал. Это осознанный precision-over-recall, но «доля сохранённых маркеров» из research/15 этим НЕ меряется.
- Атрибуция ловит только ту форму имени, что есть в
decl. Мой же тест это и показал: без «Фан Юаню» неоднозначная атрибуция читается как однозначная. Значит качество осей B/C/D — функция ПОЛНОТЫ склонений в сиде, а не только кода. Для полигона это прямое следствие: неполныйdeclтихо снижаетAttributed, а не флаги. - Сегментация реплик — построчная. Реплика, разорванная переносом строки внутри одного хода, разъедется на две; реплика без маркера (косвенная речь) не видна вовсе.
SourceRepliesмеряет исходник, но сопоставить с целевымRepliesнельзя — выравнивания нет. Это два независимых счётчика; читать разницу как «потеряли реплики» без ручной сверки нельзя.- Схема есть, потребителя-инъекции нет.
register,lexicon_markers,exemplars,brightness,address_default,form,closenessхранятся, валидируются и (при включённой инъекции) хешируются — но сегодня их не читает НИКТО. Это ратифицированное состояние (D21 п.2), но честное имя ему — «зарезервировано», а не «работает». - Не измерено: холодный старт голоса. Как поведёт себя флаггер на книге, где банк пуст, а профили есть, — не проверено ни фикстурой, ни прогоном.
- Пре-существующее, не моё, но рядом: ALTER-шаги миграций (v8–v14) не идемпотентны, вопреки шапке
migrate.go:9-11, которая обещает сходимость полу-применённой БД. Счётчик версий это скрывает. Мой апгрейд-тест показывает, что путь v12→v14 работает; про полу-применённый шаг не утверждаю ничего.
7. Пинги оркестратору
- Отклонение от буквы дизайна, названное вслух: атрибуция ЦЕЛЕВАЯ, а не исходная. Фаза 1 набросала исходный пре-гейт «имя + 说/道 у кавычки». При стройке выяснилось то, что на бумаге не видно: кий называет говорящего в ИСХОДНИКЕ, а проверять надо реплику в ЦЕЛИ, и выравнивания src↔tgt в движке нет — перенести спикера через границу нечем. Целевой стороне выравнивание не нужно: хвост атрибуции русской тире-строки уже называет спикера словами, которые банк умеет матчить (
speech_verb+decl-формы). Поэтому оси A–D работают на целевой стороне, а исходныйspeech-cue.txtпостроен и используется ТОЛЬКО как знаменательSourceReplies. Скоуп не расширен и не сужен; изменился механизм внутри пункта. - Решение владельца: шипить ли
configs/langpacks/zh/speech-cue.txt. Файл опциональный, формат и загрузчик готовы, но его байты входят вVersion()пакета (langpack.go:188-191) ⇒LangpackVersion(snapshot.go:376) ⇒ пере-покупка ОБЕИХ волн всех zh-книг. Цена ≠ ноль, польза = один знаменатель. Поэтому не шипнул: тот же аргумент, которымterminology.txtне шипят при совпадении с дефолтом (langpack.go:245-247). Пока файла нет,SourceReplies= 0 = «не измерено». - golden сознательно НЕ расширен новыми счётчиками. Директива требует пустой маскированный дифф; добавление полей в capture — это ратифицированный пере-капчер, которого в директиве нет. Новые счётчики закрыты сквозными тестами через настоящий драйвер. Если оркестратор хочет их в golden — это отдельная строка «сдвинутой оси».
- Флаггер утечки НЕ гейтится
gates.voice(сознательно): спойлер-окно — стоячий механизм безопасности C1, а не опциональный замер. Следствие: у книги с окнами счётчикn_spoiler_leaksначнёт заполняться без всякого включения. На провод и на снапшот это не влияет (retrieval_stateпересчитывается каждый прогон), но это ИЗМЕНЕНИЕ наблюдаемого поведения без флага — называю строкой, а не прячу. - Записка 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.
СТОП. Не коммичу.