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

48 KiB
Raw Permalink Blame History

ПАК-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_hash0 вхождений вне комментариев (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:390persistRetrievalState вызывается безусловно, не только при свежем вызове). Прямой прецедент — 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… нет (опциональный файл, readOptionallangpack.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 по моей зоне чист.
  • Аддендумы получены и учтены (шапка).

СТОП — жду ратификации.