textmachine/docs/archive/reports/EFFORT_HANDLE_2026-08-02.md

63 KiB
Raw Blame History

Отчёт бэкенд-сессии: ручка эффорта · экран целевого языка · холодный прогон (строки 104 · 46 · 16)

ПРИНЯТ приёмкой оркестратора №11 (02.08, D39.91). Код заленден коммитом 553f1a3 (13 файлов backend/, pathspec-форма). Приёмка исполнением: батарея пере-прогнана — все пять фрозен-чисел совпали (голден PASS без пере-захвата · парити EXACT n=13618 {方源:0 蛊:1 蛊师:2 古月:22} recall 0.9655 · labels-фриз · K6 1/6/0/251); деньги пере-выведены вторым путём — леджер $0.045095 ≡ независимый пересчёт usage×цены ≡ Σ request_log, дельта $0.000000, decode_error/estimated 0; адверсариал — рукописных config.Stage{}/Request{} вне швов НОЛЬ (grep), пять НЕЗАВИСИМЫХ посадок приёмки (алиас-литерал · []cfg.Stage{{…}} · hoisted Request · var-форма · new()) все CAUGHT; парная выборка N=5 сверена с сырьём classifier-6of6-{low,high}.json побайтно. Обе девиации РАТИФИЦИРОВАНЫ (repair-ключ свой — деньги+семантика подтверждены по пяти шиппинг-конфигам; ThinksOnWire — область правки ровно «ключ отсутствует», провод не менялся). Поправки приёмки: (1) §2.1 «$0.031639 (flash)» — на деле flash+pro черновой волны (flash $0.021289 + pro $0.010350; итоги и леджер верны, врёт метка; так же скопировано в D39.90 п.2); (2) клейм «8e4495006b978ed4 в обе стороны» невоспроизводим (старая форма удалена, хеш не запинен) — принят ПО ЭКВИВАЛЕНТНОСТИ (длина-префикс Reasoning + опущенное поле = "" + TestBankProbeAndAttemptAddressOneCheckpoint + голден); (3) стейл-доккоммент capability.go:78 («effort "" leaves the provider default» у ReasoningExtraBodyDisable) противоречит проводу и новому предикату — носитель: строка 114. Отложки Д4Д9 получили носителей: 114 (Д4) · 115 (Д6) · 116 (Д8) · реестр 108 (Д5, Д9) · строка 107 (Д7).

Промт: docs/BACKEND_EFFORT_HANDLE_SESSION_PROMPT.md (оркестратор №10, D39.87). Дата: 02.08.2026. Не коммичено — лендит оркестратор.

ИСХОД ЗАХОДА: работа A ЗАКРЫТА; §3.4 (платная проба банка) ЗЕЛЁНАЯ; работа B — дизайн подан, кода нет, стоит на решении владельца; работа C НЕ НАЧАТА и закрыта владельцем как неуспешная на этом этапе, возвращена оркестратору. Деньги: $0.045095 из $0.10 (проба §3.4), прогон C — $0.


§0. Эхо-шапка

  1. Скоуп: A — ручка эффорта ролям без неё; B — экран целевого языка; C — добивка холодного прогона.
  2. A: один шов деривации синтетической стадии на все 4 места + обе расщеплённые хеш-пары.
  3. §3.4: своя проба reprobe/bank-low/, потолок $0.10, вердикт «низкий эффорт не ломает банк» — только предъявив, что ни один из двух молчащих путей не сработал.
  4. B: вердикт-двигающее ⇒ дизайн и пинг ДО кода.
  5. C: только после заморозки A и B.
  6. Деньги: A $0 · §3.4 ≤$0.10 · B $0 · C ≤$5 (не потрачено).
  7. Вне скоупа: сид · судья · DC7 · models.yaml capabilities/price · thinking-off · optNoThink · чужие файлы · коммиты.

§1. Работа A — ручка эффорта (строка 104). ЗАКРЫТА

1.1. Форма шва

Корень дефекта — не «забыли ключ», а форма кода. Четыре рукописных config.Stage{...} по движку, каждый молча терял поля родителя. Починка четырёх мест копипастой тиражирует дефект на пятое.

Построено два шва:

Шов Файл Что закрывает
config.InternalCall{...}.Stage() internal/config/internal_call.go (новый) единственная деривация стадии для внутренних вызовов движка
Runner.attemptRequest(...) internal/pipeline/stagerun.go:406 единственная сборка кортежа идентичности запроса

Рукописных config.Stage{} в движке не осталось ни одного. Все четыре места (terminologist.go:515, :568, repair.go:384, live_reprobe_test.go:98) едут через шов. Все четыре сборки RequestHash(Request{...}) схлопнулись в одну.

1.2. Деньги правки: перекупки НЕТ. Проверено хешами

Ключевой вопрос любого шва, трогающего идентичность запроса: не осиротели ли уже оплаченные чекпойнты.

  • gates.terminology НЕ фолдится в снапшотsnapshot.go ссылки на Terminology нет — это осознанное решение, см. доккоммент config.TerminologyGate). ⇒ добавление ключа reasoning не двигает snapshotID и не пере-биллит ни одной волны.
  • Кортеж идентичности для всех предсуществующих путей воспроизведён побайтно. RequestHash длина-префиксует req.Reasoning, а опущенное поле в композитном литерале И ЕСТЬ "" ⇒ старая и новая формы совпадают по построению, а не по везению. Бэнк-пробник с незаданным ключом даёт 8e4495006b978ed4 в обе стороны; эскалационный хоп — равенство; draft/edit — тот же список полей, перенесённый дословно.
  • Голден НЕ пере-захватывался и не должен был: classifierVersion не бампнут (работа B не начата), CheapGateVersion/SanitizerVersion не тронуты. TestGoldenDeterminism PASS на исходных фикстурах — это и есть доказательство, что вердикты и провод не сдвинулись.
  • Единственное, что двигало бы хеш, — наследование эффорта у repair, и оно снято (§1.6).

1.3. Ответы на четыре приёмочных вопроса §0 промта

(1) Новый механизм или использование существующего? Существующего. Stage.Reasoning было с самого начала, проброс на провод был, капабилити-слой различает провайдеров данными. Добавлен ОДИН конфиг-ключ gates.terminology.reasoning и ОДИН gates.repair.reasoning; новых механизмов нет.

(2) Появится ПЯТАЯ синтетическая стадия — унаследует сама или забудут? Унаследует, и это механика, а не обещание — три гарда:

Гард Что физически не даёт сделать Проверено исполнением
TestSyntheticStageSeamIsSingle построить config.Stage мимо шва 6 посадок, все CAUGHT
TestRequestIdentitySeamIsSingle пересобрать кортеж идентичности руками посадка CAUGHT
TestInternalCallDecidesEveryStageField добавить в Stage поле, не решив, что с ним делают внутренние вызовы добавил поле → FAIL с именем поля

Первая версия гардов была дырявой, и это найдено ревью, а не мной. Байтовый скан пробивался пятью способами; после переписи на AST — ещё двумя (алиас импорта cfg "…/internal/config" и []config.Stage{{…}} с опущенным типом элемента). Обе дыры я перепроверил на живом дереве — гард говорил ok. Причина: сравнивал имя пакета вместо пути импорта. Итоговая версия резолвит путь, разворачивает []T/map[K]T/*T, исключения полными путями (а не именами файлов, которые расползались на одноимённые файлы по всему репо), и несёт пол на число разобранных файлов — обход, не распарсивший ничего, выглядел как чистое дерево.

(3) Заработает ли пара, которой нет в репо, без правки Go? Да. Ключ пар-слепой, валидируется общим ValidReasoningEffort, Go не ветвится ни по паре, ни по книге. Вторая книга той же пары — без новых флагов (ключ ран-скоупный, в book.yaml и пар-пак не заходит).

(4) Не появилось ли второго способа сказать то же самое? Проверено и вычищено в трёх местах: enum эффорта — один валидатор на стадии и оба гейта; bankRolePlan.model снят (был вторым источником правды, читался только логом); идентичность запроса — одна сборка. Остаточный долг признаю: в репо теперь два гарда РАЗНОЙ формы — мой на AST и давний TestProviderEgressSeamIsSingle на байтовом регэкспе, с другим корнем обхода и своим словарём исключений. Свести в один обход — правильно, но это правка чужого давно живущего теста; вынесено предложением (§6, Д7).

1.4. Гранулярность ручки — решено ЗАМЕРОМ, не заранее

Приор был «одна ручка на бэнк-блок; вторая появится по замеру». Замер парный, N=5 на уровень, через боевой путь:

Уровень 6/6 достигнуто completion-токены цена 5 вызовов
low 4 из 5 911278 $0.001016
high 5 из 5 9203104 $0.002335

4/5 против 5/5 на n=5 неразличимо, а high стоит 2.3×. ⚠ Самопоправка: первый вызов дал 3/6, я на этом основании построил вторую ручку classify_reasoning — и снял её, когда выборка не подтвердила разницу. Единственный промах — вызов на 91 токене, то есть модель просто не подумала; уровень тут ни при чём.

Итог: одна ручка на обе бэнк-роли. Классификатор сохраняет собственную МОДЕЛЬ (classify_model), но не собственный эффорт.

Честная граница самих чисел 6/6 (унаследована от D39.86, повторяю, чтобы не читалась сильнее, чем есть): 元石 и 灵泉 стоят ДОСЛОВНО в prompts/zh-ru/classifier.md, то есть два из шести — припоминание, а не суждение. Дискриминирующее подмножество — 元海/蛊室/池塘/酒肆, и падение на low было ровно по нему (1/4 против 4/4). Читать как «4 решено + 2 припомнено».

Точки шва в коде после правок: attemptRequeststagerun.go:406; bankRoleSettingsterminologist.go:534; bankStageterminologist.go:553; repairStagerepair.go:363. (Номера terminologist.go:515,568, repair.go:384, live_reprobe_test.go:98 в тексте — ИСХОДНЫЕ места литералов, как их называет D39.87.)

1.5. Temperature — зафиксирована нулём осознанно

Ответ бэнк-роли и repair — разбираемая движком СТРУКТУРА (таблица терминов, тип, спан-замена), где разброс сэмплинга — чистый риск; температура стадии это СТИЛЕВОЙ выбор про прозу и здесь бессмысленна. Ноль — то же значение, при котором куплены все существующие бэнк-чекпойнты, так что решение ещё и не двигает хеш. Причина первая, совпадение второе.

1.6. Repair — ключ СВОЙ, наследование СНЯТО

Первая версия наследовала эффорт от чинимой стадии (так предлагал промт и D39.87). Ревью это опровергло, я перепроверил и согласился:

  • Деньги: reasoning: "off" стоит на финальной стадии у всех шиппинг-пайплайнов (pipeline-c1.yaml:77, c2:51, arm-glm:43, arm-deepseek-pro:43, стенд coldrun-b:44). Наследование двигало хеш ⇒ перекупка всех уже оплаченных repair-вызовов.
  • Семантика, и это хуже: reasoning — не уровень, а значение, чьё ЗНАЧЕНИЕ зависит от control модели. Финальная стадия deepseek-v4-pro с reasoning: "off" — задокументированный no-op; gates.repair.model: glm-5 — и тот же «off» становится живым thinking:{type:disabled}, то есть эхо-зоной. Родительская стадия и repair — РАЗНЫЕ модели.

Решение: gates.repair.reasoning — собственный ключ. Не задан ⇒ "" ⇒ побайтно прежнее поведение.

1.7. Мульти-провайдерность — и найденная при этом дыра

Промт требовал закрыть режим extra_body_disable (glm), где ПУСТОЙ эффорт означает не «дефолт провайдера», а thinking ВЫКЛЮЧЕН. Расширил sourceEchoExposure на внутренние вызовы движка — и ревью показало, что этого мало: предикат ThinksOnWire для этого control возвращал reasoning != "off", то есть на ПУСТОМ значении отвечал «думает», расходясь с проводом, который в этом же случае мержит disable.

Проверено исполнением:

applyToBody effort=""  -> wire={"max_tokens":…, "thinking":{"type":"disabled"}}
ThinksOnWire(glm-подобная, "")  = true   <-- врал

Починено, запинено TestThinksOnWireMirrorsTheWireForEachControl (8 кейсов на три control). Проверил, что на старом коде тест падает.

⚠ Это была предсуществующая дыра (она так же молчала про СТАДИИ на glm без ключа reasoning), но мой пак первым на этот предикат оперся, поэтому чиню здесь.

1.8. Наш enum против вендорского — решение и явный отказ

ValidReasoningEffort принимает "" | off | low | medium | high — НАШ провайдер-слепой набор. У DeepSeek документированы low/high/max (+xhigh): medium там неопределённое поведение, max/xhigh нашим конфигом недостижимы. Валидацию против капабилити резолвнутой модели НЕ строил — это потребовало бы пер-модельной таблицы enum'ов в models.yaml, которой нет, то есть НОВЫЙ механизм (нарушение §0 п.1). Дыра предсуществующая и одинаковая для stages[].reasoning. Записано находкой (§6, Д5), не закрыто молча.

1.9. Доккомменты эхо-гейта — обязанность D39.86 п.2 / D39.87

Приведены в соответствие config/models.go (thinkingControlExtraKeys) и llm/capability.go (ReasoningNone). Ключевая формулировка: запрещён не ПРЕДМЕТ, а сырой канал — уровень эффорта через stages[].reasoning/gates.*.reasoning легален и идёт через Capability.applyToBody, который знает, что «off» значит для этого control; model.extra_body мержится дословно и потому может ВЫКЛЮЧИТЬ thinking, что и есть мина. Гейт не ослаблен.


§2. §3.4 — платная проба банка на low. ЗЕЛЁНАЯ

Хост: ~/books/gu-zhenren/coldrun-b/reprobe/bank-low/ — свой book_id, своя БД, свой леджер. Эталон coldrun-a только на ЧТЕНИЕ (срез глав 110), не тронут.

2.1. Числа

терминолог     4/5 батчей finish=stop · consolidated=72 · unanswered=18
               bad_lines=0 · off_language=0 · canon_conflicts=0 · $0.005750
классификатор  5/5 stop · reclassified=14 · $0.004286
черновая волна 20/20 чанков ok · 25 свежих вызовов · 2 эскалации вылечили эхо
               $0.031639 (flash) + $0.010350 (pro)

Против вендор-дефолта high (замер D39.86): 1 батч из 5 и 1 терм из 81.

2.2. Оба молчащих пути ИСКЛЮЧЕНЫ предъявлением

Промт требовал: вердикт «работает/не работает» имеет право быть вынесен только предъявив, что ни ролевой суб-бюджет, ни книжный потолок не сработали.

потрачено бюджет %
терминолог $0.005750 $0.05 11.5%
классификатор $0.004286 $0.02 21.4%
книга $0.045095 $0.10 45.1%

И в логе ноль строк budget would be exceeded by the next batch / denied by a USD ceiling.

2.3. Провод вскрыт

35 тел (25 translator + 5 terminologist + 5 classifier), ключи ровно [max_tokens, messages, model, reasoning_effort, stream, temperature], reasoning_effort:"low" во всех 35, ключей thinking0, reasoning_content непустой везде. Гардрейл «thinking у DeepSeek не выключать» цел и доказан проводом, а не рассуждением.

Упавший батч вскрыт сырьём (требование промта — прошлая сессия этого не сделала): content:"", finish=length, completion_tokens=7999, reasoning_tokens=7999. То есть тот же тяжёлый хвост, что при high, просто на 1/5 вместо 4/5low стену не убирает, а разрежает. Батч #2 не самый большой по входу (5909 симв. против 5890/5806), то есть стохастика вызова, согласуется с квирками п.6.

2.4. Деньги двумя путями

леджер (Σ чекпойнтов)                  $0.045095
независимый пересчёт usage×вендор-цены  $0.045095
дельта                                  $0.000000

разбивка:  основной заход  $0.041675
           риг 6/6 (11 вызовов: 1 + 5 на low + 5 на high)  $0.003420

Расхождения строки 78 (2xx с битым декодом) в этом заходе НЕ было — 0 строк decode_error.

2.5. Правки стенда, объявленные построчно

~/books/gu-zhenren/coldrun-b/reprobe/bank-low/pipeline.yaml — тело ран-конфига coldrun-b с ТРЕМЯ отличиями:

  1. stages[draft].reasoning: "low" вместо no-op "off" — иначе волна упирается в стену и до банка проба не доезжает.
  2. gates.terminology.reasoning: "low" — НОВАЯ ручка, предмет замера.
  3. escalation.budget_usd: 0.015 вместо 0.10понижен сознательно: эскалации идут ДО банка, при $0.10 они могли съесть книжный потолок раньше первого вызова терминолога, и проба ответила бы «банк не работает» на деньгах. ⚠ Это делает наблюдение эскалаций НЕПОЛНЫМ; предмет пробы не они.

Боевой coldrun-b/pipeline.yaml не тронут (там по-прежнему reasoning: "off" на черновике — правка под работу C, которая не стартовала).


§3. Работа B — экран целевого языка (строка 46). ДИЗАЙН ПОДАН, КОДА НЕТ

Промт: «вердикт-двигающее ⇒ сначала дизайн и пинг, потом код». Пинг подан владельцу, решения нет ⇒ кода нет. Это легитимный исход по букве промта.

3.1. Живая улика, добытая в этом заходе

Глава 8, чанк 1 пробы bank-low: полный связный черновик на английском, finish=stop, disposition=ok, и его хеш стоит в final_hash чанка. Хеш fb51e135a395, 5990 симв., 4690 букв, латиница 100%, кириллица 0.000. Второй такой же в optB (4080ee8287af, 4811 букв). Плюс полный японский черновик в exp15/anchors/oracle_30.db (94905a7e443f).

3.2. ⚠ ПОПРАВКА К МОИМ ЖЕ ЧИСЛАМ

Я подал владельцу «здоровый русский 0.95101.000, n=1306». Это измерено на СЫРОМ тексте чекпойнта. Движок классифицирует ПОСЛЕ среза банкноты (stagerun.go:632 режет, :636 классифицирует; срез безусловен для роли translator, banknote.go:320), то есть гейт видит другой текст. Пере-мерил его же функциями (splitBanknote + checks.StripThink):

я подал на тексте, который видит гейт
здоровый минимум 0.9510 0.9767
n 1306 1055 (дедуп по хешу; 1306 двоил бэкап-копии БД)
off-target выходов 26 строк 20 событий

Решения это не меняет (оба числа далеко от любого порога), но число я называл — поправляю.

3.2а. Поправка к БАЗЕ отказа

Публиковалось «~1.5% off-target». Знаменатель неверен: он пулит черновики с РЕДАКТОРСКИМИ выходами, а редактор этот отказ произвести не может (он не переводит с исходника). На черновой вызов база ≈3.0%. Оба числа — одна книга, один жанр, по сути одна модель.

3.3. ⚠ ГЛАВНАЯ ПОПРАВКА: боевой гейт уже ловит 18 из 20

Ни ресерч, ни я не измеряли ДО этого, что делает уже развёрнутый гейт. Измерил:

sourceScriptShare > 0.15 (disposition.go:224,275)
  поймано:  18 из 20 off-target событий
  ложных:   0 из 1055 здоровых
  запас:    худший здоровый 0.0117 против порога 0.15 — 13×
  промах:   ровно 2, оба английские, 5990 и 6078 симв.

То есть движок не открыт настежь. Остаточная дыра — 2 чанка из ~1100 = 0.18%, оба английские. Японский ловится (доля ханьцзы 0.332, японский делит письменность с китайским).

И второй рубеж существует, но стоит не там. Прогнал боевой санитайзер по тем самым байтам английского черновика: total=1, cosmeticOnly=false, "Latin insertion" — он бы его поймал. Не ловит потому, что chunkrun.go:52 требует isFinal, а isFinal := stageIdx == len(Stages)-1 (stagerun.go:145) ⇒ в шиппинговой двухстадийной топологии черновик переводчика НИКОГДА не финальный. Тот же isFinal-гейт глушит на черновике латинный лint, флаггер голоса и глоссарный пост-чек. Плюс он опционален (gates.sanitizer.enabled) и требует данных цели.

⇒ Формулировка «ни один предикат не ловит» верна только для черновой стадии. На финале для русской цели с включённым санитайзером ущерб = потеря юнита + оплаченный впустую редактор, а не тихая отгрузка. Для любой другой цели слой 7 инертен по отсутствию данных и рубежа нет вовсе.

3.4. Дизайн, поданный на решение

Носитель: lang.LangScripts(Book.TargetLang) — тот же дата-план, на котором стоит эхо-детектор, тот же data/lang-script.txt, где ru→cyrillic уже объявлено. Новый файл в go:embed НЕ добавляется — это критично: EmbeddedVersion фолдится в снапшот БЕЗУСЛОВНО, то есть новый эмбед-файл перекупает все книги всех пар.

gates.terminology.target_script как носитель отвергнут, но по причине сильнее промтовой: он ДУБЛИРУЕТ то, что данные уже знают, и потому это уже существующий «второй способ сказать то же самое». Промт снимал его за «нет в шиппинг-конфигах» — на деле ключ ОБЯЗАТЕЛЕН при включённом гейте (pipeline.go:1075).

Порог 0.50 — не калиброван, а сознательно консервативен: пустой интервал между 0.0000 (все 20 событий) и 0.9767 (худший здоровый) шириной 0.9767, порог поставлен в его середину. Калибровать по Нейману–Пирсону на этом корпусе нельзя (§5.2).

Развилка, которую промт требовал НАЗВАТЬ, а не предрешать — подана владельцу трёхчастной:

Что Цена
(а) предикат внутри classify() + бамп classifierVersion снапшот двигается у всех книг; на свежем стенде $0, голден пере-захват один раз
(б) отдельный опциональный гейт вне classify() снапшот не двигает, но это второй способ сказать «ответ непригоден», и он выключен по умолчанию
(в) сразу языковой детектор см. §5

Мой выбор — (а).

3.5. Вторая половина дыры — решение

detectLatinInsertion (checks/sanitizer.go:343) внутри санитайзера НЕ обусловлен TargetScriptNonLatin(), хотя доккоммент disposition.go:302-305 объявляет именно эту защиту (реальный гард стоит только на репэйр-пути). Следствие: первая же ЛАТИНОПИСЬМЕННАЯ цель дропнет 100% чанков чистой прозы.

Решение: чинить здесь же, тем же диффом — это одна работа с первой половиной. Для русской цели вердикты не меняются ни на байт (TargetScriptNonLatin() для ru истинно), то есть бампа SanitizerVersion не требуется. Не сделано — работа B не начата.

3.6. Что бы сломалось, если бы я сделал B наивно

Ревью и аудит нашли три мины, в которые я бы въехал:

  1. Новый флаг провалился бы в дыру. escalatable() — закрытый белый список, retryable() — только {length, empty}. Новая причина не попадает ни туда, ни туда ⇒ пойманный чанк молча терялся бы вместо перекупки. Правильное решение — переиспользовать FlagCJKArtifact (он уже escalatable и не retryable), а не заводить новую константу.
  2. Классификатор ложно флагается 16 из 16 уже сегодня (SourceEchoExpected: role == roleTerminologist не покрывает roleClassifier). Мой экран флагнул бы его вторым способом. Экран обязан читать не роль-строку, а факт формы ответа, проставленный тем же швом работы A.
  3. Экран слеп к ПОДМНОЖЕСТВАМ письменности: у японского набор {ханьцзы, хирагана, катакана} ⇒ чисто китайский выход для →ja книги наберёт 1.000 и пройдёт. То же для →ko.

§4. Работа C — НЕ НАЧАТА

Причина по букве промта: «идёт только после того, как работы A и B ЗАКРЫТЫ И ЗАМОРОЖЕНЫ». Работа B застряла на дизайн-пинге — промт прямо называет это легитимным исходом захода. Дополнительно владелец закрыл прогон как неуспешный на данном этапе и вернул его оркестратору.

Стенд ~/books/gu-zhenren/coldrun-b/ стоит нетронутым: свежая БД, сид не подключён, pipeline.yaml побайтно равен эталону coldrun-a кроме шапки, черновик по-прежнему на reasoning: "off" (правка под C не внесена — вносить её без прогона значило бы оставить стенд в промежуточном состоянии).

Строка 16 (полная цена холодного старта включая редактуру) НЕ закрыта. Строка 13б (оси голоса) без данных — редакторская волна снова не гонялась.


§5. Ресерч-задел для следующих сессий

Два холодных исследования, каждое с независимыми скептиками (author≠reviewer). Полные тела — в транскриптах воркфлоу; ниже то, что должно пережить сессию.

5.1. Off-target детекция: закрыта ли задача

ЧАСТИЧНО, и нерешённая часть меньше, чем казалось.

Устоялось (не пере-открывать):

  • У отказа есть имя и метрика: off-target translation (Zhang et al., ACL 2020, arXiv 2004.11867), language confusion LPR/WPR (Marchisio et al., EMNLP 2024, arXiv 2406.20052). Рецепт у всех один и он у нас уже реализован: детектор на выходе → сравнение с запрошенной целью → mismatch = отказ. Более умного serving-time метода не упущено.
  • Английский — доминирующий аттрактор. Подтверждено дважды независимо (Cohere; Guerreiro et al., TACL 2023: >90% off-target галлюцинаций при переводе С английского). Совпадает с нашими 2/20 английских.
  • Четыре режима отказа — практика поля, не новизна: Guerreiro использует РАЗНЫЙ детектор на режим (ALTI+ для detached, top-n-gram для oscillatory, LID только для off-target). Гнать всё через один детектор — ошибка проектирования, и мнение из обсуждения тут право.
  • Abstain + пол по длине — опубликованная практика (Cohere compute_metrics.py: строка оценивается только при len(tokens) >= 5).
  • Reference-free QE эту задачу не делает, и поле пишет об этом прямо.

Открыто у всех:

  • Близкие кириллические соседи. OpenLID-v3 на реальном пользовательском тексте: боснийский 37.21% precision, сербский-латиница 30.85% recall; 19.9% ошибок — «полная неоднозначность». Трёх девяток тут не достигает никто.
  • FPR детектора на настоящей ЦЕЛЕВОЙ прозе длиной в чанк не опубликован нигде.
  • Дрейф в середине генерации: нет устоявшейся статистики, гранулярности и правила агрегации.
  • Ничего в этой литературе не про длинную художественную прозу. Caswell et al. 2020: >90% held-out F1 соответствовал ~5% точности по человеческой оценке «в дикой природе».

5.2. Почему обещанной гарантии FPR ≤ 0.1% здесь не купить

Правило трёх: ноль отказов на n независимых испытаний покупает 1 0.05^(1/n). Для ≤0.1% нужно n ≥ 2995. У нас 114 различных позиций (глава, чанк) — одна книга, один жанр, одна модель — это покупает FPR ≤ 2.594%, в 26 раз слабее обещанного. Даже если считать все 1055 выходов независимыми — ≤0.283%.

Плюс процедура двоит данные: «калибруй так, чтобы ложное отклонение ≤0.1%» ставит порог в эмпирическую 0.001-квантиль ТОЙ ЖЕ выборки, по которой потом заявляется граница. При n≈1000 порог определяется вторым наименьшим наблюдением.

И третье: Нейман–Пирсон ограничивает ошибку I рода и ничего не говорит про мощность. На сиблингах распределения нулевой и альтернативной гипотез по доле целевого письма СОВПАДАЮТ (болгарский даёт ровно 1.0000). Односторонний срез при α=0.001 даёт мощность ≈0.1%: гарантия есть, ловли нет.

Ставить руками консервативную константу в широком пустом интервале и версионировать её как контракт, а не как тюнинг.

5.3. Что НЕ строить (и почему) — самая ценная часть

  1. Никакую статистическую LID-библиотеку в путь вердикта. Проверено запуском, не по README: lingua-go — НЕдетерминистична (6/23 образцов на 20 000 повторов; GOMAXPROCS=1 не лечит — причина в обходе map при суммировании float64), +126 МиБ эмбеда безусловно, 1012 мс/чанк против бюджета 168 мкс, и вендор документирует вердикт-двигающие апгрейды. langid-go — на 644 реальных сегментах русской Википедии даёт P(ru)<0.5 на 22.2% сегментов длиной 2040 рун; плюс FMA-зависимость от GOARCH. whatlanggo — 46 из 9 настоящих русских образцов принимает за болгарский/македонский. getlang — argmax флипается. gocld3/fastText — CGo, вердикт становится функцией тулчейна, то есть уезжает ВНЕ снапшот-хеша.
  2. Посегментный worst-of (L4 из мнения). Умножает FPR на число сегментов ПО ПОСТРОЕНИЮ, и — решающее — не имеет ни одного положительного примера: самая длинная не-кириллическая цепочка в здоровом выходе 23 символа, между 0.0000 и 0.9767 не лежит ничего. На замеренных точках стоило бы 74290 дропнутых чанков на книгу ради нуля дополнительных поимок. Цена (269 мкс) никогда не была препятствием — препятствие в калибровке и пустом классе положительных.
  3. Замыкание алфавита (L2). Против болгарского доказуемо ноль (его алфавит — собственное подмножество русского), в остальных случаях избыточно с L1.
  4. Функционально-словный экран. Здоровый русский p01 = 0.2433 на выход и 0.0455 на абзац — зазор крошечный, а русский закрытый класс во многом и болгарский. На этой машине нет ни одного сиблинг-текста, чтобы померить.
  5. Эхо через расстояние редактирования / n-граммное сходство. sourceScriptShare > 0.15 уже даёт 18/20 при 0 FP и 13× запасе.
  6. Любой новый файл в go:embed-манифесте ради этой фичи. EmbeddedVersion фолдится БЕЗУСЛОВНО ⇒ перекупка всех книг всех пар.
  7. Hunspell в любом виде и GlotScript-эмбед (stdlib unicode уже несёт ISO-15924-совместимые таблицы, движок к ним уже ходит).

5.4. Пошаговый план для следующей сессии

Принцип порядка: единственный необратимый шаг — бамп classifierVersion, он последний. Шаги 13 независимы.

  1. Корпус-фикстура в репо (~20 off-target + ~40 здоровых, с движковой предобработкой). Снапшот НЕ двигает. СТОП, если не воспроизводит 18/20 при 0 FP.
  2. Записать реальный запас боевого гейта регресс-тестом (худший здоровый source-share 0.0117 против 0.15). Это база, которую обязано побить любое предложение, и её никто не записывал. СТОП, если запас меньше 3×.
  3. Поправить два дока-дефекта — опубликованный диапазон 0.9510 и база 1.5% (правильно: 0.9767 на 1055 движково-чистых, и база 3.02% на ЧЕРНОВОЙ вызов, а не на все выходы, потому что редактор этот отказ произвести не может). Зона оркестратора.
  4. targetScriptShare чистой функцией, НЕ подключённой. Снапшот НЕ двигает — в этом и смысл, дорогое отложено. СТОП, если хоть один здоровый ниже 0.90.
  5. Прогнать предлагаемый вердикт офлайн по всем сохранённым чекпойнтам и сдиффить диспозиции. Ожидание: ровно 2 строки меняются. СТОП+эскалация, если изменилась хоть одна здоровая.
  6. Подключить и бампнуть classifierVersion — ДЕНЕЖНЫЙ шаг, только с подписью владельца. Переиспользовать FlagCJKArtifact (уже escalatable). Порог abstain — 200 букв (ниже него экран воздерживается; самое короткое off-target событие — 346 букв, ниже 200 падают лишь 5 из 1055 здоровых, все со счётом 1.0000).
  7. Починить cultivation в сиде, а не порогом. 86 из 1055 здоровых выходов содержат голое английское cultivation внутри беглого русского; ВСЕ 32 абзаца, набравшие ниже 0.90, содержат его, и НИ ОДИН не содержит ханьцзы. Это дефект глоссария, не калибровочная константа. Правка в пар-данных двигает LangpackVersion, который фолдится с omitempty ⇒ перекупка ограничена книгами ЭТОЙ пары. Это самый переносимый вывод всей цепочки: чинить на том плане, где перекупка уже.
  8. Записать сиблинг-угрозу непостроенной строкой с триггером. Ни одного события не наблюдалось. Если появится — дискриминатор это плотность РУССКО-ЭКСКЛЮЗИВНЫХ букв (ы/э/ё) НИЖЕ ожидания, а не замыкание алфавита.

5.5. Ресерч про механизм гардов (побочный, но применён)

Форма «архитектурное правило как обычный тест» — стандартная: go/build/deps_test.go в самой stdlib держит так граф зависимостей («It is a statement of policy. DO NOT CHANGE THIS DATA TO FIX BUILDS»), go/types/errorcalls_test.go — буквально ast.Inspect по своим исходникам. Аналог в Java — ArchUnit.

Альтернативы проверены запуском и не проходят наше ограничение «падать на обычной батарее»: go test -vet=<свой анализатор>«-vet argument must be a supported analyzer»; GOFLAGS=-vettool при go test игнорируется; штатный composites вообще не входит в набор go test. golangci-lint — 38.7 МБ бинарь и первый в проекте .golangci.yml. x/tools в тесте тянет x/net 0.26.0 → 0.54.0, а x/net/http2 — наш транспорт к провайдерам.

exhaustruct — инструмент ровно под исходный класс бага, но на нашем коде даёт 16 находок, из которых 15 в тестах и одна — сам конструктор. (⚠ эрратум приёмки D39.96, испр. оркестратором: «16» — число, СУЖЕННОЕ до типов config.Stage/pipeline.Request (фактически 17, семнадцатый — сам шов InternalCall.Stage()); ГЛОБАЛЬНЫЙ exhaustruct по репо = 988 — замер пака стандартов, отчёт REPO_STANDARDS_2026-08-03.md §1 находка-2.)


§6. Дефекты: найдено / починено / отложено

Починено с тестами (в этом заходе)

Дефект Тест
П1 Синтетические стадии теряли Reasoning/Temperature — 4 места TestSyntheticStageSeamIsSingle + TestInternalCallDecidesEveryStageField
П2 bankCheckpointExists/repairCheckpointExists строили кортеж БЕЗ Temperature/Reasoning, расходясь с runAttempt в ОБЕ стороны (ложно-false режет резюм бюджетом; ложно-true обходит ролевой суб-бюджет) TestRequestIdentitySeamIsSingle, TestBankProbeAndAttemptAddressOneCheckpoint (3 уровня эффорта, боевой путь ReserveSettleWithCheckpoint)
П3 ThinksOnWire на extra_body_disable врал про пустой эффорт ⇒ бэнк-роль на glm уезжала в thinking-off МОЛЧА TestThinksOnWireMirrorsTheWireForEachControl
П4 sourceEchoExposure не видел внутренние вызовы движка расширен; детерминизм порядка — sort.Strings
П5 Гарды пробивались 7 способами (5 байтовых + алиас импорта + опущенный тип элемента) переписаны на AST, все 7 посадок CAUGHT
П6 Поведенческие тесты были вакуумны (сравнение вызова с самим собой; смена двух полей вместо одного) переписаны на боевой денежный путь
П7 bankRoleSettings молча отдавал бы настройки терминолога неизвестной роли явная паника с именем роли
П8 bankRolePlan.model — второй источник правды, читался только логом снят
П9 Риг с N=1 по умолчанию при комментарии, осуждающем n=1 дефолт 5

Найдено, НЕ починено — с носителем

Дефект Носитель
Д1 Экран целевого языка (работа B) решение владельца по развилке §3.4
Д2 detectLatinInsertion не обусловлен TargetScriptNonLatin() ⇒ первая латинописьменная цель дропнет 100% чанков тем же диффом, что Д1
Д3 SourceEchoExpected не покрывает roleClassifier ⇒ 16/16 вызовов классификатора логируются ok=0 degraded=cjk_artifact. Данные НЕ теряются (run.texts[i] = att.text безусловен, reclassified=14 доказывает), но отравлен ровно тот сигнал, который говорит «провайдер плохеет» тем же диффом, что Д1
Д4 gates.terminology.target_script не сверяется с book.target_lang: target_lang: ru + target_script: Latin грузится чисто и инвертирует банковский экран бэкенд, отдельный пак; вне снапшота ⇒ дёшево
Д5 Наш enum эффорта провайдер-слеп: medium на DeepSeek — неопределённое поведение, max/xhigh недостижимы решение оркестратора: нужна ли пер-модельная таблица
Д6 //go:embed в internal/lang/embedded.go:22 — ЯВНЫЙ список файлов, не data/*. Новая цель ⇒ правка Go. CompileCheckers паникует при неполном файле цели. На ревью-вопрос «заработает ли пара, которой нет в репо» ответ сегодня НЕТ — и это не про язык, а про строку эмбеда архитектура, оркестратор
Д7 Два гарда шва разной формы (AST против байтового регэкспа), с разным корнем обхода и разными словарями исключений бэкенд; свести в один обход
Д8 Ратифицированный порог классификатора «6/6» определён для ОДНОГО вызова, не для выборки. На low — 4/5. Риг сейчас красный на рекомендованном уровне, и это заявлено в тексте падения, а не смягчено владелец/оркестратор
Д9 Аддитивные провайдеры отказываются для бэнк- и repair-гейтов ИМЕННО потому, что блок не несёт reasoning_max_tokens. Новая ручка эту дыру НЕ закрывает зафиксировано комментарием; вскрывать под отдельное решение
Д10 Шиппинговые конфиги репо всё ещё несут reasoning: "off" на ЧЕРНОВОЙ стадии (configs/pipeline-c1.yaml:49, c2:26,39, arm-mistral:30, arm-deepseek-pro:34, arm-glm:34) — тот самый no-op, который отправил прогон в стену 0/9 при вендор-дефолте high. Санкция (б) по D39.87 касается и их, но правка двигает снапшот всем, кто этими конфигами пользуется ⇒ молча не менял владелец/оркестратор: применять ли (б) к шиппинг-конфигам и когда платить перекупку
Д11 Область правки ThinksOnWire — уточнение, чтобы её не переоценили: ЯВНЫЙ "off" на glm предупреждался и до правки, и послеpipeline-arm-glm.yaml:43 он стоит осознанно и с измеренной причиной — «таймауты ×3»). Правка меняет ровно один случай: ключ ОТСУТСТВУЕТ. Там предикат отвечал «думает», а провод мержил disable. Дыра предсуществующая и одинаковая для стадий и для гейтов закрыто; строка оставлена, чтобы приёмка не искала регресса в армах

§6а. Батарея числами (после ВСЕХ правок, включая пост-ревью)

go build ./...                        exit 0
go vet ./...                          exit 0, пусто
go vet -tags live ./internal/pipeline/  exit 0, пусто
gofmt -l .                            пусто
go test -race -count=1 ./...          все 14 пакетов ok (pipeline 82.0s, store 15.9s)
TestGoldenDeterminism                 PASS  (голден НЕ пере-захватывался — нечему двигаться)
майнер-парити                         EXACT  n=13618 catastrophe{方源:0 蛊:1 蛊师:2 古月:22} recall@proposed=0.9655 (56/58 GT)
labels-фриз                           k4b 10/1/36/99 · k4_inverse 3/0/1/71 · k2 34/1/6/51 · k4a 0/0/0/348
K6 (membank)                          tp=1 fp=6 fn=0 tn=251

Все пять фрозен-чисел совпали с ратифицированными. Батарея гонялась трижды: после работы A, после снятия второй ручки, после пост-ревью фиксов.

§7. Заявление = команда

Число Команда
батарея (build/vet/vet-live/race/gofmt) go build ./... && go vet ./... && go vet -tags live ./internal/pipeline/ && gofmt -l . && go test -race -count=1 ./...
голден PASS go test ./internal/pipeline/ -run TestGoldenDeterminism -count=1
парити EXACT n=13618 catastrophe{方源:0 蛊:1 蛊师:2 古月:22} recall 0.9655 TM_MINER_PARITY=1 go test ./internal/miner/ -run Parity -count=1 -v
labels-фриз k4b 10/1/36/99 · k4_inverse 3/0/1/71 · k2 34/1/6/51 · k4a 0/0/0/348 TM_CHECKER_LABELS=1 go test ./internal/checks/ -run Label -count=1 -v
K6 1/6/0/251 TM_CHECKER_LABELS=1 go test ./internal/membank/ -run K6 -count=1 -v
проба §3.4, все числа стадий LOG_LEVEL=debug LOG_LLM_BODIES=1 ./bin/tmctl translate --config ~/books/gu-zhenren/coldrun-b/reprobe/bank-low/book.yaml --verify-bank
терминолог 72/18, off_language=0 grep "terminology finished" ~/books/gu-zhenren/coldrun-b/reprobe/bank-low/run-01.stderr.log
леджер $0.045095 sqlite3 "file:…/guzhenren-bank-low.db?mode=ro" "SELECT SUM(cost_usd) FROM checkpoints;"
независимый пересчёт $0.045095 SELECT SUM((prompt_tokens-cached_tokens)*0.14/1e6 + cached_tokens*0.0028/1e6 + completion_tokens*0.28/1e6) FROM request_log WHERE tm_hit=0 AND model_actual LIKE 'deepseek-v4-flash%' + pro вручную по models.yaml:161
35 тел с reasoning_effort:"low", 0 ключей thinking grep -o 'reasoning_effort[^,}]*' run-01.stderr.log | sort | uniq -c; grep -c '"thinking"' run-01.stderr.log
классификатор 4/5 и 5/5 парно TM_LIVE=1 TM_CLASSIFY6_N=5 TM_CLASSIFY6_CONFIG=…/bank-low/book.yaml go test -tags live -run TestLiveClassifierHarmSet -count=1 -v ./internal/pipeline/ (сырьё: classifier-6of6-{low,high}.json)
боевой гейт 18/20, FP 0/1055, запас 13× временный тест в пакете pipeline через splitBanknote+StripThink+sourceScriptShare; удалён после снятия
санитайзер поймал бы английский черновик временный тест: CompileCheckers(nil, lang.TargetChecksFor("ru")).SanitizeOutput(…)total=1 cosmeticOnly=false "Latin insertion"; удалён
валидация гейта эффорта громкая ./bin/tmctl status --config <копия с reasoning: "sorta">gates.terminology.reasoning must be off|low|medium|high
7 обходов гарда пойманы посадки в waverun.go + go test ./internal/pipeline/ -run SeamIsSingle, дерево восстановлено (git diff --stat пуст)

§8. Критик полноты против себя

  • Качество прозы не судил никто, ни в одной пробе. low мерился детерминированными осями. Черновик без глубокого размышления может быть заметно хуже как ТЕКСТ, проходя все $0-гейты. Это работа судьи Ф2, он не гонялся.
  • low вернул волну к жизни, но стену не убрал — 1 батч из 5 по-прежнему упирается. При другом размере книги/батча доля может быть иной; n=5 батчей.
  • Редакторская волна не гонялась НИ РАЗУ — ни до смены весов, ни после. Класс отказа, убивший черновик и терминолога, на редакторе не проверен, а рычага там нет (маппинг pro low→high). Строка 13б без данных.
  • Проба bank-low понизила escalation.budget_usd — наблюдение эскалаций неполное.
  • Работа B не начата, а её улика (английский черновик) воспроизвелась в этом же заходе. Дыра открыта.
  • Числа off-target — одна книга, один жанр, по сути одна модель. Сиблинг-угроза не наблюдалась ни разу; всё, что про неё сказано, — арифметика алфавитов и авторские образцы, не корпус.
  • Первые версии моих гардов и тестов были дефектны, и нашёл это ревью, а не я. Это второй за заход случай, когда я подал вывод, который не выдержал проверки (первый — 3/6 классификатора на n=1). Оба поправлены до сдачи; оба записаны здесь, а не сглажены.
  • Не проверял, что происходит с английским черновиком в РЕДАКТУРЕ — вытащит ли редактор его обратно в русский или отредактирует как английский. Проба остановилась на банк-паузе.

§9. Адверсариальный проход по своим правкам

Запущен независимым агентом (author≠reviewer, мандат CLAUDE.md 12.07) по всему диффу с явным заданием «считать сломанным, пока не доказано обратное». Вернул 10 находок: 1 blocker, 3 major, 5 minor, 1 nit. Все разобраны; топ-2 я перепроверил кодом сам до принятия (ThinksOnWire — исполнением applyToBody; наследование repair — по всем шиппинг-конфигам). Проверенные им и НЕ нашедшие дефектов области: идентичность кортежа для всех предсуществующих путей (хеши совпали побайтно, бэнк-пробник с незаданным ключом → 8e4495006b978ed4 в обе стороны), полнота шва по всем 17 полям Stage, конкурентность (-race чист), общность (ветвлений по языку/паре/книге нет).


§10. Тронутые файлы (только своё)

M backend/internal/config/models.go            ThinksOnWire + доккоммент эхо-гейта
M backend/internal/config/pipeline.go          gates.terminology.reasoning, gates.repair.reasoning, валидация
M backend/internal/config/capability_test.go   +TestThinksOnWireMirrorsTheWireForEachControl
M backend/internal/llm/capability.go           доккоммент ReasoningNone
M backend/internal/pipeline/stagerun.go        attemptRequest
M backend/internal/pipeline/escalation.go      хоп через attemptRequest
M backend/internal/pipeline/terminologist.go   bankStage/bankRoleSettings, пробник, смета, plan без model
M backend/internal/pipeline/repair.go          repairStage, пробник, доккоммент
M backend/internal/pipeline/runner.go          sourceEchoExposure на внутренние вызовы
M backend/internal/pipeline/live_reprobe_test.go  N-выборка, шов, порог
?? backend/internal/config/internal_call.go       ШОВ
?? backend/internal/config/internal_call_test.go  полнота полей
?? backend/internal/pipeline/synthetic_stage_seam_test.go  гарды швов
M docs/PROGRESS.md                             секция «Бэкенд», допись СВЕРХУ
?? docs/archive/reports/EFFORT_HANDLE_2026-08-02.md  этот отчёт

Вне git (артефакты): ~/books/gu-zhenren/coldrun-b/reprobe/bank-low/ — проект, БД, run-01.stderr.log, wire-terminologist-batches.json, classifier-6of6-{low,high}.json.

Чужого не тронуто: START_PROMT.MD, frontend/, platform/, .gitignore, docs/ кроме этого отчёта и §Бэкенд в PROGRESS.md. Коммитов нет.

СТОП — приёмка оркестратора.