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

255 lines
48 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# ПАК-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.0650.1» склеен из двух методов и недооценивает юниты (1.0/главу против 1.4 мини-прогона), но верх
> покрывает ($0.0910.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:659678`), порядок бюджета — `priorityRank` (`memory.go:564`) по осям confirmed/sticky/approved | **расходится с формулировкой** → пере-подтверждено в §3.V3 |
| 4 | D39.42 п.3: ВСЕ строки банка фолдятся в версию | `ComputeVersionScoped` (`memory.go:341374`) фолдит для каждой строки Status/Src/Dst/Sense/Type/Gender/Decl/Since/Until/AllowShort/Aliases | **совпадает** + находка Н2 (что НЕ фолдится) |
| 5 | D39.42 п.5: точечная ре-редактура по ключу | `classifySnapshotMove` (`repin.go:6191`): `moveBankOnly` ⟺ единственный различающийся ключ payload = `memory_version` (`repin.go:72`) | **совпадает** + находка Н3 (несущее следствие для схемы) |
| 6 | Прецедент «гейт-конфиг вне снапшота» | `TerminologyGate` (`config/pipeline.go:266270`) — не фолдится даже включённым, обоснование: производит ФАЙЛ, на провод попадает только через `mined_delta → memory_version` | **совпадает; прецедент применим к флаггеру** |
| 7 | D39.53: языковой экран в гейте, не в langpack | `config/pipeline.go:284292` — байты langpack двигают `LangpackVersion` (`snapshot.go:376`) ⇒ пере-покупка обеих волн всех книг пары | **совпадает; прямо применимо к T/V-данным** |
| 8 | D39.47: жанровый словарь отменён как класс | `lang/langpack.go:260264` — отсутствие зафиксировано решением | **совпадает; аргумент переносится на реестр обращений** (§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:3844`) уже есть и уже используются | **частично построено** — целевая половина есть, исходной нет (§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:427437` «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:9697`) → БД (`glossary.go:154`) → читаются обратно (`glossary.go:207`) — и **не потребляются ничем**: `grep Speech|FirstPerson` вне `store`/`seed` даёт 0 потребителей, поля нет даже в `entry` матчера (`memory.go:134155`). То есть 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:359373`) пишет 10 полей + алиасы. Вне фолда остались: `Speech`, `FirstPerson`, `TranslitPolicy`, `NicknameTranslation`, `Note`, `RubyReading/RubyClass/Confidence`, `Source` (используется как фильтр, не как байты). Пока они не на проводе — безвредно. **В момент, когда любое из них попадает в инъекцию, оно ОБЯЗАНО войти в фолд тем же диффом**, иначе воспроизводится ровно класс тихой пере-оплаты, закрытый D39.42 п.3 (`memory.go:342350`: правка меняет `content_hash` и resume, но не снапшот, а `projectRebill` проецирует по снапшоту ⇒ $0 в смете при реальной пере-оплате). Это несущий инвариант всего пака.
**Н3 — `repin` диктует ХРАНЕНИЕ, а не только цену.** `classifySnapshotMove` признаёт сдвиг re-pinnable, только если единственный изменившийся ключ payload — `memory_version`; любой новый ключ или любой другой изменившийся ключ = `moveOther` (`repin.go:7880,84`). ⇒ если голос/обращения получают СВОЙ компонент снапшота, то подпись voice-профиля перестаёт быть bank-only-сдвигом и стоит **всей редакторской волны** вместо затронутых юнитов — механизм D39.42 п.5 молча теряется для новой оси. Если же их содержимое входит в `memory_version`, точечная ре-редактура наследуется бесплатно и без единой правки `repin.go`. **Это решает V2.**
**Н4 — две ловушки безусловного фолда.** `RenderFormatVersion` (`snapshot.go:427`) и `ContextAssembly` (`snapshot.go:421425`, у полей `contextSnap` нет `omitempty`) фолдятся БЕЗУСЛОВНО. Наивная реализация — «бампнуть `RenderFormatVersion` под новый блок» и «добавить `voice_token_budget` в `contextSnap`» — пере-покупает **каждую книгу на свете**, включая те, у которых голоса нет. Тот же аргумент уже записан в коде (`memory.go:380384`). Обход существует и запинен прецедентами: условный тег внутри `ComputeVersionScoped` (`memory.go:385387`, `editor-unverified-section-v1`) и `omitempty` на нулевом значении.
---
## 3. Ответы по V1V6
### 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 7278% → ~53%);
- **адресата** — он на 1015 пп хуже спикера даже у LLM (70.379.9%), а явные кии его почти никогда не называют: 说 даёт говорящего, адресат приходит из 对X说 или вокатива;
- спикера-местоимения (他说) — нужна кореференция;
- чередование говорящих в многоходовом диалоге.
**Несущее следствие, которое меняет форму деливерабла.** Проверка «ты/вы против реестра ПАР» требует обоих концов пары. Один конец (спикер) детерминированно достижим, второй — нет. Поэтому безусловный флаггер пака-19 должен стоять на трёх осях, которым адресат не нужен, и на одной, которой нужен, — с раздельным учётом:
| Ось | Что нужно на входе | Достижимо сегодня |
|---|---|---|
| A. Внутреннее противоречие ты/вы в ОДНОЙ реплике | ничего, кроме границ реплики | да |
| B. Самоназвание против `self_ref` профиля | только спикер | да (явный кий) |
| C. Вхождение `ng_lexicon` спикера | только спикер | да |
| D. Регистр пары против реестра | спикер **и** адресат | только эвристикой «в сцене ровно двое» |
Оси AC — безусловный урожай пака. Ось D строится, но **отчитывается отдельным счётчиком с явной оговоркой точности** и не смешивается с AC в одном числе. Мнение: смешать их — это и есть «намерить сходимость сильнее данных»; счётчик D без Аннотатора честнее читать как индикатор, а не как метрику.
**Гендер-аннотация нарративная (бэклог-15).** Сегодня `gender` — одно значение на строку банка с одним окном. Смена по ходу сюжета выражается уже сейчас — двумя строками с непересекающимися окнами (запинено: `memseed_test.go:427437`). Чего НЕТ и что действительно требуется бэклогом-15 — **перспективы**: «пол скрыт от читателя, но известен персонажу X». Это второй ортогональный признак, и он не выражается ни окном, ни `gender`. Дизайн-ответ: перспектива — свойство READER-состояния, то есть та же ось, что reveal; её носитель — `reveal_ch` строки, а не новое поле пола. Директива `hidden` (`memory.go:718`, `GenderHidden`) уже реализует «избегать родовых форм до раскрытия» — то есть перспектива читателя уже закодирована, просто называется другим словом.
### V2. Схема и хранение
**Рекомендация: три сущности живут как ТИПЫ ЗАПИСЕЙ БАНКА, а их содержимое фолдится в `memory_version`.** Форма таблиц вторична (отдельные таблицы допустимы), фолд — нет.
Обоснование — Н3: единственный компонент снапшота, который `repin.go:72` умеет прощать, — `memory_version`. Собственный компонент снапшота = потеря точечной ре-редактуры для новой оси. Записка `migrate.go:355375` предполагает «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:385387`). Тогда: авторство профилей при выключенной инъекции — байт-идентичный хеш и $0; включение инъекции — громкий `--resnapshot` для этой книги; правка профиля при включённой инъекции — громкий сдвиг + точечная ре-редактура. Книга без профилей хешируется как раньше, побайтно.
### V3. Инъекция (проектируется; кода не будет)
**Форма.** Одна инъекция на роль (ограничение `render.go:245` — один аргумент-строка), секциями. Порядок — пере-подтверждение D21 против двухсекционной реальности D39.45:
```
1. КАНОН (CONFIRMED-глоссарий) — обязывающее
2. ОБРАЩЕНИЯ (регистр пары) — обязывающее
3. ГОЛОС (профиль спикера чанка) — обязывающее
4. ⟨проверить⟩ (неподписанный банк) — НЕ обязывающее, всегда последним
```
**Аргумент, а не умолчание.** D21 говорил «CONFIRMED-глоссарий > address > voice > reveal» до того, как глоссарий раскололся надвое. Правильный инвариант — не «после глоссария», а **«обязывающее перед необязывающим»**: заголовок `EditorUnverifiedHeader` (`lang/embedded.go:321325`) вводит «дальше — непроверенное», и всё, поставленное после него, наследует это чтение. Вставить обращения/голос между секциями — значит подвесить подписанный закон под непроверенную шапку.
**Идёт ли неподписанное голосовое в помеченную секцию — НЕТ, и это аргумент.** Вторая секция существует потому, что терминолог МАШИННО производит рендеринги в масштабе книги (D39.42) и выбрасывать их целиком расточительно. У voice-профиля в этом паке производителя нет вовсе: Аннотатор вне скоупа, exemplars заморожены, строки пишет владелец. ⇒ «неподписанный профиль» — категория без источника. Заводить её значит выдать модели необязывающую стилевую инструкцию, что хуже её отсутствия (research/15 §1.2: экземпляры — рычаг именно как авторитет; формат важнее наличия). **Голос и обращения — CONFIRMED-only; неподписанной строки этих типов не существует.**
**Позиция и кэш.** Инъекция — отдельное system-сообщение ПОСЛЕ кэш-границы (`render.go:249262`), то есть не кэшируется. Идея D21 п.1е «топ-23 героя в кэшируемый префикс» требует вложить блок в `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:2024`) `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:110`), новый флаггер туда садится без исключений.
**Как отчитывается:** счётчиками в `retrieval_state` через тот же путь, что стилевые флаггеры (`persistRetrievalState`, `chunkrun.go:106`), и агрегатом в `QualityReport` (`pipeline/quality.go:4146`) — отдельными полями с `omitempty`, чтобы книга без голоса сериализовалась байт-идентично (прецедент: `UnsignedBankTerms`, `quality.go:112114`).
**Версионирование — рекомендация: СВОЙ гейт `gates.voice`, своя версия-константа, НЕ фолдить.**
- Против включения в семью cheap-gates: `CheapGateVersion` (`cheapgates.go:94`) фолдится БЕЗУСЛОВНО (`snapshot.go:432`) ⇒ бамп = пере-оплата каждой книги каждой пары за механизм, которым она не пользуется. Это ровно то, что отказался делать пак-20 (`langpack.go:245247`).
- За «не фолдить вообще»: флаггер — observability, никогда не диспозиция (как все чекеры, `cheapgates.go:2426`), а его счётчики пересчитываются каждый прогон и самозалечиваются (`waverun.go:390` — `persistRetrievalState` вызывается безусловно, не только при свежем вызове). Прямой прецедент — `TerminologyGate` (`config/pipeline.go:266270`), которая не фолдится даже включённой и вместо этого логирует свою версию алгоритма с прогоном (`terminologist.go:5355`).
- **Принятая цена:** правка правила молча сдвигает записанные счётчики между прогонами под одним снапшотом. Смягчение — то же, что у терминолога: версия алгоритма логируется с прогоном и попадает в отчёт. Альтернатива (фолдить условно, как banknote — `snapshot.go:113130`) остаётся дешёвым апгрейдом, если владелец решит, что счётчики должны быть воспроизводимы под снапшотом.
**Слой 2 (эмбеддинги) — не в этом паке**, подтверждаю.
### V5. Общность (несущее)
Три разных вида данных, три разных дома — и это не вкусовщина, а три разных ответа на вопрос «о чём этот факт»:
| Данные | Пример | Дом | Прецедент |
|---|---|---|---|
| Различение T/V, формы 2 л. | «ты/вы», императивы | **ЦЕЛЕВЫЕ**, `lang/data/target-<tgt>.txt` | `speech_verb`/`inner_marker` (`checkers.go:3844`, `embedded.go:25`) |
| Кии прямой речи | 说/道/问 | **ИСХОДНЫЕ**, `configs/langpacks/<src>/speech-cue.txt`, ОПЦИОНАЛЬНЫЙ файл | `terminology.txt` (`langpack.go:244258`) |
| Кто кому «вы» | 方源→李兄 = вы | **КНИЖНЫЕ**, сид | D39.47 (`langpack.go:260264`) |
Третья строка — прямой перенос аргумента отмены жанрового словаря: пар-широкий файл «как персонажи обращаются друг к другу» прописал бы одну социальную модель всем книгам пары, а решение о регистре — подпись владельца на банк КНИГИ.
**Мысленный тест (обязателен) — пара, которой в репо НЕТ. Беру 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:137146`, где два класса терминаторов уже данные). Если этого не сделать, «имя + кий вплотную к кавычке» окажется правилом про китайский, зашитым в Go, — то есть ровно утечкой §0.
**Пара БЕЗ T/V (→en, →ja):** целевой файл не несёт строк `tv_*` → флаггер инертен, 0 срабатываний, ноль правок Go. Прецедент поведения — `TargetChecks.HasData()` (`embedded.go:37`) и «пара, не включившаяся в механизм, никогда не флагается» (`langpack.go:229231`).
**Явная граница скоупа (и защита от углубления 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.20.3» устарела по фактуре, а не по методу.** Она считалась при grok-редакторе ($1.25/M входа — research/15 §1.5); действующий интерим-редактор deepseek-v4-pro втрое дешевле по входу, и потолок падает до ~$0.0650.1. Драфт-слот воспроизводит ратифицированную цифру ($0.020.05) точно. Развилки «дороже — повод для решения» не возникает ни в одном слоте.
**Что мерить и чем** (нормы D39.46/47 — арм без фактора обязателен):
- точность флаггера — на экспорте ХОЛОДНОГО мини-прогона, руками по срабатываниям; бэклог-24 уже держит строку «срабатывания голос-флаггера»;
- **детерминированный скорер > судьи:** счёт флипов ты/вы на книгу — сам по себе метрика, судья не нужен;
- арм без фактора: тот же прогон с `gates.voice.enabled: false` даёт нулевую линию бесплатно (флаггер $0, прогон один и тот же);
- **не мерить здесь:** blind speaker attribution — сторона полигона, в пак не входит (промт это фиксирует, подтверждаю).
---
## 4. Адверсариальный селф-ревью (5 линз)
**Линза «деньги и пере-оплата».** Три места, где наивная реализация пере-покупает ВСЕ книги: бамп `RenderFormatVersion` (`snapshot.go:427`), поле без `omitempty` в `contextSnap` (`snapshot.go:2024`), бамп `CheapGateVersion` (`snapshot.go:432`). Все три обойдены механизмом, не дисциплиной (условный тег / нулевой `omitempty` / отдельный гейт). Четвёртое, менее очевидное: `LangpackVersion` (`snapshot.go:376`) — поэтому кии речи идут ОПЦИОНАЛЬНЫМ файлом; пара, которая его не шлёт, сохраняет байт-стабильную версию пакета. **Остаточный риск:** если фазу 2 будут строить «по образцу banknote», соблазн добавить `voiceSnap` отдельным ключом велик — и он тихо ломает `repin` (Н3). Это надо вынести в «ровно так» фазы 2.
**Линза «durable-схема и миграции».** Миграции append-only (`migrate.go:911`); новые строки — аддитивные колонки/таблицы с дефолтами, как 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:178184` уже фиксирует «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:427437`).
- Алиасам собственное окно не нужно: алиас наследует окно своей строки (`memory.go:264280`), а pre/post-алиасы принадлежат РАЗНЫМ строкам.
- Единственное, чего действительно нет, — **флаггер утечки**: «dst строки, отклонённой спойлер-окном, появился в выходе». И он почти бесплатен: отклонённые строки уже собираются (`memory.go:472`, `Selection.Rejected`), а проверка присутствия dst с учётом склонений уже написана (`dstFormPresent`, `mempostcheck.go:144`). Это ~15 строк переиспользования и **ноль схемных изменений**.
⇒ **Предлагаю переопределить деливерабл «reveal-окна» как «флаггер спойлер-утечки + документированный паттерн двух строк», без колонки `reveal_ch` и без фазы на алиасах.** Колонка `reveal_ch`, предположенная запиской `migrate.go:372375`, была бы третьим способом выразить то, что уже выражается двумя, — и каждый лишний способ означает ещё одну ветку в `spoilerBlocked`.
---
## 6. Пинги оркестратору (расхождения промта/записей с кодом)
1. **`migrate.go:372375` (записка D21.10) предполагает колонку `reveal_ch` и фазу на алиасах — обе избыточны** (§5). Записка написана до того, как непересекающиеся окна были запинены тестом. Прошу ратифицировать сокращение, а не строить третий способ.
2. **`glossary.speech` и `glossary.first_person` — мёртвый код данных** (Н1): пишутся из сида, хранятся, не читаются никем. Либо они становятся носителями новых сущностей, либо их надо объявить deprecated. Тихо оставить нельзя: сид владельца выглядит работающим и не работает.
3. **D21 п.8 «+$0.20.3 editor-side» считалась при grok-редакторе**; при действующем интериме потолок ~$0.0650.1 (§V6). Цифру в контракте стоит пометить как привязанную к модели.
4. **Посылка «голос выигрывается на драфте» (D21 п.1г/P3) не измерена после флипа D1** — это уже зафиксировано баннером ревизии D31 в `research/15`, но промт пака-19 подаёт драфт-слот как «дефолт». Прошу читать как «слот не выбран», иначе полигон получит арм с предрешённым исходом.
5. **Ось D флаггера (регистр пары) без Аннотатора неполна структурно, а не количественно** (§V1-бис): адресат детерминированно недостижим. Если ратификация ожидает от «безусловного флаггера» именно проверку ПАР, то безусловная половина D21 меньше, чем звучит, и это стоит зафиксировать до фазы 2, а не после.
---
## 7. Приёмка фазы 1
- Эхо-блок — §1 · ШАГ 0 с таблицей и 4 находками — §2 · V1V6 с вариантами/рекомендацией/рисками — §3 · адверсариальный селф-ревью по 5 линзам — §4 · право «не делать» использовано — §5 · пинги — §6.
- Кода не тронуто; коммитов нет; `git status` по моей зоне чист.
- Аддендумы получены и учтены (шапка).
**СТОП — жду ратификации.**