44 KiB
Промт: движок, пак «писатель книги» — переведённая книга становится ФАЙЛОМ
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. Карта чтения — ПЯТЬ позиций, дальше только по их ссылкам
backend/internal/pipeline/export.go— прежде всего блок CAVEATS надExport: три состояния КНИГИ, о которых файл обязан не соврать. ⚠ Ещё два состояния — ПОЮНИТНЫЕ, в CAVEATS их нет; они в §3.2.backend/cmd/tmctl/render.go:562-630=func renderExport— существующий рендер: чем--plaintextНЕ годится в качестве книги (аудит-баннеры, флаги в скобках) и арифметикаwithheld/incomplete(:573-587), которую твой файл обязан повторить.backend/internal/chunk/ingest.go— как книга РАЗБИРАЕТСЯ из epub: писатель обязан быть обратим к этому чтению, иначе круг «загрузил → получил» не замыкается (§4 — равенством, не парсом).docs/architecture/12-go-style-notes.md§0 — норматив общности: пары языков это ДАННЫЕ.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) — повтори эту арифметику, не изобретай:
- withheld —
Disposition != "pending" && FinalText == "": substantive-флаг или skip выше по конвейеру, текст сознательно не отгружен (D2: заражённый вывод не отгружается;export.go:359-395).PendingUnitsего НЕ считает.ApplyHeadingна пустой текст заголовок не клеит, так что глава молча начнётся со СЛЕДУЮЩЕГО юнита. - incomplete —
DroppedMembers > 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) с константой в полосе
10–19, с доводом; чисел вне 0–5/10–19 не заводить. Молча выбранная форма здесь дороже неверной: её потом
читает платформенная дверь выдачи.
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_statusdraft 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_status57/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-onlyexport/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 и CLIzipна машине нет, но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.goTestExportManifestPending(:186) иTestExportDetectsConfigDrift(:229) на хелперах изrunner_test.go. ⚠ Ghost-посадки в тестах НЕТ (грепGhostRowsпо*_test.goпуст) — строишь сам, отдельной строкой в отчёте. Golden-харнесс (golden_test.go) — гард wire пайплайна, не инструмент для твоего файла;TM_UPDATE_GOLDENне трогать. Каждое состояние обязано отработать так, как ты решил, и это видно в файле или в отказе. Файл реальной книги, её копии и cmp-вывод — рабочее временное в песочнице: в отчёт идут команды и вывод; воспроизводимость на приёмке держит герметичный тест писателя вbackend/на синтетической книге (chunktest.BuildEPUB, образец герметичногоExport—export_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/ — зона
оркестратора»).