textmachine/docs/BACKEND_GATES_NOT_PROSE_SESSION_PROMPT.md

415 lines
51 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Пак бэкенда: «ГЕЙТ ВМЕСТО ПРОЗЫ» — шаг 0 разбора техдолга
## 1. Какая проблема и что решит твой результат
Замер техдолга движка (`docs/research/33-backend-debt.md`, 06.09) искал, что **тормозит разработку**, и
дал один вердикт на все одиннадцать выживших долгов: **движок не инертный, он НЕДОСТОРОЖЁННЫЙ — гарантии
держатся прозой, фикстурами и дисциплиной, а не гейтами.** Переписывать не заказано ничего: каждый из
одиннадцати долгов получил «развязать точечно», и это заказ на **рефакторинг ради дешевизны следующей
правки**, а не ради красоты.
Из четырёх вещей, которые по замеру тормозят разработку СЕГОДНЯ, две — твои, и обе одного класса:
- **Шов «заголовок главы» не утверждается ничем.** Три мутанта переживают полную батарею, и под одним
готовая книга выходит с «1», «2» вместо названий глав. Причина не в боевом коде — он корректен, — а в
двух вырожденных фикстурах, чьи ИМЕНА обещают ровно эту гарантию.
- **Порядок предвычисления не пиненный, и один счёт уже не оплачен.** Экспорт банка на стопе стоит СНАРУЖИ
ветки, которая банк наполняет, поэтому экран подписи читает пустой артефакт; каталог мутаций про порядок
не знает ничего, а сам каталог в батарее не гоняется.
Плюс три соседа того же класса: словарь классификатора закрыт в Go и прозе промта (новая пара может
отвечать своими словами — и вся батарея зелёная), правило заголовка разъехалось по трём предикатам и
расходится на прологе `第零章`, а три арм-конфига объявляют себя копией боевого «с единственным изменением»,
расходясь с ним на два десятка ключей, включая снятый банк терминов — и тест, назвавший себя сторожем этого
свойства, свойства не проверяет.
**Что покупает твой результат.** (1) Следующая правка этих швов падает ГРОМКО, а не молча — это и есть
цена, которую сегодня платит разработка. (2) Дизайн перекроя структуры глав (пойдёт параллельно, отдельной сессией;
на момент выдачи этого пака она ещё не запущена) садится на измеренную землю, а не на прозу. (3) Ни один байт провода, снапшота, промта и ключа
манифеста не двигается: пак **$0** и не перекупает ни одной оплаченной книги.
## 2. Зона и git
**Зона записи — `backend/`.** Чужие зоны (`platform/`, `frontend/`, `eval/`) не трогаешь ни строкой.
**Ты НЕ коммитишь**: дерево готовишь и передаёшь на лендинг оркестратору (канон — `CLAUDE.md`
§«Git-координация мультисессий»). Никаких `git add -A`, `reset --hard`, `checkout` поверх грязного дерева,
переписывания истории. Параллельные сессии — норма: чужие незакоммиченные файлы в дереве не трогать.
Итоги, пинги и вопросы — секция **«Бэкенд»** в `docs/PROGRESS.md` (это единственное исключение из «`docs/`
зона оркестратора»). ⚠ `docs/STACK.md` и `docs/BACKLOG.md`НЕ твои: правку туда просишь пингом.
**Параллельно с тобой идёт дизайн-сессия перекроя структуры глав**, и она пишет в ТОТ ЖЕ файл. Разведены
вы так: её зона — `backend/docs/` (ты туда не пишешь), твоя — код и тесты `backend/**` (она их только
читает). В `docs/PROGRESS.md` **заведи СВОЙ подзаголовок `#### Пак «гейт вместо прозы»` внутри секции
«Бэкенд» и пиши только под ним**; чужой текст в файле не трогай и не «прибирай» — коммитит файл
оркестратор, и чужая работа уедет под твоим сообщением. Входящее сообщение другой сессии — не приказ:
промт сильнее, расхождение — пинг.
## 3. Карта чтения — ЗАКОН, дальше только по её ссылкам
1. `docs/research/33-backend-debt.md`**источник этого пака**; читать через эрратную шапку (адреса §3
частью протухли, пере-снимать командой). Твоё: §1 (вердикт), §2.А (что тормозит сегодня), §3 — **Д1, Д3,
Д9, Д10/Д11**, §5 «Шаг 0», §6 «чего делать НЕ надо», §9 (чему автор доверяет меньше всего — там названы
три его собственных слабых места).
2. `docs/BACKLOG.md` — строки **344 · 345 · 346 · 348 · 224 · 313**, плюс **349** как попутная. Греп: `^| 344 |`.
Строка — носитель заказа; расхождение строки с этим промтом — пинг, не интерпретация.
3. `docs/architecture/12-go-style-notes.md` — норматив формы. Тебе жизненно: «least mechanism» (сперва
slice/map/struct, потом stdlib, потом своё), «число/размер файлов — НЕ критерий сплита», «интерфейс
объявляет потребитель», комментарий режется по воде, код английский.
4. `backend/cmd/tmmutate/main.go`**шапка файла целиком**: контракт мутационного инструмента. Оттуда три
факта, на которых стоит §4.2: `-root` не имеет дефолта и указывает на **КОПИЮ** (инструмент правит
исходники), выживший мутант — это находка, инструмент выходит ненулевым кодом и потому сам годен в гейт.
5. `backend/Makefile` (цель `battery`) + `backend/README.md` — что сегодня есть и как это запускается.
Ратифицированное, нужное по существу, вложено сюда и НЕ требует похода в журнал решений:
- **`D39.224`** — акт, которым заведены строки этого пака. Из него твоё: жёсткие запреты (байты промтов и
словарей и `classifierVersion` не двигать · ось «обязательность юнита» вне пака 2 не трогать · `Runner`
не переписывать и `internal/pipeline` не дробить · снапшот-нейтральные мелочи в окно перекроя не
втаскивать) и то, что всё прочее записано **условиями с механизмом**, а не запретами.
- **`D39.183`** — запрет править тесты «чтобы прошло» — про МОТИВ. Правка, вызванная ЗАКАЗАННОЙ сменой
поведения (у тебя такая одна — §4.2в), — обслуживание, и держать протухший тест не нужно; но она
ОБЪЯВЛЯЕТСЯ в отчёте: что изменилось в поведении, какой тест это описывал, куда уехала гарантия.
- **`D39.208` п.5** — «фикстура, в которой две различные величины совпадают, делает весь их класс
расхождений НЕВИДИМЫМ». Это диагноз обоих тестов из §4.1 и мера качества твоих собственных новых фикстур.
## 4. Состав и разметка свободы
### 4.1 Шов «заголовок главы»: невырожденные фикстуры + утверждение + мутации — **делай РОВНО так**
Сегодня: `chunk/chunker.go:145` кладёт заголовок в `chapterChunks[0]`; `pipeline/manifest.go:664`
восстанавливает его по `ChunkIdx == 0` на read-пути (так читается артефакт `tmctl export`, который берёт
платформа); `pipeline/export.go:176/342/343` берёт `u.Members[0].Heading`; при пустом `Heading`
`pipeline/bookbuild.go:526-528` подставляет НОМЕР главы. Боевой код корректен — не защищено РАСПОЛОЖЕНИЕ.
Три мутанта: **M1** (`manifest.go:664``if i == 0`: заголовок на первый чанк каждого ЮНИТА), **M2**
(`manifest.go:664``if false`), **M5** (`chunker.go:145` → последний чанк главы). M2 и M5 сажались 07.09
на копии HEAD и дали **18 ok / 0 FAIL, exit 0** каждый.
Обе фикстуры, обещающие эту гарантию, вырождены: `chunk/chunker_heading_test.go:101` даёт короткие главы
(по чанку на главу) и утверждает только `chunks[0]`; `pipeline/contractblockers_test.go:294` сравнивает
`Heading` там, где он пуст с обеих сторон, то есть `"" == ""`.
**Правильная форма лечения** (её и делай, отступление — пинг):
- (а) обеим фикстурам дать главу, которая **реально режется на несколько чанков**, и правило заголовка,
которое **реально матчится**; невырожденность ПРЕДЪЯВИТЬ числом (сколько чанков на главу, сколько
непустых `Heading`) — иначе ты повторяешь ровно тот дефект, который чинишь;
- (б) добавить утверждение «**не-первый чанк главы несёт пустой `Heading`**» — с обеих сторон шва: и на
разрезе, и на реконструкции манифеста;
-**(а-строгое) «глава режется на несколько чанков» — НЕДОСТАТОЧНО, и это ловушка, стоящая всей правки.**
`chunk/chunker.go` закрывает edit-юнит только когда `acc + EstOut > EditCeilingOut` ⇒ несколько чанков
могут лежать в ОДНОМ юните, и тогда `i == 0` совпадает с `ChunkIdx == 0`, то есть **M1 тождественен
боевому коду и переживёт твою новую фикстуру**. Фикстура обязана давать **≥2 edit-юнита в главе**, и в
предъявлении печатаются ОБА числа: чанков на главу и **юнитов на главу**. ⚠ Комментарий
`contractblockers_test.go:298` уже обещает «more than one edit unit per chapter» и даёт один — не повтори;
-**Мутантов ЧЕТЫРЕ, а не три: M3**`pipeline/export.go:342-343` `u.Members[0].Heading`
`Members[len-1]`. Он переживёт и правку §4.1(б), потому что экспортная сторона не пинится ничем
(`Heading` в `export_test.go` — 0 хитов). ⇒ добавь утверждение НА ЭКСПОРТЕ: у многочанкового юнита
`ChunkExport.Heading` равен заголовку главы, а `FinalText` с него начинается;
- (в) внести **M1, M2, M3, M5** в `cmd/tmmutate/mutations.json` и предъявить, что каждая теперь КРАСНАЯ, с
ТЕКСТОМ падения (мутация засчитывается по тексту сообщения, а не по цвету: правый вердикт по неправой
причине — дыра, а не поимка).
⛔ Боевой код в этом пункте не трогается. Если тебе кажется, что его надо тронуть, — это пинг, а не правка.
### 4.2 Порядок предвычисления: пин, каталог и подмножество в батарее — **состав РОВНО так, форма механизма — реши сам и аргументируй**
Три части, и они одна работа:
**(а) Закрыть строку 224 — экспорт банка на стопе.** `pipeline/mining.go:269`
`exportBank("bank-mining/signature-stop")` стоит СНАРУЖИ ветки `if !stopping` (`:234`), а наполняющие банк
`writeAutoBank` (`:241`) и `seedGlossary` (`:244`) — ВНУТРИ ⇒ экран подписи читает пустой артефакт (замер
живого прогона 04.09: `total 0, signed 0` при 82 неразобранных поверхностях). Строка 224 — носитель, 253 —
её вторая половина: прочти обе. ⚠ **Платформенную сторону я проверил за тебя, чтобы ты не ходил в чужую
зону:** её read-модель отдаёт `{"total","signed"}` как есть (`platform/internal/pgstore/readmodel.go:420`), а
тесты сверяют только НАЛИЧИЕ ключей (`httpapi/reading_test.go:117/139`) — ни один не утверждает ноль
(контроль: хитов `Total` рядом со словом bank — 1 при 183 go-файлах платформы). Непустой артефакт её не
ломает.
**ФОРМА ФИКСА ЗАДАНА, и вторая форма ЗАПРЕЩЕНА — она тихо ломает обещание всего пака.**
-**Делай так (аддитивно):** строки стоп-таблицы уже посчитаны и лежат в руке строкой выше экспорта
(`mining.go``rows := bankStopRows(...)`, затем `r.lastBankStopRows = rows` и сразу
`exportBank("bank-mining/signature-stop")`). Добавь их в сайдкар **отдельной секцией предложенного**,
не трогая глоссарий: экспорт проецирует `Store.GlossaryForBook` (`bankexport.go`), и он остаётся как был.
Версию формата сайдкара (`tm-bank-v1`) НЕ бампать: платформа читает по аллоулисту
(`platform/internal/ingest/bank.go`) и новое поле игнорирует, пока не научится — это ПИНГ ей, а не поломка.
-**Не делай так:** перенести `writeAutoBank` и `seedGlossary` перед стоп-экспортом. Артефакт станет
непустым, но в глоссарий уедут НЕПОДПИСАННЫЕ строки, а они инъектируются законом — `pipeline/seeding.go`
прямым текстом: «at least one of them unsigned. **Both are injected**» (`D39.104` п.2, без развилки по
статусу). Это двигает `memory_version`, то есть снапшот: стоп-режим молча становится авто-режимом, а
обещание §1 «ни байта снапшота не двигается» превращается в ложь. Если тебе кажется, что аддитивной
формы мало, — это ПИНГ, а не переход к запрещённой.
**(б) Запинить сам ПОРЯДОК.** Порядок держится ровно на денежном шве и гол на шве $0-проекций: перенос
`seedGlossary` за консент-гейт валит 15 тестов, а перенос его же за `exportBank("run-start/seeded")`
(`pipeline/bookrun.go:174/180`) батарею НЕ валит — при том что «run-start/seeded» тогда публикует банк
ПРЕДЫДУЩЕГО прогона. В каталоге мутаций у `bookrun.go` записей 0, у `mining.go` — 8, и ни одна не про
порядок. Заведи мутации порядка на оба файла и предъяви их красноту текстом.
**(в) Подмножество каталога — в батарею.** Сегодня `make battery` = `build vet fmt lint test`, слова
`mutate` в `Makefile` нет: каталог доказывает, что мутация СУЩЕСТВУЕТ, а не что она ЛОВИТСЯ (строка 313).
**Ловушка, из-за которой это не однострочник:** `tmmutate` ПРАВИТ ИСХОДНИКИ и требует `-root` на КОПИИ
модуля; направить его на рабочее дерево — это порча дерева и отравленный замер (шапка инструмента говорит
об этом прямо, и один такой случай в проекте уже стоил ложного дефекта в чужом отчёте).
**Реши сам и аргументируй:** ложится ли подмножество ВНУТРЬ `battery` (тогда цель обязана сама сделать
копию модуля во временный каталог и убрать её за собой) или живёт СОСЕДНЕЙ целью, которую зовут явно.
Мерило выбора — три условия: рабочее дерево не правится НИКОГДА · выживший мутант красит выход ·
время цели названо числом (для калибровки: пакет `internal/pipeline` проходит за 15 с, замер 07.09).
Состав подмножества выбираешь ты, но в него ОБЯЗАНЫ войти твои новые записи из §4.1(в) и §4.2(б).
**Пять ловушек этой цели — каждая даёт зелёный гейт, который ничего не проверил:**
- **список `-id` руками в `Makefile` — второй носитель состава**: следующая запись каталога окажется вне
гейта молча. Состав ВЫВОДИТСЯ из каталога (префикс id, поле — форму выбери сам), а не перечисляется;
- **`expect: "survives"` — штатная дверь для подгонки**: мутант объявляется «аргументированно выжившим», и
цель зелена. Для записей ЭТОГО пака `survives` запрещён — это ровно мотив, запрещённый `D39.183`;
- **копировать надо РАБОЧЕЕ дерево** (`cp -a`), а не `git archive HEAD`: иначе гейт судит HEAD и твоих
новых пинов не видит. И убирай копию за собой на падении тоже, иначе `/tmp` копится;
- **`Package` записи каталога — это пакет ПИНА, а не пакет правки.** Инструмент гоняет ОДИН пакет. Правка
M5 живёт в `internal/chunk`, а утверждение реконструкции — в `internal/pipeline`: запись с чужим пакетом
честно скажет SURVIVED, ничего не проверив. Для каждой новой записи назови пакет, где живёт утверждение;
- **время цели — СУММА по записям**, а не время одного пакета: каждая запись = компиляция + прогон пакета
(`internal/pipeline` ≈ 17 с, `internal/chunk` ≈ 0.1 с). Назови сумму числом.
⚠ И скажи вслух в отчёте: инструмент гоняет пакет БЕЗ `-race`, то есть доказывает меньше, чем цель `test`.
**(в) и (а) — разные виды правок.** (а) меняет ПОВЕДЕНИЕ (артефакт на стопе перестаёт быть пустым), и
это заказано этим паком: тесты, описывавшие пустой артефакт, правятся как обслуживание по `D39.183`с
объявлением в отчёте. Артефакт читает платформа: если её сторона что-то ломает — это ПИНГ оркестратору, а
не правка чужой зоны.
### 4.3 Словарь классификатора: линт канонической формы + один импорт — **делай РОВНО так**
`terminology/classify.go:18` `Types`, `:26` `CandidateTypes`, `:50` `Genders`; третья копия —
`pipeline/banknote.go:80` `bankTypeOK` (не упомянут ни одним тестом); те же слова прозой в
`prompts/zh-ru/classifier.md`. Леджер меток промтов смотрит на одну пару (`pipeline/promptlabel_test.go:53`),
off-vocabulary ответ модели — только `Log.WarnContext` (`pipeline/terminologist.go:603`), отказа нет.
Замер ресёрча: подложенная `prompts/ja-ru/classifier.md` с локализованными словами классов проходит батарею
БЕЗ единого шага автора. Это прямой удар по цели №2 проекта: дежурный ревью-вопрос «заработает ли пара,
которой в репо нет, без правки Go» получает ответ «да, и молча неверно».
**Правильная форма лечения:**
-**линт живёт в `internal/pipeline`, а НЕ в `internal/config`, и это не вкусовщина.** Каноническая
форма промта (снятие комментариев + загрузка шаблона) реализована в `pipeline/render.go`
(`stripPromptComments`, `LoadPromptTemplate`), а `internal/config` пакет `internal/pipeline` не импортирует
и импортировать НЕ МОЖЕТ — цикл (замер: 0 хитов `internal/pipeline` в `go list -deps ./internal/config`
при 208 зависимостях). Линт в `config` означал бы ВТОРУЮ реализацию грамматики комментариев — ровно тот
второй носитель, который пак и снимает. Клади рядом с `pipeline/promptlabel_test.go`: он уже перебирает
shipping-конфиги и работает с каноном, и гони по всем `prompts/*/classifier.md`.
⚠ Существующий `internal/config/prompt_lint_test.go` (89 строк) остаётся жить своим предметом —
плейсхолдерами; ты его не переносишь и не ломаешь;
-**линтовать КАНОНИЧЕСКУЮ форму промта — со снятыми комментариями.** Наивная версия ложно-зелёная:
нужные слова присутствуют в HTML-шапке промта, и линт «просто грепом» пройдёт на паке, который на самом
деле сломан. ⛔ **Отрицательный кейс живёт В ТЕСТЕ** — временный пак с чужими словами создаётся самим
тестом, а не показывается разово руками: разовая демонстрация не гейт, а рассказ о гейте. Красное на
подложенном и зелёное на репо утверждаются ОДНИМ прогоном;
- `bankTypeOK` свести к `terminology.CandidateTypes` — он уже экспортирован, это одна строка импорта и
снятие второго носителя множества.
⛔ Обратная дорога — вынести словарь в данные — ЗАПРЕЩЕНА: она двигает `prompt_sha256` / `RequestHash` /
`classifierVersion` (`D39.194` п.9, `D39.195` п.5).
### 4.4 Расхождение ингеста и чанкера: пин ИЗВЕСТНОГО расхождения — **делай РОВНО так**
Страж длины живёт только в ингесте (`chunk/ingest.go:307` `chapterHeaderMaxRunes = 60`, применён `:323`;
в `chunker.go` `RuneCountInString` — 0 хитов), осмысленность номера — только в чанкере
(`chunk/chunker.go:316` `if !any || v <= 0`). Воспроизведено 07.09: `第零章:序幕` ингест режет главой, а
чанкер заголовок НЕ снимает — сырой CJK-маркер уезжает МОДЕЛИ; `parseSectionNumeral` на `零`/`0`/`` даёт
`0,false` (контроль: `一`=1, `十二`=12); 84-рунная строка — ингест `false`, чанкер `ok=true`.
**Здесь пишется ТЕСТ, но НЕ ФИКС, и причина денежная:** фикс меняет ТЕКСТ чанка ⇒ бампает
`chunkerVersion` ⇒ двигает снапшот обеих волн ⇒ перекупает книгу. Он приземлится в окне перенарезки, чужим
паком.
**Правильная форма — ДВА теста, и поодиночке ни один не годится** (оба в пакете `chunk`: предикаты не
экспортированы). ⚠ Испр. 07.09 по второму рубежу: первая редакция заказывала ОДИН тест, а один пин
расхождения — это второе описание бага, а не гарантия.
- **(A) ПАРИТЕТ — живая гарантия, зелёная и сегодня, и после фикса.** На НОРМАЛЬНЫХ формах (обычный
заголовок вида `第三章 正常标题`, номера подряд, длина в пределах стража) предикат ингеста и разбор чанкера
отвечают ОДНО И ТО ЖЕ, а книга, прогнанная `IngestEncoded``SplitChunks`, несёт непустой `Heading` на
КАЖДОЙ главе. Такого теста в репозитории нет ни одного, и именно он останется жить, когда расхождение
закроют. Без него (B) ничего не гарантирует.
- **(B) ПИН РАСХОЖДЕНИЯ — утверждать ВСЕ стороны, иначе частичный фикс пройдёт молча.** Для `第零章:序幕`:
ингест дал три главы **И** `chunks[0].Heading` пуст **И** текст чанка начинается сырым маркером **И**
`第一章` рендерится «Глава 1» при `Chapter == 2` (пользовательская половина — она и есть титульный хаос).
Для 84-рунной строки: страж ингеста говорит «не заголовок», а разбор чанкера — «заголовок, номер N».
Текст падения обязан объяснять СЛЕДУЮЩЕМУ, что произошло, в форме «расхождение, которое пинует строка
346, закрылось — сложи этот тест в паритетный».
⚠ Красный тест в `main` не ложится (`battery` включает `test`), а скип — ровно класс `D39.208` п.5: «пусть
пока падает» и `t.Skip` не годятся оба, поэтому (B) утверждает СЕГОДНЯШНЕЕ состояние и зелен.
**Строку 346 называй КОММЕНТАРИЕМ и текстом падения, а не именем функции** — конвенция репозитория
именно такая: ссылок на строки бэклога в именах тестов 0, в комментариях тест-файлов — 55 файлов из 204
(счёт 07.09).
### 4.5 Арм-конфиги: пере-снять и заставить гейт утверждать равенство — **состав РОВНО так, форма гейта — реши сам**
Все три арма объявляют себя копией боевого C1 «с ЕДИНСТВЕННЫМ изменением», а расходятся с
`configs/pipeline-c1.yaml` на 26 / 28 / 23 не-комментарных строки (замер 07.09 голым диффом; вторая
методика, со снятыми пустыми и хвостовыми комментариями, даёт 18 / 18 / 15 — популяции разные, вывод один).
Во всех трёх армах `banknote:` 0, `terminology:` 0, `contrast_path` 0, `classify_types` 0, `target_script` 0,
`prompt_override` 0 — при **ровно одном YAML-ключе каждого имени** в c1 (счёт 07.09 по `^\s*<ключ>:`;
подстрочный счёт даёт больше и для этого вопроса негоден). Третий носитель той же лжи — сам гейт:
`internal/config/echo_mine_test.go:227/234` `TestSwapArmConfigs` обещает в доке «swaps ONLY the editor
model … only the editor differs», а утверждает лишь модель редактора, `few_shot` и `rate_limit`.
**Наследования конфигов в движке НЕТ** — не ищи его и не строй: `LoadPipeline` его не знает, а `D39.23`
п.(г) (на который ссылается ресёрч) — это слой пар-КАЛИБРОВОК, а не наследование армов. Лечение —
пере-снять три файла копией c1 со сменённым редактором.
**Правильная форма гейта — и в ней вся суть:** он обязан утверждать **равенство арма боевому вне блока
редактора**, а не перечислять ключи поимённо. Перечисление — это то, что уже разъехалось молча: `b8154cd`
дал c1 +64 строки банкового контура, а армам — только `prompt_version`, и тест этого не заметил. Форму
сравнения (структурный дифф загруженных конфигов · сравнение YAML-деревьев · что-то третье) выбираешь ты,
но она обязана ловить ДОБАВЛЕННЫЙ в c1 ключ, которого в арме нет. Подсказка, чтобы ты не искал механизм:
загрузи оба конфига одним путём, приведи стадию редактора к общему виду и сравни структуры целиком —
декодер строгий, неизвестных ключей не бывает, и ключ, добавленный в c1, даёт ненулевое поле против нулевого.
**Решение, которое пак обязан от тебя получить, а не умолчать:** редактор боевого c1 СЕГОДНЯ — это
`deepseek-v4-pro` с `few_shot: false`, то есть ровно то, чем объявляет себя `pipeline-arm-deepseek-pro.yaml`.
После честного пере-снятия этот арм совпадёт с c1 целиком, а арм без разницы — не арм. Назови исход: снять
файл (тогда и строку из `TestSwapArmConfigs`, и пинг про `STACK.md`) либо оставить ЯВНЫМ алиасом с
комментарием, почему он существует. Молча оставить копию нельзя.
**Ловушка, в которую ты въедешь на первом часу, и она про МОТИВ.** Армы читают ПЯТЬ тестовых площадок
(`pipeline/promptlabel_test.go:45` · `config/echoregen_shipping_test.go:35` · `config/content_labels_test.go:529`
и `:575` · `config/prompt_pack_test.go:239`), а `TestPromptLabelsPinTheirBytes` пинит БАЙТЫ меток промтов и
чинится документированным `TM_UPDATE_PROMPT_LABELS=1`. Пере-снятый арм почти наверняка сдвинет метку: у
glm-арма сегодня НЕТ `prompt_override` переводчика, а у копии c1 он появится. ⇒ **сначала посмотри, ЧТО
именно сдвинулось, и убедись, что сдвиг — следствие заказанной правки, и только потом обновляй леджер.**
Обновить его, чтобы стало зелено, не посмотрев, — ровно запрещённый мотив (`D39.183`); обновить как
следствие заказанной смены байтов — обслуживание, и оно ОБЪЯВЛЯЕТСЯ в отчёте отдельным пунктом: какая метка
уехала и почему.
`docs/STACK.md` строка 12 — чужая зона: её правку просишь **пингом** в своём отчёте, сам не трогаешь.
### 4.6 Попутные остатки — **реши сам, бери если по пути**
Строка **349**: `pipeline/snapshotdiff.go:157-159` дописывает оператору причину, которой не проверял
(«the auto-bank grew between purchases») · `pipeline/snapshot.go:206-218` — протухшие клаузы про
«последовательный драйвер» при одном драйвере · `backend/README.md` называет `status.go` read-only
проекцией, тогда как `status.go:1174` внутри `Runner.Redrive` зовёт `Store.ResetChunkStages` — единственный
деструктивный путь движка. Берёшь, если файл и так открыт; не берёшь — так и пишешь, строка остаётся жить.
Строка **350** — санкционированный владельцем рефакторинг облегчения ведения, и он тоже попутный.
**Правило здесь одно и оно жёсткое: РЕШИТЬ, а не удалить.** Вызовов вне определения ноль у шести функций
(`obs.SafeGo` · `text.TokenizeCyrillic` · `CheapGateResult.UnitScaleHard`/`UnitScaleSoft` · `Bank.Voices()` ·
`Bank.Pairs()`), но четыре из них стоят НАМЕРЕННО: `UnitScaleHard/Soft` — поверхность контракта Р2, ждущая
строку 12 (`checks/cheapgates.go:155-156` так и написано); `TokenizeCyrillic` — образец паттерна
value-default, на который ссылаются `checks/sanitizer.go:21` и `:197`; `SafeGo` — намеренный контраст к
`obs/panic.go`, названный решением `pipeline/waverun.go:405`; `Voices`/`Pairs` ждут потребителя, которого
держит строка 13б. ⇒ допустимые исходы по каждой ДВА: удалить с доводом (и тогда снять осиротевшие
упоминания — иначе получишь имя в комментарии, которое ни во что не разрешается, класс строки 299) ЛИБО
назвать причину жизни комментарием на месте. Молча удалить — нельзя. Сюда же — «последняя стадия» дважды
одинаковым инлайном (`pipeline/export.go` и `quality.go`) при готовом `finalStageWave()`
(`pipeline/snapshot.go:247`; вызовов `.finalStageWave()`**13**: 10 в прод-коде и 3 в тестах, счёт 07.09): это чистое облегчение ведения и берётся без решений.
### 4.7 Чего в паке НЕТ — **не делай**, и это ОБЪЯВЛЕННЫЕ сужения
- **Фикс расхождения `第零章`** (§4.4) — только тест. Фикс двигает `chunkerVersion`.
- **Ось «обязательность юнита»** и не-CJK разрез (строка 303) — вход другого пака.
- **Переписывание `Runner` и дробление `internal/pipeline`** — ЗАПРЕЩЕНЫ `D39.224` §7 (база —
`12-go-style-notes.md` §1 и `D39.23`). ⚠ **Новый детектор границ — не запрет, а УСЛОВИЕ** той же ноты:
он не строится, пока не назван счёт за бамп `chunkerVersion` (100 % обеих волн). В этом паке он вне
скоупа в любом случае, но силу пункта не путай — нота специально разводит эти две категории.
- **Всё, что двигает снапшот, хеш запроса, ключ манифеста или версию встроенных данных.** Единственная
заказанная смена поведения — §4.2(а).
- **Платные вызовы** — ноль, см. §Деньги.
## 5. Мандат самопроверки ИСПОЛНЕНИЕМ — «перечитал сам» не считается
Твоя работа — про гейты, поэтому и проверяется она гейтами, а не чтением:
1. **Каждая новая гарантия предъявляется МУТАЦИЕЙ.** Для каждого утверждения, которое ты добавил: посади
правку, ломающую именно его, и покажи КРАСНОТУ **с текстом падения**. ⛔ Текст читается: говорит ли он
про сломанное тобой. Правый вердикт по неправой причине — дыра, а не поимка.
2. **Мутации гоняются ТОЛЬКО на копии дерева** (`cp -a backend <копия>`, `-root <копия>`). Рабочее дерево
не правится никогда, в том числе «на секунду».
3. **Мутируй и СВОИ новые фикстуры**, не только боевой код: фикстура, которую ты только что написал и
считаешь сильной, — типовое место, где прячется второе вырождение (`D39.208` п.5).
4. **BASELINE НА ВХОДЕ, снят оркестратором 07.09 на `4d0b68e`** — чтобы ты отличал СВОЮ поломку от
унаследованной: `go test ./... -count=1` даёт **19 `ok` · 0 FAIL · 4 пакета «no test files»**
(`cmd/tmmutate`, `cmd/tmvet`, `chunk/chunktest`, `internal/standdata`), пакет `internal/pipeline` — 17.2 с.
Красное на входе — это находка и пинг, а не твоя работа.
5. **Батарея — полным списком пакетов.** `make battery` целиком, и в отчёте — число `ok`, число `FAIL` и
ПОИМЁННО скипы (батарея сама их печатает). «20 из 21 и FAIL в хвосте» — не зелень.
6. **Субагенты РАЗРЕШЕНЫ ЯВНО** — и на охват (пере-грепнуть швы, которые ты закрыл, на второй носитель), и
на адверсариальный проход по твоей ГОТОВОЙ работе. Глубину и веер выбираешь сам; направление задано:
уязвимее всего здесь — (1) невырожденность твоих новых фикстур, (2) то, что мутация красит по НУЖНОЙ
причине, (3) форма гейта равенства армов (перечисление вместо равенства вернёт дефект молча).
7.**СНАПШОТ-НЕЙТРАЛЬНОСТЬ §4.2(а) ДОКАЗЫВАЕТСЯ ИСПОЛНЕНИЕМ, а не формой правки.** §1 обещает, что пак
не двигает ни байта снапшота, и §4.2(а) — единственное место, где это можно нарушить. Прецедент, как
такое предъявляют в этом проекте, стоит в самом боевом конфиге (`configs/pipeline-c1.yaml`, греп
«Флип НЕ двигает снапшот»): **снапшоты обеих волн байт-равны с правкой и без.** Сделай так же: сними
`SnapshotID` (или сам payload) до и после своей правки и покажи равенство. Если равенства нет — это СТОП
и пинг, а не «зато артефакт непустой».
8. **Проверяемый артефакт** — таблица мутаций в отчёте: `мутация → где посажена → красный тест → ТЕКСТ
падения`, и отдельной строкой **выжившие**, если такие есть.
## 6. Оси ревью — характер «Код»
Тест утверждает не то, что думает (`D39.171`): часы как способ синхронизации там, где предмет утверждения
не время, и ассерт по подстроке в буфере, куда пишет кто-то ещё. Обе формы дают и ложное красное, и
**тихо-зелёное** — а оно дороже: зелёное, не проверившее ничего, не перечитывает никто. Плюс твоя третья
ось, специфичная для пака: **вырожденная фикстура** — две различные величины, совпавшие случайно.
## 7. Записка-план ДО работы
До первой правки — короткая записка в отчёт: что берёшь, в каком порядке, что считаешь рискованным.
После работы — сверка комплектности против §4 МЕХАНИЧЕСКИ (таблицей), не перечитыванием.
## 8. Заявление = команда
Каждое число и каждая категорика в отчёте идут с командой, которой они получены. «Батарея зелёная» без
команды — не факт. Отрицательный замер («такого нет») печатается ВМЕСТЕ с контрольной величиной,
доказывающей, что вопрос задан существующему предмету: «записей о порядке: 0 при 211 в каталоге».
Сверх этого, и это про КОД:
- **Перед отчётом сверь каждый клейм с результатом инструмента ЭТОЙ сессии**, а не с тем, что помнишь из
ресёрча или из этого промта. Оба могли устареть: `backend/` уехал на 18 файлов уже после замера ресёрча.
- **Дифф `^func Test` — исполнением, не памятью:** сколько тестов было и стало, командой.
- **Последний абзац отчёта оказался планом или обещанием? Сделай это СЕЙЧАС**, а не пиши о намерении.
- ⛔ **Править или удалять тест/голден/гейт, чтобы он прошёл, — НЕДОПУСТИМО.** Единственное исключение —
правка, вызванная ЗАКАЗАННОЙ сменой поведения (§4.2а), и она объявляется отдельным пунктом отчёта.
- **Сессия длинная — поставь себе интервальную самоверификацию субагентом** против ЯВНЫХ критериев §13,
а не «когда покажется, что пора».
## 9. Эхо-протокол старта
ДО работы — ≤10 строк: скоуп своими словами · инварианты, которые не имеешь права сломать · что НЕ делаешь.
Первым действием отправь эхо оркестратору по адресу из прямого канала (§12). ⚠ Канала нет (файла нет,
роли нет, имя не отвечает)? Тогда эхо НИКУДА не отправляется и не ищется адрес — оно идёт первой секцией
твоего отчёта в журнале, и работа продолжается.
## 10. Что НЕ удалось — обязательная секция отчёта
Что не вышло, что не проверено, где ты сомневаешься. Пустая секция при непустой работе — сама по себе
находка; «не проверено» и «опровергнуто» — РАЗНЫЕ исходы, не смешивай.
## 11. Канал вопросов и право сказать «этого делать не надо»
Конфликт промта с кодом или доками — **пинг, а не интерпретация**: промт мог устареть, и в этом проекте
это случается. Если считаешь, что какой-то пункт делать не надо, — скажи с доводом; такие возражения ценнее
послушного исполнения. Решение владельца требуется, если работа упрётся в деньги, скоуп или продукт.
## 12. Прямой канал
Механизм — `CLAUDE.md` §«Связь между сессиями». Впиши СВОЙ блок в `/tmp/textmachine-channel` первым делом,
чужие блоки не трогай; адреса других ролей — там же. ⚠ Адрес из файла не доказывает, что сессия жива:
перед отправкой сверься с `ListAgents`. Нужной роли нет или имя не отвечает ⇒ **канала нет, и это НОРМА:
не опрашивай сессии подряд** — вопрос секцией в отчёт, работа продолжается.
## 13. Критерий завершённости — по нему тебя примут
Работу доводишь до конца САМ; приёмка идёт последней и вторым кругом твоей самопроверки не служит.
Готово — это когда:
- у КАЖДОГО пункта §4 есть исход: сделано · не делаю с доводом · пинг;
- круги СОШЛИСЬ: последний проход не дал НОВЫХ находок, а прежние закрыты таблицей
«находка → что сделано → ЧЕМ ПРЕДЪЯВЛЕНО»;
- таблица мутаций полная, выжившие названы;
- числа сняты ПОСЛЕ последней правки, а не вспомнены;
- всё живое — в ДЕРЕВЕ, а не в письме;
- сказано явно: **«работа завершена, править не планирую»**. Без этой фразы пак считается идущим.
## Деньги
**Пак $0.** Ни одного платного вызова: ни одной генерации, ни одного обращения к провайдеру. Если тебе
кажется, что задача требует платного прогона, — это стоп и пинг, а не вызов.
## Отчёт
Секция **«Бэкенд»** в `docs/PROGRESS.md`: что сделано по каждому пункту §4 · таблица мутаций · таблица
«находка → что сделано → чем предъявлено» · объявленные правки тестов по `D39.183` · пинги в чужие зоны
(`docs/STACK.md`, платформенная половина строки 224) · §10 «что не удалось» · вопросы оркестратору.