# Пак-20, продолжение: СТРОЙКА дисциплины банкноты и языкового экрана **База:** D39.51 (дизайн ратифицирован, пп.1–7 санкционированы) + D39.52 (п.8 — строка примера в промпте). **Зона:** `backend/`. **Деньги сессии: $0** — ни одного платного вызова, вся приёмка на сохранённом сырье. **Статус:** построено и проверено. Сессия НЕ коммитит, лендинг — оркестратора. > **Ревью-шапка (оркестратор №8, 26.07): ПРИНЯТО И ЗАЛЕНДЕНО, D39.53.** Приёмка исполнением: vet/gofmt > чисты (llm.go — до-паковый), свежие прогоны terminology/text `-race -count=1` и golden зелёные; моя > адверсариальная мутация вне списка сессии (дедуп голосов по чанкам снят) — КРАСНАЯ, восстановление > побайтно. Прогон предиката по живому сырью — 27 латинских строк совпали побайтно + два честных улова > сверх метрики полигона (китайские пересказы 第四代族长/长老) — принято как улучшение, не расхождение. > §5 (парсер банкноты мёртв для не-CJK исходника, parse_fail на каждой строке) — дефолт решения: > чинить пар-слепым правилом «строка непуста и встречается в тексте книги» НА холодном мини-прогоне > (версия парсера фолдится → перекупка; холодная база платит ноль). Вето владельца — одной строкой. > **ПОПРАВКА ПРИЁМКИ (улов владельца, после лендинга): клейм §2.5 «шапка-комментарий, которую модель > не видит» — ЛОЖЕН.** `LoadPromptTemplate` комментарии НЕ вырезает (render.go:92-110) — 12-строчная > мета-шапка `terminologist.md` уезжает модели в system каждого вызова, и боевые байты промпта ≠ > измеренным полигоном. Фикс забронирован (бэклог 14б: вырезание `` в загрузчике до сплита, > SHA по канонической форме — файлы без комментариев байт-в-байт прежние; шапка ужимается до 1–2 строк; > тест-пин «комментарий не в проводе»). Приёмка D39.53 этот клейм не проверила — записано как промах. --- ## §1 Что построено | № | Пункт | Где | |---|---|---| | 1 | Языковой экран на выходе роли | `terminology/script.go` (`OffLanguage`, `ScriptByName`) + отказ строки в `ParseReply` там же, где эхо-гейт | | 2 | Счётчик `OffLanguage` + громкий лог батча | `terminology.ReplyStats`, `terminologyResult.OffLanguage`, warn в `runTerminologist` | | 3 | Языковой фильтр черновой стороны | `pipeline/banknote.go` — `bankFold`, счётчик в стоп-логе | | 4 | Нормализация обрамляющих кавычек | `text/enclosure.go` (`TrimEnclosure`), применяется в свёртке | | 5 | Агрегация из чекпойнтов | `store.RoleResponsesForBook` + `Runner.bankObservedForBook` | | 6 | `kwic_width` в пар-данные | `lang.TerminologySizing` + необязательный `terminology.txt`; порядок резолвинга в `terminologyOpts` | | 7 | Хвост доки про жанровый словарь | `config.TerminologyGate` | | 8 | Строка примера формата | `prompts/zh-ru/terminologist.md` — ровно три строки, которые мерил полигон | **Объявление письма цели:** `gates.terminology.target_script` (имя скрипта Unicode). Обязателен при `enabled: true` — гейт, который не может отработать, отвергается на ЗАГРУЗКЕ, той же идиомой, что бюджет и путь промпта. Резолвится через `unicode.Scripts`, поэтому набор допустимых значений — Unicode'овский, а не список языков, который движку пришлось бы вести. **Свёртка банкноты переписана в один накопитель.** Раньше это были две функции над строками телеметрии; теперь `bankFold` — единственное место, где предложение становится уликой, и потому единственное место, где применяются все три дисциплины. Побочно удалена `bankProposalsByKey` — она дублировала `proposalsFromObserved` и оставалась только ради теста. **Голоса считаются по ЧАНКАМ, а не по строкам хранилища.** Это то, что делает объединение безопасным: чанк, перекупленный дважды, голосует один раз, поэтому union не раздувает частотный фактор ранжирования. --- ## §2 Приёмка ### §2.1 Мутации | Мутация | Что сделано | Результат | |---|---|---| | **M-Я1** | экран снят из `ParseReply` целиком | **КРАСНАЯ**: `TestParseReplyRefusesForeignLanguageRenderings`, `TestOffLanguageSamplesAreBounded` | | **M-Я2** | предикат ослаблен порогом «строка короче 3 рун — не проверять» | **КРАСНАЯ**: обе плюс `TestOffLanguageSeparatesForeignRenderingsFromTargetOnes` (ловится на `is`) | Оба раза файл восстановлен из копии, сьюта после восстановления зелёная. ### §2.2 Прогон предиката по ЖИВОМУ сырью пакета-8 126 сохранённых вызовов замера 3 (сетка + арм без якоря), **827 принятых строк ответа**. Разбор — боевым `ParseReply` без объявленного письма (чтобы увидеть все принятые строки), затем предикат. | Арм | Помечено | из них латиница | из них ханьцзы | |---|---|---|---| | 0×0 · 3×40 · 3×120 · 5×80 · 8×40 | **0** | 0 | 0 | | 8×120 | 7 | **5** | 2 | | без якоря | 22 | **22** | 0 | | **Итого** | **29** | **27** | **2** | **Критерий «ровно 27 строк §3.4 и ни одной сверх» выполнен на своих условиях: 27 латинских — те самые 27** (5 в 8×120 + 22 в арме без якоря, побайтно совпадает с таблицей полигона), и ни одного срабатывания в четырёх армах, где отчёт даёт 0/40. **Расхождение 29 против 27 объясняю, а не сглаживаю.** Полигон считал «латиница без единой кириллицы»; предикат считает «есть буквы, и ни одна не в письме цели». Ханьцзы — тоже буквы, поэтому предикат видит два ответа, которых метрика полигона не могла увидеть по построению: ``` 四代族长 → 第四代族长 家老 → 长老 ``` Модель ответила китайским пересказом вместо русского перевода. Эхо-гейт их пропускает (ответ не равен исходнику побайтно), `wellFormedLemma` пропускает. Это не ложные срабатывания — это два настоящих улова в классе, к которому исходный замер был слеп. Ошибок в другую сторону нет: 798 корректных строк не задеты. **Независимые подтверждения на том же сырье:** - **арм со строкой примера (замер D39.51):** 9 вызовов, 120 принятых строк, **0 помечено** — боевой код воспроизводит объявленные полигоном 0/120; - **проба хвоста (замер 2):** 21 вызов, 312 принятых строк, **18 помечено**, все латиница — включая названный в отчёте `是为 → is` и пиньинь-передачи `方源 → Fangyuan`, `方正 → Fangzheng`, которые для zh→ru переводом на целевой язык не являются. ### §2.3 Регресс, общность, инварианты | Проверка | Итог | |---|---| | 16 живых целевых строк (подписанные владельцем + пограничные: 2 буквы, только цифры, в кавычках, двойной пробел) | зелёные, ни одного ложного срабатывания | | Смешанная строка `Фан Юань (Fang Yuan)` | проходит — экран судит язык, а не написание | | **Пин общности** | при латинской цели вердикты переворачиваются; при ханьской работают обе; `ScriptByName` строгий, опечатка падает на загрузке, а не выключает экран молча | | Пин отсутствия объявления | без письма экран инертен — состояние, которое конфиг отвергает для включённого гейта, зафиксировано тестом, а не оставлено сюрпризом | | **Инвариант агрегации** | `TestBankUnionRecoversASupersededSampling`: второй ответ на тот же чанк пишется через обычный денежный путь, объединение достаёт затёртую выборку, счётчик чанков остаётся 1 | | Байт-стабильность карты при резюме | держит существующий `TestAutoWireIsDeterministicAndDoesNotMoveTheDraftWave` (авто-банк побайтно равен, черновая волна не двигается, резюм $0) | | Детерминизм свёртки | 8 прогонов подряд — порядок ключей и рендерингов идентичен | | Кавычки | `《咏梅》` и `咏梅` сходятся в один кандидат с двумя вариантами; односторонний огрызок не «чинится» молча; пустая пара не срезается в ничто | | `NormVersion` не сдвинут | пин-страж: нормализатор кавычки НЕ снимает, версия артефакта прежняя | | Пар-данные | пак zh-ru без файла хешируется как раньше; файл добавили — версия сдвинулась; пустой/битый файл падает на загрузке | | Конфиг | пропущенное/неизвестное/не в том регистре имя письма — громкая ошибка загрузки; `Hiragana` объявляется так же, как `Cyrillic` | ### §2.4 Сьюта `go test ./...` зелёная · `-race` зелёная на pipeline (67 c), terminology, lang, text, store, config · `TestGoldenDeterminism` PASS · `go vet` чисто · `gofmt -l` — только предсуществующий `internal/llm/llm.go`. Диффстат: 20 файлов, +454 / −100. --- ## §3 Оси | Изменение | Снапшот | Кто перекупается | |---|---|---| | Языковой экран, счётчик, фильтр свёртки, кавычки, агрегация | не двигается | никто напрямую. У книги, где в банке лежала иноязычная строка или пара кавычечных дублей, содержимое банка изменится → обогащённая версия сдвинется → редакторская волна оплатится ОДИН раз. Книга без таких строк — байт-в-байт прежняя | | `target_script` в конфиге | не двигается (гейт не фолдится) | никто | | `kwic_width` в пар-данные | не двигается: файл для zh-ru не шипуется, `Version()` побайтно прежний (проверено тестом) | никто | | Строка примера в промпте | волны не двигаются | только батчи терминолога (их адрес включает байты запроса) | | Агрегация | — | у книги с ОДНОЙ покупкой черновиков поведение прежнее; у книги, где черновики покупались повторно, карта подписи вырастет один раз | Прод после стройки не дорожает ни на цент: всё детерминированное и $0. --- ### §2.5 Синк с полигоном: промпт побайтно тот, что мерили Строка примера взята не по описанию, а сверена с артефактом. Скопированный полигоном промпт лежит в его скретчпаде (`d51/prompt/terminologist-example.md`); диффом от «Ты —» до конца файла **тело промпта, который дал 0 из 120, и тело промпта в репозитории совпадают побайтно** (различается только шапка- комментарий, которую модель не видит). Табуляция в строке примера — настоящая, проверено `cat -A`. Прочие пункты отчётов полигона сверены и разведены по местам: | Находка полигона | Где в стройке | |---|---| | Срыв языка без якоря (22/120) | п.1–3, приёмка §2.2 | | `是为 → is` в пробе хвоста | ловится, §2.2 | | Кавычки `《咏梅》` против `咏梅` | п.4 | | Невоспроизводимость канала между прогонами | п.5 | | `kwic_width` в рунах — утечка пары | п.6 | | P1 consistency не разделяет | НЕ строил ничего: права на порядок подачи метрика не заработала | | Утечка алфавита («категория A» с латинской буквой) | НЕ строил: отнесено в пак-21. Экран её не ловит по построению (кириллица в строке есть) — и стройка этого не меняет ни в одну сторону. Замечу только, что арм с примером дал таких строк больше (7 против 3), так что в холодном прогоне их стоит посчитать | | `allow_short`, холодный старт, второй детектор | не эта стройка | --- ## §3.1 Селф-ревью по диффу (после стройки, до отчёта) Прошёл диффом по всем файлам; три вещи нашёл и починил, одну назвал. 1. **Висячая ссылка в доке.** Комментарий `bankObservedByKey` ссылался на `bankProposalsByKey` — функцию, которую я в этой же стройке удалил. Исправлено на реального потребителя. 2. **Объединение читало ВСЮ историю ответов книги в память.** `RoleResponsesForBook` тянула каждый сохранённый черновик; на десяти главах это мегабайты, на тысяче глав с повторами — сотни. Добавил необязательный фильтр объёма по подстроке: маркер канала передаётся ПАРАМЕТРОМ запроса, поэтому в SQL не появилось второй копии константы, а читаются только те ответы, которые в принципе могут что-то дать. Семантику это не трогает — разбор по-прежнему один, в Go. 3. **Лишний параметр.** `loadTargetScript` принимал логгер, хотя `r.Log` к этому моменту уже установлен. 4. **Назвал, менять не стал:** комментарий у `packAlgoVersion` требует бампать тег «на новый файл», а я тег не двигал. Расписал правило точнее прямо в коде: бампается тег, когда меняется ЧТЕНИЕ уже существующих байт; необязательный файл, которого нет ни у одной пары, ничего не читает иначе и потому версию двигать не должен — иначе перепокупаются обе волны каждой книги ради механизма, которым никто не пользуется. Добавлен пин на сам тег, чтобы будущий бамп был решением, а не побочным эффектом. **Изменение поведения, которое надо назвать явно.** Голоса теперь считаются по чанкам, поэтому повторённая В ОДНОМ блоке строка `src→dst` даёт один голос, а не два, как раньше. Это исправление (один чанк — одно свидетельство, а не «модель дважды написала одно и то же»), но на уже накопленных данных оно может слегка сдвинуть ранжирование вариантов §C2-3. Осей это не двигает: ранжирование — вход роли и таблицы стопа, не байты запроса волн. --- ## §4 Отклонения и решения, принятые по ходу 1. **Кавычки нормализуются ТОЛЬКО в свёртке, запись сырой строки не тронута.** В дизайне допускались оба места. Одно место — одно определение; сохранённая строка остаётся записью того, что модель написала на самом деле, а сведение ключей — свойство join'а, а не наблюдения. 2. **Запрос чекпойнтов не фильтрует производные $0-строки в SQL.** Они несут не-провайдерский `finish_reason`, и обычное правило «доверяем только завершённой генерации» отсекает их само. Второй копии этого правила в SQL быть не должно. 3. **Предложения отвергнутых попыток входят в объединение** — как и объявлено в дизайне: черновик, забракованный за дефект ТЕКСТА, не делает объявленные в нём термины неправдой, а подписи всё равно нет. 4. **Тестовые фикстуры терминолога теперь объявляют письмо.** Это не «правка под тест»: гейт без объявления перестал грузиться — ровно то поведение, которое строилось. --- ## §5 Найдено при стройке, НЕ починено (нужно решение) **Канал банкноты мёртв для любого не-CJK исходника.** `parseBanknote` отвергает строку, в исходной части которой нет ханьского иероглифа (`hasBankSrcHan`, диапазон U+3400–U+9FFF). Проверено исполнением: ``` исходник не-CJK: Silversaint→Серебряный святой, San Michon→Сан-Мишон → принято 0, parse_fail=true исходник CJK: 方源→Фан Юань → принято 1, parse_fail=false ``` То есть на английской или любой другой не-иероглифической паре канал, который несёт 5/6 кандидатов банка, не просто молчит — он отвергает каждую строку и поднимает флаг ошибки разбора. Это прямая утечка конкретной языковой семьи в общий слой, и она из существующего кода, не из этой стройки. Чинить в этом паке не стал: правило живёт в ПАРСЕРЕ, чья версия фолдится в снапшот, поэтому правка = `--resnapshot` и перепокупка черновой волны каждой книги с включённым каналом. Это отдельное решение об оси. Содержательно правильное правило, по-моему, не «в исходнике есть иероглиф», а «исходная строка непуста и встречается в тексте книги» — проверка, которая пар-слепа по построению и вдобавок отсекает выдуманные строки, чего нынешняя не делает. --- ## §6 Что на владельце 1. **Лендинг стройки** (оркестратора) — пп.1–8 готовы, приёмка §2 исполнена. 2. **Решение по §5** — чинить ли пар-зависимость парсера банкноты и когда: правка дешёвая, ось дорогая (перепокупка черновой волны). Естественный момент — холодный мини-прогон, где книга покупается заново. 3. Ранее отложенное и не изменившееся: судья-с-декоем ждёт холодного старта; `allow_short` для `蛊`/`转` — данными на холодном прогоне; пере-замер P1 — оттуда же, $0 из чекпойнтов. **СТОП.** Сессия не коммитила.