24 KiB
Пак-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 Селф-ревью по диффу (после стройки, до отчёта)
Прошёл диффом по всем файлам; три вещи нашёл и починил, одну назвал.
- Висячая ссылка в доке. Комментарий
bankObservedByKeyссылался наbankProposalsByKey— функцию, которую я в этой же стройке удалил. Исправлено на реального потребителя. - Объединение читало ВСЮ историю ответов книги в память.
RoleResponsesForBookтянула каждый сохранённый черновик; на десяти главах это мегабайты, на тысяче глав с повторами — сотни. Добавил необязательный фильтр объёма по подстроке: маркер канала передаётся ПАРАМЕТРОМ запроса, поэтому в SQL не появилось второй копии константы, а читаются только те ответы, которые в принципе могут что-то дать. Семантику это не трогает — разбор по-прежнему один, в Go. - Лишний параметр.
loadTargetScriptпринимал логгер, хотяr.Logк этому моменту уже установлен. - Назвал, менять не стал: комментарий у
packAlgoVersionтребует бампать тег «на новый файл», а я тег не двигал. Расписал правило точнее прямо в коде: бампается тег, когда меняется ЧТЕНИЕ уже существующих байт; необязательный файл, которого нет ни у одной пары, ничего не читает иначе и потому версию двигать не должен — иначе перепокупаются обе волны каждой книги ради механизма, которым никто не пользуется. Добавлен пин на сам тег, чтобы будущий бамп был решением, а не побочным эффектом.
Изменение поведения, которое надо назвать явно. Голоса теперь считаются по чанкам, поэтому
повторённая В ОДНОМ блоке строка src→dst даёт один голос, а не два, как раньше. Это исправление (один
чанк — одно свидетельство, а не «модель дважды написала одно и то же»), но на уже накопленных данных оно
может слегка сдвинуть ранжирование вариантов §C2-3. Осей это не двигает: ранжирование — вход роли и
таблицы стопа, не байты запроса волн.
§4 Отклонения и решения, принятые по ходу
- Кавычки нормализуются ТОЛЬКО в свёртке, запись сырой строки не тронута. В дизайне допускались оба места. Одно место — одно определение; сохранённая строка остаётся записью того, что модель написала на самом деле, а сведение ключей — свойство join'а, а не наблюдения.
- Запрос чекпойнтов не фильтрует производные $0-строки в SQL. Они несут не-провайдерский
finish_reason, и обычное правило «доверяем только завершённой генерации» отсекает их само. Второй копии этого правила в SQL быть не должно. - Предложения отвергнутых попыток входят в объединение — как и объявлено в дизайне: черновик, забракованный за дефект ТЕКСТА, не делает объявленные в нём термины неправдой, а подписи всё равно нет.
- Тестовые фикстуры терминолога теперь объявляют письмо. Это не «правка под тест»: гейт без объявления перестал грузиться — ровно то поведение, которое строилось.
§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–8 готовы, приёмка §2 исполнена.
- Решение по §5 — чинить ли пар-зависимость парсера банкноты и когда: правка дешёвая, ось дорогая (перепокупка черновой волны). Естественный момент — холодный мини-прогон, где книга покупается заново.
- Ранее отложенное и не изменившееся: судья-с-декоем ждёт холодного старта;
allow_shortдля蛊/转— данными на холодном прогоне; пере-замер P1 — оттуда же, $0 из чекпойнтов.
СТОП. Сессия не коммитила.