textmachine/docs/CHAPTER_STRUCTURE_SESSION_PROMPT.md

47 KiB
Raw Blame History

ПАК «ФОРМАТЫ И ЧЕСТНЫЙ СЛОВАРЬ» — движковая сессия

ВЫДАН 05.09 сессии textmachine-main-fe. Три круга совместного ревью (оркестратор + Fable 5); состав пака и словарь — итог восьми кругов сходимости, тела разборов в D39.202/D39.207.

Зона записи: backend/. Коммитит оркестратор (textmachine-main-27), ты готовишь дерево и передаёшь. Отчёт: секция «Бэкенд» в docs/PROGRESS.md. Денег: $0, ни одного платного вызова.


0. КАРТА ЧТЕНИЯ — ПЯТЬ ПОЗИЦИЙ, ОНА ЗАКОН

Дальше — только по её ссылкам. Голову журнала решений целиком НЕ читать (канон это запрещает).

  1. backend/internal/chunk/ingest.go целиком — предмет пака живёт здесь.
  2. docs/research/27-chapter-detection.md — через его ревью-шапку. Дизайн НЕ ратифицирован; это материал, а не заказ.
  3. ## D39.205 (пропущенный кусок — критическая поломка) и в ## D39.202только п.3-бис (гейт умирает вытеснением) и п.9 (чего не сторожит ничто). Тело ноты целиком не нужно. ⚠ ПОДНОМЕР своего тела не имеетD2.3 живёт пунктом внутри ^## D2.
  4. Строки бэклога 297 (словарь провенанса) и 299 (имя, не разрешающееся ни во что) в docs/PROGRESS.md.
  5. docs/architecture/12-go-style-notes.md — норматив стиля, только больные места.

Код читаешь по мере правки. Несущие файлы, которые ты ТОЧНО будешь править: chunk/ingest.go · pipeline/manifest.go (ключ, тег, title_raw) · chunk/chunker.go (stripHeading, matchHeaderLine) · lang/langpack.go (⚠ parseHeading СЕГОДНЯ ТРЕБУЕТ marker, :677-679 — после «только template» он упадёт, это заказанная правка) · lang/embedded.go.


1. ЧТО УЖЕ ПОСТРОЕНО — НЕ ЗАКАЗЫВАТЬ ЗАНОВО

Самый дорогой раздел: половина ошибок прошлых паков — постройка второго носителя.

  • Провенанс разреза уже есть — поле Document.Structure, и оно спроектировано верно: отвечает на «КАКОЙ ПУТЬ провёл границы» (ingest.go:94-95). Неверна его первая строка (:70), рекламирующая «how much the chapter cut can be trusted», — и именно её списала платформа. Поле не переделывать; чинить словарь и формулировку.
  • Валидатор V2-7 «формат провёл ВСЕ границы» (ingest.go:170-176) — ЗАМЕЩАЕТСЯ конкуренцией (§2.4), а не сохраняется рядом с ней. После конкуренции его предикат (len(parts)>1 && len(chapters)==len(parts), ingest.go:178; ⚠-блок разбора — :172-177) становится мёртвым кодом. Не пытайся сохранить обе логики — это прямо противоречащие друг другу правила.
  • Доминирующий юнит с полом ≥2 для CJK уже есть (detectChapterUnit, ingest.go:249-263) — это мини-индуктор, а не список паттернов. Пол ≥2 — единственный валидатор, живущий в паке 1.
  • Читательские слова цели уже разведены и грузятся СВОИМ загрузчиком по ЯЗЫКУ, вне пар-пака (lang.LoadReaderWords, reader.go:87) — прецедент, по образцу которого делается структура источника.
  • Скрипт языка уже выводится (lang.LangScripts, script.go:67), CJK-дефолт живёт данными.

2. ЧТО ЗАКАЗАНО

2.1 Шаг 0 — ПРИБОР ДО ПРАВОК. Поведение не меняется, $0

Без него пак не имеет права ложиться: под ним подписанный банк владельца (окна в ординалах глав) и боевая книга.

  1. Пин — ПОЛНЫЙ МАНИФЕСТ до правок, а не хеш: id глав + номера (лучше весь JSON). Из хеша не построить карту сдвигов §2.5, а она заказана. Хеш — производное, снимай и его. НО: под books/ не запускай НИЧЕГО. tmctl manifest пишет сайдкар рядом с проектной базой книги (manifestPath() = ProjectDB + ".manifest.json", pipeline/manifest.go:189), то есть ПИШЕТ в каталог книги — а запись там запрещена (§7). Форма: копируешь исходник и book.yaml в собственный каталог сессии (не /tmp — см. §4, не books/) и все замеры и пины делаешь на копии. В копии обязательно перенаправь project_db и source_file. project_db — ЯВНОЕ поле yaml (config/book.go:86), и «рядом с yaml» подставляется, только если оно ПУСТО (:199-201); у стендовых книг там абсолютные пути. Скопируешь как есть — сайдкар уедет обратно под books/ (запись, которая запрещена) или по мёртвому пути. ⚠ А langpack_root / langpack_extend НЕ трогай — они только читаются, и подмена дала бы другой Pack.Version(), то есть другой cutTag: пин снялся бы с другого разреза, и сравнивать было бы нечего. И знай, КУДА они указывают: у большинства стендовых конфигов langpack_root смотрит в СОСЕДНЕЕ дерево (/home/ubuntu/projects/textmachine/backend/configs/langpacks), а деревья различаются — комментарий одного из конфигов прямо говорит, что ru/ есть только там. Поэтому §7 разрешает ЧТЕНИЕ соседнего дерева ровно по тем путям, на которые указывают конфиги стенда. Перенаправлять их нельзя (см. выше), писать туда нельзя.
  2. Синтетические фикстуры по стандартам: EPUB 3 с nav, EPUB 2 с NCX. Сегодня тестовый билдер пишет только container→OPF→spine→xhtml и nav/NCX не пишет вовсе (chunktest/epub.go:42-82).
  3. Оракул — КОД, не замер: EPUB с nav разворачивается в txt, путь разреза обязан восстановить nav-границы. Ground truth без ручной разметки и без единого вызова модели. ⚠ Сам ЗАМЕР — пак 2.
  4. Замеры ДО правок, каждый числом в отчёт: есть ли \f в стенд-книгах · есть ли главы-огрызки от print-TOC · сбрасывается ли нумерация по томам.

Файлы стенда — поимённо (см. §7 о доступе): 蛊真人 (GB18030 txt, ровно 2283 «第N节») · EPUB владельца Кристофф/en · EPUB владельца isekai/ja. ПРО НОСИТЕЛЬ ГЕЙТА, иначе пин повиснет в воздухе: конфигов book.yaml для 蛊真人 в дереве книг ДЕСЯТОК, и ни один не указывает на полную книгу — все на срезы (slice25, ch1-10, ch1-5, ch4-5). Полный guzhenren-gb18030.txt не привязан ни к одному конфигу. ⇒ Гейт §3 = КАЖДЫЙ существующий конфиг 蛊真人 воспроизводит свой разрез байт-в-байт. Пин снимается по каждому, не по «стенду вообще». Полную книгу гоняешь ОТДЕЛЬНО, своим конфигом в своём каталоге; её результат идёт числом в отчёт и гейтом НЕ является — базовой линии для неё не существует. ⚠ Платная книга платформы bk_SS5VES2JELESJSTR живёт в workdir платформы, гейту §3 не видна и в него не входит; её заметки теряются по слову оркестратора.

2.2 Данные структуры — ПО ИСТОЧНИКУ, вне пар-пака

Ревью-вопрос проекта: «заработает ли пара, которой в репо НЕТ, без правки Go?»

  • marker/units/выбор читателя номеров/грамматика шаблона → файл структуры ИСТОЧНИКА, свой загрузчик по Book.SourceLang, по образцу reader.txt.
  • В пар-пак класть НЕЛЬЗЯ: книга без каталога пары идёт с nil-паком (runner.go:397-407).
  • ОДИН ИСТОЧНИК НА ФАКТ — и дрейф здесь ЖИВОЙ, а не гипотетический. Замерено 05.09: cjk-section.txt (embedded, читает ИНГЕСТ) несёт пять юнитов 章节節回話, а zh-ru/heading.txt (пар-пак, читает ЧАНКЕР) — четыре 章节節回; parseHeading кладёт units дословно и на CJKSection не смотрит. ⇒ комментарий ingest.go:184-185 («the SAME data … ONE source, no ingest↔chunker byte-drift») сам протух — класс строки 299, поправить вместе с кодом. ⇒ Пак не предотвращает возврат дрейфа, а ЗАКРЫВАЕТ живой, и инвентарь — РЕШЕНИЕ, а не переезд. Ратифицировано: инвентарь — ПЯТЬ юнитов, источник cjk-section.txt. Довод: пятый (話) добавлен D39.60 §3.1 и на нём стоит пин ja-детекции; ингест уже режет по пяти, отстал именно чанкер, и сведение к пяти НЕ трогает разрез — меняется только stripHeading. ⚠ И это обязано быть ЗАМЕРЕНО шагом 0: встречается ли шапкой в 蛊真人. Встречается — меняются чанки и контент-хеши, и это идёт в ветку (б) гейта §3, а не проходит молча.
  • Инвентарь скрипт-дефолта CJK берётся ИЗ cjk-section.txt, НИКОГДА из пар-файла. У ja каталога пары нет вовсе (configs/langpacks/ — только ru/, zh/, zh-ru/), значит ja-книга идёт по лестнице на скрипт-дефолт. Соберёшь дефолт из опустошаемого zh-ru/heading.txt — потеряешь , и TestChapterUnitJaWebnovel (chunker_heading_test.go:73) покраснеет НЕ заказанным.
  • ОДИН ИСТОЧНИК НА ФАКТ. Ингест И stripHeading чанкера читают тот же самый файл. Сегодня чанкер берёт HeadingRule пары; после разведения heading.txt оставляет ТОЛЬКО template. Оставить marker в обоих местах = вернуть дрейф ingest↔chunker, который pair-14 закрыл («ONE source, no ingest↔chunker byte-drift», ingest.go:181-186).
  • Лестница при отсутствии данных: язык → скрипт → embedded-дефолт ПО СКРИПТУ → честное none. Дефолт НЕЛЬЗЯ класть под internal/lang/data/, хотя прецедент cjk-section.txt туда и манит: EmbeddedVersion() хеширует весь каталог (embedded.go:44-56, ReadDir("data"); «a byte edit of ANY embedded file is a loud --resnapshot»). Положишь туда — нарушишь «не в снапшот» МЕХАНИЗМОМ, тест §4 п.3 покраснеет, и самый соблазнительный «фикс» будет хаком (руками исключить файл из обхода). Клади в ОТДЕЛЬНЫЙ embed-каталог со своей версией (internal/lang/structure/ или как назовёшь).
  • Объём читателей номеров в паке 1 — РОВНО СУЩЕСТВУЮЩИЙ (арабские + CJK). Схема данных пишется под общий случай, но римские/словесные читатели НЕ строятся — их заказывает пак 2, если замер покажет нужду.

ВЕРСИОНИРОВАНИЕ — ТОЧНО, иначе воспроизводится F1 MAJOR (D39.122). Данные структуры решают ГРАНИЦЫ ⇒ обязаны войти в cutInputs (manifest.go:240-255) ⇒ двигают cutTag ⇒ двигают unit-id. manifestKey фолдит компоненты ПОРОЗНЬ (manifest.go:336-354), и одного его НЕДОСТАТОЧНО: правка данных перережет книгу, а id юнитов переживут — ровно тот дефект, ради которого тег и заведён. В снапшот волн — НЕ класть. Тест TestEveryCutInputMovesTheTag (cuttag_test.go:26) подвинется САМ — он идёт по полям рефлексией. ⚠ Ручной список живёт в соседнем TestTheCutTagActuallyCarriesTheEngineData (cuttag_test.go:54-61) — новый компонент туда сам не попадёт, добавляй руками.

ФОРМА компонента — не на твоё усмотрение, она уже задана прецедентом: Structure: <версия парсера> + sha256(байты структурных данных, РЕАЛЬНО загруженных для этой книги — /structure.txt либо дефолт по скрипту). Никогда omitempty — дефолт есть всегда. ⚠ И не копируй тег с Langpack буквально: у него omitempty КАК РАЗ стоит, и в голдене langpack_version отсутствует вовсе. Бери из его формы ПРИНЦИП — «считается одинаково на пишущем и читающем пути», — а не строку тега. Считается ОДИНАКОВО на пишущем и читающем пути (runner.go:384-386: «loaded on BOTH the write path and the read-only path so status/export/report reproduce the identical snapshot»). Это ровно форма Langpack: r.packVersion().

ЧТО НЕ ПЕРЕЕЗЖАЕТ, и это тихая ловушка: числительные (Zero/Digit/Unit/Magnitude) остаются в data/cjk-section.txt. Их читает не только ингест — на них стоят ДЕНЕЖНЫЕ $0-гейты величины чисел (checks/cheapgates.go:601, 636, 649, 666lang.DefaultCJKSection()), а это ВЕРДИКТНЫЕ данные, и им место в снапшоте. Уносишь числительные — молча меняются вердикты гейтов. Переезжают ТОЛЬКО marker / units / грамматика шаблона — и переезжают они из ПАР-ФАЙЛА (zh-ru/heading.txt), а НЕ из cjk-section.txt. internal/lang/data/cjk-section.txt НЕ ПРАВИТСЯ ВООБЩЕ — ни одной строкой. Он уже объявляет себя единственным источником истины для разреза, и чанкер просто начинает читать ЕГО (через lang.DefaultCJKSection()) вместо своей отставшей копии — вот этим дрейф и закрывается. Тронешь файл — сдвинется EmbeddedVersion, а он тянет три вещи, каждая из которых тебя остановит: голден несёт четыре строки embedded_version наравне с chunker_version; TestChapterMarkerIsData (langpack_test.go:454) краснеет НЕ заказанным; и — главное — CURRENT-STATE (docs/PROGRESS.md) держит правило лендинга: правка lang/data/*.txt двигает EmbeddedVersioncutTag, и такой лендинг НЕ кладётся, пока жива книга платного прогона. Оставить там же мёртвые копии ключей тоже нельзя — это класс строки 299.

2.3 EPUB по стандарту

  • Читать nav (EPUB 3) и NCX (EPUB 2); spine — порядок ЧТЕНИЯ, а не оглавление.
  • Учитывать linear и properties — сегодня opfPackage их не разбирает (ingest.go:500-509).

Нормализация nav — правилами, а не на усмотрение (research/27 — материал, не заказ): цели сортируются по позиции в spine · цели вне spine или неразрешимые — игнор со счётом · вложенные ol сплющиваются (каждая цель — граница; иерархия томов — §5) · дубли целей схлопываются · приоритет nav → NCX → spine · nav с нулём разрешимых целей = отсутствует.

ТЕКСТ КНИГИ НЕ ТЕРЯЕТСЯ НИКОГДА (D39.205: пропущенный кусок — критическая поломка):

  • служебные документы — НЕ текст книги и исключаются со счётом, и список ТОЧНЫЙ, потому что наивная форма не сработает: исключаются (1) документ с properties="nav"; (2) документы, чья роль — toc или cover — объявлена в landmarks (EPUB 3) ЛИБО в OPF <guide> через <reference type=…> (EPUB 2). Обе формы обязательны: landmarks — конструкция EPUB 3, у EPUB 2 nav-документа нет вовсе, и сессия, проверившая только landmarks, приклеит toc.xhtml к главе 1 ровно на том «массовом EPUB 2», которым это правило себя и мотивирует. ⚠ properties="cover-image" стоит на ИЗОБРАЖЕНИИ, а не на документе, и среди документов spine не исключит ничего; landmarks — это <nav epub:type="landmarks"> ВНУТРИ nav-документа, в properties их нет. Грепнуть один properties и на этом успокоиться — значит приклеить к главе 1 текстовую страницу оглавления (toc.xhtml в spine — массовый EPUB 2) и титул. Больше НИЧЕГО не исключается;
  • linear="no" НИКОГДА не начинает главу, даже если nav на него указывает (сноски, приложения) — прикрепляется к предыдущему по spine;
  • непокрытые контент-документы ДО первой цели nav (титул, обложка-страница) прикрепляются к ПЕРВОЙ главе — зеркало правила преамбулы txt;
  • остальные непокрытые — к предыдущей главе;
  • каждое прикрепление считается; число в WARN и в отчёт.

Вырожденный nav — ОЖИДАЕМЫЙ исход пака 1, а не твой баг. Скрейп с тремя целями на триста документов даст три главы-кита с ярлыком declared и огромным счётом прикреплений. Словарь честен, разрез — нет; лечение (валидаторы runt/whale, кросс-аудит) — пак 2 (§5). Тревога здесь — СЧЁТ, и это всё, что от пака 1 требуется. Не лезь чинить кита.

2.4 Словарь: четвёртое значение и КОНКУРЕНЦИЯ свидетелей

Словарь: declared · delimited · detected · none.

  • declared — формат утверждает СТРУКТУРУ ГЛАВ (nav/NCX).
  • delimited (новое) — в файле был РАЗДЕЛИТЕЛЬ, но «это глава» — допущение движка. Сюда уходят и EPUB-spine, и \f в txt: spine объявляет порядок чтения, form feed — разрыв СТРАНИЦЫ. ⚠ Реальный источник \f — не человек, а извлечение из PDF (pdftotext ставит его на каждом разрыве страницы по умолчанию).
  • detected — движок вывел границу из СОДЕРЖИМОГО.
  • none — свидетеля нет (и при <2 главах — уже так).

КОНКУРЕНЦИЯ вместо сложения — заказанная СМЕНА ПОВЕДЕНИЯ. Сегодня txt режется по \f, а ПОТОМ каждая часть — по шапкам (ingest.go:159-167), пути СКЛАДЫВАЮТСЯ. Заказано: есть шапки → режем по шапкам, книга detected; шапок нет → режем по \f, книга delimited. Смешанного случая не остаётся по построению, агрегировать нечего, structureOf сводится к «none при <2 главах, иначе свидетель победившего пути».

И ГЛАВНОЕ — ЧТО С БАЙТАМИ ПРОИГНОРИРОВАННОГО \f. text.NormalizeSource form feed не трогает (ingest.go:44-46). Сегодня он до провода не доходит, потому что он и есть символ разреза; после конкуренции проигнорированный \f останется в тексте главы и уедет модели, в контент-хеш и в чекеры. ⇒ игнорируемый \f заменяется на разрыв абзаца (\n\n) в ингесте, до нормализации, и считается.

ПОРЯДОК ОПЕРАЦИЙ — НАЗВАН ЯВНО, потому что наивный даёт ЦИКЛ и МОЛЧАЛИВУЮ ПОТЕРЮ ГЛАВЫ. Решить «есть ли шапки» до замены НЕЛЬЗЯ: перед \f нет \n, поэтому шапка сразу за ним лежит на ОДНОЙ СТРОКЕ с прозой предыдущей главы — её отвергают сразу два предиката (длина строки против chapterHeaderMaxRunes, и якорь ^ регэкспа). Заменить безусловно ДО детекции тоже нельзя — уничтожается разделитель, нужный ветке отката. Форма: (1) \f\n\n на РАБОЧЕЙ КОПИИ; (2) детекция шапок идёт ПО КОПИИ; (3) шапки победили → режем копию, книга detected, форм-фиды сочтены; (4) шапки проиграли → режем ОРИГИНАЛ по \f, книга delimited. ⚠ Существующий тест этого НЕ ловит: он проверяет только doc.Structure, а при наивном порядке ярлык остаётся верным (detected) — глава теряется молча при зелёном тесте. Это ровно тот класс, который пак объявляет своим (D39.205).

Слабость выражается СЧЁТОМ, не классом: form feeds ignored: N, documents attached: M — в WARN и в отчёт, НЕ в манифест: потребителя на проводе у счёта сегодня нет, а поле без потребителя — механизм ради галочки.

Провенанс — свойство ГРАНИЦЫ. Глав N, границ N1; у первой главы открывающей границы нет. ⚠ ПРАВИЛО ПРЕАМБУЛЫ ЗАМОРОЖЕНО: текст до первой шапки сливается в главу 1, и код объясняет зачем — «dense numbering aligns with the section numbers», «第一节» обязано быть главой 1 (splitTextChapters, ingest.go:265-270). Не трогать: правка сдвинет нумерацию ВСЕЙ стенд-книги. ⚠ Следствие, которое надо знать, а не чинить: у главы 1 шапка стоит НЕ в её начале, значит title_raw главы 1 берётся из проглоченной шапки.

2.5 title_raw и карта сдвигов

  • title_raw едет по проводу аддитивно. Определение: строка шапки как в источнике (txt) / navLabel дословно (EPUB). Обоснование — собственное: заголовок, не едущий по проводу, невозможно отрендерить. НЕ ссылаться на «шесть заголовков из семнадцати»: у числа нет носителя (эррата 05.09-е).
  • ПОНИЖЕНИЕ подзаголовка в прозу (chunker.go:155-177) НЕ ТРОГАТЬ. Снимешь — изменятся чанки и модель перестанет переводить подзаголовок. title_raw добавляется РЯДОМ, не вместо.
  • В EPUB-экспорт и в nav title_raw в этом паке НЕ входит — он на языке источника.
  • БАМП chunkerVersion — ОДИН РАЗ, И ЭТО ЗАКАЗАНО ЯВНО. Пак меняет РАЗРЕЗ (EPUB spine→nav, конкуренция \f), а правило ингеста по конвенции покрывается chunkerVersion (text/source.go: «covered by chunkerVersion (a deliberate re-translation, not a silent one)»). Забыть бамп — оставить протухший манифест выглядящим текущим. ⇒ chunker_version уезжает в снапшот, и голден краснеет: testdata/golden/capture.golden несёт ЧЕТЫРЕ строки chunker_version (проверено grep -c). Голден пере-захватывается ОДИН раз, в самом конце, и ТОЛЬКО после того, как ты предъявишь пин: разрез самого голдена не изменился (его фикстура — ja-txt с 第一章, форм-фидов ноль). Объявление по D39.183 обязательно. ⚠ Без этого абзаца ты упёрся бы в красный голден и по §7 обязан был бы остановиться и пинговать — поэтому он здесь: правка ЗАКАЗАНА.
  • Карта сдвигов — МАШИННАЯ (JSON), set-diff по id и номеру: same/moved/gone/new. Прозой нельзя: на прозе не построить мигратор потом. Мигратор в пак НЕ входит — пак производит УЛИКУ, а не ремонт, и это сказано вслух.

3. ГЕЙТ ЛЕНДИНГА

Зелено, если выполнено ОДНО из двух:

  • (а) разрез стенд-книги воспроизводится байт-в-байт против пина §2.1, ЛИБО
  • (б) расхождение перечислено, приложена машинная карта сдвигов, в отчёте ЯВНО названа цена: что именно теряется и у кого, и это подписано владельцем.

Почему не только (а). Чистое «байт-в-байт» защищало бы ОШИБКИ нынешнего разреза: правильный детектор чинит главы-огрызки, а гейт бы его за это не пустил. Это девятая форма ложной зелени — гейт, ЗАЩИЩАЮЩИЙ дефект (D39.202). ⚠ Такого детектора в паке 1 нет (он в §5, пак 2) — довод объясняет ФОРМУ гейта на будущее, а не заказывает тебе детектор.

Ветка (б) честна ровно наполовину, и это записано сознательно: мигратора окон банка нет, и пак его не строит. Сдвиг окна = новый TermID = каскад на решения владельца в платформе. Поэтому (б) обязывает НАЗВАТЬ потерю, а не починить.

Поток подписи — не твой. Ты сдаёшь карту и названную цену в отчёте; оркестратор несёт владельцу; лендинг — после его слова. Не жди сам и не «подписывай» сам.

ГЕЙТ ПРИМЕНЯЕТСЯ ТОЛЬКО К 蛊真人 — под ней подписанный банк владельца (окна в ординалах глав). EPUB владельца под гейт НЕ попадают: их разрез ОБЯЗАН измениться (spine → nav), это и есть заказ пака. Для них ветка (а) провалена по построению, и требовать под них подпись владельца бессмысленно. Они — прибор §2.1 п.3: их новый разрез ИЗМЕРЯЕТСЯ и идёт в отчёт числом, а не гейтится.

Ожидаемый исход для 蛊真人 — ветка (а). В паке 1 её разрез может сдвинуть только \f-конкуренция. Ветка (б) существует на случай, если \f там окажется.


4. САМОПРОВЕРКА — ИСПОЛНЕНИЕМ, И ВЕЕР НАПРАВЛЕН

Самоотчёт «проверено» без исполнения регулярно оказывается ложным. После кода — адверсариальный проход по СВОЕЙ готовой работе. Глубину и веер выбираешь ты; направление — вот что уязвимо здесь:

  1. Резка xhtml по якорям nav (несколько глав в одном файле) — самое уязвимое место пака. У extractXHTML не меньше четырёх пинов байт-паритета (ingest_test.go:323, 359, 423, 467: void-теги, несбалансированный ruby, raw-text-элементы, голый амперсанд). Ошибка здесь ТИХАЯ, и ломает она именно их.
  2. Загрузчик структуры обязан достигать книги БЕЗ пар-пака — тест на книгу без langpack_root.
  3. Данные структуры реально двигают cutTag (и через него unit-id) и в payload manifestKey (manifest.go:336-354), и НЕ двигают снапшот волн — тест на каждое из трёх.
  4. Байты игнорируемого \f — тест, что в тексте главы его не остаётся.
  5. ЧИСЛО ГЛАВ, а не только ярлык, на фикстуре «шапка сразу за \f» (ingest_test.go:74): сегодня тест смотрит только doc.Structure, и потерянная глава проходит зелёной. Тест на СЧЁТ.
  6. Выравнивание title_raw с ПЛОТНОЙ нумерацией — тест с ПУСТОЙ главой между двумя непустыми: пустая глава номер не потребляет, а title_raw поедет параллельным срезом, и промах на одну позицию даст заголовок главы N на главе N+1 молча. Тот же класс уже чинили для ruby (ingest.go:568-605, denseNo).
  7. Именованный артефакт: посади дефект в разбор nav так, чтобы одна глава потерялась молча, и предъяви имя покрасневшего теста. Не краснеет ничто — это находка, а не повод двигаться.

Каталог мутаций (backend/cmd/tmmutate): -root обязан быть КОПИЕЙ; копия вне git ⇒ GOFLAGS=-buildvcs=false. Не работай под /tmp: посторонний /tmp/.git ломает go build всему, что там собирается, и уже дал этой смене ложную зелень.


5. ГРАНИЦЫ — ЧЕГО В ЭТОМ ПАКЕ НЕТ

Названо, чтобы не открылось по ходу и не было сделано «по здравому смыслу»:

  • Лексиконы не-CJK, корпус, ЗАМЕР оракулом, сбор файлов, сеть — пак 2. Пак 1 ничего не собирает.
  • Структурные валидаторы (монотонность номеров, коридор hits/объём, headSpread, runt/whale) — ПАК 2. В паке 1 живёт только уже построенный пол ≥2. Причина не стилистическая: у стенд-книги нумерация СБРАСЫВАЕТСЯ по томам (eval/dovodka/narezka.py:18), и валидатор монотонности отверг бы или перекроил её шапки — то есть уронил бы гейт §3 на верной книге.
  • Индуктор скелетов строк — пак 2, и только если замер покажет нужду.
  • Мигратор окон банка, chunk_status, unit_resolutions — не здесь (строка 298, D15.2).
  • Идентичность по ИСХОДУ (cutTag как дайджест нарезки) — свой пак. Здесь только сравнение манифестов до/после в приборе шага 0.
  • Публикация провенанса по главе, kind, вердикт качества, иерархия томов, print-TOC-линковка, split/merged в карте — не строить. Потребителя нет, фронт заморожен.
  • FB2 — только если владелец скажет, что у беты есть FB2-источники.
  • Платформенное: предикат «двух свидетелей» и structure_version · снятие временного правила по declared (строка 297) · глагол «перечитать журнал» — всё это НЕ твоя зона. Не добавляй полей «для платформы».
  • Рендер при сбросе нумерации по томам — ВОПРОС ВЛАДЕЛЬЦА, не чинить. ⚠ Прежняя редакция обещала, что ты увидишь это «в nav платного EPUB» — неверно и снято: платная книга платформы собрана из десятиглавого txt-среза, nav там нет вовсе, и сброса по томам в главах 110 быть не может. Увидеть это можно только на ПОЛНОЙ 蛊真人, где нумерация и сбрасывается. Это выбор рендера (номер книги против плотного ординала), он вне пака. Увидел — запиши числом в отчёт и иди дальше.
  • narezka.py полигона (второй приватный zh-детектор) — пинг полигону от оркестратора, не твоё.
  • Сквозного прогона не-CJK НЕ БУДЕТ и не заказывается: промтов пары для en/ja не существует (backend/prompts/ — только zh-ru). Не-CJK путь проверяется только на уровне ingest/manifest, $0. Не пытайся «доказать бету» — не сможешь, и это не твой дефект.

6. ОБЯЗАТЕЛЬСТВО, КОТОРОЕ ПАК ДАЁТ ПЛАТФОРМЕ

Пак НЕ бампает manifestVersion. Значения и поля аддитивны; платформа игнорирует неизвестные поля и деградирует на неизвестных значениях. Бамп понадобился бы, только если читатель прочтёт документ как текущий и НЕВЕРНО: единица без плотного номера или с дубликатом · тотал, не равный списку · пустой id · смена ФОРМЫ id.

Граница обязательства: каждая единица манифеста нумерована ПЛОТНО и 1-based. «Фрагмент» — атрибут нумерованной единицы, а не отсутствие номера. ⚠ Контракт при этом обещает number: null как законный — и шов его отвергает; противоречие записано эрратой 05.09 в openapi.yaml. Снимать его — не в этом паке.

Стыковка с платформой — предусловие ЛЕНДИНГА и забота ОРКЕСТРАТОРА, не твоя (platform/ — чужая зона). От тебя нужно одно: сдать в отчёте манифест-фикстуру со значением delimited, чтобы платформенный тест мог на ней проверить деградацию в «знаки».


7. ДИСЦИПЛИНА

  • Доступ к книгам. books/ — симлинк в соседнее дерево. ЧИТАТЬ файлы под books/ РАЗРЕШЕНО и необходимо (§2.1 без этого неисполним). ЗАПРЕЩЕНО: любая запись под books/, любые git-операции там, и любая ЗАПИСЬ в /home/ubuntu/projects/textmachine. ⚠ ЧТЕНИЕ соседнего дерева разрешено там, куда указывают конфиги стенда — прежде всего backend/configs/langpacks (langpack_root): без этого §2.1 неисполним, потому что пин обязан сниматься ровно с тем пар-паком, с которым книга резалась.
  • Тесты и гейты не подгонять под зелень. ⚠ Правка, вызванная заказанной сменой поведения, — обслуживание. Заказанные здесь правки тестов, поимённо: · priceprojection_test.go:144 — кейс «a form feed is the format speaking» ждёт StructureDeclared; станет delimited; · тест V2-7 «формат провёл ВСЕ границы» — меняются ДВА кейса, не один: «один случайный \f среди шапок» (шапки побеждают, книга detected) и ПЕРВЫЙ кейс ingest_test.go:68 «the format drew every boundary» (ГЛАВА A\fГЛАВА BStructureDeclared), который становится delimited. ⚠ Уточнение, чтобы ты не искал несуществующего: ассерт переезжает РОВНО ОДИН — тест сверяет только doc.Structure; у кейса с одним \f среди шапок ожидание то же и до, и после, у него меняются объясняющий комментарий и ЧИСЛО глав (§4 п.5). ⚠ А у кейса ingest_test.go:74 ожидание НЕ меняется (detected и до, и после) — у него меняется ЧИСЛО ГЛАВ, которого никто не проверяет: см. §4 п.5. И само имя TestTXTStructureIsDeclaredOnlyWhenTheFormatDrewEVERYBoundary после конкуренции ни во что не разрешается — переименовать тем же актом (класс строки 299, который этот же промт требует ниже для комментариев); · ingest_encoding_test.go:218-227 (GB18030, 第一章…\f第二章…) — счёт глав сохранит (две шапки побеждают), но его комментарий про разрез по \f станет ложью — поправить текст комментария, это класс строки 299; · ⚠ TestIngestTXTFormFeedChapters (ingest_test.go:34-46) проверяет разрез A\fB\fC, где шапок нет — при конкуренции НЕ меняется, меняется только ярлык на delimited. Не «чини» его. Каждая такая правка ОБЪЯВЛЯЕТСЯ в отчёте: что изменилось в поведении, какой тест это описывал, куда уехала гарантия. Не заказано и не ратифицировано — пинг, не правка.
  • Комментарии, описывающие сменённое поведение, — тоже носители. Правь их вместе с кодом; в частности протухает bookfile/epub.go:15-19 («ридер делает главу из КАЖДОГО документа spine»). Это класс строки 299 — имя или утверждение, которое больше ни во что не разрешается.
  • Не хакать вокруг ошибки. Нашёл дефект в чужой зоне — опиши, не чини.
  • Никогда не читать .env. PUML не рендерить. Не коммитить. Не двигать HEAD. Не трогать чужое незакоммиченное. ⚠ Якорный линт доков (docs/scripts/counts.py --lint) — гейт ОРКЕСТРАТОРА; доки с якорями ты не пишешь, но знай, что он есть, и не строй второй такой.
  • Утверждение о замере — с КОМАНДОЙ (D39.207 п.5): пишешь «проверено» или «не проверено, потому что X» — рядом стоит команда, которой это показано.
  • Идентификатор, не виденный ЦЕЛИКОМ в выводе команды, не воспроизводить по памяти — ни хеш, ни номер строки. Обрезающий cut/head молча превращает улику в догадку (research/29 §5.6: так был сфабрикован sha256, совпавший на 52 знака из 64).

8. ЧТО ПРИСЛАТЬ И В КАКОМ ПОРЯДКЕ

  1. Эхо старта: своими словами — что понял предметом и что считаешь границей. До кода.
  2. Записка-план в отчёт: что делаешь, в каком порядке, и чем сдача осмысленна, если тебя оборвут на середине. До кода.
  3. Замеры шага 0 — числами, до правок.
  4. Код.
  5. Отчёт: батарея (команда, скипы числом, EXIT) · каталог · посадка именованного артефакта (file:line · что подменено · какое ИМЯ теста покраснело) · объявления по D39.183 · счета form feeds ignored / documents attached · манифест-фикстура с delimited · что НЕ проверено и почему.

Споткнулся о невыполнимое, о противоречие в самом задании или о нехватку доступа — пиши сразу, не обходи молча. Обход тихо становится нормой, и следующая смена наследует его как факт.