textmachine/docs/BACKEND_BOOKWRITER_PACK_SESSION_PROMPT.md

44 KiB
Raw Blame History

Промт: движок, пак «писатель книги» — переведённая книга становится ФАЙЛОМ

0. Какая проблема и что решит твой результат

Проект переводит книги и не умеет отдать книгу. Ни одной стороной. Движок отдаёт только поток: renderExport (backend/cmd/tmctl/render.go:565) печатает стабильный JSON tm-export-v1 либо --plaintext с аудит-баннерами === CHAPTER N CHUNK M ===. Греп epub|fb2|docx по backend/**/*.go минус _test.go даёт 43 строки, из них 26 в internal/chunk/ingest.go — всё на стороне ЧТЕНИЯ. ⚠ И писатель EPUB в дереве УЖЕ ЕСТЬbackend/internal/chunk/chunktest/epub.go:37-112 (BuildEPUB/BuildEPUBAt): настоящий OCF-контейнер, mimetype первым и zip.Store, container.xml, content.opf с manifest+spine, XHTML. Он ТЕСТОВЫЙ, обратим ингесту по построению и запинен (ingest_test.go:537 TestEPUBFixtureMimetypeIsOCFConformant). Его шапка предупреждает: дублирование zip/OPF-строителя развело бы две копии. Начинать с него — как именно, размечено в §3.1. Платформа обещанные каноном ручки выдачи не монтирует, её ExportFormats захардкожен пустым (platform/cmd/tmplatformd/runner.go:191).

Что решит твой результат: переведённую книгу можно будет открыть в читалке. Сегодня цель №1 канона («издательское художественное качество») не проверяется ничем на выходе: результат некуда открыть. Носитель — строка 236 единого бэклога, вес «блокер-очереди», пункт 1 очереди №20.

Чего твой результат НЕ решает: дверь платформы (createExport/getExport) — отдельная работа ПОСЛЕ живого прогона, потому что до него неизвестно, что реально кладётся в файл.

1. Зона записи и git

Пишешь только в backend/. Не коммитишь: дерево готовишь и передаёшь оркестратору. Канон git — в CLAUDE.md, здесь не дублируется.

2. Карта чтения — ПЯТЬ позиций, дальше только по их ссылкам

  1. backend/internal/pipeline/export.goпрежде всего блок CAVEATS над Export: три состояния КНИГИ, о которых файл обязан не соврать. ⚠ Ещё два состояния — ПОЮНИТНЫЕ, в CAVEATS их нет; они в §3.2.
  2. backend/cmd/tmctl/render.go:562-630=func renderExport — существующий рендер: чем --plaintext НЕ годится в качестве книги (аудит-баннеры, флаги в скобках) и арифметика withheld/incomplete (:573-587), которую твой файл обязан повторить.
  3. backend/internal/chunk/ingest.go — как книга РАЗБИРАЕТСЯ из epub: писатель обязан быть обратим к этому чтению, иначе круг «загрузил → получил» не замыкается (§4 — равенством, не парсом).
  4. docs/architecture/12-go-style-notes.md §0 — норматив общности: пары языков это ДАННЫЕ.
  5. docs/PROGRESS.md, строка БЭКЛОГА 201 (grep -n '^| 201 |') — синтетический заголовок едет внутри текста юнита; твоя граница, см. §3.3.

3. Работа

3.1 Что построить (делай РОВНО так)

Движковый писатель книги из BookExport, вызываемый из tmctl. Минимум два формата: EPUB (книга для читалки) и чистый текст — без аудит-баннеров === CHAPTER …, без флагов в скобках и без операторских маркеров вроде gapMarker (render.go:56-67 — английская лексика аудит-поверхности, читателю не годится). ⚠ Это запрет на ОПЕРАТОРСКИЙ словарь, не на честность: как показать читателю неполноту — решение §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=func ExportArgs:191 ingest.DecodeExport); менять форму tm-export-v1 — значит менять платформу из чужой зоны.

ОДИН файл на книгу на формат — так требует контракт (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), а не набором файлов.

Основа — chunktest/epub.go (инварианты — РОВНО так, форму выноса решаешь сам). Напрямую из tmctl он невызываем: принимает *testing.T и зовёт t.Fatal/t.TempDir() (epub.go:37-50), а копировать его второй раз запрещает его же шапка. Значит общую OCF-часть (mimetype первым и zip.Store, container.xml, запись entry) выносишь в прод-пакет без import "testing" и переводишь chunktest на неё — либо пишешь прод-писатель от его инвариантов и аргументируешь, почему две копии не разойдутся. Инварианты: import "testing" в прод-коде запрещён · тест-файлы, импортирующие chunktest (их 9), остаются зелёными БЕЗ правки ассертов и сигнатур · в прод НЕ переезжает ничего тестового: ручки MType/EntryName, OPF-заглушка с <dc:title>Test</dc:title> и висячим unique-identifier="uid" (epub.go:89-90), XHTML-шаблон epub.go:103 с <title>c</title><style>.x{color:red}</style> — это ДЕКОЙ, на который сидит ingest_test.go:95; поднятый в прод, он уедет читателю в каждую главу.

Файл обязан быть ДЕТЕРМИНИРОВАН: два прогона на одних данных дают побайтово одинаковый файл. EPUB — это zip. У archive/zip из stdlib таймметки записей по умолчанию нулевые, порядок записей = порядок вызовов Create — стартовый chunktest/epub.go этим уже детерминирован. Утечку вносишь только сам: FileHeader.Modified/zip.FileInfoHeader от файловой системы, обход map при записи, и — главное — <meta property="dcterms:modified">, которую EPUB 3 ТРЕБУЕТ (§3.3-бис): значение бери детерминированное из данных прогона, не time.Now(). Провалившийся cmp чинить удалением этого элемента НЕЛЬЗЯ — получишь неконформный EPUB. Докажи прогоном двух ПРОЦЕССОВ, а не рассуждением.

3.2 Пять состояний, о которых файл не имеет права соврать (делай РОВНО так)

Три — счётчики BookExport; Export их уже вычисляет (блок CAVEATS) — не изобретай заново:

  • PendingUnits > 0 — книга переведена НЕ ЦЕЛИКОМ.
  • GhostRows > 0 — есть строки вне текущего манифеста (в Chunks их нет по построению — состояние доступно только счётчиком).
  • ConfigDrift == true — текущий конфиг рендерит не тот снапшот, что несут строки.

⚠ Ещё ДВА — ПОЮНИТНЫЕ, в счётчиках BookExport их НЕТ; renderExport считает их сам (render.go:573-587, баннеры :601-622) — повтори эту арифметику, не изобретай:

  • withheldDisposition != "pending" && FinalText == "": substantive-флаг или skip выше по конвейеру, текст сознательно не отгружен (D2: заражённый вывод не отгружается; export.go:359-395). PendingUnits его НЕ считает. ApplyHeading на пустой текст заголовок не клеит, так что глава молча начнётся со СЛЕДУЮЩЕГО юнита.
  • incompleteDroppedMembers > 0 при непустом FinalText: c-lite выбросил член юнита, текст настоящий, но с дырой без шва (export.go:45-54; причина — DroppedReason).

Живая посадка на обаacceptance (числа в §4): при pending_units=0 там удержан открывающий юнит книги и усечён юнит главы 10. Файл, сверенный только с тремя счётчиками, там выйдет «молча полным» — худший исход этого пака.

Рамка ратифицирована ДО этого пака и бьёт «решаешь сам»:

  • Контракт 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», то есть решается ЗДЕСЬ. Следствие: «только отказ» как единственное поведение НЕДОПУСТИМО — дверь платформы обязана уметь выдать неполную книгу.
  • D29.1(б): reader-facing выдача — fail-closed (0 красных для structurally_complete). D25.1 требует ратифицировать лестницу run_complete / structurally_complete / quality_reviewed / publishable ДО первого reader-facing артефакта; семантика «publishable» и право waiver — у владельца. Решение оркестратора №20, не твой вопрос: reader-facing артефакт — выдача читателю через дверь платформы; этот пак строит МЕХАНИЗМ файла с двумя обязательными поведениями ниже, а лестница ратифицируется владельцем между этим паком и дверью — по твоему вопросу в отчёте (какие ступени писатель обязан различать, что печатать на титуле и в имени файла). Поэтому лестницу в коде не изобретай и в BookExport не добавляй; молчаливый полный файл на книге с любой дырой НЕДОПУСТИМ.

Делай РОВНО так: файл с любой дырой (pending · withheld · incomplete) либо не пишется — отказ с перечнем дыр (глава/юнит/причина), либо пишется с явной пометкой неполноты на титуле и видимым маркером на месте КАЖДОЙ дыры. Оба поведения обязаны существовать (одно по умолчанию, другое флагом) — какое по умолчанию и как выглядят пометка и маркер, решаешь и аргументируешь; словесная форма для читателя — данные пары, не литерал в Go (§3.5). Для ghost и drift контракт ничего не фиксирует — отказ или пометка на твой довод. Три ограничения на форму:

  • Не в снапшот. Загрузчик пака складывает байты КАЖДОГО присутствующего опционального файла в 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 до и после добавления твоего файла-данных — тот же. ⚠ На стенде сдвиг НЕВИДИМ: minirun уже под дрейфом, cmp двух прогонов его не ловит.
  • Книга без langpack (acceptance; runner.go:346 → пак nil) словесной формы не имеет по построению — а это единственная реальная посадка на withheld/incomplete. Там пометка и маркер — несловесные (номер главы/юнита, символ) либо отказ; что именно — реши и аргументируй. Словесный fallback в Go запрещён, langpack_root в копию — запрещён (§3.3).
  • ⚠ Маркер дыры — НЕ в FinalText и не в export.go: провод платформы судит translated/withheld предикатом «текст непуст» (platform/internal/ingest/export.go:104=func unitState).

3.3 Синтетический заголовок — НЕ твоя развилка (делай РОВНО так)

Заголовок «Глава N» сегодня клеится ВНУТРЬ текста юнита — export.go:266, и ещё в ДВУХ точках waverun.go:647 и waverun.go:721 (третья — путь draft-only отгрузки). Инвариант, который они держат и который назван в самом коде: tmctl export и tmctl translate отгружают побайтово одинаковый текст.

Трогать ApplyHeading и любую из трёх точек ЗАПРЕЩЕНО. Дизайн глав НЕ ратифицирован (D39.122 п.2д: heading манифеста — ВРЕМЕННЫЙ рендер движка), а форма для него уже назначена: строка бэклога 160 «Структура глав, Этап 0», гейт которой пал словом владельца 15.08 (D39.136 п.3) — она едет ОТДЕЛЬНЫМ, следующим бэкенд-паком с мандатом максимального качества; отсрочка — решение оркестратора (очередь №20 ставит писателя первым), не твоё. Строка 201 — её родня и стоит «когда-нибудь (с 160)». Этот пак её не решает и не предвосхищает.

Что делать вместо: заголовок для оглавления — тот же детерминированный литерал, который Export УЖЕ держит в момент сборки записи: u.Members[0].Heading (export.go:266). Он приходит через readModelChunks (manifest.go:550) — из сайдкара ManifestChapter.Heading (manifest.go:108), когда тот есть, иначе из пере-нарезки; на обоих путях литерал один (проверено: экспорт minirun без сайдкара и с ним побайтово одинаков). ⚠ Сайдкар <project_db>.manifest.json (manifest.go:137) у minirun/acceptance/coldrun-a ОТСУТСТВУЕТ, loadManifest (manifest.go:436-443) на отсутствующий файл МОЛЧА отдаёт nil, а ChunkExport заголовка не несёт вовсе. Поэтому: писатель сайдкар НЕ читает, tmctl manifest НЕ требует и манифест побочно НЕ пишет. Донести заголовок до писателя — либо in-process (писатель в pipeline, рядом с Export), либо аддитивным полем ChunkExport (heading, omitempty — безопасно, §3.3-бис). Форма tm-export-v1 при этом не меняется, разбирать текст не надо. Если текст юнита начинается тем же заголовком — срезать ИЗВЕСТНЫЕ байты префикса (ApplyHeading — чистая склейка heading + "\n\n" + text, chunker.go:186-192), только при Heading != "", а не угадывать эвристикой.

Heading бывает "", и это ратифицированно легальное состояние (D39.100 п.1, К-3: зашитой формы «Глава N» не существует, легальна книга без глав вовсе; manifest.go:102-103). Два пути: книга без правила заголовка — acceptance/book.yaml не объявляет langpack_root (runner.go:346), и у неё "" у ВСЕХ 25 глав; глава без структурного маркера — глава 1 той же книги остаётся "" даже с правилом. Не «чини» это добавлением langpack_root в копию: состав книги от этого не меняется (те же 37/0/20 — экспорт джойнит по (chapter, chunk_idx)), но правило заголовка меняет ЭКСПОРТНЫЙ ТЕКСТApplyHeading наклеит «Глава N» поверх модельного заголовка в 24 юнитах из 37 («Глава 3\n\nГлава 3: Иди-…»), файл перестанет быть той книгой, что переводилась, а посадка «книга без правила» исчезнет. Что стоит в оглавлении при Heading == "" — реши сам и аргументируй, при двух запретах: никакого словесного литерала в Go («Глава», «Chapter», на любом языке) — только данные (номер главы числом, маркер из данных пары); модельные заголовки внутри прозы («Раздел второй…», «### Шестой раздел…» и — при Heading == "" — «Глава 3: …» у юнитов 3/0, 12/0, 25/0 acceptance) не извлекать и не править — это строка 160; срез префикса только по u.Members[0].Heading, никогда по слову «Глава». Пустой текст ссылки в nav — невалидный EPUB. ⚠ Если найдёшь причину, по которой так нельзя, — пинг, а не своё решение: вопрос гейчен владельцем.

3.3-бис Метаданных книги в BookExport НЕТ — учти до кода

BookExport несёт Version, BookID, TotalUnits, PendingUnits, GhostRows, ConfigDrift, CurrentSnapshot, Chunks — и ни заголовка, ни языка, ни автора. Поля юнита — ChunkExport (export.go:25-55): Chapter, ChunkIdx, SnapshotID, Disposition (ok|flagged|pending), FlagReason, Detail, FinalText, Source (только --pairs), DroppedMembers/DroppedReason; заголовка нет — он внутри FinalText (§3.3).

EPUB 3 требует ПЯТЬ вещей, не три: dc:identifier (на него ссылается unique-identifier пакета), dc:title, dc:language, <meta property="dcterms:modified"> в форме CCYY-MM-DDThh:mm:ssZ и nav-документ (properties="nav" в manifest). Фикстура chunktest несёт только dc:title — epubcheck даёт на ней 5 ошибок; она образец контейнера, не метаданных. nav в spine НЕ класть: ingest.go читает каждый spine-документ как главу (проверено: nav в spine → лишняя глава), и круг §4 ломается. dcterms:modified — не time.Now() (§3.1): константа-эпоха либо величина из данных экспорта (например, из CurrentSnapshot) — выбор аргументируй. dc:identifier — детерминированная строка от BookID.

Заголовок и язык достижимы из config.Book (book.go:25-27: Title, SourceLang, TargetLang). ⚠ Title — отображаемое имя книги, не заголовок на языке цели: на стенде у всех книг title: 蛊真人, из платформы приходит ввод пользователя или имя файла; поле входит в замороженный BriefHash (book.go:337-345). Бери как есть, назови ограничение в отчёте; перевод названия не выдумывать (ни литералом, ни транслитерацией, ни платным вызовом) и title в book.yaml не менять (смена = re-bill). Автора в проекте нет вовсе — источника для dc:creator не существует, и выдумывать его не надо. dc:language берётся из TargetLang, а не литералом: пары — данные. Решаешь сам: расширять ли BookExport/ChunkExport (⚠ добавление поля безопасно: DecodeExport платформы — json.Unmarshal без DisallowUnknownFields, platform/internal/ingest/export.go:78-80=func DecodeExport; полигон формы не проверяет; ПЕРЕНОС существующего поля — нет) или брать метаданные мимо него.

3.4 Куда пишется файл — размечено, не додумывай

cmd/tmctl сам файлов не пишет (греп os.Create|os.WriteFile по cmd/tmctl/*.go минус тесты пуст), но движок пишет их слоем ниже, и у них есть конвенция: сайдкары через pipeline.writeFileAtomic (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=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=refusal band, запинен exitcontract_test.go): отказ §3.2 — новый pipeline.RefusalClass (refusal.go) с константой в полосе 1019, с доводом; чисел вне 05/1019 не заводить. Молча выбранная форма здесь дороже неверной: её потом читает платформенная дверь выдачи.

3.5 Границы

  • Платформу не трогать вообще. Её дверь — следующая работа, не твоя.
  • Формат tm-export-v1 не менять без явного довода: его потребляет полигон и платформа (аддитивные поля по §3.3-бис — не смена формы).
  • Пары языков — данные. Если твой писатель знает слово «Глава», «Оглавление» или порядок имени автора — это утечка.
  • Внешние зависимости: EPUB это zip + XHTML, и стандартная библиотека это умеет (archive/zip, encoding/xml); норма — «least mechanism» (docs/architecture/12-go-style-notes.md §1, строка 24: сперва stdlib, только потом своё/зависимость). ⚠ Носителя STACK_DECISIONS у бэкенда НЕТ — файлы с этим именем лежат у фронта и платформы, это чужие зоны: новая зависимость = довод в записке-плане §6 и пинг оркестратору ДО go get.

4. Самопроверка ИСПОЛНЕНИЕМ (без неё работа не принята)

  • ДАННЫЕ — замерено на копиях 30.08 (HEAD 8977001), пере-прогони сам. В books/ нет ни одной книги, которую tmctl export берёт как есть, и ни одной без CONFIG-DRIFT.
    • coldrun-a НЕ переведена — не строй на ней доказательство. Только волна draft (20 строк chunk_status), финальной стадии нет ни у одного юнита; схема v14; после миграции экспорт даёт 14/14 pending и drift. Годна лишь как естественный образец «pending».
    • minirun (схема v10; chunk_status draft 20 / edit 14) — после миграции 14/14 ok, pending 0, config_drift: true (обе волны). Единственная «полная» реальная книга; заголовки есть (langpack_root задан → первые юниты начинаются «Глава N\n\n»). Рядом трекнуты export.json/export.txt — экспорт ТОЙ ЖЕ БД июльским бинарём (config_drift: false, старая форма без export_version): сегодняшний tmctl немигрированную v10 экспортировать отказывается (exit 13), так что это единственный до-миграционный оракул; после миграции final_text/snapshot_id/disposition обязаны совпасть 14/14 — покажи командой, это доказательство, что миграция и дрейф текст не тронули. Файлы не править.
    • acceptance (v7; chunk_status 57/57 строк — это НЕ юниты) — на HEAD экспорт даёт 37 юнитов, ghost_rows: 20, drift, 35 ok + withheld 1/0 + incomplete 10/0; нарезка сменилась с июля, «57» на HEAD не существует, восстанавливать не надо. Заголовков нет (§3.3). Это посадка сразу на ghost, drift, withheld и incomplete.
    • Дрейф на этом HEAD неустраним — разошлись Go-константы снапшота (версии chunker/classifier/ sanitizer/style_check) и промты; снять его может только платный перепрогон (--resnapshot = re-pay, не делать). Следствие для §3.2 — в записку-план, не в конец работы: чистый бездрейфовый файл на реальной книге НЕДОСТИЖИМ; чистый путь и pending доказывай на синтетике (харнессы ниже), реальные книги — как посадки на drift/ghost/withheld/incomplete. Выбрал «отказ при дрейфе» по умолчанию — реального EPUB у тебя не будет: заяви это в плане с доводом.
    • books/ — отдельный git-репозиторий со своим origin, .db в нём ТРЕКАЮТСЯ; работай на копии в песочнице — но cp -r каталога КОПИЕЙ НЕ ЯВЛЯЕТСЯ. book.yaml, pipeline*.yaml и pairs/*.yaml всех трёх книг несут АБСОЛЮТНЫЕ пути, и на этой машине они РЕЗОЛВЯТСЯ: /home/ubuntu/books — симлинк на оригинальный books-репозиторий, /home/ubuntu/projects/textmachine/… — ЧУЖОЙ ворктри (ветка polygon, живая сессия). 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
      • сиды gu-zhenren/guzhenren-seed*.yaml + исходный txt в песочницу → переписать КАЖДЫЙ абсолютный путь в копиях (project_db первым, затем pipeline, source_file, glossary_seed, langpack_extend, models, langpack_root, pairs/zh-ru.yaml: prompts_root, pipeline.yaml: prompt_override и mining.contrast_path — последний формально: файла в этом дереве нет, его открывает только майнер, mining.go:58) на песочницу и на backend/ ЭТОГО ворктри → grep -rn --include='*.yaml' '/home/ubuntu/books\|/home/ubuntu/projects/textmachine/' <копия> обязан быть пуст (старые пути в run-*.log и в комментарии langpack-extend/zh/surnames-compound.txt движок не читает — не трогать) → только потом tmctl. Доказательство: git -C books status --short -- gu-zhenren до и после — одинаковый вывод, без новых backups/, .lock, -wal, -shm (весь books/ не сравнивай — параллельная полигон-сессия пишет в dovodka/ и chtenie-rol/; строки D gu-zhenren/*/*.db-wal/-shm стоят там ДО тебя — репозиторий трекает wal/shm).
    • Хирургия конфигов В КОПИИ (движок называет каждую причину сам, exit 10): у minirun и coldrun-a удалить ретайрнутые mined_delta:/mined_rejects: (D39.156 п.3); у acceptance в pipeline-acceptance.yaml убрать context.stm_depth/overlap_tokens и prompt: у обеих стадий (промты резолвятся конвенцией <prompts_root>/<пара>/<роль>.md) и положить рядом pairs/zh-ru.yaml (образец — minirun/pairs/, prompts_root на backend/prompts этого дерева). Все правки — списком в отчёт.
  • Файл проходит валидатор EPUB — и это проверяется механически. Проверено 30.08: calibre/ ebook-convert, python-библиотек epub и CLI zip на машине нет, но java ЕСТЬ (/usr/bin/java, OpenJDK 21) и сеть доступна — epubcheck приносится одной командой в песочницу: curl -sL -o epubcheck.zip https://github.com/w3c/epubcheck/releases/download/v5.3.0/epubcheck-5.3.0.zip && unzip -q epubcheck.zip && java -jar epubcheck-5.3.0/epubcheck.jar <book>.epub (проверено: качается ~33 МБ, запускается, валидирует). Это официальный валидатор W3C — механический верификатор оси «издательский результат». Прогони им КАЖДЫЙ сданный EPUB, порог — 0 fatals / 0 errors, warnings перечисли в отчёте с диспозицией; ошибки чинить у писателя, не глушить. Jar живёт в песочнице — в репозиторий и в зависимости не попадает. Не удалось принести — в obstacle команда и её вывод, и тогда «проверено структурно, не валидатором» — не выдавай второе за первое. ⚠ Валидатор — не читалка: «валиден» ≠ «открылся»; читалки на машине нет, эту ось так и помечай.
  • Круг замыкается — РАВЕНСТВОМ, не парсом. chunk.IngestEncoded(<твой.epub>, "", "") (epub-ветка языка и langpack не читает — ingest.go:77-82) и покажи командой: (а) len(doc.Chapters) == число глав книги — ингест кладёт в doc.Chapters КАЖДЫЙ XHTML-документ spine, пустой тоже (ingest.go:534; пустота лишь лишает его плотного номера, :540), поэтому nav/титул в spine не класть даже без текста, а глава без текста — легальная пустая глава; (б) абзацы каждой главы (разрез по пустым строкам, trim, пустые прочь — как splitParagraphs, chunker.go:458) совпадают с абзацами, которые положил писатель, с учётом решения §3.3 о заголовке. Абзац = свой блочный элемент (<p>); <br/> абзаца не даёт (blockTags, ingest.go:614); побайтово текст не совпадёт — сравнивай абзацы. err == nil сам по себе не доказывает ничего: вся книга одним документом «разбирается» одной главой.
  • Детерминизм: два прогона в двух ПРОЦЕССАХ → cmp побайтно. Покажи команду и результат.
  • Пять состояний §3.2 — по посадке на каждое: pending (coldrun-a или синтетика) · ghost, drift, withheld, incomplete (все четыре есть на acceptance) · чистая полная книга — только синтетика. Две посадки УЖЕ есть — не изобретай: backend/internal/pipeline/export_test.go TestExportManifestPending (:186) и TestExportDetectsConfigDrift (:229) на хелперах из runner_test.go. ⚠ Ghost-посадки в тестах НЕТ (греп GhostRows по *_test.go пуст) — строишь сам, отдельной строкой в отчёте. Golden-харнесс (golden_test.go) — гард wire пайплайна, не инструмент для твоего файла; TM_UPDATE_GOLDEN не трогать. Каждое состояние обязано отработать так, как ты решил, и это видно в файле или в отказе. Файл реальной книги, её копии и cmp-вывод — рабочее временное в песочнице: в отчёт идут команды и вывод; воспроизводимость на приёмке держит герметичный тест писателя в backend/ на синтетической книге (chunktest.BuildEPUB, образец герметичного Exportexport_test.go).
  • Править или удалять тест ради зелени НЕДОПУСТИМО (D39.121). Это не абстракция: любой вариант, трогающий заголовок, красит backend/internal/chunk/chunker_heading_test.go:121, и самый дешёвый способ вернуть зелень — правка ассерта. Несогласие с тестом — пинг, а не правка.
  • Дифф ^func Test — ИСПОЛНЕНИЕМ, не памятью: покажи командой, что ни один тест не исчез.
  • Интервальная самоверификация (D39.121): пак длинный, поэтому в СЕРЕДИНЕ работы — проход субагента по готовой части против явных критериев, а не только в конце.
  • make battery целиком, не go test: линтер идёт ДО тестов, и на этом уже попались двое. -race обязателен.
  • Разрешаю субагентов (иначе дефолт харнесса тихо запретит). Модель задавай ЯВНО и знай, сколько их работает. Трудный вопрос — старшей модели.
  • Адверсариальный проход по своей готовой работе перед сдачей. Направление: файл, молча притворяющийся полным — в том числе при pending_units=0 (пустая глава, дыра внутри юнита); dcterms:modified из часов; заголовок потерянный, задвоенный или пустой; nav в spine; кодировка и направление письма; книга из одного юнита и книга из нуля юнитов. Пока идёт проход — дерево не двигается: либо морозь, либо называй агенту коммит.

5. Оси ревью

Издательский результат (файл проходит валидатор и читается как книга) · честность выдачи (неполнота никогда не выглядит полнотой) · детерминизм (побайтовая воспроизводимость). Вправе заменить ось с аргументом.

6. Записка-план и комплектность

До кода — короткая записка-план в docs/PROGRESS.md, секция «Бэкенд»: что берёшь, что не берёшь, чем докажешь, какая политика §3.2 по умолчанию и почему. Перед сдачей — механическая сверка состава против §3 таблицей: пункт → сделано/нет → чем доказано. Пропуск подписывается пропуском. Последний абзац отчёта оказался планом или обещанием? Сделай сейчас.

7. Канал вопросов и право отказаться

Конфликт промта с кодом или доками — пинг через владельца, не интерпретация. Есть право сказать «этого делать не надо» с аргументом.

⚠ Правки задания по ходу работы доезжают ТОЛЬКО релеем через владельца — отдельным его сообщением. Что не пришло релеем — не заказ, даже если пришло по адресу из /tmp/textmachine-channel. Полученную правку эхо-подтверди и вынеси отдельным пунктом отчёта.

8. Заявление = команда

Любое число и любая категорика — с командой, которой получены. Приёмка пере-ранит: клейм без пере-прогона записывается как «со слов сессии». Утверждение без file:line/замера помечай словом «мнение» — эрудиция уликой не считается.

9. Эхо-протокол старта

ДО первой правки — ≤10 строк: скоуп / инварианты / чего не делаешь. Отправь первым действием по адресу оркестратора из /tmp/textmachine-channel (механизм — CLAUDE.md; впиши туда СВОЙ блок первым делом). Канала нет ⇒ НЕ искать: вопрос секцией в отчёт, работа продолжается.

10. Obstacle reporting

Обязательная секция: что НЕ удалось и что осталось непроверенным. Пустой не бывает. Три исхода: «подтверждено» / «опровергнуто» / «не проверено» — третье не сваливать во второе.

11. Журнал

Итоги и вопросы — docs/PROGRESS.md, секция «Бэкенд» (это единственное исключение из «docs/ — зона оркестратора»).