diff --git a/docs/BACKEND_BOOKWRITER_PACK_SESSION_PROMPT.md b/docs/BACKEND_BOOKWRITER_PACK_SESSION_PROMPT.md index e3b7310d..9a565507 100644 --- a/docs/BACKEND_BOOKWRITER_PACK_SESSION_PROMPT.md +++ b/docs/BACKEND_BOOKWRITER_PACK_SESSION_PROMPT.md @@ -30,7 +30,7 @@ zip/OPF-строителя развело бы две копии. **Начина 1. `backend/internal/pipeline/export.go` — **прежде всего блок CAVEATS над `Export`**: три состояния КНИГИ, о которых файл обязан не соврать. ⚠ Ещё два состояния — ПОЮНИТНЫЕ, в CAVEATS их нет; они в §3.2. -2. `backend/cmd/tmctl/render.go:562-630` — существующий рендер: чем `--plaintext` НЕ годится в качестве +2. `backend/cmd/tmctl/render.go:562-630`=`func renderExport` — существующий рендер: чем `--plaintext` НЕ годится в качестве книги (аудит-баннеры, флаги в скобках) и арифметика `withheld`/`incomplete` (`:573-587`), которую твой файл обязан повторить. 3. `backend/internal/chunk/ingest.go` — как книга РАЗБИРАЕТСЯ из epub: писатель обязан быть обратим к @@ -49,12 +49,12 @@ zip/OPF-строителя развело бы две копии. **Начина читателю не годится). ⚠ Это запрет на ОПЕРАТОРСКИЙ словарь, не на честность: как показать читателю неполноту — решение §3.2. Существующий `--plaintext` не трогать и не переименовывать — это его ВЫВОД, запинённый `export_cli_test.go`/`exportgap_cli_test.go`; вынос арифметики withheld/incomplete в -общий хелпер допустим. JSON — машинная поверхность двух потребителей: полигон (`eval/README.md:80` — -«`--plaintext` для глаз, дефолтный JSON для скриптов») и платформа (`platform/internal/runner/engine.go:148` -`ExportArgs` → `:191` `ingest.DecodeExport`); менять форму `tm-export-v1` — значит менять платформу из +общий хелпер допустим. JSON — машинная поверхность двух потребителей: полигон (`eval/README.md:80`=`для глаз` — +«`--plaintext` для глаз, дефолтный JSON для скриптов») и платформа +(`platform/internal/runner/engine.go:148`=`func ExportArgs` → `:191` `ingest.DecodeExport`); менять форму `tm-export-v1` — значит менять платформу из чужой зоны. -⛔ **ОДИН файл на книгу на формат** — так требует контракт (`docs/architecture/14-api-contract/openapi.yaml:2489-2492`: +⛔ **ОДИН файл на книгу на формат** — так требует контракт (`docs/architecture/14-api-contract/openapi.yaml:2489-2492`=`A built copy of the book`: `Export` = «A built copy of the book, behind a link»); деление на главы живёт ВНУТРИ файла (spine EPUB, разделители в txt), а не набором файлов. @@ -99,7 +99,7 @@ EPUB — это zip. У `archive/zip` из stdlib таймметки запис худший исход этого пака. **Рамка ратифицирована ДО этого пака и бьёт «решаешь сам»:** -- Контракт 14 (D39.99; `docs/architecture/14-api-contract/openapi.yaml:836-853`, `createExport`): неполная +- Контракт 14 (D39.99; `docs/architecture/14-api-contract/openapi.yaml:836-853`=`finished or not`, `createExport`): неполная книга экспортируется — «may be exported, finished or not», а ЧТО содержит файл неполной книги — «not fixed here», то есть решается ЗДЕСЬ. Следствие: «только отказ» как единственное поведение НЕДОПУСТИМО — дверь платформы обязана уметь выдать неполную книгу. @@ -119,8 +119,8 @@ EPUB — это zip. У `archive/zip` из stdlib таймметки запис данные пары, не литерал в Go (§3.5). Для ghost и drift контракт ничего не фиксирует — отказ или пометка на твой довод. Три ограничения на форму: - ⛔ **Не в снапшот.** Загрузчик пака складывает байты КАЖДОГО присутствующего опционального файла в - `pack.Version()` (`backend/internal/lang/langpack.go:289-298`), а она входит в snapshot-id стадий - (`pipeline/snapshot.go:381`): новый файл-данные по этому контракту = resnapshot = перекупка каждой книги + `pack.Version()` (`backend/internal/lang/langpack.go:289-298`=`h.Write`), а она входит в snapshot-id стадий + (`backend/internal/pipeline/snapshot.go:381`=`LangpackVersion`): новый файл-данные по этому контракту = resnapshot = перекупка каждой книги с `langpack_root`. Читательские слова — факт РЕНДЕРА, не перевода: канал для них НЕ складывается в `Version()` (отдельный не-фолдимый файл рядом с паком или вне пака) — форму решаешь и аргументируешь. Доказательство командой: `current_snapshot` экспорта копии minirun до и после добавления твоего @@ -130,7 +130,7 @@ EPUB — это zip. У `archive/zip` из stdlib таймметки запис несловесные (номер главы/юнита, символ) либо отказ; что именно — реши и аргументируй. Словесный fallback в Go запрещён, `langpack_root` в копию — запрещён (§3.3). - ⚠ Маркер дыры — НЕ в `FinalText` и не в `export.go`: провод платформы судит `translated`/`withheld` - предикатом «текст непуст» (`platform/internal/ingest/export.go:104`). + предикатом «текст непуст» (`platform/internal/ingest/export.go:104`=`func unitState`). ### 3.3 Синтетический заголовок — НЕ твоя развилка (делай РОВНО так) @@ -199,23 +199,23 @@ nav-документ (`properties="nav"` в manifest). Фикстура `chunkte **Автора в проекте нет вовсе** — источника для `dc:creator` не существует, и выдумывать его не надо. ⛔ `dc:language` берётся из `TargetLang`, а не литералом: пары — данные. **Решаешь сам:** расширять ли `BookExport`/`ChunkExport` (⚠ добавление поля безопасно: `DecodeExport` платформы — `json.Unmarshal` без -`DisallowUnknownFields`, `platform/internal/ingest/export.go:78-80`; полигон формы не проверяет; ПЕРЕНОС +`DisallowUnknownFields`, `platform/internal/ingest/export.go:78-80`=`func DecodeExport`; полигон формы не проверяет; ПЕРЕНОС существующего поля — нет) или брать метаданные мимо него. ### 3.4 Куда пишется файл — размечено, не додумывай `cmd/tmctl` сам файлов не пишет (греп `os.Create|os.WriteFile` по `cmd/tmctl/*.go` минус тесты пуст), но движок пишет их слоем ниже, и у них есть конвенция: сайдкары через `pipeline.writeFileAtomic` -(`internal/pipeline/artifact.go:26`; используют `manifest.go:401`, `bankexport.go:152`, `mining.go`) — место +(`backend/internal/pipeline/artifact.go:26`=`func writeFileAtomic`; используют `manifest.go:401`, `bankexport.go:152`, `mining.go`) — место рядом с `ProjectDB`, запись атомарная, путь ПУБЛИКУЕТСЯ в конверте `StatusArtifacts` (`status.go:115-145`), -и платформа берёт пути ТОЛЬКО оттуда (`platform/internal/runner/artifacts.go:39`; закон шва +и платформа берёт пути ТОЛЬКО оттуда (`platform/internal/runner/artifacts.go:39`=`no bank read-out path`; закон шва `docs/architecture/17-seam-inbound-law.md` §1 — движок владеет путями своей схемы, платформа путь не выводит сама). Приор (опровергается аргументом): файл книги — такой же движковый артефакт: детерминированное место рядом с `ProjectDB`, атомарная запись, строка в `StatusArtifacts`; флаг явного пути — сверх этого, не вместо. **Реши и аргументируй:** форма вызова — новый глагол `tmctl` или флаг у `export` (глаголы — замороженный список `main.go:182`; форму потом читает дверь платформы) · флаг пути или конвенция · поведение при существующем файле (перезапись или отказ — у бэкапа отказ, -`store/backup.go`) · публикуешь ли путь в конверте уже в этом паке. Коды выхода — замороженный словарь (`backend/cmd/tmctl/main.go:51-145`, запинен +`store/backup.go`) · публикуешь ли путь в конверте уже в этом паке. Коды выхода — замороженный словарь (`backend/cmd/tmctl/main.go:51-145`=`refusal band`, запинен `exitcontract_test.go`): отказ §3.2 — новый `pipeline.RefusalClass` (`refusal.go`) с константой в полосе 10–19, с доводом; чисел вне 0–5/10–19 не заводить. Молча выбранная форма здесь дороже неверной: её потом читает платформенная дверь выдачи. @@ -262,7 +262,7 @@ nav-документ (`properties="nav"` в manifest). Фикстура `chunkte песочнице — но `cp -r` каталога КОПИЕЙ НЕ ЯВЛЯЕТСЯ.** `book.yaml`, `pipeline*.yaml` и `pairs/*.yaml` всех трёх книг несут АБСОЛЮТНЫЕ пути, и на этой машине они РЕЗОЛВЯТСЯ: `/home/ubuntu/books` — симлинк на оригинальный books-репозиторий, `/home/ubuntu/projects/textmachine/…` — ЧУЖОЙ ворктри (ветка polygon, - живая сессия). `LoadBook` абсолютный путь не пере-рутит (`config/book.go:173`), `tmctl migrate` + живая сессия). `LoadBook` абсолютный путь не пере-рутит (`backend/internal/config/book.go:173`=`filepath.IsAbs`), `tmctl migrate` мигрирует `book.ProjectDB` на месте и кладёт `backups/` рядом (`migrate.go:83-84`); даже read-only `export`/`status` открывают WAL и создают `.lock`/`-wal`/`-shm` рядом с оригиналом. Порядок: `go build -o <песочница>/tmctl ./cmd/tmctl` из `backend/` → скопировать каталог книги + `gu-zhenren/langpack-extend` diff --git a/docs/PROGRESS.md b/docs/PROGRESS.md index 7cf36b59..8e33b48f 100644 --- a/docs/PROGRESS.md +++ b/docs/PROGRESS.md @@ -176,7 +176,7 @@ | 196 | **Остаточный класс строки 176: неперехватываемые `fatal error` Go** (конкурентная запись в map, OOM, стек-оверфлоу) по-прежнему дают exit 2, то есть читаются платформой как «завершено с флагами» — `recover` их не ловит по устройству языка (подтверждено линзой шва исполнением). Строка 176 закрыта для ПАНИК, не для fatal. Лечение — вне exit-контракта: сторож процесса у платформы либо маркер живости в артефакте прогона | бэкенд/платформа | когда-нибудь | отдельное решение | приёмка №18, линза шва | | 197 | **Фикс-лист приёмки пака честности ФЧ-1…ФЧ-8** (D39.149; тело — [archive/PROGRESS-2026-08-17.md](archive/PROGRESS-2026-08-17.md), запись приёмки №18): три НЕПРИБИТЫЕ гарантии (снятие фильтра по книге в `CheckpointUsageForBook` не валит ни один тест — воспроизведено оркестратором · ключ ячейки без `chunk_idx` съедает деньги соседнего чанка · ветка «строка не пере-оценена» не ловится на продовом пути) · два докстринга `reprice.go` описывают НЕ то, что делает код · 187 исполнен наполовину (тот же снятый закон в докстринге теста и `backend/docs/D15.2-*.md:317`) · `HistoricalRows` не доезжает до `status`/`--json`, второй операторский текст (`--accept-rebill`) без basis · **ФЧ-5 РЕШЁН владельцем 17.08 (D39.150): прайсить по модели, которую стадия резолвит СЕЙЧАС, + округление вверх и оговорка в тексте согласия** · `Status()` жёстко падает при ошибке репрайсера против соседней политики деградации · паника поверх ceiling-стопа даёт `failed` вместо `paused` (PD-113) · три стейл-докстринга. Носителя-сессии НЕТ — по слову владельца 17.08 пока только записано | бэкенд | скоро (следующее касание денежного пути; ФЧ-5 обязателен до первого пере-прогона со сменой модели) | дофикс-промт ЛИБО попутно ближайшим паком | **D39.149**, **D39.150** | | 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:266` (испр. оркестратором №20 30.08: якорь уехал с `:230`) приклеивает детерминированный порядковый к экспортному тексту первой юнит-главы (`ce.FinalText = chunk.ApplyHeading(...)`), колонка `Source` при этом остаётся heading-stripped. Платформа переносит обе как есть и честно отдаёт `heading: null` — то есть клиент нарисует СВОЙ порядковый на языке своего интерфейса над абзацем, который уже начинается с русского «Глава N», а исходная колонка соответствующего маркера не несёт: пара визуально рассинхронизирована на каждой первой главе. Конкретное следствие открытого К-2 контракта; родня движковой строки **160** (глава без заголовка) | бэкенд | когда-нибудь (с 160) | отдельное решение | приёмка P7 (D39.153) | +| 201 | **Движковое «Глава N» доезжает читателю ВНУТРИ текста, обходя дисциплину `heading: null`** (линза шва P7, пере-прочитано оркестратором): `backend/internal/pipeline/export.go:266`=`ApplyHeading` (испр. оркестратором №20 30.08: якорь уехал с `:230`) приклеивает детерминированный порядковый к экспортному тексту первой юнит-главы (`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 на десятиглавую книгу. **Решение владельца 20.08: ПОСЛЕ холодного прогона движка** — гонять одну неготовую систему против другой смысла нет | платформа | скоро (гейт: холодный прогон движка) | короткая сессия по промту оркестратора | слово владельца 20.08, D39.153 | | 203 | **Хвосты контракта после синка 0.4.0 — ОТКРЫТА ЧЕТВЕРТЬ, остальное исполнено** (тела релеев — `platform/docs/archive/P7_ACCEPTANCE_HANDOFF_2026-08-17.md` §7; сюда переписаны НЕ тела, а статус — один носитель на факт). ⚠ Сверено грепом при лендинге, потому что первая редакция этой строки уже разошлась с деревом: **(б) ИСПОЛНЕН** — срезанная фраза компаньона §2.14 стоит на месте (`14-api-contract/README.md:556`=`различать причины отказа клиент не может по замыслу`) · **(д) ИСПОЛНЕН** — клетка приложения А заполнена `unspecified` (`README.md:1376` — ⚠ первая редакция писала `:1391`, это строка ДРУГОГО приложения; испр. 20.08) · **(г) наполовину** — `unspecified` ратифицирован в каноне (`openapi.yaml:1784-1787`), открыта только ГРАНИЦА ступеней замечаний (две ступени при девяти рангах движка) → это строка **148** · **(и)** закрыт полем `stop_requested` (D39.152) · (л)(м)(н)(о) закрыты контрактной сессией. **ОТКРЫТО РЕАЛЬНО:** ⚠ **(а) ИСПОЛНЕН 27.08 минором 0.5.0 (D39.161)** — канон получил предупреждение о конверте вне `/v0`; открытым по строке остаётся (к). Прежний текст пункта: (а) канон-половина про `/auth` — компаньон поверхность описывает (§2.14), а канон говорит только «Signing in is not part of this surface» (`openapi.yaml:35-36`) и НЕ предупреждает, что конверт вне `/v0` может прийти БЕЗ обязательного `code`; сгенерированный по канону клиент об это спотыкается · (з) ⚠ **СНЯТО КАК РАТИФИКАЦИЯ 22.08 — конфликта моделей НЕТ, я нёс причину и следствие наоборот.** Прежняя редакция писала: «D39.144 ратифицировал подпись = ОДИН акт, движок держит пер-термный гейт полноты, одно из двух должно уступить». Сквозная трассировка это опровергла КОДОМ: гейт проверяет полноту не ПОДПИСЕЙ, а ФАЙЛА решений, и ОДИН акт его снимает — `loadMinedDelta` штампует только `Source`, статус не трогает (`mining.go:855-867`) → опущенный статус дефолтится в `approved` (`membank/memseed.go:96`=`status = "approved"`) → `unsignedEngineSurfaces` выбрасывает лишь `Source=="mined" && Status!="approved"` (`mining.go:710-719`) ⇒ промоутнутые строки остаются сид-поверхностями, дельта пустеет, стоп снимается. Шапка файла говорит это прямым текстом. **Один файл со всем банком = один ОК.** Обход в платформе (`--verify-bank` не передаётся на resume) — костыль под ОТСУТСТВУЮЩУЮ ДОСТАВКУ, а не под расхождение моделей: как только доставка появится, флаг можно просто оставить включённым. Настоящий и единственный блокер темы — владение `book.yaml`, строка **199(а) [ЗАКРЫТА D39.166]**. Ратификации здесь НЕ нужно; остаётся снять обход одной боевой строкой (`platform/internal/runs/spawn.go`) и семью строками ставшего ложным обоснования — работа платформенного пака, не решение владельца · (к) ключ `project_db` в `book.yaml`: договорить, кто им владеет — шаблон оператора его не содержит, движок делает необязательным. ⚠ **Следствие «на штатном деплое банк не читается» ОПРОВЕРГНУТО живым кодом** (проверено при лендинге): ⚠ носитель довода СМЕНИЛСЯ лендингом P9 (D39.162): опровержение опиралось на фолбэк `.db`, который платформа вычисляла сама, а он СНЕСЁН вместе с дублем конвенции (строка 213). Сегодня путь публикует ДВИЖОК конвертом артефактов, и пустой путь — ГРОМКИЙ отказ (`platform/internal/runner/artifacts.go:39`=`the engine published no bank read-out path`), то есть банк либо читается, либо деплой говорит об этом вслух; первая редакция этой строки повторила клейм хендоффа зоны, не сверив с деревом — остаток чисто договорной · **(л) остаток закрытой строки 148 — Приложение А компаньона.** ⚠ Сама 148 СНЯТА 21.08 по слову владельца: она держала мир, которого больше нет — «карта потеряна инцидентом 08.08, реконструкция у оркестратора №15, решить, брать ли черновиком». Реконструкция брать нечего: прежняя карта вела «причина движка → ФРАЗА», то есть предполагала, что фразу рисует СЕРВЕР, а батч 0.3.0 эту политику ОТМЕНИЛ — `Note` несёт машинный `code`, фразу рисует клиент, `Note.message` с провода снят (`14-api-contract/README.md:1608`=`Карта «причина → КОД контракта`). Реально пуста другая клетка: соответствие причин движка кодам замечаний, и правила её заполнения в компаньоне уже написаны (фраза по ДОККОММЕНТУ `disposition.go`, а не по имени константы — инверсия уже стоила двух фраз; класс 2 схлопывается в ОДИН код). Исполнитель — контрактная сессия, не оркестратор ⚠ **ПЕРЕ-ДИСПОЗИЦИЯ 27.08 (D39.158 п.7):** хвост (з) закрыт — «ратификации не нужно» отвечало на вопрос про один файл решений, не про флажок; движковый гейт полноты снят, не обойдён.| контракт/доки | скоро (следующее касание контракта, вместе с 200) | контрактная сессия | приёмка 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 в лог, то есть деградация честная, но карта протухнет незаметно. Лечение: движок публикует свой словарь причин артефактом-данными (тем же классом, что манифест и сайдкары банка), платформа читает его вместо копии. ⚠ Носителем был ТОЛЬКО регистр платформы (PD-246) — то есть обязательство к зоне движка жило внутри зоны автора; поймано ревьюером полноты выгрузки при лендинге | бэкенд | когда-нибудь (со следующим касанием эмиссии флагов) | отдельное решение | приёмка P7 (D39.153), релей §7(в) |