# Промт сессии: ПЛАТФОРМА, пак P13 — «шов возврата оплаченного прогона» > ⚠⚠ **АРХИВ. ПРОМТ ОТРАБОТАН — ИНСТРУКЦИИ ОТСЮДА НЕ ИСПОЛНЯЮТСЯ.** > Выдан 02.09, сдан 03.09, принят с дофиксом (`D39.188`). Исход: шесть пунктов состава исполнены, > заказ пункта 3 оказался НЕДОСТАТОЧЕН и зона это доказала (`PD-438`, major), дофикс по шести пунктам > отработан, приёмка второго круга оставила одну правку записки о хосте. > ⚠ Часть якорей этого файла МЕРТВА по построению: пак переписал цели (строка рантбука, `Quarantine`). > Живые носители: `D39.188` · `docs/PLATFORM_P13_DOFIX_2026-09-03.md` · `platform/docs/platform-PROGRESS.md`. > **Роль:** платформа. **Зона записи:** `platform/`. Чужие зоны читаешь, не правишь. **Не коммитишь** > и вообще не трогаешь индекс и историю — почему именно, и полный список запретов: `CLAUDE.md`, > «Git-координация мультисессий». > **Обязателен к ссылке:** `platform/docs/ENGINEERING_STANDARDS.md`; отступление — пинг, не > самовольство. Направление зоны — `platform/docs/PLATFORM_DIRECTION.md`. ## Какая проблема и что изменит твой результат Оплаченный прогон сегодня может подорожать, зависнуть или замереть на экране — **без вины пользователя и без единого сигнала оператору**. Четыре независимых пути ведут к одному и тому же: человек заплатил, а получил либо не то, либо ничего, и вывести его из этого состояния нечем, кроме psql. После пака оплаченный прогон возвращается на **те деньги, за которые был продан**, оператор перестаёт начислять кредиты дважды, а чужая строка в общем журнале книги перестаёт замораживать проекцию чужого прогона. ## Карта чтения — пять позиций, дальше только по их ссылкам 1. `platform/README.md` — устройство зоны · `platform/docs/ENGINEERING_STANDARDS.md` (обязателен). 2. `platform/docs/platform-PROGRESS.md` — зонный журнал: текущее состояние и **пинги оркестратора** (там же ПИНГ №21 про гейт батареи и ПИНГ №22 про заниженную ставку и снятый сайдкар). 3. Ряды регистра, названные ниже поимённо: `PD-168` · `PD-89` · `PD-426` · `PD-214` · `PD-374` — `platform/docs/DEFECT_REGISTER.md`, греп по номеру. 4. `docs/architecture/15-money-path.md` §3 — деньги платформы по шагам (холд, расплата, `run abandon`). 5. `docs/architecture/14-api-contract/` — канон контракта; **зеркало фронта НЕ норматив**. ⚠ Ратифицированное вложено в тело этого промта; **код-факты добываешь САМ** — указатели ниже отправные точки, код первичен. Числа ниже даны С КОМАНДОЙ ПОЛУЧЕНИЯ: пере-ранишь — сойдётся; не сошлось — это находка, неси её пингом, а не подгоняй под текст. ## ⚠ ПРЕДУСЛОВИЕ, НЕ ПУНКТ РАБОТЫ — прочти прежде всего **Условий хоста у батареи ЧЕТЫРЕ, и все четыре на этом хосте выполнимы. Выставь их до первой команды** — иначе половина зоны молча не гоняется: | условие | как выставить | что откроет | |---|---|---| | `TM_PLATFORM_TEST_DSN` | стенд уже поднят (`/tmp/.s.PGSQL.55433`); DSN — `platform/docs/STACK_DECISIONS.md`, раздел стенда разработчика, греп `port=55433` | денежный путь `runs`, `pgstore`, `books`, `cmd/tmplatformctl` | | `TM_PLATFORM_TEST_ENGINE_BIN` + `TM_PLATFORM_TEST_BOOK_TEMPLATE` | движковый бинарь и шаблон деплоя | 3 теста `internal/runner` + `internal/books` | | достижимый пользовательский systemd | на этом хосте `/run/user/1000` жив, ставить нечего | `systemdOrSkip`, `internal/runner/systemd_test.go` | | **`export PATH=$HOME/go/bin:$PATH`** | ⚠ без него `make check` НЕ ЗАПУСКАЕТСЯ ВОВСЕ: `tools-check` падает на `sqlc` («got: none»), хотя нужный `v1.31.1` лежит в `~/go/bin` | весь `make check` | Проверка последнего одной командой: `cd platform && make tools-check` → `Error 1` на sqlc; `cd platform && PATH=$HOME/go/bin:$PATH make tools-check` → выход 0. **Сети нет — ничего не ставь**, бинарь уже на диске, вопрос только в PATH. **Замер 02.09 на этом хосте, без DSN** (числа — с командами; голый `go test ./...` печатает ровно 18 строк «ok» и НИ ОДНОГО из этих чисел, поэтому команда счёта — verbose): ``` cd platform && go test ./... -count=1 -v > /tmp/battery_v.txt 2>&1; echo "exit=$?" grep -cE '^=== RUN [^/]+$' /tmp/battery_v.txt # 661 верхнеуровневых тестов grep -c '^--- PASS' /tmp/battery_v.txt # 342 grep -c '^--- SKIP' /tmp/battery_v.txt # 319 верхнеуровневых скипов grep -c '^--- FAIL' /tmp/battery_v.txt # 0 grep -c '^ok ' /tmp/battery_v.txt # 18 пакетов grep -c -- '--- SKIP' /tmp/battery_v.txt # 330 — ТО ЖЕ САМОЕ вместе с подтестами ``` ⚠ **Снимаю кажущееся противоречие заранее:** ПИНГ №21 в зонном журнале пишет про пустое окружение **330** скипов, здесь стоит **319**. Это ОДИН И ТОТ ЖЕ прогон: `make check` грепает `-- '--- SKIP'`, то есть считает подтесты вместе с верхнеуровневыми. Оба числа верны, расходиться тут не о чем. Среди пропущенных — **110 тестов денежного пути реконсилятора** (верхнеуровневых; вхождений сообщения вместе с подтестами — 112): ``` cd platform go test ./internal/runs/ -count=1 -v 2>&1 \ | awk '/reconciler.s money path/{p=1;next} p&&/^--- SKIP/{n++} {p=0} END{print n+0}' # 110 go test ./internal/runs/ -count=1 -v 2>&1 | grep -c "reconciler's money path" # 112 ``` ⚠ `make check` про скипы ЧЕСТЕН — печатает строку «did NOT run …» и имена скипнутых тестов. Молчит именно голый `go test ./...`. **Цена, которую ты обязан не повторить:** в регистре **21 ряд** заведён с формулировкой «мутация пережила ПОЛНУЮ батарею», и **восемь из них открыты** — `PD-433` · `PD-375` · `PD-382` · `PD-383` · `PD-386` · `PD-388` · `PD-389`, плюс `PD-423` (там фраза в смысле «пин ЗЕЛЁН в полной батарее со скипами 0»). **Ни один не называет DSN**, то есть «полная батарея» этих вердиктов могла быть той самой, где тёмно 319 тестов. ``` cd platform && awk -F'|' '/^\| PD-/{l=tolower($0); if(l~/полную батарею|полной батарее|всю батарею/){st=$(NF-2);gsub(/^ +| +$/,"",st); if(st=="open")o++;else f++}}END{print "OPEN="o,"FIXED="f,"TOTAL="o+f}' docs/DEFECT_REGISTER.md # OPEN=8 FIXED=13 TOTAL=21 ``` ⇒ **Каждая посадка мутации в твоём отчёте обязана нести строку «гонялось при DSN = да».** Иначе пак проверит себя ровно тем способом, каким заводились ряды, которые он чинит. ## Состав пака — четыре правки, гейт и одна строка рантбука ### 1. ГЛАВНОЕ — денежный бюджет прогона ЧИТАТЬ, а не пере-выводить из ставки `internal/runs/reconcile.go`, функция `reopen` (`:1350`). **Ставка читается там ДВАЖДЫ, и оба места денежные:** ``` cd platform && grep -n 'Pricing' internal/runs/reconcile.go # 1363: budget := s.Pricing.Ceiling(l.CeilingChapters) # 1397: consent = s.Pricing.Ceiling(l.CeilingChapters) ``` - `:1363` — **бюджет** перезапуска: `remaining = budget - spent`, отсюда `Ceiling` в `RestartInput`. - `:1397` — **funded consent на пере-оплату** при `book.BankMoved`, уезжает в `RestartInput.AcceptRebill`. Стоит в ветке `if from == fromAFinishedRun`, то есть **только на пользовательском `Resume`** — ровно на том пути, который и опасен. Комментарий `:1384-1386` подтверждает денежный смысл: «funded consent, the same figure Start grants». Вызывающих у `reopen` двое: свиповый `restart` (`:1224`, вызов `:1244`, `fromALiveRun`) и контрактный `Resume` (`:1481`, вызов `:1548`, `fromAFinishedRun`). Смена `TM_PLATFORM_USD_PER_CHAPTER` (`internal/config/config.go:485`=`TM_PLATFORM_USD_PER_CHAPTER`) между покупкой и продолжением молча пере-оценивает уже оплаченный прогон в обе стороны; при удвоении ставки на тонком счёте он становится невозобновляемым. Носитель — `PD-168` (стоит `minor`, вес занижен; замер ряда: холд `5.500000` вместо `2.500000`). **Задача (делай РОВНО так, отступление = пинг):** ⚠ **Первая половина УЖЕ построена, строить нечего.** `PD-168` дословно: «Исходная сумма восстановима без пересчёта — она лежит в холде первой попытки (`reservations.ceiling_micro_usd` / `run_attempts.ceiling_micro_usd`)». Обе колонки в схеме есть: ``` cd platform && grep -n 'ceiling_micro_usd' internal/pgstore/migrations/*.sql # ЧЕТЫРЕ хита, из них колонки — две: # 00007_credits.sql:72 — reservations.ceiling_micro_usd # 00009_runner.sql:36 — run_attempts.ceiling_micro_usd (:70 — её же `drop` в Down-миграции) # 00011_ceiling_argument.sql:5 — КОММЕНТАРИЙ-предупреждение, не колонка ``` Значит задача **не «завести хранение»**, а «заставить `reopen` ЧИТАТЬ уже хранимое вместо `s.Pricing.Ceiling(...)`». **Схему хранилища платформы при этом не трогаем — миграция не нужна и не заказана.** Понадобилась вопреки этому — стоп и пинг, а не «сделаю аккуратно». ⚠ **Лечение обязано закрыть ОБА места, `:1363` и `:1397`.** Закрыт один — это половина, и в отчёте так и объявляется: путь остаётся под «согласием, которого пользователь не давал». ⚠ Не спутай с третьей колонкой: `run_attempts.ceiling_arg_micro_usd` (`00011_ceiling_argument.sql`) — это КНИЖНЫЙ аргумент `--ceiling-usd` для движка, а не бюджет попытки; миграция прямо предупреждает «It is NOT ceiling_micro_usd». **Решаешь сам и аргументируешь:** какую именно из двух колонок читать, как быть, если холда первой попытки нет (старые прогоны), и нужен ли отдельный аккуратный фолбэк. ### 2. `PD-89` — печатать ключ идемпотентности НА ПУТИ ОШИБКИ `cmd/tmplatformctl/main.go`, функция `write()` (`:184`; ⚠ якорь ряда `:143-150` дрейфанул и указывает теперь на вызывающего `grant`). `id = newKey()` чеканится на `:189`, `applied, err := op(id, now)` — на `:191`, и `if err != nil { return err }` (`:192-194`) возвращает ошибку, **не напечатав ключ**. Неоднозначный обрыв на коммите → оператор повторяет без `--key` → `newKey()` чеканит новый → **второе начисление**. Комментарий `:195-198` описывает ровно эту ловушку, но только для пути чтения баланса («a failed BALANCE READ would have bought a double credit»). **Задача (делай РОВНО так):** на пути ошибки ключ обязан быть виден оператору. **Посадка мутации обязательна.** Форма печати (куда, каким текстом, `out` или `stderr`) — **решаешь сам и аргументируешь**. ### 3. `PD-426` + `PD-214` — чужая строка не должна карантинить НАШУ проекцию `internal/ingest/tail.go`, функция `apply` (`:145`). Четыре отказа стоят выше ветки, решающей принадлежность: `:150` декод payload · `:153` `checkVersion` · `:156` пустой `EngineRunID` · `:162` `Seq != 1`. Ошибка любого из них терминальна для проекции: `quarantines()` (`internal/runs/reconcile.go:770-781`=`func quarantines(err error) bool`) пропускает только `context.Canceled/DeadlineExceeded` и `pgstore.IsTransient`, всё прочее едет в `Store.Quarantine`. Снятия карантина в дереве НЕТ (`grep -rniE 'unquarantine|clearquarantine|quarantine_reason *= *null|dequarantine'` по `.go`/`.sql` — пусто при живом положительном контроле `internal/pgstore/runs.go:1470`=`func (s *Store) Quarantine`), операторской ручки нет, а гейдж `tm_platform_quarantined_attempts` (`internal/metrics/metrics.go:80`=`Name: "quarantined_attempts"`) это видит и молчит о том, что делать. ⚠ **ПОСЫЛКА, КОТОРУЮ НАДО ЗНАТЬ ТОЧНО — иначе живая проба будет искать несуществующее.** Полностью ЗАКОННЫЙ чужой hello сегодняшней сборки (мажор потока тот же, `engine_run_id` непустой, `seq == 1`) **НЕ карантинит** — он уходит в `default` и молча пропускается, и это защищено ЗЕЛЁНЫМ пином: ``` cd platform && go test ./internal/ingest/ -count=1 -run TestAnotherAttemptsStreamInTheSameJournalIsSkipped -v # --- PASS (тело пина — tail_test.go:185-187: hello(1,"eng-1") … hello(1,"eng-2"), ошибки нет) ``` Карантинит **чужая ИЛИ БИТАЯ** hello-строка, и условий ровно четыре — они же перечислены в `PD-214` («ручной `tmctl` оператора в каталоге книги, старый мажор, баг чужой сборки»): другой **МАЖОР** потока (`checkVersion` сравнивает ТОЛЬКО мажор, `internal/ingest/events.go:196-208`=`func checkVersion(got string) error`; у платформы `StreamVersion = "1.1"`, у движка `"1.2"` — обе стороны на мажоре 1, поэтому сегодня проходит) · пустой `engine_run_id` · `seq != 1` · нераспарсиваемый payload. ⚠ Формулировка `PD-426` («попасть можно по чужому ЗАКОННОМУ handshake'у») — переупрощение, ряд описывает класс шире, чем воспроизводится; читай `PD-214`, он точнее. **Задача — СВОЙСТВО, а не перестановка (делай РОВНО так, отступление = пинг):** > **строка, которая не наша, не должна карантинить нашу проекцию.** Что это значит по букве, чтобы ты не внёс дефект буквальным исполнением: - ⚠ **Отказ по ДЕКОДУ (`:150`) остаётся ВЫШЕ и не двигается.** `EngineRunID` живёт ВНУТРИ payload `Hello` (`internal/ingest/events.go:82-92`=`EngineRunID string`), в `Envelope` (`:71-79`) его нет — значит без успешного декода принадлежность просто НЕИЗВЕСТНА. Переставляемы только три отказа, не четыре. - Понижаются **только** `checkVersion`, пустой `EngineRunID` и `Seq != 1` — **и ТОЛЬКО в ветке, где `want` уже известен** (`want != ""`): там принадлежность решается сравнением `h.EngineRunID == want`, и чужая строка обязана уходить в «не наше, пропустить» ДО валидации. - ⛔ **При `want == ""` валидация обязана остаться полной.** Ветка `case want == "":` (`:166`) УСЫНОВЛЯЕТ поток — зовёт `sink.Begin` и ставит `want, mine = h.EngineRunID, true`. Снять валидацию там значит принять поток ЧУЖОГО МАЖОРА как свой. Это ровно тот дефект, который пак чинит, только с другой стороны. - Зелёный пин `TestAnotherAttemptsStreamInTheSameJournalIsSkipped` обязан остаться зелёным: он описывает уже правильную половину поведения. - Курсорная семантика (`held` против `pos`, движение курсора на hello) — не предмет правки; тронул — объясни зачем. **Вторая половина ряда — ручка снятия карантина.** Что она есть — «делай РОВНО так»; **форма (подкоманда `tmplatformctl`, аргументы, что именно сбрасывается, кто имеет право) — решаешь сам и аргументируешь**. **Посадка мутации обязательна** на обе половины. ### 4. МЕЛКОЕ, закрывающее урок пака: гейт батареи печатает ВСЕ условия хоста, а не одно ⚠ **Дефектов тут ДВА, и они в РАЗНЫХ целях. Заказывается (а), не (б).** **(а) — заказ.** Цель `check` печатает над списком скипов **жёстко зашитый литерал**: ``` cd platform && sed -n '91,100p' Makefile # if grep -q -- '--- SKIP' .check.log; then \ # echo "--- did NOT run (set TM_PLATFORM_TEST_DSN for the schema tests) ---"; \ ``` Строка ни от чего не зависит: она печатается и когда DSN ВЫСТАВЛЕН, а скипы дал совсем другое условие, — то есть отправляет читателя к ручке, которая уже включена. Демонстрация: с выставленным DSN `go test ./internal/books/ ./internal/runner/ -count=1 -v` даёт 3 скипа, причина у них `TM_PLATFORM_TEST_ENGINE_BIN`, а рецепт всё равно советует DSN. Это `PD-374` («отправляя читателя к ручке, которая тут ни при чём») и ПИНГ оркестратора №21 в зонном журнале с таблицей **пусто 330 / DSN 3 / +ENGINE_BIN 0**. **(б) — НЕ заказ, не путай.** Поведение «останавливается на первом недостающем» принадлежит цели `tools-check` (`Makefile:60-66`): там каждая проверка делает `exit 1`, поэтому при живом golangci-lint и отсутствующем sqlc печатается только про sqlc. Это отдельная история; тронешь — объяви отдельно. **Задача (делай РОВНО так):** цель `check` печатает ВСЕ условия хоста и что каждое включает. **Решаешь сам и аргументируешь: форма.** Приор ПИНГа №21 — выводить перечень из тех же переменных, которые читают хелперы тестов, чтобы список не разъехался с реальностью третий раз; довод против «просто дописать вторую строку» — он чинит сегодняшний счёт, а не механизм. Условий сегодня четыре (таблица в предусловии), и `systemdOrSkip` с PATH-до-`sqlc` в подсказке сегодня не названы вовсе. ### 5. ГЕЙТ ПРОТИВ КЛАССА, а не против ряда Колонка серьёзности регистра разъехалась с сутью: открытых рядов **94**, из них `major` — **2**, и следующая разведка снова доложит «major всего два», хотя тревожных рядов на порядок больше. ``` cd platform && awk -F'|' '/^\| PD-/{st=$(NF-2);gsub(/^ +| +$/,"",st);w=$4;gsub(/^ +| +$/,"",w); if(st=="open"){t++; if(w~/major/)m++}}END{print "open="t,"major="m}' docs/DEFECT_REGISTER.md # open=94 major=2 ``` ⚠ **Предикат «слова тревоги» задаю дословно, потому что от его прочтения число прыгает вдвое.** Список маркеров (стеммы): `деньг` · `холд` · `молча` · `блокир` · `невидим` · `500` · `паник`. ``` cd platform && for M in any distinct; do printf "%s: " "$M"; awk -F'|' -v mode="$M" ' /^\| PD-/{st=$(NF-2);gsub(/^ +| +$/,"",st);w=$4;gsub(/^ +| +$/,"",w); if(st=="open" && w !~ /major/){l=tolower($0); n=split("деньг холд молча блокир невидим 500 паник",m," "); c=0; for(i=1;i<=n;i++){ if(mode=="distinct"){ if(l~m[i]) c++ } else { t2=l; c+=gsub(m[i],"",t2) } } if(c>=2) k++}}END{print k+0}' docs/DEFECT_REGISTER.md; done # any: 21 (≥2 ВХОЖДЕНИЯ любого маркера) # distinct: 13 (≥2 РАЗНЫХ маркера) ``` То есть **21** — это счёт по «≥2 вхождения», а не по «два разных слова»; по «двум разным» их 13 (а по буквальным словоформам — 11 и 18 соответственно). Обе величины настоящие, но гейт обязан знать, какую он считает. **Задача (делай РОВНО так):** гейт зоны печатает открытые не-`major` ряды, несущие маркеры тревоги. **Решаешь сам и аргументируешь: место, предикат и порог** — и объявляешь выбранный предикат в отчёте командой, дающей его число. Приор по месту: `platform/internal/gates/` — там уже есть прецедент чтения канона выше модуля (`contract_test.go`, константа `zoneRoot + "/../docs/..."`). ⛔ **`docs/scripts/counts.py` НЕ ТРОГАТЬ** — это единственное, что сегодня читает `platform/docs/DEFECT_REGISTER.md` (`grep -rn 'DEFECT_REGISTER' --include=*.go --include=Makefile --include=*.py --include=*.sh platform/ docs/scripts/` → один хит, `counts.py:51`), и его шапка прямо запрещает правку чужим сессиям: «скрипт — зона ОРКЕСТРАТОРА». Красный `--check`/`--lint` не обходить и не «чинить» подгонкой — это тот же запрет, что подгонка тестов; непонятно или кажется неправым → пинг. ### 6. МЕЛКОЕ, найденное при сборке промта: рантбук ТВОЕЙ зоны советует деструктивный порядок деплоя `platform/deploy/README.md:213`=`сначала платформа, потом движок` — «Правило то же, что у схемы хранилища: сначала платформа, потом движок». По коду безопасного порядка у формы манифеста НЕТ: гейт интейка — строгое равенство ОДНОЙ константе, окна двух версий нет (обе ссылки — в разделе ПОРЯДОК ДЕПЛОЯ ниже). Платформа, выкаченная первой со знанием новой формы, отправит в `parser_unavailable` КАЖДУЮ книгу ещё не обновлённого движка — ровно тот симптом, который абзац рантбука строкой выше описывает как беду ОБРАТНОГО порядка. **Делай РОВНО так:** ряд в `docs/DEFECT_REGISTER.md` (класс `doc`) + правка строки рантбука. **Решаешь сам и аргументируешь:** формулировка — назвать бамп формы стоп-миром или описать временное окно двух версий как условие, при котором безопасный порядок вообще появляется. ⚠ Увидишь в коде окно версий, которого я не нашёл, — это находка, а не твоя ошибка: скажи и не чини. ## Границы — что НАМЕРЕННО не берём - **`PD-412`/`PD-413` (LATERAL в полосном агрегате).** Находки настоящие и замеренные (1.359 мс → 0.307 мс на книге 2283 глав — числа принадлежат `PD-412`), но это переписывание запроса в горячем read-модельном пути, а арифметика полосы в этом паке не трогается. Контенция, а не корректность денег. - **`PD-162` (клин на удалённом каталоге).** Настоящий и денежный, но его АВТОМАТИЧЕСКАЯ терминальность — это эскроу (`platform/BACKLOG.md` П-18/П-20), а строить половину эскроу рядом с проектируемым целым зона уже отказывалась: **пере-диспозиция P6 по `PD-154`** (греп `половину эскроу` в `platform/docs/` — единственный хит, `DEFECT_REGISTER.md:101`). ⚠ Ряд называет и ВТОРОЕ, дешёвое лечение — «либо проверкой каталога при допуске». Этим паком оно тоже НЕ берётся: пак закрывает денежный шов возврата, а валидация каталога на допуске — это правка входной двери `Start`, отдельный предмет с собственной ценой на ложных отказах. Операторская ручка на этот класс уже построена (`run abandon [--release-hold]`, P8-FIX; строка П-20 фиксирует, что «пока эскроу нет, ручка остаётся ручной»). ⚠ Считаешь границу неверной — скажи аргументом, право такое у тебя есть (см. «Канал вопросов»). - **Дверь выдачи** — отдельный пак: `createExport`/`getExport` в каноне 0.9.0 есть, платформой не смонтированы (`grep -rn 'createExport\|getExport' platform/internal/ --include=*.go` — пусто), но дверь требует минора канона и сводит на себя все точки столкновения. ## ⚠ ПОРЯДОК ДЕПЛОЯ — почему пак безопасен параллельно бэкенду Зоны независимы по коду (разные модули, перекрёстных импортов нет в обе стороны, `go.work` в корне отсутствует), но **НЕ по деплою**, и сцеплений ДВА. - **Форма манифеста БЕЗОПАСНОГО ПОРЯДКА НЕ ИМЕЕТ.** Гейт интейка — строгое равенство ОДНОЙ константе (`internal/ingest/manifest.go:112`=`if m.Version != KnownManifestVersion`; сама константа — `:88`=`KnownManifestVersion = "tm-manifest-v2"`). Окна из двух версий нет, поэтому «платформа первой» ровно так же смертельно, как «движок первым»: односторонняя выкатка отправляет КАЖДУЮ книгу в `parser_unavailable`, который жжёт бюджет попыток. Бамп формы = стоп-мир либо специально построенное временное окно двух версий. - **Схема хранилища ДВИЖКА** ⇒ «движок первым → `tmctl migrate` по каждой книге → и только потом платформа снимает свой обход» (D39.158 п.6). Голова схемы сегодня 16 (`backend/internal/store/migrate.go:485`=`func SchemaHead() int`, `len(migrations)`) — это персистентная память ДВИЖКА, а не Postgres платформы. **Поэтому пак НЕ меняет: форму манифеста · схему хранилища ДВИЖКА · версию контракта.** Понадобилось — **стоп и пинг**, не «сделаю аккуратно». Платформенная миграция этим абзацем НЕ запрещена: по пункту 1 она не нужна (сумма уже персистится) и потому не заказана; понадобилась — тоже пинг, но по другому основанию. ## Свобода и её разметка **«Делай РОВНО так» (отступление = пинг):** - предусловие хоста (четыре условия) · строка «DSN = да» у каждой посадки; - **ФАКТ каждой из шести правок** — что именно закрывается: (1) `reopen` читает хранимую сумму в ОБОИХ местах, `:1363` и `:1397`, схему платформы не трогая; (2) ключ печатается на пути ошибки; (3) свойство «чужая строка не карантинит нашу проекцию» с тремя оговорками пункта 3 (декод остаётся выше · понижение только при известном `want` · при `want == ""` валидация полная); (4) цель `check` печатает ВСЕ условия; (5) гейт против класса живёт в зоне платформы; (6) ряд регистра и правка строки рантбука; - **обязательные посадки у пунктов 2 и 3**; - границы выше · три запрета из порядка деплоя · запрет трогать `docs/scripts/counts.py`. **«Решаешь сам и аргументируешь» — ФОРМА и только она:** какую колонку холда читать и как быть без холда · форма печати ключа · форма ручки снятия карантина · форма печати условий в `Makefile` · место, предикат и порог гейта пункта 5 · формулировка правки рантбука в пункте 6 · остальной состав посадок · порядок работ внутри пака. ⚠ Катч-олла «всё остальное — сам» больше нет: если чего-то нет ни в одном списке — это пинг, а не твоё усмотрение. ## Мандат самопроверки — исполнением, не перечитыванием Батарея зелёная — это НЕ отчёт. Обязательны все три: 1. **Живая проба каждого пункта состава.** Поимённо, с пометкой про DSN: - п.1 `reopen` — **DSN нужен** (денежный путь `internal/runs`, 110 тестов тёмных без него); - п.2 `write()` — юнит-посадки идут **без DSN**, но сквозной `TestCommandsAgainstALiveDatabase` в `cmd/tmplatformctl/main_test.go` **DSN-гейчен** (без DSN в `./cmd/...` скипается 6 тестов); - п.3 `ingest/tail.go` — **DSN не нужен**, пакет гоняется целиком (0 скипов); - п.4 `Makefile` — DSN не нужен, но проба обязана гоняться при РАЗНЫХ окружениях, иначе она не видит того самого дефекта; - п.5 гейт — DSN не нужен; - п.6 рантбук — кода не касается, «проба» это ряд регистра и `counts.py --check` зелёным. 2. **Собственные адверсариальные посадки ВНЕ списка выше.** 3. **Отдельный проход по СВОЕЙ ГОТОВОЙ работе** свежим контекстом. ⚠ Класс, купленный прошлой сменой: веер по исходным уликам находит втрое меньше, чем веер, читающий готовый результат. **Субагенты разрешены явно.** Модель задавай ИМЕНЕМ и знай, сколько их у тебя работает; обычно хватает одного-двух, но веер под предмет выбираешь ты. ### Предметные оси ревью — под характер ЭТОЙ работы **(1) ШОВ — инвентарь каналов другой стороны чтением ЧУЖОГО кода, не по памяти (D39.132).** Пункт 3 живёт целиком на шве с движком, и спроектировать порядок проверок по своей половине шва нельзя. Открой и прочитай: `backend/internal/runevents/` (что движок реально пишет в общий `events.jsonl`, словарь типов, `StreamVersion`, комментарий `Envelope` — «a resumed run is a NEW process that appends a second hello to the same file and numbers from 1 again») и `backend/internal/pipeline/events.go` (греп `hello at seq 16` — отказ `Seq != 1` наблюдался НА РЕАЛЬНОМ БИНАРЕ, 33 строки, 15 байт-в-байт дублей). Вопросы, на которые обязан быть ответ ЧТЕНИЕМ: с какого `seq` нумерует второй процесс · какие мажоры потока сегодня возможны в одном журнале · что происходит при дозаписи. ⛔ Читаешь — не правишь: `backend/` чужая зона. **(2) ДЕНЬГИ — пере-вывод сумм из холда первой попытки.** Не «код компилируется», а «на живой БД при изменённой `TM_PLATFORM_USD_PER_CHAPTER` резервируется та сумма, на которую человек согласился» — и по пути `Resume`, и по пути свипа, и в `AcceptRebill`. ### ⚠ ПРОВЕРЯЕМЫЙ АРТЕФАКТ находок — обязателен Тест на вырожденность, применяй к каждой строке своего отчёта: **если её букву удовлетворяет НИЧТО — строка дефектна**. «Батарея зелёная», «перечитал сам», «субагент ответил: находок нет» — это не отчёт, это его отсутствие. В журнал идут ТРИ таблицы: - **Посадки:** строка на посадку — что мутировано (`file:line` + сама мутация) · какой тест покраснел (ИМЕНЕМ) · `DSN = да/нет`. ⚠ Если по посадке НЕ покраснел ни один тест — так и пиши: это находка (дыра в покрытии), а не провал отчёта, и она сама становится строкой регистра. - **Живые пробы:** путь · команда · наблюдённый выход **цитатой, не пересказом** · вердикт. - **Веер:** на каждого агента — модель ИМЕНЕМ · что он читал · находки с `file:line`. «Находок нет» принимается ТОЛЬКО со списком того, что проверялось; голое «находок нет» означает, что веер не отработал — задание переформулируй и гоняй заново. ⚠ Запрет подгонки тестов и гейтов — `CLAUDE.md` (D39.183). Здесь важен один его край: заказанность решает ЗАКАЗ, а не ты, и **этот промт заказывает смену поведения в пунктах 1–4** — протухший под неё тест правится как обслуживание, но правка ОБЪЯВЛЯЕТСЯ в отчёте (что изменилось, какой тест это описывал, куда уехала гарантия). ## Отчёт **Записка-план ДО работы** (эхо-протокол: скоуп · инварианты · что НЕ делаю, ≤10 строк) — в `platform/docs/platform-PROGRESS.md`, и тем же текстом первым действием по каналу (см. ниже). Итог — туда же. ⛔ В `docs/PROGRESS.md` платформа НЕ пишет (канон, таблица ролей). **Каждое число и каждая категорика — с КОМАНДОЙ ПОЛУЧЕНИЯ, которая это число реально печатает.** Приёмка пере-ранит. ⚠ Команда, печатающая «ok» вместо числа, командой получения не является. Обязательная секция **«Что НЕ удалось и что НЕ проверено»**: исходов ТРИ, третий законен, и молчание об упёршемся пункте дороже самого упора. ## Канал вопросов и право отказаться **Конфликт промта с кодом — пинг через владельца, не интерпретация.** Если число выше не сошлось, если факт о коде не воспроизводится, если два места этого промта говорят разное — это находка, неси её, а не подгоняй работу под текст. **Право сказать «этого делать не надо» с аргументом у тебя есть** — включая пункты состава и границы выше. Сессия, приносящая развилку вместо тихой девиации, делает правильно. ## Прямой канал Механизм целиком — `CLAUDE.md`, «Связь между сессиями»: впиши свой блок в `/tmp/textmachine-channel`, сверься с `ListAgents` перед отправкой, канала нет — это нормально и опрашивать сессии подряд не надо. Что добавляет этот пак: **эхо-записку отправь ПЕРВЫМ действием по адресу оркестратора — это и есть проверка канала**; и знай, что в файле прямо сейчас лежат блоки закрытых смен, один с пометкой «⚠ СМЕНА СДАНА 31.08 … по адресу не писать».