# ПАК-19 «голос и состояние» — ФАЗА 1 (дизайн). Отчёт бэкенд-сессии, 2026-07-30 **Статус:** дизайн, кода нет. Дерево чистое по моей зоне (проверено `git status`). Аддендумы получены: дельта D39.54 (пачка 14а+14б залендена; фикс-строка `pipeline/render.go` комментарий поля `SHA256` — «hash of the raw file» → каноническая форма — взята в фазу 2). **СТОП после этого отчёта.** > **Ревью-шапка (оркестратор №9, 30.07): ДИЗАЙН ПРИНЯТ С ПОПРАВКАМИ, D39.55; стройка — после «идём» владельца.** > Верификация (7 адверсариальных верификаторов, приёмка дизайн-класса): 82 клейма — 71 CONFIRMED / 9 PARTIAL / > 2 REFUTED, фабрикаций ноль. Оба REFUTED — полнота, не факты: (1) «ровно так» п.7 промта (бюджет инъекции > входит в денежный потолок/reserve) в отчёте не закрыт → дозакрыть абзацем в отчёте фазы 2; (2) критика > полноты нет (5 линз §4 — другой мандат) → обязателен в отчёте стройки. Несущие клеймы (repin/фолды/мёртвые > колонки/reveal-механика/цены) подтверждены исполнением. Поправки фактов: пин «две строки одного src» живёт > в `memseed_test.go:125-133` и `:167-174`, а не в `:427-437` (там handoff ruby-чтения двух РАЗНЫХ src); > «ru-формальность» — research/15 §1.5 (`:90`), не §2.1; «рычаг как авторитет» — глосса (в §1.2 дословно > только «формат важнее наличия»; опора «в минус» — §3.2/T2); комментариев `verdictSnapshotID`/`guard_hash` > семь, не четыре (вывод «0 кодовых вхождений» устоял). Слепые классы флаггера утечки: source-anchored > (identity-twist без срабатывания ключа) и sticky-carry (`memory.go:495-497` дропает без `Rejected` — фикс > одной строкой в фазу 2); скоуп флаггера ратифицирован как «утечка при СРАБОТАВШЕМ ключе». Диапазон > «$0.065–0.1» склеен из двух методов и недооценивает юниты (1.0/главу против 1.4 мини-прогона), но верх > покрывает ($0.091–0.098 < 0.1) — принят как порядок величины. Новые «ровно так» фазы 2 — в D39.55. --- ## 1. Эхо-блок 1. Фаза 1 = только дизайн; деливерабл — этот файл → СТОП → ратификация. 2. Строю безусловную половину D21: схема трёх сущностей · детерминированный ты/вы-флаггер · reveal-окна. 3. Инъекция проектируется, в прод не проводится (D21 п.2 + `store/migrate.go:349`). 4. ШАГ 0 первым: синк против новой реальности банка (двухсекционная инъекция, `ComputeVersionScoped`, `repin.go`, `TerminologyGate`). 5. Ни одного пар/языкового литерала в Go; `address_pair` = материализация журнала, не второе хранилище; voice-профиль заморожен per-job, пока не доказано обратное. 6. Любая ось, входящая в снапшот или в `CheapGateVersion`, двигает пере-оплату — называется отдельной строкой. 7. НЕ делаю: Annotator · слой-2 · ru-target долг · метрики пилота · масштаб · платные прогоны · коммиты. 8. Расхождение промта с кодом = пинг, не тихая интерпретация. 9. Право сказать «этого делать не надо» использую — см. §6. 10. Отчёт меряется плотностью: улики — `file:line`, всё неулико́ванное помечено «мнение». --- ## 2. ШАГ 0 — обязательный синк «источник ↔ код ↔ вердикт» | # | Источник | Код | Вердикт | |---|---|---|---| | 1 | D21 п.1а: профиль ЗАМОРОЖЕН per-job **до лендинга D15.2** | `verdictSnapshotID`/`guard_hash` — **0 вхождений** вне комментариев (`snapshot.go:90`, `cheapgates.go:45`, `sanitizer.go:89`, `migrate.go:361` — все «moves to … with D15.2»); этап А залендён (D33), Б/В сознательно не строились (D39.31, бэклог 49) | **совпадает; заморозка ОСТАЁТСЯ.** Предусловие не наступило — см. §3.V1 | | 2 | D21 п.2: `address_pair` = материализация журнала ты/вы (D7-Правки-1) | журнала нет; есть статическая колонка `glossary.speech` (`migrate.go:184`), заполняется из сида (`memseed.go:96`), пишется/читается (`store/glossary.go:154,207) | **не построено** + находка Н1 | | 3 | D21 п.1б: приоритет `CONFIRMED-глоссарий > address > voice > reveal` | инъекция редактора ДВУХСЕКЦИОННАЯ (`memory.go:659–678`), порядок бюджета — `priorityRank` (`memory.go:564`) по осям confirmed/sticky/approved | **расходится с формулировкой** → пере-подтверждено в §3.V3 | | 4 | D39.42 п.3: ВСЕ строки банка фолдятся в версию | `ComputeVersionScoped` (`memory.go:341–374`) фолдит для каждой строки Status/Src/Dst/Sense/Type/Gender/Decl/Since/Until/AllowShort/Aliases | **совпадает** + находка Н2 (что НЕ фолдится) | | 5 | D39.42 п.5: точечная ре-редактура по ключу | `classifySnapshotMove` (`repin.go:61–91`): `moveBankOnly` ⟺ единственный различающийся ключ payload = `memory_version` (`repin.go:72`) | **совпадает** + находка Н3 (несущее следствие для схемы) | | 6 | Прецедент «гейт-конфиг вне снапшота» | `TerminologyGate` (`config/pipeline.go:266–270`) — не фолдится даже включённым, обоснование: производит ФАЙЛ, на провод попадает только через `mined_delta → memory_version` | **совпадает; прецедент применим к флаггеру** | | 7 | D39.53: языковой экран в гейте, не в langpack | `config/pipeline.go:284–292` — байты langpack двигают `LangpackVersion` (`snapshot.go:376`) ⇒ пере-покупка обеих волн всех книг пары | **совпадает; прямо применимо к T/V-данным** | | 8 | D39.47: жанровый словарь отменён как класс | `lang/langpack.go:260–264` — отсутствие зафиксировано решением | **совпадает; аргумент переносится на реестр обращений** (§3.V5) | | 9 | README/D39: «новый Ф2-consumer role = данные + один рендерер» | реестр `roleInjectionRenderers` (`chunkrun.go:97`) есть; но `MessagesWithInjection` (`render.go:245`) принимает ОДНУ строку инъекции | **совпадает частично** → голос/обращения обязаны быть СЕКЦИЯМИ одной инъекции | | 10 | research/15 §2.3: флаггеру нужна атрибуция речи | целевые данные `speech_verb`/`inner_marker` (`lang/data/target-ru.txt`, `checks/checkers.go:38–44`) уже есть и уже используются | **частично построено** — целевая половина есть, исходной нет (§3.V1-бис) | | 11 | Бэклог-15: гендер-аннотация НАРРАТИВНАЯ (since_ch-смены + перспектива) | `gender` — строка на строку банка + окно `since/until`; директива `genderConstraintNote` (`memory.go:718`) male/female/hidden | **частично**: смена по главам выражается двумя строками, «перспектива» — нет (§3.V2) | | 12 | D21 п.3: reveal-окна + pre/post-алиасы | `spoilerBlocked` (`memory.go:533`) + непересекающиеся окна на одном src — **уже поддержаны и запинены** (`memseed.go:247` + `memseed_test.go:427–437` «a spoiler handoff of the same reading»); у `glossary_aliases` (`migrate.go:206`) окна НЕТ, но оно и не нужно (алиасы наследуют окно строки) | **построено на ~90%** → §6, предложение сократить скоуп | ### Находки синка (4) **Н1 — колонка `speech` и `first_person` мертвы, но живут в сиде.** `seed.Term.Speech`/`FirstPerson` (`seed/seed.go:30,33`) → `store.GlossaryEntry` (`memseed.go:96–97`) → БД (`glossary.go:154`) → читаются обратно (`glossary.go:207`) — и **не потребляются ничем**: `grep Speech|FirstPerson` вне `store`/`seed` даёт 0 потребителей, поля нет даже в `entry` матчера (`memory.go:134–155`). То есть D7-«series-bible-lite» есть как хранилище и отсутствует как механизм. Для пака-19 это не косметика: `first_person` — это ровно `self_ref` voice-профиля, а `speech` — ровно та статическая матрица, которую D21 п.2 объявляет superseded журналом. **Решение дизайна: не расширять `speech`, а объявить её deprecated и перенести смысл в типизированные строки (§3.V2); `first_person` — переиспользовать как поле профиля.** **Н2 — что НЕ фолдится в `memory_version`.** Фолд-цикл (`memory.go:359–373`) пишет 10 полей + алиасы. Вне фолда остались: `Speech`, `FirstPerson`, `TranslitPolicy`, `NicknameTranslation`, `Note`, `RubyReading/RubyClass/Confidence`, `Source` (используется как фильтр, не как байты). Пока они не на проводе — безвредно. **В момент, когда любое из них попадает в инъекцию, оно ОБЯЗАНО войти в фолд тем же диффом**, иначе воспроизводится ровно класс тихой пере-оплаты, закрытый D39.42 п.3 (`memory.go:342–350`: правка меняет `content_hash` и resume, но не снапшот, а `projectRebill` проецирует по снапшоту ⇒ $0 в смете при реальной пере-оплате). Это несущий инвариант всего пака. **Н3 — `repin` диктует ХРАНЕНИЕ, а не только цену.** `classifySnapshotMove` признаёт сдвиг re-pinnable, только если единственный изменившийся ключ payload — `memory_version`; любой новый ключ или любой другой изменившийся ключ = `moveOther` (`repin.go:78–80,84`). ⇒ если голос/обращения получают СВОЙ компонент снапшота, то подпись voice-профиля перестаёт быть bank-only-сдвигом и стоит **всей редакторской волны** вместо затронутых юнитов — механизм D39.42 п.5 молча теряется для новой оси. Если же их содержимое входит в `memory_version`, точечная ре-редактура наследуется бесплатно и без единой правки `repin.go`. **Это решает V2.** **Н4 — две ловушки безусловного фолда.** `RenderFormatVersion` (`snapshot.go:427`) и `ContextAssembly` (`snapshot.go:421–425`, у полей `contextSnap` нет `omitempty`) фолдятся БЕЗУСЛОВНО. Наивная реализация — «бампнуть `RenderFormatVersion` под новый блок» и «добавить `voice_token_budget` в `contextSnap`» — пере-покупает **каждую книгу на свете**, включая те, у которых голоса нет. Тот же аргумент уже записан в коде (`memory.go:380–384`). Обход существует и запинен прецедентами: условный тег внутри `ComputeVersionScoped` (`memory.go:385–387`, `editor-unverified-section-v1`) и `omitempty` на нулевом значении. --- ## 3. Ответы по V1–V6 ### V1. Скоуп и порядок **Рекомендация (порядок фазы 2):** (1) схема трёх сущностей + загрузка из сида + условный фолд; (2) reveal — только флаггер утечки, без новой схемы (§6); (3) детерминированный флаггер обращений/голоса как отдельный гейт. Инъекция — раздел §3.V3, кода за ним нет. **Заморозка профиля per-job — снимать НЕ предлагаю, и это замер, а не осторожность.** Условие D21 п.1а — «до лендинга D15.2». В коде `verdictSnapshotID` и `guard_hash` не существуют (0 вхождений); четыре живых комментария (`snapshot.go:90`, `cheapgates.go:45`, `sanitizer.go:89`, `migrate.go:361`) описывают их как будущее. Пока их нет, mid-run append памяти двигает `memory_version` → снапшот → пере-оплата книги (D8/D15.1); ни одного «дешёвого» пути нет. **Аргумента за снятие у меня нет — приношу отсутствие аргумента, а не послабление.** **Границу ратификации держу дословно:** строится схема + флаггер + reveal; инъекция проектируется. Основание — D21 п.2 (`05-decisions-log.md:254`) и `store/migrate.go:349` («gated on the pilot and on D15.2 landing»). ### V1-бис. Вход, которого пока нет — честный ответ **Атрибуции прямой речи в движке НЕТ.** Что есть: - целевая половина: `chevronSpeechShape` (`cheapgates.go:250`) + `isSpokenChevronLine` (`checkers.go:467`) отвечают «эта строка — произнесённая реплика», по данным `speech_verb`/`inner_marker`. Это **не** «кто говорит»; - банк даёт множество персонажей, чьи имена сработали в чанке (AC-матчер); - исходной половины нет вовсе: `grep 说|道|quote` по `internal/miner/` — 0 попаданий. **Что флаггер НЕ поймает без Аннотатора** (числа — research/15 §2.3, не мои): - имплицитные реплики без атрибуции — концентрат ошибок (en 72–78% → ~53%); - **адресата** — он на 10–15 пп хуже спикера даже у LLM (70.3–79.9%), а явные кии его почти никогда не называют: 说 даёт говорящего, адресат приходит из 对X说 или вокатива; - спикера-местоимения (他说) — нужна кореференция; - чередование говорящих в многоходовом диалоге. **Несущее следствие, которое меняет форму деливерабла.** Проверка «ты/вы против реестра ПАР» требует обоих концов пары. Один конец (спикер) детерминированно достижим, второй — нет. Поэтому безусловный флаггер пака-19 должен стоять на трёх осях, которым адресат не нужен, и на одной, которой нужен, — с раздельным учётом: | Ось | Что нужно на входе | Достижимо сегодня | |---|---|---| | A. Внутреннее противоречие ты/вы в ОДНОЙ реплике | ничего, кроме границ реплики | да | | B. Самоназвание против `self_ref` профиля | только спикер | да (явный кий) | | C. Вхождение `ng_lexicon` спикера | только спикер | да | | D. Регистр пары против реестра | спикер **и** адресат | только эвристикой «в сцене ровно двое» | Оси A–C — безусловный урожай пака. Ось D строится, но **отчитывается отдельным счётчиком с явной оговоркой точности** и не смешивается с A–C в одном числе. Мнение: смешать их — это и есть «намерить сходимость сильнее данных»; счётчик D без Аннотатора честнее читать как индикатор, а не как метрику. **Гендер-аннотация нарративная (бэклог-15).** Сегодня `gender` — одно значение на строку банка с одним окном. Смена по ходу сюжета выражается уже сейчас — двумя строками с непересекающимися окнами (запинено: `memseed_test.go:427–437`). Чего НЕТ и что действительно требуется бэклогом-15 — **перспективы**: «пол скрыт от читателя, но известен персонажу X». Это второй ортогональный признак, и он не выражается ни окном, ни `gender`. Дизайн-ответ: перспектива — свойство READER-состояния, то есть та же ось, что reveal; её носитель — `reveal_ch` строки, а не новое поле пола. Директива `hidden` (`memory.go:718`, `GenderHidden`) уже реализует «избегать родовых форм до раскрытия» — то есть перспектива читателя уже закодирована, просто называется другим словом. ### V2. Схема и хранение **Рекомендация: три сущности живут как ТИПЫ ЗАПИСЕЙ БАНКА, а их содержимое фолдится в `memory_version`.** Форма таблиц вторична (отдельные таблицы допустимы), фолд — нет. Обоснование — Н3: единственный компонент снапшота, который `repin.go:72` умеет прощать, — `memory_version`. Собственный компонент снапшота = потеря точечной ре-редактуры для новой оси. Записка `migrate.go:355–375` предполагает «likely a `voice_profiles` table» — таблица допустима, **но её строки обязаны идти в тот же хеш**; иначе подпись одного профиля стоит редакторской волны. **Авторская поверхность — сид.** `seed.File` (`seed/seed.go:18`) — единственный артефакт, который и майнер пишет, и банк читает; voice/address курируются владельцем (D21 п.1д) и не майнятся вовсе. ⇒ две новые секции YAML рядом с `terms:`; контракт round-trip сохраняется. **`address_pair` как журнал, а не второе хранилище.** Требование D21 п.2 выполняется без новой машинерии: строка `(speaker, addressee, register, since_ch, until_ch)` **уже есть журнальная запись**, а «состояние на главе N» — проекция ровно того же вида, что `spoilerBlocked` (`memory.go:533`). Переход ты↔вы = вторая строка с непересекающимся окном. Отдельная таблица «текущего состояния» не нужна и была бы вторым источником истины, запрещённым D21 п.2. **Durable / производное / пере-оплачивается:** | | Что | Где | |---|---|---| | Durable | сид (подпись владельца) → строки банка, идемпотентный REPLACE | файл + `glossary`-семейство | | Производное | проекция регистра на главу, per-chunk отбор, счётчики флаггера | `retrieval_state`, пересчитывается каждый прогон (`waverun.go:390` — вызов безусловен) | | Пере-оплачивается | ТОЛЬКО если инъекция включена — и тогда только юниты, чьи инъектированные байты реально изменились (`repin.go`) | — | **Политика append в середине книги: запрещена (наследуется от D8/D15.1).** Профиль заморожен per-job; добавление строки между волнами = сдвиг `memory_version` = обычная громкая пере-оплата с согласием Р6 (`rebill.go`), а не тихий append. **Условный фолд — механизм, не дисциплина.** Строки голоса/обращений входят в `ComputeVersionScoped` **только когда инъекция включена**, по образцу `hasUnverified` (`memory.go:385–387`). Тогда: авторство профилей при выключенной инъекции — байт-идентичный хеш и $0; включение инъекции — громкий `--resnapshot` для этой книги; правка профиля при включённой инъекции — громкий сдвиг + точечная ре-редактура. Книга без профилей хешируется как раньше, побайтно. ### V3. Инъекция (проектируется; кода не будет) **Форма.** Одна инъекция на роль (ограничение `render.go:245` — один аргумент-строка), секциями. Порядок — пере-подтверждение D21 против двухсекционной реальности D39.45: ``` 1. КАНОН (CONFIRMED-глоссарий) — обязывающее 2. ОБРАЩЕНИЯ (регистр пары) — обязывающее 3. ГОЛОС (профиль спикера чанка) — обязывающее 4. ⟨проверить⟩ (неподписанный банк) — НЕ обязывающее, всегда последним ``` **Аргумент, а не умолчание.** D21 говорил «CONFIRMED-глоссарий > address > voice > reveal» до того, как глоссарий раскололся надвое. Правильный инвариант — не «после глоссария», а **«обязывающее перед необязывающим»**: заголовок `EditorUnverifiedHeader` (`lang/embedded.go:321–325`) вводит «дальше — непроверенное», и всё, поставленное после него, наследует это чтение. Вставить обращения/голос между секциями — значит подвесить подписанный закон под непроверенную шапку. **Идёт ли неподписанное голосовое в помеченную секцию — НЕТ, и это аргумент.** Вторая секция существует потому, что терминолог МАШИННО производит рендеринги в масштабе книги (D39.42) и выбрасывать их целиком расточительно. У voice-профиля в этом паке производителя нет вовсе: Аннотатор вне скоупа, exemplars заморожены, строки пишет владелец. ⇒ «неподписанный профиль» — категория без источника. Заводить её значит выдать модели необязывающую стилевую инструкцию, что хуже её отсутствия (research/15 §1.2: экземпляры — рычаг именно как авторитет; формат важнее наличия). **Голос и обращения — CONFIRMED-only; неподписанной строки этих типов не существует.** **Позиция и кэш.** Инъекция — отдельное system-сообщение ПОСЛЕ кэш-границы (`render.go:249–262`), то есть не кэшируется. Идея D21 п.1е «топ-2–3 героя в кэшируемый префикс» требует вложить блок в `msgs[0]` до границы, то есть сделать стабильный префикс зависимым от состояния книги. Считаю по прайсу: 300 ток./чанк × ~1000 чанков (500 глав × 2, каденс мини-прогона) = 300k входных токенов; в хвосте — 300k × $0.14/M = **$0.042**, в кэшируемом префиксе — × $0.0028/M = **$0.00084** (`models.yaml:149`). Экономия ≈ $0.04 на книге при COGS $0.85. **Рекомендация: не трогать префикс.** Мнение: 5% экономии не окупают превращение `msgs[0]` в книго-зависимый байт (окна профиля мигрируют — D21 п.1е сам это оговаривает). **Бюджет.** Сегодня `glossary_token_budget: 800` (`pipeline-c1.yaml:26`) — общий на чанк. Новые оси получают собственные knobs в `context`, **обязательно с `omitempty` и нулевым значением по умолчанию** (Н4): у существующих полей `contextSnap` (`snapshot.go:20–24`) `omitempty` нет, и добавление поля без него меняет payload каждой книги. Кап голоса ≤300 ток./чанк (D21 п.1б) принимается как есть. **Пер-ролевая разница.** Реестр `roleInjectionRenderers` (`chunkrun.go:97`) уже позволяет развести: переводчику — голос (D21: голос выигрывается на драфте), редактору — обращения + канон. **Честно: посылка «голос выигрывается на драфте» СЕГОДНЯ НЕ ИЗМЕРЕНА** — баннер ревизии D31 в `research/15` (шапка файла) фиксирует, что P3 снималась на ПАССИВНОМ моно-grok-редакторе, который вернул черновик дословно 3/4; после флипа D1 редактор билингвальный и активный. ⇒ выбор слота — вопрос полигон-эксперимента, не дизайна; в отчёт кладу оба провода, а не рекомендацию. **Что фолдится (и чего фолдить нельзя):** - содержимое строк → внутрь `memory_version`, условно (V2); - ФОРМАТ блока → **условный тег внутри `ComputeVersionScoped`, НЕ бамп `RenderFormatVersion`** (Н4: `snapshot.go:427` безусловен; книга без голоса не должна платить); - флаг «инъекция включена» → он и есть условие тега; отдельного компонента не нужно. Блок `context_assembly` получает новую ось только в виде `omitempty`-нулей — то есть присутствует, но байт-нейтрален до включения. Это и есть осознанный ответ на требование «новая ось обязана быть в нём или получить осознанный отказ». ### V4. Детерминированный детектор отклонения голоса (слой 1, $0) **Где живёт:** `internal/checks` — это пакет чистых функций без стора и часов (шапка `checks/cheapgates.go:1–10`), новый флаггер туда садится без исключений. **Как отчитывается:** счётчиками в `retrieval_state` через тот же путь, что стилевые флаггеры (`persistRetrievalState`, `chunkrun.go:106`), и агрегатом в `QualityReport` (`pipeline/quality.go:41–46`) — отдельными полями с `omitempty`, чтобы книга без голоса сериализовалась байт-идентично (прецедент: `UnsignedBankTerms`, `quality.go:112–114`). **Версионирование — рекомендация: СВОЙ гейт `gates.voice`, своя версия-константа, НЕ фолдить.** - Против включения в семью cheap-gates: `CheapGateVersion` (`cheapgates.go:94`) фолдится БЕЗУСЛОВНО (`snapshot.go:432`) ⇒ бамп = пере-оплата каждой книги каждой пары за механизм, которым она не пользуется. Это ровно то, что отказался делать пак-20 (`langpack.go:245–247`). - За «не фолдить вообще»: флаггер — observability, никогда не диспозиция (как все чекеры, `cheapgates.go:24–26`), а его счётчики пересчитываются каждый прогон и самозалечиваются (`waverun.go:390` — `persistRetrievalState` вызывается безусловно, не только при свежем вызове). Прямой прецедент — `TerminologyGate` (`config/pipeline.go:266–270`), которая не фолдится даже включённой и вместо этого логирует свою версию алгоритма с прогоном (`terminologist.go:53–55`). - **Принятая цена:** правка правила молча сдвигает записанные счётчики между прогонами под одним снапшотом. Смягчение — то же, что у терминолога: версия алгоритма логируется с прогоном и попадает в отчёт. Альтернатива (фолдить условно, как banknote — `snapshot.go:113–130`) остаётся дешёвым апгрейдом, если владелец решит, что счётчики должны быть воспроизводимы под снапшотом. **Слой 2 (эмбеддинги) — не в этом паке**, подтверждаю. ### V5. Общность (несущее) Три разных вида данных, три разных дома — и это не вкусовщина, а три разных ответа на вопрос «о чём этот факт»: | Данные | Пример | Дом | Прецедент | |---|---|---|---| | Различение T/V, формы 2 л. | «ты/вы», императивы | **ЦЕЛЕВЫЕ**, `lang/data/target-.txt` | `speech_verb`/`inner_marker` (`checkers.go:38–44`, `embedded.go:25`) | | Кии прямой речи | 说/道/问 | **ИСХОДНЫЕ**, `configs/langpacks//speech-cue.txt`, ОПЦИОНАЛЬНЫЙ файл | `terminology.txt` (`langpack.go:244–258`) | | Кто кому «вы» | 方源→李兄 = вы | **КНИЖНЫЕ**, сид | D39.47 (`langpack.go:260–264`) | Третья строка — прямой перенос аргумента отмены жанрового словаря: пар-широкий файл «как персонажи обращаются друг к другу» прописал бы одну социальную модель всем книгам пары, а решение о регистре — подпись владельца на банк КНИГИ. **Мысленный тест (обязателен) — пара, которой в репо НЕТ. Беру en→ru.** | Шаг | Что нужно дописать | Правка Go? | |---|---|---| | Различение ты/вы | ничего — цель та же (`target-ru.txt` уже есть) | нет | | Кии речи | `configs/langpacks/en/speech-cue.txt`: said, asked, replied… | нет (опциональный файл, `readOptional` — `langpack.go:537`) | | Реестр обращений | секции сида книги | нет | | Алфавит кавычек | `"…"` вместо `「…」` — **обязан прийти данными** | **да, если алгоритм зашьёт CJK-смежность** | ⇒ **Проектное ограничение, которое надо записать в фазу 2:** сопоставление кия ведётся через нормализацию + **алфавит кавычек как данные** (прецедент — `lang.Terminators`, `embedded.go:137–146`, где два класса терминаторов уже данные). Если этого не сделать, «имя + кий вплотную к кавычке» окажется правилом про китайский, зашитым в Go, — то есть ровно утечкой §0. **Пара БЕЗ T/V (→en, →ja):** целевой файл не несёт строк `tv_*` → флаггер инертен, 0 срабатываний, ноль правок Go. Прецедент поведения — `TargetChecks.HasData()` (`embedded.go:37`) и «пара, не включившаяся в механизм, никогда не флагается» (`langpack.go:229–231`). **Явная граница скоупа (и защита от углубления ru-target долга, бэклог 32):** v1 флаггера смотрит **только поверхности местоимений и императивов из целевых данных**. Никакой лемматизации, никакого согласования глагола, никакого `TokenizeCyrillic`-расширения. research/15 §2.1 говорит, что ru-формальность = местоимение 2 л. + согласование; беру только первую половину — она бинарна и надёжна на одной реплике, а вторая тянет морфологию, то есть слой 7 и чужой пак. ### V6. Деньги и измеримость **Флаггер: $0.** Детерминированный, в горячем пути, без вызовов. **Инъекция, если её когда-нибудь включат** (пересчёт «другим способом» — по действующему прайсу, а не по цифре D21): | Слот | Токенов | Цена входа | На книгу-500 | |---|---|---|---| | Драфт (~1000 чанков) | 300/чанк | $0.14/M (`models.yaml:149`) | **$0.042** | | Редактор (~500 юнитов) | 300/юнит | $0.435/M (`models.yaml:161`) | **$0.065** | **D21 п.8 «верхняя граница editor-side +$0.2–0.3» устарела по фактуре, а не по методу.** Она считалась при grok-редакторе ($1.25/M входа — research/15 §1.5); действующий интерим-редактор deepseek-v4-pro втрое дешевле по входу, и потолок падает до ~$0.065–0.1. Драфт-слот воспроизводит ратифицированную цифру ($0.02–0.05) точно. Развилки «дороже — повод для решения» не возникает ни в одном слоте. **Что мерить и чем** (нормы D39.46/47 — арм без фактора обязателен): - точность флаггера — на экспорте ХОЛОДНОГО мини-прогона, руками по срабатываниям; бэклог-24 уже держит строку «срабатывания голос-флаггера»; - **детерминированный скорер > судьи:** счёт флипов ты/вы на книгу — сам по себе метрика, судья не нужен; - арм без фактора: тот же прогон с `gates.voice.enabled: false` даёт нулевую линию бесплатно (флаггер $0, прогон один и тот же); - **не мерить здесь:** blind speaker attribution — сторона полигона, в пак не входит (промт это фиксирует, подтверждаю). --- ## 4. Адверсариальный селф-ревью (5 линз) **Линза «деньги и пере-оплата».** Три места, где наивная реализация пере-покупает ВСЕ книги: бамп `RenderFormatVersion` (`snapshot.go:427`), поле без `omitempty` в `contextSnap` (`snapshot.go:20–24`), бамп `CheapGateVersion` (`snapshot.go:432`). Все три обойдены механизмом, не дисциплиной (условный тег / нулевой `omitempty` / отдельный гейт). Четвёртое, менее очевидное: `LangpackVersion` (`snapshot.go:376`) — поэтому кии речи идут ОПЦИОНАЛЬНЫМ файлом; пара, которая его не шлёт, сохраняет байт-стабильную версию пакета. **Остаточный риск:** если фазу 2 будут строить «по образцу banknote», соблазн добавить `voiceSnap` отдельным ключом велик — и он тихо ломает `repin` (Н3). Это надо вынести в «ровно так» фазы 2. **Линза «durable-схема и миграции».** Миграции append-only (`migrate.go:9–11`); новые строки — аддитивные колонки/таблицы с дефолтами, как v11/v12 (`migrate.go:306,328`), то есть прошлые строки и провод не двигаются. Опасность иная: `ReplaceGlossary` заменяет весь глоссарий книги из детерминированных входов; новые типы записей обязаны попасть в ту же транзакцию замены, иначе появится второй жизненный цикл и рассинхрон с `term_id` (логическая связь без FK — `migrate.go:209`). **Остаточный риск:** `address_pair` ссылается на ДВА `term_id`, а `glossary.id` — свежий autoincrement на каждую замену (`migrate.go:173`); значит пара обязана храниться по СТАБИЛЬНОМУ ключу (src+sense), а не по `id`. Это надо явно записать, иначе реестр протухнет после первой же пере-засевки. **Линза «общность §0».** Ревью-вопрос «заработает ли на паре, которой нет»: см. таблицу V5 — заработает при одном условии (алфавит кавычек данными). Самая слабая точка честно названа: `speech-cue` формулируется как «кий вплотную к кавычке», и «вплотную» для zh (без пробелов) и для en (с пробелами и атрибуцией ПОСЛЕ реплики) — разные правила. Мнение: v1 должен принимать оба порядка (кий до / кий после) как данные-флаг в файле пары, а не как две ветки Go. **Линза «байт-точность и снапшот».** Единственная ось, которую пак сознательно двигает, — `memory_version`, и только для книги, где инъекция включена. Ожидание для golden: маскированный структурный дифф **пуст**, потому что фикстура голоса не имеет; ни одна ось не сдвигается. Если фаза 2 обнаружит непустой дифф — это дефект реализации, а не смена поведения. Отдельная строка на будущее: `banknote`-подобный `omitempty`-указатель для voice в снапшот **не заводить** (Н3). **Линза «граница с ru-target долгом».** Флаггер целиком питается целевыми данными и не добавляет ни одного `isRuTarget` (сейчас 11 сайтов, бэклог 32) и ни одного вызова `TokenizeCyrillic`. Точка риска: соблазн проверить согласование глагола. Явно запрещено в V5. **Остаточно:** `checks/repair.go:178–184` уже фиксирует «Go-level target assumption, которое не переопределяется данными» — новый флаггер не должен добавить второй такой. --- ## 5. Что предлагаю НЕ делать (право D39.46/47) **Reveal-окна как НОВЫЙ механизм строить не надо — он на ~90% уже построен, и это измерено, а не предположено.** - Пара «до раскрытия / после раскрытия» выражается двумя строками банка с непересекающимися окнами: `UNIQUE(book_id, src, sense, since_ch, until_ch)` (`migrate.go:202`) их различает, `windowsOverlap` (`memseed.go:247`) их пропускает (`[1,N]` vs `[N+1,∞]`), `spoilerBlocked` (`memory.go:533`) выбирает ровно одну, и это **уже запинено тестом** как «a spoiler handoff of the same reading» (`memseed_test.go:427–437`). - Алиасам собственное окно не нужно: алиас наследует окно своей строки (`memory.go:264–280`), а pre/post-алиасы принадлежат РАЗНЫМ строкам. - Единственное, чего действительно нет, — **флаггер утечки**: «dst строки, отклонённой спойлер-окном, появился в выходе». И он почти бесплатен: отклонённые строки уже собираются (`memory.go:472`, `Selection.Rejected`), а проверка присутствия dst с учётом склонений уже написана (`dstFormPresent`, `mempostcheck.go:144`). Это ~15 строк переиспользования и **ноль схемных изменений**. ⇒ **Предлагаю переопределить деливерабл «reveal-окна» как «флаггер спойлер-утечки + документированный паттерн двух строк», без колонки `reveal_ch` и без фазы на алиасах.** Колонка `reveal_ch`, предположенная запиской `migrate.go:372–375`, была бы третьим способом выразить то, что уже выражается двумя, — и каждый лишний способ означает ещё одну ветку в `spoilerBlocked`. --- ## 6. Пинги оркестратору (расхождения промта/записей с кодом) 1. **`migrate.go:372–375` (записка D21.10) предполагает колонку `reveal_ch` и фазу на алиасах — обе избыточны** (§5). Записка написана до того, как непересекающиеся окна были запинены тестом. Прошу ратифицировать сокращение, а не строить третий способ. 2. **`glossary.speech` и `glossary.first_person` — мёртвый код данных** (Н1): пишутся из сида, хранятся, не читаются никем. Либо они становятся носителями новых сущностей, либо их надо объявить deprecated. Тихо оставить нельзя: сид владельца выглядит работающим и не работает. 3. **D21 п.8 «+$0.2–0.3 editor-side» считалась при grok-редакторе**; при действующем интериме потолок ~$0.065–0.1 (§V6). Цифру в контракте стоит пометить как привязанную к модели. 4. **Посылка «голос выигрывается на драфте» (D21 п.1г/P3) не измерена после флипа D1** — это уже зафиксировано баннером ревизии D31 в `research/15`, но промт пака-19 подаёт драфт-слот как «дефолт». Прошу читать как «слот не выбран», иначе полигон получит арм с предрешённым исходом. 5. **Ось D флаггера (регистр пары) без Аннотатора неполна структурно, а не количественно** (§V1-бис): адресат детерминированно недостижим. Если ратификация ожидает от «безусловного флаггера» именно проверку ПАР, то безусловная половина D21 меньше, чем звучит, и это стоит зафиксировать до фазы 2, а не после. --- ## 7. Приёмка фазы 1 - Эхо-блок — §1 · ШАГ 0 с таблицей и 4 находками — §2 · V1–V6 с вариантами/рекомендацией/рисками — §3 · адверсариальный селф-ревью по 5 линзам — §4 · право «не делать» использовано — §5 · пинги — §6. - Кода не тронуто; коммитов нет; `git status` по моей зоне чист. - Аддендумы получены и учтены (шапка). **СТОП — жду ратификации.**