Store the acceptance reports of the chapter-boundary pack and the fix list that returns it: two cuts change books without the phenomenon
This commit is contained in:
parent
a8a9927ed7
commit
13965cc71e
8 changed files with 1261 additions and 0 deletions
|
|
@ -0,0 +1,104 @@
|
|||
**Приёмка пака «ГРАНИЦА ГЛАВЫ»: вердикт по PROMPT.md и коду, отчёт сессии не читан**
|
||||
|
||||
**Итог.** Всё, что заказано в коде, сделано и работает на исполнении. Четыре пункта нарушены: два только по букве, два по существу (условие исключения и формулировки докблоков). Ещё три находки (Н5–Н7) касаются пина и документов. МАЖОРОВ нет, МИНОРОВ 7.
|
||||
|
||||
Замороженное дерево я не правил. Разница с исходником (`diff -rq`) — ровно список `zone.status`. Команда `gofmt -l` на 428 файлах пуста.
|
||||
|
||||
Отчёт сессии и записку `CHAPTER_BOUNDARY_PLAN.md` я не открывал. Одна оговорка: в выдаче моего грепа по дереву случайно мелькнули две строки таблицы записки. На выводы они не повлияли.
|
||||
|
||||
**Чем снимал.** Всё на своих копиях в `/home/ubuntu/tm-accept-verdict/`:
|
||||
- `before` — `git archive` от HEAD a1b20fc;
|
||||
- `after`, `probe`, `mut1` — копии замороженного дерева;
|
||||
- `harness/main.go` — повторяет `Runner.ingestSource` (events.go:694): `LoadSourceStructure`, затем `LoadWithOverlay(...).Inline`, затем `IngestEncoded`, плюс `SplitChunks` с правилом пары. Выходы лежат в `out/*.json`;
|
||||
- `tmctl manifest` (настоящий драйвер) — `runs/*/`.
|
||||
|
||||
### 1. Пункты промта
|
||||
|
||||
| Пункт | Исход | Чем доказано |
|
||||
|---|---|---|
|
||||
| §4.1(а) | подтверждено исполнением | Кристофф: 60 документов исключены с ролью `toc-entry` через существующий `Excluded`, отдельного счётчика нет. Пропало ровно 60 абзацев, все 60 дословно стоят заголовками глав. На `fifty_shades_1_en.epub` правило сработало 0 раз (`toc-entry`=0). Дифференциальный пин — `chaptercut_test.go:83`, пин «PART ONE» — `:118`. Условие записано в докблоке `chaptercut.go:14-17` (оговорки — Н3) |
|
||||
| §4.1(б) объединение | подтверждено исполнением; имя на совпавшей границе — **опровергнуто по букве** (Н2) | 12 номеров в документах с целью: разрезаны 12 из 12 (`015`:1 · `025`:5 · `033`:3 · `041`:3). isekai: 44 до и 44 после, тексты и имена побайтно равны |
|
||||
| §4.1(в) | подтверждено | Документ режется на куски. Имя главы — от того, кто её открыл (`chaptercut.go:46`). Инвариант «куски покрывают документ» плюс рунная половина — `ingest.go:170-217`. `ruby` приписаны документу, ограничение названо (`ingest.go:874-878`), пин `chaptercut_test.go:386`. `extractXHTML` не изменён: тела функции до и после совпадают, 259 строк |
|
||||
| §4.1(г) | подтверждено | Пин `chaptercut_test.go:132`, мутации ловят |
|
||||
| §4.1(д) | подтверждено | Добавлена метка `combined`, словарь расширен аддитивно. `tmctl manifest` на Кристоффе: `structure=combined`. Платформа неизвестное слово считает недоверенным (`platform/internal/ingest/manifest.go:303-305`) ⇒ форма заказа предложит заказ в знаках |
|
||||
| §4.1(е) | подтверждено | Кристофф с разметкой пары и без неё (`nil`): 95 и 95 глав, имена равны, тексты равны с точностью до `*`. Голых римских числительных по строкам 104, по абзацам 84 |
|
||||
| §4.1(ж) | подтверждено | Новые ключи `numerals/bare/roman/words/joiners/max_runes`. Файл `en/structure.txt`, отпечаток `structure-v1-file-c9868e18b3c3`. `numeral.txt` не тронут (`cmp`). Пин `TestStructureFileIsNotPartOfThePairPack` |
|
||||
| §4.1(з) | подтверждено | `Forms()`: форма без юнита только у грамматики без юнитов; довод записан в докблоке. `fifty_shades_1_en.txt`: 1 глава до, 26 после |
|
||||
| §4.1(и) | подтверждено | В `en-ru/` два файла, `heading.txt` нет |
|
||||
| §4.1-бис | подтверждено | Один предикат `ParseHeader` (`chunker.go:173`, `headings.go:61`). Паритет-пин перевёрнут. Посадка `HEADING-a-zero-prologue…` краснеет по тексту: `opens with heading "", want "Глава 0"` |
|
||||
| §4.2 | подтверждено | Книжных данных нет. Источник имени у 95 глав: навигация 71 · печатное оглавление 22 · голый номер 2 (II, III в SUNSET) |
|
||||
| §4.3, §4.6 | подтверждено | `bookbuild.go`, Runner, платформа не тронуты. В `internal/pipeline` правка только константы `render.go`; `capture.golden` отличается лишь хешами и `chunker_version` |
|
||||
| §4.5 | подтверждено | Версия нарезки v7→v8. Ключ китайской книги сдвинулся: `f12921f2…` → `2cf96774…`. id глав равны, id юнитов новые (тег кроя). Отпечаток CJK прежний: `structure-v1-cjk-f153a20e3185` в обоих деревьях |
|
||||
| §13: китайская книга не изменилась по тексту | подтверждено | 11 китайских входов (включая полный guzhenren: 2283 главы, 5071 чанк): тексты глав, чанков и `Heading` побайтно равны |
|
||||
| §13: корпус до/после | подтверждено | 17 входов. Изменились только Кристофф (71→95, `declared`→`combined`) и `fifty_txt` (1→26). Прогон детерминирован: 6 из 6 одинаковых sha |
|
||||
| §13: гейт зоны целиком, круги, отчётные пункты | не проверено | Гейт гоняет другая линза. Я прогнал `chunk` и `lang`: `ok`, с `-race` тоже, в режиме `-v` 292 PASS · 0 SKIP |
|
||||
|
||||
### 2. Запреты
|
||||
|
||||
- **Ветка по языку, паре или письменности.** Греп по 1446 добавленным строкам прод-Go (без комментариев) дал 1 совпадение: `unicode.Pd` — это общая категория тире, а не письменность. Контроль: тот же регэксп на `chunker.go`/`ingest.go` из HEAD даёт 12.
|
||||
- **Порог в знаках или «почти равно».** Новых нет: `HeaderMaxRunes`=60 перенесён, `headerFloor`=2 считает единицы. Тождество проверяется точным `==` после `NormalizeSource`.
|
||||
- **Правило «прописные = имя».** 0 совпадений.
|
||||
- **Книжная ступень.** Тела `LoadSourceStructure` и `structureFingerprint` идентичны HEAD.
|
||||
|
||||
### 3. Удалённые и переписанные тесты
|
||||
|
||||
`comm` по всем `func Test` двух деревьев: удалены 2, всего стало 1555 → 1600.
|
||||
|
||||
- `TestIngestAndChunkerAgreeOnOrdinaryChapterHeaders` переименован в `TestIngestAndChunkerAgreeOnChapterHeaders` (`headingparity_test.go:24`). Сравнение двух предикатов снято, потому что предикат теперь один.
|
||||
- `TestIngestAndChunkerDivergeOnAZeroPrologueAndALongHeader` удалён. Куда уехали его гарантии:
|
||||
- «第零章 режется и получает имя» — строка `:31` плюс проверки `:62` и `:66`;
|
||||
- «порядковый номер ≠ номер в имени» — там же: глава 2 (第一章) обязана быть «Глава 1»;
|
||||
- «ровно на потолке» — `:35`;
|
||||
- «за потолком» — `TestAHeaderPastTheCeilingIsNeitherCutNorStripped` (`:79`).
|
||||
- `TestChapterUnitJaWebnovel`: то же утверждение про 話, переписано на `chooseByCount`.
|
||||
- `structure_newlang_test.go:51`: проверка усилена — номер, юнит и подзаголовок.
|
||||
- `ingest_encoding_test.go`: изменилась только сигнатура.
|
||||
|
||||
### 4. Мутации
|
||||
|
||||
Прогнал 49 записей на `mut1`: 12 записей, бывших вне гейта, плюс 36 новых плюс перенацеленная `HEADING-…ordinary-header`. Вердикты: **поймано 49 · выжило 0 · не измерено 0**, сгнивших якорей 0 из 593. Каждый вердикт проверил по тексту падения — все про предмет посадки. Популяция `battery` = 346 + 12 + 36 = 394, совпадает. Копия после прогона побайтно равна замороженному дереву.
|
||||
|
||||
### 5. Находки
|
||||
|
||||
1. **[МИНОР] Печатное оглавление и 5 не-тождеств приклеены к последней главе.**
|
||||
- Что: документы `058`, `124` и осколки `076`/`087`/`100`/`116`/`123` уходят в главу 95 «About the Author». В ней 149 абзацев; 111 из них, начиная с «Contents», — текст оглавления (2757 знаков), и он пойдёт в перевод.
|
||||
- Почему это находка: исход «приклеено к предыдущей главе» не входит в три, которые разрешил промт (граница · преамбула · исключение с ролью).
|
||||
- Чем доказано: `out/kristoff.after.json`, глава 95.
|
||||
- Что делать: выбрать один из трёх исходов или объявить исход с доводом. Объявлено ли в отчёте — не проверено.
|
||||
2. **[МИНОР] §4.1(б), где промт требует «РОВНО ТАК»: имя на совпавшей границе берётся у ярлыка, а не у заголовка грамматики.**
|
||||
- Где: `chaptercut.go:151` и `:208-209`, пин `chaptercut_test.go:561`.
|
||||
- Проба P1: ярлыки «Opening»/«Middle» на документах «Chapter One: The Road»/«Chapter Two: The Sea» дают имена `["Opening","Middle"]`.
|
||||
- Смягчающее: буквальное правило переименовало бы 26 из 32 глав `fifty_shades_1_en.epub` («Chapter One» → «CHAPTER ONE»), а это нарушает другое требование промта — «книга без явления не меняется».
|
||||
- Что делать: пинг или объявление в отчёте.
|
||||
3. **[МИНОР] Условие исключения тождества недостаточно, и в докблоке это не оговорено.**
|
||||
- Что: `chaptercut.go:14-17` утверждает «nothing is lost while the heading it names, and the name, stand in the prose».
|
||||
- Пробы P4 и P4b: часть 1 — страница-тождество «Chapter Two(: Night)» на цели, её проза в документе без цели; главы части 2 без целей. Итог: страница исключена как `toc-entry` (11–18 рун), заголовок части 1 пропал из чтения, глав 3 вместо 4. Условие докблока при этом выполнено: имя стоит в прозе части 2.
|
||||
- Случай назван ограничением №3 (`:26-28`), но вывод в `:14-17` не оговорён, у ограничения нет пина, и формулировка «its chapter's text joins» умалчивает, что строка заголовка исключается.
|
||||
- Что делать: оговорить вывод и поставить пин на ограничение.
|
||||
4. **[МИНОР] Докблок пола структуры не называет условия, при котором вывод перестаёт держаться.**
|
||||
- Что: `headings.go:16-18` — «one is a stray line, two are a structure».
|
||||
- Проба P6: книга без TOC из 6 документов с двумя случайными абзацами «Chapter 3»/«Chapter 7» даёт **2 главы `detected`**. До пака было бы 6 глав `delimited`. `detected` — единственная метка, по которой платформа продаёт заказ в главах.
|
||||
- Что делать: назвать условие в докблоке. Ужесточать ли правило для EPUB — мнение.
|
||||
5. **[МИНОР] Первая половина пина `TestEPUBSectionWitnessNeedsAProvenContentsDocument` (`chaptercut_test.go:283`) не меряет охрану, которую заявляет.**
|
||||
- Что: фикстура без единого документа-оглавления, поэтому её держит ранний выход `len(contents)==0` (`chaptercut.go:300`), а не проверка `!contents[u.group]` (`:317`).
|
||||
- Чем доказано: посадка `CUT-a-section-needs-a-proven-contents-document` поймана только посторонним тестом `…CompeteWithTheSpine:232`; профильный тест остался зелёным (лог `mutlogs/`).
|
||||
- Что делать: в фикстуру пина добавить настоящий документ-оглавление.
|
||||
6. **[МИНОР, косметика] Склеенные тексты в `mutations.json`.** У 4 перенацеленных записей поле `why` склеено без разделителя: «…ingests RE-AIMED by…».
|
||||
7. **[МИНОР] Протухшие ссылки на удалённые символы в `backend/docs`.**
|
||||
- Где: `CHAPTER_STRUCTURE_DESIGN.md:72,1273,1287,1301-1302` (`matchHeaderLine`, `chapterHeaderMaxRunes`, `detectChapterUnit`) и `MONEYSTOP_REPORT.md:49`.
|
||||
- Что делать: баннер или актуализация. Дизайн объявлен источником формы, поэтому ссылки там важны.
|
||||
|
||||
### 6. Чего я не проверил и где мой прибор слеп
|
||||
|
||||
- Полный гейт `battery` и `lint` — у другой линзы.
|
||||
- Отчётные пункты §13 (N и судьбы глав объявлены с доводом, «круги сошлись», «работа завершена») не проверял. Код даёт: N = 95; SUNSET — 3 главы («Sunset» с I, затем «II», «III»); DAWN — отдельная глава; 6 титулов частей — отдельные главы.
|
||||
- Продуктовый путь платформы не проверен.
|
||||
|
||||
**Файлы:**
|
||||
- `/home/ubuntu/tm-accept-verdict/harness/main.go`
|
||||
- `/home/ubuntu/tm-accept-verdict/corpus.tsv`
|
||||
- `/home/ubuntu/tm-accept-verdict/out/`
|
||||
- `/home/ubuntu/tm-accept-verdict/runs/`
|
||||
- `/home/ubuntu/tm-accept-verdict/mut1.log`
|
||||
- `/home/ubuntu/tm-accept-verdict/mutlogs/`
|
||||
- `/home/ubuntu/tm-accept-verdict/test_chunk_lang.log`
|
||||
|
|
@ -0,0 +1,110 @@
|
|||
# Приёмка пака «ГРАНИЦА ГЛАВЫ»: пере-прогон чисел, гейт, шов с платформой
|
||||
|
||||
**Вердикт по линзе.** Все числа сессии, которые я пере-снял, подтверждены исполнением. Гейт зелёный. Golden изменился только в версиях и хешах. Платформа читает новый манифест и ничего не отказывает. Нашлось два замечания: одно МАЖОР (условие для холодного прогона, в отчёте не названо) и одно МИНОР.
|
||||
|
||||
Где что лежит: всё в `/home/ubuntu/tm-accept-7b-rerun/`. Там копия замороженного дерева `new/backend`, развёртка HEAD `head/{backend,platform}`, копии для проб `probe-{head,new}`, `golden-head`, `plat/platform`, а логи и манифесты в `work/`. Замороженное дерево я не правил. Сверка: HEAD + `zone.diff` + `zone.untracked` совпадает с ним полностью (`diff -rq`), кроме `.env.example`, которого в замороженном дереве нет.
|
||||
|
||||
## 1. Числа настоящим драйвером — ПОДТВЕРЖДЕНО ИСПОЛНЕНИЕМ
|
||||
|
||||
Бинари собраны так: `go build ./cmd/tmctl` в копии нового дерева и в развёртке HEAD. Для каждой книги снято `tmctl manifest --config <book.yaml> --json` обоими бинарями. book.yaml собран по шаблону оператора из `platform/deploy/README.md` (pipeline-c1, models, `langpack_root`, абсолютные пути).
|
||||
|
||||
| книга | глав до/после | метка | id глав совпали | имена совпали | чанки до/после | ключ сдвинулся |
|
||||
|---|---|---|---|---|---|---|
|
||||
| kristoff | 71/95 | declared→combined | нет | нет | 386/364 | да |
|
||||
| fifty-epub | 31/31 | declared→declared | да | да | 175/175 | да |
|
||||
| fifty-txt | 1/26 | none→detected | нет | нет | 160/170 | да |
|
||||
| isekai-epub | 44/44 | declared | да | да | 163/163 | да |
|
||||
| isekai-txt | 1/1 | none | да | да | 141/141 | да |
|
||||
| enkan | 1/1 | none | да | да | 37/37 | да |
|
||||
| gz-utf8 | 2283/2283 | detected | да | да | 5071/5071 | да |
|
||||
| gz-gb | 2283/2283 | detected | да | да | 5071/5071 | да |
|
||||
| gz-slice | 25/25 | detected | да | да | 57/57 | да |
|
||||
| jinpingmei | 1/1 | none | да | да | 66/66 | да |
|
||||
|
||||
Таблица сессии совпала со мной во всех ячейках.
|
||||
|
||||
⚠ Условие замера у японских книг (isekai, enkan). Промтов `ja-ru` в репозитории нет, поэтому движок на них отказывает с `EXIT=10`. Я прогнал их через теневой каталог промтов: `ja-ru` → ссылка на `zh-ru`, одинаково для HEAD и для нового дерева. На нарезку промты не влияют. Но это же значит, что японскую книгу через платформу сегодня не прогнать вообще.
|
||||
|
||||
**Кристофф, 95 глав — сверено по самой книге:**
|
||||
- **Источники имени** я вывел заново из самого EPUB (`toc.ncx`, `058`, `123`), не из отчёта:
|
||||
- ярлык NCX — 65;
|
||||
- печатное оглавление `058` — 16;
|
||||
- титул части из NCX — 6;
|
||||
- раздел печатного оглавления — 6;
|
||||
- только номер — 2.
|
||||
- С приложением записки совпали все 95 строк: номер, имя, источник, знаки. Команда сверки вернула `rows mismatching my re-derivation: 0 of 95`.
|
||||
- Все 81 имя вида «Chapter N. Name» совпадают с началом своей главы в прозе: римский номер плюс имя после свёртки регистра. Расхождений 0 из 81 (контроль: единственное несовпадение — глава 1 «Dramatis Personae», и это объявленная преамбула).
|
||||
- **12 номеров в документах с целью навигации разрезаны:** `015`:1 · `025`:5 · `033`:3 · `041`:3. Поиск начала каждой из 95 глав в документах spine нашёл все 95.
|
||||
- **Тождества:** исключено 60 документов с ролью `toc-entry`, 1700 рун. Все 60 ярлыков стали именами новых глав, каждый ровно один раз.
|
||||
- **Сохранение текста.** Мультимножество строк HEAD минус новое дерево — ровно эти 60 ярлыков; в новом дереве сверх HEAD — 0 строк. Руны без переводов строк: 1 589 174 − 1 587 474 = 1700, то есть ровно исключённые ярлыки. Прибавилось 38 переводов строк на новых резах.
|
||||
|
||||
**fifty-txt:** 26 глав, от CHAPTER ONE до CHAPTER TWENTY-SIX. Непробельные руны совпадают: 667 783 = 667 783.
|
||||
|
||||
**Китайские книги побайтно.** Проба `probe-*/backend/internal/pipeline/zz_acc_chunkhash_test.go` берёт настоящий раннер и режет через `ingestSource` + `SplitChunksWithChapters`. Она хеширует каждый текст главы и каждый чанк со всеми полями (`%#v`).
|
||||
- gz-utf8 и gz-gb: `chapters_sha=88f1445d615de96c`, `chunks_sha=f67336538d375218` — одинаковы на HEAD и в новом дереве.
|
||||
- gz-slice и jinpingmei тоже одинаковы.
|
||||
- Отпечаток грамматики `structure-v1-cjk-f153a20e3185` не сдвинулся. У английских книг он сменился: `-none` → `-file-c9868e18b3c3`.
|
||||
- `第零章` в корпусе: 0. Заголовков длиннее 60 рун: 0. Контроль: 2289 строк в форме заголовка из 215 675 прочитанных, максимум 23 руны.
|
||||
|
||||
## 2. Гейт — ПОДТВЕРЖДЕНО ИСПОЛНЕНИЕМ
|
||||
|
||||
- `cat /proc/self/cgroup` показал `0::/`; `memory.max` у корня нет, то есть потолка нет. Рядом с копией положена ссылка `books`, чтобы окружение было как у сессии.
|
||||
- `make battery` на копии дал `MAKE_EXIT=0`, линтер — `0 issues`.
|
||||
- Полнота: в `go list ./...` 24 пакета, вердиктов тоже 24 (20 `ok` + 4 без тестов, `FAIL` 0). `comm -23` пуст (контроль: 24 строки в каждом списке).
|
||||
- Скипов 6, все стендовые: `TestMinerFullBookParity`, `TestProbeBankBasisM1`, `TestProbeBankBatchOrdinalShift`, `TestCorpusBankKeyConflicts`, `TestHelperEventsRun`, `TestHelperKillLoop`.
|
||||
- Лог: `work/battery.log`.
|
||||
- Предупреждение линтера про `/tmp/tm-verify-0509/.../journal.go:90` проверено отдельно. С чистым кешем `runevents` даёт `0 issues`. Вызов `rw.Close` исключён конфигом `.golangci.yml:78-79`, так что проглоченной находки нет.
|
||||
|
||||
## 3. Golden — ПОДТВЕРЖДЕНО ИСПОЛНЕНИЕМ
|
||||
|
||||
- В диффе 172 строки. После маскировки 64-символьных хешей (132 токена) и строки версии нарезки (4 вхождения) не остаётся ни одной строки.
|
||||
- `final_text` изменился в 0 из 16 строк.
|
||||
- Сильнее: HEAD, в котором я поменял только константу `chunkerVersion` на v8 и перегенерировал golden (`TM_UPDATE_GOLDEN=1`), даёт файл, побайтно равный сданному (`cmp` без различий). Значит, всё изменение в golden — это смена версии.
|
||||
|
||||
## 4. Шов с платформой — ПОДТВЕРЖДЕНО ИСПОЛНЕНИЕМ
|
||||
|
||||
Пробы лежат в `plat/platform/internal/readmodel/zz_acc_seam_test.go` и `internal/books/zz_acc_provision_test.go`. Обе вызывают настоящий бинарь движка через `runner.Manifest`.
|
||||
|
||||
**Новый Кристофф на платформе:**
|
||||
|
||||
| проверка | результат |
|
||||
|---|---|
|
||||
| `Readable` / `Whole` | ошибок нет |
|
||||
| интейк | принят |
|
||||
| `ChapterOrdersOffered` | `false` — заказ в ЗНАКАХ, как и при `declared` сегодня |
|
||||
| `Priced` | `true`, отказов 0 |
|
||||
| материализация | 95 глав и 302 пары сохранены |
|
||||
| текст найден | у 302 из 302 пар |
|
||||
| пары без размера | 0 |
|
||||
| нумерация глав | плотная |
|
||||
|
||||
- Форма манифеста не изменилась: наборы полей на всех уровнях совпадают, версия `tm-manifest-v2`. Форма экспорта тоже (`tm-export-v1`).
|
||||
- Новые счётчики (`targets_readdressed`, `targets_ambiguous`, `sections_from_toc`) выводятся только в лог ошибок движка; платформа его не разбирает.
|
||||
- Колонка `structure` в базе без CHECK: это прочитано в `00033_order_and_price.sql`, а не исполнено.
|
||||
- gz-utf8 на 2283 главах: принят, `chapter_orders=true`, сохранено 4276 из 4276 пар.
|
||||
- Тесты платформы в моей копии: `ingest` — 99 прошло, 0 упало, 1 скип (`TestARealReadOutIsReadTheWayThisBuildClaims`); `readmodel`, `books`, `runs`, `runner` — зелёные. Три живых $0-теста против нового движка прошли, 0 скипов: `TestTheRealEnginesPriceProjectionIsReadByThisBuild`, `TestWhatTheLiveManifestCarriesAndThisBuildDeclinesToRead`, `TestTheRenderedConfigurationIsOneTheEngineActuallyLoads`.
|
||||
|
||||
## Находки
|
||||
|
||||
**[МАЖОР] Весь эффект пака на книге прогона зависит от `langpack_root` в шаблоне оператора, и без него откат молчаливый. В отчёте это условие не названо.**
|
||||
- **Где:** `backend/internal/lang/structure.go:545-548`. При пустом корне язык с CJK-письменностью получает встроенную грамматику, а `en` — `nil, nil`. Вызывается из `backend/internal/pipeline/runner.go:471-472`. В `backend/example/book.yaml` ключа `langpack_root` нет (`grep -c` даёт 0).
|
||||
- **Чем доказано:** платформа сама рендерит book.yaml (`books.Service.provision`), дальше настоящий `tmctl manifest`:
|
||||
- шаблон с `langpack_root` → `chapters=95 structure="combined"`;
|
||||
- шаблон без него → `chapters=71 structure="declared"`, `Readable` без ошибок, ни одного предупреждения о грамматике;
|
||||
- настоящий шаблон этого хоста `/home/ubuntu/tm-78-stand/book-template.yaml` (17.09) → тоже `chapters=71 structure="declared"`.
|
||||
- **На хосте ключа нет у 3 шаблонов из 7:** `tm-78-stand/book-template.yaml`, `~/.local/share/tmstand/book-template.yaml`, `book-template-zero.yaml`. У шаблонов `tm-coldrun-a/b` он есть.
|
||||
- **Что делать:**
|
||||
- строкой отчёта и в промте прогона: шаблон оператора обязан нести `langpack_root`, проверить это на шаблоне прогона;
|
||||
- сделать откат видимым: заметка ингеста или предупреждение, когда у языка источника нет грамматики (отпечаток `-none` уже знает это, но его никто не читает);
|
||||
- либо ряд платформе на проверку шаблона.
|
||||
|
||||
**[МИНОР] Два последствия для платформы, которых в отчёте нет.**
|
||||
- **(а) fifty-txt.** HEAD платформы + HEAD движка: отказ на интейке, `ErrStructureNotDeliverable` (1 глава, `platform/internal/books/parse.go:219`). Новый движок: книга принимается, метка `detected`, заказ предлагается в ГЛАВАХ (`platform/internal/ingest/manifest.go:304`). Это смена продуктового поведения; судя по 26 настоящим главам — к лучшему, но её надо назвать.
|
||||
- **(б) Номер нарезки входит в id каждой пары** (`backend/internal/pipeline/manifest.go:455`). Поэтому у всех 10 книг сменились id 100% пар, в том числе у побайтно неизменных китайских: например, `9c893d348b9480a8:c4536645:0` → `9c893d348b9480a8:0c927ea2:0` (4276 из 4276). Id глав при этом у неизменных книг не сдвинулись. На развёртке с книгами следующая материализация — это пере-нарезка: сдвигается `structure_version`, id пар обнуляются. Отчёт §5 называет только ключ и снапшоты.
|
||||
- **Что делать:** добавить строку в §5 отчёта.
|
||||
|
||||
## Не проверено
|
||||
|
||||
- Путь интейка через Postgres (SQL `FinishParse` / `SaveStructure`). Без `TM_PLATFORM_TEST_DSN` пропущено 269 тестов; колонку без CHECK я только прочёл.
|
||||
- Японские книги через настоящую пару: промтов `ja-ru` нет.
|
||||
- Вне пака, уже известно (`configs/pairs/en-ru.yaml:48-52`): все шаблоны хоста несут `transcription: palladius`. По коду `runner.go:299-302` английскую книгу это отказывает на `translate`. Исполнением не проверял: платное не запускал. Для промта прогона двух книг через один шаблон это существенно.
|
||||
|
|
@ -0,0 +1,147 @@
|
|||
# Приёмка пака «ГРАНИЦА ГЛАВЫ». Линза: поиск вне карты отчёта и собственные посадки
|
||||
|
||||
Замороженное дерево `/home/ubuntu/tm-accept-7b/backend` не тронуто. Все мои копии побайтно совпадают с ним, если не считать моих тестовых файлов `zz_*` (проверено `diff -rq`). HEAD сравнения — `a98f510`, взят через `git archive`. У `backend/` отличия от базы только в файлах пака. Платного ничего не запускал. Потолка памяти у cgroup нет (`0::/`).
|
||||
|
||||
**Итог.** Нашёл 2 МАЖОРа и 6 МИНОРов по поведению, плюс 9 выживших посадок из 22. Моих посадок, оставшихся неизмеренными, нет. Ни одна находка не затрагивает холодный прогон: у Кристоффа 95 глав, у китайского txt изменений нет. Но на книгах, где явления нет и которых нет в корпусе, требование §4.2 не выполнено: движок меняет нарезку, хотя не должен.
|
||||
|
||||
## Как измерял
|
||||
|
||||
- **Прибор.** Один и тот же файл `zz_hunter_probe_test.go` запускался на HEAD и на дереве, свой zip-сборщик вместо `chunktest`: на HEAD у `chunktest` нет поля `Labels`. Путь — настоящий `IngestEncoded`, грамматика — `lang.LoadSourceStructure("../../configs/langpacks", <язык>)`.
|
||||
- **Повтор:** `HUNTER_OUT=<файл> go test ./internal/chunk -run 'TestHunterProbe$' -count=1` в `/home/ubuntu/tm-accept-7b-hunter-probe/backend` и в `/home/ubuntu/tm-accept-7b-hunter-head/backend`.
|
||||
- **Выводы** лежат в `/home/ubuntu/tm-accept-7b-hunter-cat/out/`.
|
||||
- **Контроль, что прибор читал книги.** Через тот же прибор Кристофф даёт 95 глав, `combined`, `targets_readdressed 65`, `sections_from_toc 6` — это совпадает с отчётом. `fifty_shades_1_en.txt` даёт 26 глав, `detected`.
|
||||
|
||||
## МАЖОРЫ
|
||||
|
||||
**1. [МАЖОР] Ярлык без имени уводит границы глав в пронумерованное стихотворение. Книга с верным NCX разваливается, а число глав не меняется.**
|
||||
- **Где.**
|
||||
- `internal/chunk/headings.go:127` — ярлык «Chapter N» без имени называет любой голый заголовок только по номеру, через границы документов.
|
||||
- `internal/chunk/chaptercut.go:167-178` — ветка `len(other) > 0`: граница цели снимается, её имя уходит на этот заголовок.
|
||||
- `configs/langpacks/en/structure.txt:8` — `bare roman`.
|
||||
- **Чем доказано** (P5 в probe-файле). Шесть документов, NCX 1:1 с ярлыками «Chapter 1…6». Заголовки в прозе — цифры «1»…«6», их грамматика не разбирает. В главе 5 одно стихотворение со строфами I, II, III.
|
||||
- HEAD: 6 глав, `declared`, всё верно.
|
||||
- Дерево: 6 глав, `combined`, `documents_attached 3 targets_readdressed 3`:
|
||||
- «Chapter 4» (762 руны) — склейка документов 1–4;
|
||||
- «Chapter 5»;
|
||||
- «Chapter 1», «Chapter 2», «Chapter 3» — это строфы I/II/III внутри главы 5;
|
||||
- «Chapter 6».
|
||||
- С ярлыками «Chapter One…» (P5b) результат тот же.
|
||||
- Результат детерминирован: 3 прогона дали один md5.
|
||||
- Контроль P5c: ярлыки-названия вместо «Chapter N» — переадресации нет, но см. МАЖОР 2.
|
||||
- **Статус: подтверждено исполнением.**
|
||||
- **Почему это не названное ограничение.** В коде (`chaptercut.go:26-28`) механизм описан так: «страница главы, чей ярлык повторяет свободный заголовок (вторая часть без своих целей)». Это книга с явлением. Здесь явления нет. Вся нужная для поломки «свободность» — одна строфа с голым номером, и сигнал остаётся только в заметках ингеста.
|
||||
- **Что делать.** Переадресация через документ по одному номеру не должна допускать ярлык с маркером к голому заголовку без маркера. Как вариант: без имени переадресовывать только на первый заголовок документа, на котором не стоит цель. Нужен пин на фикстуре P5. Решает оркестратор.
|
||||
|
||||
**2. [МАЖОР] Когда оглавление ничего не подтверждает, форма заголовка выбирается по счёту. Пронумерованные сцены, строфы и `第N节` внутри глав из верного NCX становятся главами.**
|
||||
- **Где.**
|
||||
- `internal/chunk/chaptercut.go:104-106`: если `chooseFormByTargets` ничего не нашла, срабатывает `scan.chooseByCount(st)`, и это происходит и тогда, когда цели есть.
|
||||
- Объединение границ — строки 204-216.
|
||||
- Данные: `en/structure.txt:8` `bare roman`; для китайского — встроенный CJK, юнит `节`.
|
||||
- **Чем доказано.** Во всех случаях ниже NCX 1:1, ярлыки — названия глав:
|
||||
|
||||
| Проба | Книга | HEAD | Дерево |
|
||||
|---|---|---|---|
|
||||
| P4b | 4 главы со сценами I/II/III | 4, `declared` | 16, `combined`; каждая сцена — глава с названием «I»/«II»/«III» |
|
||||
| P7b | 4 главы, одно стихотворение I/II | 4 | 6 |
|
||||
| P5c | главы с цифровыми заголовками, одно стихотворение I–III | 6 | 9 |
|
||||
| Q1 (zh EPUB) | главы с `第一节`/`第二节` внутри | 3, `declared` | 9, `combined` |
|
||||
|
||||
- P4: картинка вместо заголовка + сцены + безымянные ярлыки → 4 превращаются в 13. Порядок имён перепутан: у «Chapter 2» первая строка «II»; начальная проза главы 2 приклеена к сцене III главы 1.
|
||||
- **Статус: подтверждено исполнением.**
|
||||
- **Почему МАЖОР, хотя ограничение названо.** Оно названо (`chaptercut.go:24`, §10 отчёта), но на экзотическом примере («примечания Chapter One»). На деле срабатывает обычный класс: сцены с римскими номерами в литературной прозе, китайские `节` внутри глав. §4.2 требует от языковых данных доказанной инертности. Доказательство — корпус из 10 книг, и ни в одной нет этого класса. Строка `bare roman` — это грамматика Кристоффа, поданная как грамматика языка (§5.4 промта).
|
||||
- **Цепочка последствий.** В книге читателя главы нумеруются через `bookbuild.go:556` → 16 записей оглавления вместо 4. Липкое окно памяти сбрасывается на каждой сцене (`wave.go:81-86`).
|
||||
- **Что делать.** При наличии целей и нуле подтверждений «ярлык → заголовок» не резать внутри документов с целью. Либо явно ратифицировать ограничение отдельным рядом бэклога. Решает оркестратор.
|
||||
|
||||
## МИНОРЫ
|
||||
|
||||
**3. [МИНОР] Оглавление, вплотную примыкающее к заголовку главы 1, даёт лишнюю главу, названную как глава 1.**
|
||||
- **Где.** `headings.go:83`: серия записей строится по соседству юнитов, пустые строки txt юнитами не считаются. Последняя запись списка и настоящий заголовок попадают в одну серию, и запись «Chapter One» не помечается как `listed` (`:94`).
|
||||
- **Доказано.**
|
||||
- Q4 (EPUB, список в документе главы 1): HEAD 2 `declared` → дерево 3 `combined`, у первой главы текст «Contents», название «Chapter One: The Road».
|
||||
- T3 (en txt): 3 главы при 2 настоящих.
|
||||
- Q2 (zh txt): HEAD 4 → дерево 3 — улучшение, но не исправление.
|
||||
- Пин `TestTXTContentsListIsNotCut` зелёный только потому, что в его фикстуре между списком и главой стоит строка «Book Title». Фикстура держит это условие постоянным и о нём молчит.
|
||||
- **Подтверждено.**
|
||||
|
||||
**4. [МИНОР] Для английского страж «после номера не идёт содержимое» ничего не проверяет.**
|
||||
- **Где.** `internal/lang/structure.go:167`. Пробел — не содержимое, поэтому любое предложение до 60 рун, начинающееся с «Chapter <число> », считается заголовком.
|
||||
- **Доказано.** Q3: строка «Chapter three was about her mother and the farm.» режет главу (дерево 3, HEAD 1). T4: «Chapter two, verse five, she read aloud.» — то же.
|
||||
- В отчёте не названо. **Подтверждено.**
|
||||
|
||||
**5. [МИНОР] `italic_carriers` считает курсив в исключённых фрагментах `toc-entry`.**
|
||||
- **Где.** `ingest.go:789`: все документы читаются с `ir` до решения об исключении. Служебные страницы при этом нарочно читаются без разметки (`:743`, с комментарием «why»). Это тот же класс, что закрытая SP2-находка по сервисным страницам.
|
||||
- **Доказано.** Probe3: без фрагмента `italic_carriers=1`; с курсивным фрагментом `italic_carriers=2`, а маркеров в тексте глав по-прежнему 2, то есть 1 прогон. **Подтверждено.**
|
||||
|
||||
**6. [МИНОР] Шов с платформой: слово `detected` стало шире, а платформа доверяет только ему.**
|
||||
- **Где.** Платформа продаёт главами только при `detected` (`platform/internal/ingest/manifest.go:303-305`). EPUB без оглавления с найденной голой формой теперь получает `detected` (`epubcut.go:94`).
|
||||
- **Доказано.** Q5: 3 документа со сценами I/II. HEAD — 3 главы, `delimited`, заказ в знаках. Дерево — 6 глав, `detected`: заказ предлагается «главами», а это сцены.
|
||||
- Отчёт в пункте (д) разобрал только `combined`. Именно этот сдвиг не назван. **Подтверждено.**
|
||||
- **Что проверил по остальному шву** — исчерпывающего перечисления слов нигде нет, `combined` и новые поля никого не ломают:
|
||||
- `cmd/tmctl/render.go:1114` печатает слово как есть;
|
||||
- `frontend/src` — 0 упоминаний слов метки, прочитано 113 файлов;
|
||||
- новые счётчики и роль `toc-entry` идут только в лог (`events.go:701`);
|
||||
- `TitleRaw` попадает только в манифест (`manifest.go:483`);
|
||||
- `stripHeading` без шаблона пары ничего не делает (`chunker.go:170`), то есть у en-ru заголовки не теряются.
|
||||
|
||||
**7. [МИНОР] Ruby в документе из нескольких глав датируется ранней главой.**
|
||||
- P12 (ja): чтение `魔法` стоит в третьей главе, датировано ch=2. Это совпадает с объявленным ограничением «рано, не поздно».
|
||||
- **Подтверждено как объявленное.** Моя посадка H10 «датировать по последнему куску» поймана пином `TestEPUBRubyInASplitDocumentIsDatedByTheDocument`: «ruby = … Chapter:3, want … chapter 1».
|
||||
|
||||
**8. [МИНОР] Классы, где изменений нет** — вывод HEAD и дерева побайтно одинаков:
|
||||
- P7: «Henry V», «Part II», «MIX», «CIVIL», одиночная «V» в прозе;
|
||||
- P8/P8b: вложенный NCX;
|
||||
- P9: пустые документы spine;
|
||||
- P10: повторяющиеся имена и страница-тождество «Interlude»;
|
||||
- P11/P11b: zh, тома, перезапуск нумерации;
|
||||
- T1b: zh txt с BOM+CRLF.
|
||||
|
||||
P1 (nav с фрагментами `#id`, разбираемые заголовки): 2 `delimited` → 4 `combined` — это улучшение. P2 (фрагменты, заголовки-названия) — без изменений. **Подтверждено.**
|
||||
|
||||
## Посадки вне каталога: 22 штуки, по одной
|
||||
|
||||
- **Прибор.** Мой каталог `/home/ubuntu/tm-accept-7b-hunter-cat/hunter.json`, прогнан родным `tmmutate` на копии `/home/ubuntu/tm-accept-7b-hunter-mut/backend`. Логи — в `logs1/` и `logs2/`.
|
||||
- **Первый проход** — пакет, где живёт пин: 12 поймано, 10 выжило.
|
||||
- **Второй проход** — выжившие против `./...`. Полнота: 24 из 24 пакетов (20 ok + 4 без тестов), 0 FAIL, 6 скипов (стендовые, те же, что в отчёте).
|
||||
- **Базовые прогоны.** chunk и lang: 201 PASS, 0 скипов. `./...`: 132 с, 0 FAIL.
|
||||
|
||||
**Поймано** (по тексту падения, текст про посаженное):
|
||||
- H01 (тождество ослаблено до префикса): «ambiguous = 0 …», «a fragment that is not an identity keeps its text».
|
||||
- H03 (именем считается любой следующий юнит): «unnamed: chapters = 2, want 3».
|
||||
- H07 (`chunkerVersion` не бампнут): `TestGoldenDeterminism` «golden mismatch … snapshot_draft».
|
||||
- H08 (роль `toc` вместо `toc-entry`): «excluded roles = ["toc" "toc"], want … toc-entry».
|
||||
- H09 (имена от ярлыков выброшены): «named: 2 chapters titled ["I" "II"]».
|
||||
- H10 (ruby по последнему куску): «dated chapter 1».
|
||||
- H11 (`collapsed` важнее `combined`): «structure = "delimited" … want … combined».
|
||||
- H12 (юнит необязателен при объявленном): «ParseHeader("第三,我们") … want not a header».
|
||||
- H16 (`sections_from_toc` не в заметках): «SectionsFromTOC = 1 produces NO alarm».
|
||||
- H19 (голое число может быть префиксом): «ParseHeader("XIX.") … want not a header».
|
||||
- H21 (серия из одного юнита): «chapters open with ["BOOK ONE" "BOOK TWO"]».
|
||||
- H22 (`max_runes` из данных игнорируется): «max_runes 80 must accept a 71-rune line».
|
||||
|
||||
**Выжили** — это находки, [МИНОР] как дыры в пинах, кроме H18. Свидетели сняты исполнением на `/home/ubuntu/tm-accept-7b-hunter-wit/backend`, `zz_wit_test.go`, сценарий `witrun.sh`:
|
||||
|
||||
- **H18 [МАЖОР-дыра]** `chaptercut.go:244`: записи списка оглавления засчитываются как улика формы. Кристофф: **95 → 74 главы** (`targets_readdressed` 65 → 0), и весь `./...` зелёный. Приёмочное число пака N=95 не охраняет ни один тест гейта. В 270 тестовых файлах нет теста, читающего книгу; всё на этой книге — разовый замер. Лечение: стендовый пин по образцу `TestMinerFullBookParity` или синтетика с печатным оглавлением формы-маркера.
|
||||
- **H06** `chaptercut.go:258` — «близость» сделана плоской. Свидетель W06: 3 → 8 глав. Единственный пин проходит за счёт порядка тай-брейка форм.
|
||||
- **H13** `headings.go:124` — сравнение юнитов выключено. W13b (zh, NCX только на `第N回`, главы `第N章`): 6 → 4; две страницы исключены как `toc-entry`, главы переименованы.
|
||||
- **H02** `headings.go:130` — равенство имени ослаблено до префикса. W02b: страница «Chapter 2: The» исключена, а заголовок «Chapter 2: The Sea» получил её имя.
|
||||
- **H17** `chaptercut.go:195` — строка оглавления переименовывает заголовок с собственным именем. W17: «CHAPTER ONE: THE ROAD» превратилось в «Chapter One: The Road». Правило D11 «строка, ДОБАВЛЯЮЩАЯ имя» ничем не закреплено.
|
||||
- **H20** `chaptercut.go:312` — «побеждает первая группа» выключено. W20: из 8 прогонов 5 режут одно, 3 другое, из-за порядка обхода map. Пин детерминизма отсутствует.
|
||||
- **H15** `ingest.go:825` — `Runes: 0` у `toc-entry`. Размер исключения, заявленный в отчёте, не закреплён.
|
||||
- **H04** `ingest.go:835` (проверка тайлинга) и **H05** `ingest.go:854` (проверка хвоста). Каталожная посадка `EPUB-conservation…` всё равно краснеет через оставшиеся стражи («reached no chapter» / «do not tile it»). Но у каждого стража есть свой класс отказа без посадки: нахлёст кусков (это двойная оплата) ловит только тайлинг, короткий последний кусок — только хвост. Поэтому удаление любого из них оставляет гейт зелёным.
|
||||
|
||||
**Эквивалентная, не находка:** H14 `chaptercut.go:154` (`absorbedBy[own[0]] == i` избыточно). Держится, пока на группу приходится не больше одной цели: так устроен `resolveTOC` через `seen[i]` в `epubtoc.go`. Если адаптер начнёт выдавать несколько целей на документ, условие станет несущим.
|
||||
|
||||
## Не проверено
|
||||
|
||||
- Реальные английские EPUB с классами МАЖОРов 1–2: таких нет в `books/` (7 EPUB, из них английских 2). Частоту класса я оцениваю, а не измеряю.
|
||||
- Путь через платформу (`TM_PLATFORM_BOOKS_DIR` не задан).
|
||||
|
||||
## Файлы
|
||||
|
||||
- Пробы:
|
||||
- `/home/ubuntu/tm-accept-7b-hunter-probe/backend/internal/chunk/zz_hunter_probe_test.go`
|
||||
- `/home/ubuntu/tm-accept-7b-hunter-wit/backend/internal/chunk/zz_wit_test.go`
|
||||
- Каталог посадок и логи: `/home/ubuntu/tm-accept-7b-hunter-cat/` (`hunter.json`, `pass1.log`, `pass2.log`, `logs1/`, `logs2/`, `logs-combo-H04/`, `logs-combo-H05/`, `out/`, `witrun.sh`)
|
||||
- Копии:
|
||||
- `/home/ubuntu/tm-accept-7b-hunter-mut/backend` — чистая, побайтно равна замороженному дереву;
|
||||
- `/home/ubuntu/tm-accept-7b-hunter-head/backend` — HEAD `a98f510` плюс мой probe-файл.
|
||||
|
|
@ -0,0 +1,76 @@
|
|||
**Итог приёмки мутационной кампании пака «ГРАНИЦА ГЛАВЫ»**
|
||||
|
||||
Все пять заявлений сессии подтверждены исполнением: гейт 394, поймано 394, выжило 0, не измерено 0, протухших якорей 0 из 593. Мажорных находок нет, минорных две: одна запись поймана случайным совпадением в фикстуре чужого теста, у одной пере-прицеленной записи устарело поле `why`.
|
||||
|
||||
## Как прогнано
|
||||
|
||||
- **Копия.** tar из замороженного дерева в `/home/ubuntu/tm-accept-mut/backend`, с теми же исключениями `.env*` и `bin`, что в Makefile. Проверки перед запуском: `test -f go.mod`, `pwd` совпал с ожидаемым, `diff -rq` против замороженного дерева пуст. После кампании `diff -rq` снова пуст, копия восстановлена байт в байт.
|
||||
- **Команда.** Та же, что в `Makefile:116`: `go run ./cmd/tmmutate -root <копия> -battery`. Без `-run` и без сужений.
|
||||
- **Два отличия от Makefile.**
|
||||
- Добавлен `-logs`: он только сохраняет полный вывод каждой записи и на выбор записей не влияет.
|
||||
- `TM_CHECKER_LABELS_DIR` и `TM_RUN_A_DIR` указаны на `/home/ubuntu/projects/textmachine-main/books/gu-zhenren/…`. Рецепт Makefile берёт `$(MAKEFILE_DIR)/../books`, а рядом с замороженным деревом каталога книг нет, и корпусные пины молча скипнулись бы. С этими переменными они реально бегут: `TestTheInstrumentAgainstTheColdRunOracle` и три теста `TestK6`/`TestTheAnchoredTolerance` дали 4 PASS, 0 SKIP.
|
||||
- **Память.** Прогон шёл в cgroup `0::/`, ограничения памяти там нет, ни одного убийства.
|
||||
- **Время.** С 03:57:15 до 05:03:53. Окончание ждал по строке `MUT_EXIT` в логе, а не по коду обёртки.
|
||||
|
||||
## 1–2. Итог кампании
|
||||
|
||||
**Прогнано 394 · поймано 394 · выжило 0 · НЕ ИЗМЕРЕНО 0 · протухших якорей 0 из 593.**
|
||||
|
||||
- **Сверка с составом гейта.** Записей с `battery` в `mutations.json` замороженного дерева (sha256 `55fe9206…`) ровно 394. Строки вердикта есть у всех 394, повторов нет, лишних нет, лог-файлов 394.
|
||||
- **Базовые прогоны.** Все 12 пакетов были зелёными до первой посадки.
|
||||
- **Хвост лога:** `394 mutation(s) run, 0 unexpected outcome(s)` · `anchors swept: 0 of 593 entr(ies) rotten` · `MUT_EXIT=0`.
|
||||
- **Нет ли поимок чужим падением.** По 394 логам: panic — 0, timeout — 0, `build failed` / `[setup failed]` — 0. Записей без единой строки `--- FAIL` — 0. Нет записей, пойманных только «вездесущими» тестами (порог — тест, падающий в 15 и более записях); самый частый ловец падает в 10 записях.
|
||||
- **Независимая проверка якорей питоном** по тому же правилу, что в харнессе (каждый `find` встречается ровно один раз в накопленном тексте файла): 593 записи, 605 правок, 104 файла, нарушений 0, записей с `find == replace` — 0.
|
||||
- **Скипы в пакетах новых записей** (чистая копия, те же переменные): `ok internal/chunk` 152 PASS / 0 SKIP; `ok internal/lang` 49 PASS / 0 SKIP.
|
||||
- **Каталог против HEAD** (blob `9e8ea58` против `75fa36f`, те же хэши, что в `zone.diff`):
|
||||
- было 557 записей, стало 593: добавлено 36, удалено 0;
|
||||
- в гейте было 346, стало 394: 36 новых и 12 введённых в гейт;
|
||||
- изменённых записей 13: у 12 добавлен флаг `battery`, у трёх из них ещё и правки; тринадцатая — пере-прицел записи, уже стоявшей в гейте.
|
||||
|
||||
## 3. Тексты падений
|
||||
|
||||
Прочитал не выборку из 10, а все 49 записей (36 новых, 12 введённых в гейт, 1 пере-прицеленная).
|
||||
|
||||
- 36 новых прогнал второй раз, отдельной программой на отдельной копии `/home/ubuntu/tm-accept-mut2` через `-id`. Итог 36 RED, наборы упавших тестов совпали с кампанией 36 из 36.
|
||||
- У 48 записей текст падения говорит ровно о посаженном предмете. Примеры:
|
||||
- `CUT-ruby-is-dated-by-its-document-once`: «ruby = [...Chapter:1 ...Chapter:2 ...Chapter:3], want the one reading dated chapter 1»;
|
||||
- `HEADER-number-words-multiply-by-a-larger-word`: «"Chapter One Hundred and Five" … N:106 … want n=105»;
|
||||
- `HEADING-a-zero-prologue-is-stripped-where-it-is-cut`: «header "第零章:序幕" opens with heading "", want "Глава 0"»;
|
||||
- `CUT-the-rule-reads-the-pair-free-text`: «marked: chapters open with ["I" "II"], want I, II and the italic III»;
|
||||
- `CUT-a-run-stays-inside-one-document`: поимка не зависит от совпадения в фикстуре — проверил, при изменённой фикстуре тест так же краснеет.
|
||||
- Исключение — запись `CUT-a-section-needs-a-proven-contents-document`, находка 1 ниже.
|
||||
|
||||
## 4. Четыре пере-прицеленные записи
|
||||
|
||||
Старые версии прогнал на копии `git archive HEAD` (`/home/ubuntu/tm-accept-mut-head`): 4 RED, 0 протухших из 557. Старых целей в новом дереве нет: `groupChapters`, `pre = append(pre, i)`, `if toc.collapsed > 0`, `chapterHeaderMaxRunes`, `last.docs = append` — 0 совпадений по 18 файлам пакета chunk. Значит, пере-прицел был вынужденным.
|
||||
|
||||
| Запись | Ловец до (HEAD) | Ловец после | Вывод |
|
||||
|---|---|---|---|
|
||||
| `EPUB-collapsed-targets-downgrade-the-provenance` | `TestEPUBDuplicateNavTargetsCollapse`, `epubtoc_test.go:277` «structure = "declared", want "delimited"» | тот же тест, тот же текст | Предмет тот же. Посадка гасит единственную ветку, где `collapsed` что-то значит (`epubcut.go:90`); ветка `combined` на строке 87 «declared» не даёт |
|
||||
| `EPUB-preamble-joins-chapter-one` | 2 теста, «spine document "OEBPS/title.xhtml" is neither excluded nor part of a chapter» | 7 тестов, среди них те же два с тем же текстом | Предмет тот же |
|
||||
| `EPUB-conservation-no-document-leaves-unaccounted` | 5 тестов, «neither excluded nor part of a chapter» | 44 теста: те же с тем же текстом плюс сообщения о покрытии документа кусками («pieces do not tile it», «the rest of it reached no chapter») | Предмет тот же, защита стала сильнее |
|
||||
| `HEADING-ingest-and-chunker-answer-the-same-on-an-ordinary-header` | «the ingest cut 2 chapters out of 3 headers» | `headingparity_test.go:49` «the ingest cut 4 chapters out of 5 headers» и ещё 26 тестов | Цель — прямой преемник старой константы, удобной заменой не является. Поле `why` устарело, находка 2 |
|
||||
|
||||
## Находки
|
||||
|
||||
**1. [МИНОР] Запись `CUT-a-section-needs-a-proven-contents-document` поймана случайностью фикстуры чужого теста; тест, названный под это правило, посадку не видит.**
|
||||
- **Что.** Посадка убирает `!contents[u.group] ||` в `backend/internal/chunk/chaptercut.go:317`. Названный под правило тест `TestEPUBSectionWitnessNeedsAProvenContentsDocument` (`chaptercut_test.go:282`) при посадке проходит. Его половина `notContents` (строка 283) не содержит ни одного документа оглавления, поэтому `addSections` выходит раньше, на `chaptercut.go:300` (`if len(contents) == 0 { return }`), и до проверки на строке 317 не доходит.
|
||||
- **Кто ловит на самом деле.** Только `TestEPUBWithoutATableTheHeadingsCompeteWithTheSpine`, и только потому, что в `chaptercut_test.go:227` все документы несут одну и ту же строку `prose`: абзац документа `a` совпадает с первым абзацем `a2`, и по этому совпадению открывается раздел.
|
||||
- **Чем доказано** (копия `mut2`, после проверки восстановлена, `diff -rq` пуст):
|
||||
- посадка плюс `-run 'TestEPUBSectionWitnessNeedsAProvenContentsDocument$' -v` дают `--- PASS`, `ok`;
|
||||
- посадка плюс первый абзац `a2` заменён на отличный текст: весь пакет `go test ./internal/chunk/ -count=1` даёт `ok`, то есть посадка выжила;
|
||||
- тот же изменённый вариант без посадки тоже `ok`, то есть изменение фикстуры само по себе ничего не ломает.
|
||||
- **Что делать.** Дать половине `notContents` настоящий документ оглавления со списком, называющим главы. Тогда между строкой «Epilogue» и открытием раздела останется только проверка на строке 317, и названный тест станет ловцом. Пере-проверить посадкой.
|
||||
|
||||
**2. [МИНОР] У пере-прицеленной `HEADING-ingest-and-chunker-answer-the-same-on-an-ordinary-header` поле `why` описывает механизм, который новая посадка произвести не может.**
|
||||
- **Что.** Текст «Shrink the ingest's length guard … while the chunker still reads them as headers» после пере-прицела ложен. Потолок длины теперь один: `n > s.maxRunes` в `backend/internal/lang/structure.go:148`, внутри `ParseHeader`. Эту функцию зовут обе стороны: нарезка в `headings.go:61` и чанкер в `chunker.go:173`. Посадка `HeaderMaxRunes = 6` (`structure.go:30`) сужает потолок обеим сторонам сразу, расхождения между ними не возникает. На деле запись теперь стережёт соседнее свойство: «потолок пропускает обычный заголовок». Расхождение по нулевой главе стережёт новая запись `HEADING-a-zero-prologue-is-stripped-where-it-is-cut`; расхождение по потолку закрыто самим устройством кода.
|
||||
- **Чем доказано** (копия `mut2`): посадка плюс `-run` двух тестов проверки согласия дают `--- FAIL: TestIngestAndChunkerAgreeOnChapterHeaders` («ingest cut 4 chapters out of 5») и `--- PASS: TestAHeaderPastTheCeilingIsNeitherCutNorStripped`. Во второй половине этого теста 7-рунная строка открывает главу через разрыв страницы, и чанкер её не снимает: он тоже применяет суженный потолок.
|
||||
- **Что делать.** Переписать `why` под фактическое свойство и назвать, кто теперь стережёт каждую из двух половин расхождения. Приписки «RE-AIMED» к старому тексту мало. У записей `EPUB-collapsed` и `EPUB-conservation` старый текст с «groups whole documents» / «lands in exactly one chapter» тоже устарел, но предмет у них тот же (ловец и текст падения совпадают до и после), поэтому это косметика.
|
||||
|
||||
## Файлы
|
||||
- Лог кампании: `/home/ubuntu/tm-accept-mut/campaign.log`
|
||||
- Полный вывод по каждой записи: `/home/ubuntu/tm-accept-mut/logs/<id>.log` (394 файла)
|
||||
- Список 49 записей с посадками: `/home/ubuntu/tm-accept-mut/the48.txt`
|
||||
- Повторный прогон 36 новых: `/home/ubuntu/tm-accept-mut2/run.log` и `/home/ubuntu/tm-accept-mut2/logs/`
|
||||
- Старые версии четырёх записей на HEAD: `/home/ubuntu/tm-accept-mut-head/logs/`
|
||||
- Скрипты разбора: `/home/ubuntu/tm-accept-mut/tools/analyze.py`, `/home/ubuntu/tm-accept-mut/tools/failtext.py`
|
||||
|
|
@ -0,0 +1,93 @@
|
|||
# Приёмка пака «ГРАНИЦА ГЛАВЫ» — возврат на дофикс (19.09, оркестратор `textmachine-main-f3`)
|
||||
|
||||
Судилось замороженное дерево `/home/ubuntu/tm-accept-7b` (копия дерева сдачи, `diff -rq` — 0 различий). Четыре линзы,
|
||||
отчёты рядом: `1-blind-verdict.md` (вердикт до чтения отчёта) · `2-rerun-gate-seam.md` (числа настоящим драйвером, гейт,
|
||||
шов с платформой) · `3-out-of-map-hunt.md` (вне карты отчёта + 22 своих посадки) · `4-mutation-campaign.md` (полная кампания).
|
||||
|
||||
**ПОДТВЕРЖДЕНО ИСПОЛНЕНИЕМ — всё, что заявлено в отчёте сессии:** таблица корпуса совпала во всех ячейках · Кристофф 95 глав,
|
||||
источник имени у всех 95 выведен заново из самой книги и совпал 95 из 95 · 12 номеров в документах с целью разрезаны ·
|
||||
сохранение текста — ровно 60 исключённых ярлыков (1700 рун) · китайские главы и чанки побайтно равны HEAD · гейт `make battery`
|
||||
`MAKE_EXIT=0`, 24/24 пакета, 6 стендовых скипов · golden — только версия нарезки (HEAD с одним бампом константы даёт
|
||||
побайтно тот же файл) · платформа читает новый манифест, отказов 0 · кампания **394 / 394 / 0 / 0**, протухших якорей 0 из
|
||||
593, тексты падений прочитаны у всех 49 записей пака.
|
||||
|
||||
**ВЕРДИКТ: ВОЗВРАТ.** Два мажора нарушают собственный инвариант пака §4.2 — «книга без явления не меняется», — и оба
|
||||
воспроизведены оркестратором повторным прогоном пробы охотника: вывод байт в байт совпал с `probe-head.txt` / `probe-tree.txt`.
|
||||
|
||||
## МАЖОРЫ — до лендинга
|
||||
|
||||
**М1. Ярлык без имени уводит границы в пронумерованное стихотворение.** Проба `P5`: шесть глав, навигация 1:1 с ярлыками
|
||||
«Chapter 1…6», заголовки в прозе — цифры, в главе 5 стихотворение со строфами I/II/III. HEAD — 6 верных глав. Дерево — тоже 6,
|
||||
но глава 1 «Chapter 4» есть склейка документов 1–4, а строфы стали главами «Chapter 1/2/3». Число глав беду не показывает.
|
||||
Механизм: безымянный ярлык с маркером сопоставляется голому заголовку ТОЛЬКО по номеру и через границу документа
|
||||
(`internal/chunk/headings.go` ≈:127, `internal/chunk/chaptercut.go` ≈:167–178).
|
||||
|
||||
**М2. Голая форма режет сцены и строфы внутри глав верной навигации.** Проба `P4b`: 4 главы с названиями и римскими сценами
|
||||
внутри — HEAD 4, дерево **16** (каждая сцена — глава «I»/«II»/«III»); `P7b` 4 → 6; `P5c` 6 → 9; `Q1` (китайский EPUB, `第N节`
|
||||
внутри глав) 3 → 9. Механизм: когда навигация ничего не подтверждает, форма выбирается по счёту и при наличии целей
|
||||
(`chaptercut.go` ≈:104–106). Строка `bare roman` в `configs/langpacks/en/structure.txt` — грамматика Кристоффа под видом грамматики
|
||||
языка: ровно уязвимость §5 п.4 промта; корпус пака этого класса не содержал, поэтому инертность на нём не доказывала ничего.
|
||||
|
||||
⛔ **ПРИНЦИП ЛЕЧЕНИЯ М1 И М2 — РОВНО ТАК; механизм — РЕШИ САМА с доводом.** Голая форма заголовка и сопоставление ярлыка с
|
||||
заголовком по ОДНОМУ НОМЕРУ через границу формы — слабые улики. Без ИМЕННОГО свидетеля (ярлык навигации или строка печатного
|
||||
оглавления, совпавшие с заголовком по имени) они границу не проводят. У Кристоффа свидетель именной (65 ярлыков и 16 строк
|
||||
оглавления совпадают по имени), у проб охотника — нет.
|
||||
**Обязательные инварианты, пинами:** (1) каждая фикстура пробы охотника, где явления нет (`P4`, `P4b`, `P5`, `P5b`, `P5c`, `P7b`,
|
||||
`Q1`, `Q5`), даёт на дереве то же, что на HEAD, — главы, имена, метку; (2) Кристофф — 95 глав с теми же именами, иначе новое
|
||||
число с доводом; (3) улучшения, которые проба показала (`P1`: фрагменты навигации 2 → 4), не теряются. Проба лежит рядом:
|
||||
`zz_hunter_probe_test.go.txt` — бери фикстуры как есть.
|
||||
|
||||
**М3. Приёмочное число N = 95 не стережёт ни один тест гейта.** Посадка охотника `H18` (записи списка оглавления засчитываются как
|
||||
улика формы) даёт Кристоффа **95 → 74**, и весь `./...` зелёный: книгу не читает ни один тест, всё про неё — разовый замер. ⇒
|
||||
стендовый пин по образцу существующих корпусных (условие запуска — каталог книг на машине гейта — НАЗВАТЬ; на этом хосте он есть,
|
||||
рецепт `make` его передаёт) плюс синтетический пин с печатным оглавлением маркерной формы, ловящий `H18` без книги.
|
||||
|
||||
**М4. Молчаливый откат, если в шаблоне оператора нет `langpack_root`.** Без него грамматика английского не грузится, книга режется
|
||||
как раньше — 71 глава `declared`, — и ни одного предупреждения (`internal/lang/structure.go` ≈:545–548). Настоящий шаблон стенда
|
||||
этого хоста ключа не несёт (3 шаблона из 7). ⇒ откат обязан быть громким: заметка ингеста или предупреждение, когда у языка
|
||||
источника грамматика не разрешилась (отпечаток `-none` это знает, но его никто не читает). Пин на условное сообщение — с фикстурой,
|
||||
где оно ОБЯЗАНО прозвучать, и с парой, где обязано молчать. (Требование к шаблону прогона — сторона оркестратора.)
|
||||
|
||||
## МИНОРЫ — в этом же круге
|
||||
|
||||
1. Условие исключения тождества недостаточно, докблок `chaptercut.go` ≈:14–17 («nothing is lost…») это не оговаривает; пробы
|
||||
блайнд-линзы `P4`/`P4b` (страница-тождество главы части 1, проза в документе без цели) — ограничение №3 названо, но без пина и
|
||||
без оговорки вывода.
|
||||
2. Докблок пола структуры (`headings.go` ≈:16–18) не называет, где вывод перестаёт держаться; проба `P6` (без оглавления, 6
|
||||
документов, две случайные строки «Chapter 3»/«Chapter 7») — 2 главы `detected` вместо 6 `delimited`, а `detected` — единственная
|
||||
метка, по которой платформа продаёт главами. Сюда же `Q5` охотника: EPUB без оглавления со сценами I/II получает `detected`.
|
||||
3. Пин `TestEPUBSectionWitnessNeedsAProvenContentsDocument` не меряет свою охрану: в фикстуре нет документа-оглавления, держит
|
||||
ранний выход; посадку ловит чужой тест по совпадению фикстуры. Дать половине `notContents` настоящий документ-оглавление.
|
||||
4. Оглавление вплотную к заголовку главы 1 даёт лишнюю главу (`Q4`, `T3`); пин `TestTXTContentsListIsNotCut` зелён только из-за
|
||||
строки «Book Title» в фикстуре — фикстура держит условие постоянным и о нём молчит.
|
||||
5. Страж «после номера не идёт содержимое» для английского не проверяет ничего: «Chapter three was about her mother and the farm.»
|
||||
режет главу (`Q3`, `T4`).
|
||||
6. `italic_carriers` считает курсив в исключённых фрагментах `toc-entry` (служебные страницы нарочно читаются без разметки, эти — нет).
|
||||
7. Посадки охотника, выжившие как дыры в пинах: `H06` (плоская близость, 3 → 8), `H13` (сравнение юнитов выключено, zh 6 → 4 с
|
||||
переименованием), `H02` (равенство имени до префикса), `H17` (строка оглавления переименовывает заголовок со своим именем — правило
|
||||
D11 «ДОБАВЛЯЮЩАЯ имя» не закреплено), **`H20` (порядок обхода карты: 5 прогонов из 8 режут одно, 3 — другое; пина детерминизма нет —
|
||||
цель №6 проекта)**, `H15` (размер `toc-entry` не закреплён), `H04`/`H05` (у каждого стража — тайлинг и хвост — свой класс отказа
|
||||
без своей посадки). Каждую — пином, который её ловит по тексту падения.
|
||||
8. `mutations.json`: у 4 пере-прицеленных записей `why` склеен без разделителя; у `HEADING-ingest-and-chunker-answer-the-same-on-an-ordinary-header`
|
||||
`why` описывает механизм, который посадка больше произвести не может, — переписать под фактическое свойство и назвать, кто стережёт
|
||||
обе половины бывшего расхождения.
|
||||
9. Протухшие ссылки на удалённые символы в `backend/docs/CHAPTER_STRUCTURE_DESIGN.md` (≈:72, 1273, 1287, 1301–1302) и
|
||||
`backend/docs/MONEYSTOP_REPORT.md` (≈:49) — баннер или актуализация.
|
||||
10. Строками отчёта, которых нет: (а) `fifty_shades_1_en.txt` на HEAD платформой ОТКАЗЫВАЛСЯ на интейке (1 глава), после пака —
|
||||
принимается и продаётся главами: смена продуктового поведения; (б) версия нарезки входит в id каждой пары — у всех 10 книг
|
||||
сменились id 100 % пар, включая побайтно неизменные китайские (4276 из 4276): на развёртке с книгами следующая материализация —
|
||||
пере-нарезка.
|
||||
|
||||
## ПРИНЯТО КАК ОБЪЯВЛЕННОЕ — не дефекты
|
||||
Текст оглавлений (`058`, `124`, пять осколков) в главе «About the Author» — принят оркестратором как остаток при записке-плане ·
|
||||
имя на совпавшей границе уровня абзаца от ярлыка (D11) — принято: иначе книга без явления сменила бы 26 имён · ruby по документу —
|
||||
объявленное ограничение, пин ловит.
|
||||
|
||||
## СТОРОНА ОРКЕСТРАТОРА — не тебе
|
||||
`langpack_root` и отсутствие пар-связанных ключей (`transcription: palladius`) в шаблоне холодного прогона · перечень слов метки в
|
||||
контракте (`combined`) · ряд платформе про доверие к `combined` и расширение `detected` · якоря ноты `D39.270`, описывающие код, которого
|
||||
после пака нет.
|
||||
|
||||
## ПОВТОРНАЯ ПРИЁМКА
|
||||
Пере-прогон пробы охотника (все фикстуры, HEAD против дерева) · гейт целиком · полная мутационная кампания · числа Кристоффа и
|
||||
корпуса настоящим драйвером. Числа в отчёте — после последней правки.
|
||||
|
|
@ -0,0 +1,129 @@
|
|||
=== P1 nav-fragments headings parse
|
||||
chapters=2 structure=delimited notes=[targets_collapsed 2 chapters 2 structure delimited]
|
||||
ch1 runes=102 title="Foreword" opens="Foreword"
|
||||
ch2 runes=317 title="Chapter 1" opens="Chapter 1"
|
||||
=== P2 nav-fragments title headings
|
||||
chapters=2 structure=delimited notes=[targets_collapsed 2 chapters 2 structure delimited]
|
||||
ch1 runes=102 title="Foreword" opens="Foreword"
|
||||
ch2 runes=313 title="The Road" opens="The Road"
|
||||
=== P4 image heading + roman scenes + nameless labels
|
||||
chapters=4 structure=declared notes=[]
|
||||
ch1 runes=390 title="Chapter 1" opens="It was a long day and the road was longe…"
|
||||
ch2 runes=390 title="Chapter 2" opens="It was a long day and the road was longe…"
|
||||
ch3 runes=390 title="Chapter 3" opens="It was a long day and the road was longe…"
|
||||
ch4 runes=390 title="Chapter 4" opens="It was a long day and the road was longe…"
|
||||
=== P4b titled chapters + roman scenes
|
||||
chapters=4 structure=declared notes=[]
|
||||
ch1 runes=402 title="The Road" opens="The Road"
|
||||
ch2 runes=401 title="The Sea" opens="The Sea"
|
||||
ch3 runes=402 title="The Hill" opens="The Hill"
|
||||
ch4 runes=402 title="The Town" opens="The Town"
|
||||
=== P5 digit headings, labels Chapter N, one poem I-III in ch5
|
||||
chapters=6 structure=declared notes=[]
|
||||
ch1 runes=189 title="Chapter 1" opens="1"
|
||||
ch2 runes=189 title="Chapter 2" opens="2"
|
||||
ch3 runes=189 title="Chapter 3" opens="3"
|
||||
ch4 runes=189 title="Chapter 4" opens="4"
|
||||
ch5 runes=430 title="Chapter 5" opens="5"
|
||||
ch6 runes=189 title="Chapter 6" opens="6"
|
||||
=== P5b same, labels Chapter <Word>
|
||||
chapters=6 structure=declared notes=[]
|
||||
ch1 runes=189 title="Chapter One" opens="1"
|
||||
ch2 runes=189 title="Chapter Two" opens="2"
|
||||
ch3 runes=189 title="Chapter Three" opens="3"
|
||||
ch4 runes=189 title="Chapter Four" opens="4"
|
||||
ch5 runes=430 title="Chapter Five" opens="5"
|
||||
ch6 runes=189 title="Chapter Six" opens="6"
|
||||
=== P5c control, labels are titles
|
||||
chapters=6 structure=declared notes=[]
|
||||
ch1 runes=189 title="Title One" opens="1"
|
||||
ch2 runes=189 title="Title Two" opens="2"
|
||||
ch3 runes=189 title="Title Three" opens="3"
|
||||
ch4 runes=189 title="Title Four" opens="4"
|
||||
ch5 runes=430 title="Title Five" opens="5"
|
||||
ch6 runes=189 title="Title Six" opens="6"
|
||||
=== P5d marker headings in most chapters + letter I/II in ch7
|
||||
chapters=8 structure=declared notes=[]
|
||||
ch1 runes=105 title="Chapter 1" opens="CHAPTER ONE"
|
||||
ch2 runes=105 title="Chapter 2" opens="CHAPTER TWO"
|
||||
ch3 runes=107 title="Chapter 3" opens="CHAPTER THREE"
|
||||
ch4 runes=106 title="Chapter 4" opens="CHAPTER FOUR"
|
||||
ch5 runes=106 title="Chapter 5" opens="CHAPTER FIVE"
|
||||
ch6 runes=105 title="Chapter 6" opens="CHAPTER SIX"
|
||||
ch7 runes=245 title="Chapter 7" opens="SEVEN"
|
||||
ch8 runes=99 title="Chapter 8" opens="EIGHT"
|
||||
=== P7 roman in prose, titled NCX
|
||||
chapters=4 structure=declared notes=[]
|
||||
ch1 runes=167 title="The King" opens="The King"
|
||||
ch2 runes=127 title="The Letter" opens="The Letter"
|
||||
ch3 runes=125 title="The List" opens="The List"
|
||||
ch4 runes=200 title="The End" opens="The End"
|
||||
=== P7b titled NCX + one poem I/II
|
||||
chapters=4 structure=declared notes=[]
|
||||
ch1 runes=102 title="The Road" opens="The Road"
|
||||
ch2 runes=101 title="The Sea" opens="The Sea"
|
||||
ch3 runes=245 title="The Hill" opens="The Hill"
|
||||
ch4 runes=102 title="The Town" opens="The Town"
|
||||
=== P8 nested NCX parts→chapters
|
||||
chapters=6 structure=declared notes=[]
|
||||
ch1 runes=8 title="PART ONE" opens="PART ONE"
|
||||
ch2 runes=103 title="Chapter 1" opens="Chapter 1"
|
||||
ch3 runes=103 title="Chapter 2" opens="Chapter 2"
|
||||
ch4 runes=8 title="PART TWO" opens="PART TWO"
|
||||
ch5 runes=103 title="Chapter 3" opens="Chapter 3"
|
||||
ch6 runes=103 title="Chapter 4" opens="Chapter 4"
|
||||
=== P8b nested NCX part on chapter doc
|
||||
chapters=3 structure=delimited notes=[targets_collapsed 1 chapters 3 structure delimited]
|
||||
ch1 runes=103 title="Part One" opens="Chapter 1"
|
||||
ch2 runes=103 title="Chapter 2" opens="Chapter 2"
|
||||
ch3 runes=103 title="Chapter 3" opens="Chapter 3"
|
||||
=== P9 empty spine documents
|
||||
chapters=4 structure=declared notes=[documents_attached 1 chapters 4 structure declared]
|
||||
ch1 runes=103 title="Chapter 1" opens="Chapter 1"
|
||||
ch2 runes=103 title="Chapter 2" opens="Chapter 2"
|
||||
ch3 runes=0 title="Interlude" opens=""
|
||||
ch4 runes=103 title="Chapter 3" opens="Chapter 3"
|
||||
=== P10 repeated names + identity page Interlude
|
||||
chapters=5 structure=declared notes=[]
|
||||
ch1 runes=103 title="Interlude" opens="Interlude"
|
||||
ch2 runes=103 title="Chapter 1" opens="Chapter 1"
|
||||
ch3 runes=103 title="Interlude" opens="Interlude"
|
||||
ch4 runes=103 title="Chapter 2" opens="Chapter 2"
|
||||
ch5 runes=9 title="Interlude" opens="Interlude"
|
||||
=== P11 zh EPUB volumes + restart numbering
|
||||
chapters=6 structure=declared notes=[]
|
||||
ch1 runes=6 title="第一卷 风起" opens="第一卷 风起"
|
||||
ch2 runes=42 title="第一章 开始" opens="第一章 开始"
|
||||
ch3 runes=14 title="第二章 继续" opens="第二章 继续"
|
||||
ch4 runes=6 title="第二卷 云涌" opens="第二卷 云涌"
|
||||
ch5 runes=14 title="第一章 再来" opens="第一章 再来"
|
||||
ch6 runes=14 title="第二章 结束" opens="第二章 结束"
|
||||
=== P11b zh nameless labels, restart
|
||||
chapters=4 structure=declared notes=[]
|
||||
ch1 runes=14 title="第一章" opens="第一章 开始"
|
||||
ch2 runes=14 title="第二章" opens="第二章 继续"
|
||||
ch3 runes=14 title="第一章" opens="第一章 再来"
|
||||
ch4 runes=14 title="第二章" opens="第二章 结束"
|
||||
=== P12 ja ruby, two chapters in doc b
|
||||
chapters=2 structure=declared notes=[]
|
||||
ch1 runes=24 title="第一章 始まり" opens="第一章 始まり"
|
||||
ch2 runes=34 title="第二章 続き" opens="第二章 続き"
|
||||
ruby 漢字(かんじ) ch=1
|
||||
ruby 魔法(まほう) ch=2
|
||||
=== T1 txt BOM+CRLF words
|
||||
chapters=1 structure=none notes=[]
|
||||
ch1 runes=312 title="" opens="Chapter One"
|
||||
=== T1b txt BOM+CRLF zh
|
||||
chapters=3 structure=detected notes=[]
|
||||
ch1 runes=10 title="第一章 开始" opens="第一章 开始"
|
||||
ch2 runes=10 title="第二章 继续" opens="第二章 继续"
|
||||
ch3 runes=10 title="第三章 结束" opens="第三章 结束"
|
||||
=== T2 txt stanza numbers in prose
|
||||
chapters=1 structure=none notes=[]
|
||||
ch1 runes=206 title="" opens="A Novel"
|
||||
=== T3 txt contents list + chapters
|
||||
chapters=1 structure=none notes=[]
|
||||
ch1 runes=277 title="" opens="Contents"
|
||||
=== T4 txt prose opening with Chapter N,
|
||||
chapters=1 structure=none notes=[]
|
||||
ch1 runes=283 title="" opens="Chapter One"
|
||||
|
|
@ -0,0 +1,165 @@
|
|||
=== P1 nav-fragments headings parse
|
||||
chapters=4 structure=combined notes=[targets_collapsed 2 chapters 4 structure combined]
|
||||
ch1 runes=102 title="Foreword" opens="Foreword"
|
||||
ch2 runes=103 title="Chapter 1" opens="Chapter 1"
|
||||
ch3 runes=103 title="Chapter 2" opens="Chapter 2"
|
||||
ch4 runes=103 title="Chapter 3" opens="Chapter 3"
|
||||
=== P2 nav-fragments title headings
|
||||
chapters=2 structure=delimited notes=[targets_collapsed 2 chapters 2 structure delimited]
|
||||
ch1 runes=102 title="Foreword" opens="Foreword"
|
||||
ch2 runes=313 title="The Road" opens="The Road"
|
||||
=== P4 image heading + roman scenes + nameless labels
|
||||
chapters=13 structure=combined notes=[targets_readdressed 2 chapters 13 structure combined]
|
||||
ch1 runes=189 title="Chapter 1" opens="It was a long day and the road was longe…"
|
||||
ch2 runes=96 title="II" opens="II"
|
||||
ch3 runes=189 title="III" opens="III"
|
||||
ch4 runes=95 title="I" opens="I"
|
||||
ch5 runes=96 title="Chapter 2" opens="II"
|
||||
ch6 runes=189 title="III" opens="III"
|
||||
ch7 runes=95 title="I" opens="I"
|
||||
ch8 runes=96 title="II" opens="II"
|
||||
ch9 runes=97 title="Chapter 3" opens="III"
|
||||
ch10 runes=90 title="Chapter 4" opens="It was a long day and the road was longe…"
|
||||
ch11 runes=95 title="I" opens="I"
|
||||
ch12 runes=96 title="II" opens="II"
|
||||
ch13 runes=97 title="III" opens="III"
|
||||
=== P4b titled chapters + roman scenes
|
||||
chapters=16 structure=combined notes=[]
|
||||
ch1 runes=102 title="The Road" opens="The Road"
|
||||
ch2 runes=95 title="I" opens="I"
|
||||
ch3 runes=96 title="II" opens="II"
|
||||
ch4 runes=97 title="III" opens="III"
|
||||
ch5 runes=101 title="The Sea" opens="The Sea"
|
||||
ch6 runes=95 title="I" opens="I"
|
||||
ch7 runes=96 title="II" opens="II"
|
||||
ch8 runes=97 title="III" opens="III"
|
||||
ch9 runes=102 title="The Hill" opens="The Hill"
|
||||
ch10 runes=95 title="I" opens="I"
|
||||
ch11 runes=96 title="II" opens="II"
|
||||
ch12 runes=97 title="III" opens="III"
|
||||
ch13 runes=102 title="The Town" opens="The Town"
|
||||
ch14 runes=95 title="I" opens="I"
|
||||
ch15 runes=96 title="II" opens="II"
|
||||
ch16 runes=97 title="III" opens="III"
|
||||
=== P5 digit headings, labels Chapter N, one poem I-III in ch5
|
||||
chapters=6 structure=combined notes=[documents_attached 3 targets_readdressed 3 chapters 6 structure combined]
|
||||
ch1 runes=762 title="Chapter 4" opens="1"
|
||||
ch2 runes=189 title="Chapter 5" opens="5"
|
||||
ch3 runes=44 title="Chapter 1" opens="I"
|
||||
ch4 runes=46 title="Chapter 2" opens="II"
|
||||
ch5 runes=139 title="Chapter 3" opens="III"
|
||||
ch6 runes=189 title="Chapter 6" opens="6"
|
||||
=== P5b same, labels Chapter <Word>
|
||||
chapters=6 structure=combined notes=[documents_attached 3 targets_readdressed 3 chapters 6 structure combined]
|
||||
ch1 runes=762 title="Chapter Four" opens="1"
|
||||
ch2 runes=189 title="Chapter Five" opens="5"
|
||||
ch3 runes=44 title="Chapter One" opens="I"
|
||||
ch4 runes=46 title="Chapter Two" opens="II"
|
||||
ch5 runes=139 title="Chapter Three" opens="III"
|
||||
ch6 runes=189 title="Chapter Six" opens="6"
|
||||
=== P5c control, labels are titles
|
||||
chapters=9 structure=combined notes=[]
|
||||
ch1 runes=189 title="Title One" opens="1"
|
||||
ch2 runes=189 title="Title Two" opens="2"
|
||||
ch3 runes=189 title="Title Three" opens="3"
|
||||
ch4 runes=189 title="Title Four" opens="4"
|
||||
ch5 runes=189 title="Title Five" opens="5"
|
||||
ch6 runes=44 title="I" opens="I"
|
||||
ch7 runes=46 title="II" opens="II"
|
||||
ch8 runes=139 title="III" opens="III"
|
||||
ch9 runes=189 title="Title Six" opens="6"
|
||||
=== P5d marker headings in most chapters + letter I/II in ch7
|
||||
chapters=8 structure=declared notes=[]
|
||||
ch1 runes=105 title="Chapter 1" opens="CHAPTER ONE"
|
||||
ch2 runes=105 title="Chapter 2" opens="CHAPTER TWO"
|
||||
ch3 runes=107 title="Chapter 3" opens="CHAPTER THREE"
|
||||
ch4 runes=106 title="Chapter 4" opens="CHAPTER FOUR"
|
||||
ch5 runes=106 title="Chapter 5" opens="CHAPTER FIVE"
|
||||
ch6 runes=105 title="Chapter 6" opens="CHAPTER SIX"
|
||||
ch7 runes=245 title="Chapter 7" opens="SEVEN"
|
||||
ch8 runes=99 title="Chapter 8" opens="EIGHT"
|
||||
=== P7 roman in prose, titled NCX
|
||||
chapters=4 structure=declared notes=[]
|
||||
ch1 runes=167 title="The King" opens="The King"
|
||||
ch2 runes=127 title="The Letter" opens="The Letter"
|
||||
ch3 runes=125 title="The List" opens="The List"
|
||||
ch4 runes=200 title="The End" opens="The End"
|
||||
=== P7b titled NCX + one poem I/II
|
||||
chapters=6 structure=combined notes=[]
|
||||
ch1 runes=102 title="The Road" opens="The Road"
|
||||
ch2 runes=101 title="The Sea" opens="The Sea"
|
||||
ch3 runes=102 title="The Hill" opens="The Hill"
|
||||
ch4 runes=20 title="I" opens="I"
|
||||
ch5 runes=115 title="II" opens="II"
|
||||
ch6 runes=102 title="The Town" opens="The Town"
|
||||
=== P8 nested NCX parts→chapters
|
||||
chapters=6 structure=declared notes=[]
|
||||
ch1 runes=8 title="PART ONE" opens="PART ONE"
|
||||
ch2 runes=103 title="Chapter 1" opens="Chapter 1"
|
||||
ch3 runes=103 title="Chapter 2" opens="Chapter 2"
|
||||
ch4 runes=8 title="PART TWO" opens="PART TWO"
|
||||
ch5 runes=103 title="Chapter 3" opens="Chapter 3"
|
||||
ch6 runes=103 title="Chapter 4" opens="Chapter 4"
|
||||
=== P8b nested NCX part on chapter doc
|
||||
chapters=3 structure=delimited notes=[targets_collapsed 1 chapters 3 structure delimited]
|
||||
ch1 runes=103 title="Part One" opens="Chapter 1"
|
||||
ch2 runes=103 title="Chapter 2" opens="Chapter 2"
|
||||
ch3 runes=103 title="Chapter 3" opens="Chapter 3"
|
||||
=== P9 empty spine documents
|
||||
chapters=4 structure=declared notes=[documents_attached 1 chapters 4 structure declared]
|
||||
ch1 runes=103 title="Chapter 1" opens="Chapter 1"
|
||||
ch2 runes=103 title="Chapter 2" opens="Chapter 2"
|
||||
ch3 runes=0 title="Interlude" opens=""
|
||||
ch4 runes=103 title="Chapter 3" opens="Chapter 3"
|
||||
=== P10 repeated names + identity page Interlude
|
||||
chapters=5 structure=declared notes=[]
|
||||
ch1 runes=103 title="Interlude" opens="Interlude"
|
||||
ch2 runes=103 title="Chapter 1" opens="Chapter 1"
|
||||
ch3 runes=103 title="Interlude" opens="Interlude"
|
||||
ch4 runes=103 title="Chapter 2" opens="Chapter 2"
|
||||
ch5 runes=9 title="Interlude" opens="Interlude"
|
||||
=== P11 zh EPUB volumes + restart numbering
|
||||
chapters=6 structure=declared notes=[]
|
||||
ch1 runes=6 title="第一卷 风起" opens="第一卷 风起"
|
||||
ch2 runes=42 title="第一章 开始" opens="第一章 开始"
|
||||
ch3 runes=14 title="第二章 继续" opens="第二章 继续"
|
||||
ch4 runes=6 title="第二卷 云涌" opens="第二卷 云涌"
|
||||
ch5 runes=14 title="第一章 再来" opens="第一章 再来"
|
||||
ch6 runes=14 title="第二章 结束" opens="第二章 结束"
|
||||
=== P11b zh nameless labels, restart
|
||||
chapters=4 structure=declared notes=[]
|
||||
ch1 runes=14 title="第一章" opens="第一章 开始"
|
||||
ch2 runes=14 title="第二章" opens="第二章 继续"
|
||||
ch3 runes=14 title="第一章" opens="第一章 再来"
|
||||
ch4 runes=14 title="第二章" opens="第二章 结束"
|
||||
=== P12 ja ruby, two chapters in doc b
|
||||
chapters=3 structure=combined notes=[]
|
||||
ch1 runes=24 title="第一章 始まり" opens="第一章 始まり"
|
||||
ch2 runes=13 title="第二章 続き" opens="第二章 続き"
|
||||
ch3 runes=17 title="第三章 終わり" opens="第三章 終わり"
|
||||
ruby 漢字(かんじ) ch=1
|
||||
ruby 魔法(まほう) ch=2
|
||||
=== T1 txt BOM+CRLF words
|
||||
chapters=3 structure=detected notes=[]
|
||||
ch1 runes=102 title="Chapter One" opens="Chapter One"
|
||||
ch2 runes=102 title="Chapter Two" opens="Chapter Two"
|
||||
ch3 runes=104 title="Chapter Three" opens="Chapter Three"
|
||||
=== T1b txt BOM+CRLF zh
|
||||
chapters=3 structure=detected notes=[]
|
||||
ch1 runes=10 title="第一章 开始" opens="第一章 开始"
|
||||
ch2 runes=10 title="第二章 继续" opens="第二章 继续"
|
||||
ch3 runes=10 title="第三章 结束" opens="第三章 结束"
|
||||
=== T2 txt stanza numbers in prose
|
||||
chapters=2 structure=detected notes=[]
|
||||
ch1 runes=106 title="I" opens="A Novel"
|
||||
ch2 runes=99 title="II" opens="II"
|
||||
=== T3 txt contents list + chapters
|
||||
chapters=3 structure=detected notes=[]
|
||||
ch1 runes=51 title="Chapter One: The Road" opens="Contents"
|
||||
ch2 runes=112 title="Chapter One: The Road" opens="Chapter One: The Road"
|
||||
ch3 runes=111 title="Chapter Two: The Sea" opens="Chapter Two: The Sea"
|
||||
=== T4 txt prose opening with Chapter N,
|
||||
chapters=3 structure=detected notes=[]
|
||||
ch1 runes=139 title="Chapter One" opens="Chapter One"
|
||||
ch2 runes=40 title="Chapter two, verse five, she read aloud." opens="Chapter two, verse five, she read aloud."
|
||||
ch3 runes=102 title="Chapter Two" opens="Chapter Two"
|
||||
|
|
@ -0,0 +1,437 @@
|
|||
package chunk
|
||||
|
||||
// Acceptance probe (not part of the tree under review): builds synthetic sources with its own zip writer so the
|
||||
// same file runs unchanged on HEAD and on the tree, ingests them through IngestEncoded and prints a summary.
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"textmachine/backend/internal/lang"
|
||||
)
|
||||
|
||||
type hDoc struct {
|
||||
id, body string
|
||||
nonLinear bool
|
||||
}
|
||||
|
||||
type hBook struct {
|
||||
docs []hDoc
|
||||
ncx string // raw navMap inner xml ("" → no NCX)
|
||||
nav string // raw <ol> inner of the toc nav ("" → no nav)
|
||||
}
|
||||
|
||||
func hWrite(t *testing.T, b hBook) string {
|
||||
t.Helper()
|
||||
p := filepath.Join(t.TempDir(), "book.epub")
|
||||
f, err := os.Create(p)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
zw := zip.NewWriter(f)
|
||||
w, _ := zw.CreateHeader(&zip.FileHeader{Name: "mimetype", Method: zip.Store})
|
||||
w.Write([]byte("application/epub+zip"))
|
||||
add := func(name, s string) {
|
||||
w, err := zw.Create(name)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
w.Write([]byte(s))
|
||||
}
|
||||
add("META-INF/container.xml", `<?xml version="1.0"?><container version="1.0" xmlns="urn:oasis:names:tc:opendocument:xmlns:container"><rootfiles><rootfile full-path="OEBPS/content.opf" media-type="application/oebps-package+xml"/></rootfiles></container>`)
|
||||
var man, spine strings.Builder
|
||||
for _, d := range b.docs {
|
||||
man.WriteString(`<item id="` + d.id + `" href="` + d.id + `.xhtml" media-type="application/xhtml+xml"/>`)
|
||||
lin := ""
|
||||
if d.nonLinear {
|
||||
lin = ` linear="no"`
|
||||
}
|
||||
spine.WriteString(`<itemref idref="` + d.id + `"` + lin + `/>`)
|
||||
add("OEBPS/"+d.id+".xhtml", `<?xml version="1.0" encoding="utf-8"?><html xmlns="http://www.w3.org/1999/xhtml"><head><title>t</title></head><body>`+d.body+`</body></html>`)
|
||||
}
|
||||
spineAttr := ""
|
||||
if b.ncx != "" {
|
||||
man.WriteString(`<item id="ncx" href="toc.ncx" media-type="application/x-dtbncx+xml"/>`)
|
||||
spineAttr = ` toc="ncx"`
|
||||
add("OEBPS/toc.ncx", `<?xml version="1.0" encoding="utf-8"?><ncx xmlns="http://www.daisy.org/z3986/2005/ncx/" version="2005-1"><navMap>`+b.ncx+`</navMap></ncx>`)
|
||||
}
|
||||
if b.nav != "" {
|
||||
man.WriteString(`<item id="navdoc" href="nav.xhtml" media-type="application/xhtml+xml" properties="nav"/>`)
|
||||
add("OEBPS/nav.xhtml", `<?xml version="1.0" encoding="utf-8"?><html xmlns="http://www.w3.org/1999/xhtml" xmlns:epub="http://www.idpf.org/2007/ops"><head><title>nav</title></head><body><nav epub:type="toc"><ol>`+b.nav+`</ol></nav></body></html>`)
|
||||
}
|
||||
add("OEBPS/content.opf", `<?xml version="1.0" encoding="utf-8"?><package xmlns="http://www.idpf.org/2007/opf" version="3.0" unique-identifier="uid"><metadata xmlns:dc="http://purl.org/dc/elements/1.1/"><dc:title>T</dc:title></metadata><manifest>`+man.String()+`</manifest><spine`+spineAttr+`>`+spine.String()+`</spine></package>`)
|
||||
if err := zw.Close(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
f.Close()
|
||||
return p
|
||||
}
|
||||
|
||||
func hP(ps ...string) string {
|
||||
var b strings.Builder
|
||||
for _, p := range ps {
|
||||
b.WriteString("<p>" + p + "</p>")
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func hNP(i int, href, label string) string {
|
||||
return fmt.Sprintf(`<navPoint id="n%d"><navLabel><text>%s</text></navLabel><content src="%s"/></navPoint>`, i, label, href)
|
||||
}
|
||||
|
||||
const hProse = "It was a long day and the road was longer, and nobody on it said a word about the weather."
|
||||
|
||||
func hSummary(name string, d *Document, err error) string {
|
||||
var b strings.Builder
|
||||
fmt.Fprintf(&b, "=== %s\n", name)
|
||||
if err != nil {
|
||||
fmt.Fprintf(&b, "ERR %v\n", err)
|
||||
return b.String()
|
||||
}
|
||||
fmt.Fprintf(&b, "chapters=%d structure=%s notes=%v\n", len(d.Chapters), d.Structure, d.IngestNotes())
|
||||
for i, c := range d.Chapters {
|
||||
first := strings.SplitN(c, "\n", 2)[0]
|
||||
if len([]rune(first)) > 40 {
|
||||
first = string([]rune(first)[:40]) + "…"
|
||||
}
|
||||
title := ""
|
||||
if i < len(d.Titles) {
|
||||
title = d.Titles[i]
|
||||
}
|
||||
fmt.Fprintf(&b, " ch%-2d runes=%-5d title=%q opens=%q\n", i+1, len([]rune(c)), title, first)
|
||||
}
|
||||
for _, e := range d.Excluded {
|
||||
fmt.Fprintf(&b, " excluded %s role=%s runes=%d\n", e.Entry, e.Role, e.Runes)
|
||||
}
|
||||
for _, r := range d.Ruby {
|
||||
fmt.Fprintf(&b, " ruby %s(%s) ch=%d\n", r.Base, r.Reading, r.Chapter)
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func hStruct(t *testing.T, lng string) *lang.SourceStructure {
|
||||
st, err := lang.LoadSourceStructure("../../configs/langpacks", lng)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return st
|
||||
}
|
||||
|
||||
func TestHunterProbe(t *testing.T) {
|
||||
out := os.Getenv("HUNTER_OUT")
|
||||
if out == "" {
|
||||
t.Skip("HUNTER_OUT unset")
|
||||
}
|
||||
var all strings.Builder
|
||||
run := func(name, lng string, b hBook) {
|
||||
p := hWrite(t, b)
|
||||
d, err := IngestEncoded(p, "", lng, hStruct(t, lng), nil)
|
||||
all.WriteString(hSummary(name, d, err))
|
||||
}
|
||||
runTXT := func(name, lng, src string) {
|
||||
p := filepath.Join(t.TempDir(), "book.txt")
|
||||
os.WriteFile(p, []byte(src), 0o644)
|
||||
d, err := IngestEncoded(p, "", lng, hStruct(t, lng), nil)
|
||||
all.WriteString(hSummary(name, d, err))
|
||||
}
|
||||
|
||||
// P1: EPUB 3 nav with fragments, three chapters in one document, parseable headings.
|
||||
{
|
||||
body := `<h2 id="c1">Chapter 1</h2>` + hP(hProse) + `<h2 id="c2">Chapter 2</h2>` + hP(hProse) + `<h2 id="c3">Chapter 3</h2>` + hP(hProse)
|
||||
run("P1 nav-fragments headings parse", "en", hBook{
|
||||
docs: []hDoc{{id: "intro", body: hP("Foreword", hProse)}, {id: "ch", body: body}},
|
||||
nav: `<li><a href="intro.xhtml">Foreword</a></li><li><a href="ch.xhtml#c1">Chapter 1</a></li><li><a href="ch.xhtml#c2">Chapter 2</a></li><li><a href="ch.xhtml#c3">Chapter 3</a></li>`,
|
||||
})
|
||||
}
|
||||
// P2: same, headings not parseable (titles).
|
||||
{
|
||||
body := `<h2 id="c1">The Road</h2>` + hP(hProse) + `<h2 id="c2">The Sea</h2>` + hP(hProse) + `<h2 id="c3">The Hill</h2>` + hP(hProse)
|
||||
run("P2 nav-fragments title headings", "en", hBook{
|
||||
docs: []hDoc{{id: "intro", body: hP("Foreword", hProse)}, {id: "ch", body: body}},
|
||||
nav: `<li><a href="intro.xhtml">Foreword</a></li><li><a href="ch.xhtml#c1">The Road</a></li><li><a href="ch.xhtml#c2">The Sea</a></li><li><a href="ch.xhtml#c3">The Hill</a></li>`,
|
||||
})
|
||||
}
|
||||
// P4: heading is an image; labels «Chapter N» without a name; roman scene numbers inside every chapter.
|
||||
{
|
||||
var docs []hDoc
|
||||
var ncx strings.Builder
|
||||
for i := 1; i <= 4; i++ {
|
||||
id := fmt.Sprintf("c%d", i)
|
||||
docs = append(docs, hDoc{id: id, body: `<p><img src="h.png" alt=""/></p>` + hP(hProse, "I", hProse, "II", hProse, "III", hProse)})
|
||||
ncx.WriteString(hNP(i, id+".xhtml", fmt.Sprintf("Chapter %d", i)))
|
||||
}
|
||||
run("P4 image heading + roman scenes + nameless labels", "en", hBook{docs: docs, ncx: ncx.String()})
|
||||
}
|
||||
// P4b: same but empty-paragraph heading and labels that are titles (unparsed).
|
||||
{
|
||||
var docs []hDoc
|
||||
var ncx strings.Builder
|
||||
for i, ttl := range []string{"The Road", "The Sea", "The Hill", "The Town"} {
|
||||
id := fmt.Sprintf("c%d", i+1)
|
||||
docs = append(docs, hDoc{id: id, body: `<p></p>` + hP(ttl, hProse, "I", hProse, "II", hProse, "III", hProse)})
|
||||
ncx.WriteString(hNP(i, id+".xhtml", ttl))
|
||||
}
|
||||
run("P4b titled chapters + roman scenes", "en", hBook{docs: docs, ncx: ncx.String()})
|
||||
}
|
||||
// P5: digit headings (arabic is not bare), labels «Chapter N», ONE poem with stanzas I II III in chapter 5.
|
||||
mkP5 := func(label func(int) string) hBook {
|
||||
var docs []hDoc
|
||||
var ncx strings.Builder
|
||||
for i := 1; i <= 6; i++ {
|
||||
id := fmt.Sprintf("c%d", i)
|
||||
ps := []string{fmt.Sprint(i), hProse, hProse}
|
||||
if i == 5 {
|
||||
ps = append(ps, "I", "Shall I compare thee to a summer's day?", "II", "Thou art more lovely and more temperate.", "III", "Rough winds do shake the darling buds.", hProse)
|
||||
}
|
||||
docs = append(docs, hDoc{id: id, body: hP(ps...)})
|
||||
ncx.WriteString(hNP(i, id+".xhtml", label(i)))
|
||||
}
|
||||
return hBook{docs: docs, ncx: ncx.String()}
|
||||
}
|
||||
run("P5 digit headings, labels Chapter N, one poem I-III in ch5", "en", mkP5(func(i int) string { return fmt.Sprintf("Chapter %d", i) }))
|
||||
words := []string{"", "One", "Two", "Three", "Four", "Five", "Six"}
|
||||
run("P5b same, labels Chapter <Word>", "en", mkP5(func(i int) string { return "Chapter " + words[i] }))
|
||||
run("P5c control, labels are titles", "en", mkP5(func(i int) string { return fmt.Sprintf("Title %s", words[i]) }))
|
||||
// P5d: labels «Chapter N», one chapter holds a numbered two-point letter «I»/«II».
|
||||
{
|
||||
var docs []hDoc
|
||||
var ncx strings.Builder
|
||||
for i := 1; i <= 8; i++ {
|
||||
id := fmt.Sprintf("c%d", i)
|
||||
ps := []string{"CHAPTER " + strings.ToUpper(words[min(i, 6)]), hProse}
|
||||
if i == 7 {
|
||||
ps = []string{"SEVEN", hProse, "I", "First, the money.", "II", "Second, the key.", hProse}
|
||||
}
|
||||
if i == 8 {
|
||||
ps = []string{"EIGHT", hProse}
|
||||
}
|
||||
docs = append(docs, hDoc{id: id, body: hP(ps...)})
|
||||
ncx.WriteString(hNP(i, id+".xhtml", fmt.Sprintf("Chapter %d", i)))
|
||||
}
|
||||
run("P5d marker headings in most chapters + letter I/II in ch7", "en", hBook{docs: docs, ncx: ncx.String()})
|
||||
}
|
||||
// P7: roman numerals that are part of prose; NCX 1:1 with titles.
|
||||
{
|
||||
var docs []hDoc
|
||||
var ncx strings.Builder
|
||||
ps := [][]string{
|
||||
{"The King", "Henry V was not a patient man.", "Part II of the plan failed.", hProse},
|
||||
{"The Letter", "Dear M,", hProse, "Yours, X"},
|
||||
{"The List", "MIX", "CIVIL", "DID", hProse},
|
||||
{"The End", hProse, "V", hProse},
|
||||
}
|
||||
for i, p := range ps {
|
||||
id := fmt.Sprintf("c%d", i+1)
|
||||
docs = append(docs, hDoc{id: id, body: hP(p...)})
|
||||
ncx.WriteString(hNP(i, id+".xhtml", p[0]))
|
||||
}
|
||||
run("P7 roman in prose, titled NCX", "en", hBook{docs: docs, ncx: ncx.String()})
|
||||
}
|
||||
// P7b: titled NCX 1:1, one chapter has a poem with stanzas I, II.
|
||||
{
|
||||
var docs []hDoc
|
||||
var ncx strings.Builder
|
||||
for i, ttl := range []string{"The Road", "The Sea", "The Hill", "The Town"} {
|
||||
id := fmt.Sprintf("c%d", i+1)
|
||||
ps := []string{ttl, hProse}
|
||||
if i == 2 {
|
||||
ps = append(ps, "I", "Verse one line.", "II", "Verse two line.", hProse)
|
||||
}
|
||||
docs = append(docs, hDoc{id: id, body: hP(ps...)})
|
||||
ncx.WriteString(hNP(i, id+".xhtml", ttl))
|
||||
}
|
||||
run("P7b titled NCX + one poem I/II", "en", hBook{docs: docs, ncx: ncx.String()})
|
||||
}
|
||||
// P8: nested NCX (parts → chapters), part pages are identity pages, chapters have headings.
|
||||
{
|
||||
docs := []hDoc{
|
||||
{id: "p1", body: hP("PART ONE")},
|
||||
{id: "c1", body: hP("Chapter 1", hProse)},
|
||||
{id: "c2", body: hP("Chapter 2", hProse)},
|
||||
{id: "p2", body: hP("PART TWO")},
|
||||
{id: "c3", body: hP("Chapter 3", hProse)},
|
||||
{id: "c4", body: hP("Chapter 4", hProse)},
|
||||
}
|
||||
ncx := `<navPoint id="a"><navLabel><text>PART ONE</text></navLabel><content src="p1.xhtml"/>` + hNP(1, "c1.xhtml", "Chapter 1") + hNP(2, "c2.xhtml", "Chapter 2") + `</navPoint>` +
|
||||
`<navPoint id="b"><navLabel><text>PART TWO</text></navLabel><content src="p2.xhtml"/>` + hNP(3, "c3.xhtml", "Chapter 3") + hNP(4, "c4.xhtml", "Chapter 4") + `</navPoint>`
|
||||
run("P8 nested NCX parts→chapters", "en", hBook{docs: docs, ncx: ncx})
|
||||
}
|
||||
// P8b: nested NCX where the part points at the first chapter's document (no part page).
|
||||
{
|
||||
docs := []hDoc{
|
||||
{id: "c1", body: hP("Chapter 1", hProse)},
|
||||
{id: "c2", body: hP("Chapter 2", hProse)},
|
||||
{id: "c3", body: hP("Chapter 3", hProse)},
|
||||
}
|
||||
ncx := `<navPoint id="a"><navLabel><text>Part One</text></navLabel><content src="c1.xhtml"/>` + hNP(1, "c1.xhtml", "Chapter 1") + hNP(2, "c2.xhtml", "Chapter 2") + `</navPoint>` + hNP(3, "c3.xhtml", "Chapter 3")
|
||||
run("P8b nested NCX part on chapter doc", "en", hBook{docs: docs, ncx: ncx})
|
||||
}
|
||||
// P9: empty spine documents: an untargeted empty doc between chapters, a targeted empty doc.
|
||||
{
|
||||
docs := []hDoc{
|
||||
{id: "c1", body: hP("Chapter 1", hProse)},
|
||||
{id: "e1", body: ``},
|
||||
{id: "c2", body: hP("Chapter 2", hProse)},
|
||||
{id: "e2", body: `<p></p>`},
|
||||
{id: "c3", body: hP("Chapter 3", hProse)},
|
||||
}
|
||||
ncx := hNP(1, "c1.xhtml", "Chapter 1") + hNP(2, "c2.xhtml", "Chapter 2") + hNP(3, "e2.xhtml", "Interlude") + hNP(4, "c3.xhtml", "Chapter 3")
|
||||
run("P9 empty spine documents", "en", hBook{docs: docs, ncx: ncx})
|
||||
}
|
||||
// P10: repeated chapter names (unparsed labels repeated).
|
||||
{
|
||||
docs := []hDoc{
|
||||
{id: "a", body: hP("Interlude", hProse)},
|
||||
{id: "b", body: hP("Chapter 1", hProse)},
|
||||
{id: "c", body: hP("Interlude", hProse)},
|
||||
{id: "d", body: hP("Chapter 2", hProse)},
|
||||
{id: "e", body: hP("Interlude")},
|
||||
}
|
||||
ncx := hNP(1, "a.xhtml", "Interlude") + hNP(2, "b.xhtml", "Chapter 1") + hNP(3, "c.xhtml", "Interlude") + hNP(4, "d.xhtml", "Chapter 2") + hNP(5, "e.xhtml", "Interlude")
|
||||
run("P10 repeated names + identity page Interlude", "en", hBook{docs: docs, ncx: ncx})
|
||||
}
|
||||
// P11: zh EPUB (script without case/spaces): NCX labels 第N章, volume pages, sections 第N节 inside chapters.
|
||||
{
|
||||
docs := []hDoc{
|
||||
{id: "v1", body: hP("第一卷 风起")},
|
||||
{id: "c1", body: hP("第一章 开始", "正文一。", "第一节", "正文。", "第二节", "正文。")},
|
||||
{id: "c2", body: hP("第二章 继续", "正文二。")},
|
||||
{id: "v2", body: hP("第二卷 云涌")},
|
||||
{id: "c3", body: hP("第一章 再来", "正文三。")},
|
||||
{id: "c4", body: hP("第二章 结束", "正文四。")},
|
||||
}
|
||||
ncx := hNP(1, "v1.xhtml", "第一卷 风起") + hNP(2, "c1.xhtml", "第一章 开始") + hNP(3, "c2.xhtml", "第二章 继续") + hNP(4, "v2.xhtml", "第二卷 云涌") + hNP(5, "c3.xhtml", "第一章 再来") + hNP(6, "c4.xhtml", "第二章 结束")
|
||||
run("P11 zh EPUB volumes + restart numbering", "zh", hBook{docs: docs, ncx: ncx})
|
||||
}
|
||||
// P11b: zh EPUB, nameless labels (第N章 only), numbering restarts per volume, headings in prose with titles.
|
||||
{
|
||||
docs := []hDoc{
|
||||
{id: "c1", body: hP("第一章 开始", "正文一。")},
|
||||
{id: "c2", body: hP("第二章 继续", "正文二。")},
|
||||
{id: "c3", body: hP("第一章 再来", "正文三。")},
|
||||
{id: "c4", body: hP("第二章 结束", "正文四。")},
|
||||
}
|
||||
ncx := hNP(1, "c1.xhtml", "第一章") + hNP(2, "c2.xhtml", "第二章") + hNP(3, "c3.xhtml", "第一章") + hNP(4, "c4.xhtml", "第二章")
|
||||
run("P11b zh nameless labels, restart", "zh", hBook{docs: docs, ncx: ncx})
|
||||
}
|
||||
// P12: ja EPUB with ruby, two chapters in one document (ruby in the second).
|
||||
{
|
||||
docs := []hDoc{
|
||||
{id: "a", body: hP("第一章 始まり", "本文。", `<ruby>漢字<rt>かんじ</rt></ruby>がある。`)},
|
||||
{id: "b", body: hP("第二章 続き", "本文。", "第三章 終わり", `<ruby>魔法<rt>まほう</rt></ruby>の本文。`)},
|
||||
}
|
||||
ncx := hNP(1, "a.xhtml", "第一章 始まり") + hNP(2, "b.xhtml", "第二章 続き")
|
||||
run("P12 ja ruby, two chapters in doc b", "ja", hBook{docs: docs, ncx: ncx})
|
||||
}
|
||||
// T1: txt with BOM and CRLF, English words headings.
|
||||
runTXT("T1 txt BOM+CRLF words", "en", "\ufeffChapter One\r\n"+hProse+"\r\n\r\nChapter Two\r\n"+hProse+"\r\n\r\nChapter Three\r\n"+hProse+"\r\n")
|
||||
runTXT("T1b txt BOM+CRLF zh", "zh", "\ufeff第一章 开始\r\n正文。\r\n第二章 继续\r\n正文。\r\n第三章 结束\r\n正文。\r\n")
|
||||
// T2: txt, roman in prose lines.
|
||||
runTXT("T2 txt stanza numbers in prose", "en", "A Novel\n"+hProse+"\nI\nverse\nII\nverse\n"+hProse+"\n")
|
||||
// T3: txt with a contents list at the top and chapters with Title-only headings of the marker form.
|
||||
runTXT("T3 txt contents list + chapters", "en", "Contents\nChapter One: The Road\nChapter Two: The Sea\n\nChapter One: The Road\n"+hProse+"\nChapter Two: The Sea\n"+hProse+"\n")
|
||||
// T4: dialogue that opens like a header.
|
||||
runTXT("T4 txt prose opening with Chapter N,", "en", "Chapter One\n"+hProse+"\n“Chapter two, verse five,” she said.\nChapter two, verse five, she read aloud.\nChapter Two\n"+hProse+"\n")
|
||||
|
||||
if err := os.WriteFile(out, []byte(all.String()), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHunterProbe2(t *testing.T) {
|
||||
out := os.Getenv("HUNTER_OUT2")
|
||||
if out == "" {
|
||||
t.Skip("HUNTER_OUT2 unset")
|
||||
}
|
||||
var all strings.Builder
|
||||
run := func(name, lng string, b hBook) {
|
||||
p := hWrite(t, b)
|
||||
d, err := IngestEncoded(p, "", lng, hStruct(t, lng), nil)
|
||||
all.WriteString(hSummary(name, d, err))
|
||||
}
|
||||
runTXT := func(name, lng, src string) {
|
||||
p := filepath.Join(t.TempDir(), "book.txt")
|
||||
os.WriteFile(p, []byte(src), 0o644)
|
||||
d, err := IngestEncoded(p, "", lng, hStruct(t, lng), nil)
|
||||
all.WriteString(hSummary(name, d, err))
|
||||
}
|
||||
// Q1: zh EPUB, titled labels (not parseable), 第N节 sections inside every chapter document.
|
||||
{
|
||||
var docs []hDoc
|
||||
var ncx strings.Builder
|
||||
for i, ttl := range []string{"风起", "云涌", "雷动"} {
|
||||
id := fmt.Sprintf("c%d", i+1)
|
||||
docs = append(docs, hDoc{id: id, body: hP(ttl, "正文开始。", "第一节", "正文。", "第二节", "正文。")})
|
||||
ncx.WriteString(hNP(i, id+".xhtml", ttl))
|
||||
}
|
||||
run("Q1 zh titled labels + 第N节 sections", "zh", hBook{docs: docs, ncx: ncx.String()})
|
||||
}
|
||||
// Q2: zh txt, contents list right before chapter one (blank lines only).
|
||||
runTXT("Q2 zh txt contents list adjacent", "zh", "目录\n第一章 开始\n第二章 继续\n\n\n第一章 开始\n正文一。\n第二章 继续\n正文二。\n")
|
||||
// Q3: en txt, prose sentence opening with «Chapter <word>» and a space.
|
||||
runTXT("Q3 en txt prose sentence", "en", "Chapter One\n"+hProse+"\nChapter three was about her mother and the farm.\n"+hProse+"\nChapter Two\n"+hProse+"\n")
|
||||
// Q4: en EPUB, 1:1 NCX titled, contents list in the same document as chapter one.
|
||||
{
|
||||
docs := []hDoc{
|
||||
{id: "a", body: hP("Contents", "Chapter One: The Road", "Chapter Two: The Sea", "Chapter One: The Road", hProse)},
|
||||
{id: "b", body: hP("Chapter Two: The Sea", hProse)},
|
||||
}
|
||||
ncx := hNP(1, "a.xhtml", "Chapter One: The Road") + hNP(2, "b.xhtml", "Chapter Two: The Sea")
|
||||
run("Q4 en EPUB contents list shares chapter one's document", "en", hBook{docs: docs, ncx: ncx})
|
||||
}
|
||||
// Q5: en EPUB without a table: chapters are documents with image headings, roman scenes inside.
|
||||
{
|
||||
var docs []hDoc
|
||||
for i := 1; i <= 3; i++ {
|
||||
docs = append(docs, hDoc{id: fmt.Sprintf("c%d", i), body: `<p><img src="h.png"/></p>` + hP(hProse, "I", hProse, "II", hProse)})
|
||||
}
|
||||
run("Q5 en EPUB no table, roman scenes", "en", hBook{docs: docs})
|
||||
}
|
||||
for _, f := range strings.Split(os.Getenv("HUNTER_BOOKS"), ",") {
|
||||
if f == "" {
|
||||
continue
|
||||
}
|
||||
parts := strings.SplitN(f, "=", 2)
|
||||
d, err := IngestEncoded(parts[1], "", parts[0], hStruct(t, parts[0]), nil)
|
||||
s := hSummary(parts[1], d, err)
|
||||
lines := strings.Split(s, "\n")
|
||||
all.WriteString(strings.Join(lines[:min(len(lines), 12)], "\n") + "\n")
|
||||
}
|
||||
if err := os.WriteFile(out, []byte(all.String()), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHunterProbe3Italics(t *testing.T) {
|
||||
if os.Getenv("HUNTER_OUT3") == "" {
|
||||
t.Skip("unset")
|
||||
}
|
||||
im := &lang.InlineMarkup{ItalicOpen: "*", ItalicClose: "*"}
|
||||
mk := func(withFragment bool) hBook {
|
||||
docs := []hDoc{
|
||||
{id: "c1", body: hP("I", "THE ROAD", "It was a <i>very</i> long day.")},
|
||||
{id: "c2", body: hP("II", "THE SEA", hProse)},
|
||||
}
|
||||
ncx := hNP(1, "c1.xhtml", "Chapter I. The Road")
|
||||
if withFragment {
|
||||
docs = append(docs, hDoc{id: "f2", body: `<p><i>Chapter II. The Sea</i></p>`})
|
||||
ncx += hNP(2, "f2.xhtml", "Chapter II. The Sea")
|
||||
} else {
|
||||
ncx += hNP(2, "c2.xhtml", "Chapter II. The Sea")
|
||||
}
|
||||
return hBook{docs: docs, ncx: ncx}
|
||||
}
|
||||
var b strings.Builder
|
||||
for _, wf := range []bool{false, true} {
|
||||
d, err := IngestEncoded(hWrite(t, mk(wf)), "", "en", hStruct(t, "en"), im)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
fmt.Fprintf(&b, "fragment=%v chapters=%d excluded=%v italic_carriers=%d text_markers=%d\n", wf, len(d.Chapters), d.Excluded, d.ItalicCarriers, strings.Count(strings.Join(d.Chapters, "\n"), "*"))
|
||||
}
|
||||
os.WriteFile(os.Getenv("HUNTER_OUT3"), []byte(b.String()), 0o644)
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue