48 KiB
ПАК-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 = только дизайн; деливерабл — этот файл → СТОП → ратификация.
- Строю безусловную половину D21: схема трёх сущностей · детерминированный ты/вы-флаггер · reveal-окна.
- Инъекция проектируется, в прод не проводится (D21 п.2 +
store/migrate.go:349). - ШАГ 0 первым: синк против новой реальности банка (двухсекционная инъекция,
ComputeVersionScoped,repin.go,TerminologyGate). - Ни одного пар/языкового литерала в Go;
address_pair= материализация журнала, не второе хранилище; voice-профиль заморожен per-job, пока не доказано обратное. - Любая ось, входящая в снапшот или в
CheapGateVersion, двигает пере-оплату — называется отдельной строкой. - НЕ делаю: Annotator · слой-2 · ru-target долг · метрики пилота · масштаб · платные прогоны · коммиты.
- Расхождение промта с кодом = пинг, не тихая интерпретация.
- Право сказать «этого делать не надо» использую — см. §6.
- Отчёт меряется плотностью: улики —
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-<tgt>.txt |
speech_verb/inner_marker (checkers.go:38–44, embedded.go:25) |
| Кии прямой речи | 说/道/问 | ИСХОДНЫЕ, configs/langpacks/<src>/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. Пинги оркестратору (расхождения промта/записей с кодом)
migrate.go:372–375(записка D21.10) предполагает колонкуreveal_chи фазу на алиасах — обе избыточны (§5). Записка написана до того, как непересекающиеся окна были запинены тестом. Прошу ратифицировать сокращение, а не строить третий способ.glossary.speechиglossary.first_person— мёртвый код данных (Н1): пишутся из сида, хранятся, не читаются никем. Либо они становятся носителями новых сущностей, либо их надо объявить deprecated. Тихо оставить нельзя: сид владельца выглядит работающим и не работает.- D21 п.8 «+$0.2–0.3 editor-side» считалась при grok-редакторе; при действующем интериме потолок ~$0.065–0.1 (§V6). Цифру в контракте стоит пометить как привязанную к модели.
- Посылка «голос выигрывается на драфте» (D21 п.1г/P3) не измерена после флипа D1 — это уже зафиксировано баннером ревизии D31 в
research/15, но промт пака-19 подаёт драфт-слот как «дефолт». Прошу читать как «слот не выбран», иначе полигон получит арм с предрешённым исходом. - Ось D флаггера (регистр пары) без Аннотатора неполна структурно, а не количественно (§V1-бис): адресат детерминированно недостижим. Если ратификация ожидает от «безусловного флаггера» именно проверку ПАР, то безусловная половина D21 меньше, чем звучит, и это стоит зафиксировать до фазы 2, а не после.
7. Приёмка фазы 1
- Эхо-блок — §1 · ШАГ 0 с таблицей и 4 находками — §2 · V1–V6 с вариантами/рекомендацией/рисками — §3 · адверсариальный селф-ревью по 5 линзам — §4 · право «не делать» использовано — §5 · пинги — §6.
- Кода не тронуто; коммитов нет;
git statusпо моей зоне чист. - Аддендумы получены и учтены (шапка).
СТОП — жду ратификации.