diff --git a/platform/docs/DEFECT_REGISTER.md b/platform/docs/DEFECT_REGISTER.md
index 4dc3d888..2c55d2fe 100644
--- a/platform/docs/DEFECT_REGISTER.md
+++ b/platform/docs/DEFECT_REGISTER.md
@@ -33,7 +33,7 @@
| ID | Класс | Серьёзность | Где | Суть | Статус | Источник |
|---|---|---|---|---|---|---|
-| PD-447 | doc | minor | `docs/STACK_DECISIONS.md` §«Как поднять локально» и `deploy/README.md` шаг 3–4 (оба исправлены); носители дефолта — `internal/config/config.go:281`=`TM_PLATFORM_ADDR` и `cmd/tmplatformctl/seed.go:41`=`base URL of the running tmplatformd` | **Оба стендовых рецепта зоны принимали ОТВЕТ ПО АДРЕСУ за доказательство того, что отвечает СВОЙ процесс, и потому проходили при мёртвом собственном демоне.** Замерено исполнением 04.09: на машине разработки четвёртые сутки жил чужой `tmplatformd` на `127.0.0.1:8080` со своей базой; демон рецепта умирает в этой ситуации на `bind: address already in use` — молча, он запущен фоном, — а смоук следующей строкой получает `healthz=200` и `readyz=ready` ОТ ЧУЖОГО ПРОЦЕССА. ⚠ **Дороже смоука — шаг сида:** `tmplatformctl seed` дарит `$25` кредита и грузит книгу ЧЕРЕЗ ЖИВОЙ ИНТЕЙК, то есть по этому рецепту деньги и данные уезжают в чужой деплой. Второй адрес там же был написан отдельным литералом (`--url http://127.0.0.1:8080`), что и есть штатный способ разъехаться с `TM_PLATFORM_ADDR` незаметно. ⚠ **Три лечения на три РАЗНЫЕ половины, ни одно не заменяет другое:** явный `_ADDR` уводит с общего дефолта; проба по `pid` слушателя отвечает на вопрос, на который `200` не отвечает в принципе — ЧЕЙ это процесс; `--noproxy '*'` нужен потому, что при заданных `http_proxy` голый `curl` на `127.0.0.1` уходит во внешний прокси. Проба предъявлена в обе стороны: свой pid на своём порту — проходит, чужой демон против своего pid — отвергается. ⚠ **Дефолт `8080` в коде НЕ меняется, и это решение:** коллизия — `tmplatformd` против `tmplatformd`, любой другой номер даст ту же аварию на втором одновременном стенде, а цену смены заплатят все существующие деплои и доки. Чинится посылка, а не номер. **Вес minor, а не major, по радиусу:** пишет только дев-стенды, боевого пути этим рецептом нет, деньги — стендовый грант. | fixed(дерево пака «закрыть цикл», 04.09) | найдено оркестратором №22 при независимой проверке отзыва причины смертей демона, 04.09; починено зоной |
+| PD-447 | doc | minor | `docs/STACK_DECISIONS.md` §«Как поднять локально» и `deploy/README.md` шаг 3–4 (оба исправлены); носители дефолта — `internal/config/config.go:281`=`TM_PLATFORM_ADDR` и `cmd/tmplatformctl/seed.go:41`=`base URL of the running tmplatformd` | **Оба стендовых рецепта зоны принимали ОТВЕТ ПО АДРЕСУ за доказательство того, что отвечает СВОЙ процесс, и потому проходили при мёртвом собственном демоне.** Замерено исполнением 04.09: на машине разработки четвёртые сутки жил чужой `tmplatformd` на `127.0.0.1:8080` со своей базой; демон рецепта умирает в этой ситуации на `bind: address already in use` — молча, он запущен фоном, — а смоук следующей строкой получает `healthz=200` и `readyz=ready` ОТ ЧУЖОГО ПРОЦЕССА. ⚠ **Дороже смоука — шаг сида:** `tmplatformctl seed` дарит `$25` кредита и грузит книгу ЧЕРЕЗ ЖИВОЙ ИНТЕЙК, то есть по этому рецепту деньги и данные уезжают в чужой деплой. Второй адрес там же был написан отдельным литералом (`--url http://127.0.0.1:8080`), что и есть штатный способ разъехаться с `TM_PLATFORM_ADDR` незаметно. ⚠ **Три лечения на три РАЗНЫЕ половины, ни одно не заменяет другое:** явный `_ADDR` уводит с общего дефолта; проба по `pid` слушателя отвечает на вопрос, на который `200` не отвечает в принципе — ЧЕЙ это процесс; `--noproxy '*'` нужен потому, что при заданных `http_proxy` голый `curl` на `127.0.0.1` уходит во внешний прокси. Проба предъявлена в обе стороны: свой pid на своём порту — проходит, чужой демон против своего pid — отвергается. ⚠ **Дефолт `8080` в коде НЕ меняется, и это решение:** коллизия — `tmplatformd` против `tmplatformd`, любой другой номер даст ту же аварию на втором одновременном стенде, а цену смены заплатят все существующие деплои и доки. Чинится посылка, а не номер. **Вес minor, а не major, по радиусу:** пишет только дев-стенды, боевого пути этим рецептом нет, деньги — стендовый грант. | fixed(`adf5e53`) | найдено оркестратором №22 при независимой проверке отзыва причины смертей демона, 04.09; починено зоной |
| PD-443 | bug | minor | `internal/exports/exports.go` `Build`/`Sweep`, `internal/pgstore/queries/exports.sql`, миграция `00032_exports.sql` (`on delete cascade`) | **Артефакт экспорта может пережить СТРОКУ, которая одна умеет его найти, и тогда его не удалит никто.** Путь артефакта детерминирован (`
//.`), но каталог со строками не сверяет ничто: GC удаляет только те пути, которые НЕСУТ строки. Три живых пути, найдены адверсариальным проходом по готовой работе: **(а)** демон убит между `rename` движка и `FinishExport` — путь в строку так и не попал, свип пере-водит её в `failed`, файл остаётся навсегда (не экзотика: `KillMode=mixed` в юните и дренаж очереди на 10 с делают это штатным исходом рестарта под нагрузкой); **(б)** книга удалена — `on delete cascade` сносит строки, файлы и каталог книги под `Dir` не трогает никто (сегодня у `DeleteBook` вызывающих нет, но внешний ключ уже стоит и ловушка взведена); **(в)** артефакт, чей unlink не прошёл, — ⚠ **ЭТОТ ПУТЬ ЗАКРЫТ ТЕМ ЖЕ ПАКОМ**: `path` теперь переживает смену состояния и снимается только после реального удаления (`UnlinkedExports`/`ForgetExportPath`), то есть unlink стал ПОВТОРЯЕМЫМ; пин `TestAFileTheSweepCouldNotRemoveKeepsItsRowPointingAtIt`. Четвёртый путь — временные файлы движка (`.<имя>.tmp-*`) после SIGKILL — тоже закрыт (`exports.Service.discard`), потому что `tmctl build` контекста не читает и его всегда добивает SIGKILL. ⚠ **Лечение (а) и (б) — одно и то же и это НЕ райдер:** сверка каталога со строками, то есть реконсилятор файловой системы со своим дизайном (что считать сиротой, как отличить чужой файл от своего, что делать с каталогом книги, которой нет). Делать его заодно с дверью значило бы решить мимоходом. **Радиус сегодня ограничен:** TTL двери — сутки по умолчанию, файлы лежат под одним каталогом деплоя, и оператор видит рост диска раньше, чем что-либо ломается. ⚠ **ДВА СИБЛИНГА, НАЗВАННЫЕ ПРИЁМКОЙ 04.09 — один закрыт, один остаётся здесь.** (I) STAGING-файл движка (`.<имя>.tmp-*`) переживает смерть демона ПОСРЕДИ сборки: `discard` живёт в том же процессе, поэтому при `kill -9` убирать его некому, а следующая сборка того же экспорта не случится — строка уже не `pending`. Это тот же класс, что (а), и лечится тем же реконсилятором каталога. (II) ⚠ **ВТОРОЙ ВОРКЕР НА ОДНОЙ СБОРКЕ — ЗАКРЫТО ТЕМ ЖЕ ДОФИКСОМ, а не только названо:** оба писали бы по ОДНОМУ пути (он выводится из id экспорта), и уборка проигравшего снесла бы файл, только что опубликованный выигравшим. Клейм сделан ИСКЛЮЧАЮЩИМ — `where … and state = 'pending' and started_at is null`, — поэтому второй воркер получает `ErrExportSettled` и до сборки не доходит. Сегодня недостижимо (River ведёт одно задание рода за раз), но именно это превращает вторую реплику из потерянного артефакта в дубль сборки. Пин — в `TestAQueuedBuildAndAClaimedOneAreJudgedOnDifferentClocks`. Воспроизведение (а): `kill -9` демона между строкой лога `export built` и следующей записью в БД. | open | адверсариальный проход пака «закрыть цикл» по своей же работе, 04.09 |
| PD-444 | bug | minor | `internal/httpapi/bank.go:142`=`Invalid(w, r)` (ветка строгого разбора) против `internal/httpapi/bank.go:146`=`Invalid(w, r, items...)` (ветка валидации); механизм — `internal/httpapi/problem.go:164`=`Errors []Item` | **Дверь правок банка отвечает `400 invalid_request` БЕЗ единого указателя на то, ЧТО не так.** Замерено живым прогоном 04.09: две попытки с чужими именами членов (`decisions` вместо `corrections`) вернули голое тело, оба ответа `Content-Length: 119` — ни `errors[]`, ни имени члена, ни позиции. ⚠ **Сам отказ ВЕРЕН и оспаривать его нечего:** схема канона объявляет `additionalProperties: false` на обоих уровнях, и незнакомый член — это клиент, уверенный, что он что-то задал; молчаливая версия этого — правка, применённая наполовину. Дефект в другом: канон нигде не требует МОЛЧАТЬ о том, какой член виноват, а механизм у зоны уже построен и на соседней ветке применяется — `validateCorrections` возвращает `[]Item` и отдаёт его в `Invalid`. Ветка строгого разбора теряет даже то, что у неё в руках: `encoding/json` называет поле в тексте ошибки (`json: unknown field "decisions"`), а обработчик его не только не отдаёт, но и не логирует — соседняя ветка «тело не доехало» логирует. Цена: клиент двери — редактор пользователя, и `400` без адреса отлаживается перебором. ⚠ Лечение НЕ трогает канон: `errors[]` в нём уже объявлен, довести до ответа нужно ветку разбора. | open | живой платный прогон пака «закрыть цикл», наблюдение H12, 04.09 |
| PD-446 | doc | minor | канон `docs/architecture/14-api-contract/` §`PausedReason`; носители в зоне — `internal/pgstore/books.go:1140`=`PausedCreditExhausted = ingest.PausedCreditExhausted` и `internal/httpapi/v0.go:713`=`ContractHaltReason` | **`paused_reason: credit_exhausted` при 34% НЕТРОНУТОГО баланса — слово называет кредит там, где исчерпан потолок ПРОГОНА.** Замерено 04.09: в один и тот же момент прогон стоял с `paused_reason: credit_exhausted`, а `GET /v0/usage` отвечал `state: ok, halt_reason: null` при остатке `0.102494` из `0.30`. ⚠ **Оба ответа ВЕРНЫ, и счётная половина этого дефекта уже вылечена — пере-открывать её нельзя:** halt читается с АККАУНТА, а не с паузы последнего прогона (`ReadUsage`, разбор в `books.go` над телом), и именно поэтому `halt_reason` здесь честно пуст. Остаётся ОДНО: у `PausedReason` и `AccountHaltReason` разные словари с одним и тем же единственным значением, и это слово — `credit_exhausted`. Пользователь, читающий «кредит исчерпан» рядом с «остаток 34%», получает противоречие, которого в фактах нет. ⚠ Зона правку канона своей рукой не делает: кандидат в состав минора — значение `PausedReason` обязано называть ПРОГОН (`run_ceiling_reached`), словарь аккаунта не трогается. Пока минор не принят, ряд держит вопрос открытым. | open | живой платный прогон пака «закрыть цикл», наблюдение H14, 04.09 |
@@ -116,7 +116,7 @@
| PD-216 | bug | info | `internal/runs/reconcile.go` `outcome` (полоса отказов) | **Exit 12 (`project_locked`) закрывает прогон терминально, хотя это единственный класс полосы, который проходит САМ.** Обоснование «отказ воспроизводим по построению, поэтому перезапуск — петля» верно для 10/11/19 и неверно для лока: другой процесс отпустит проект. Денег не теряется (холд возвращается целиком, списывается 0), но прогон убит и пользователь покупает новый. Не исправлено намеренно: перезапуск на 12 без бэк-оффа — это PD-215 в чистом виде, поэтому оба лечатся вместе | open | адверсариальное ревью P6 (линза денег и гонок) |
| PD-243 | hardening | info | `internal/ingest/resync.go` `DecodeStatus` | **Декодер принимает `null`, `{}` и объект сплошь незнакомых полей за валидный отчёт.** На денежных путях это перекрыто (PD-40: `Spend == nil` откладывает расчёт) и при exit 2 — согласием отчёта (PD-237); остаётся `eta_seconds`, который присваивается безусловно, так что пустой отчёт стирает ETA живого прогона. Лечится проверкой того же класса, что в манифесте: отчёт обязан называть книгу | open | кросс-семейное ревью дофикса P6 (линза шва) |
| PD-244 | bug | info | `internal/runs/reconcile.go` `settle` | **Единственный выход `settle`, который оставляет холд открытым молча:** при `s.Engine == nil` возвращается nil — ни возврата, ни `MarkSettled`, ни строки в логе. В проде недостижимо (`cmd/tmplatformd/runner.go` всегда ставит Engine), но это ровно та тишина, из-за которой класс PD-162 искали глазами | open | кросс-семейное ревью дофикса P6 (денежная линза) |
-| PD-246 | standards | info | `internal/ingest/notes.go`, движковый `pipeline/status.go` `flagReasonSeverity` | **Платформа держит РУКОПИСНУЮ КОПИЮ закрытого словаря чужой зоны, и единственный страж — строка в логе.** Флаг-причины принадлежат движку (`flagReasonSeverity`, 15 значений), карта «причина → контрактный код» живёт на платформе, а выпускаются две программы независимо. Значит существует окно, в котором движок уже эмитит причину, которую эта сборка не знает, и расхождение видно только по ERROR в логе — то есть тогда, когда кто-то его прочитает. ⚠ Проводу окно закрыто и это НЕ дефект: причина вне карты проецируется кодом `unspecified`, а канон уже предписывает клиенту нейтральную фразу для незнакомого кода — то есть значение отдано ветке, которая ратифицирована, а не изобретено правило. Открытым остаётся ДВОЕ: (1) фразы для `unspecified` в приложении А нет (пишет владелец, строка 148) — как и ступеней у всех 15 строк, где граница `attention`/`glance` сегодня догадка платформы; (2) гейта на расхождение нет и со стороны платформы быть не может — импортировать `backend/internal` запрещено ревью-гардом модулей (D39.85). **Предложение зоны (пинг оркестратору, строкой в бэклог ДВИЖКА):** публиковать список флаг-причин данными — артефакт рядом с манифестом либо `tmctl flag-reasons --json` ($0, таблица уже существует), — тогда тест платформы читает его и ПАДАЕТ, если в карте нет строки. Класс «словарь разъехался тихо» закрывается насовсем. ⚠ Обратное решение — чтобы движок эмитил сразу контрактный код — отвергнуто с доводом: это зеркальная утечка, продуктовое слово (`source_residue`, `term_not_applied`) поехало бы в движок, который о контракте знать не должен, и отменило бы причину самого переименования ⚠ **ЗАКРЫТА ЧАСТЬ ПРО ПЛЕЙСХОЛДЕР (акт 5, ответ контрактной сессии 20.08):** `unspecified` ратифицирован как ОБЯЗАННОСТЬ сервера, а не строка чужого словаря — то, что зона уже отдаёт, стало легальным. Открытым остаётся то, ради чего строка заведена: рукописная копия закрытого словаря движка и отсутствие стража, кроме строки в логе ⚠ **СОБЫТИЕ, РАДИ КОТОРОГО СТРОКА ЗАВЕДЕНА, НАСТУПИЛО — 04.09, и оно поймано ПИНГОМ, а не стражем.** Бэкенд-сессия (`textmachine-main-7e`) завела ШЕСТНАДЦАТУЮ причину `FlagOffTargetLang = "off_target_lang"` (`backend/internal/pipeline/disposition.go`) — ответ не на том языке, который просили, и это не эхо исходника. ⚠ Счёт пере-снят СВОЕЙ рукой, а не принят на слово: `grep -c '^\t"[a-z_]*":' internal/ingest/notes.go` → **15** ключей, `flagReasonSeverity` движка на HEAD → те же 15 имён, шестнадцатое живёт пока только в НЕЗАЛЕНДЖЕННОМ дереве бэкенда. То есть окно, которое строка описывает, открыто ровно сейчас, и предсказанная деградация верна: причина вне карты проецируется в `unspecified` и пишет ERROR в лог. **Карту НЕ дополняю, и это не лень:** контрактного кода у новой причины ещё нет, а изобрести его значило бы править приложение А канона своей рукой — состав минора идёт оркестратору на ратификацию (`off_target_lang` → новый код замечания + ступень). **И самое существенное:** класс поймал не гейт, а ПИСЬМО соседней сессии — то есть стража по-прежнему нет, и предложение зоны (движок публикует причины ДАННЫМИ, строка 204 единого бэклога) остаётся единственным, что закрывает его насовсем | open | сессия P7 (самопроверка против приложения А) |
+| PD-246 | standards | info | `internal/ingest/notes.go`, движковый `pipeline/status.go` `flagReasonSeverity` | **Платформа держит РУКОПИСНУЮ КОПИЮ закрытого словаря чужой зоны, и единственный страж — строка в логе.** Флаг-причины принадлежат движку (`flagReasonSeverity`, 15 значений), карта «причина → контрактный код» живёт на платформе, а выпускаются две программы независимо. Значит существует окно, в котором движок уже эмитит причину, которую эта сборка не знает, и расхождение видно только по ERROR в логе — то есть тогда, когда кто-то его прочитает. ⚠ Проводу окно закрыто и это НЕ дефект: причина вне карты проецируется кодом `unspecified`, а канон уже предписывает клиенту нейтральную фразу для незнакомого кода — то есть значение отдано ветке, которая ратифицирована, а не изобретено правило. Открытым остаётся ДВОЕ: (1) фразы для `unspecified` в приложении А нет (пишет владелец, строка 148) — как и ступеней у всех 15 строк, где граница `attention`/`glance` сегодня догадка платформы; (2) гейта на расхождение нет и со стороны платформы быть не может — импортировать `backend/internal` запрещено ревью-гардом модулей (D39.85). **Предложение зоны (пинг оркестратору, строкой в бэклог ДВИЖКА):** публиковать список флаг-причин данными — артефакт рядом с манифестом либо `tmctl flag-reasons --json` ($0, таблица уже существует), — тогда тест платформы читает его и ПАДАЕТ, если в карте нет строки. Класс «словарь разъехался тихо» закрывается насовсем. ⚠ Обратное решение — чтобы движок эмитил сразу контрактный код — отвергнуто с доводом: это зеркальная утечка, продуктовое слово (`source_residue`, `term_not_applied`) поехало бы в движок, который о контракте знать не должен, и отменило бы причину самого переименования ⚠ **ЗАКРЫТА ЧАСТЬ ПРО ПЛЕЙСХОЛДЕР (акт 5, ответ контрактной сессии 20.08):** `unspecified` ратифицирован как ОБЯЗАННОСТЬ сервера, а не строка чужого словаря — то, что зона уже отдаёт, стало легальным. Открытым остаётся то, ради чего строка заведена: рукописная копия закрытого словаря движка и отсутствие стража, кроме строки в логе ⚠ **СОБЫТИЕ, РАДИ КОТОРОГО СТРОКА ЗАВЕДЕНА, НАСТУПИЛО — 04.09, и оно поймано ПИНГОМ, а не стражем.** Бэкенд-сессия (`textmachine-main-7e`) завела ШЕСТНАДЦАТУЮ причину `FlagOffTargetLang = "off_target_lang"` (`backend/internal/pipeline/disposition.go`) — ответ не на том языке, который просили, и это не эхо исходника. ⚠ Счёт пере-снят СВОЕЙ рукой, а не принят на слово: `grep -c '^\t"[a-z_]*":' internal/ingest/notes.go` → **15** ключей, `flagReasonSeverity` движка на HEAD → те же 15 имён, шестнадцатое живёт пока только в НЕЗАЛЕНДЖЕННОМ дереве бэкенда. То есть окно, которое строка описывает, открыто ровно сейчас, и предсказанная деградация верна: причина вне карты проецируется в `unspecified` и пишет ERROR в лог. **Карту НЕ дополняю, и это не лень:** контрактного кода у новой причины ещё нет, а изобрести его значило бы править приложение А канона своей рукой — состав минора идёт оркестратору на ратификацию (`off_target_lang` → новый код замечания + ступень). **И самое существенное:** класс поймал не гейт, а ПИСЬМО соседней сессии — то есть стража по-прежнему нет, и предложение зоны (движок публикует причины ДАННЫМИ, строка 204 единого бэклога) остаётся единственным, что закрывает его насовсем ⚠ **СОБЫТИЕ, КОТОРОЕ ЭТОТ РЯД ПРЕДСКАЗЫВАЛ, НАСТУПИЛО 04.09 — и ряд остаётся ОТКРЫТЫМ, потому что наступление не есть лечение.** Движок завёл шестнадцатую причину `off_target_lang`, канон ратифицировал ей код `wrong_language` (минор `0.10.0`, `D39.194`), а рукописная карта платформы знала пятнадцать — то есть причина ехала читателю как `unspecified` со ступенью «взгляд», и единственным стражем был предсказанный этим рядом лог (`internal/httpapi/reading.go` «notes carry a flag reason this build cannot name»). Строка добавлена, батарея зелёная, но **окно между двумя независимо выпускаемыми программами закрывает не строка, а механизм**, которого по-прежнему нет. Замер стоимости окна теперь есть: между лендингом движковой причины и лендингом платформенной строки прошёл один рабочий день, и всё это время код на проводе был ложным. | open | сессия P7 (самопроверка против приложения А) |
| PD-247 | bug | info | `internal/httpapi/stream.go` `pump` | **Живой поток ОПРАШИВАЕТ базу дважды в секунду на каждое открытое соединение** (кадры + состояние книги). Кадры минтят писатели в своих транзакциях и в других процессах, поэтому подписки у этой стороны нет; выбран опрос, а не `LISTEN/NOTIFY`, потому что кадр это ПОКА, и секунда задержки на поке не наблюдаема рядом с переводом. Цена названа числом: 12 вкладок на книгу = 24 запроса/с к Postgres, оба по индексу и по одной книге. Лечится `pg_notify` в `emitFrame` + один слушатель на процесс — работа на полдня, которая нужна не раньше второго десятка одновременных читателей | open | сессия P7 (собственная оценка) |
| PD-248 | bug | info | `internal/readmodel/readmodel.go` `Refresh` | **Материализация читающей поверхности стоит ДВУХ полных ре-чанков исходника** — `tmctl manifest --json` и `tmctl export --json --pairs`, каждый из которых заново ингестит и режет книгу (~1,4–1,5 с CPU на 23 МБ, строка 100 единого бэклога). Зовётся на границах работы (конец интейка, конец прогона), то есть не на запрос пользователя, но на книге в 2283 главы это секунды CPU и десятки мегабайт JSON через пайп на каждый конец прогона. Дешевле было бы читать сайдкар манифеста напрямую (он уже лежит рядом с БД проекта) и просить у движка экспорт ТОЛЬКО изменившихся глав — второго канала у движка нет, это запрос строкой единого бэклога **Доработка 20.08:** интейк больше не платит за ТРЕТЬЮ ре-нарезку — `books.Parse` передаёт уже прочитанный манифест в `readmodel.RefreshCut`; остаются два (манифест + экспорт), и это цена самих каналов. | open | сессия P7 (собственная оценка) |
| PD-249 | bug | info | `internal/pgstore/runs.go` `ReadRunForSpawn` | **Чтение прогона под спавн линейно по числу ЖИВЫХ прогонов:** читает их все и ищет нужный в цикле. Сегодня незаметно (живых прогонов единицы), но это O(живых) на КАЖДУЮ задачу спавна, и растёт ровно тогда, когда платформа становится нужной. Находка §9 контракт-ревью (research/28), проверена чтением кода: запрос действительно без предиката по id | open | research/28 §9 (пинг оркестратора №17), сверено P7 |
diff --git a/platform/docs/platform-PROGRESS.md b/platform/docs/platform-PROGRESS.md
index 141cd6ee..6897f045 100644
--- a/platform/docs/platform-PROGRESS.md
+++ b/platform/docs/platform-PROGRESS.md
@@ -34,6 +34,10 @@
**Рецепт самодостаточен:** `<скретчпад сессии>/stand/UP.sh` — поднимает Postgres, если он упал, поднимает демона, дожидается `readyz` и кладёт cookie-jar. Пробы — `. <скретчпад>/stand/api.sh`, дальше `api GET /v0/capabilities`.
+⚠ **СТЕНД ОПУЩЕН 04.09 после окончания приёмки — но опущен НАПОЛОВИНУ, и вторая половина жива НАРОЧНО.** Остановлен только демон на `8099`. ⛔ **Postgres на `55433` НЕ гасится ни при каких условиях: кластер ОБЩИЙ** — на нём и `tmstand34` (этот стенд), и `tmstand`, база ЧУЖОГО живого стенда (демон на `8080`, аптайм с ~31.08, `pid 1090739`). Остановка кластера уронила бы чужую сессию: это ровно тот случай, когда «прибраться за собой» означает сломать соседа. Запрет продублирован в самом `UP.sh` — читают рецепт, а не журнал.
+
+⚠ **Данные платного прогона пережили остановку и сверены ПОСЛЕ неё** (`psql` по `tmstand34`): 2 книги · 10 глав · 14 экспортов · 5 прогонов; деньги по `credit_ledger` — `grant 0.600000`, `settlement -0.278319`, холды в ноль (`-1.512494` / `+1.512494`), **остаток `0.321681 USD`**. Списание сходится с отчётом пака до микродоллара. Поднять обратно — `UP.sh`; порт `8099` свободен.
+
**Адреса стенда:** книга `bk_SS5VES2JELESJSTR` · аккаунт `u_YZE7TAXW5YQNRLHM` · БД `tmstand34` на сокете `/tmp/.s.PGSQL.55433` · демон `127.0.0.1:8099` · метрики `127.0.0.1:9474`. Всё ВНЕ дерева репозитория.
⚠ **ДЕМОН УМИРАЛ ТРИЖДЫ ЗА СМЕНУ, И ПРИЧИНУ Я СНАЧАЛА НАЗВАЛА НЕВЕРНО — исправляю здесь же, потому что первая редакция этого абзаца успела записать ошибку как факт.** Я написала «хост убивал демона по нехватке памяти, `dmesg | grep -ci 'killed process'` даёт 56». Число верное, вывод — нет: **`dmesg` не называет `tmplatformd` НИ РАЗУ** (`dmesg | grep -i 'killed process' | grep -ci tmplatformd` → **0**; ядро убивало `python3` соседей). Это была улика окружения, выданная за улику события — ровно тот класс, от которого предостерегает канон. **Настоящая причина названа третьей смертью, пришедшей с объяснением:** демон запускался ФОНОВОЙ ЗАДАЧЕЙ сессии и жил в её группе процессов, поэтому останов задачи уносил демона с собой. ⚠ **Лечение — в рецепте:** `UP.sh` поднимает демона через `setsid nohup … < /dev/null &` + `disown`, и он становится лидером сессии (проверено: `PGID == SID == PID`, `ps -o pid,pgid,sid`). Смерть от чужой памяти этим не исключена — она просто ни разу не подтвердилась. ⚠ **Что при этом ОСТАЁТСЯ верным и важным:** `connection refused` от стенда сначала проверяется как смерть демона, а не как дефект зоны, и все три смерти пережиты БЕЗ ПОТЕРЬ — книга, банк, замечания и артефакты на месте, Postgres не падал ни разу. Состояние цело потому, что оно в Postgres, а прогоны — транзиентные юниты (`D39.106`): ратифицированное решение, предъявившее себя в бою трижды. Сверено после третьего подъёма: `chapters_done 1` из 10, `note_count 6`, `revision 74`.
@@ -42,6 +46,20 @@
⚠ **И одно живое подтверждение, которого я не планировала.** Пока сессия занималась другим, GC двери отработал сам, и его числа сошлись: `owed == artifacts == rows` в обоих проходах (`rows=4 artifacts=4 owed=4`, затем `1/1/1`), а `select state, count(*), count(path) from exports group by state` дал `expired | 12 | 0` — ни одна истёкшая строка не называет файл. То есть **повторяемый unlink (дофикс F5) ни разу не оставил долга на следующий проход**, и проверено это не тестом, а стендом, работавшим сам по себе.
+## ПОСЛЕ ЛЕНДИНГА `adf5e53`: КАНОН УЕХАЛ В `0.10.0`, А СБОРКА ОБЪЯВЛЯЛА `0.9.0` — зона была КРАСНОЙ (04.09, `textmachine-main-34`)
+
+⚠ **Найдено собственной пере-проверкой уже ПОСЛЕ того, как пак принят и заленджен.** Оркестратор ратифицировал контрактный минор (`D39.194`, канон `58bca19`) следом за моим кодом (`adf5e53`), и `go test ./internal/gates/` покраснел: `this build announces contract 0.9.0 and the ratified canon is 0.10.0`. Гейт читает канон, а не копию числа, поэтому поймал сразу.
+
+⚠ **Поднять одну константу было НЕЛЬЗЯ, и это главное в этой записи.** Минор объявляет шестнадцатую причину замечания — `off_target_lang` → `wrong_language` (Приложение А). Карта причин на платформе **рукописная и знала пятнадцать** (`internal/ingest/notes.go`), то есть причина ехала читателю как `unspecified` со ступенью «взгляд». Подъём версии без этой строки объявил бы контракт, которого сборка не отдаёт, — ровно тот дефект, ради которого гейт и существует, только вывернутый наизнанку.
+
+**Сделано две половины, а не одна.** (1) Шестнадцатая строка карты: `"off_target_lang": {"wrong_language", StepAttention}`. (2) `ContractVersion` → `0.10.0`. Батарея после: `make check` выход 0, скипов 0, «every test ran», линтер `0 issues`, `ALARM 11 (baseline 11)`.
+
+⚠ **СТУПЕНЬ — МОЁ РЕШЕНИЕ, и я называю его решением, а не выводом.** Канон оставляет колонку «Ступень» пустой (⬜, ждёт слов владельца). Правило этой карты — «потерял ли читатель текст», и ответ на чужом языке не оставляет от главы ничего пригодного, поэтому `StepAttention`, рядом с `empty_answer`. Ранг движка для этой причины сегодня — дефолтные 8 («недоступно»), что для внятного ответа на чужом языке ложно; ранг 1 приезжает движковым паком. **Эта половина карты не читает ранг вовсе**, так что расхождение на неё не влияет.
+
+⚠ **ПРАВКА ТЕСТА ОБЪЯВЛЯЕТСЯ (D39.183).** `TestEveryReasonAnswersItsOwnStep` держал `len(notes) != 15` и перечисление пятнадцати причин; стало шестнадцать и добавлена строка `off_target_lang → StepAttention`. Что изменилось в поведении: причина, ранее ехавшая как `unspecified`/«взгляд», теперь едет как `wrong_language`/«внимание». Куда уехала гарантия: тест по-прежнему пинит КАЖДУЮ строку поимённо и их счёт — он не ослаблен, он описывает новую карту. Правка вызвана ратификацией, а не желанием зелени.
+
+⚠ **`PD-246` НЕ закрыт этой правкой и закрыт быть не мог.** Ряд предсказывал ровно это: словарь принадлежит движку, карта платформы — рукописная копия, программы выпускаются независимо. Событие наступило, строка добавлена — но окно между двумя релизами закрывает механизм, которого нет. Замер стоимости окна теперь есть и вписан в ряд: **один рабочий день, всё это время код на проводе был ложным.**
+
## СМОУК, КОТОРЫЙ ПРОХОДИЛ ПРИ МЁРТВОМ ДЕМОНЕ — оба стендовых рецепта зоны исправлены (04.09, `textmachine-main-34`)
⚠ **Находка не моя: её увидел оркестратор №22, когда пошёл ПРОВЕРЯТЬ ИСПОЛНЕНИЕМ мою ретракцию про причину смертей демона.** Я назвала чужой стенд на 8080 неудобством («следующая сессия решит, что видит свои данные»). Он проверил и показал, что это не неудобство: **мой же рецепт воспроизводит коллизию и глушит её ложной зеленью.**
@@ -433,7 +451,7 @@ make vuln # No vulnerabilities found.
4. **Третий адрес двери** (`.../exports/{exportId}/content`) канон описывает словами и `operationId` ему не даёт. Предлагаю дать: сегодня это единственный маршрут поверхности, которого нет в перечне операций, и генерируемый клиент про него не знает — а браузер по нему НАВИГИРУЕТ, что и есть его способ существования.
5. **Шестнадцатая причина флага.** Бэкенд-сессия завела `off_target_lang` (`backend/internal/pipeline/disposition.go`, ещё не заленджено). Приложению А нужен ряд: новый код замечания + ступень. ⚠ Фраза пишется по ДОККОММЕНТУ константы, а не по её имени — правило уже записано в компаньоне канона. Карту `internal/ingest/notes.go` НЕ дополняла: контрактного кода ещё нет, а изобрести его значило бы править канон своей рукой.
-6. **`Export.url` объявлен `format: uri`, а отдаётся ОТНОСИТЕЛЬНЫЙ путь** — расхождение с ратифицированным контрактом, найдено адверсариальным проходом. Предлагала минор `uri` → `uri-reference`; **✅ РАТИФИЦИРОВАН оркестратором 04.09**, правит он, и по его же сверке вхождений ДВА, а не одно. Довод принят в усиленном виде: абсолютный URL заставил бы сервис знать свой публичный ориджин, которого за edge-прокси он надёжно не знает — тот же класс, что `PD-101`. Кода это не касается: относительный путь и был тем, что дверь отдаёт.
+6. **`Export.url` объявлен `format: uri`, а отдаётся ОТНОСИТЕЛЬНЫЙ путь** — расхождение с ратифицированным контрактом, найдено адверсариальным проходом. Предлагала минор `uri` → `uri-reference`; **✅ РАТИФИЦИРОВАН оркестратором 04.09** нотой `D39.194` (контрактный минор `0.10.0`), правит он. ⚠ **ВХОЖДЕНИЕ ОДНО — как я и называла изначально.** Первая редакция этого пункта повторяла его сверку «вхождений ДВА»; он её ОТОЗВАЛ, прочитав второе место перед правкой: там поле `type` проблемы RFC 9457 со значением `about:blank`, а это по стандарту абсолютный URI, и трогать его нельзя. ⚠ Класс ошибки назван им прямо и стоит того, чтобы жить здесь, а не в оговорке: **ратификация по грепу без чтения предмета** — совпадение строки принято за совпадение смысла. Тот же класс, что мой собственный счёт красных якорей по обрезанному выводу. Довод принят в усиленном виде: абсолютный URL заставил бы сервис знать свой публичный ориджин, которого за edge-прокси он надёжно не знает — тот же класс, что `PD-101`. Кода это не касается: относительный путь и был тем, что дверь отдаёт.
7. **`409 book_not_ready` на `createExport` — ✅ РЕШЕНО 04.09 ОРКЕСТРАТОРОМ И ИСПОЛНЕНО: код СНЯТ.** Я вынесла это как «канон спорит сам с собой»; оркестратор прочитал описание целиком и показал, что противоречия нет — есть одна недвусмысленная фраза «Nothing about a book's state conflicts with exporting it», которую нарушал КОД. Он прав, и `book_not_ready` на этой двери был изобретением кода. Снято: запрос принимается ВСЕГДА, а что получает книга без дерева глав — отвечает сам экспорт. Пины: `TestABookThatWasNeverCutIsAcceptedAndAnsweredByItsExportRatherThanRefused`, `TestNoBookStateIsARefusalOnThisDoor`, `TestTheWorkersReadKnowsWhetherTheBookWasEverCut`; посадки P1/P2/P3 — все три красят адресно.
⚠ **И ОДНО ВОЗРАЖЕНИЕ ПО ОСТАТКУ ТОГО ЖЕ РЕШЕНИЯ, С ЗАМЕРОМ.** Оркестратор велел отдавать книге без глав «артефакт с пометкой», применяя `D39.178` («дверь ВСЕГДА строит файл»), и оставил мне текст пометки. **Артефакта не существует, и это проверено исполнением, а не чтением:**
diff --git a/platform/internal/httpapi/capabilities.go b/platform/internal/httpapi/capabilities.go
index e375a91f..42fd0ef3 100644
--- a/platform/internal/httpapi/capabilities.go
+++ b/platform/internal/httpapi/capabilities.go
@@ -10,7 +10,7 @@ import "net/http"
// client generated against another one refuses to work and says so — which is why this must be
// raised in the same commit as the code that implements a new minor, and never as a courtesy
// afterwards.
-const ContractVersion = "0.9.0"
+const ContractVersion = "0.10.0"
// Capabilities is what this deployment can do: one flat document, the same for every account.
type Capabilities struct {
diff --git a/platform/internal/ingest/notes.go b/platform/internal/ingest/notes.go
index 56349603..02c39e4f 100644
--- a/platform/internal/ingest/notes.go
+++ b/platform/internal/ingest/notes.go
@@ -57,6 +57,18 @@ var notes = map[string]note{
"glossary_miss": {"term_not_applied", StepGlance},
"length": {"length_mismatch", StepGlance},
"sanitizer_stripped": {"markup_cleaned", StepGlance},
+ // The sixteenth, ratified with contract minor 0.10.0 (D39.194). It is the row PD-246 predicted:
+ // the engine owns the vocabulary, this map is a hand copy, and the two ship independently — so
+ // the reason arrived here as `unspecified` until this line existed.
+ //
+ // ⚠ The STEP is this side's reading and not the engine's rank, by the rule above: an answer that
+ // came back in the wrong language is text the reader LOST — nothing of the chapter is usable — so
+ // it is Attention, alongside `empty_answer`. The canon leaves the column ⬜ awaiting the owner's
+ // phrases; the reading is recorded here so it can be argued with.
+ // ⚠ The engine's own rank for it is TODAY the default 8 ("unavailable"), which is false for a
+ // fluent answer in the wrong language; rank 1 arrives with the engine pack. That is the engine's
+ // half and it does not reach this map: nothing here reads the rank.
+ "off_target_lang": {"wrong_language", StepAttention},
}
// NoteCode maps one engine reason onto what the wire carries.
diff --git a/platform/internal/ingest/notes_test.go b/platform/internal/ingest/notes_test.go
index 2b63b7e8..919348df 100644
--- a/platform/internal/ingest/notes_test.go
+++ b/platform/internal/ingest/notes_test.go
@@ -61,12 +61,14 @@ func TestEveryReasonAnswersItsOwnStep(t *testing.T) {
"loop_degenerate": StepAttention, "decode_error": StepAttention,
"empty": StepAttention, "upstream_not_ok": StepAttention,
"glossary_miss": StepGlance, "length": StepGlance, "sanitizer_stripped": StepGlance,
+ // The sixteenth (minor 0.10.0): the reader lost the chapter, so Attention.
+ "off_target_lang": StepAttention,
} {
if _, step := NoteCode(reason); step != want {
t.Errorf("%s answers step %q, want %q", reason, step, want)
}
}
- if len(notes) != 15 {
- t.Errorf("appendix A has fifteen reasons, the map has %d", len(notes))
+ if len(notes) != 16 {
+ t.Errorf("appendix A has sixteen reasons, the map has %d", len(notes))
}
}