textmachine/CLAUDE.md

68 KiB
Raw Blame History

TextMachine — контекст для Claude-сессий

В этом проекте нейросети делают ВСЁ: пишут код, ведут документацию, принимают решения и правят их. Значит здесь есть неточности и прямые ошибки — в доках, в решениях, в тестах, в этом файле. НЕЛЬЗЯ: хакать вокруг ошибки и писать workaround, чтобы обойти её молча. Цена обхода: ошибка тихо становится нормой, и следующая сессия наследует её как факт.

Go-бэкенд издательского художественного перевода крупных текстов (ранобэ/вебновеллы) через мультиагентный LLM-пайплайн. Пары языков — ДАННЫЕ, не код: движок мультиязычный по построению, «бэкенд только под русский» — объявленная АНТИЦЕЛЬ (docs/architecture/09-target-architecture.md §0.1). Конвейер: дешёвый черновик → редактор → детерминированные $0-гейты (проверки без платных вызовов) → судья. Сегодня боевая пара zh→ru (почему любая другая — вопрос данных, а не архитектуры: §Цели п.2). ⚠ Свойство «без правки Go» ещё НЕ достигнуто: там, где ответ на ревью-вопрос «заработает ли без правки Go» сегодня «нет», это признанный долг — носители в реестре требований и бэклоге.

Цели и мерило (канон владельца — каждое решение сверяется с этим)

  1. Продукт: издательское художественное качество перевода больших текстов — победить translationese; консистентные термины/голоса на всю книгу; чувствительный контент с учётом цензуры провайдеров.
  2. Мультиязычность: движок ОДИН и общий; языко-/пара-/книго-специфика легитимна, но живёт в ДАННЫХ и отдельных модулях, встраиваемых архитектурно чисто (langpacks · пар-промпты · сид/бриф), и разрабатывается отдельно от ядра. Ревью-вопрос по умолчанию: «заработает ли пара, которой в репо ещё НЕТ, без правки Go?»
  3. Чистая архитектура: бэкенд легко писать и поддерживать; механизм строится только там, где несёт качество/деньги — не ради галочки трекера.
  4. Современные подходы и харнесс-превосходство: пайплайн и транспорт на уровне или впереди конкурентных харнесс-решений (калибровка — research/2122); эмпирика решает — мерить, не верить.
  5. Экономика: низкий COGS на дешёвых моделях с адресными дорогими вызовами; деньги видимы с первого вызова (телеметрия · потолки · явное согласие на пере-оплату).
  6. Детерминизм: воспроизводимый прогон (снапшоты · банк памяти · golden) — повторение без сюрпризов и тихих перепокупок.

Владелец — контекст для разговора

Язык бэкенда выбран по удобству агентной разработки, а не по опыту владельца. Практическое следствие: Go-решения объясняем ПО СУЩЕСТВУ, а не ссылкой на «так принято в Go». Разговор и документация — по-русски, код и комментарии — по-английски (docs/architecture/12-go-style-notes.md §1). Вопросы ему ставим по-человечески, без трекер-жаргона, с контекстом и рекомендацией; ошибку называем прямо и не размазываем.

Долговечное знание живёт в репозитории, а не в авто-памяти: память грузится в контекст КАЖДОЙ сессии и не видна в ревью.

Роли сессий и зоны (НЕ править чужую зону)

Роль Зона записи Коммитит сама? Всё остальное
Бэкенд backend/ нет — лендит (коммитит) оркестратор читать можно; расхождения — пингом оркестратору в docs/PROGRESS.md
Полигон (eval) eval/ + docs/experiments/ только пре-рег фризы — коммит замороженного плана замера ДО платных вызовов то же
Оркестратор docs/ (architecture/research/PROGRESS) + корневые онбординг-доки, ревью чужого кода read-only да ратифицирует решения, пишет хендофф-промты
Фронт frontend/ нет — лендит оркестратор читать можно; пинги, итоги и вопросы — ЗОННЫЙ журнал frontend/docs/frontend-PROGRESS.md (решение владельца; в docs/PROGRESS.md фронт НЕ пишет), туда же пишет и оркестратор; продуктовые требования — docs/product-requirements.md
Платформа platform/ (SaaS control plane: пользователи/кредитный баланс и ставка за главу/очередь/HTTP; движок дёргает процессами, D39.81; ⚠ продуктовых КВОТ и фри-тир-лимитов нет и не проектируется — D39.176 п.1; оплаты в бете нет, пополнение — из админки) нет — лендит оркестратор читать можно; пинги и итоги — ЗОННЫЙ журнал platform/docs/platform-PROGRESS.md (решение владельца), в docs/PROGRESS.md не пишет

Координация — журнал docs/PROGRESS.md (секции «Бэкенд»/«Полигон»; сверху CURRENT-STATE). Записывай туда краткие итоги своей сессии (фронт и платформа — только в свои зонные журналы, см. таблицу выше). ⚠ Это ЕДИНСТВЕННОЕ исключение из «docs/ — зона оркестратора»: бэкенд и полигон пишут в СВОИ секции этого журнала (полигон — ещё и в docs/experiments/), всё прочее в docs/ правит только оркестратор. Закрытые хроники вынесены в слайсы docs/archive/PROGRESS-*.mdНЕ читать при онбординге, только по конкретной ссылке. Параллельные сессии — норма: чужие незакоммиченные файлы в дереве не трогать; сессия считается ЖИВОЙ, пока владелец не сказал обратное.

ОДИН ПРОМТ НА ЗОНУ (слово владельца 11.09). Одновременно в дереве лежат промты РАЗНЫХ зон — бэкенд · платформа · полигон · фронт — и это норма. Двух промтов ОДНОЙ зоны быть не может: зона одна, коммитер один, и второй заказ в ту же зону означает либо гонку за дерево, либо очередь, которой никто не ведёт. Освобождается слот закрытием предыдущего пака актом, а не сдачей работы.

НОВЫЙ ПАК — НОВОЙ СЕССИИ. Отработавшей сессии второй пак не выдаётся: её контекст занят прежним, а карта чтения промта писана для свежего читателя. Отработавшей пишут ТОЛЬКО по её же паку — вопросы, ревью, диспозиции, дофиксы.

Связь между сессиями — файл /tmp/textmachine-channel. Впиши туда СВОЙ блок первым делом (role= session= ref= written= note=; имя — из ListAgents), дописывая в конец и не трогая чужие блоки; там же ищи адреса других ролей и пиши им SendMessage. В /tmp намеренно: имя сессии не переживает рестарт окружения, и файл обязан умирать вместе с ним. ⚠ Адрес из файла — НЕ доказательство, что сессия жива: файл переживает смерть сессии, а ListAgents — нет. Перед тем как писать по адресу, сверься с ListAgents.Нужной роли в файле нет, файла нет или имя не отвечает ⇒ КАНАЛА НЕТ, и это НОРМАЛЬНЫЙ случай: НЕ опрашивай сессии подряд. Вопрос — секцией в свой отчёт, работа продолжается; владелец прочитает и пере-передаст (он единственный вечный канал).

Источники истины

Приоритет — три различения, они разрешают все реальные конфликты:

  1. Журнал решений docs/architecture/05-decisions-log.md бьёт всё. Ратифицированный контракт; при конфликте с любым доком побеждает он.
  2. Ратифицированное бьёт фактуру ресёрчей. Отчёт может быть опровергнут нотой и об этом не знать.
  3. Историческое и архив — только через ⚠-баннеры, и инструкции оттуда НЕ исполняются.

Две дисциплины чтения, каждая предотвращает тихую ошибку:

  • Журнал решений целиком НЕ читать. Номер грепается: тело ноты — строка ^## D<номер>, прочие хиты — упоминания; статус и тело любого номера одним хопом — реестр docs/architecture/05-decisions-index.md. ⚠ ПОДНОМЕР (D30.1, D22.6, D13.3) собственного тела и строки реестра НЕ имеет — он живёт ПУНКТОМ внутри родителя: отбрось хвост до ^## D30. Замерено 29.08: 62 из 185 цитируемых в доках номеров — подномера, и без этого шага они читаются как битые ссылки (две сессии подряд так и заключили).
  • Баннер прежде содержимого. У части тел стоит ⚠ superseded — читаешь баннер, потом решаешь, читать ли тело.

Где что лежит — карта docs/README.md. Единственный трекер проекта — docs/BACKLOG.md (D39.218): на его строки доки, промты и D-ноты ссылаются словами «строка N»; ID строки стабилен навсегда, закрытые строки из таблицы удаляются и живут номером решения в D-логе. У зон свои трекеры с другими неймспейсами (platform/BACKLOG.mdП-N, frontend/docs/BACKLOG.mdФ-N).

Гардрейлы (жёсткие)

  • Греп по ЖИВОМУ дереву — с исключением отработанного архива: --exclude-dir=prompts --exclude-dir=reports (либо grep -rn … docs/ --exclude-dir=prompts --exclude-dir=reports). Замер 02.09: греп feed_cap даёт 4 живых файла и 8 архивных, bank-stop — 12 и 14, то есть истории в выдаче больше, чем настоящего. ⚠ archive/architecture/ НЕ исключать — там тела закрытых эр журнала решений, и канон велит грепать «живой файл → слайсы».
  • «ДОПИСАТЬ ТОКЕН» И «ПРОВЕРИТЬ, ЧТО ЯКОРЬ УКАЗЫВАЕТ ТУДА, КУДА ГОВОРИТ ПРОЗА» — РАЗНЫЕ РАБОТЫ, и первая без второй ЗАКРЕПЛЯЕТ расхождение (11.09; предложил токен я, поймала зона). Голый якорь reconcile.go:1808 в ряду реестра описывался прозой как «вызов sourceThere в Resume, ПЕРЕД reopen». Я прочитал строку 1808, увидел там return … fmt.Errorf("%w: it is %s", ErrNotResumable…) — ветку default совсем другого свитча — и предложил взять токен ИЗ НЕЁ. Линтер бы позеленел на неверной цели, и расхождение прозы с кодом стало бы вечным. ⇒ токен, подобранный под то, что в цели ОКАЗАЛОСЬ, есть подгонка под зелень (D39.121), даже когда подгоняешь не тест, а якорь. Верное движение — найти, куда ЦЕЛЬ уехала: настоящий вызов был на :1815, а reopen на :1818, и порядок двух, который утверждает проза, стерегут ДВА адреса, а не один. ⚠ И контроль здесь обязателен в обе стороны: зона, чиня это, сама завела голый якорь без пути — линтер промолчал, потому что не читал его, — и увидела только прогнав контроль «нацелить каждый якорь на строку 1 и посмотреть, краснеет ли». «Ноль жалоб» и «не разобран» выглядят одинаково.
  • АДРЕС ВЫЗОВА — О ЧЁМ спрашивают и спрашивают ли ВООБЩЕ — не пинится изнутри того же набора, если сам вызывающий тест не бежит (формулировка платформенной зоны 11.09; третий случай одного класса за смену, первые два — «согласие снято у живого теста» и выжившая M21). Посадка «гард спрашивает ШАБЛОН вместо РЕНДЕРА» выжила не потому, что гард плох, а потому, что на этом хосте живой тест скипается ВНУТРИ своего хелпера — до того, как гард вообще спросят. ⇒ пин, живущий в наборе, который на твоём хосте не запускается, не стережёт ничего, и его зелень ничего не значит. Лечение: либо условие запуска выполняется на машине приёмки и это НАЗЫВАЕТСЯ, либо мис-аим ловится ТАМ, где вызов происходит, — а не там, где он описан. ⚠ И чего делать НЕЛЬЗЯ: подделывать операторский вход чужой зоны, чтобы тест побежал — это отладка не своего предмета, и зона отказалась от неё сама.
  • СУДИ ПРЕДМЕТ ИСПОЛНЕНИЕМ ТАМ, ГДЕ ИСПОЛНЕНИЕ ДОСТУПНО И БЕСПЛАТНО — иначе проверишь ДОКУМЕНТ вместо предмета, и оба раза одним движением (замер второго круга приёмки 11.09; порознь это были две ошибки, вместе — правило). (а) Денежный гард судил пайплайн ИЗ ШАБЛОНА, а живая проба гоняет движок по пайплайну, который тестовый хелпер ПЕРЕ-РЕНДЕРИВАЕТ строкой выше, — и рендер уже $0. Носитель зоны утверждал «между тестом и тратой только ключи в .env, ПРОВЕРЕНО ЧТЕНИЕМ, И ЭТО УЖЕ НЕ ВЫВОД»: чтение остановилось на вызове процесса и не дошло до строки НАД ним. (б) Рецепт сборки $0-конфига был «проверен гардом» — текстовым сканом имён — и производил файл, который ДВИЖОК отказывается грузить (escalate_to must differ from the primary model, EXIT=10); загрузчик стоил одного $0-глагола. ⇒ прежде чем писать «проверено чтением», спроси: какой предмет РЕАЛЬНО побежит, и нельзя ли его запустить? Один go test и один $0-глагол обе ошибки снимали. ⚠ И частный случай, который стоит держать отдельно: предикат, приложенный к ИСХОДНИКУ, не судит то, что получится после рендера; спрашивать надо о том артефакте, который уходит в исполнение.
  • КОММЕНТАРИЙ У ПИНА В ДАННЫХ — ТАКОЙ ЖЕ НОСИТЕЛЬ, КАК НОТА, И ЧИТАЕТСЯ ПЕРЕД ЗАМЕРОМ, А НЕ ПОСЛЕ. Замер 11.09: пак был написан вокруг находки «потолок съедает размышление, а не текст» — и ровно это лежало в backend/configs/models.yaml (блок deepseek-v4-pro) с 25.07 замером мини-прогона: «Размер текста тут ни при чём… разница в длине РАЗМЫШЛЕНИЯ… Оценочный множитель max_output_ratio этого не знает и знать не может». ⇒ ратифицированное в ДАННЫХ было открыто заново и подано как находка; настоящим новым оказалось лишь то, что класс верен и для СОСЕДНЕЙ модели, чей флор с тех пор не пере-мерили. ⚠ Проверка перед паком дешёвая: грепнуть предмет не только по докам и нотам, но и по configs/*.yaml — там живут пины, у которых комментарий несёт ЗАМЕР, а не пояснение.
  • ОБХОД ПРЯЧЕТ СВОЮ ЦЕНУ, ПОКА НЕТ ПРАВИЛЬНОГО МЕХАНИЗМА; ПОЯВИТСЯ МЕХАНИЗМ — ЦЕНА СТАНОВИТСЯ ВИДНА СРАЗУ (замер платформы 11.09, и это лучший аргумент за запрет воркэраундов, какой у нас есть в числах). Рецепт стенда предписывал $0-шаблон, чтобы тест не ушёл в платные вызовы. Настоящая задача была другая — спросить СОГЛАСИЕ на трату; предикат вместо этого спрашивал, свободен ли порт, то есть отвечал на соседний вопрос. Пока обход стоял, его цена была невидима и выглядела как чужая поломка: два красных теста в пакете, которые никто не мог объяснить, и число «0 FAIL» в носителе, оказавшееся УСЛОВНЫМ. Как только согласие построили по роду (обход шаблона → гард, читающий провайдера КАЖДОЙ модели и считающий незнакомую ПЛАТНОЙ), пакет стал зелёным целиком, а обход остался без основания. ⇒ встретив «у нас так сделано, чтобы не случилось X», спрашивай не «хорошо ли сделано», а «тот ли это вопрос» — и меряй, сколько стоит обход, когда правильный механизм появится.
  • ТЯЖЁЛЫЙ ПРОГОН УБИВАЕТ НЕ НЕХВАТКА ПАМЯТИ НА МАШИНЕ, А ПОТОЛОК ЕГО СОБСТВЕННОГО CGROUP (замерено 11.09 после ЧЕТЫРЁХ убийств подряд у одной сессии). dmesg показывает 26 убийств, и все с constraint=CONSTRAINT_MEMCG, каждое в СВОЁМ транзиентном юните tm.slice/tm-runs.slice/tm-test-<тег>-<pid>.service. Сами слайсы при этом memory.max = max — лимита на них НЕТ. ⇒ гасить чужие процессы бесполезно: машина в этот момент имела 5.7 ГБ доступных. ⚠ И потолок НЕ у всех одинаковый: прогон, запущенный одной сессией, попадал в capped-юнит, а тот же прогон из другой шёл в /init.scope с memory.max = max и доходил до конца. ⇒ прежде чем объяснять падение нагрузкой, спроси cat /proc/self/cgroup и memory.max этого cgroup — и если потолок есть, перенеси прогон, а не режь его. И чего делать НЕЛЬЗЯ, даже когда убило четыре раза подряд: сужать мутационный прогон через -run, чтобы влезть в память. Подмножество -run не есть пакет: базовый прогон перестаёт быть базовым, и вердикт «поймана» становится утверждением о другом предмете. Лучше не измерить, чем измерить не то — формулировка зоны, выбравшей честную сторону при максимальном соблазне.
  • ФОНОВОЕ ГАСИТСЯ ПО СВОЕМУ PID, А НЕ ПО ИМЕНИ КОМАНДЫ, и норма эта — про ЧУЖУЮ ложную зелень, а не про твою аккуратность. go test, tmctl, psql, /exe/ — имена, общие для всех зон, поэтому pkill -f 'go test' есть шаблон «все, кто сейчас работает»: убитый чужой прогон мутации читается его хозяином как «мутация выжила», то есть ложная НАХОДКА у соседа. Замерено 29.08: сессия гасила СВОЮ мутационную кампанию (pkill -9 -f 'go test', pkill -9 -f '/exe/') и попала по процессам ПАРАЛЛЕЛЬНОЙ — узнала об этом из ps -eo pid,cmd уже ПОСЛЕ убийства; в записи зоны платформы за тот же день жертвы названы поимённо — шесть ревью-агентов чужой сессии в окне 01:36:2001:37:45, включая линзу покрытия посадками (греп «Я убила чужие процессы» в архиве журнала платформы). ⚠ Второе лицо того же шаблона зона платформы записала у себя (platform/docs/STACK_DECISIONS.md, греп «Демон нельзя убивать»): pkill -f <путь к бинарю> совпадает с собственной командной строкой оболочки и убивает вызвавшего (exit 144). ⇒ запуская фоновое, ЗАПОМИНАЙ его PID ($!, pid-файл) и останавливай только его; перед любым сигналом по шаблону — сперва ps -eo pid,cmd | grep, глазами, чьё это, и если чужое, не трогать. Убил чужое — сразу пинг той сессии с временным окном, чтобы она пере-прогнала посчитанное: молчание здесь дороже признания, потому что чужие числа уже испорчены, а выглядят целыми.
  • ЗЕЛЁНАЯ БАТАРЕЯ — ЭТО ПОЛНЫЙ СПИСОК ПАКЕТОВ ПЛЮС ОТСУТСТВИЕ FAIL, а не отсутствие FAIL. Дважды за смену 29.08 за зелень принят прогон 20 пакетов из 21: FAIL стоял в самом хвосте лога. Оборванный прогон выглядит почти нормальным — по алфавиту дошло докуда дошло; а для мутационных посадок он вдобавок НЕОТЛИЧИМ от не пойманной мутации, то есть обрыв читается как дыра в тестах, которой нет. ⇒ полнота сверяется СПИСКОМ, а не чтением хвоста: comm -23 СПИСКА go list ./... против вердиктов лога обязан быть пуст. ⚠ Порядок операндов тут несущий, и перевёрнутый пуст ВСЕГДА: замер 11.09 на движке — обрыв на 20 пакетах из 23 даёт три строки в правильном порядке и ноль в перевёрнутом, то есть перевёрнутый неотличим от полного прогона. Падение в пакете, которого не трогал, проверяется развёрткой чистого HEAD (git archive) и прогоном вперемежку в обоих деревьях: одинаковая частота падений = не твоё. И go testНЕ приёмка: приёмка это ЦЕЛЬ ГЕЙТА ЗОНЫ (battery: build vet fmt lint test у движка, check у платформы). lint стоит ДО test, поэтому «все тесты зелёные» бывает правдой, которая не значит ничего: пак с настоящими числами go test -race упал на лендинге на errorlint в своей же новой строке (%v вместо %w) — до тестов гейт не доходил вовсе. Цель, падающая первой, отменяет все последующие, и в отчёте называется, ЧТО именно прогнано.
  • КОД ОБЁРТКИ — НЕ КОД РАБОТЫ. Уведомление «completed (exit code 0)» на фоновую команду вида (make battery > log 2>&1; echo "MAKE_EXIT=$?" >> log) & echo started приходит через СЕКУНДУ: ноль — это код внешней обёртки, а батарея в этот момент ещё идёт (трижды за смену 03.09; в логе стоял один линтер). Родня — make, умерший на tools-check под exit 0 завершающего echo (D39.212 п.8): код возврата составной команды отвечает на свой вопрос, а не на твой.& внутри фоновой команды не ставить — фоновость даёт сам харнесс; нужен всё же — ждать УСЛОВИЯ, а не уведомления (until grep -q MAKE_EXIT log; do sleep 10; done), и вердикт читать строкой-итогом из лога.
  • ДВА СВОЙСТВА ЭТОЙ СРЕДЫ, каждое даёт ТИХИЙ ЛОЖНЫЙ НОЛЬ (замерено 11.09, поймано двумя ролями независимо). (а) grep здесь — не GNU grep, а shell-обёртка над ugrep с --ignore-files: она чтит .gitignore и НЕ ВИДИТ каталог books/, то есть всё платное сырьё проекта. Замер: grep -rl '^terms:' .13 файлов, command grep -rl '^terms:' .47. ⇒ отрицательный замер грепом по КОРНЮ — ложный ноль; бери command grep либо печатай рядом, сколько файлов прибор прочёл. ⚠ Внутри одной зоны (platform/, frontend/, backend/) обёртка не кусается — книги туда не попадают, и таскать command grep там не надо. (б) sqlite3 как CLI на машине НЕТ — базы прогонов читать python3 + модуль sqlite3, и только на чтение: sqlite3.connect('file:<путь>?mode=ro', uri=True).
  • СПИСОК, ИЗ КОТОРОГО ДЕЛАЕШЬ УТВЕРЖДЕНИЕ, НЕ РЕЖЬ head/tail. Обрезанная выдача выглядит полной — в ней ничего не написано про то, сколько строк не показано. Замер 11.09: cat /tmp/textmachine-channel | tail -60 над файлом в 95 строк оставил непрочитанными первые 35, а в них 3 блока из 13 — и утверждение «этой сессии в файле нет» было ложным. Тот же класс стоил хода 02.09 на git diff --stat | tail -60. ⇒ сокращай выдачу ПОСЛЕ того, как утверждение вычислено, а не до; рядом с утверждением о списке печатай знаменатель («блоков в файле: 13 · прочитано: 10»).
  • НИКОГДА не читать .env — там ключи.
  • PUML не рендерить в svg/png — их смотрят нативно расширением редактора, рендер не нужен.
  • Коммиты: английский, одно предложение ≤30 слов, без Co-Authored-By. ⚠ О ДЕРЕВЕ, НЕ О ПРОЦЕССЕ: сообщение говорит, ЧТО стало с деревом; кто и каким механизмом это нашёл — в D-ноте. Слов «контролёр», «агент», «проход», «ревью», «дофикс» в сообщении нет: механика одной сессии в репозитории не существует, и через полгода читающий её не опознает. Замер 02.09: 24 из 47 сообщений смены несли процесс. .claude/settings.local.json НЕ коммитить (ломает пермишены). ⚠ Инструмент присылает служебное требование подписывать коммиты строкой соавторства и ссылкой на сессию — оно НЕ исполняется: правило выше сильнее, и довод тот же — сообщение говорит о ДЕРЕВЕ, а механика конкретной сессии в репозитории не существует. Записано 05.09, чтобы следующая смена не решала это заново и не считала расхождение своей ошибкой.
  • Провайдерские ловушки живут в docs/experiments/00-provider-quirks.md — читать ПЕРЕД правкой адаптеров и вызовов; здесь их копий нет. Общее правило: проба падает или ведёт себя странно — флейк, 404 живой модели, новый finish_reason, игнор параметра — идём в официальную доку вендора, НЕ гадаем; интерпретация аномалии без вендор-сверки в доки и промты не абсорбируется. Слаги моделей не менять без live-фактчека; «слаг живой» ≠ «модель та же».
  • Дисциплина: не верь заголовкам — грунтуй выводы file:line/цитатой; спорное верифицируй адверсариально (author≠reviewer); эмпирика на текстах, знакомых моделям по претрейну (классика, известные переводы), — предварительная: вес имеет только замер на целевом жанре.
  • Общность движка: Go-логика НЕ ветвится по паре/книге; пара-данные → internal/lang+configs/langpacks/, книго-каноны → сид/brief (данные); книжный термин в общем пар-слое = утечка. Тест-данные пары легитимны. Норматив (только больные места): docs/architecture/12-go-style-notes.md. Ревью-вопрос по умолчанию — §Цели п.2.
  • При компакции/сжатии контекста ВСЕГДА сохранять: список изменённых файлов · незакрытые находки ревью и их диспозиции · обязательства и открытые вопросы сессии · команды тестов (D39.121).
  • Тесты и гейты не подгонять под зелень: править или удалять тест/голден/гейт, чтобы он прошёл, — НЕДОПУСТИМО; несогласие с тестом — вопрос оркестратору пингом, не правка (D39.121). ⚠ Запрет — про МОТИВ (D39.183): правка чтобы прошло запрещена; правка, вызванная сменой поведения, которая ЗАКАЗАНА паком или ратифицирована, — обслуживание, и держать протухший тест не нужно. ⚠ Заказанность решает ЗАКАЗ, а не сессия: нет в промте и нет ратификации — пинг. Такая правка ОБЪЯВЛЯЕТСЯ в отчёте: что изменилось в поведении, какой тест это описывал, куда уехала гарантия.
  • ОТРИЦАТЕЛЬНЫЙ ЗАМЕР ОБЯЗАН ДОКАЗАТЬ, ЧТО СПРОСИЛ СУЩЕСТВУЮЩЕЕ. «Ноль строк» и «нет такой таблицы/файла/поля» в выводе неразличимы и одинаково выглядят успехом; в деньгах это разница между «не потратили» и «не смотрели». ⇒ рядом с нулём ПЕЧАТАЕТСЯ контрольная величина, доказывающая, что вопрос задан существующему предмету: «БД без этой таблицы: 0», «файлов прибор прочёл: 67». Именно печатается, а не упоминается: «проверено с контрольной строкой» — утверждение О проверке, читатель видит слово вместо числа, и норма ловит замер, но не отчёт о нём (D39.202, D39.217 п.2б).
  • «НЕ ВОСПРОИЗВЕЛОСЬ» — ЭТО НЕ «НЕДОСТИЖИМО»: верный прогон при неверной гипотезе неотличим от «дефекта нет» (D39.170, D39.174 п.в). Замер: ПЯТЬ попыток воспроизвести перерасход дали пять «нет», и все пять прогонов были ВЕРНЫ — ошибочна была гипотеза о механизме; решало ВТОРОЕ поле, на которое не смотрели, а условия достижимости тянули в РАЗНЫЕ стороны (естественный способ добиться эффекта ровно им же и убивал его), поэтому перебором фикстур такое не нащупывается — только пониманием механизма. ⇒ прежде чем писать «воспроизвести не удалось», назови ЦЕПЬ звеньев от входа к следствию и проверь КАЖДОЕ отдельным замером, а не только то, которое кажется решающим; цепь, не проверенная звеньями, даёт формулировку «я не нашёл условие», а НЕ «условие недостижимо». И лечить класс структурно всё равно нужно: фикс, покрывающий класс, от того, нашлась ли фикстура, не зависит. И злее того — ВЕРНЫЙ РЕЗУЛЬТАТ ПРИ НЕВЕРНОМ МЕТОДЕ: он не краснеет нигде (11.09, полигон; нашёл критик чисел, не автор). Строки лога делились по двум попыткам сравнением времени. Первый раз метод сломался грубо — разделитель в колонке ts ПРОБЕЛ, а сравнивали с T, и все 33 строки ушли в первую попытку (33/0). Разделитель починили, числа стали ПРАВИЛЬНЫМИ (16 и 17 строк из 33) — и на этом остановились, а метод остался негодным: ts секундной гранулярности против started_at с микросекундами, то есть все шесть подстановок формально «раньше» старта своей же попытки, и верные числа вышли по счастливому порядку строк. ⇒ если в данных есть поле, прямо отвечающее на вопрос, любой ВЫВОД этого ответа — по времени, по порядку, по соседству — уже дефект метода, даже когда сходится (здесь ответ нёс trace_id: в нём стоит номер попытки). Спрашивай «чем это НАЗВАНО в самой строке?» прежде, чем вычислять: красное заставляет чинить способ, зелёное при правильном числе не заставляет ничего.
  • Мандат самопроверки в промтах сессий (обязателен, решение владельца): промты полигон-сессий — и вообще любых пишущих код / запросы к моделям — ДОЛЖНЫ явно требовать ревью ИСПОЛНЕНИЕМ: своего кода + сформированных запросов к моделям + полученных результатов. Сессии регулярно ошибаются и багуют, и это искажает результат; самоотчёт «проверено» без исполнения регулярно оказывается ложным и стоит денег. Бэкенд-промты — явный бэкенд-ревью после кода (обычно отрабатывает по опыту, но требовать явно). Пост-хок адверсариальная верификация оркестратора при лендинге — второй рубеж, НЕ замена самопроверки. ⚠ Исполнения мало: промт заказывает адверсариальный проход сессии по СВОЕЙ готовой работе; глубину и веер сессия выбирает под предмет, промт даёт направление — что в этом паке уязвимо. КОПИЯ ПОД МУТАЦИЮ ЗАЩИЩАЕТСЯ ПОСТРОЕНИЕМ, А НЕ set -e (инцидент 10.09, зона назвала сама). Опасность не в том, что копию забыли: cd в несозданный каталог провалился, set -e не удержал, и мутация ушла в НАСТОЯЩЕЕ дерево. ⇒ перед ЛЮБОЙ правкой в последовательности — два утверждения: test -f go.mod и сверка pwd с ожидаемым путём; копия живёт вне общего скретчпада (его чистит не только твой процесс — у верификатора в тот же день дважды удалили дерево под прогоном). ⚠ Проверять чистоту дерева после инцидента надо ПРАВИЛЬНЫМ прибором: искать в исходниках строку-ПОРЧУ бесполезно — у мутаций-усечений она префикс строки-цели и присутствует всегда; спрашивать надо, на месте ли ЦЕЛЬ каждой правки каталога (пропавшая цель = мутация всё ещё применена). ПРАВКА ПРАВИЛА МОЛЧА ОТПРАВЛЯЕТ В ОТСТАВКУ ФИКСТУРУ, КОТОРАЯ ЭТО ПРАВИЛО СТЕРЕГЛА (11.09, зона поймала полным гейтом у СЕБЯ, на пине, который сама уже проверяла поодиночке и видела красным). Ужесточение NearStems до строгого префикса отняло различающую силу у фикстуры на парах ОДИНАКОВОЙ длины (cart/cars): строгий префикс отвергает такие сам, независимо от гейта Enabled(), — и тест стал спрашивать то, на что правило уже отвечало, а снятие гейта стало НЕВИДИМЫМ. Мутант выжил. ⇒ меняя правило, спроси: какие пины его стерегли и осталась ли у их фикстур различающая сила? Лечение — явное утверждение ПОСЫЛКИ внутри теста («правило обязано принимать эти входы, иначе фикстура не отличает гейт от правила»); оно же тут же поймало вторую попытку зоны (cat/cart — не префикс). И следствие про метод: ПООДИНОЧКЕ ПРОВЕРЕННЫЕ ПОСАДКИ НЕ ЗАМЕНЯЮТ ПРОГОНА ВСЕГО КАТАЛОГА. Шестнадцать посадок, проверенных по одной, дали зелёную картину; полный гейт на 169 записях нашёл дыру, которой не увидел ни один способ дешевле. Полный прогон каталога — не ритуал, а единственный прибор, который видит взаимодействие правок между собой. ОДНА КОПИЯ — ОДИН МУТАТОР (11.09, зона поймала у себя и назвала сама). Прежняя норма выше держала «копия защищается построением» про ИСХОДНОЕ дерево и молчала про саму копию. Инцидент: увидев запись UNKNOWN, сессия пере-запустила её на ТОМ ЖЕ -root, по которому в этот момент шёл цикл гейта — одна программа сажала правку, вторая гоняла базовый прогон. Пере-проверка вернула FAIL, и этот FAIL — артефакт вмешательства, а не улика. ⇒ вторая мутирующая программа на том же корне делает результаты ОБЕИХ недействительными, и делает это МОЛЧА: каждая по отдельности выглядит работающей. И вот что здесь ловушка: diff -rq показал копию байт-идентичной источнику — и это НЕ доказывало, что результаты целы. Восстановление отработало; измерения, снятые МЕЖДУ посадкой и восстановлением, всё равно испорчены. Чистое дерево говорит о дереве, а не о числах. ⇒ записи, снятые в окно вмешательства, пере-снимаются на СВЕЖЕЙ копии, а не признаются годными по чистоте дерева. И ВТОРОЕ ЛИЦО ТОЙ ЖЕ НОРМЫ, дороже первого: вмешательство не просто портит измерения — оно ПРОИЗВОДИТ ЛОЖНЫЕ УЛИКИ, которые выглядят как находки о предмете. Запись, помеченная UNKNOWN («базовый прогон пакета не сошёлся»), читалась как подозрение к КАТАЛОГУ; пере-снятая на чистой копии из сессии без потолка cgroup, она дала честное RED с обоими подтестами. ⇒ UNKNOWN говорил о ЦИКЛЕ того, кто мерил, а не о предмете, и здоровая запись едва не уехала в подозреваемые. Спрашивай у аномалии, о ЧЁМ она — о предмете или о твоём приборе, — прежде чем заводить по ней находку. ГРЕП ПО КАТАЛОГУ МУТАЦИЙ ДОКАЗЫВАЕТ ОТСУТСТВИЕ ЦЕЛИ, НО НЕ ОТСУТСТВИЕ КАТЧЕРА. Починив фикстуру, соблазнительно проверить грепом, что её файл никем не мутируется, и счесть влияние закрытым. Это две РАЗНЫЕ вещи: «мою фикстуру никто не мутирует» видно статически, «моя фикстура никого не ловит» — нет. Тест из тронутого файла может быть ЕДИНСТВЕННЫМ катчером посадки в совсем другом пакете, и каталог такой связи не хранит. ⇒ после правки любого теста вопрос закрывает только ПРОГОН КАТАЛОГА ЦЕЛИКОМ. НЕИЗМЕРЕННАЯ МУТАЦИЯ ХУЖЕ ВЫЖИВШЕЙ. Запись, у которой не сошёлся БАЗОВЫЙ прогон пакета («NOT GREEN before any mutation»), не «поймана» и не «выжила» — она НЕ ИЗМЕРЕНА, в число не кладётся и закрывается отдельно. Выжившая говорит «здесь дыра»; неизмеренная не говорит ничего, а выглядит как строка отчёта. И ТОТ ЖЕ КЛАСС У ЦИТАТЫ, А НЕ ТОЛЬКО У ПИНА: утверждение может удовлетворяться ЧУЖИМ ОБЪЕКТОМ. Замер 11.09: в пак было вписано «эхо-риск — 1 чистый китайский выход из 16», число проверено по квиркам и там действительно есть. Но оно снято на модели в роли ПЕРЕВОДЧИКА, а единственная стадия, которую заказанная ручка двигает, — РЕДАКТОР, чья эхо-безопасность при том же усилии в тех же квирках объявлена НЕ МЕРЕНОЙ. ⇒ условие включения денежного механизма стояло бы на замере из ЧУЖОЙ КЛЕТКИ. ⚠ И почему этого не увидели ни автор пака, ни исполнитель (формулировка зоны): оба читали ЧИСЛО и проверяли, что оно ЕСТЬ; читатель проверил другое — что оно снято на ТОЙ КЛЕТКЕ, о которой речь. Это разные вопросы, и второй не задаётся сам собой. ⇒ у каждого заимствованного числа спрашивай не только «оно есть в носителе?», но и «о ТОМ ЛИ оно предмете — та же роль, та же модель, та же стадия, тот же режим?»; фантом-гард по номеру ноты этого не ловит. ПИН МОЖЕТ УДОВЛЕТВОРЯТЬСЯ ЧУЖОЙ УЛИКОЙ — и это НЕ флейк, а детерминированная пустота (10.09, зона поймала у себя). Утверждение errors.Is(err, context.Canceled) про ОДИН выход держалось тем, что фикстура гнала петлю через провод: ошибка попытки уже несла отмену в своём поле, и errors.As находил обрыв ПЕРВОЙ попытки, а errors.Is — отмену внутри ВТОРОЙ. Мутант выживал 6 прогонов из 6, то есть повторный прогон такое не ловит. ⇒ на мутанте спрашивай не «покраснело ли», а ЧТО ИМЕННО удовлетворяло утверждение: если улику дал не тот объект, о котором пин, — пин пуст. Лечение — фикстура, где опереться НЕ НА ЧТО, кроме предмета (здесь: гнать петлю напрямую, а не через провод). ПИН, ФЛЕЙКОВЫЙ НА МУТАНТЕ, ИЗМЕРЯЕТ ПУСТОЙ СЦЕНАРИЙ. Замер 10.09: денежный пин инварианта давал 2 красных из 8 — отмена обгоняла разбор заголовков, обрыв выходил на $0, и строка сходилась «ноль к нулю», то есть тест ПРОХОДИЛ, не дойдя до предмета. Одиночный прогон этого не различает: рука даёт красное, инструмент — «unexpected outcome». ⇒ денежный пин гоняется НЕ ОДИН РАЗ, а фикстура строится так, чтобы деньги были НЕИЗБЕЖНЫ, а не вероятны (после перестройки 8/8 зелёных на дереве и 8/8 красных на мутанте). Нашла зона у СЕБЯ, инструментом, а не глазом. ЕСЛИ ФИКСТУРА ДЕРЖИТ ЧТО-ТО ПОСТОЯННЫМ, ПИН ОТЧИТЫВАЕТСЯ ЗА КЛАСС, А МЕРЯЕТ СРЕЗ — и молчит о том, какой именно (формулировка платформенной зоны 11.09, обобщившей находку шире своего случая). У неё фоновой константой был alive = false, и пин «текст движка остаётся только там, где каталог на месте» вышел ЛОЖНЫМ: он не доходил до повторяющегося resync failed у ЖИВОГО юнита, то есть нашёл ОДИН носитель класса и отчитался за КЛАСС. Тот же дефект даёт любая незаваренная константа фикстуры: замороженные часы (у неё они родили монетку newestRun, делавшую два других пина пустыми), пустой воркдир, единственный экземпляр сервиса. ⇒ у каждого пина, утверждающего КЛАСС, спрашивай: что эта фикстура держит постоянным и о чём она поэтому молчит — и либо варьируй, либо сузи утверждение до среза, который она действительно меряет. И два утверждения о цвете ОДНОГО пина, снятые РАЗНЫМИ приборами, сравнивать нельзя. Замер 11.09: «8 FAIL из 8» снято без -race, зелёная сторона — -count=3; пере-снято ОДНИМ прибором с обеих сторон (-race -count=8) и дало 8/8 в обе стороны. Счёт вёлся по ПОЛНОМУ списку имён (grep -oE '^--- (PASS|FAIL): Test…' | sort | uniq -c), а не по хвосту вывода. ⚠ И -count=8 в одном процессе — не восемь отдельных прогонов; если предмет чувствителен к состоянию процесса, это разные замеры, и разницу называют. «SKIP ПРЕВРАТИЛСЯ В PASS» ДОКАЗЫВАЕТ, ЧТО ТЕСТ ИДЁТ, А НЕ ЧТО ОН ДЕРЖИТ (формулировка зоны 11.09). Введя корпусные переменные в гейт, легко счесть дело сделанным по тому, что скипы исчезли. Это про ЗАПУСК. Что ступень НЕСУЩАЯ, доказывает только одно: посадка, сломавшая предмет, краснеет ИМЕННО ЕЮ. Замер: C-verdict-leaves-the-anchored-column поймана ДВУМЯ тестами — синтетическим пином и оракулом холодного прогона; значит сломать первичную колонку и получить зелёный гейт нельзя даже мимо синтетики. ⇒ после включения любой ступени в гейт спроси её посадкой, а не списком скипов. И ТА ЖЕ НОРМА — ПРО САМ ХАРНЕСС, А НЕ ТОЛЬКО ПРО ЧИТАЮЩЕГО (зона поймала у себя 11.09, приёмка этого не видела). Её мутационный харнесс склеивал ДВА пакета в ОДИН аргумент go test; тот не мог его разрешить, выходил ненулевым — и харнесс читал ненулевой код как «мутация поймана». Три записи получили ложное RED, то есть прибор сам печатал поимки там, где их не было; нашлось руками на четвёртой. ⇒ харнесс, который судит по КОДУ ВОЗВРАТА, не отличает «тест упал на предмете» от «команда не собралась»; оба выглядят успехом мутационной кампании. Лечение: харнесс обязан требовать от падения ТЕКСТ про предмет, а на неразрешённой команде — выдавать ОТДЕЛЬНЫЙ исход (неизмеренная), и под сам этот дефект ставится пин. ⚠ Класс родствен D39.212 п.8 («код возврата составной команды отвечает на свой вопрос, а не на твой») и норме про UNKNOWN: неизмеренное, прочитанное как пойманное, хуже выжившего — оно закрывает вопрос, не задав его. МУТАЦИЯ ЗАСЧИТЫВАЕТСЯ ПО ТЕКСТУ СООБЩЕНИЯ, А НЕ ПО ФАКТУ КРАСНОТЫ. Читай ТЕКСТ падения: говорит ли он про сломанное тобой. Правый вердикт по неправой причине — дыра, а не поимка, и от настоящей поимки отличается только тем, прочёл ли кто-нибудь текст, а не цвет (D39.217 п.2в). ФИКСТУРА ОБЯЗАНА НАЗВАТЬ ГРАНИЦУ, КОТОРУЮ ПРОШЛА: у сквозного теста граница — побочный эффект того, как сконфигурирован прогон, а не то, что тест утверждает (08.09, зона нашла у СЕБЯ и назвала прямо, D39.228 п.5). Проекция банка публикуется на ПЯТИ границах прогона, и каждая несёт свою метку внутри самого документа; все три сквозных теста к ней открывали раннер так, что читали артефакт границ «авто-продолжение» и «прогон закончен», а предметом была граница ПОДПИСИ — единственная, ради которой проекция и существует. Порча «не публиковать секцию именно на границе подписи» шла ЗЕЛЁНОЙ по всей батарее. ⇒ у артефакта больше одного момента записи — тест утверждает СНАЧАЛА, какой момент он прочёл (сверка метки границы), и только потом содержимое; иначе зелёный тест описывает не тот момент и молчит ровно там, где предмет. Вопрос себе: «сколько раз этот файл переписывается за прогон и какой из разов я сейчас читаю?» Пришло письмо «всё закрыто» — иди перечитывать СВОИ утверждения о закрытом, а не чужие правки. Fable 5 — ПОТОЛОК 12 АГЕНТА НА СЕССИЮ, слово владельца 11.09: «я не разрешаю больше 1-2 на сессию». Прежняя редакция называла это рекомендацией — она отозвана. Модель задавай агенту ЯВНО и знай, сколько их у тебя работает. ⚠ И форма важнее числа: одному агенту с ПОСТОЯННЫМ контекстом дослылают вопросы, а не поднимают второго — контекст и есть ценность. Опус-субагенты потолком не ограничены, но тратятся так же.
  • Git-координация мультисессий:
    • Коммитит только оркестратор. Сессии зон — бэкенд, полигон, фронт, платформа — не коммитят: своё дерево готовят и передают на лендинг (исключение: пре-рег фризы полигона). Чужую зону не трогает НИКТО.
    • Форма коммита — ТОЛЬКО с явным списком путей: git commit -- <путь> <путь>. Причина механическая: голый git commit уносит ВЕСЬ индекс, включая чужие застейдженные файлы, и чужая работа уезжает под твоим сообщением. Pathspec-форма индекс не трогает.
    • НИКОГДА git add -A / git add . / git commit -a — только явные пути.
    • Перед каждым коммитом: git status (опознать ЧУЖОЕ) и git diff --cached --name-only. Увидел в индексе путь вне своей зоны — не коммить его и не «прибирать»: снимай не чужое из индекса, а свою задачу — коммить pathspec-формой.
    • Не оставляй свои файлы застейдженными между операциями: пока они в индексе, их может унести чужой коммит. Стейдж и коммит — одной командой.
    • НИКАКИХ reset --hard, перезаписей истории (amend/rebase несвежих коммитов) и checkout поверх грязного дерева, пока возможны незакоммиченные правки параллельных сессий — rewrite стирает их безвозвратно; history-rewrite — только по согласованию через оркестратора при чистом дереве.
    • Чужое уже уехало в твой коммит? Историю НЕ переписывать — сообщить владельцу/оркестратору; содержимое при этом цело, теряется только атрибуция.
    • Никогда не коммитить В ЭТОТ репозиторий: START_PROMT.MD (файл владельца) · .claude/settings.local.json · книгу и производные. ⚠ С 24.08 книги лежат в <репозиторий>/books и версионируются ОТДЕЛЬНЫМ git-репозиторием со своим origin; внешний держит /books/ в .gitignore. Следствия: git status внешнего репо правки книг НЕ показывает (смотреть git -C books status), а clean -xdf снёс бы каталог вместе с его историей.

Онбординг новой сессии (порядок чтения)

Развилка ПЕРВАЯ, до всего остального — есть ли у тебя хендофф-промт (файл задания, выданный оркестратором).

  • С промтом: этот файл → СВОЙ промт; его карта чтения (≤5 позиций) — ЗАКОН, дальше только по её ссылкам. README, CURRENT-STATE и голову журнала решений читать НЕ нужно, ЕСЛИ карта промта их не называет (у ролевого промта оркестратора называет — там его номер и очередь): ратифицированное вложено в тело промта, остальное берётся грепом по мере вопросов.
  • Без промта (холодный вход): СНАЧАЛА таблица активных промтов в конце docs/README.md. Назван промт твоей роли — он и есть твой: дальше действует ветка «С промтом», и его карта чтения ЗАКОН. Не назван («активного НЕТ») — полный маршрут ниже. ⚠ Порядок именно такой: слепой замер входа 24.08 показал, что послушная холодная сессия проходит весь маршрут (~300 тыс. знаков) и только потом узнаёт, что её работа уже заказана промтом.
  1. Этот файл → docs/README.md (карта) → CURRENT-STATE в docs/PROGRESS.md. Непонятный жаргон/сокращения — docs/glossary.md.
  2. docs/architecture/05-decisions-log.md: шапка (карта актуальности + эрраты) и реестр docs/architecture/05-decisions-index.md — одна строка на ноту. Из тел — только 23 последние. ⚠ «Живая голова» целиком в обязательное чтение НЕ входит: это ВСЯ голова D39.124+, и она растёт каждую смену (на 10.09 — больше сотни нот), и требование её прочесть противоречило бы дисциплине «целиком НЕ читать» выше; остальные номера грепаются по мере вопросов. Тела закрытых эр — в слайсах docs/archive/architecture/ (греп: живой файл → слайсы).
  3. По роли (пути — от КОРНЯ репозитория): Бэкенд — шапка-таблица docs/architecture/09-target-architecture.md (что построено) → backend/README.mddocs/architecture/03-implementation-notes.md (через баннер) → docs/architecture/12-go-style-notes.md; ⚠ порядок именно такой: сессии, впервые видящей движок, механика нужнее стиль-норм; Полигон — eval/README.md + docs/experiments/00-provider-quirks.md + docs/experiments/09-pilot-protocol.md; Фронт/продукт — docs/product-requirements.md + docs/research/16-reader-ide-alignment.md (через ревью-шапку) + контракт docs/architecture/14-api-contract/; Платформа — platform/README.md + зонный журнал + docs/research/23-engine-platform-seam.md + контракт 14; всем — шапка-таблица docs/architecture/09-target-architecture.md (статус слоёв). Тулчейн на чистой машине — строка «Тулчейн» в карте docs/README.md.