diff --git a/docs/BACKEND_CONSENT_SESSION_PROMPT.md b/docs/BACKEND_CONSENT_SESSION_PROMPT.md index 2ec1e708..bf80adb0 100644 --- a/docs/BACKEND_CONSENT_SESSION_PROMPT.md +++ b/docs/BACKEND_CONSENT_SESSION_PROMPT.md @@ -1,91 +1,329 @@ # Промт сессии: БЭКЕНД — «число согласия на пути второй покупки» > **Роль:** бэкенд. **Зона записи:** `backend/`. **Не коммитишь** — дерево готовишь и передаёшь на -> лендинг оркестратору. Чужие зоны читаешь, не правишь. Норматив стиля — `docs/architecture/12-go-style-notes.md`. +> лендинг оркестратору. Чужие зоны (`platform/`, `frontend/`, `eval/`) читаешь, не правишь; в `docs/` +> пишешь ТОЛЬКО в секцию «Бэкенд» журнала `docs/PROGRESS.md` — это единственное разрешённое каноном +> исключение. Git: никаких `git add -A`/`git add .`/`git commit -a`, никаких `reset --hard`, `rebase`, +> `checkout` поверх грязного дерева — рядом идут параллельные сессии, и rewrite стирает их работу +> безвозвратно. Норматив стиля — `docs/architecture/12-go-style-notes.md`. ## Какая проблема и что изменит твой результат Продукт продаёт главы **частями**. Покупатель берёт кусок, потом ещё кусок — и на этом пути число, -которым движок спрашивает согласия на оплату, **врёт в трёх местах**, а один юнит может быть оплачен -дважды. После пака покупатель, берущий книгу частями, видит число, которое не занижает при смене -модели стадии, признаётся, какая его часть — старые деньги, и не платит второй раз за юнит, который -уже двигал. +которым движок спрашивает согласия на оплату, **врёт** (занижает при смене модели стадии и не +раскрывает, какая его часть — старые деньги), а один юнит может быть оплачен дважды. После пака +покупатель, берущий книгу частями, видит число, которое не занижает молча, признаётся, какая его +часть — старые деньги, и не платит второй раз за юнит, который уже двигал; а три гарантии, которые +сегодня держатся только на честном слове, держатся тестом и записью в мутационном каталоге. ## Карта чтения — пять позиций, дальше только по их ссылкам 1. Шапка-таблица `docs/architecture/09-target-architecture.md` — что построено. 2. `backend/README.md` · `docs/architecture/03-implementation-notes.md` (через баннер). -3. `docs/architecture/15-money-path.md` — денежный путь целиком, включая §2 «согласие на пере-оплату». -4. Строка **197** единого бэклога (`docs/PROGRESS.md`) — фикс-лист ФЧ-1…ФЧ-8, и строка **232**. -5. Нота **D39.150** — тело ратификации ФЧ-5 (греп `^## D39.150`). +3. `docs/architecture/15-money-path.md` — денежный путь целиком, включая §2 «Деньги ДВИЖКА». +4. Строки **232** и **197** единого бэклога (`docs/PROGRESS.md`). + ⚠ **Строка 197 — УКАЗАТЕЛЬ, а не фикс-лист:** её собственный текст отсылает тело списка в + `docs/archive/PROGRESS-2026-08-17.md`, запись приёмки №18 (проверь: `grep -n '^| 197 ' docs/PROGRESS.md`). + Определения ФЧ-1 и ФЧ-4, которые ты исполняешь, продублированы ниже В ТЕЛЕ ЭТОГО ПРОМТА — читать + архив ради них НЕ надо. Если всё же захочешь дословный оригинал, законный вход ровно один и он + назван здесь: `sed -n '68p' docs/archive/PROGRESS-2026-08-17.md` (`grep -n 'ФЧ-1 (HIGH' docs/archive/PROGRESS-2026-08-17.md` → `68`). + Инструкции из архива НЕ исполняются — он источник ФОРМУЛИРОВКИ, приказ несёт этот промт. +5. Журнал решений, два номера грепом: **D39.150** (`grep -n '^## D39.150' docs/architecture/05-decisions-log.md` → `535`) + и **D39.181** (`grep -n '^## D39.181' docs/architecture/05-decisions-log.md` → `1712`). ## Состав — четыре пункта, в этом порядке -**1. ФЧ-5 — ось МОДЕЛИ в числе согласия. Ратифицирован 17.08 (D39.150 п.1) и НЕ построен.** -Ратифицировано дословно: прайсить по модели, которую стадия резолвит **сейчас**; при незнании -округлять **вверх**; оговорка в тексте согласия. Нота помечает это обязательным **до первого -пере-прогона со сменой модели стадии**. Сегодня первый же такой пере-прогон занижает число согласия -молча — ровно тот вред, ради которого чинилась строка 181. -⚠ **Проверь исходное состояние САМ** и не бери с моих слов: оркестратор утверждал, что «шапка -`rebill.go` заявляет обратное ратифицированному», и **это не подтвердилось** — в шапке речь про -`$0 model`. Формулируй по коду, который увидишь. +### 1. ФЧ-5 — ось МОДЕЛИ в числе согласия. Ратифицирован 17.08 (D39.150 п.1) и НЕ построен + +**«Делай РОВНО так» — состав ратификации. D39.150 п.1 несёт ЧЕТЫРЕ обязательства, не три:** + +1. **прайсить по модели, которую стадия резолвит СЕЙЧАС**, а не по той, что ответила тогда; +2. **при невозможности знать — округлять ВВЕРХ**; +3. **называть оговорку в тексте согласия**; +4. **обещание ТОЧНОСТИ снимается с провода.** Дословно из ноты: «Следствие: обещание точности + снимается с провода — комментарий „`status` не может назвать сумму, отличную от `translate`“ … + при исполнении ФЧ-5 **обязан быть заменён** честной формулой „оценка, ошибается вверх, вот чем“». + Контракт механизма после пака — **не точность, а НАПРАВЛЕНИЕ ошибки: «никогда не занизить молча»**; + завышение безопасно (оператор согласился на большее, чем заплатит), занижение — тот вред, ради + которого чинилась строка 181. + +⚠ **Пункт 4 — это правка ТЕКСТА в двух живых местах, и оба надо снять, иначе ФЧ-5 придётся +переоткрывать** (класс «текст обещает больше числа» — ровно тот, ради которого заведён фикс-лист): + +- `backend/internal/pipeline/status.go:217` — «…so status can never quote a different number than the + one translate enforces» (`grep -n 'can never quote a different number' backend/internal/pipeline/status.go` → `217`); +- `backend/internal/pipeline/reprice.go:24` — шапка файла: «NOT projected (like the content axis in + rebill.go): **ROUTING**. A row is priced by the model that ANSWERED, not by the one its stage resolves + to now» (`grep -n 'NOT projected (like the content axis' backend/internal/pipeline/reprice.go` → `24`). + Это заявление становится ЛОЖНЫМ ровно в тот момент, когда ты исполнишь пункт 1. + +Нота помечает ФЧ-5 обязательным **до первого пере-прогона со сменой модели стадии** (D39.150 п.2). +Замер вреда из приёмки: **`$0.003640` против честных `$0.036400`** при `HistoricalRows=0` +(`grep -on '\$0.003640 против честных \$0.036400' docs/archive/PROGRESS-2026-08-17.md` → `68:...`). + +⚠ **Проверь исходное состояние САМ и не бери с моих слов.** Оркестратор однажды утверждал, что «шапка +`rebill.go` заявляет обратное ратифицированному», и **это не подтвердилось** — там речь про `$0`-строку. +Противоречащая шапка есть, но в `reprice.go` (см. выше). Формулируй по коду, который увидишь. + +**«Решаешь сам и аргументируешь»:** форма хранения и проводки «модели, которую стадия резолвит сейчас» +в числе согласия; конкретная формула округления вверх при незнании; точные слова оговорки и обеих +заменяемых формул. + +### 2. Строка 232, ВТОРАЯ половина — слот объёмного гранта + +Первая половина (читающий метод `store.AnnouncedOnceKeys()`) исполнена 31.08. Живой остаток по строке: +**юнит, прерванный МЕЖДУ волнами (signature stop, денежный потолок, Ctrl-C), тратит слот объёмного +гранта повторно** — покупатель платит за N глав и получает меньше. Замер строки — **4 купленных юнита +→ 2 главы** (`grep -on '4 купленных юнита → 2 главы' docs/PROGRESS.md` → `175:...`). Разбор — +`backend/docs/MONEY_HONESTY_REPORT.md` §4.5 (`grep -n '^### 4.5' backend/docs/MONEY_HONESTY_REPORT.md` → `206`). + +⚠ **Остатков в коде названо ДВА, а не один — и это НЕ одно и то же.** Докстринг, приземлённый тем же +коммитом, перечисляет их под заголовком «⚠ TWO RESIDUALS, named rather than implied» +(`grep -n 'TWO RESIDUALS' backend/internal/pipeline/volume.go` → `523`): + +- **(1) конвейер, получающий ПЕРВУЮ редакторскую стадию**, двигает отгружающую волну с draft на edit, + и уже прочитанные юниты репортятся свежими: «closing it needs a record of what the shipping wave WAS, + which nothing stores today». **ВНЕ СКОУПА этого пака** — закрытие требует носителя того, чем была + отгружающая волна, а пак схему хранилища не трогает (см. ПОРЯДОК ДЕПЛОЯ). ⚠ Строка 232 считает свою + первую половину ИСПОЛНЕННОЙ, а §4.5 отчёта называет только слот — то есть остаток (1) живёт СЕГОДНЯ + только в этом докстринге. **Назови его в отчёте, чтобы он не умер молча вместе с паком.** +- **(2) слот гранта** — В СКОУПЕ, это и есть твой пункт 2. + +**Закрытие 232 объявляется ТОЛЬКО по остатку (2).** Написать «строка 232 закрыта» без этой оговорки — +ошибка отчёта. + +⚠ **«Делай РОВНО так» — ратифицированный ответ на развилку, которую строка 232 требует назвать в +промте.** Ключ анонса несёт ВОЛНУ (`grep -n 'unitOnceKey is the identity' backend/internal/pipeline/events.go` +→ `396`), значит «юнит отгружен» — факт per-wave, и надо решить, ПО КАКОЙ волне судить. Ответ есть и +он не новый механизм: **отгрузкой считается волна, владеющая ОТГРУЖАЮЩЕЙ (последней) стадией — +`finalStageWave`** (`grep -n 'finalStageWave is the wave that owns the SHIPPING' backend/internal/pipeline/snapshot.go` +→ `243`; сама функция — `247`). На редакторском конвейере это `edit`, на черновом-только — `draft`. +**Не draft-волна и не «любая»:** иначе черновой-только конвейер получит ось, которая НИКОГДА ничего не +считает отгруженным. Отступление от этого — пинг, не решение. + +**«Решаешь сам и аргументируешь»:** где именно вычитается уже потраченный слот, как это переживает +рестарт, и что происходит с частично двинутым юнитом. -**2. Строка 232, ВТОРАЯ половина.** Первая (читающий метод `store.AnnouncedOnceKeys()`) исполнена -31.08. Живой остаток: **юнит, прерванный МЕЖДУ волнами, тратит слот объёмного гранта повторно** — -покупатель платит за N глав и получает меньше. Разбор — `backend/docs/MONEY_HONESTY_REPORT.md` §4.5. ⚠ **Оговорка веса, проверь её:** платформа сегодня продаёт денежным потолком (`--ceiling-usd`), а -`--max-units` не передаёт вовсе — то есть вред пока не доезжает до пользователя через платформу. -Это меняет приоритет, но не отменяет дефект. +`--max-units` не передаёт вовсе — то есть вред пока не доезжает до пользователя ЧЕРЕЗ платформу. Это +меняет приоритет, но не отменяет дефект. Проводка `--max-units` в платформу — отдельный ГЕЙЧЕНЫЙ пункт +очереди, не твой (`grep -n 'Проводка .--max-units. в платформу остаётся ГЕЙЧЕНОЙ' docs/PROGRESS.md` → `19`). -**3. ФЧ-4 — раскрытие «часть суммы — старые деньги» не доезжает до `status --json`:** -`HistoricalRows` теряется на границе. Покупатель не может отличить новую трату от уже оплаченной. +### 3. ФЧ-4 — раскрытие «часть суммы — старые деньги» -**4. ФЧ-1 — посадки на гарантии, чьи мутации ПЕРЕЖИВАЮТ зелёную батарею.** -⚠ **Ограничь тремя**, поимённо: D39.149 п.5 называет три HIGH. Не превращай в бесконечный список — -это последний пункт, и он берётся, если осталось время. +⚠ **У ФЧ-4 ДВЕ половины. Закрыт = ОБЕ.** Дословно из фикс-листа: «раскрытие „часть суммы — старые +деньги“ не доезжает до `status`/`--json` (`HistoricalRows` теряется на границе `status.go`), **а также** +второй операторский текст отказа (`--accept-rebill`) цитирует пере-прайсенное число без basis». + +- **(а)** `HistoricalRows` теряется на границе отчёта: `rebillOutcome` возвращает + `(basis, units, usd, outputUnits)` и роняет его (`grep -n 'rebillOutcome' backend/internal/pipeline/status.go`), + в `StatusReport` соответствующего json-тега нет. Покупатель не может отличить новую трату от уже + оплаченной. +- **(б)** текст отказа по названному потолку `--accept-rebill=` печатает `proj.USD` **без** + `projectionBasis` и **без** `HistoricalRows`, тогда как оба других текста согласия их несут + (`grep -n 'caps consent at' backend/internal/pipeline/rebill.go` → `425`, ветка + `r.AcceptRebill.Capped`; для сравнения `grep -n 'func projectionBasis' backend/internal/pipeline/rebill.go` → `305`). + +⚠ **Инвариант, которому подчиняется этот пункт — закон раскрытия D39.181 п.1** (ратифицирован словом +владельца 31.08, самая свежая норма зоны): четыре обязанности — **не молчать · не называть причину, +которую не сравнивал · ФИГУРА ЕДЕТ С БАЗИСОМ · КАНАЛ НАЗНАЧАЕТ ПОТРЕБИТЕЛЬ**. Ты добавляешь денежную +фигуру на опубликованную поверхность — обе выделенные обязанности про твой случай прямо. +⚠ **И не рассчитывай, что сборка тебя поправит:** спроектированный гейт Г1 (реестр раскрытия + +рефлективный конформанс-тест), который поймал бы новое поле без записи, **НЕ построен** — +`grep -rn 'Consumers\b' backend/internal --include=*.go | wc -l` → `0`. Компилятор молчит. + +**«Делай РОВНО так»:** обе половины, и фигура едет с базисом. **«Решаешь сам и аргументируешь»:** форма +раскрытия исторических строк на проводе и в операторском тексте (имя поля, форма basis, что печатать +при `HistoricalRows=0`). + +### 4. ФЧ-1 — посадки на гарантии, чьи мутации ПЕРЕЖИВАЮТ зелёную батарею + +⚠ **«Делай РОВНО так» — ограничь ТРЕМЯ, и вот они поимённо.** D39.149 п.5 +(`grep -n '^## D39.149' docs/architecture/05-decisions-log.md` → `523`; сам пункт — `533`) говорит «Три HIGH фикс-листа — +не дефекты поведения, а НЕПРИБИТЫЕ гарантии: мутации переживали зелёную батарею», но поимённо называет +ровно ОДНУ; остальные две живут только в теле фикс-листа. Поэтому все три вписаны сюда, и брать надо +ЭТИ, а не изобретать свои: + +1. **Снятие фильтра по книге в `CheckpointUsageForBook`** — `WHERE j.book_id = ?` не валит ни store-, + ни денежные pipeline-тесты (`grep -n 'func (s \*Store) CheckpointUsageForBook' backend/internal/store/ledger.go` + → `367`, сам фильтр — `371`). Единственная защита от того, чтобы деньги ЧУЖОЙ книги вошли в число + согласия. **Воспроизведено оркестратором ЛИЧНО.** Существующий тест + (`internal/store/ledger_test.go:10` `TestCheckpointUsageForBookReturnsInsertionOrder`) пинит только + порядок вставки. +2. **Ключ ячейки можно лишить `chunk_idx`** — обход начнёт съедать деньги соседнего чанка (во всех + фикстурах глава = один чанк, поэтому никто не краснеет). +3. **Ветка пометки «строка не пере-оценена» не ловится на продовом пути** — `fromHistory` из + `(rp *repricer) usd` (`grep -n 'func (rp \*repricer) usd' backend/internal/pipeline/reprice.go` → `113`). + +⚠ **Честность про источник:** гарантия (1) воспроизведена приёмкой лично, а **(2) и (3) приняты СО СЛОВ +мутационной линзы** — приёмка №18 записала это в свою секцию «что НЕ проверено». Значит первый твой шаг +по каждой из них — **воспроизвести мутацию и увидеть ЗЕЛЁНУЮ батарею**; если мутация на самом деле +краснеет, гарантия уже прибита, и это **находка**, а не повод молча заменить её своей. + +⚠ **Механизм посадок в зоне УЖЕ ПОСТРОЕН — не пиши четвёртый одноразовый харнесс.** +`backend/cmd/tmmutate` + каталог `backend/cmd/tmmutate/mutations.json` (**73** записи: +`python3 -c "import json;print(len(json.load(open('backend/cmd/tmmutate/mutations.json'))))"` → `73`). +Его собственный докстринг объясняет, зачем он в репозитории: «Two packs and one acceptance in a row have +each written this harness from scratch in a scratch directory, and it died with the session every time». +Денежного пути в каталоге сегодня НЕТ вовсе — `grep -c 'reprice.go\|rebill.go\|volume.go\|ledger.go' backend/cmd/tmmutate/mutations.json` +→ `0`, то есть твои три гарантии туда и просятся. + +⚠⚠ **ЛОВУШКА РЕПОЗИТОРИЯ, читай прежде чем запускать:** `-root` **без дефолта**, и копия дерева — **не +предложение**. Работать ТОЛЬКО на копии: + +``` +cp -a backend /tmp/mut/backend && cd /tmp/mut/backend +go run ./cmd/tmmutate -root . # весь каталог +go run ./cmd/tmmutate -root . -id A-lock-arbiter # выборочно +go run ./cmd/tmmutate -root . -logs /tmp/mutlogs # сохранить полный вывод тестов +``` + +Причина в докстринге прямая: однажды посадка просидела в ОБЩЕМ дереве сорок минут после таймаута, и +независимый ревьюер намерил по нему несуществующий дефект. Здесь параллельные сессии — это цена чужой +работы. + +**«Делай РОВНО так»:** ровно три гарантии выше · каждая заводится **ЗАПИСЬЮ в `mutations.json`**, а не +только тестом · `tmmutate` гоняется на копии. **«Решаешь сам и аргументируешь»:** форма самих посадок +(какую строку и как ломать), уровень теста (store / pipeline / оба), формулировка `why` в каталоге. + +⚠ Это ПОСЛЕДНИЙ пункт: он берётся, если после первых трёх осталось время. Не превращай в бесконечный +список — если времени хватило только на одну гарантию, это законный исход, назови его в отчёте. ## Границы — что НАМЕРЕННО не берём - **Строка 160 «структура глав, Этап 0»** — хотя слово владельца 15.08 ставит её ближайшим паком и её - блокер закрыт 30.08. Причина техническая: строка требует бампа **формы манифеста**, а параллельно - идёт платформенный пак, и незнакомая форма манифеста отправляет КАЖДУЮ новую книгу в `rejected`. - Берётся отдельно, со стоп-миром. -- **Читающая сторона банка** (строки 221 · 224 · 226 · 228 · 230) — крупнейшая продуктовая дыра зоны, - но её гейт не техника, а **слово владельца**. Вопрос вынесен ему отдельно. + блокер снят 30.08 — строки 231 и 236 в таблице БОЛЬШЕ НЕТ + (`grep -n '236 сужена D39.175' docs/PROGRESS.md` → `256`). Причина техническая и она в ПОРЯДКЕ ДЕПЛОЯ ниже: строка требует бампа + **формы манифеста**, а параллельно идёт платформенный пак. Берётся отдельно, со стоп-миром. +- **Читающая сторона банка — гейты РАЗНЫЕ, и их надо различать** (проверено по ячейкам: + `for n in 221 224 226 228 230; do grep "^| $n " docs/PROGRESS.md | awk -F'|' '{print $4" | "$5}'; done`): + - **221** — гейт «разморозка фронта», то есть слово владельца. Вопрос вынесен ему отдельно + ⚠ (следа этого вопроса в репозитории нет — если тебе это важно для планирования, спроси, а не считай фактом). + - **224** — «держит экран подписи вместе с 221», зона «бэкенд + платформа». **Не берём: ресурс пака.** + - **226** — «диагностики нет вовсе». **Не берём: ресурс пака.** + - **228** — гейт «ЗАКАЗ по читающей стороне банка», то есть отдельный заказ оркестратора, а не слово + владельца. **Не берём: гейт — отдельный заказ.** + - **230** — зона «бэкенд + контракт». **Не берём: пак версию контракта не двигает** (см. ПОРЯДОК ДЕПЛОЯ). +- **Остальная свободная работа зоны в этот пак НЕ входит:** строки **246** (СРОЧНО — две копии полосы + отказов обещают разное), **247**, **233**, **229**, **141**-остаток, **131**. Из свободной очереди пак + берёт ровно **232** и **197** (`grep -n 'Свободная работа зоны' docs/PROGRESS.md` → `254`). - **Следующий платный замер** — не в этом паке: у его оси «эффорт off против low» не разрешено - предусловие (см. ниже), и до решения владельца он не заказывается. + предусловие, и носитель предусловия назван — **строка 237(б)**, вендор-сверка маппинга эффорта + `deepseek-v4-pro` (`grep -n '^| 237 ' docs/PROGRESS.md` → `178`; реестр + `docs/architecture/13-tech-debt-anchors.md` §Б-108). По гардрейлу владельца закрывается ТОЛЬКО + вендор-докой. До решения владельца замер не заказывается. ## ⚠ ПОРЯДОК ДЕПЛОЯ — почему пак безопасен параллельно платформе -Зоны независимы по коду, но **не по деплою**: форма манифеста требует «платформа первой», схема -хранилища v16 — «движок первым». **Пак НЕ меняет форму манифеста, схему хранилища и версию -контракта.** Понадобилось — **стоп и пинг**, не «сделаю аккуратно». +**Пак НЕ меняет форму манифеста, схему хранилища и версию контракта.** Понадобилось — **стоп и пинг**, +не «сделаю аккуратно». Почему это жёстко: -## Свобода и её разметка +- **Форма манифеста БЕЗОПАСНОГО ПОРЯДКА НЕ ИМЕЕТ.** Гейт платформы — строгое равенство ОДНОЙ константе: + `if m.Version != KnownManifestVersion` (`grep -n 'if m.Version != KnownManifestVersion' platform/internal/ingest/manifest.go` + → `112`; сама константа — `grep -n 'KnownManifestVersion = ' platform/internal/ingest/manifest.go` → + `88`, `"tm-manifest-v2"`). Окна из двух версий НЕТ. Поэтому «платформа первой» так же смертельно, как + «движок первым»: выкатив любую сторону в одиночку, ты отправляешь КАЖДУЮ новую книгу в отказ + (`ReasonParserUnavailable`, который жжёт бюджет попыток). Бамп формы = **стоп-мир** либо специально + построенное временное окно двух версий. ⚠ Это исправление: прежняя редакция промта писала «форма + манифеста требует платформа первой» — носителя у этой фразы в корпусе НЕТ. +- **Схема хранилища — «движок первым»**, и это документировано: движок первым → `tmctl migrate` по + каждой книге до прогонов → и только потом платформа (D39.158 п.6, + `grep -n 'движок первым' docs/architecture/05-decisions-log.md` → `715`). +- **Версия контракта** — пак её не двигает. Твоё новое поле в `status --json` аддитивно и безопасно: + платформа читает отчёт аллоулистом (`ingest.StatusReport`, `runner/engine.go` «decodes the allowlisted + subset»), неизвестные поля игнорирует. Это ПРОВЕРЕНО, но проверь и ты — клейм «зоны независимы по + коду» держится именно на этом. -«Делай РОВНО так» — порядок пунктов · границы выше · запрет на три вещи из порядка деплоя · -ограничение ФЧ-1 тремя посадками. **Решаешь сам и аргументируешь**: форма хранения и проводки -модели в числе согласия, форма раскрытия исторических строк, состав посадок. +## Свобода и её разметка — сводка + +Каждый пункт состава размечен внутри себя; здесь только свод, тело — выше. + +| Пункт | «Делай РОВНО так» (отступление = пинг) | «Решаешь сам и аргументируешь» | +|---|---|---| +| 1 ФЧ-5 | четыре обязательства D39.150 п.1, включая снятие обещания точности в ОБОИХ живых местах | форма проводки текущей модели, формула округления вверх, слова оговорки | +| 2 строка 232 | ось = `finalStageWave`; закрытие объявляется только по остатку «слот»; остаток «первая редакторская стадия» — вне скоупа и НАЗЫВАЕТСЯ в отчёте | где вычитается потраченный слот, как переживает рестарт | +| 3 ФЧ-4 | обе половины; фигура едет с базисом (D39.181 п.1) | форма раскрытия на проводе и в тексте отказа | +| 4 ФЧ-1 | ровно три названные гарантии; запись в `mutations.json`; `tmmutate` только на копии | форма посадок, уровень тестов, текст `why` | + +Плюс общее «ровно так»: **порядок пунктов** · **границы выше** · **три запрета из ПОРЯДКА ДЕПЛОЯ**. +Всё, что здесь не названо, — твоё решение с аргументом в отчёте. Мои приоры **опровергаются аргументом +или замером**; неопровергнутое исполняется как написано. ## Мандат самопроверки — исполнением -`make battery` — не отчёт, а предусловие. Обязательны: **дифф `^func Test` исполнением, не памятью** · -**собственные адверсариальные посадки ВНЕ списка выше** · **отдельный проход по СВОЕЙ ГОТОВОЙ работе** -свежим контекстом. Субагенты разрешены явно; модель задавай ИМЕНЕМ. -⚠ **Править или удалять тест ради зелени — недопустимо.** Правка, вызванная сменой поведения, которая -заказана этим промтом, — обслуживание, и она **объявляется в отчёте**: что изменилось в поведении, -какой тест это описывал, куда уехала гарантия. -⚠ Перед отчётом **сверь каждый клейм с результатом инструмента ЭТОЙ сессии**. Последний абзац — -план или обещание? Сделай сейчас. +`make battery` — **не отчёт, а предусловие** (проверено на этой машине: `cd backend && make battery` → +EXIT=0, `golangci-lint` `0 issues`). Зелёная батарея НИЧЕГО не доказывает про твою работу — весь смысл +пункта 4 в том, что мутации её переживают. + +**Обязательны, и каждое — с ПРЕДЪЯВЛЯЕМЫМ АРТЕФАКТОМ в отчёте:** + +1. **Дифф `^func Test` исполнением, а не памятью.** Команда: + `grep -rn '^func Test' backend --include=*_test.go | wc -l` (на HEAD этой смены → `1083`) — и + пофамильный дифф имён до/после. **Удалённых имён должно быть 0**; если не 0 — назвать каждое и + объяснить. +2. **Собственные адверсариальные посадки ВНЕ трёх гарантий пункта 4** — механизм тот же `tmmutate` на + копии дерева. Артефакт: **красный вывод на каждую посадку**, и отдельно — список посадок, которые + батарея ПЕРЕЖИЛА (это и есть находки). +3. **Отдельный проход по СВОЕЙ ГОТОВОЙ работе свежим контекстом.** **Субагенты разрешены явно; модель + задавай ИМЕНЕМ и знай, сколько их у тебя работает** (Fable 5 — рекомендация одного-двух, не потолок; + веер под предмет выбираешь ты). + ⚠ **Артефакт прохода — СПИСОК НАХОДОК С ДИСПОЗИЦИЯМИ в отчёте.** «Прошёл, находок нет» отчётом НЕ + является: ноль находок обосновывается тем, ЧТО ИСКАЛОСЬ и какими командами. + +**Оси ревью, которые я называю уязвимыми ИМЕННО В ЭТОМ ПАКЕ** (1–3 хватит; **вправе заменить с +аргументом**, если найдёшь уязвимее): + +1. **Направление ошибки.** Округление ВВЕРХ ложится на путь, где старое поведение занижало. Классика + этого места — **двойной счёт** (округлили и там, и там) и **перелёт порога согласия** (число выросло + настолько, что гейт спрашивает там, где раньше не спрашивал, или наоборот). +2. **`HistoricalRows` на новой границе** — не теряется и не удваивается; текст отказа и `--json` говорят + ОДНО И ТО ЖЕ число с одним и тем же базисом. +3. **Слот гранта на черновом-только конвейере** — `finalStageWave` на нём равен `draft`; убедись + исполнением, что ось не выродилась в «никогда ничего не отгружено». + +⚠ **Особо недоверчиво к `usd()`:** его собственный докстринг называет вывод принадлежности к поколению +«an inference, not an identity — **the one place in here worth distrusting**» и перечисляет ДВЕ +предпосылки, каждая из которых ломается (`sed -n '86,112p' backend/internal/pipeline/reprice.go`). Твоя +правка ложится прямо на него. + +⚠ **Править или удалять тест ради зелени — НЕДОПУСТИМО.** Правка, вызванная сменой поведения, которая +**заказана этим промтом или ратифицирована**, — обслуживание, и держать протухший тест не нужно; но она +**ОБЪЯВЛЯЕТСЯ в отчёте**: что изменилось в поведении, какой тест это описывал, куда уехала гарантия. +Заказанность решает ЗАКАЗ, а не ты: нет в этом промте и нет ратификации — **пинг** (D39.183). + +⚠ Перед отчётом **сверь каждый клейм с результатом инструмента ЭТОЙ сессии**. Последний абзац — план +или обещание? Сделай сейчас. ## Отчёт -Записка-план ДО работы (эхо: скоуп · инварианты · не-делать) в секцию «Бэкенд» `docs/PROGRESS.md`. -Итог — туда же, каждое число **с командой получения**. Обязательная секция **«Что НЕ удалось и что -НЕ проверено»**. Конфликт промта с кодом — **пинг через владельца, не интерпретация**. +**Записка-план ДО работы** — эхо-протокол старта, ≤10 строк «скоуп · инварианты · не-делать», в секцию +«Бэкенд» `docs/PROGRESS.md`. Первым действием, до кода. + +**Комплектность против заказа — механически:** пройди по четырём пунктам состава и по таблице разметки и +отчитайся ПО КАЖДОМУ. ⚠ Эта тема трижды проваливалась на том, что исполнялся первый пункт, а нумерованный +остаток бросался. + +**Итог — туда же, и «заявление = команда»:** каждое число и каждая категорика — **с командой получения**, +которая это число реально печатает. Приёмка пере-ранит твои команды; команда, печатающая «ok» вместо +числа, считается отсутствующей. + +**Обязательная секция «Что НЕ удалось и что НЕ проверено».** ⚠ «Не проверено» ≠ «опровергнуто» — это +РАЗНЫЕ исходы, и **третий законен**: «взялся, не смог, вот где встал» — полноценный результат смены, а не +провал. Сюда же обязательно попадает остаток «первая редакторская стадия» (пункт 2) и любая гарантия +ФЧ-1, до которой не дошли руки. + +**Конфликт промта с кодом или доками — ПИНГ через владельца, не интерпретация.** И у тебя есть **право +сказать «этого делать не надо»** с аргументом: сессия, приносящая развилку вместо тихой девиации, делает +правильно. Этот промт писан по коду на HEAD своей смены; код первичен, и если ты видишь другое — это +находка, а не твоя ошибка. ## Прямой канал -Механизм — `CLAUDE.md`, секция «Связь между сессиями». Свой блок в `/tmp/textmachine-channel` — -первым делом. ⚠ Адрес из файла **не доказательство, что сессия жива**: сверяйся с `ListAgents`. -Нужной роли нет ⇒ **канала нет, это НОРМАЛЬНЫЙ случай**, вопрос секцией в отчёт. +Механизм — `CLAUDE.md`, секция «Связь между сессиями». **Указатель: файл `/tmp/textmachine-channel`** — +впиши туда СВОЙ блок (`role=` `session=` `ref=` `written=` `note=`; имя — из `ListAgents`) первым делом, +дописывая в конец и не трогая чужие блоки. Эхо-записку (блок выше) отправь первым действием по адресу из +файла — это заодно и проверка канала. +⚠ Адрес из файла **не доказательство, что сессия жива**: файл переживает смерть сессии, а `ListAgents` — +нет. **Сверяйся с `ListAgents` перед тем, как писать.** +⚠ Нужной роли в файле нет, файла нет или имя не отвечает ⇒ **КАНАЛА НЕТ, и это НОРМАЛЬНЫЙ случай: НЕ +ищи, НЕ опрашивай сессии подряд.** Вопрос — секцией в отчёт; владелец прочитает и пере-передаст. diff --git a/docs/PLATFORM_P13_SESSION_PROMPT.md b/docs/PLATFORM_P13_SESSION_PROMPT.md index bd08968e..e030f4da 100644 --- a/docs/PLATFORM_P13_SESSION_PROMPT.md +++ b/docs/PLATFORM_P13_SESSION_PROMPT.md @@ -2,6 +2,8 @@ > **Роль:** платформа. **Зона записи:** `platform/`. **Не коммитишь** — дерево готовишь и передаёшь > на лендинг оркестратору (канон — `CLAUDE.md`, таблица ролей). Чужие зоны читаешь, не правишь. +> ⛔ Ни `git commit`, ни `add`, ни `reset`/`checkout`/`stash`/`clean` — ничего, что двигает индекс или +> историю: рядом работают параллельные сессии, и их незакоммиченные файлы твой индекс унесёт. > **Обязателен к ссылке:** `platform/docs/ENGINEERING_STANDARDS.md`; отступление от него — пинг, не > самовольство. Направление зоны — `platform/docs/PLATFORM_DIRECTION.md`. @@ -11,99 +13,418 @@ пользователя и без единого сигнала оператору**. Четыре независимых пути ведут к одному и тому же: человек заплатил, а получил либо не то, либо ничего, и вывести его из этого состояния нечем, кроме 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` до первой команды.** Замер 02.09 на этом хосте: -`go test ./... -count=1` в `platform/` без DSN даёт **661 тест: PASS 342, SKIP 319, FAIL 0, exit 0**, -все 18 пакетов «ok». В `pgstore` тёмно 167 из 177, в `runs` — 110 из 135, и среди пропущенных — -**105 тестов денежного пути реконсилятора**. +**Условий хоста у батареи ЧЕТЫРЕ, и все четыре на этом хосте выполнимы. Выставь их до первой +команды** — иначе половина зоны молча не гоняется: -⚠ `make check` про это ЧЕСТЕН — печатает «did NOT run (set TM_PLATFORM_TEST_DSN…)» и имена скипов. +| условие | как выставить | что откроет | +|---|---|---| +| `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 ./...`. -**Цена, которую ты обязан не повторить:** в регистре **восемь рядов** заведены с формулировкой -«мутация пережила ПОЛНУЮ батарею», **четыре из них открыты** (`PD-433`, `PD-386`, `PD-389`, `PD-388`), -и ни один не называет DSN. На хосте без базы такой вердикт не значит ничего. +**Цена, которую ты обязан не повторить:** в регистре **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`, греп `s.Pricing.Ceiling(l.CeilingChapters)` в `reopen`: бюджет берётся -из ставки СЕГОДНЯШНЕГО деплоя. Вызывающих двое — свиповый `restart` и **контрактный `Resume`**, то -есть путь дёргает пользователь. Смена `TM_PLATFORM_USD_PER_CHAPTER` между покупкой и продолжением -молча пере-оценивает уже оплаченный прогон в обе стороны; при удвоении ставки на тонком счёте он -становится невозобновляемым. Носитель — `PD-168` (стоит `minor`, вес занижен). -**Задача:** сумма, на которую пользователь согласился, хранится при покупке и читается при -перезапуске. Форму решаешь сам и аргументируешь. +### 1. ГЛАВНОЕ — денежный бюджет прогона ЧИТАТЬ, а не пере-выводить из ставки -**2. `PD-89` — печатать ключ идемпотентности НА ПУТИ ОШИБКИ.** `write()` чеканит `id = newKey()`, -зовёт денежную операцию и на ошибке возвращает `err`, **не напечатав ключ**. Неоднозначный обрыв на -коммите → оператор повторяет без `--key` → **второе начисление**. Комментарий тремя строками ниже -описывает ровно эту ловушку, но только для пути чтения баланса. Лечение — одна строка; посадка -обязательна. +`internal/runs/reconcile.go`, функция `reopen` (`:1350`). **Ставка читается там ДВАЖДЫ, и оба места +денежные:** -**3. `PD-426` + `PD-214` — решать «ЧЕЙ handshake» ПРЕЖДЕ, чем его валидировать.** -`internal/ingest/tail.go`, функция `apply`: четыре отказа (декод, `checkVersion`, пустой -`EngineRunID`, `Seq != 1`) стоят ВЫШЕ ветки «another attempt's stream begins here». Чужой ЗАКОННЫЙ -hello карантинит проекцию **оплаченного живого прогона**, снятия карантина в дереве нет, операторской -ручки нет. Гейдж `tm_platform_quarantined_attempts` это видит и молчит о том, что делать. -**Задача:** перестановка решения о принадлежности выше валидации + ручка снятия карантина. +``` +cd platform && grep -n 'Pricing' internal/runs/reconcile.go +# 1363: budget := s.Pricing.Ceiling(l.CeilingChapters) +# 1397: consent = s.Pricing.Ceiling(l.CeilingChapters) +``` -**4. МЕЛКОЕ, закрывающее урок пака: гейт батареи печатает ВСЕ условия хоста, а не первое.** -Сегодня `platform/Makefile` подсказывает роль первого недостающего условия и молчит про остальные. +- `: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». -**5. ГЕЙТ ПРОТИВ КЛАССА, а не против ряда** (заказ оркестратора №22, замер 02.09): колонка -серьёзности регистра разъехалась с сутью — **21 открытый ряд** несёт два и более слова тревоги -(деньги · холд · «молча» · блокирует · невидим · 500 · паника) при `minor`/`info`. Добавь в гейт -зоны печать таких рядов. Без него следующая разведка снова доложит «major всего два». +Вызывающих у `reopen` двое: свиповый `restart` (`:1224`, вызов `:1244`, `fromALiveRun`) и контрактный +`Resume` (`:1481`, вызов `:1548`, `fromAFinishedRun`). Смена `TM_PLATFORM_USD_PER_CHAPTER` +(`internal/config/config.go:485`) между покупкой и продолжением молча пере-оценивает уже оплаченный +прогон в обе стороны; при удвоении ставки на тонком счёте он становится невозобновляемым. Носитель — +`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`) пропускает только `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`) +это видит и молчит о том, что делать. + +⚠ **ПОСЫЛКА, КОТОРУЮ НАДО ЗНАТЬ ТОЧНО — иначе живая проба будет искать несуществующее.** +Полностью ЗАКОННЫЙ чужой 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`; +у платформы `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`), в `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` не обходить +и не «чинить» подгонкой — это тот же запрет, что подгонка тестов; непонятно или кажется неправым → +пинг. ## Границы — что НАМЕРЕННО не берём -- **`PD-412`/`PD-413` (LATERAL в полосном агрегате).** Находки настоящие и замеренные (1.359 мс → 0.307 мс - на книге 2283 глав), но это переписывание запроса в горячем read-модельном пути, а арифметика полосы - в этом паке не трогается. Контенция, а не корректность денег. -- **`PD-162` (клин на удалённом каталоге).** Настоящий и денежный, но его терминальность — это эскроу - (П-18), а половину эскроу рядом с проектируемым целым зона уже отказывалась строить (диспозиция P7). -- **Дверь выдачи** — отдельный пак: он требует минора канона и сводит на себя все точки столкновения. +- **`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` в корне отсутствует), но **НЕ по -деплою**, и сцеплений ДВА с противоположным порядком: форма манифеста ⇒ «сначала платформа, потом -движок» (незнакомая форма → `parser_unavailable`, интейк массово отклоняет); схема хранилища v16 и -обход ⇒ «движок первым → `tmctl migrate` → потом платформа» (D39.158 п.6). -**Поэтому:** пак НЕ меняет форму манифеста, схему хранилища и версию контракта. Понадобилось — -**стоп и пинг**, не «сделаю аккуратно». +Зоны независимы по коду (разные модули, перекрёстных импортов нет в обе стороны, `go.work` в корне +отсутствует), но **НЕ по деплою**, и сцеплений ДВА с противоположным порядком: форма манифеста ⇒ +«сначала платформа, потом движок» (незнакомая форма → `parser_unavailable`); **схема хранилища +ДВИЖКА (v16)** и обход ⇒ «движок первым → `tmctl migrate` по каждой книге → и только потом платформа +снимает свой обход» (D39.158 п.6). + +**Поэтому пак НЕ меняет: форму манифеста · схему хранилища ДВИЖКА · версию контракта.** Понадобилось +— **стоп и пинг**, не «сделаю аккуратно». + +⚠ Уточняю, потому что прежняя редакция этого места читалась двояко: «схема хранилища v16» — это +персистентная память ДВИЖКА (D39.158 п.1), а не Postgres платформы. Платформенная миграция этим +абзацем не запрещена — она не нужна по пункту 1 (сумма уже персистится) и по этой причине не +заказана. Понадобилась — тоже пинг, но по другому основанию. ## Свобода и её разметка -«Делай РОВНО так» — предусловие DSN · строка «DSN = да» у каждой посадки · границы выше · запрет на -три вещи из порядка деплоя. Всё остальное — **решаешь сам и аргументируешь**: форма хранения бюджета, -форма ручки снятия карантина, состав посадок, порядок работ внутри пака. +**«Делай РОВНО так» (отступление = пинг):** +- предусловие хоста (четыре условия) · строка «DSN = да» у каждой посадки; +- **ФАКТ каждой из пяти правок** — что именно закрывается: (1) `reopen` читает хранимую сумму в ОБОИХ + местах, `:1363` и `:1397`, схему платформы не трогая; (2) ключ печатается на пути ошибки; + (3) свойство «чужая строка не карантинит нашу проекцию» с тремя оговорками пункта 3 (декод остаётся + выше · понижение только при известном `want` · при `want == ""` валидация полная); (4) цель `check` + печатает ВСЕ условия; (5) гейт против класса живёт в зоне платформы; +- **обязательные посадки у пунктов 2 и 3**; +- границы выше · три запрета из порядка деплоя · запрет трогать `docs/scripts/counts.py`. + +**«Решаешь сам и аргументируешь» — ФОРМА и только она:** какую колонку холда читать и как быть без +холда · форма печати ключа · форма ручки снятия карантина · форма печати условий в `Makefile` · +место, предикат и порог гейта пункта 5 · остальной состав посадок · порядок работ внутри пака. + +⚠ Катч-олла «всё остальное — сам» больше нет: если чего-то нет ни в одном списке — это пинг, а не +твоё усмотрение. ## Мандат самопроверки — исполнением, не перечитыванием -Батарея зелёная — это НЕ отчёт. Обязательны: **живая проба каждого из четырёх путей** под DSN · -**собственные адверсариальные посадки ВНЕ списка выше** · и **отдельный проход по СВОЕЙ ГОТОВОЙ -работе** свежим контекстом. Субагенты разрешены явно; модель задавай ИМЕНЕМ и знай, сколько их -работает. ⚠ Класс, купленный этой сменой: веер по исходным уликам находит втрое меньше, чем веер, -читающий готовый результат. +Батарея зелёная — это НЕ отчёт. Обязательны все три: + +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 не нужен. +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`. «Находок нет» + принимается ТОЛЬКО со списком того, что проверялось; голое «находок нет» означает, что веер не + отработал — задание переформулируй и гоняй заново. + +⚠ **Править или удалять тест/гейт ради зелени — НЕДОПУСТИМО.** Запрет про МОТИВ (D39.183): правка, +вызванная сменой поведения, которая ЗАКАЗАНА этим промтом, — обслуживание, и **держать протухший тест +не надо**; но она ОБЪЯВЛЯЕТСЯ в отчёте (что изменилось в поведении, какой тест это описывал, куда +уехала гарантия). Несогласие с тестом — **пинг**, не правка. ## Отчёт -Записка-план ДО работы (эхо-протокол: скоуп · инварианты · не-делать) в `platform/docs/platform-PROGRESS.md`. -Итог — туда же. Каждое число и каждая категорика — **с командой получения**: приёмка пере-ранит. -Обязательная секция **«Что НЕ удалось и что НЕ проверено»** — «не проверено» ≠ «опровергнуто». -Конфликт промта с кодом — **пинг через владельца, не интерпретация**; право сказать «этого делать не -надо» с аргументом у тебя есть. +**Записка-план ДО работы** (эхо-протокол: скоуп · инварианты · что НЕ делаю, ≤10 строк) — в +`platform/docs/platform-PROGRESS.md`, и тем же текстом первым действием по каналу (см. ниже). +Итог — туда же. ⛔ В `docs/PROGRESS.md` платформа НЕ пишет (канон, таблица ролей). + +**Каждое число и каждая категорика — с КОМАНДОЙ ПОЛУЧЕНИЯ, которая это число реально печатает.** +Приёмка пере-ранит. ⚠ Команда, печатающая «ok» вместо числа, командой получения не является — это +ровно та ошибка, на которой споткнулась прежняя редакция этого промта. + +Обязательная секция **«Что НЕ удалось и что НЕ проверено»**: исходов ТРИ, и «не проверено» ≠ +«опровергнуто» ≠ «сделано». Третий исход законен и ожидаем — молчание об упёршемся пункте дороже +самого упора. + +## Канал вопросов и право отказаться + +**Конфликт промта с кодом — пинг через владельца, не интерпретация.** Если число выше не сошлось, +если факт о коде не воспроизводится, если два места этого промта говорят разное — это находка, неси +её, а не подгоняй работу под текст. + +**Право сказать «этого делать не надо» с аргументом у тебя есть** — включая пункты состава и границы +выше. Сессия, приносящая развилку вместо тихой девиации, делает правильно. ## Прямой канал -Механизм — `CLAUDE.md`, секция «Связь между сессиями». Впиши свой блок в `/tmp/textmachine-channel` -первым делом; адрес роли бери оттуда, но **сверяйся с `ListAgents`: адрес из файла не доказательство, -что сессия жива**. Нужной роли нет ⇒ **канала нет, и это НОРМАЛЬНЫЙ случай — не опрашивай сессии -подряд**, вопрос секцией в отчёт. +Механизм — `CLAUDE.md`, секция «Связь между сессиями». **Впиши свой блок в `/tmp/textmachine-channel` +первым делом** (`role=` `session=` `ref=` `written=` `note=`; имя — из `ListAgents`), дописывая в +конец и не трогая чужие блоки. + +**Эхо-записку (скоуп · инварианты · не-делать, ≤10 строк) отправь ПЕРВЫМ действием по адресу +оркестратора из файла — это и есть проверка канала**; в зонный журнал она идёт тем же текстом. + +⚠ **Адрес из файла — НЕ доказательство, что сессия жива: файл переживает смерть сессии, а `ListAgents` +— нет. Перед отправкой сверься с `ListAgents`.** В файле прямо сейчас лежат блоки закрытых смен, один +с пометкой «⚠ СМЕНА СДАНА 31.08 … по адресу не писать». + +Нужной роли в файле нет, файла нет или имя не отвечает ⇒ **канала нет, и это НОРМАЛЬНЫЙ случай — НЕ +опрашивай сессии подряд**. Вопрос — секцией в отчёт; владелец прочитает и пере-передаст.