textmachine/docs/BACKEND_MONEY_PACK_SESSION_PROMPT.md

232 lines
25 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.

# Промт: бэкенд, пак «деньги» — продажа обретает настоящий стоп, а смета становится доступной до покупки
> **Выдан оркестратором №19, 28.08.2026.** Основание — **D39.165** §1 (три продуктовых развилки сняты
> словом владельца) и строка бэклога **231**. Пак идёт ПАРАЛЛЕЛЬНО платформенному.
> ⚠ **ФАЙЛЫ вы не делите** — модули отдельные, платформа кода движка НЕ компилирует и его тестов НЕ
> запускает (проверено). **Но ШОВ общий и ведётся РУКАМИ в трёх точках:** таблица кодов выхода ·
> argv из строковых литералов · аллоулист полей `status --json`. **Твой §3.1 трогает первые две** —
> форму ответа я решил за тебя (§3.1), а не оставил на согласование.
## §0. Какая проблема и что решит твой результат
**1. Ручка «купить N глав» ничего не ограничивает.** Платформа продаёт ГЛАВЫ, а движку передаёт
только денежный потолок: `--ceiling-usd` (`backend/cmd/tmctl/invocation.go:129`), и тот каппит
**КУМУЛЯТИВНУЮ** трату по книге (`backend/internal/store/ledger.go:67`
`bookTotal+estimate > c.BookUSD`). **Про число глав движок не знает вовсе**, флага такого нет.
**Измерено приёмкой 28.08 (леджеры реальных прогонов, $0):** настоящая цена главы — средняя
**$0.01150.0190**, максимум одной главы **$0.0375**; константа продажи платформы — **$0.03**. То
есть покупка «10 глав» отдаёт движку $0.30, а $0.30 на этом материале покупает **1626 глав**.
Пользователь просит одно, получает другое, и полоса каппится на купленном.
**И хвост цены порождает КАЧЕСТВО, а не объём** (наблюдение владельца, подтверждено данными):
вызов может отработать успешно и вернуть мусор — деньги списаны, юнит уезжает на ретрай, иногда на
эскалационный хоп к дорогой модели. В леджере это видно с именами причин (`empty`, `length`,
`cjk_artifact`, `sanitizer_defect`), и доля ретраев+эскалаций гуляет **1.5% → 23%** между прогонами.
**2. Смету пере-прохода показать НЕЧЕМ — и это названная цена вчерашнего минора 0.7.0.** Движок уже
считает её (`projectRebill`, `backend/internal/pipeline/rebill.go:111`) и публикует в `status --json`.
Но `status` материализует банк из СОХРАНЁННОГО глоссария
(`backend/internal/pipeline/status.go:733-744`, `projectStoredMemory`его собственный комментарий:
«A seed-FILE edit not yet re-run is NOT reflected here»), а `bank-apply` пишет только ФАЙЛЫ решений.
Свёртка происходит внутри СЛЕДУЮЩЕГО `translate`. **Итог: сразу после правки банка движок честно
отвечает «ничего не двигалось», и платформе нечего показать покупателю.** Носитель — строка **231**.
**Что решит результат:** движок получает СПОСОБНОСТЬ остановиться по объёму, а пользователь получает
возможность увидеть, что затронет пере-проход, ДО оплаты.
**ЧЕСТНАЯ ГРАНИЦА, которую первая редакция промта скрывала (нашёл опровергатель).** «Купил N глав —
получил N глав» этим паком НЕ становится правдой, потому что **число глав до движка не доезжает**:
`CeilingChapters` живёт на платформе (`platform/internal/runs/runs.go:232`) до самого спавна, а
`runner.TranslateArgs` собирает argv литералами и канала для него не имеет. **Проводка — платформенная
половина, и она в этом паке НЕ заказана.** Ты строишь механизм; труба к нему — отдельный заказ, и я
его завожу строкой. Не считай пак провалившимся оттого, что после него обещание §0 ещё не выполнено.
## §1. Зона записи и git
**Твоя зона — `backend/`.** Итоги и находки — своя секция журнала `docs/PROGRESS.md` («Бэкенд»).
- **Ты НЕ коммитишь.** Лендит оркестратор после адверсариальной приёмки.
-**`platform/` НЕ ТРОГАТЬ.** Там параллельно работает платформенный пак. Нужна правка на той
стороне — это ПИНГ, а не правка.
-**`docs/` — не твоя зона**, кроме своей секции журнала. ⚠ Перед записью в неё **перечитай файл**:
журнал правят две сессии, и 28.08 оркестратор уже унёс чужую секцию своим коммитом из-за общего
окна записи.
-**Стендовый `tmctl` собирай из ЗАФИКСИРОВАННОЙ копии дерева, а не из рабочего**, если он тебе
понадобится: рабочее дерево грязное твоими же правками, и бинарь из него мерит непонятно что.
- В дереве незакоммиченная работа полигона (20 позиций) — не касаться.
## §2. Карта чтения — ≤5 позиций, ЗАКОН
1. **`backend/cmd/tmctl/main.go:252-262`** — три денежных флага и почему они ОРТОГОНАЛЬНЫ; там же
предупреждение «`--ceiling-usd`НЕ бюджет прогона» · **`invocation.go:120-210`** (парсинг и
валидация флагов).
2. **`backend/internal/store/ledger.go:30-80`** — где потолок реально судит трату (`Reserve`).
3. **`backend/internal/pipeline/rebill.go`** — `RebillProjection`, `projectRebill` (`:111`), порог
согласия и его текст отказа (`:318-324`).
4. **`backend/internal/pipeline/status.go:733-744`** — `projectStoredMemory`; **читать ВНИМАТЕЛЬНО:
это и есть слепое окно из §0.2**.
5. **`docs/architecture/05-decisions-log.md`, тело D39.165** (греп `^## D39.165`) — основание пака;
**и эррата 28.08-к в шапке того же файла** — там названа моя ошибка про смету, не повтори её.
## §3. Состав пака
### §3.1. Потолок ОБЪЁМА рядом с денежным — ЗАКАЗ; форма свободна
**Заказано:** движок умеет остановиться по объёму работы, а не только по деньгам, и **говорит, по
какому потолку встал**.
**Ратифицировано D39.165 §1б и обсуждению не подлежит:** потолок живёт в ДВИЖКЕ. Резать волну на
стороне платформы нельзя — она волной не владеет и пер-юнитной цены заранее не знает.
**Свободен и обязан обосновать — В ЧЁМ мерить. ⚠ Кандидатов ТРИ, а не два (поправка опровергателя):**
(а) **выходной юнит**`ManifestUnit`, та самая гранулярность, которую движок отгружает, и она же
платформенный `chapters.units_total` с интейка; (б) **чанк**; (в) **`chunk×stage` — БИЛЛИНГОВАЯ
единица движка**, ровно та, в которой считает смета (`rebill.go:322`: «%d chunk×stage unit(s)»).
**Ловушка ровно здесь:** режущий волну естественно считает то, за что ПЛАТЯТ, то есть (в), а
платформа продаёт (а) — и купивший десять глав получит их долю. **Дефект пака воспроизведётся слоем
ниже, в той же форме.** Выбор объясни и назови, как он ложится на платформенную единицу продажи.
⚠ **И ещё одно, чего первая редакция не спрашивала: как против объёмного потолка считаются РЕПИНЫ и
РЕТРАИ.** Пере-проход, пере-привязывающий пятьсот юнитов за $0 (`rebill.go:315-320`), сожжёт объёмный
потолок, не потратив цента. Ответь на это явно.
⛔ **ФОРМА ОТВЕТА ЧЕРЕЗ ШОВ РЕШЕНА МНОЙ, и это НЕ предмет твоего выбора — потому что выбор здесь
ломает деньги в обе стороны (нашёл опровергатель).** Словарь кодов выхода объявлен ЗАМОРОЖЕННЫМ самим
движком (`backend/cmd/tmctl/main.go:125-127`: «both bands are frozen seams and a fresh code would be a
word added to a ratified dictionary»), а платформа незнакомый код читает как ОТКАЗ
(`platform/internal/ingest/exit.go:168-169`, `default: OutcomeFailed`). Отсюда:
- переиспользуешь код денежного потолка → платформа объявит прогон ПРИОСТАНОВЛЕННЫМ и возобновит его
за уже купленный объём — дефект, ради которого пак заведён, воспроизведётся слоем ниже;
- заведёшь новый код → пользователь, получивший РОВНО купленное, увидит «ошибка сервиса».
**РЕШЕНИЕ: остановка по объёму — это НЕ остановка, а ЗАВЕРШЕНИЕ.** По деньгам прогон встал ПОСРЕДИ
работы, там честно «приостановлен» и возобновление. По объёму он сделал ровно то, что куплено, — это
УСПЕХ, код **0**, замороженный словарь не трогается вовсе, платформа читает штатное «готово».
Различение, которого требует пункт ниже, живёт тогда не в КОДЕ ВЫХОДА, а в отчёте/логе. Если найдёшь,
что это ломает что-то, чего я не вижу, — **пинг, и я пере-решаю**: на этой развилке уже стоял выбор,
ломающий деньги в обе стороны.
**Две грабли, забранные у внешних систем — исполнить обе:**
- **остановка обязана НАЗЫВАТЬ свой потолок.** У OpenHands (`max_budget_per_task` + `max_iterations`)
агент упирается в лимит итераций МОЛЧА — известная жалоба. У нас «встал по деньгам» и «встал по
объёму» — разные ответы пользователю и разные ремеди;
- **проверять ПЕРЕД началом единицы работы, а не после.** У LiteLLM пост-фактум-проверка токенного
потолка всегда пробивает его на один вызов. Денежный потолок у нас проверяется в `Reserve` до
вызова — объёмный обязан вести себя так же.
**Не сломай ортогональность.** `--ceiling-usd` — кумулятивный КНИЖНЫЙ кап, не бюджет прогона
(`main.go:256-260`). Новый потолок — про работу ЭТОГО прогона. Смешаешь семантику — сломаешь
платформенный расчёт холдов, который на кумулятивности и стоит.
### §3.2. Смета БЕЗ покупки — ЗАКАЗ; форма свободна
**Заказано:** платформа может узнать «сколько юнитов затронет пере-проход», НЕ запуская `translate` и
ничего не покупая.
**Что знать** (проверено мной, пере-проверь): смета уже считается (`projectRebill`) и уже публикуется
в `status --json` полями `rebill_units`/`rebill_usd`. Мешает ровно одно — **тайминг свёртки**:
`status` читает СОХРАНЁННЫЙ глоссарий, а решения лежат в ФАЙЛАХ до следующего `translate`.
**Форма свободна.** Очевидные кандидаты, оба со своей ценой:
- **флаг у `status`** («сверни решения в память для расчёта и не пиши») — дёшево, но `status`
ратифицирован как ЧИСТО ЧИТАЮЩИЙ ремонтный глагол, и свёртка внутри него это свойство ломает;
- **новый $0-глагол** — честнее по разделению, но новая поверхность CLI и новый документ.
**Родня, которую надо знать: `resnapshot --dry-run`** — строка **124(в)**, поименована в законе шва
как будущая дверь. Она просит ТУ ЖЕ способность «оценить, ничего не покупая». **Если твоя форма
закрывает обе — скажи это прямо, это удешевляет проект.**
⛔ **Границы, обе жёсткие, но сформулированы ТОЧНО (первая редакция обе переоценила — нашёл
опровергатель):**
- **$0 — абсолютно:** ни одного провайдерского вызова.
- **Read-only — с ОДНОЙ названной оговоркой:** «ни одной записи в стор» абсолютом быть НЕ МОЖЕТ —
первое касание проекта создаёт и мигрирует базу движка по построению (`backend/internal/pipeline/runner.go:193-197`;
ратифицировано D39.122, у `status` тот же побочный эффект). Требование верное: **никаких записей
СВЕРХ этого first-touch**, и ни одной записи в банк/глоссарий/чекпойнты.
- **Ключей НЕ требует.** ⚠ Якорь первой редакции (`dotenv.go:53-55`) я ВЫДУМАЛ — там середина
докблока про другое. Настоящий носитель: `backend/cmd/tmctl/invocation.go:152-155` (текст отказа,
перечисляющий читающие глаголы) и пин `backend/cmd/tmctl/keysfile_test.go`.
**ТРЕТИЙ КАНДИДАТ, ДЕШЕВЛЕ ОБОИХ МОИХ — его нашёл опровергатель, и я его не видел.** У `bank-apply`
УЖЕ ЕСТЬ режим проекции (`invocation.go:126`: «print the projection of the decisions and write
NOTHING»), он берёт тот же арбитр-флок, ничего не пишет и печатает JSON-отчёт. **И у него уже на
руках ровно недостающее состояние**: `readBookState` поднимает bank + seed + delta + rejects ДО
свёртки. Не хватает только statuses и manifest для `projectRebill`. Рассмотри этот путь ПЕРВЫМ: он не
трогает «`status` — чисто читающий» вовсе.
**ЛОВУШКА, КОТОРАЯ УБЬЁТ ВЕСЬ СМЫСЛ, если её не увидеть.** Свернуть решения В СТОР ты не сможешь
(`store.go:117-128`, `OpenReadOnly` ставит `PRAGMA query_only=1`), значит фолд будет В ПАМЯТИ. Но
канонический фолд идёт ЧЕРЕЗ стор: `seeding.go` пишет банк, читает его обратно `ORDER BY src, sense,
…` и материализует. Порядок НЕ безразличен — `membank/memory.go:610-617` сортирует стабильно «в
порядке замороженных entries», а дальше токенный бюджет РЕЖЕТ хвост. **Материализуешь «будущий банк»
в порядке добавления — получишь другой набор строк глоссария, другие отрендеренные байты, другой
content-hash: бесплатная смета назовёт одно число, а платный прогон выставит другое.** Это ровно тот
дефект, который пак продаёт как решённый. Докажи тождество, а не предположи его.
**И мина формы:** оба поля сметы объявлены `omitempty` (`status.go:208-209`). D39.166 п.2 уже
заплатил за это однажды: «$0-цена мурует дверь — юниты по нулевой цене приходят БЕЗ цены». Если
переиспользуешь форму, «ноль юнитов» и «не считалось» на проводе станут неотличимы.
**Факт из строки 231, который тебе полезен:** поля `rebill_units`/`rebill_usd` СНЯТЫ с аллоулиста
шва до появления потребителя — вернутся вместе с твоим глаголом.
### §3.3. Чего в паке НЕТ — пропуски подписаны
- **Калибровка константы $0.03 — НЕ твоя и НЕ сейчас.** Она платформенная и гейчена строкой **202**
(живой прогон текущего стека). Ты даёшь МЕХАНИЗМ, цену считает не движок.
- **Цена от объёма исходника** (D39.165 §1а) — платформенная половина, носитель `chapters.units_total`.
- **Строки 228 / 230 / 141-остаток / 131** — соседи, не заказ. Увидишь — назови строкой, не чини.
## §4. Самопроверка ИСПОЛНЕНИЕМ — «перечитал сам» её не удовлетворяет
1. **Батарея зоны** под `-race`, линтер 0 issues; числа — с командами.
2. **Потолок объёма доказывается ПОСТ-ФИКСНЫМ инвариантом:** прогон с потолком в N единиц
останавливается на N, отвечает признаком «встал по ОБЪЁМУ» (отличимым от денежного), и
останавливается ПЕРЕД началом N+1, а не после.
3. ⛔ **Смета доказывается $0 — и РЕПРО ПЕРВОЙ РЕДАКЦИИ БЫЛО ВЫРОЖДЕННЫМ, я повторил ошибку, о
которой сам же написал эрратy 28.08-и.** Состояние «`translate` не запускался» негодно: у такой
книги строк `chunk_status` нет вовсе, и `projectRebill` отдаёт ПУСТО и до фикса, и после
(`rebill.go:132-134` пропускает строки без снапшота). **Верное состояние: прогон СОСТОЯЛСЯ**
(строки `chunk_status` есть, снапшоты записаны) → **ПОТОМ применена правка банка** → и твой
механизм отдаёт НЕнулевой счёт затронутых юнитов, тогда как сегодняшний `status` на том же
состоянии отвечает нулём. Разница «сегодня ноль / после фикса N» и есть деливерабл.
При этом леджер не двинулся ни на цент, а в стор не ушло ничего сверх first-touch.
4. **Свои посадки мутаций:** минимум по одной на пункт; вердикт — по ДЕЛЬТЕ против чистой базовой
линии и по ТОПИЧНОСТИ упавшего теста, не по цвету батареи.
5. **Адверсариальный проход по своей готовой работе — заказан; глубину и веер выбираешь сама.**
⚠ Куда смотреть в ЭТОМ паке: деньги (перерасход либо отказ законной работе) · тождество бесплатной сметы и платного прогона · шов (форма ответа, argv).
Дефекты, которые внёс сам пак, называй первыми — приёмке они не видны.
**Fable 5** — обычно хватает одного-двух агентов, но это РЕКОМЕНДАЦИЯ, не потолок: веер под предмет выбирает сессия. Модель задавай ЯВНО и знай, сколько их работает.
## §5. Оси ревью — 13, вправе заменить с аргументом
1. **Ось денег:** может ли новый потолок дать перерасход или, наоборот, отказать законной работе;
не сломана ли кумулятивная семантика денежного капа.
2. **Ось «пара, которой в репо нет»:** ветвления по паре/книге в Go быть не должно.
3. **Ось детерминизма:** трогаешь сборку запроса или снапшот — не перекупается ли прогон.
## §6. Записка-план, комплектность
**До правок** — записка-план в своей секции журнала. **В конце** — таблица комплектности против §3:
пункт → что сделано → каким ИСПОЛНЕНИЕМ подтверждено. Слово вместо команды = пункт НЕ сделан.
## §7. Эхо-протокол старта
ДО работы — ≤10 строк: **скоуп · инварианты · не-делать**. Первое действие — вписать свой блок в файл
канала (§10), сразу второе — послать мне эхо.
## §8. Obstacle — обязательная секция
**«Что НЕ удалось и что НЕ проверено»** отдельной секцией, не россыпью.
## §9. Канал вопросов и твоё право отказаться
Конфликт промта с кодом или доками — **пинг, не интерпретация в свою пользу**.
**У тебя есть право сказать «этого делать не надо» — с аргументом.** За последние сутки исполнители
опровергли ЧЕТЫРЕ моих заказа и все четыре раза были правы; два я пере-проверил своей посадкой и
отозвал. Отказ с разбором дороже послушного исполнения.
**Тесты и гейты не подгонять под зелень** (D39.121).
## §10. Как со мной связаться
**Адрес — в файле `/tmp/textmachine-channel`.** Впиши свой блок первым делом, чужие не трогай.
**Канала нет ⇒ НЕ искать:** вопрос секцией в отчёт, работа продолжается.