diff --git a/docs/architecture/05-decisions-log.md b/docs/architecture/05-decisions-log.md index 7cc47194..baa49c2c 100644 --- a/docs/architecture/05-decisions-log.md +++ b/docs/architecture/05-decisions-log.md @@ -19,7 +19,7 @@ > ⚠ **Эррата 27.08-ж (D39.160 п.2) — ошибка ОРКЕСТРАТОРА, найденная исполнителем пака.** Нота утверждала, что после сноса отменённой двери «канон и деплой СОВПАДУТ точно», а промт минора (§3.2-бис) — что счётчики `pending_decisions`/`complete` «навсегда нули». **Верно по ПУТЯМ, неверно по ПОЛЯМ:** проекция `GET /bank` продолжает их слать, и это не нули — `bankCountsTx` (`platform/internal/pgstore/readmodel.go:330`) считает `proposed`-строки, о чём говорит её собственный комментарий. Клиент 0.5.0 лишние поля игнорирует, но аллоулист-норма нарушена до монтажа (2в). Носитель — `PD-399`. ⚠ Контрактная сессия принесла это ПИНГОМ по §11 промта, вместо того чтобы тихо подогнать работу под неверную посылку; это и есть поведение, которого норма требует. > ⚠ **Эррата 27.08-з (D39.156, состав пункта 2в): «воркер решений → глагол перед возобновлением» СНЯТ — посылка изменилась.** Пункт писался, когда двери в контракте не было и подразумевалось НАКОПЛЕНИЕ: платформа копит решения у себя и скармливает их движку перед `resume`. С дверью канона 0.5.0 накопления не существует — правка ПРИМЕНЯЕТСЯ в момент подачи, и гарантия «до возобновления» у синхронной формы СИЛЬНЕЕ воркерной: применено прежде, чем клиент получил `200`. Проверено исполнением с обеих сторон: движок на стопе ВЫХОДИТ (`cmd/tmctl/main.go:95`, код 3), флок не-блокирующий и отпускается ядром на выходе процесса (`store/store.go:187`), между стопом и возобновлением живого процесса на проекте нет — запинено `pipeline/bankchain_test.go:62-64`; кап 5000 решений выведен ИМЕННО из синхронности («the call stops fitting the caller's timeout»). Остаётся не воркер, а пер-книжная сериализация в обработчике. `Resume` «с решениями как они есть» не тронут. > ⚠ **Эррата 28.08-и (D39.165 §3, размер мины) — ошибка ОРКЕСТРАТОРА, найденная опровергателем промта P10.** Нота утверждает: «первый же ПРОДОЛЖАЮЩИЙ прогон после первой же правки банка УПАДЁТ». **Переоценено.** Гард снапшота стреляет по СУЩЕСТВУЮЩЕМУ джобу (`backend/internal/pipeline/stagerun.go:47` — `EnsureJob` создаёт джоб стадии в момент, когда стадия впервые исполняется), а правка в ГЛАВНОМ окне — стоп подписи `awaiting_bank` — двигает edit-снапшот, когда edit-джобов ЕЩЁ НЕТ: возобновление создаёт их свежими, и гард молчит. Драфт-волна mined-строк не видит вовсе (`seeding.go:143-153`). **Дефект СТОИТ, но его триггер уже: правка, сделанная ПОСЛЕ появления edit-джобов** — пауза потолком посреди редактуры и ДОЧИТАННАЯ книга. ⚠ Срочность при этом НЕ падает: флагманский случай продукта («поправил имя героя в дочитанной книге») — ровно тот, где edit-джобы существуют, то есть мина бьёт именно по нему. **Цена ошибки была бы прямой:** репро на потоке `awaiting_bank` показало бы ЗЕЛЕНЬ без фикса, и пак мог быть отозван как мнимый. Промт P10 §4.2 исправлен: репро обязано фиксировать состояние «edit-джоб существует ДО правки». -> ⚠ **Эррата 28.08-к (D39.165 §3 + решение оркестратора о глава-полосе) — ДВЕ ошибки, обе найдены широким самопроходом платформенной сессии, обе доказаны исполнением.** **(1) Посылка «смета УЖЕ публикуется в `status --json`» верна только ПОСЛЕ свёртки.** `bank-apply` пишет только ФАЙЛЫ решений, а `status` считает ре-билл от СОХРАНЁННОГО глоссария (`backend/internal/pipeline/status.go:733-744`, `projectStoredMemory` — его собственный комментарий: «A seed-FILE edit not yet re-run is NOT reflected here… that drift surfaces on the next translate's re-seed»). Свёртка происходит внутри СЛЕДУЮЩЕГО `translate`, поэтому сразу после правки движок отвечает `units=0`/`drift=false`. Следствие: продажа «затронуто N юнитов» и холд от сметы В ТЕКУЩЕМ ШВЕ НЕДОСТИЖИМЫ — для них нужен движковый глагол «свернуть банк и оценить ВНЕ translate», которого нет. **(2) Решение оркестратора «полоса пере-прохода — в ГЛАВАХ» ОТМЕНЯЕТСЯ: его посылка опровергнута.** Я рассудил, что $0-репин двигает полосу, потому что идёт через тот же `resumeFromChunkStatus`, — и не проверил анонс. Движок анонсирует юнит ОДИН РАЗ на жизнь книги (announce-once, `backend/internal/pipeline/events.go:49,143-162`), пере-проход не ре-анонсирует ни репины, ни пере-переводы ⇒ `done` остался бы НУЛЁМ навсегда. Это ровно тот класс, от которого предостерегает памятка «не выводить из соседнего механизма, не проверив свой». ⚠ **Что при этом НЕ отменяется:** запрет класть ЮНИТЫ в поле, объявленное в главах, стоит — но объявленная в каноне «одна единица работы» запретом не является, потому что она НЕ молчаливая. +> ⚠ **Эррата 28.08-к (D39.165 §3 + решение оркестратора о глава-полосе) — ДВЕ ошибки, обе найдены широким самопроходом платформенной сессии, обе доказаны исполнением.** **(1) Посылка «смета УЖЕ публикуется в `status --json`» верна только ПОСЛЕ свёртки.** `bank-apply` пишет только ФАЙЛЫ решений, а `status` считает ре-билл от СОХРАНЁННОГО глоссария (`backend/internal/pipeline/status.go:962`=`seed-FILE edit` ⚠ (адрес испр. 06.09: цитата УЕХАЛА, не исчезла; в теле `D39.165` она осталась по прежнему адресу — тело ноты не переписывается, D23.3)``, `projectStoredMemory` — его собственный комментарий: «A seed-FILE edit not yet re-run is NOT reflected here… that drift surfaces on the next translate's re-seed»). Свёртка происходит внутри СЛЕДУЮЩЕГО `translate`, поэтому сразу после правки движок отвечает `units=0`/`drift=false`. Следствие: продажа «затронуто N юнитов» и холд от сметы В ТЕКУЩЕМ ШВЕ НЕДОСТИЖИМЫ — для них нужен движковый глагол «свернуть банк и оценить ВНЕ translate», которого нет. **(2) Решение оркестратора «полоса пере-прохода — в ГЛАВАХ» ОТМЕНЯЕТСЯ: его посылка опровергнута.** Я рассудил, что $0-репин двигает полосу, потому что идёт через тот же `resumeFromChunkStatus`, — и не проверил анонс. Движок анонсирует юнит ОДИН РАЗ на жизнь книги (announce-once, `backend/internal/pipeline/events.go:49,143-162`), пере-проход не ре-анонсирует ни репины, ни пере-переводы ⇒ `done` остался бы НУЛЁМ навсегда. Это ровно тот класс, от которого предостерегает памятка «не выводить из соседнего механизма, не проверив свой». ⚠ **Что при этом НЕ отменяется:** запрет класть ЮНИТЫ в поле, объявленное в главах, стоит — но объявленная в каноне «одна единица работы» запретом не является, потому что она НЕ молчаливая. > ⚠ **Эррата 29.08-а (D39.172, две строки, объявленные заведёнными) — ошибка ОРКЕСТРАТОРА №19.** Тело ноты дважды утверждает «Заведено строкой» / «строка заведена» — про `Touch`, выбрасывающий `RowsAffected`, и про пересборку `tmctl` в рецепте стенда. **На момент ратификации ни одной из этих строк не существовало:** последняя строка регистра платформы была `PD-430`, и проверка грепом по `Touch|RowsAffected|пересбор` давала только совпадения слов в чужих строках. Утверждение о будущем записано как о свершившемся — ровно тот класс, который эта же смена ловила у сессий трижды. **СНЯТА 29.08: строки заведены зоной — `PD-431` (`Touch`) и `PD-432` (пересборка `tmctl`), `PD-423` получил вторую точку. Проверено грепом по регистру.** Тело ноты не переписывается (D23.3). ⚠ Сюда же третий пункт того же абзаца: `PD-423` предписано ПЕРЕ-ПРОВЕРИТЬ (у сессии `sqlc` тест зелёный в трёх прогонах), и пометки в строке регистра тоже нет. > ⚠ **Эррата 30.08-б (D39.165 §2 и §3, две находки платформенной сессии P12).** (а) **Якорь протух:** §2 цитирует фразу канона о `chapters_done` по `openapi.yaml:1542-1545` — текст уехал на **`1554-1559`**, по прежним строкам сейчас `RejectReason`. Сама цитата верна дословно; нота не битая, битым стал только номер (класс строки бэклога 219). (б) **§3 назвал живое САМОПРОТИВОРЕЧИЕ канона и не дал ему носителя:** правка банка «takes effect on the NEXT run» (`openapi.yaml:504`) против «finished work is not bought twice» плюс подъём потолка только у ПРИОСТАНОВЛЕННОЙ книги (`:590`) — у дочитанной книги следующего прогона купить нечем, поэтому принятая правка умирает молча. Ни в едином бэклоге, ни в регистре платформы строки не было (проверено грепом по обеим фразам) — **заведена строка бэклога 241** 30.08. ⚠ Класс — «названо в теле ноты и не получило карриера»; норма приёмки требует строку ТЕМ ЖЕ лендингом, здесь она не легла два дня. > ⚠ **Эррата 30.08-а (D39.176 п.5, `PD-421`) — ошибка ОРКЕСТРАТОРА №20, пойманная ВЛАДЕЛЬЦЕМ.** Пункт вынесен владельцу с формулировкой «открытая вкладка теряет сессию примерно через час» — **неверно, и ровно тем классом, от которого предостерегает записка передачи: вывод из соседнего числа вместо проверки.** Час — это ПЕРИОДИЧНОСТЬ `SweepSessions`, а срок жизни сессии другой: `SessionIdleTTL` = **14 суток**, `SessionMaxAge` = **30** (`platform/internal/config/config.go:338-339`=`defaultSessionIdle`), причём окно бездействия скользит на любом аутентифицированном запросе (`platform/internal/auth/middleware.go:66`=`a.Sessions.Touch(r.Context(), digest`; поток `Touch` не зовёт — грепом 0, и это не важно при окне в две недели). Реальное следствие `PD-421` — то, что и записано в её теле: сессия, протухшая по бездействию И подметённая, гасит поток с опозданием до часа, то есть после ДВУХ НЕДЕЛЬ без единого действия. **Вопрос СНЯТ с листа владельца как несущественный; правка в паке P12 не нужна, строка остаётся `info`.** Владелец вопросом «ты уверен или придумываешь?» поймал ошибку до того, как она стала заказом на работу. @@ -56,7 +56,7 @@ > ⚠ **Эррата 04.09-в (D39.194 п.3) — МИНОР 0.10.0 БЫЛ РАТИФИЦИРОВАН БЕЗ СВОЕЙ КОДОВОЙ ПОЛОВИНЫ, и это дефект акта оркестратора, а не зоны.** Канон уехал на `0.10.0` (`58bca19`), а сборка платформы продолжала объявлять `0.9.0`, потому что константу и рукописную карту причин никто не поднял: ратификация создала обязанность ВНУТРИ зоны и осталась жить только в голове оркестратора. Цена названа замером зоны — **один рабочий день, всё это время код на проводе был ложным**: шестнадцатая причина `off_target_lang` доезжала читателю как `unspecified` со ступенью «взгляд», хотя канон уже объявлял `wrong_language`. Поймала ЗОНА своей пере-проверкой ПОСЛЕ того, как оркестратор сказал «приёмка закончена»; гейт `platform/internal/gates/contract_test.go` сравнивает константу сборки с каноном НА ДИСКЕ и покраснел сразу — второй копии числа в зоне нет, и это окупилось. Починено `7e204db` (5 файлов). ⚠ **Класс, предсказанный `PD-246` и потому НЕ закрытый им:** словарь причин движковый, карта платформы рукописная, релизы независимые — окно рассинхрона закрывает механизм, которого нет. **Норма:** ратификация, порождающая обязанность в зоне, выдаётся зоне ЯВНО и тем же актом; «я решил, значит зона узнает» — не передача. > ⚠ **Эррата 04.09-б (D39.191) — ЧЬЯ ЭТО ПЕТЛЯ. Тело ноты не назвало исполнителя, и оркестратор №22 прочитал её как СВОЮ обязанность; владелец поправил в тот же час: «я имел в виду не твоё ревью, а ревью от самого бэкенда — зачем тебе то ревьювить, если это не твоя зона».** Читать так: **петлю крутит ЗОНА над своей же работой** — сессия, сдавшая пак, обязана поднять независимый адверсариальный круг по СВОЕМУ дофиксу против ИСХОДНЫХ пунктов заказа и повторять, пока круг не вернётся чистым с предъявленными посадками. Приёмка оркестратора при этом НЕ отменяется и петлёй не является: она второй рубеж при ЛЕНДИНГЕ (пост-хок адверсариальная верификация, мандат промтов), и её задача — судить сданное, а не вести зону за руку по её же коду. Разделение важно ценой: оркестратор, ревьюирующий чужую зону вместо неё, становится узким местом и отнимает у зоны обязанность, которую она несёт лучше — она знает свой код. Тело ноты не переписано (D23.3). -> ⚠ **Эррата 04.09-а (D39.189 п.2б) — «обязательный `--ceiling-usd` на движковом вызове» верно для ПРЯМОГО прогона движка и НЕИСПОЛНИМО на пути через API платформы.** Там флаг никто не ставит руками: платформа выводит число сама — `bookCap = committed + increment` (`platform/internal/runs/spawn.go`, греп `func (m meter) bookCap`), где `increment` считается по ставке `DefaultPerChapter = 30_000` ($0.03, `platform/internal/pricing/pricing.go`), заниженной ×4.47 (D39.179 п.1). Сессия, исполняющая букву ноты, полезла бы крутить `TM_PLATFORM_USD_PER_CHAPTER` — хак под тест, запрещённый каноном. **Читать так:** потолок платного пака на API-пути задаётся ГРАНТОМ тест-аккаунту на объявленную сумму (`tmplatformctl grant --user --usd `) — холд делает перерасход невозможным по построению, — а путь до завершённой книги идёт цепочкой «покупка → пауза по потолку → пополнение → новый прогон», что и есть третий сценарий строки 216 и D39.176 п.3. Тело ноты не переписано (D23.3). Найдено контролёром при сверке плана, пере-проверено оркестратором по коду. +> ⚠ **Эррата 04.09-а (D39.189 п.2б) — «обязательный `--ceiling-usd` на движковом вызове» верно для ПРЯМОГО прогона движка и НЕИСПОЛНИМО на пути через API платформы.** Там флаг никто не ставит руками: платформа выводит число сама — `bookCap = committed + increment` (`platform/internal/runs/spawn.go`, греп `func (m meter) bookCap`), ⚠ **ИСПР. 06.09: механизм СПИЛЕН** (`2812dd6`) — ставки за главу больше нет ни константой, ни ручкой, `increment` приходит из ПРОЕКЦИИ ДВИЖКА, а сама формула `bookCap` жива дословно. Прежняя редакция: где `increment` считается по ставке `DefaultPerChapter = 30_000` ($0.03, `platform/internal/pricing/pricing.go`), заниженной ×4.47 (D39.179 п.1). Сессия, исполняющая букву ноты, полезла бы крутить `TM_PLATFORM_USD_PER_CHAPTER` — хак под тест, запрещённый каноном. **Читать так:** потолок платного пака на API-пути задаётся ГРАНТОМ тест-аккаунту на объявленную сумму (`tmplatformctl grant --user --usd `) — холд делает перерасход невозможным по построению, — а путь до завершённой книги идёт цепочкой «покупка → пауза по потолку → пополнение → новый прогон», что и есть третий сценарий строки 216 и D39.176 п.3. Тело ноты не переписано (D23.3). Найдено контролёром при сверке плана, пере-проверено оркестратором по коду. > ⚠ **Эррата 03.09-а (D39.170) — формула «покупатель платит за N юнитов и получает ровно N» БОЛЬШЕ НЕ ВЕРНА.** Пак «число согласия на пути второй покупки» (лендинг 03.09) закрыл вторую половину строки 232: юнит, который прежний прогон НАЧАЛ и не отгрузил по оси `finalStageWave`, дописывается ВНЕ гранта. Отсюда грант `--max-units N` оплачивает ДО **2N** выходных юнитов — замерено приёмкой на живом раннере (грант 2 → `Paid()=4`, шесть вызовов провайдера), и тем же замером подтверждено, что в перенос попадает даже юнит, ФЛАГНУВШИЙСЯ без текста, то есть каждая следующая покупка платит за его пере-атаку заново. ⛔ **Семантика НЕ ратифицирована: слово владельца 03.09 — «подумаем на этот счёт», решение отложено.** До него `--max-units` держит не деньги, а слоты; деньги держит только `--ceiling-usd`. Тело D39.170 не переписано (D23.3); живой носитель формулировки — `15-money-path.md`, пункт про `--max-units`.