textmachine/docs/archive/reports/PACK20_BANKNOTE_BUILD_2026-07-26.md

24 KiB
Raw Permalink Blame History

Пак-20, продолжение: СТРОЙКА дисциплины банкноты и языкового экрана

База: D39.51 (дизайн ратифицирован, пп.17 санкционированы) + 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 по канонической форме — файлы без комментариев байт-в-байт прежние; шапка ужимается до 12 строк; тест-пин «комментарий не в проводе»). Приёмка D39.53 этот клейм не проверила — записано как промах.


§1 Что построено

Пункт Где
1 Языковой экран на выходе роли terminology/script.go (OffLanguage, ScriptByName) + отказ строки в ParseReply там же, где эхо-гейт
2 Счётчик OffLanguage + громкий лог батча terminology.ReplyStats, terminologyResult.OffLanguage, warn в runTerminologist
3 Языковой фильтр черновой стороны pipeline/banknote.gobankFold, счётчик в стоп-логе
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) п.13, приёмка §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+3400U+9FFF). Проверено исполнением:

исходник не-CJK:  Silversaint→Серебряный святой, San Michon→Сан-Мишон  → принято 0, parse_fail=true
исходник CJK:     方源→Фан Юань                                        → принято 1, parse_fail=false

То есть на английской или любой другой не-иероглифической паре канал, который несёт 5/6 кандидатов банка, не просто молчит — он отвергает каждую строку и поднимает флаг ошибки разбора. Это прямая утечка конкретной языковой семьи в общий слой, и она из существующего кода, не из этой стройки.

Чинить в этом паке не стал: правило живёт в ПАРСЕРЕ, чья версия фолдится в снапшот, поэтому правка = --resnapshot и перепокупка черновой волны каждой книги с включённым каналом. Это отдельное решение об оси. Содержательно правильное правило, по-моему, не «в исходнике есть иероглиф», а «исходная строка непуста и встречается в тексте книги» — проверка, которая пар-слепа по построению и вдобавок отсекает выдуманные строки, чего нынешняя не делает.


§6 Что на владельце

  1. Лендинг стройки (оркестратора) — пп.18 готовы, приёмка §2 исполнена.
  2. Решение по §5 — чинить ли пар-зависимость парсера банкноты и когда: правка дешёвая, ось дорогая (перепокупка черновой волны). Естественный момент — холодный мини-прогон, где книга покупается заново.
  3. Ранее отложенное и не изменившееся: судья-с-декоем ждёт холодного старта; allow_short для / — данными на холодном прогоне; пере-замер P1 — оттуда же, $0 из чекпойнтов.

СТОП. Сессия не коммитила.