diff --git a/docs/PROGRESS.md b/docs/PROGRESS.md index 4a65762f..1c80f0c5 100644 --- a/docs/PROGRESS.md +++ b/docs/PROGRESS.md @@ -160,7 +160,7 @@ | 198 | **Апгрейд движка стирает замечания и счётчики книги БЕЗВОЗВРАТНО — композиция двух половин, каждая известна по отдельности** (приёмка P7, линза «вне карты», обе половины пере-прочитаны оркестратором). Платформа при смене `manifest_key` сносит ВСЕ `unit_resolutions` книги (`platform/internal/pgstore/readmodel.go:164-165`), рассчитывая, что поток их пере-наполнит; движок анонсирует юнит РОВНО ОДИН РАЗ за жизнь книги — ключ `unit::::` БЕЗ метки нарезки (`backend/internal/pipeline/events.go:399-400`), а леджер анонсов переживает прогоны (`backend/internal/store/outbox.go:96` «already announced… by one that ran before it») ⇒ совпавшие координаты не переанонсируются НИКОГДА. Обостряется порядком: долг на материализацию ставится только на ГРАНИЦАХ работы, поэтому первая зачистка после апгрейда случается в КОНЦЕ первого пост-апгрейдного прогона и сносит замечания ТОГО ЖЕ прогона, за который заплачено. Следствия на проводе: «0 из N» на переведённой книге навсегда · шкала снова предлагает купить переведённое · замечания читателя исчезают. **Лечится с обеих сторон:** движковая половина — метка нарезки в ключе анонса (решение зоны движка), платформенная — не сносить вслепую либо восстанавливать из экспорта. ⚠ Гейт холодного прогона: смысл упражнения — гонять книгу против МЕНЯЮЩЕГОСЯ движка, то есть первый же апгрейд обнулит библиотеку | бэкенд + платформа | **скоро (до первого реального пользователя И до холодного прогона с апгрейдами)** | связка: решение по ключу анонса в движке → правка платформы | приёмка P7 (D39.153) | | 201 | **Движковое «Глава N» доезжает читателю ВНУТРИ текста, обходя дисциплину `heading: null`** (линза шва P7, пере-прочитано оркестратором): `backend/internal/pipeline/export.go:58`=`ApplyHeading` (испр. оркестратором №20 30.08: якорь уезжал с `:230` и с `:266` — код растёт) приклеивает детерминированный порядковый к экспортному тексту первой юнит-главы (`ce.FinalText = chunk.ApplyHeading(...)`), колонка `Source` при этом остаётся heading-stripped. Платформа переносит обе как есть и честно отдаёт `heading: null` — то есть клиент нарисует СВОЙ порядковый на языке своего интерфейса над абзацем, который уже начинается с русского «Глава N», а исходная колонка соответствующего маркера не несёт: пара визуально рассинхронизирована на каждой первой главе. Конкретное следствие открытого К-2 контракта; родня движковой строки **160** (глава без заголовка) | бэкенд | когда-нибудь (с 160) | отдельное решение | приёмка P7 (D39.153) | | 202 | **Живой перевод книги НАСКВОЗЬ через API платформы не гонялся НИ РАЗУ** (obstacle сессии P7, подтверждён приёмкой): всё после «прогон стартовал и остановился» — подпись банка, halt на потолке, резюм после пополнения — покрыто батареей против живого Postgres, но с движком-заглушкой; настоящий манифест, сайдкар банка и причины флагов будут ПЕРВЫМИ данными, форму которых платформа не сочинила сама. Форма проверки (решена): только через API, как ходил бы клиент — интейк → старт → живой движок с ключами провайдера → поток событий → подпись и продолжение → чтение глав/юнитов/замечаний/банка и сверка с тем, что движок реально произвёл; качество перевода НЕ предмет (полигон). ⚠ **Цена ≈$0.05 СНЯТА как протухшая** (D39.179 п.1: пере-пин DeepSeek, множитель ×4.47; испр. 02.09). Замер 31.08 на ровно такой десятиглавой книге: **куплено $0.436110** при собственной проекции движка **`projected_book_usd = 1.1377277707`** и полном оттоке кассы **≈$1.44** (§4 отчёта). ⚠ Это цена НЕПОЛНОГО захода — доставлено 3 юнита из 14; как смету для покупателя брать нельзя. ⚠ Путь ключей платформы — переменная `TM_PLATFORM_ENGINE_KEYS_PATH`, требует АБСОЛЮТНОГО пути (греп по `platform/internal/config/`). **Решение владельца 20.08: ПОСЛЕ холодного прогона движка** — гонять одну неготовую систему против другой смысла нет | платформа | скоро (⚠ прежний гейт «холодный прогон движка» ПАЛ 31.08; живой блокер теперь один — файл ключей платформы `TM_PLATFORM_ENGINE_KEYS_PATH`, лечение операционное и $0) | короткая сессия по промту оркестратора | слово владельца 20.08, D39.153 | -| 203 | **Хвосты контракта после синка 0.4.0 — ОТКРЫТ ОДИН ПУНКТ, остальное исполнено** (тела релеев — `platform/docs/archive/P7_ACCEPTANCE_HANDOFF_2026-08-17.md` §7; сюда переписан НЕ текст, а статус — один носитель на факт). **ИСПОЛНЕНЫ, сверено грепом при лендинге:** (а) минором 0.5.0 (D39.161) — канон предупреждает, что конверт вне `/v0` может прийти БЕЗ обязательного `code` · (б) срезанная фраза компаньона §2.14 на месте (`14-api-contract/README.md:509`=`различать причины отказа клиент не может по замыслу`) · (д) клетка приложения А заполнена `unspecified` (`README.md:1376`) · (и) поле `stop_requested` (D39.152) · (л)(м)(н)(о) — контрактной сессией (остаток по фразам Приложения А несёт строка **204** и лист владельца) · **(г) наполовину** — `unspecified` ратифицирован в каноне (`docs/architecture/14-api-contract/openapi.yaml:1903`=`unspecified` is reserved`; адрес пере-нацелен 02.09 — минор 0.9.0 сдвинул прежний 1784-1787), открыта только ГРАНИЦА ступеней замечаний (две ступени при девяти рангах движка); ⚠ её прежний носитель — строка 148 — СНЯТ 21.08 по слову владельца · **(з) СНЯТ как ратификация 22.08 сквозной трассировкой (разбор, на который ссылается строка 191):** конфликта моделей НЕТ — гейт движка проверяет полноту не ПОДПИСЕЙ, а ФАЙЛА решений, и ОДИН файл со всем банком его снимает (`loadMinedDelta` штампует только `Source`, `mining.go:855-867` → опущенный статус дефолтится в `approved`, `membank/memseed.go:96`=`status = "approved"` → `unsignedEngineSurfaces` выбрасывает лишь `Source=="mined" && Status!="approved"`, `mining.go:710-719`). Пере-диспозиция 27.08 (D39.158 п.7): движковый гейт полноты УСТРАНЁН, не обойдён; остаётся снять обход в платформе — одна боевая строка (`platform/internal/runs/spawn.go`, `--verify-bank` не передаётся на resume) плюс семь строк ставшего ложным обоснования, работа платформенного пака. **ОТКРЫТО РЕАЛЬНО — (к):** ключ `project_db` в `book.yaml`, договорить, кто им владеет: шаблон оператора его не содержит, движок делает необязательным. ⚠ Следствие «на штатном деплое банк не читается» ОПРОВЕРГНУТО живым кодом: путь публикует ДВИЖОК конвертом артефактов, пустой путь — ГРОМКИЙ отказ (`platform/internal/runner/artifacts.go:39`=`the engine published no bank read-out path`); остаток (к) чисто договорной| контракт/доки | скоро (следующее касание контракта) | контрактная сессия | приёмка P7 (D39.153) | +| 203 | **Хвосты контракта после синка 0.4.0 — ОТКРЫТ ОДИН ПУНКТ, остальное исполнено** (тела релеев — `platform/docs/archive/P7_ACCEPTANCE_HANDOFF_2026-08-17.md` §7; сюда переписан НЕ текст, а статус — один носитель на факт). **ИСПОЛНЕНЫ, сверено грепом при лендинге:** (а) минором 0.5.0 (D39.161) — канон предупреждает, что конверт вне `/v0` может прийти БЕЗ обязательного `code` · (б) срезанная фраза компаньона §2.14 на месте (`14-api-contract/README.md:509`=`различать причины отказа клиент не может по замыслу`) · (д) клетка приложения А заполнена `unspecified` (`README.md:1376`) · (и) поле `stop_requested` (D39.152) · (л)(м)(н)(о) — контрактной сессией (остаток по фразам Приложения А несёт строка **204** и лист владельца) · **(г) наполовину** — `unspecified` ратифицирован в каноне (`docs/architecture/14-api-contract/openapi.yaml:1903`=`is reserved and is the server`; адрес пере-нацелен 02.09 — минор 0.9.0 сдвинул прежний 1784-1787), открыта только ГРАНИЦА ступеней замечаний (две ступени при девяти рангах движка); ⚠ её прежний носитель — строка 148 — СНЯТ 21.08 по слову владельца · **(з) СНЯТ как ратификация 22.08 сквозной трассировкой (разбор, на который ссылается строка 191):** конфликта моделей НЕТ — гейт движка проверяет полноту не ПОДПИСЕЙ, а ФАЙЛА решений, и ОДИН файл со всем банком его снимает (`loadMinedDelta` штампует только `Source`, `mining.go:855-867` → опущенный статус дефолтится в `approved`, `membank/memseed.go:96`=`status = "approved"` → `unsignedEngineSurfaces` выбрасывает лишь `Source=="mined" && Status!="approved"`, `mining.go:710-719`). Пере-диспозиция 27.08 (D39.158 п.7): движковый гейт полноты УСТРАНЁН, не обойдён; остаётся снять обход в платформе — одна боевая строка (`platform/internal/runs/spawn.go`, `--verify-bank` не передаётся на resume) плюс семь строк ставшего ложным обоснования, работа платформенного пака. **ОТКРЫТО РЕАЛЬНО — (к):** ключ `project_db` в `book.yaml`, договорить, кто им владеет: шаблон оператора его не содержит, движок делает необязательным. ⚠ Следствие «на штатном деплое банк не читается» ОПРОВЕРГНУТО живым кодом: путь публикует ДВИЖОК конвертом артефактов, пустой путь — ГРОМКИЙ отказ (`platform/internal/runner/artifacts.go:39`=`the engine published no bank read-out path`); остаток (к) чисто договорной| контракт/доки | скоро (следующее касание контракта) | контрактная сессия | приёмка P7 (D39.153) | | 204 | **Движок не публикует причины флагов ДАННЫМИ — карта причин у платформы рукописная и расходится молча** (релей §7(в) хендоффа P7; зона платформы в чужой бэклог не пишет и просила строку сюда — `platform/docs/archive/platform-PROGRESS-P7.md:206`). Сегодня `platform/internal/ingest/notes.go` держит рукописную копию закрытого словаря ЧУЖОЙ зоны: 15 причин движка → коды замечаний контракта, сверено побайтно на 20.08 и верно СЕЙЧАС. Импортировать движок платформе нельзя (D39.85 — разные модули), поэтому расхождение появится молча в тот день, когда движок добавит причину: платформа отдаст `unspecified` и напишет ERROR в лог, то есть деградация честная, но карта протухнет незаметно. ⚠ **Правила заполнения контрактных фраз по этим причинам УЖЕ НАПИСАНЫ** и выводить их заново не надо — компаньон `14-api-contract/README.md`, греп `по ДОККОММЕНТУ` (фраза пишется по доккомменту `disposition.go`, а не по имени константы; класс 2 схлопывается в ОДИН код). Лечение: движок публикует свой словарь причин артефактом-данными (тем же классом, что манифест и сайдкары банка), платформа читает его вместо копии. ⚠ Носителем был ТОЛЬКО регистр платформы (PD-246) — то есть обязательство к зоне движка жило внутри зоны автора; поймано ревьюером полноты выгрузки при лендинге | бэкенд | когда-нибудь (со следующим касанием эмиссии флагов) | отдельное решение | приёмка P7 (D39.153), релей §7(в) | | 205 | **Гейт якорей: ИСПОЛНЕН 21.08 — сверка по СОДЕРЖИМОМУ, двусторонняя дельта, объявленная слепота** (`docs/scripts/counts.py --lint`; область — `docs/` + `platform/docs` + `frontend/docs`; из хука убран быстрый выход по «коммит не трогает docs/»: цели у нас в основном КОД, то есть главный двигатель протухания приезжал ровно тем коммитом, на котором хук молчал). Форма якоря: ОПТ-ИН токен ожидания `` `путь:12-14`=`подстрока` ``, сверка ПРОБЕЛЬНО-нормализованная. ⚠ **Слепота печатается САМИМ гейтом каждый прогон — читать оттуда, копии здесь нет.** Одна её грань в вывод не попала: в ручном прогоне «дельта» = всё незакоммиченное кем угодно, поэтому ТРЕБОВАНИЕ токена живёт только под `--from-index` (ПРОВЕРКА уже написанных токенов дельтой не ограничивается вовсе — токен был истинным при записи, значит упавший есть гниль по построению). **ОСТАТОК:** миграция корпуса на токен-форму органическая — токен требуется на ЗАТРОНУТЫХ строках, счёт растёт сам с каждым касанием. ⚠ Числа здесь НЕ держим (прежняя редакция держала «4 из ~580» и протухла за сутки): текущий счёт токенов, якорей и доков гейт печатает КАЖДЫЙ прогон| оркестратор/доки | сделано 21.08, миграция органическая | исполнено; остаток гасится касаниями | аудит работы №18 + разбор с внешним ревьюером, 21.08 | | 207 | **Пол самосогласованности манифеста стоит только у материализатора, интейк тот же документ ПРИНИМАЕТ — и по такой книге можно ОПЛАТИТЬ прогон** (`PD-367`, вторая волна ревью P8-FIX). Манифест `{ChaptersTotal: 120, UnitsTotal: 400}` с пустым списком глав `Whole()` отвергает, а `books.Parse` заводит книгу `not_started` с `chapter_count=120` и пустым деревом; потолок считается ОТ `chapter_count`. **Очевидное лекарство опровергнуто исполнением самой зоной:** применить `Whole()` на интейке нельзя — вся батарея интейка ездит на манифестах без списка глав, контракт интейка это только счётчики. Значит решение не зонное: либо контракт интейка меняется, либо пол ставится в другом месте цепи | оркестратор → платформа | скоро | нужно решение (лекарство ломает запиненный контракт интейка) | D39.154 п.10, `PD-367` | @@ -188,7 +188,7 @@ | 249 | **Колонка веса бэклога не судится ничем, и девять рядов её потеряли.** Правило таблицы (легенда ниже) даёт ТРИ веса — «блокер-очереди» · «скоро» · «когда-нибудь», — но `counts.py` словаря весов бэклога НЕ ИМЕЕТ: `WEIGHT_VOCAB` в нём — словарь РЕГИСТРА платформы (`major/minor/info`), а `backlog()` лишь считает подстроку «скоро». Замер 02.09 самопроверочным скриптом (`cells(l)[-4]` по всем рядам): **девять** ячеек веса не содержат ни одного слова словаря — ряды **5 · 13б · 94 · 96 · 97 · 149 · 205 · 221 · 246** («отложено-до-замера», «СРОЧНО», «—», «сделано 21.08», «гейчена разморозкой фронта»…). Три из них — 96/97 — надгробия переехавших в зонный бэклог строк, у них «—» законно; остальные шесть потеряли вес молча. ⚠ Найдено не ревизией, а ПОБОЧНО: контролёр приёмки заметил, что мои же две правки снесли вес-слово, я починил свои и пере-считал весь трекер. ⚠ **ВТОРАЯ ПОЛОВИНА КЛАССА, найдена тем же заходом: у ячейки веса нет ни словаря, ни ЭКРАНИРОВАНИЯ.** Счётчик ищет подстроку ГДЕ УГОДНО в ячейке, поэтому упоминание веса в прозе неотличимо от самого веса: фраза «переведена из «скоро»» ре-добавила два ряда в счёт (53 → 56 при одной новой строке), поймано пере-счётом скриптом. **Лечение ОДНИМ правилом на обе половины: вес обязан быть ПРЕФИКСОМ ячейки** (`startswith` по трём словам), и «скоро» считается тоже по префиксу, а не подстрокой — тогда потеря веса и ре-добавление счётом умирают одной проверкой, без экранирования. Сегодня такой гейт даёт **9 нарушений, а не 0** — он родится КРАСНЫМ, и это правильно: покажет то, что есть. ⚠ Два ряда девятки шире класса: **149** («исполнен и заленден 10.08») и **205** («сделано 21.08») объявляют себя ИСПОЛНЕННЫМИ прямо в колонке веса, хотя по правилу таблицы закрытые строки в ней не живут — это либо живой остаток без веса, либо непойманный класс C; **246** несёт самодельное «СРОЧНО», которого в словаре нет. **Решение владельца нужно только на форму гейта; сами девять ячеек чинит оркестратор.** | оркестратор | скоро ($0, гигиена трекера) | режим `counts.py` + правка девяти ячеек | ревизия доков 02.09, приёмка контролёра | | 217 | **Скрипты полигона захардкодили АБСОЛЮТНЫЕ пути двух корней и не работают на этой машине вовсе.** ДВА РАЗНЫХ слома. **Корень репо** — смена пользователя: литерал в 121 файле, из них 104 дословно `REPO = Path("/home/ubuntu/projects/textmachine")`, портабельных форм всего 5. **Корень книг** — оба события порознь: литерал `/home/ubuntu/books` 86 файлов (пользователь), портабельный `Path.home()/"books"` 89 файлов (переезд каталога; на прежних машинах работал). Объединение 173 файла. Пользователь теперь `ubuntu-26`, книги переехали в `<репозиторий>/books` ⇒ ломается и то и другое, включая ЖИВОЙ гейт фазы Д `eval/conformance.py:48`=`REPO = Path("/home/ubuntu/projects/textmachine")`. Живых носителей 26 по корню книг (среди них портабельная форма преобладает 20:6) плюс корневые гейты и сборщики корпусов; остальное — скрипты закрытых эксп-12–16, их владелец велел ВЫБРОСИТЬ, а не чинить (D39.157 п.4а). Форма правки — за зоной; приор оркестратора: один модуль путей, корень резолвится МАРКЕРОМ вверх по дереву (движок так и делает — `backend/internal/miner/miner_parity_test.go:29`=`derived from the repository MARKER`), книги от него, обе ручки перекрываются env | полигон | скоро (блокирует любой прогон зоны) | пак путей полигона | переезд машины 24.08, №19 | | 219 | **Line-якоря в тела D-лога обречены съезжать, и механизм именно в дисциплине эррат.** Эрраты вписываются в КАРТУ ШАПКИ (append-only, D23.3), то есть в начало файла, — значит каждая эррата сдвигает номера строк ВСЕХ тел ниже, и любой якорь вида `05-decisions-log.md:NNN` умирает молча. ⚠ Замерено на себе 27.08: три эрраты за сессию убили якоря в `docs/architecture/17-seam-inbound-law.md` (греп `Эррата, которую ратифицирующая нота обязана поставить`) и `docs/research/25-seam-cold-review.md:5`=`адресуем НОМЕРОМ, а не строкой` (оба целили в `:232`, тело уехало на `:239`; ⚠ 02.09 обе площадки уже переведены на форму номера ноты — прежний голый указатель `17-seam-inbound-law.md:120` показывает пустую строку). Лечение — не пере-нацеливание (оно повторится через эррату), а СМЕНА ФОРМЫ якоря: в тела D-лога целиться номером ноты (`^## D39.106`), который стабилен навсегда, а не строкой. ⚠ Строка ОБЪЯВЛЕНА в CURRENT-STATE 24.08 и до 27.08 в таблице НЕ СУЩЕСТВОВАЛА — очередь три дня ссылалась на носитель, которого нет; поймано свипом якорей при выдаче контрактного минора (⚠ D39.157 п.6 объявил класс закрытым 24.08 — носителя в таблице тогда не было). ⚠ **Первая половина лекарства ПОСТРОЕНА:** `counts.py --lint` сверяет якоря в тела D-лога СТРУКТУРНО и без токена — владелец целевой строки (ближайший `^## D<номер>`) обязан совпасть с номером, названным рядом; остаётся перевести живые якоря на форму номера | оркестратор | скоро (растёт с каждой эрратой) | `--lint` учит форму `файл:^## D<номер>`, живые якоря в D-лог переводятся на неё | ревью доков 24.08, механизм и отсутствие носителя — 27.08, D39.160 | -| 220 | **Репо-широкий гейт доков бутстрапится пакетным менеджером ОДНОЙ зоны, и та заморожена.** Диспетчер `.git/hooks/pre-commit` зоно-нейтрален по построению (`for hook in */scripts/githooks/pre-commit`, `#!/bin/sh`, node не нужен) и судит `CLAUDE.md` + `docs/**` + `platform/docs/**` + `frontend/docs/**` (`docs/scripts/counts.py:500`=`ROOT / "CLAUDE.md"`), но единственный его УСТАНОВЩИК — `frontend/scripts/githooks/install.mjs`, вызываемый ключом `prepare` из `frontend/package.json`. Охват и бутстрап не совпадают ⇒ инструмент решает свою задачу не в полной мере. Проверено исполнением 24.08: на этой машине `.git/hooks/` содержал только `*.sample`, гейт МОЛЧАЛ, ничего не проверив, и поймал первые четыре дефекта только после ручной установки; сессия бэкенда или доков `npm install` не делает никогда. Приор: зоно-нейтральный установщик в `docs/scripts/githooks/` как ЕДИНСТВЕННЫЙ писатель диспетчера, `prepare` фронта зовёт его — фронтовая половина выписана пингом №19 в зонный журнал фронта. ⚠ Ручная установка — временная мера, пока строка открыта; записывать её как порядок работы ЗАПРЕЩЕНО (это ровно тот обход, цену которого называет шапка `CLAUDE.md`) | доки + фронт | скоро | зоно-нейтральный бутстрап + правка `prepare` при разморозке | аудит переезда 24.08, №19 | +| 220 | **Репо-широкий гейт доков бутстрапится пакетным менеджером ОДНОЙ зоны, и та заморожена.** Диспетчер `.git/hooks/pre-commit` зоно-нейтрален по построению (`for hook in */scripts/githooks/pre-commit`, `#!/bin/sh`, node не нужен) и судит `CLAUDE.md` + `docs/**` + `platform/docs/**` + `frontend/docs/**` (`docs/scripts/counts.py:541`=`ROOT / "CLAUDE.md"`), но единственный его УСТАНОВЩИК — `frontend/scripts/githooks/install.mjs`, вызываемый ключом `prepare` из `frontend/package.json`. Охват и бутстрап не совпадают ⇒ инструмент решает свою задачу не в полной мере. Проверено исполнением 24.08: на этой машине `.git/hooks/` содержал только `*.sample`, гейт МОЛЧАЛ, ничего не проверив, и поймал первые четыре дефекта только после ручной установки; сессия бэкенда или доков `npm install` не делает никогда. Приор: зоно-нейтральный установщик в `docs/scripts/githooks/` как ЕДИНСТВЕННЫЙ писатель диспетчера, `prepare` фронта зовёт его — фронтовая половина выписана пингом №19 в зонный журнал фронта. ⚠ Ручная установка — временная мера, пока строка открыта; записывать её как порядок работы ЗАПРЕЩЕНО (это ровно тот обход, цену которого называет шапка `CLAUDE.md`) | доки + фронт | скоро | зоно-нейтральный бутстрап + правка `prepare` при разморозке | аудит переезда 24.08, №19 | | 49 | **Этапы Б+В спеки D15.2** (content-addressed resume / `guard_hash` — D39.31 сознательно не строил) ; этап В = tmctl export-контракт D29.1 (annot-v1 · цвет-мап+rollup · политика красных fail-closed) + операторский протокол-документ релиза гендер-твиста (D5.1): сам механизм УЖЕ построен — until_ch-правка → точечный re-edit со сметой и $0-пере-пином (`repin.go`/`rebill.go`, D39.42 п.5/D39.45) | бэкенд | скоро (ПОДТЯНУТА D39.81: annot-v1 = источник замечаний для фронта — критический путь подключения) | отдельное решение (annot-половина — по контракту 14, D39.99) | D39.34(4), D33 п.5, D39.81 | | 49а | **ALTER-шаги миграций v8–v14 не идемпотентны вопреки шапке `migrate.go:9-11`** (полу-применённая БД не сходится; счётчик версий скрывает) — находка критика полноты пака-19 | бэкенд | когда-нибудь | отдельное решение | D39.56, PACK19_BUILD §6.9 | | 50 | F3-остаток идемпотентности | бэкенд | когда-нибудь | отдельное решение | D39.34(4) | diff --git a/docs/architecture/05-decisions-log.md b/docs/architecture/05-decisions-log.md index de31161b..462049d5 100644 --- a/docs/architecture/05-decisions-log.md +++ b/docs/architecture/05-decisions-log.md @@ -22,7 +22,7 @@ > ⚠ **Эррата 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` остался бы НУЛЁМ навсегда. Это ровно тот класс, от которого предостерегает памятка «не выводить из соседнего механизма, не проверив свой». ⚠ **Что при этом НЕ отменяется:** запрет класть ЮНИТЫ в поле, объявленное в главах, стоит — но объявленная в каноне «одна единица работы» запретом не является, потому что она НЕ молчаливая. > ⚠ **Эррата 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:266-267`=`SessionIdleTTL`), причём окно бездействия скользит на любом аутентифицированном запросе (`platform/internal/auth/middleware.go:66`=`Touch`; поток `Touch` не зовёт — грепом 0, и это не важно при окне в две недели). Реальное следствие `PD-421` — то, что и записано в её теле: сессия, протухшая по бездействию И подметённая, гасит поток с опозданием до часа, то есть после ДВУХ НЕДЕЛЬ без единого действия. **Вопрос СНЯТ с листа владельца как несущественный; правка в паке P12 не нужна, строка остаётся `info`.** Владелец вопросом «ты уверен или придумываешь?» поймал ошибку до того, как она стала заказом на работу. +> ⚠ **Эррата 30.08-а (D39.176 п.5, `PD-421`) — ошибка ОРКЕСТРАТОРА №20, пойманная ВЛАДЕЛЬЦЕМ.** Пункт вынесен владельцу с формулировкой «открытая вкладка теряет сессию примерно через час» — **неверно, и ровно тем классом, от которого предостерегает записка передачи: вывод из соседнего числа вместо проверки.** Час — это ПЕРИОДИЧНОСТЬ `SweepSessions`, а срок жизни сессии другой: `SessionIdleTTL` = **14 суток**, `SessionMaxAge` = **30** (`platform/internal/config/config.go:266-267`=`SessionIdleTTL`), причём окно бездействия скользит на любом аутентифицированном запросе (`platform/internal/auth/middleware.go:66`=`a.Sessions.Touch(r.Context(), digest`; поток `Touch` не зовёт — грепом 0, и это не важно при окне в две недели). Реальное следствие `PD-421` — то, что и записано в её теле: сессия, протухшая по бездействию И подметённая, гасит поток с опозданием до часа, то есть после ДВУХ НЕДЕЛЬ без единого действия. **Вопрос СНЯТ с листа владельца как несущественный; правка в паке P12 не нужна, строка остаётся `info`.** Владелец вопросом «ты уверен или придумываешь?» поймал ошибку до того, как она стала заказом на работу. > ⚠ **Эррата 29.08-б (D39.171 механизм cgroup + D39.172 число операторов) — ДВЕ ошибки ОРКЕСТРАТОРА №19.** > (а) **Механизм, который я объявил причиной неприменения `MemoryMax`, НЕВЕРЕН.** Я вывел «вызывающий > процесс обязан жить внутри `user@.service`» из пробы `systemd-run --user --scope` без diff --git a/docs/architecture/15-money-path.md b/docs/architecture/15-money-path.md index e2056a42..38a4dc51 100644 --- a/docs/architecture/15-money-path.md +++ b/docs/architecture/15-money-path.md @@ -32,7 +32,7 @@ 2. **Выбор потолка — шкала в ГЛАВАХ** (D39.110: от 1 до максимума, ноль не выбираем; потолок принадлежит ПРОГОНУ, в `book.yaml` не пишется). **Максимум = Balance КАК ЕСТЬ** (D39.115 п.2а; эррата в шапке D-лога — `docs/architecture/05-decisions-log.md:4`=`Balance КАК ЕСТЬ`): `Hold` — дебет в момент взятия, баланс уже не содержит открытых холдов, повторное вычитание вдвое укорачивает шкалу. Реализация: `pricing.Model.Scale` — `affordable = balance / PerChapter`, кламп по остатку глав, дефолт = верх шкалы (ратифицировано D39.123 п.2е; компромисс «весь баланс под одну книгу» принят осознанно) — `platform/internal/pricing/pricing.go`, греп `func (m Model) Scale(`. 3. **Ставка $0.03/глава — конфиг-константа платформы:** `DefaultPerChapter = 30_000` micro-USD (`platform/internal/pricing/pricing.go`, греп `DefaultPerChapter`), переопределяется `PerChapterMicroUSD` (`platform/internal/config/config.go`, греп `PerChapterMicroUSD`). ⚠⚠ **ЧИСЛА НИЖЕ — ИЮЛЬСКИЕ ДЕНЬГИ И С 16.08 НЕ ДЕЙСТВУЮТ (испр. ревизией 02.09).** D39.179 п.1 ратифицировал: DeepSeek пере-пинен под цены, вступившие **16.08 16:00 UTC**, множитель к июльским деньгам **×4.47**. ⇒ по действующему прайсу та же выборка даёт среднюю главу **≈$0.051–$0.085** и максимум **≈$0.168**, то есть константа $0.03 не завышена, а **ЗАНИЖЕНА**, и знак вывода перевернулся. ⚠ Иллюстрация живыми деньгами (НЕ калибровка): холодный прогон 31.08 дал полную цепь главы $0.093 и $0.151 при `projected_book_usd = 1.1377277707` — но это **n=2 главы из десяти**, прогон остановлен снапшот-гардом, промпт черновика был банкнотным вместо конвенционного, и сам отчёт запрещает такое употребление дословно (`archive/reports/COLDRUN_V16_REPORT_2026-08-31.md`, греп `калибровать ставку платформы`). **Провенанс ниже — ИСТОРИЯ (D39.165 §1, 28.08):** средняя **$0.0115–$0.0190**, максимум одной главы $0.0375, «завышена в **1.6–2.6×**» — ⚠ но НЕ покрывает максимум: источник (D39.165 §1) добавляет «и отдельные главы её уже пробивают» ($0.0375 против $0.03 = ×1.25), и «купить 10 глав» отдаёт движку $0.30, покупающий ~16–26 глав («денежная ручка с подписью „главы"», `PD-410`). РЕШЕНО: цена — от ОБЪЁМА ИСХОДНИКА (носитель `chapters.units_total`, пишется интейком за $0), настоящий стоп — объёмный потолок В ДВИЖКЕ (см. §2). ⛔ Константу по сегодняшним числам НЕ калибровать (D39.165 ⛔г) — гейт строка 202 бэклога (`docs/PROGRESS.md:162`=`Живой перевод книги НАСКВОЗЬ через API платформы`). Исторический провенанс: exp08 v2 $0.0219/глава → ревизия D30.4 +15–25% → округление ВВЕРХ (низкая ставка опаснее высокой — прогон встанет на потолке посреди книги; `platform/internal/pricing/pricing.go`, греп `Provenance, so the number is auditable`). Временная мера беты — честная оценка движковая (строка 166 бэклога, `docs/PROGRESS.md:135`=`Движковая поверхность оценки`). 4. **Холд прироста ДО спавна.** `holdTx` пишет отрицательную строку `('hold', -amount)` и тем же знаком двигает кэш баланса (`platform/internal/pgstore/credits.go`, греп `func holdTx(` — строку леджера пишет `appendLedger(… "hold", -amount …)`, кэш баланса двигает `MoveBalance` внутри неё); нехватка — `ErrInsufficientCredit`. Холд + прогон + попытка + очередь — одна транзакция (`platform/internal/pgstore/runs.go`, греп `func (s *Store) StartRun(`; рестарт — греп `func (s *Store) RestartRun(`; форма D39.123 п.2д). -5. **Аргумент движку: `bookCap = committed + прирост×ставка`, БЕЗ reserved — PD-158, дословно из кода: `return m.committed + increment`** (`platform/internal/runs/spawn.go`, греп `return m.committed + increment`). Обе фигуры читаются ОДНИМ вызовом `status --json` перед стартом (`bookMeter` — `platform/internal/runs/spawn.go`, греп `func (s *Service) bookMeter(`; отказы — греп `Absent is not zero`); отсутствие любой из них = отказ спавна («absent is not zero», PD-40). Почему без reserved: write-open движка занулит leftover до первой резервации, значит цифра из read-only status к моменту сравнения уже мертва; ратифицированная ранее формула с reserved переплачивала запасом ровно на leftover сверх холда — исполнено обеими формулами против гейта (D39.123 п.2б, поправка к D39.122(в)). Аргумент ≠ холд: он больше холда на committed (`docs/glossary.md:55`=`ЭТО НЕ ХОЛД`). Фактически ушедшее значение хранится в `run_attempts.ceiling_arg_micro_usd` (PD-144, миграция 00011); потолок уходит argv-шаблоном `{{usd}}` (шаблон — `platform/internal/config/config.go`, греп `CeilingArg`; подстановку делает `platform/internal/runner/engine.go`, греп `{{usd}}`; подстановку и рендер argv делает `platform/internal/runner/engine.go`, греп `func (t CeilingTemplate) Args(`; `platform/internal/runs/runs.go`, греп `func (s *Service) ceilingFor(` — однострочная делегация к нему; `spawn.go` потолок только передаёт); строка 145 закрыта D39.122. +5. **Аргумент движку: `bookCap = committed + прирост×ставка`, БЕЗ reserved — PD-158, дословно из кода: `return m.committed + increment`** (`platform/internal/runs/spawn.go`, греп `return m.committed + increment`). Обе фигуры читаются ОДНИМ вызовом `status --json` перед стартом (`bookMeter` — `platform/internal/runs/spawn.go`, греп `func (s *Service) bookMeter(`; отказы — греп `Absent is not zero`); отсутствие любой из них = отказ спавна («absent is not zero», PD-40). Почему без reserved: write-open движка занулит leftover до первой резервации, значит цифра из read-only status к моменту сравнения уже мертва; ратифицированная ранее формула с reserved переплачивала запасом ровно на leftover сверх холда — исполнено обеими формулами против гейта (D39.123 п.2б, поправка к D39.122(в)). Аргумент ≠ холд: он больше холда на committed (`docs/glossary.md:55`=`ЭТО НЕ ХОЛД — он больше холда`). Фактически ушедшее значение хранится в `run_attempts.ceiling_arg_micro_usd` (PD-144, миграция 00011); потолок уходит argv-шаблоном `{{usd}}` (шаблон — `platform/internal/config/config.go`, греп `CeilingArg`; подстановку делает `platform/internal/runner/engine.go`, греп `{{usd}}`; подстановку и рендер argv делает `platform/internal/runner/engine.go`, греп `func (t CeilingTemplate) Args(`; `platform/internal/runs/runs.go`, греп `func (s *Service) ceilingFor(` — однострочная делегация к нему; `spawn.go` потолок только передаёт); строка 145 закрыта D39.122. 6. **Baseline и списание.** `baseline = committed` книги на старте попытки (`platform/internal/runs/spawn.go`, греп `baseline, bookCap := m.committed`); списание попытки = разница счётчика с baseline; счётчик ниже baseline = подменённая БД, сеттлится в ноль с WARN — сами цифры в лог не идут (D39.84; `platform/internal/runs/reconcile.go`, греп `the book's meter reads below`). 7. ⚠ **Терминальный вердикт ОПЕРАТОРА — путь холда мимо settle (добавлено 22.08, P8-FIX/D39.154).** Прогон, чей движок не ответит уже никогда, закрывает человек: `tmplatformctl run abandon --run --reason <текст> [--release-hold]`. Команда ОТКАЗЫВАЕТ, если попытка ещё называет юнит или несёт базовую линию траты — над живым движком прогон не закрывают. Холд возвращается ЦЕЛИКОМ в обоих случаях (к abandon допускается только попытка, не дошедшая до движка, значит она ничего не потратила), а флаг решает лишь КОГДА — сразу или на ближайшем свипе. ⚠ **С пака P11 (`PD-385`) ВЕТВЕЙ ДВЕ, и абзац выше — только ЖИВАЯ** (испр. ревизией 02.09; образец — секция «Строка PHASE = settling» в `platform/deploy/README.md`). У прогона, чья попытка уже КОНЧИЛАСЬ, команда идёт второй ветвью: отказ «юнит/базовая линия» к ней НЕ применяется, к abandon допускается попытка, которая ДОШЛА до движка и ПОТРАТИЛА, холд всё равно возвращается ЦЕЛИКОМ по каждой осиротевшей попытке, а флаг `--release-hold` там ИГНОРИРУЕТСЯ; единственная защита — порог `abandonAfter` неудач расчёта (отказ `ErrSettlementNotStuck`). ⚠ **Ветвление идёт по `runs.finished_at`, а НЕ по наличию осиротевшей попытки** — ратифицированная оговорка `PD-418`. ⚠ **И вторая ветвь НЕ лечит живую:** прогон с намертво заблокированной расплатой уходит в ЖИВУЮ ветвь и закрыт отказом по юниту до сих пор — `PD-424` стоит `open` именно на этой половине. @@ -42,7 +42,7 @@ ## 4. Фронт (зона `frontend/`) -Максимум шкалы — `Balance` КАК ЕСТЬ (D39.115 п.2а); `Reserved` — справочная величина, `ReadAccount` отдаёт обе раздельно (`platform/internal/pgstore/credits.go`, греп `func (s *Store) ReadAccount(`). Шкала — в ГЛАВАХ: `CeilingBounds` — «in CHAPTERS. The conversion to money lives on the platform and is not exposed here in any form» (`docs/architecture/14-api-contract/openapi.yaml`, греп `The conversion to money lives on the platform`); клиенту запрещено клампить повторно; `max_chapters: 0` = состояние «исчерпано» вместо шкалы. Суммы на экран не идут: `Usage` несёт ТРИ обязательных члена — `state` (ok/low/exhausted), `remaining_percent` и `halt_reason` — и ни окон, ни сумм (`docs/architecture/14-api-contract/openapi.yaml`, греп `required: [state, remaining_percent, halt_reason]`); продуктовый носитель — ПТ-35 (`docs/product-requirements.md:63`=`ПТ-35`): стоп по потолку = `paused` + оповещение «перевод остановлен: лимиты исчерпаны»; остаток — страница лимитов (S4+), слово владельца В-3. ⚠ **К-13 ЗАКРЫТ** (D39.132 п.2а: `null` на проводе; пере-суждён ревью 28 §4 №9 и НЕ опрокинут — с уходом дневной оси различие «свой потолок / нет кредита» перестало быть продуктовым). +Максимум шкалы — `Balance` КАК ЕСТЬ (D39.115 п.2а); `Reserved` — справочная величина, `ReadAccount` отдаёт обе раздельно (`platform/internal/pgstore/credits.go`, греп `func (s *Store) ReadAccount(`). Шкала — в ГЛАВАХ: `CeilingBounds` — «in CHAPTERS. The conversion to money lives on the platform and is not exposed here in any form» (`docs/architecture/14-api-contract/openapi.yaml`, греп `The conversion to money lives on the platform`); клиенту запрещено клампить повторно; `max_chapters: 0` = состояние «исчерпано» вместо шкалы. Суммы на экран не идут: `Usage` несёт ТРИ обязательных члена — `state` (ok/low/exhausted), `remaining_percent` и `halt_reason` — и ни окон, ни сумм (`docs/architecture/14-api-contract/openapi.yaml`, греп `required: [state, remaining_percent, halt_reason]`); продуктовый носитель — ПТ-35 (`docs/product-requirements.md:63`=`ПТ-35 | Лимиты «как в Claude Code»`): стоп по потолку = `paused` + оповещение «перевод остановлен: лимиты исчерпаны»; остаток — страница лимитов (S4+), слово владельца В-3. ⚠ **К-13 ЗАКРЫТ** (D39.132 п.2а: `null` на проводе; пере-суждён ревью 28 §4 №9 и НЕ опрокинут — с уходом дневной оси различие «свой потолок / нет кредита» перестало быть продуктовым). ## 5. Открытые дыры и их носители diff --git a/docs/product-requirements.md b/docs/product-requirements.md index 9ea032d4..5145e3fd 100644 --- a/docs/product-requirements.md +++ b/docs/product-requirements.md @@ -86,7 +86,7 @@ |---|---|---|---| | ПТ-37 | **Нарезка по границам СЦЕН**, а не по объёму: «граница сцены — удобная порция, где не теряется смысл; сцены можно склеить, крупная должна вмещаться в контекст» (V6-1/V6-3) | ⭕ | не построено. Книга режется бюджетом ОЦЕНЁННЫХ ВЫХОДНЫХ токенов: пакуются целые абзацы, при переполнении спуск до границ предложений, предложение не разрывается (`backend/internal/chunk/chunker.go:324-330`=`prefer paragraph boundaries`). Сцены как семантической единицы нет; ближайшее — «инерция сцены» A5, где границей сцены считается граница ГЛАВЫ (`backend/internal/pipeline/wave.go:72`=`a new chapter is a scene change`). Полигон отдельно фиксирует, что требование «не исполнено ни разу» | | ПТ-38 | **Майнинг ВСЕХ терминов дешёвой моделью с явной JSON-схемой и КООРДИНАТАМИ вхождений** (V6-1) | 🔶 | майнер и фокус-классификатор построены; KWIC-контексты вхождений персистятся и едут терминологу. Чего нет: ответ терминолога — не JSON, а три поля через табуляцию (`backend/prompts/zh-ru/terminologist.md`), офсетов/координат в нём нет вовсе. Служебная инфа о книге (нарезка по главам) построена; поиск идиом и прочего — не начат | -| ПТ-39 | **Решение по термину С ОБОСНОВАНИЕМ** от дорогой модели, с контекстом первого появления (V6-2) | ⭕ | терминолог возвращает РОВНО три поля — термин, перевод, уверенность 0–100 — и уверенность явно НЕ арбитр, она только сортирует лист ревью. Обоснования нет ни в промпте, ни в парсере. ⚠ Формат его примет аддитивно: поле `Note` в схеме сида существует (`backend/internal/seed/seed.go:81`=`Note`), то есть строить надо ПРОИЗВОДИТЕЛЯ, а не формат — тот же паттерн, что у оси рода (строка 210) | +| ПТ-39 | **Решение по термину С ОБОСНОВАНИЕМ** от дорогой модели, с контекстом первого появления (V6-2) | ⭕ | терминолог возвращает РОВНО три поля — термин, перевод, уверенность 0–100 — и уверенность явно НЕ арбитр, она только сортирует лист ревью. Обоснования нет ни в промпте, ни в парсере. ⚠ Формат его примет аддитивно: поле `Note` в схеме сида существует (`backend/internal/seed/seed.go:81`=`Note string `), то есть строить надо ПРОИЗВОДИТЕЛЯ, а не формат — тот же паттерн, что у оси рода (строка 210) | | ПТ-40 | **Раздельные правила перевода по классам сущностей** — имена отдельно, магические предметы отдельно (V6-2, ПРЯМОЙ вопрос владельца «мы кстати так и делаем или нет?») | 🔶 | **Наполовину да.** Отдельный фокус-классификатор построен, со своей ролью, промптом и осью цены, и раскладывает термины по ЧЕТЫРЁМ закрытым классам `name`/`place`/`title`/`term`, где имена и топонимы транскрибируются, а титулы и реалии переводятся по смыслу (`backend/prompts/zh-ru/classifier.md`). Заведён 01.08, потому что черновой тип врал в 12–22%. **Чего нет:** отдельных ВЫЗОВОВ и отдельных наборов правил на класс — терминолог получает один смешанный список; отдельного класса «магические предметы» не существует, они лежат в `term` вместе с материалами, веществами и классами существ. Окупается ли разделение — не мерено | | ПТ-41 | **Литературный критик: фиксирует расхождение, НЕ предлагает решений, классифицирует проблему; МАЙНИНГ ЗАМЕЧАНИЙ как отдельный банк** (V6-4) | ⭕ | в движке роли нет. Полигон критика мерил и назвал два факта, которые дизайн обязан учесть: критик НЕ видит удалённого целого абзаца, при том что снятую частицу «не» ловит; реальный дефект в черновике удорожает критика в 14 раз. ⇒ полноту обязан ловить детерминированный гейт, а не критик | | ПТ-42 | **Точечный edit-редактор: получает строку банка замечаний, чинит названное место, фиксы инжектятся в текст; НЕ переписывальщик** (V6-5) | ⭕ | в движке нет. Смежный механизм существует и его стоит переиспользовать: точечная пере-редактура юнитов с $0-репином незатронутых (`backend/internal/pipeline/stagerun.go:88-107`=`POINTWISE RE-EDIT`) — но она срабатывает на сдвиг БАНКА, а не на строку замечаний | diff --git a/docs/scripts/counts.py b/docs/scripts/counts.py index fff79117..2d819ed9 100644 --- a/docs/scripts/counts.py +++ b/docs/scripts/counts.py @@ -149,6 +149,23 @@ def col(line: str, from_end: int) -> str: return c[-from_end].strip() +# ⚠ Словарь весов бэклога. Его НЕ БЫЛО: `WEIGHT_VOCAB` ниже — словарь РЕГИСТРА платформы +# (major/minor/info), а вес бэклога не судился ничем, и девять рядов его молча потеряли +# (ревизия 02.09, строка 249). Форма — ПРЕФИКС ячейки: см. комментарий в backlog(). +BACKLOG_WEIGHTS = ("блокер-очереди", "скоро", "когда-нибудь") +# Девять рядов потеряли вес ДО заведения этого гейта — замер 02.09. Держим их «дедовщиной» по той же +# конвенции, что STATUS_GRANDFATHERED ниже: гейт рождается ЗЕЛЁНЫМ на известном долге и КРАСНЕЕТ на +# десятом. Иначе он краснит всегда, и его учатся игнорировать. ⚠ Каждая строка здесь — ряд, который +# гейт перестал проверять; долг числится строкой бэклога 249 и снимается вместе с ней. +# У 96/97 прочерк ЗАКОНЕН — это надгробия строк, уехавших в зонный бэклог (D39.84). +WEIGHT_GRANDFATHERED = {"5", "13б", "94", "96", "97", "149", "205", "221", "246"} + + +def weight_of(line: str) -> str: + """Вес ряда бэклога — с начала ячейки, без разметки-украшений.""" + return col(line, 4).lstrip("*⚠ ") + + def malformed(rows: list, shape: int, what: str) -> list: """Строка таблицы неожиданной формы — расхождение, а не тихий IndexError.""" bad = [] @@ -172,8 +189,15 @@ def backlog(text: str) -> dict: "всего": len(rows), "бэкенд строго": sum(1 for l in rows if col(l, 5) == "бэкенд"), "бэкенд широко": sum(1 for l in rows if "бэкенд" in col(l, 5)), - "скоро": sum(1 for l in rows if "скоро" in col(l, 4)), - "блокеров очереди": sum(1 for l in rows if "блокер-очереди" in col(l, 4)), + # ⚠ Вес считается ПРЕФИКСОМ ячейки, а не подстрокой (ревизия 02.09, строка бэклога 249). + # Подстрока ловила слово в ПРОЗЕ: фраза «переведена из «скоро»» ре-добавляла ряд в счёт, и + # число скакнуло 53 → 56 при одной новой строке. Префикс убивает обе ловушки разом — и эту, + # и потерю самого слова веса при правке ячейки. + "скоро": sum(1 for l in rows if weight_of(l).startswith("скоро")), + "блокеров очереди": sum(1 for l in rows if weight_of(l).startswith("блокер-очереди")), + "вес вне словаря": [BACKLOG_ROW.match(l).group(1) for l in rows + if not weight_of(l).startswith(BACKLOG_WEIGHTS) + and BACKLOG_ROW.match(l).group(1) not in WEIGHT_GRANDFATHERED], "дубли ID": [i for i, n in Counter(ids).items() if n > 1], "битая форма": malformed(rows, BACKLOG_SHAPE, "бэклог"), } @@ -391,9 +415,26 @@ REPO_TOPS = {"backend", "docs", "eval", "frontend", "platform"} # ссылкой» вместо крика «файла нет» — ровно та слепота, против которой написан комментарий выше. # Нужен ВТОРОЙ сегмент: из `backend/internal/` — `internal`, из `frontend/src/` — `src`. ZONE_TOPS = {p.strip("/").split("/")[1] for p in LINT_PREFIXES_RAW if len(p.strip("/").split("/")) >= 2} +# Токен короче этого не различает цель — «сверено по содержимому» становится самообманом (02.09). +TOKEN_MIN = 12 +# Два якоря короче минимума ТРОГАТЬ НЕЛЬЗЯ, и это не долг, а норма: один лежит в ТЕЛЕ ноты D39.176 +# (append-only, D23.3), второй — в теле эксперимента (улика, D23.3). Гейт обязан молчать на них, +# иначе он вечно красный на местах, которые сам же запрещает править. +TOKEN_MIN_EXEMPT = { + ("docs/architecture/05-decisions-log.md", 1617), + ("docs/experiments/23-editor-tier.md", 2244), +} + LINT_SKIP_DOCS = { "docs/research/21-llm-transport-survey.md", "docs/research/26-anthropic-guidance-digest.md", + # ⚠ Временные артефакты разбора доков 01–02.09: они ЦИТИРУЮТ якоря как улику находки, а не + # указывают ими. Линтер читал цитаты как живые указатели и давал 52 предупреждения из 52 по + # ним одним, пряча настоящие за капом вывода. Уедут в архив по закрытии разбора — вместе со + # строками отсюда. Решение принято ПОСЛЕ замера и с ведома владельца, а не для зелени. + "docs/DOC_REVISION_FINDINGS.md", + "docs/DOC_REVISION_REFUTED.md", + "docs/DOC_CLEANUP_PLAN.md", } @@ -550,6 +591,15 @@ def lint_anchors() -> tuple[list[tuple[int, str]], dict[str, int]]: bad.append((0, f"{where}: якорь `{shown}` лежит в теле {owns}, а рядом названы " f"D{'/D'.join(sorted(named))} — адресуй ноту НОМЕРОМ, строка D-лога не переживает подрезку")) continue + if (token is not None and len(token.strip()) < TOKEN_MIN + and (doc_rel, n) not in TOKEN_MIN_EXEMPT): + # ⚠ Токен короче TOKEN_MIN не доказывает НИЧЕГО: «`x`» найдётся в любой строке, + # и якорь считался бы «сверенным по содержимому», не будучи им. Дыра найдена + # при заведении храповика 02.09: доля сверяемых накручивается пустышкой. + tally["notoken"] += 1 + bad.append((2, f"{where}: токен якоря `{shown}` короче {TOKEN_MIN} знаков — " + f"он не различает цель; возьми подстроку подлиннее")) + continue if token is not None: body = body_cache[hit] window = " ".join( @@ -633,6 +683,9 @@ def main() -> int: if b["дубли ID"]: bad.append(f"дубли ID строк бэклога: {b['дубли ID']}") bad.extend(b["битая форма"]) + if b["вес вне словаря"]: + bad.append(f"вес бэклога вне словаря {BACKLOG_WEIGHTS} у строк: {b['вес вне словаря']} — " + f"вес обязан быть ПРЕФИКСОМ ячейки (строка бэклога 249)") bad.extend(r["битая форма"]) bad.extend(r["хвост вне словаря"]) bad.extend(selftest_tail_vocab())