> ⚠ **ОТРАБОТАН И ПРИНЯТ 16.09 — акт `D39.257`, 29 путей в `backend/` плюс отчёт, пак $0.** > Инструкции ниже НЕ исполняются: пак закрыт. Половина ряда **445** закрыта (мусор больше не едет законом), > остаток — отказ прозой до заполнения реестра. Новые строки: **462**–**467**. > ⭐ Главное, что купил пак, — не код: посылка, на которой стоит `D39.256` («у смен передачи не было новой > улики»), перестала быть посылкой. 25 из 26 смен при идентичном входе, и компаратор НЕ слеп — у неменявшихся > строк он находит движение входа в 4 из 56 и 8 из 51. > ⛔ Два мажора приёмки: сдача сдвинула 25 якорей живых доков и отчёт молчал (перенаведены лендингом, норма — > ряд 467); арм `unresolved` считается по знаменателю, из которого сам вычтен (ряд 462). > ⚠ Условие §4.1 в теле УЖЕ реальности: дверей не три, а пять — сессия расширила по праву и была права. # Пак бэкенда: «БАНК — ЧТО МОЖНО УЗНАТЬ И ПОЧИНИТЬ ЗА $0» > Оркестратор №23. Пак **$0**: ни одного платного вызова моделей. Всё, что ниже, либо детерминированный код, > либо замер по УЖЕ КУПЛЕННОМУ сырью. Если тебе покажется, что предмет требует покупки — это вопрос > оркестратору (§11), а не повод купить. ## 1. Какая проблема и что решит твой результат Банк памяти — таблица `(src, sense, since_ch, until_ch) → dst, gender, status`, которую движок инжектит переводчику и редактору, чтобы имена и термины были одинаковы на всю книгу. Владелец назвал это место **самым умным участком движка**: «там скорее консилиум алгоритмов должен работать, который выведет, что инжектить редактору». Этот пак — НЕ тот консилиум. Это его **фундамент**: четыре вещи, которые сегодня либо молчат, либо не измерены, и каждая из них делает будущий «умный участок» либо невозможным, либо недоказуемым. ⛔ **Почему именно $0-слой первым, и это не осторожность, а порядок работ.** Умная маршрутизация кандидатов (кого спрашивать, с каким контекстом, когда считать решённым) стоит на двух вещах, которых сегодня нет: на предикате «спорный» с ИЗМЕРЕННОЙ популяцией и на честном разборе ответа роли. Построить маршрутизатор раньше — значит маршрутизировать по признаку, чьё распределение никто не видел. ## 2. Зона и git Твоя зона записи — `backend/`. Ты **не коммитишь**: готовишь дерево и передаёшь на лендинг оркестратору. Чужие незакоммиченные файлы не трогай. Свои итоги пиши в `docs/PROGRESS.md`, секция «Бэкенд» — это единственное исключение из «`docs/` — зона оркестратора». Читать чужое можно и нужно. Расхождение, которое ты нашёл вне своей зоны, — пинг в отчёт, а не правка. ## 3. Карта чтения — ЗАКОН, дальше только по её ссылкам 1. **Этот файл целиком.** 2. `docs/research/35-bank-memory-consilium.md` — итог консилиума. Читай §П3 «Исходы решения», §2.0 «что двигает каждый шаг» и **обязательно пометку в КОНЦЕ файла** (16.09): §П3 верен про ноту и неверен про дерево, и это прямо касается §4.2 ниже. 3. `docs/BACKLOG.md` — ряды **441**, **445**, **449**, **459** (заказаны) и ряды **435**, **438** (НЕ заказаны, но прочти: там названы две денежные мины следующего пака, и твой код не должен их углубить). 4. `docs/architecture/05-decisions-log.md` — тела `## D39.102` (греп) и `## D39.254`. Первое закрывает МОДЕЛЬНЫЙ совет замером, второе несёт пять решений владельца 16.09. 5. Предмет в коде: `backend/internal/pipeline/terminologist.go` и `backend/internal/pipeline/mining.go`. ⛔ Журнал решений целиком НЕ читать. Номер грепается: тело — строка `^## D<номер>`; подномер (`D39.2`) собственного тела не имеет, отбрось хвост. ## 4. Состав и разметка свободы ### 4.1 Цель без текстов инъекции обязана останавливать прогон ГРОМКО — **делай РОВНО так** ⚠ **Посылка сформулирована ТОЧНО, потому что первая редакция этого промта была тут НЕВЕРНА.** Сегодня книга с не-русской целью до молчания НЕ доходит: загрузчик падает раньше и громко, потому что промтов пары нет — `backend/internal/config/pipeline.go:870`=`A missing convention prompt` («a book whose pair has no prompt pack must stop, never silently run another pair's conventions»), а в `backend/prompts/` лежит только `zh-ru`. **Молчаливый путь достижим для БУДУЩЕЙ пары**, у которой промты и langpack уже есть, а строк инъекции ещё нет: тогда прогон покупается, а оба банк-блока рендерятся в **ноль байт** молча. Гейт стоит в рендерерах: `backend/internal/membank/memory.go:745`=`func RenderGlossaryBlock` и `backend/internal/membank/memory.go:841`=`func RenderEditorConstraintBlock`, оба начинаются с `if !tx.HasData() { return "" }`, где `HasData` — это всего лишь наличие заголовка (`backend/internal/lang/embedded.go:406`=`func (t InjectionTexts) HasData() bool`). А источник молчания — строкой выше по потоку: `backend/internal/lang/embedded.go:425`=`return InjectionTexts{}` возвращает нулевое значение **без ошибки**. **Где ставить отказ:** в `openRunner`, рядом с уже существующим отказом того же рода — `backend/internal/pipeline/runner.go:364`=`forWrite && r.Pipeline.Gates.Terminology.Enabled && r.pack == nil`. Там в руках и книга, и пайплайн, и флаги гейтов, и это **до открытия волн и до первого платного вызова**. Форма — `RefusalBadConfig`, как у соседа, а не WARN: граница между ними названа в комментарии рядом (`backend/internal/pipeline/runner.go:369-371`=`THE BANKNOTE CHANNEL BUYS OUTPUT NOBODY WILL READ`) — «механизм не может работать вовсе» против «стоит лишних денег». ⛔ **Не ставь это в `runTerminologist`.** Первый платный вызов книги — ЧЕРНОВАЯ волна, а банк-роли платятся позже, на стопе майнинга. Гейт у роли опоздает на целую волну, каждый вызов которой уже нёс банкнотный промт. Довод дословно тот же, что у соседнего `CheckMiningContrast`: «costs nothing to verify now and a whole draft wave to discover later». ⛔ **Условие отказа ШИРЕ, чем «включён гейт», и это второй денежный путь, а не придирка.** Инъекция строится при `r.baseMemory != nil` (`backend/internal/pipeline/waverun.go:592`=`if r.baseMemory != nil`), то есть **мимо гейтов**: книга с сидом (`backend/internal/config/book.go:92`=`GlossarySeed string`) и ВЫКЛЮЧЕННОЙ терминологией даёт тот же молчаливый ноль. Условие бери такое: `forWrite && (Gates.Terminology.Enabled || Gates.Banknote.Enabled || book.GlossarySeed != "") && !lang.InjectionTextsFor(book.TargetLang).HasData()`. **Текст отказа обязан сказать, ЧТО делать:** строки живут в go:embed-файле `backend/internal/lang/data/injection.txt`, и добавление цели двигает `lang.EmbeddedVersion()` ⇒ снапшот. Отказ, который этого не называет, отправляет будущую пару в молчаливую пересборку Go. **Пин обязателен**, и он должен краснеть на ДАННЫХ, а не на отказе соседа: фикстура обязана ПРОЙТИ загрузчик и соседний отказ `:356` (синтетическая пара: промты + каталог langpack) и сверять СВОЙ текст и класс отказа. Иначе пин удовлетворяется чужой уликой и зелен ни о чём. ### 4.2 Отказ роли не должен становиться передачей — **делай РОВНО так по форме, границу реши сам и обоснуй** Ряд 445: роль отвечает прозой («не термин») или мусором («90»), и это сегодня едет редактору **законом**, побайтно так же, как подписанное человеком решение. ⛔ **Сначала прочти ряд 459 и пометку в конце `research/35`** — там развилка, которую ты обязан закрыть явно, а не по умолчанию. В боевом промте zh-ru отказ **приглашается**: `backend/prompts/zh-ru/terminologist.md:36`=`не можешь выбрать перевод уверенно` велит вернуть сентинел и называет это нормальным исходом; парсер его ждёт (`backend/internal/terminology/terminology.go:694`=`const NoDst = "⟦TM-NO-DST⟧"`, `backend/internal/terminology/terminology.go:1248`=`func isNoDst`), счёт печатается прогоном (`backend/internal/pipeline/terminologist.go:649`=`"declined", res.Declined`). При этом ратифицированная нота `D39.102` числит отказ-сигнал закрытым. ⛔ **Выбора у тебя здесь НЕТ — исполним только путь (а), и это исправление первой редакции промта.** Снятие сентинела есть отмена ратифицированного режима, а не настройка: «A declined term emits status:auto (inert) instead of a guess — §C2-7's other mode» (`backend/internal/terminology/terminology.go:693`=`A declined term emits status:auto`), а ИСТОЧНИК — нота: `D39.42` п.1 ратифицирует дословно «двухрежимная эмиссия §C2-7 (есть dst → `draft`, нет → `auto`)», и нота сильнее комментария в коде. Считаешь, что сентинел надо снять, — это **вопрос оркестратору на ноту** (§11) с расчётом перекупки, а не диспозиция сессии. **Что делаешь: ужесточаешь РАЗБОР ответа, не трогая сентинел.** И раскладываешь по ДВУМ корзинам — сегодня они разные, и следующий пак маршрутизирует именно по этому различию: - **отказ** (сентинел, а также отказ прозой) → ветка `isNoDst` ⇒ `Declined` (`backend/internal/terminology/terminology.go:1136`=`if isNoDst(dst)` кладёт `out[key] = ""`; `backend/internal/pipeline/terminologist.go:581`=`res.Declined++`); - **мусор** (`dst` БЕЗ БУКВ ВООБЩЕ — «90», пунктуация) → плохая строка, ключа в ответе нет ⇒ `backend/internal/pipeline/terminologist.go:579`=`res.Unanswered++`. ⚠ **Класть отказ в «неотвечено» НЕЛЬЗЯ:** это слово означает, что роль промолчала, и комментарий счётчика прямо так и говорит. Смешав, ты сломаешь и счётчик, и будущую маршрутизацию (отказ → фоллбек другим семейством; пустота и обрыв → регенерация). ⚠ **Два уточнения критерия, оба проверены деревом:** «`dst` без букв ЦЕЛЕВОГО языка» уже построено (`OffLanguage`) — не перестраивай его; дыра именно в «без букв вообще». И **не** отвергай `dst` с цифрой: цифры законны внутри имён и серий, критерий — отсутствие букв, а не наличие цифр. ⛔ **Отказные фразы — ЯЗЫКОВЫЕ ДАННЫЕ, а не литералы в Go:** русские строки в коде прямо нарушают цель «движок один, пара — данные». Реестр фраз живёт в langpack, пустой реестр = инертный no-op. ⭐ **Что здесь дешевле, чем кажется, и это проверено:** правка ПАРСЕРА меняет содержимое банка (а с ним `memory_version` и пере-редактуру под согласием), но **не** трогает тождество запроса — парсер в кортеж `attemptRequest` не входит. То есть путь (а) не перекупает батчи. ### 4.3 Предикат «спорный» — это ДВА предиката, а не один — **делай РОВНО так** Будущий фоллбек и рецензент будут звать модель «на спорных кластерах». Слово «спорный» сегодня знаменателя не имеет. Построй его детерминированно и **измерь популяцию за $0** на уже купленном сырье. Читать ТОЛЬКО на чтение (`sqlite3.connect('file:<путь>?mode=ro', uri=True)`; CLI `sqlite3` на машине нет). Понадобится звать `buildBankCandidates`/`bankStopRows` вне платного прогона — это $0-харнесс реплея по БД, и он часть заказа. ⛔ **ДВЕ БАЗЫ ЕСТЬ, НО ОНИ В РАЗНЫХ СОСТОЯНИЯХ, и наивный реплей намеряет артефакт состояния вместо популяции.** Замерено мной 16.09: - **B — снимок НА СТОПЕ:** `/home/ubuntu/tm-coldrun-b/evidence/paid-bankstop/project.db` — `glossary` **0**, `bank_stop_presented` 67, `request_log` 18, `checkpoints` 24. Рядом сайдкары (`*.bank.json` 66 кандидатов, `*.bank-stop.txt` 648 строк). - **A — состояние ПОСЛЕ прогона:** `/home/ubuntu/tm-coldrun-a/stand/books/bk_ROEHZBD46ALFI43E/project.db` — `glossary` **69**, `bank_stop_presented` 70, `request_log` 33. Сайдкары A лежат ОТДЕЛЬНО, в `/home/ubuntu/tm-coldrun-a/evidence/bankstop-paid/` (69 кандидатов, 660 строк листа), самой БД там нет, а пути в манифесте указывают на ЧУЖОЙ хост и протухли. ⇒ на A фильтр решённых увидит ЗАПОЛНЕННЫЙ глоссарий и «решит банком» то, что на покупке никто не решал. Либо гони реплей на A с глоссарием, откатанным к стопу, либо считай pre-call-признаки по сайдкарам — и назови, что выбрал. ⛔ **ДРЕЙФ СХЕМЫ САЙДКАРОВ — готовый ложный ноль:** в A поля `conventions` нет **вовсе** (0 записей из 69), в B оно есть у всех 66. Посчитаешь «conventions ≥ 2» на A наивно — получишь 0 и это будет число об ОТСУТСТВИИ ПОЛЯ, а не о предмете; пересчитывай из `variants` той же свёрткой. ⚠ И ключи бери по ВСЕМ записям, а не по первой: часть полей пишется с `omitempty` (`invented` есть в 11 записях из 69 у A и в 17 из 66 у B). ⛔ **ЧТО УЖЕ ИЗВЕСТНО ДО ТВОЕГО ЗАМЕРА — чтобы ты не отчитался нулём как свойством предмета.** Мои числа: A `spread ≥ 2` = 9, `invented` = 11 (из 69); B `spread ≥ 2` = 8, `conventions ≥ 2` = 8, `invented` = 17 (из 66). **А вот post-call-признаки конфликта на обоих источниках имеют популяцию НОЛЬ:** в листах стопа `CONTRADICTS`, `THE BANK ALREADY HOLDS` и `NOT ASKED` — по 0 вхождений при непустых файлах (660 и 648 строк — контроль напечатан). Значит живые post-call-сигналы здесь только два, и предикат, построенный на конфликтах, ты проверить на этом сырье НЕ сможешь: так и скажи, а не выдавай ноль за измерение. ⛔ **Предикатов ДВА, и у них разные поля и разные места. Если сделаешь один, маршрутизация ДО вызова останется пустой, а отчёт скажет «предикат есть».** - **pre-call** (решает, кого и с каким контекстом спрашивать). Доступно до первой траты — там, где живёт «$0-половина роли»: `backend/internal/pipeline/terminologist.go:270`=`buildBankCandidates is the $0 half of the role`, она «runs whether or not the gate is on». Поля: разброс вариантов, конвенции, сигналы ранжирования, и «уже решено банком» — последнее снимается фильтром `backend/internal/pipeline/terminologist.go:379`=`r.dropBankSettled(ctx, cands)`, который стоит ДО первого платного вызова (`backend/internal/pipeline/terminologist.go:413`=`r.runClassifier(ctx, snapID, paid)`). ⚠ Назови честно ограничение: до классификатора тип кандидата эвристический, поэтому pre-call-предикат видит «спорный по черновикам», а не «спорный по типу». - **post-call** (решает, кого отправлять на фоллбек). Поля живут в строках стопа: `backend/internal/pipeline/mining.go:479`=`func bankStopRows`. ⚠ И там же — ловушка знаменателя: при выключенном гейте результат роли пуст (`backend/internal/pipeline/mining.go:480-485`=`A stop can fire with the terminology gate OFF` объясняет почему), то есть популяция считается по-разному в двух режимах, и это надо назвать, а не усреднить. **Знаменатели ЗАДАНЫ здесь, а не выбираются тобой** — иначе «доля спорных» окажется утверждением о твоём пороге: - pre-call: знаменатель = кандидаты на входе роли (дошедшие до `buildBankCandidates` после фильтра решённых); - post-call: знаменатель = строки, на которые роль ОТВЕТИЛА (у неотвеченных признаки не определены вовсе). **Результат — не одно число, а таблица чувствительности:** доля «спорных» при нескольких порогах, на обоих источниках, с обоими знаменателями, плюс ответ «сколько строк ушло бы на второй вызов, если бы он существовал». Одно число при невыбранном пороге замером не является. ### 4.4 Посылка «новой улики не было» обязана получить носитель — **делай РОВНО так** Экономика будущего базиса решённого стоит на утверждении «у 25 из 26 смен решения новой улики не было». Тела запросов к банк-ролям **не логировались**, то есть якорь модели не отделён от её стохастики. Это $0: по уже купленному сырью собери, что РЕАЛЬНО менялось во входе между двумя покупками одной строки. ⚠ **Скорее всего честный ответ — «тел нет»:** чекпойнты хранят ОТВЕТ, лог запросов — хеш и токены, тела пишутся только в лог под отдельным флагом. Тогда результат пака — не число, а **дизайн персистенции**: что именно надо сохранять (тело запроса банк-роли, снимок канона и кандидатов), чтобы вопрос стал отвечаемым, и чего это стоит. ⛔ **Опасное умолчание, назови его сам:** пере-рендерить сообщения роли по ТЕКУЩЕЙ базе и заключить «вход не менялся» — значит ответить о сегодняшнем состоянии, а не о той покупке. «Не воспроизвелось» без названной цепи звеньев отчётом не считается. ### 4.5 Чего в паке НЕТ — **не делай**, и это объявленные сужения - **Регенерация банк-роли и фоллбек на другую модель** — следующий пак. ⛔ И вот почему их нельзя взять «заодно»: у внутренних вызовов хоп нулевой НАМЕРЕННО (`backend/internal/config/internal_call.go:80`=`no escalation hop`), `maybeEscalate` выходит на пустом хопе (`backend/internal/pipeline/escalation.go:125`=`st.ResolvedHop == ""`), а все три боевых `escalate_to` стоят у стадии `draft`. Включение `escalation.budget_usd` банку не поможет и включит эхо-эскалацию черновика. Плюс проба «уже оплачено» у банк-батча спрашивает попытку 0 с БАЗОВЫМ бюджетом (`backend/internal/pipeline/terminologist.go:840`=`paidAfterBurns(st, st.Model`) — она обходит сгоревшие индексы, но только на ТОМ ЖЕ бюджете, — а бюджет попытки входит в тождество запроса, поэтому регенерация с другим бюджетом пробе невидима. - **Базис решённого (N-1), род-улика (N-5), стратифицированный KWIC (ряд 449)** — не в этом паке. - **Модельный совет в любой форме** — закрыт замером `D39.102` п.2 и не переоткрывается. ## 5. Мандат самопроверки ИСПОЛНЕНИЕМ — «перечитал сам» не считается Промт требует ревью **исполнением**: своего кода, своих замеров и своих чисел. Самоотчёт «проверено» без исполнения регулярно оказывается ложным. - Каждый пин — прогнать и показать вывод. Пин, который на твоей машине скипается, не стережёт ничего. - Каждое число — пере-снять вторым прибором либо напечатать рядом контрольную величину. **Отрицательный замер обязан доказать, что спросил существующее:** «0 совпадений» и «нет такого поля» неразличимы. - Сверх исполнения — **адверсариальный проход по собственной готовой работе**. Где твой пак уязвим: предикат §4.3 легко померить на удобной половине данных; отказ §4.1 легко проверить на конфиге, который и так не запускается. ## 6. Оси ревью Характер «Код» плюс ось денег: любой твой механизм обязан ответить, что он делает с чужими деньгами, если сработает ошибочно. Ось вправе заменить с аргументом. ## 7. Записка-план ДО работы До первой правки — короткая записка в `docs/PROGRESS.md`: что берёшь, в каком порядке, чем будешь мерить. ## 8. Заявление = команда Любое «проверено / измерено / не воспроизводится» в отчёте обязано быть командой, которую можно повторить. ## 9. Эхо-протокол старта Первым сообщением — своими словами: что заказано, где предмет уже, чем ты его смеришь, и **какие опасности ты видишь в самом заказе**. Право сказать «этого делать не надо» у тебя есть (§11). ## 10. Что НЕ удалось — обязательная секция отчёта Пустой она не бывает. Если кажется, что бывает, — значит не назван остаток. ## 11. Канал вопросов и право сказать «этого делать не надо» Вопрос — секцией в отчёт; работа продолжается. Владелец прочитает и пере-передаст. Если ты видишь, что пункт заказа неверен, — **скажи это и не делай**, с доводом. Это норма, а не отказ от работы. ⭐ **Тебе разрешён ОДИН советчик-фабл** (`model: "fable"` задавай явно). Он советчик, а не источник истины: его посылки проверяются деревом — у меня две его ссылки из десяти оказались с неверными номерами строк. ## 12. Прямой канал Связь между сессиями — `/tmp/textmachine-channel`: впиши свой блок (`role=` `session=` `ref=` `written=` `note=`), дописывая в конец и не трогая чужие. ## 13. Критерий завершённости — по нему тебя примут 1. §4.1 построен, отказ краснеет пином на данных, и пин прогнан. 2. §4.2: разбор ужесточён по пути (а); отказ и мусор РАЗВЕДЕНЫ по корзинам (`Declined` против `Unanswered`), обе ветви запинены; реестр отказных фраз лежит в langpack, а не литералами в Go. 3. §4.3: два предиката; $0-харнесс реплея по БД работает; таблица чувствительности по порогам снята на обоих источниках, оба ЗАДАННЫХ знаменателя использованы и названы в отчёте. 4. §4.4: ответ или честное «данных нет, нужен такой-то лог». 5. Гейт зоны зелёный целиком — `make battery`, а не `go test`; в отчёте названо, что прогнано. 6. Мутации новых мест введены в гейт, а не прогнаны разово. 7. Секция «что не удалось» заполнена. ## Деньги **Пак $0.** Платных вызовов моделей нет ни одного. Замеры — по уже купленному сырью. Если предмет требует покупки, это вопрос оркестратору, а не решение сессии. ## Отчёт `docs/PROGRESS.md`, секция «Бэкенд»: что построено, чем проверено, какие числа сняты и каким прибором, что осталось. Дерево передаёшь на лендинг — не коммитишь.