diff --git a/docs/research/27-chapter-detection.md b/docs/research/27-chapter-detection.md index 2ed7b813..1e32356c 100644 --- a/docs/research/27-chapter-detection.md +++ b/docs/research/27-chapter-detection.md @@ -1,82 +1,89 @@ -# 27. Определение глав в загружаемых книгах: стандарты · практика · алгоритмы +# 27. Определение глав в загружаемых книгах: стандарты · практика · алгоритмы · дизайн -**Оркестратор №15, 09.08.2026.** Веб-ресёрч по заказу владельца 09.08 (контекст: фактограф приёмки пака блокеров показал, что ingest-слой границ глав захардкожен под CJK-форму, выведенную из одной стенд-книги, — не-CJK txt молча становится одной главой, epub режется по spine с игнором оглавления, print-TOC порождает главы-огрызки). Три веб-агента: стандарты форматов · практика инструментов · алгоритмы; каждый факт с URL. **Статус: ФАКТУРА СОБРАНА; дизайн-решение владельцем НЕ принято — этот док не ратифицирует стройку.** Носители работ — строки бэклога (заводятся лендингом пака блокеров). +**Оркестратор №15, 09.08.2026.** Заказ владельца после находки: ingest-слой границ глав захардкожен под CJK-форму, выведенную из одной стенд-книги (не-CJK txt молча становится одной главой; epub режется по spine с игнором оглавления; print-TOC порождает главы-огрызки). Источники: веб-ресёрч с URL-грунтовкой (§1–§2) · панель двух независимых моделей (§5) · карта по боевому коду (§6). **Статус: фактура и дизайн-синтез готовы; дизайн владельцем НЕ ратифицирован — стройка не санкционирована.** Носители работ — строки бэклога 160–162; жизненный цикл дерева (§3а) — верхнеуровневое решение владельца 09.08, детали обстучать при реализации. -## §1. Ответы на вопросы владельца +## §1. Три ключевых ответа -1. **«Есть ли алгоритмы, умеющие парсить главы, или всё эвристика?»** Надёжного не-эвристического алгоритма для голого txt НЕ существует, и это доказано числами: Chapter Captor (EMNLP 2020, 9126 романов Gutenberg) — распознавание ЗАГОЛОВКОВ гибридом regex+BERT даёт F1=0.77, а предсказание границ БЕЗ заголовков (семантическая сегментация) — F1=0.453 [arxiv.org/abs/2011.04163]. Главы — авторская разметка, а не тематический сдвиг; TextTiling/C99/BERT-сегментация спроектированы под другое и как основа непригодны. Все зрелые инструменты (Calibre, chapterize, kaf-cli, AozoraEpub3) — каскад «статистика формата строк → языковые паттерны → структурные fallback'и → пороги правдоподобия». -2. **«У epub по стандарту оглавления нет?»** ЕСТЬ И ОБЯЗАТЕЛЬНО. EPUB 3: публикация «MUST contain an EPUB navigation document» с toc nav — иерархия ol/li/a с заголовками и ссылками файл#якорь [w3.org/TR/epub-33/#sec-nav-toc]. EPUB 2: обязательный NCX (navMap/navPoint, вложенность томов/глав, navLabel+ с xml:lang — единственный стандартный носитель МУЛЬТИЯЗЫЧНЫХ названий глав) [idpf.org/epub/20/spec/OPF_2.0.1_draft.htm]. FB2 (жив, критичен для ru-рынка): дерево глав = сама разметка body/section с рекурсией и title [github.com/gribuser/fb2]. То есть для epub/fb2 границы И НАЗВАНИЯ глав достаются из стандарта вообще без эвристик — наш текущий разрез «по spine с игнором nav» выбрасывает гарантированную спекой структуру. -3. **«Что с печатным оглавлением?»** Отдельное устоявшееся направление (ICDAR Book Structure Extraction 2009–2013): детект TOC-блока → парсинг записей → линковка в тело матчем заголовков. Print-TOC — не помеха, а бесплатный источник границ и КАНОНИЧЕСКИХ названий; TOC-гейты дёшевы (chapterize: кандидаты ближе 4 строк = блок оглавления). +1. **Не-эвристического алгоритма для голого txt не существует, и это доказано числами:** Chapter Captor (EMNLP 2020, 9126 романов Gutenberg) — распознавание ЗАГОЛОВКОВ гибридом regex+BERT даёт F1=0.77, а предсказание границ БЕЗ заголовков (семантическая сегментация) — F1=0.453 [arxiv.org/abs/2011.04163]. Главы — авторская разметка, а не тематический сдвиг. Все зрелые инструменты (Calibre, chapterize, kaf-cli, AozoraEpub3) — каскад «статистика формата строк → языковые паттерны → пороги правдоподобия». +2. **У epub оглавление есть по стандарту и обязательно.** EPUB 3: публикация «MUST contain an EPUB navigation document» с toc nav — иерархия заголовков + ссылки файл#якорь [w3.org/TR/epub-33/#sec-nav-toc]. EPUB 2: обязательный NCX (вложенность томов/глав; navLabel+ с xml:lang — единственный стандартный носитель МУЛЬТИЯЗЫЧНЫХ названий) [idpf.org/epub/20/spec/OPF_2.0.1_draft.htm]. FB2 (жив, критичен для ru-рынка): дерево глав = сама разметка body/section+title [github.com/gribuser/fb2]. Для epub/fb2 границы И названия достаются из стандарта без эвристик — текущий разрез «по spine с игнором nav» выбрасывает гарантированную структуру. +3. **Print-TOC — не помеха, а источник.** Устоявшееся направление (ICDAR Book Structure Extraction 2009–2013): детект TOC-блока → парсинг → линковка в тело матчем заголовков — границы плюс канонические названия даром; TOC-гейты дёшевы (кандидаты ближе 4 строк = блок оглавления). ## §2. Несущие факты по слоям -**EPUB (уточнения сверх §1):** резать по spine неверно ПО СТАНДАРТУ — якоря nav легально ведут внутрь файла (несколько глав в одном xhtml) и глава легально живёт в нескольких файлах; правильный алгоритм: поток = spine linear="yes", границы = якоря nav/NCX, спроецированные на поток. Порядку/полноте nav верить нельзя (с EPUB 3.3 порядок — SHOULD, полнота не требовалась никогда) — нормализация: сортировка целей по позиции в spine, непокрытые документы прикреплять к предыдущей главе. Служебные страницы фильтруются каскадом штатных сигналов: manifest properties (nav, cover-image) → spine linear="no" → landmarks (epub:type обязателен: cover/titlepage/toc/bodymatter) → epub:type/DPUB-ARIA в контенте (добровольный — сигнал, не гарантия). Однофайловый epub и битый nav → фолбэк на txt-эвристики. +**EPUB:** резать по spine неверно ПО СТАНДАРТУ — якоря nav легально ведут внутрь файла (несколько глав в одном xhtml), глава легально живёт в нескольких файлах; правильный алгоритм: поток = spine linear="yes", границы = якоря nav/NCX, спроецированные на поток. Порядку/полноте nav верить нельзя (с EPUB 3.3 порядок — SHOULD, полнота не требовалась никогда) — нормализация: сортировка целей по позиции в spine, непокрытые документы прикреплять к предыдущей главе. Служебные страницы фильтруются каскадом штатных сигналов: manifest properties (nav, cover-image) → spine linear="no" → landmarks (epub:type обязателен) → epub:type/DPUB-ARIA в контенте (добровольный — сигнал, не гарантия). Однофайловый epub и битый nav → фолбэк на txt-эвристики. -**TXT — состояние искусства (Calibre как эталон):** (1) статистический детект формата абзацев (line histogram, доли пустых/отступных строк); (2) списки словесных паттернов — у Calibre только en+de в коде(!), CJK-миры держат свои инструменты: kaf-cli zh «第.{1,8}章» + том «第…[卷部]», AozoraEpub3 ja — богатый список 話/章/篇/部/節/幕/プロローグ/序章/終章/間章; (3) пороги правдоподобия: схема принимается только при min_chapters=ceil(wordcount/7000) ≤ hits < 150, заголовок ≤35 симв. (kaf-cli), ≥3 глав (chapterize); (4) сцены «* * *» — отдельный класс, не главы; (5) конвенция Gutenberg: 4 пустые строки перед главой — пустострочные раны как вторичный сигнал [pgdp.net DP Formatting Guidelines; W3C WCAG T3]. **Готовой мультиязычной библиотеки паттернов-как-данных не существует** — собирать свою в langpack из проверенных списков этих инструментов (сид: kaf-cli+AozoraEpub3+Calibre+chapterize; regex-набор Chapter Captor — сид для en); прецедент конфиг-схемы — ParserConfig oomol-lab (chapter_patterns/volume_patterns/section_patterns). +**TXT (Calibre как эталон зрелости):** (1) статистический детект формата абзацев; (2) списки словесных паттернов — у Calibre только en+de(!), CJK-миры держат свои инструменты: kaf-cli zh «第.{1,8}章» + том «第…[卷部]», AozoraEpub3 ja — 話/章/篇/部/節/幕/プロローグ/序章/終章/間章; (3) пороги правдоподобия: min_chapters=ceil(wordcount/7000) ≤ hits < 150, заголовок ≤35 симв., ≥3 глав; (4) сцены «* * *» — отдельный класс, не главы; (5) конвенция Gutenberg «4 пустые строки перед главой» — пустострочные раны как вторичный сигнал [W3C WCAG T3]. **Готовой мультиязычной библиотеки паттернов-как-данных не существует** — собирать свою в структурный пак из проверенных списков (сид: kaf-cli + AozoraEpub3 + Calibre + chapterize; regex-набор Chapter Captor — сид en); прецедент конфиг-схемы — ParserConfig oomol-lab (chapter/volume/section_patterns). -**Иерархия:** том/часть над главой — отдельный уровень паттернов у всех зрелых инструментов; плоский список ломается на «Том 2, Глава 1» (нумерация перезапускается) — в модель структуры нужен уровень volume/part, иначе якоря и банк едут на многотомниках. +**Иерархия:** том/часть над главой — отдельный уровень паттернов у всех зрелых инструментов; плоский список ломается на «Том 2, Глава 1» (нумерация перезапускается) — в модели структуры нужен уровень volume/part. -**LLM-место:** полная LLM-разметка книги (LumberChunker, EMNLP 2024) — дороже/медленнее по признанию авторов, цена линейна от объёма, недетерминизм ломает снапшоты — ОТВЕРГНУТЬ как основной путь. Доказанный в проде паттерн (LILAC FSE'24, лог-парсинг): **LLM один раз ВЫВОДИТ шаблон, дальше режет детерминированный код** — шаблонов на порядки меньше экземпляров; перенос на книги естественен (один дешёвый вызов на семплах строк-кандидатов → regex книги → в снапшот → перепрогоны $0 и воспроизводимы). Для книг такой паттерн не опубликован — будем первыми. +**Место LLM:** полная LLM-разметка книги (LumberChunker, EMNLP 2024) — дороже/медленнее по признанию авторов, цена линейна от объёма, недетерминизм ломает снапшоты — как основной путь отвергнута. Доказанный в проде паттерн (LILAC, FSE'24, лог-парсинг): **LLM один раз ВЫВОДИТ шаблон, дальше режет детерминированный код**; перенос на книги естественен (вызов на семплах строк-кандидатов → шаблон книги → в снапшот → перепрогоны $0). Для книг такой паттерн не опубликован. -## §3. Рекомендуемый класс решения (предложение оркестратора, НЕ ратифицировано) +## §3. Рекомендуемый каскад (предложение оркестратора, НЕ ратифицировано) -Каскад по цене источника, LLM никогда не режет сама: -1. **Структурные источники ($0, без эвристик):** epub nav → NCX → fb2 section/title; скрейп-epub (WebToEpub/FanFicFare и т.п.) — границы точны по построению, доверять. Названия глав на языке оригинала — из тех же источников (закрывает и вопрос heading/К-3). -2. **TXT:** детект print-TOC блока (плотность кандидатов) → использовать его как источник имён и сверки; языко-НЕзависимый статистический скорер строки-заголовка (короткая · изолирована пустыми строками · монотонная нумерация · повторяющийся префикс-шаблон · равномерность по файлу · коридор hits/wordcount) + словари маркеров per-language как ДАННЫЕ langpack; несколько конкурирующих схем-кандидатов, выбор по скору. -3. **Fallback:** один дешёвый LLM-вызов выводит шаблон книги, шаблон — артефакт снапшота (LILAC-паттерн). +LLM никогда не режет сама; по цене источника: +1. **Структурные источники ($0, без эвристик):** epub nav → NCX → fb2 section/title; скрейп-epub (WebToEpub/FanFicFare) — границы точны по построению, доверять. Названия на языке оригинала — оттуда же. +2. **TXT:** print-TOC-блок → исключить из границ + оракул имён; языко-независимый индуктор (кластеризация скелетов строк + скоринг: изоляция · монотонная нумерация · коридор hits/wordcount) + словари маркеров per-language как ДАННЫЕ; конкурирующие схемы-кандидаты. +3. **Fallback:** один дешёвый LLM-вызов выводит шаблон книги; шаблон — артефакт снапшота. 4. **Всегда:** громкие WARN вместо молчаливой деградации («1 глава на 20 МБ», «скачок нумерации», «глава-огрызок»). -**Требование к дизайну (урок владельца 09.08):** модель «что бывает заголовком» выводится из КОРПУСА разнообразных книг (языки·авторы·форматы), не из стенд-книги; приёмка детекта — прогоном по корпусу с замером точности границ (полигон). Схема данных проектируется от общего кейса, а не наращиванием заплаток к CJK-форме (история вопроса: пак-2 завёл CJK-сплит до канона общности, фазы общности вынесли маркеры в данные, но форма шаблона осталась в Go и слой выпал из карты общности — D39.64 честно не включал его в критерий). +**Два жёстких требования к дизайну (владелец, 09.08):** модель «что бывает заголовком» выводится из КОРПУСА разнообразных книг, не из стенд-книги, и приёмка детекта — прогоном по корпусу (полигон); схема данных проектируется от общего кейса, а не наращиванием заплаток (исторически слой выпал из карты общности: маркеры ушли в данные, форма шаблона осталась в Go). **Цена итераций:** правки детекта двигают нарезку → реснапшот-класс на прогнанных книгах — строить один раз, ДО открытия интейка книг, пока платная книга одна. -**Цена итераций:** правки детекта двигают нарезку → снапшот → реснапшот-класс на прогнанных книгах. Проектировать один раз от корпуса и строить ДО открытия интейка книг пользователям; сейчас единственная платная книга — стенд, окно дешёвое. +### §3а. Жизненный цикл дерева глав (решение владельца 09.08, верхнеуровнево; детали обстучать при реализации) + +1. **Парсинг (загрузка):** дерево показывается сразу — черновая структура с пометкой «может быть неточно», оригинальные названия + служебный рендер «Глава N» ($0, локаль интерфейса). ⚠ Открытая продуктовая развилка: владелец предлагал давать «примерный перевод названий» уже на этой стадии — это платный вызов ДО старта прогона (деньги начинаются с загрузки, а не с явного запуска); рекомендация оркестратора — перевод со старта прогона (п.2), решение при ратификации дизайна. +2. **Старт прогона:** мини-сессия названий (§5а; требует роли title — Этап 1) даёт провизорный перевод дерева в первые секунды прогона. +3. **Черновая волна:** структура ЖИВАЯ — in-band метки черновика (Этап 2; до его постройки стадия пропускается) корректируют её по ходу: число глав может измениться против эвристики — недоразмеченное добавится, ложное удалится. Freeze на этой стадии относится к НАРЕЗКЕ/деньгам, не к виду: якорей читателей ещё нет, вид-плоскость легально ревизуется. +4. **Подпись банка (майнинг-стоп):** перегенерация дерева — ревизия структуры по накопленным меткам + адресная ревизия названий по подписанному банку (§5а фаза 2). С этой точки структура затвердевает, дерево показывает поглавный прогресс перевода. ## §4. Что НЕ брать -Тематическая сегментация как основа (F1=0.453) — максимум tie-breaker · полная LLM-разметка (цена/недетерминизм) · вебновелльные скрейперы как образец детекта (у них глава = страница сайта, задачи нет) · универсальный код без языковых данных (ни один инструмент так не решил). +Тематическая сегментация как основа (F1=0.453) — максимум tie-breaker · полная LLM-разметка (цена/недетерминизм) · вебновелльные скрейперы как образец детекта (у них глава = страница сайта) · универсальный код без языковых данных (ни один инструмент так не решил). -## §5. Панель двух независимых дизайнеров (Fable-5 · Opus-5, 09.08) — конвергенция +## §5. Панель двух независимых дизайнеров (Fable-5 · Opus-5) — конвергенция -По заказу владельца обе модели независимо проектировали алгоритм с нуля: одинаковый промт без наводок (только задача и свойства движка), запрет на чтение репо (этот док им был недоступен), веб разрешён для фактов. Полные дизайны — журнал wf_640b4366-b81. **Сошлись на ~13 несущих решениях**, и все совместимы с §1–§3: +Обе модели проектировали алгоритм с нуля по одинаковому промту без наводок (только задача и свойства движка; репо и этот док им были недоступны; веб — только для фактов). **Сошлись на ~13 несущих решениях**, все совместимы с §1–§3: 1. Один общий движок + формато-адаптеры → единое IR (поток блоков с атрибутами); языковое — ТОЛЬКО данными (грамматика шаблонов `{marker}{numeral}{sep}{name}`, читатели числительных, лексиконы, ВСЕ пороги). -2. **Индукция шаблона вместо построчных порогов**: кандидаты кластеризуются по скелету шаблона, оценивается КЛАСТЕР (доминирующее семейство + возрастающий ряд номеров); одиночное «Глава» в диалоге не примыкает к кластеру из 300 — отсекается по построению. -3. EPUB: nav/NCX приоритетно с валидацией; FB2: секции с проверкой вырожденности; TXT: индуктор. Print-TOC: детект плотного региона → исключить из границ + использовать как ОРАКУЛ названий/количества (LCS-выравнивание с телом — сильнейший валидатор). -4. **Кросс-аудит всегда**: текстовый индуктор гоняется и на EPUB/FB2 как независимая $0-гипотеза против деклараций формата (битые TOC скрейпов — массовый случай); Opus: правило маржи между гипотезами, неоднозначность = первоклассный громкий исход с выбором пользователя. -5. Скоринг-вектор структуры: правдоподобие количества (коридор от объёма) · распределение длин (runt/whale) · монотонность нумерации · согласие источников · доминантность шаблона · headSpread (границы сгрудились в начале = TOC). Три вердикта OK / DEGRADED-с-badge / FAILED. -6. **Честный FAILED**: псевдо-главы не выдумываются никогда; дерево показывает «структура не распознана» + технические «Фрагменты» (визуально НЕ главы, серые, отдельная нумерация); перевод НЕ блокируется. Выходы: выбор из отвергнутых кластеров одним кликом · ручной редактор структуры (правки = слой в снапшоте) · opt-in LLM-классификатор кандидатов со сметой. -7. **Инвариант сохранения символов**: Σ длин глав+служебного == длине тела, точно; нарушение = баг движка, платные вызовы не стартуют. -8. **Развязка денег и структуры** (оба, независимо — несущее): оплата/память ключуются контент-хешами СЕГМЕНТОВ (абзац/группа), глава — группировка-вид; пере-нарезка = перегруппировка оплаченного, перекупка только сегментов, разрезанных новой границей. Structure freeze после старта перевода; ревизия — только явной миграцией с отчётом «что осиротеет, что перекупится, почём» ДО применения. -9. Якоря читателя — на хешах абзацев (переживают смену границ по построению); id главы — контент-производный (Opus: хеш первого окна тела+титула — правка в глубине главы id не двигает); миграционная карта same/moved/split/merged/gone/new, осиротевшее показывается, не выбрасывается. -10. LLM в детекте — только opt-in классификатор ПОВЕРХ намайненных кандидатов, ограниченные токены, результат замораживается в снапшоте (replay, не повторный вызов); свободное «LLM режет книгу» — запрещено обоими. -11. Названия: извлечение по лестнице источников с провенансом; отсутствующее — служебная метка `generated:true` на локали интерфейса, НЕ фабрикация; **номер не переводится, а перерендеривается** ($0, «第十二章»→«Глава 12»); перевод названий — ранний дешёвый батч (единицы тысяч токенов на книгу) как обычные сегменты с ролью title, провизорные до стабилизации глоссария, адресная ревизия диффом по термам; канон названия сверяется $0-гейтом с заголовком в переведённом теле. -12. Калибровочный корпус из УРОДЛИВЫХ реальных книг (скрейпы/самиздат/конверсии, все скрипты; хеши+смещения границ, не тексты) + CI-запрет регрессий + запрет подгонки порогов без добавления книги в корпус — оба дословно воспроизвели наш урок «валидировать на корпусе». +2. **Индукция шаблона вместо построчных порогов**: кандидаты кластеризуются по скелету, оценивается КЛАСТЕР (доминирующее семейство + возрастающий ряд номеров); одиночное «Глава» в диалоге не примыкает к кластеру из 300 — отсекается по построению. +3. EPUB: nav/NCX приоритетно с валидацией; FB2: секции с проверкой вырожденности; TXT: индуктор. Print-TOC: исключить из границ + оракул названий/количества (LCS-выравнивание с телом — сильнейший валидатор). +4. **Кросс-аудит всегда**: текстовый индуктор гоняется и на EPUB/FB2 как независимая $0-гипотеза против деклараций формата (битые TOC скрейпов — массовый случай); правило маржи между гипотезами; неоднозначность = первоклассный громкий исход с выбором пользователя. +5. Скоринг-вектор: правдоподобие количества · распределение длин (runt/whale) · монотонность нумерации · согласие источников · доминантность шаблона · headSpread (границы сгрудились в начале = TOC). Три вердикта: OK / DEGRADED-с-badge / FAILED. +6. **Честный FAILED**: псевдо-главы не выдумываются никогда; дерево показывает «структура не распознана» + технические «Фрагменты» (визуально НЕ главы); перевод НЕ блокируется. Выходы: выбор из отвергнутых кластеров одним кликом · ручной редактор структуры (правки = отдельный слой) · opt-in LLM-классификатор кандидатов со сметой. +7. **Инвариант сохранения символов**: Σ длин глав + служебного == длине тела, точно; нарушение = баг движка, платные вызовы не стартуют. +8. **Развязка денег и структуры** (несущее, оба независимо): оплата/память ключуются контент-хешами СЕГМЕНТОВ, глава — группировка-вид; пере-нарезка = перегруппировка оплаченного. Structure freeze после старта перевода (нарезка); ревизия — только явной миграцией с отчётом «что осиротеет, что перекупится, почём» ДО применения. +9. Якоря читателя — на хешах абзацев; id главы — контент-производный (хеш первого окна тела+титула — правка в глубине главы id не двигает); миграционная карта same/moved/split/merged/gone/new, осиротевшее показывается, не выбрасывается. +10. LLM в детекте — только opt-in классификатор ПОВЕРХ намайненных кандидатов, результат замораживается в снапшоте (replay, не повторный вызов). +11. Названия: лестница источников с провенансом; отсутствующее — служебная метка `generated:true` (локаль интерфейса), НЕ фабрикация; **номер не переводится, а перерендеривается** ($0); перевод имён — батч-роль title (§5а); канон названия сверяется $0-гейтом с заголовком в переведённом теле. +12. Калибровочный корпус из УРОДЛИВЫХ реальных книг (скрейпы/самиздат/конверсии, все скрипты; хеши+смещения, не тексты) + CI-запрет регрессий + запрет подгонки порогов без добавления книги в корпус. 13. Детерминизм механики: целочисленные баллы, сортированные обходы, фиксированные тай-брейки, побайтовый golden на structure.json. -**Расхождения — только в глубине**, не в направлении: Opus типизировал провалы (F1–F7, отдельно «гибрид: половина книги размечена» и «nav и NCX согласны, но оба неверны — agreement ≠ правота»), добавил детект бойлерплейта скрейпов и хрупкость content-hash id на шаблонных шапках; Fable — batch-LLM-ассист и пользовательский редактор структуры детальнее. НИ ОДИН не предложил in-band-разметку глав моделью в потоке перевода (идея владельца 09.08): оба заменили «модель-сейфгард» бесплатным кросс-аудитом. Синтез оркестратора: in-band-разметка остаётся ТРЕТЬИМ эшелоном для книг, где кандидатов нет вовсе (заголовков стилистически нет — оба дизайна честно отправляют такие в ручной редактор), и каналом переведённых названий; её выход обязан проходить тот же валидатор, что и статические гипотезы, и не двигать замороженную нарезку (вид-плоскость). +Расхождения — только в глубине: Opus типизировал провалы (отдельно «гибрид: половина книги размечена» и «nav и NCX согласны, но оба неверны — согласие ≠ правота»), добавил детект бойлерплейта скрейпов; Fable детальнее проработал пользовательский редактор структуры. In-band-разметку моделью в потоке перевода не предложил ни один (оба заменили модель-сейфгард бесплатным кросс-аудитом) — она остаётся ТРЕТЬИМ эшелоном (идея владельца): для книг без кандидатов, для коррекции структуры по ходу черновой волны (§3а п.3) и как канал названий; её выход проходит тот же валидатор и не двигает замороженную нарезку. -**Интеграционный вывод для текущего движка** (вопрос владельца «код полагался на ранние точные границы»): оба дизайна требуют ровно того разделения, что названо в диалоге 09.08 — плоскость нарезки/оплаты (заморожена, ключи по контенту сегментов; сегодня это чанки+чекпойнты — уже близко) отделяется от плоскости структуры (версионируемый артефакт банковского типа со своим снапшотом, ревизиями, миграционными картами); полное принятие ревизии структуры в нарезку — только через существующий `--resnapshot`. Главные точки расклейки текущего кода: редакторские юниты группируются в пределах главы (граница формирует платные запросы) · id главы = хеш всего текста главы · банковские since/until по номерам глав. Это масштаб крупного пака; дизайн-ратификация — отдельным решением владельца. Детальная карта — §6. +### §5а. Мини-сессия названий (роль title; уточнено с владельцем 09.08) -## §6. Рекомендации реализации, обстучанные о боевой код (два картографа по рабочему дереву, 09.08; все file:line — рабочее дерево с незакоммиченным паком блокеров) +Названия переводит НЕ волна тела, а отдельная батч-роль `title` (механика InternalCall, чекпойнт/реплей/телеметрия наследуются): **редакторский слот, не черновой** — объём копеечный (единицы тысяч токенов на книгу), видимость максимальная; **инъекция банка той же механикой** `memory.Select` — спойлер-окна работают естественно (у названия есть номер главы). Синхронизация с банком **двухфазная** (полный банк существует только после майнинг-стопа и подписи): фаза 1 на старте прогона с сид-банком → названия провизорны; фаза 2 после подписи банка → адресная ревизия только названий, где термы разошлись (дифф по попаданиям). Волны потом переводят подзаголовок в прозе тела — $0-канон-гейт сверяет его с названием дерева, расхождение = громкий флаг. Номер рендерится всегда бесплатно. -### Жёсткие правды кода, которые дизайн обязан уважать +## §6. Рекомендации реализации, обстучанные о боевой код (карта по рабочему дереву с незакоммиченным паком блокеров) -1. **Деньги сегодня ключуются ПОЗИЦИЯМИ, не контентом.** RequestHash несёт плотный номер главы и ChunkIdx (render.go:292-317), chunk_status ищется строго по (book, chapter, chunk_idx, stage) (migrate.go:125, stagerun.go:84); контент-хеш `msgsContentHash` существует, но только как guard резюме, не как ключ поиска. Сдвиг ОДНОЙ границы главы перенумеровывает хвост книги и промахивает все его чекпойнты — перекупка хвоста при неизменных байтах. -2. **Любой новый детектор границ = bump chunkerVersion → движение обоих волновых снапшотов → moveOther → $0-репин НЕ работает вовсе** (repin.go:47-91): перекупаются обе волны на 100%. Это признанный отложенный долг D15.2 — snapshot.go:53-64 прямо: «content-addressed reuse … must be designed». Отсюда несущее следствие: **итерации детекта до развязки денег стоят полный прогон книги; единственное дешёвое окно — пока платная книга одна (стенд)**. -3. **Нарезка сама зависит от глав**: паковка параграфов и edit-юниты рестартуют на границе главы (chunker.go:120-145, 330-397), инжекция банка зависит от номера главы (спойлер-окна since/until в плотных ординалах — memory.go:642-650, sticky-reset на границе — wave.go:82-95). «Структура = вид» недостижима одной перекраской ключей — нужна развязка чанкера от глав, а это само по себе одноразовый resnapshot. -4. **Дыра Р6 (найдена картографом)**: ревизия структуры, не двигающая снапшот (ручной редактор, структура-как-данные), сегодня перекупила бы хвост МОЛЧА — projectRebill пропускает строки с совпавшим снапшотом, контент/позиционная ось невидима консент-гейту (rebill.go:32-35, 144-146). Закрывается дёшево read-path-расширением projectRebill — и это ровно миграционный отчёт §5 п.8. -5. **Паттерн-паки структуры НЕЛЬЗЯ класть в существующие планы данных**: pack.Version() и EmbeddedVersion фолдятся в волновой снапшот (snapshot.go:441-442) — файл паттернов в langpack инвалидировал бы переводческие снапшоты пары, в embedded — всех книг. Нужен ТРЕТИЙ версионный план (structure-pack), фолдящийся только в manifestKey (тот пересобирается за $0). Ловушка рядом: само ДОБАВЛЕНИЕ нового поля в снапшот ломает репин всех старых строк (unknown field = moveOther, repin.go:70-84) — одноразовая цена, планировать в то же окно. -6. **Параграф-уровня в движке нет нигде** (параграфы транзиентны, chunker.go:458-466), и параграф легально пересекает чанки (oversized режется по предложениям) — «якоря на хешах абзацев» из §5 п.9 — целиком новая плоскость; ближний эквивалент сегодня — edit-юнит (ПТ-21) + chapter-ID манифеста. +### Жёсткие правды кода -### Что переиспользуется (строить НЕ надо) +1. **Деньги ключуются ПОЗИЦИЯМИ, не контентом.** RequestHash несёт плотный номер главы и ChunkIdx (render.go:292-317), chunk_status ищется строго по (book, chapter, chunk_idx, stage) (migrate.go:125); контент-хеш `msgsContentHash` — только guard резюме, не ключ поиска. Сдвиг ОДНОЙ границы главы перенумеровывает хвост книги и промахивает все его чекпойнты — перекупка хвоста при неизменных байтах. +2. **Любой новый детектор границ = bump chunkerVersion → движение обоих волновых снапшотов → moveOther → $0-репин не работает вовсе** (repin.go:47-91): перекупаются обе волны на 100%. Признанный отложенный долг D15.2 (snapshot.go:53-64: «content-addressed reuse … must be designed»). Следствие: **итерации детекта до развязки денег стоят полный прогон книги; единственное дешёвое окно — пока платная книга одна.** +3. **Нарезка сама зависит от глав**: паковка и edit-юниты рестартуют на границе главы (chunker.go:120-145, 330-397), инжекция банка — спойлер-окна since/until в плотных ординалах (memory.go:642-650) и sticky-reset на границе (wave.go:82-95). «Структура = вид» требует развязки чанкера от глав — само по себе одноразовый resnapshot. +4. **Дыра Р6**: ревизия структуры, не двигающая снапшот (ручной редактор, структура-как-данные), сегодня перекупила бы хвост МОЛЧА — projectRebill пропускает строки с совпавшим снапшотом, контент/позиционная ось невидима консент-гейту (rebill.go:32-35, 144-146). Закрывается дёшево read-path-расширением projectRebill — и это ровно миграционный отчёт §5 п.8. +5. **Паттерн-паки структуры нельзя класть в существующие планы данных**: pack.Version() и EmbeddedVersion фолдятся в волновой снапшот (snapshot.go:441-442) — файл паттернов в langpack инвалидировал бы переводческие снапшоты пары, в embedded — всех книг. Нужен ТРЕТИЙ версионный план (structure-pack), фолдящийся только в manifestKey ($0-пересборка). Рядом ловушка: добавление нового поля в снапшот само ломает репин всех старых строк (unknown field = moveOther, repin.go:70-84) — одноразовая цена, планировать в то же окно. +6. **Параграф-уровня в движке нет нигде** (параграфы транзиентны, chunker.go:458-466), параграф легально пересекает чанки — «якоря на хешах абзацев» (§5 п.9) — целиком новая плоскость; ближний эквивалент сегодня — edit-юнит (ПТ-21) + chapter-ID манифеста. -Манифест-сайдкар пака блокеров = **~60% механики structure.json**: версионируемый документ с ключом годности и cutTag, стейл-детект с фолбэком «никогда не неправильный ответ», атомарная запись (artifact.go), $0-команда tmctl manifest, шов read-моделей readModelChunks → status/export. Расширение содержимого двигает ТОЛЬКО файл (в снапшот не входит). · projectRebill/checkRebillConsent = готовый механизм отчёта «что перекупится и почём» ДО применения. · matchHeaderLine + parseSectionNumeral + HeadingRule = готовая грамматика `{marker}{numeral}{unit}[sep]{name}` с CJK-читателем числительных. · splitTextChapters = готовый ПЕРВЫЙ паттерн-пак индуктора (частотное доминирование ≥2 + precision-гарды). · Лестница кодировок, zip/OPF/xhtml-транспорт epub, ruby-захват — как есть. · ApplyHeading = $0-канал показа названий (не в чекпойнтах). · Терминологист-батчи на синтетической оси + InternalCall = прецедент для батч-перевода названий. · WaveSignatureStop (exit 3) = прецедент «неоднозначность — громкий исход с выбором пользователя». · Контракт 14 и платформа УЖЕ готовы: Chapter.id opaque, number «не ключ», heading nullable «из данных книги», эпоха курсора = generation of the manifest, resync_required; таблица chapters платформы под opaque id построена (00002_readmodel.sql:91-106). +### Что переиспользуется (строить не надо) -### Этапность (рекомендация оркестратора; ратификация — владелец) +Манифест-сайдкар пака блокеров = **~60% механики structure.json**: версионируемый документ с ключом годности и cutTag, стейл-детект с фолбэком, атомарная запись (artifact.go), $0-команда tmctl manifest, шов read-моделей → status/export; расширение содержимого двигает ТОЛЬКО файл (в снапшот не входит). · projectRebill/checkRebillConsent = готовый отчёт «что перекупится и почём» ДО применения. · matchHeaderLine + parseSectionNumeral + HeadingRule = готовая грамматика с CJK-читателем числительных. · splitTextChapters = готовый первый паттерн-пак индуктора. · Лестница кодировок, zip/OPF/xhtml-транспорт, ruby-захват — как есть. · ApplyHeading = $0-канал показа названий (не в чекпойнтах). · Терминологист-батчи + InternalCall = прецедент мини-сессии названий. · WaveSignatureStop (exit 3) = прецедент «громкий исход с выбором пользователя». · Контракт 14 и платформа УЖЕ готовы: Chapter.id opaque, number «не ключ», heading nullable «из данных книги», эпоха курсора = generation of the manifest, resync_required; платформенная таблица chapters под opaque id построена (00002_readmodel.sql:91-106). -- **Этап 0 — $0, wire-нейтрально, можно немедленно:** titleRaw в манифест (subtitle уже добывается в matchHeaderLine и выбрасывается — только пронести) + provenance у heading (rendered/data) + тип единицы (chapter/fragment) + вердикт структуры в документ; бамп manifestVersion. Закрытие дыры Р6 в projectRebill. Слой пользовательских правок — ОТДЕЛЬНЫЙ сайдкар (в манифест нельзя: он unconditionally перестраивается, hand-edit=invalid по конструкции — manifest.go:36-38, 456-459). -- **Этап 1 — «большой перекрой», единственное resnapshot-окно, пока платная книга одна:** IR-слой + формато-адаптеры (epub nav/NCX + фильтры linear/properties — сейчас opfPackage их вообще не парсит, ingest.go:438-447; fb2) + индуктор с валидаторами и вердиктами + третий версионный план паттернов; развязка чанкера от глав (паковка по сегментам, главы = группировка-вид); спойлер-окна банка с ординалов на chapter-ID (bank-only move — репиннимый класс); контент-адресуемый resume (долг D15.2) — чтобы БУДУЩИЕ ревизии структуры перестали стоить хвост книги; смена формулы Chapter.ID на «окно» (§5 п.9) — до материализации глав у пользователей; новая стадия title (батч-перевод названий) — аддитивный фолд стадии двигает снапшот, вписать в это же окно. Всё — одним согласованным resnapshot стенд-книги с консент-отчётом. -- **Этап 2 — после:** in-band метки глав в потоке перевода (третий эшелон, промпт-сдвиг волны) + канон-гейт названий ($0) + миграционные карты якорей (same/moved/split/merged/gone/new) + корпус в CI. +### Этапность (носители — строки бэклога 160–162; ратификация дизайна — владелец) -### Граница доверия LLM ↔ статика (ответ на наброс владельца 09.08) +- **Этап 0 (строка 160) — $0, wire-нейтрально, можно немедленно:** titleRaw в манифест (subtitle уже добывается в matchHeaderLine и выбрасывается — только пронести) + provenance у heading (rendered/data) + тип единицы (chapter/fragment) + вердикт структуры; бамп manifestVersion. Закрытие дыры Р6 в projectRebill. Слой пользовательских правок — ОТДЕЛЬНЫЙ сайдкар (в манифест нельзя: он unconditionally перестраивается, hand-edit=invalid — manifest.go:36-38, 456-459). +- **Этап 1 (строка 161) — «большой перекрой», единственное resnapshot-окно, пока платная книга одна:** IR-слой + формато-адаптеры (epub nav/NCX + фильтры linear/properties — сейчас opfPackage их не парсит, ingest.go:438-447; fb2) + индуктор с валидаторами и вердиктами + третий версионный план паттернов; развязка чанкера от глав (главы = группировка-вид); спойлер-окна банка с ординалов на chapter-ID (bank-only move — репиннимый класс); контент-адресуемый resume (долг D15.2) — чтобы будущие ревизии структуры перестали стоить хвост книги; формула Chapter.ID «окно» — до материализации глав у пользователей; стадия title (§5а; аддитивный фолд стадии двигает снапшот — в это же окно); аддитивное расширение контракта 14 (fragment-тип, вердикт, badge — минорный бамп). Всё — одним согласованным resnapshot стенд-книги с консент-отчётом. +- **Этап 2 (строка 162) — после:** in-band метки глав в потоке перевода (третий эшелон + коррекция структуры по ходу черновой волны, §3а; промпт-сдвиг волны) + канон-гейт названий ($0) + миграционные карты якорей + калибровочный корпус в CI (полигон). -Языковые данные руками не пишутся никогда: сид — проверенные списки зрелых инструментов (zh/ja/en/de готовы); на новом языке/книге с низкой уверенностью универсальных сигналов — один дешёвый LLM-вызов ВЫВОДИТ шаблон книги (распознавание — сила LLM), шаблон проходит тот же статический валидатор, замораживается в снапшот, накопленное абсорбируется в паттерн-пак. LLM — генератор и арбитр данных; статика — исполнитель, валидатор (сохранение символов, монотонность, коридоры — валидировать LLM-выход может только она) и масштабатор. Полный LLM-проход по книге отвергнут (§4); in-band-разметка в потоке перевода — третий эшелон для книг без кандидатов и канал переведённых названий. +### Граница доверия LLM ↔ статика + +Языковые данные руками не пишутся никогда: сид — проверенные списки зрелых инструментов (zh/ja/en/de готовы); на новом языке/книге с низкой уверенностью универсальных сигналов — один дешёвый LLM-вызов ВЫВОДИТ шаблон книги, шаблон проходит тот же статический валидатор, замораживается в снапшот, накопленное абсорбируется в паттерн-пак. LLM — генератор и арбитр данных; статика — исполнитель, валидатор (сохранение символов, монотонность, коридоры — валидировать LLM-выход может только она) и масштабатор. Полный LLM-проход по книге отвергнут (§4).