textmachine/docs/PLATFORM_P13_SESSION_PROMPT.md

441 lines
42 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.

# Промт сессии: ПЛАТФОРМА, пак P13 — «шов возврата оплаченного прогона»
> **Роль:** платформа. **Зона записи:** `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). Здесь важен один его край: заказанность
решает ЗАКАЗ, а не ты, и **этот промт заказывает смену поведения в пунктах 14** — протухший под неё
тест правится как обслуживание, но правка ОБЪЯВЛЯЕТСЯ в отчёте (что изменилось, какой тест это
описывал, куда уехала гарантия).
## Отчёт
**Записка-план ДО работы** (эхо-протокол: скоуп · инварианты · что НЕ делаю, ≤10 строк) — в
`platform/docs/platform-PROGRESS.md`, и тем же текстом первым действием по каналу (см. ниже).
Итог — туда же. ⛔ В `docs/PROGRESS.md` платформа НЕ пишет (канон, таблица ролей).
**Каждое число и каждая категорика — с КОМАНДОЙ ПОЛУЧЕНИЯ, которая это число реально печатает.**
Приёмка пере-ранит. ⚠ Команда, печатающая «ok» вместо числа, командой
получения не является.
Обязательная секция **«Что НЕ удалось и что НЕ проверено»**: исходов ТРИ, третий законен, и молчание
об упёршемся пункте дороже самого упора.
## Канал вопросов и право отказаться
**Конфликт промта с кодом — пинг через владельца, не интерпретация.** Если число выше не сошлось,
если факт о коде не воспроизводится, если два места этого промта говорят разное — это находка, неси
её, а не подгоняй работу под текст.
**Право сказать «этого делать не надо» с аргументом у тебя есть** — включая пункты состава и границы
выше. Сессия, приносящая развилку вместо тихой девиации, делает правильно.
## Прямой канал
Механизм целиком — `CLAUDE.md`, «Связь между сессиями»: впиши свой блок в `/tmp/textmachine-channel`,
сверься с `ListAgents` перед отправкой, канала нет — это нормально и опрашивать сессии подряд не надо.
Что добавляет этот пак: **эхо-записку отправь ПЕРВЫМ действием по адресу оркестратора — это и есть
проверка канала**; и знай, что в файле прямо сейчас лежат блоки закрытых смен, один с пометкой
«⚠ СМЕНА СДАНА 31.08 … по адресу не писать».