textmachine/docs/BACKEND_DOFIX_2026-09-03.md

211 lines
21 KiB
Markdown
Raw Permalink 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.

# ДОФИКС бэкенд-паку «число согласия на пути второй покупки» — по приёмке оркестратора №22 (03.09.2026)
> **Роль:** бэкенд, та же сессия, что отработала пак. **Зона записи:** `backend/` + своя секция
> «Бэкенд» в `docs/PROGRESS.md`. Чужие зоны читаешь, не правишь. **Не коммитишь** и не трогаешь индекс
> и историю — запреты и причина: `CLAUDE.md`, «Git-координация мультисессий».
> ⛔ **В индексе сейчас лежит НЕЗАКОММИЧЕННАЯ работа двух смен (46 файлов).** Ни одной пишущей
> git-команды: ни `checkout`, ни `restore`, ни `stash`, ни `reset`, ни `clean`, ни `add`, ни `commit`.
## Что это и почему оно пришло
Пак **принят с дофиксом**. Приёмка шла не чтением отчёта: десять линз (пять на зону), каждая обязана
была проверять исполнением, каждая находка ушла опровергателю с мандатом её ОПРОВЕРГНУТЬ своей
командой. По бэкенду 20 находок, 19 пережили опровержение.
⚠ **Твой код в основном подтверждён — ниже отдельная секция «что приёмка проверила и признала», и её
переделывать не нужно.** Дофикс про другое: пак создал один дефект в том, что сам чинил, оставил
ложным одно РАТИФИЦИРОВАННОЕ утверждение, и три свойства, объявленные твоими, тестом не прибиты —
мутации приёмки их пережили.
## Карта чтения — три позиции
1. Этот файл. Твой прежний промт `docs/BACKEND_CONSENT_SESSION_PROMPT.md` — уже прочитан, перечитывать
целиком не надо.
2. Свой отчёт в `docs/PROGRESS.md`, секция «Бэкенд», запись 03.09 — по пунктам Б7Б10 правится он сам.
3. Улики ниже. **Каждая проверена приёмкой исполнением; воспроизведи их сам** — если что-то не
воспроизводится, это находка, неси её, а не подгоняй работу под текст.
## Б1 [важное] — стоп-строка и лог лгут ровно в том случае, который пак СОЗДАЛ
`bound()` теперь истинен при `Carried > 0`. Значит прогон, дошедший **до конца книги** и лишь
дописавший начатое, печатает оператору самопротиворечие:
```
backend/internal/pipeline/volume.go:220 — «stopped on the VOLUME ceiling (--max-units %d),
not on money and not at the end of the book» — префикс БЕЗУСЛОВЕН, внутри самого fmt.Sprintf
и тут же в том же предложении: «Still in the book: 0 unit(s) NEVER delivered»
```
Воспроизведено живым прогоном настоящего `Runner` (две главы, первый прогон умирает на редакторе,
второй с `MaxUnits=2`): `Left()=0 granted()=0 Paid()=2`, и обе половины предложения печатаются.
Тот же текст едет в лог `waverun.go:173`, а комментарий у вызова `waverun.go:169-170` («Only set when
the ceiling actually held work back») протух — **в файле, который пак правил**.
**Делай РОВНО так:** предложение разводится на два случая — грант кончился / книга кончилась.
**Решаешь сам:** формулировки обеих веток и нужен ли отдельный текст для смешанного случая.
⚠ Пункт 2 пака закрывался ИМЕННО как дисклоужер — объявлять его закрытым до этой правки нельзя.
## Б2 [важное] — ратифицированный контракт кадра `finished.volume` стал ЛОЖНЫМ
`D39.181 п.2` объявляет ПРИСУТСТВИЕ поля булевым признаком «грант придержал работу»:
```
backend/internal/runevents/runevents.go:189 «present only when a volume grant actually held work back»
backend/internal/runevents/runevents.go:202-204 «its PRESENCE is the boolean: the engine attaches this
only when the grant actually held something back»
те же утверждения — backend/internal/pipeline/events.go:584-588 и bookrun.go:91
```
Кадр теперь приезжает и у прогона, дошедшего до конца книги (`left_fresh:0`, `left_rework:0`), и
`carried` на провод не выходит вовсе — то есть читатель различить два состояния НЕ МОЖЕТ ничем.
Пак переписал соседние поля ТОЙ ЖЕ структуры (`Delivered`, `Free`), а утверждение тремя строками выше
оставил. Сегодня безопасно только потому, что потребителя нет: `platform/internal/ingest/events.go`
декодирует `Finished{Outcome}` и всё.
**Делай РОВНО так:** текст контракта приводится в соответствие с поведением ВО ВСЕХ ЧЕТЫРЁХ местах.
**Решаешь сам:** формулировка. ⛔ **Поля на провод НЕ добавлять** — это класс «новое поле в
опубликованном документе», он требует ратификации и минора `StreamVersion`, и владелец 03.09 уже
сказал «оставить как есть». Если считаешь, что честный контракт без поля невозможен, — **пинг**, а не
поле.
## Б3 [важное] — три свойства объявлены твоими и НЕ прибиты: мутации приёмки пережили весь пакет
Все три посажены приёмкой на копии дерева, каждая SURVIVED на `./internal/pipeline/`, и в том же
прогоне соседние посадки дали RED — то есть положительный контроль стоит.
- **(а) `volume.go:191`** — `case class == unitFresh && s.stop.Delivered > s.stop.Carried:`. Возврат к
форме `> 0` (и эквивалентной `>= 1`) переживает пакет. Существующий `TestReconcileKeepsCarriedInsideDelivered`
(`volumeslot_test.go:554`) гоняет только ветки `unitFree` и до этой ветки не доходит.
⚠ Отчёт числит за этой находкой «посадку **Y3**», **которой в репозитории нет** — см. Б7.
- **(б) `rebill.go:450`** — подмена `projectionBasis(proj)` на `projectionBasis(book)` переживает пакет.
Ветка-близнец (scoped-клауза, `:505`) от той же подмены КРАСНЕЕТ через
`TestTheScopedClauseOfTheRefusalCarriesItsOwnBasis` — значит пакет эту подмену ловить умеет.
- **(в) `rebill.go:323`** — `if p.ModelMovedRows > 0` заменяется на `>= 0`, и «0 of the N unit(s) were
bought from a model…» уезжает в КАЖДЫЙ текст согласия; пакет зелёный. У близнеца `HistoricalRows`
отрицательный пин ЕСТЬ (`reprice_test.go:569-572` проверяет, что чистая сумма НЕ содержит
«originally billed»), у новой оговорки — нет нигде.
**Делай РОВНО так:** каждое свойство получает тест, который от своей мутации краснеет, И запись в
`backend/cmd/tmmutate/mutations.json`. **Решаешь сам:** форма тестов и формулировки записей.
⚠ Приёмка сажала СВОИ формы мутаций — не копируй их, посади свою и убедись, что она красная.
## Б4 [мелочь] — докстринг `classifyUnits` не пере-считан под собственную правку
`volume.go:637`: «sorts every output unit into the **three** things this run can do to it», а `unitClass`
несёт ЧЕТЫРЕ члена, и добавленный паком `unitCarried` присваивается ВНУТРИ самой этой функции.
Вредно адресно: читатель, ищущий, куда попадает начатый-но-не-отгруженный юнит, по слову «three»
решит, что четвёртого класса нет, — и пойдёт искать в ветку `unitFresh`, ровно туда, где жил дефект
строки 232. **Делай РОВНО так**, правка на одно слово.
## Б5 [мелочь] — пример в `backend/README.md:73` противоречит новой семантике
Строка обещает «оплатить не больше 10 выходных юнитов», тогда как тело README:11 новую семантику
раскрывает верно. **Делай РОВНО так:** привести пример к телу.
## Б6 [мелочь, по желанию] — шов `store → репрайсер` прибит с одной стороны
Подмена `c.escalation` на `0` в `ledger.go:380` переживает `./internal/store/`, а `./internal/pipeline/`
краснеет через `TestAnEscalatedRowIsRePricedAtTheNewHopEndToEnd`. Дефектом сегодня не является — пин
живёт там, где живёт свойство. Но твой же довод по фильтру книги («мутация пережила обе батареи по
отдельности, значит одной мало» — оттого FC1-book-filter-**store** И -**pipeline**) применим и здесь.
**Решаешь сам:** заводить ли запись каталога на store-стороне; отказ аргументируй.
## Б7 [среднее] — отчёт ссылается на ПЯТЬ несуществующих посадок как на живые гейты
`Y2-silent-when-nothing-left` · `Y3` · `Z2` · `X29` · `Y4`. Каталог получил ровно ШЕСТЬ записей
(`FC1-*`×4, `FC5-*`×2); 35 заявленных собственных посадок жили в scratch-каталогах
(`own_mutations.json`, `round2_mut.json`, `round3_mut.json`, `draftonly_mut.json`), которых на машине
нет. Проверено грепом с границами слова, положительный контроль на ту же форму: `FC1-book-filter-store`
находится в двух местах.
Читатель отчёта принимает пять несуществующих гейтов за живые — а один из них «закрывает» гарантию,
чья мутация переживает (Б3а). **Это ровно та болезнь, ради которой `tmmutate` и лежит в репозитории.**
**Делай РОВНО так:** либо снять ссылки, либо посадить недостающие записи в каталог.
**Решаешь сам:** какие из пяти стоят записи, а какие были разведкой.
## Б8 [мелочь] — четыре числа отчёта не печатаются командами, которые он для них называет
- «денежного пути в каталоге было 0 файлов, стало **5**» — каталог даёт 18 → 21 различных файла, то
есть ТРИ новых (`reprice.go`, `volume.go`, `ledger.go`), шесть записей; греп из промта печатает 7.
Ни одно прочтение не даёт 5.
- «на входе **3** попадания» у грепа `can never quote a different number` — на входном коммите в
названной области попадание ОДНО.
- «все **18** строк» линта — гейт печатает **19**, и девятнадцатый создан самим отчётом
(строка, цитировавшая мёртвый якорь в самом отчёте).
- «в файле **485** строк» — 499: число снято до того, как отчёт дорос.
**Делай РОВНО так:** пере-считать каждое своей командой и вписать команду рядом с числом.
`python3 docs/scripts/counts.py --lint` гнать **ПОСЛЕДНИМ действием**, после того как отчёт дописан,
и починить 19-й якорь — иначе гейт остаётся красным по вине самого отчёта.
## Б9 [мелочь] — неполнота самообъявления по D39.183
`TestCheckpointsOfOneGenerationSumToTheRowsCost` (`reprice_test.go:639`) адаптирован под третий возврат
`usd()`, а в блоке самообъявления не назван, хотя остальные четыре адаптации под ту же смену сигнатур
перечислены поимённо. Ослабления нет — правка механическая. Это ровно класс, который бэклог держит
строкой **248**. **Делай РОВНО так:** дописать имя.
## Б10 [к сведению, НЕ заказ] — что приёмка нашла и что сознательно НЕ чинится этим дофиксом
Назови это в отчёте, чтобы оно не умерло молча вместе с паком.
- **`Carried` на провод не выходит**, а подсказка, дописанная паком в докстринг `Delivered`, ключуется
на `delivered > max_units` — и в форме «весь остаток перенесён» `delivered == max_units`, то есть
подсказка НЕ срабатывает и разложить кадр читателю нечем. Связано с открытым вопросом владельца о
семантике переноса; чинится вместе с ним, а не отдельно.
- **В перенос попадает и юнит, который ФЛАГНУЛСЯ и текста не отдал** — он «начат и не отгружен»,
значит каждая следующая покупка снова платит за его пере-атаку ВНЕ гранта. Приёмка это вывела из
`classifyUnits` и подтвердила зелёными тестами пака, но **цикла из трёх последовательных покупок не
гонял никто**. Это вход в решение владельца, а не дефект — не чини, но не потеряй.
## Границы — что НЕ делать
-**Не добавлять поле на провод** (см. Б2) и не двигать `StreamVersion`, форму манифеста, схему
хранилища, версию контракта.
-**Не менять семантику переноса слота.** Владелец 03.09 отложил её словом «подумаем на этот счёт».
В коде сегодня «перенос ограничен грантом», и приёмка это ЗАМЕРИЛА: грант 2 оплачивает 4 выходных
юнита, 6 вызовов провайдера. Это открытый вопрос владельца, а не дефект — трогать нельзя.
-**Не править `docs/architecture/15-money-path.md` и тело `D39.170`** — они противоречат новой
семантике `--max-units`, это правда, но это зона оркестратора, и он их правит сам. Ты правильно
сделал, что не полез.
-Не трогать чужие зоны, `.env`, соседний worktree.
## Что приёмка ПРОВЕРИЛА и признала — переделывать не нужно
- Батарея зелёная: `EXIT=0`, `0 issues`, три известных SKIP, ни одного FAIL. Тестов **1083 → 1102**,
удалённых имён **НОЛЬ** (пофамильный дифф).
- Все ШЕСТЬ каталожных мутаций пере-посажены и **все шесть КРАСНЫЕ**.
- Четыре обязательства `D39.150 п.1` исполнены ВСЕ, включая четвёртое: обещание точности снято из
`status.go`, шапка `reprice.go` заявляет обратное прежнему.
- Инвариант направления ошибки держится по построению: в `repriceCheckpoint` цена только повышается —
пол по ответившей модели, подъём до сегодняшней.
- Правки существующих тестов проверены на МОТИВ: подгонки нет, все стали СИЛЬНЕЕ (проверка трёх полей
→ проверка всей структуры; добавленное утверждение про basis).
- Границы соблюдены: схема, манифест, контракт не двинуты; `runevents/` тронут только комментариями;
два новых поля `status --json` аддитивны — платформа декодирует аллоулист.
- Кратность оценки `$0`-строки названа честно комментарием и ратифицирована «round up when it cannot
be known» — приёмка это принимает.
## Мандат самопроверки — исполнением
1. **Каждый пункт Б1Б9 проверяется своей командой**, и в отчёте стоит её выход цитатой, а не пересказ.
2. **Мутации Б3 сажаются на копии дерева** (`cp -a backend /tmp/…`) и предъявляются КРАСНЫМИ.
Посадка, не покрасневшая, — находка, а не провал: так и пиши.
3. **Отдельный проход по своей готовой работе** свежим контекстом. Субагенты разрешены явно, модель
задавай ИМЕНЕМ и знай, сколько их работает.
4.**«Прошёл, находок нет» отчётом НЕ является**: ноль находок обосновывается тем, ЧТО искалось и
какими командами.
## Отчёт
В свою секцию `docs/PROGRESS.md`. Пройди **по каждому** пункту Б1Б9 и отчитайся по каждому — эта тема
в смене проваливалась несколько раз на том, что исполнялся первый пункт, а нумерованный остаток
бросался. Каждое число — с командой получения, которая его печатает; приёмка пере-ранит.
Обязательная секция **«Что НЕ удалось и что НЕ проверено»** — третий исход законен.
Конфликт этого наряда с кодом — **пинг**, не интерпретация: наряд писан по дереву индекса, код первичен.
## Прямой канал
Механизм — `CLAUDE.md`, «Связь между сессиями»: свой блок в `/tmp/textmachine-channel`, сверка с
`ListAgents` перед отправкой; канала нет — нормальный случай, сессии подряд не опрашивать.