textmachine/backend/docs/CHAPTER_STRUCTURE_REPORT.md

526 lines
47 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Отчёт: пак «ФОРМАТЫ И ЧЕСТНЫЙ СЛОВАРЬ»
Сессия `textmachine-main-fe`, 05.09. Промт `docs/CHAPTER_STRUCTURE_SESSION_PROMPT.md` (выдан `dba8a0b`,
поправлен по находкам сессии: `24f02bb` · `23b00a3` · `5a502e7` · `cd55329`). **Денег потрачено $0**, платных
вызовов ноль: весь пак — путь `tmctl manifest` и тесты на фейковом провайдере.
---
## 1. ЧИСЛА
### Батарея
| | команда | EXIT | пакетов | FAIL | линтер | скипы |
|---|---|---|---|---|---|---|
| до правок | `make battery` (одной командой) | 0 | 19 | 0 | 0 issues | **3** |
| после | ⚠ ПО СТАДИЯМ — см. §11 | 0 | 19 | 0 | 0 issues | **3** |
**Строка «после» — НЕ `make battery`.** Одной командой она не прошла: система дважды убила её по нехватке
памяти. Прогон сделан по стадиям, команды перечислены в §11. Первая редакция этой таблицы писала `make
battery` в обеих строках и противоречила §11 того же отчёта.
Скипы поимённо и в обоих прогонах те же: `TestMinerFullBookParity` (корпусный, снимается `battery-stand`),
`TestHelperEventsRun`, `TestHelperKillLoop` (хелперы подпроцессов). **Новых скипов не появилось** — это
проверялось отдельно, потому что скип читается как зелень.
### Каталог мутаций
201 → **211 записей** (первая редакция говорила 205 — это было состояние ПЕРВОГО круга, до шести записей дофикса). Шесть прогнаны исполнением на КОПИИ дерева (`~/tm-mut-0509`, `GOFLAGS=-buildvcs=false`),
**6 RED, 0 неожиданных исходов**:
| запись | покрасневший тест |
|---|---|
| `DF-competition-order-loses-a-chapter` | `TestTXTHeaderRightAfterAFormFeedIsNotLost` |
| `EPUB-preamble-joins-chapter-one` | `TestEPUBUncoveredBeforeFirstTargetJoinsChapterOne` |
| `EPUB-service-pages-are-exactly-two-roles` | `TestEPUB3LandmarksExcludeTocAndCover`, `TestEPUB2GuideExcludesTocAndCover` |
| `PP-structure-provenance` (пере-нацелена) | `TestTheCutSaysWhetherItWasReadOrGuessed` + два подтеста |
| `TITLE-raw-follows-the-kept-index` | `TestTitleRawSurvivesAChapterThatTakesNoNumber` |
| `EPUB-collapsed-targets-downgrade-the-provenance` | `TestEPUBDuplicateNavTargetsCollapse` |
⚠ Полный прогон всех 205 записей НЕ делался — см. §6.
### Замеры шага 0
| # | замер | число |
|---|---|---|
| 1 | `\f` в стенд-книгах | **0** во всех шести файлах |
| 2 | главы-огрызки от print-TOC в 蛊真人 | **0** (мин. глава 1804 знака, медиана 3289) |
| 3 | сброс нумерации по томам | **есть**: 2283 шапки → 945 уникальных номеров, кратность до 6 |
| 4 | шапки `第N节` | ровно **2283**; 章/節/回/話 — по нулю |
| 5 | `話` в 蛊真人 | **0** вхождений вообще |
| 6 | преамбула до первой шапки | 3130 знаков |
| 7 | `declared` во всём стенде до пака | **0** (семь `detected`, один `none`) |
Замер 1 значит, что конкуренция §2.4 физически не могла сдвинуть разрез боевой книги. Замер 5 — что сведение
чанкера к пяти юнитам не тронуло ни одного её чанка.
### Счета формата
`form feeds ignored`, `documents attached`, `documents excluded`, `toc unresolved`, `targets collapsed`,
`service roles inside documents` — ШЕСТЬ счётов — живут в `chunk.Document`,
уезжают в WARN (`events.go`, `ingestSource`) и **НЕ на провод** — потребителя у них сегодня нет.
---
## 2. ГЕЙТ ЛЕНДИНГА — ВЕТКА (а), ВОСЕМЬ ИЗ ВОСЬМИ
Гейт сверяет **id глав и номера** (ратифицировано находкой 4). Пины сняты до правок и после, тем же
прибором, конфигами сессии в `~/tm-pins-0509`.
| исходник | sha256[:16] | глав | structure | разрез |
|---|---|---|---|---|
| `ch1` | `eda401ef10edbf40` | 1 | none | байт-в-байт |
| `ch1-2` (= `coldrun-b`) | `eb87729061be6d39` | 2 | detected | байт-в-байт |
| `ch4-5` | `33dc2b5b7e491435` | 2 | detected | байт-в-байт |
| `ch1-5` | `8755afd3e6aab298` | 5 | detected | байт-в-байт |
| `s11-16` | `5c22ec0ddaf19bb0` | 6 | detected | байт-в-байт |
| `ch1-10` | `0b5f9b0266d8c32a` | 10 | detected | байт-в-байт |
| `ch26-42` | `1ebcc9efb0fa5006` | 17 | detected | байт-в-байт |
| `slice25` | `78f4cb7d990f5e1a` | 25 | detected | байт-в-байт |
**Двинулось ровно заказанное** (пример `ch1-10`): `key` `1e62a68d93f73534…``01a00e3669302bc1…`;
unit-id главы 1 `c3aa4cbbb53de134:d4fb75e2:0``c3aa4cbbb53de134:1b30b1dd:0` — **префикс id главы
неподвижен, уехал сегмент `cutTag`**. Это и есть §2.2, исполненный буквально.
**Носитель гейта — восемь ИСХОДНИКОВ, а не 31 конфиг**, и это не сужение: разрез глав есть функция байтов
исходника, кодировки и данных структуры. Проверено ИСПОЛНЕНИЕМ до правок: тот же исходник под другим
конфигом (другой пайплайн, другой `book_id`) дал идентичные id и номера глав, разошлись только `key` и
`cutTag`.
---
## 3. ЧТО ПОСТРОЕНО
1. **`internal/lang/structure.go`** — данные структуры ИСТОЧНИКА: тип `SourceStructure`, загрузчик
`LoadSourceStructure` с лестницей (`<root>/<язык>/structure.txt` → CJK-дефолт по скрипту → nil),
отпечаток для тега разреза. Загрузчик отдельный от пар-пака и достигает книги БЕЗ него.
**Файл `zh/structure.txt` НЕ создан намеренно:** он дублировал бы `cjk-section.txt` и завёл бы ровно тот
второй носитель, который пак закрывает. Механизм при этом работает: новая пара кладёт свой файл, Go не
правится.
2. **Живой дрейф закрыт.** Было: ингест — пять юнитов из `cjk-section.txt`, чанкер — четыре из
`zh-ru/heading.txt`. Стало: одно резолвленное значение на обоих путях. `heading.txt` оставил только
`template`; `parseHeading` отвергает `marker`/`units` громко, называя, куда уехал факт.
`cjk-section.txt` НЕ тронут ни одной строкой.
3. **Компонент `Structure`** в `cutInputs` и в payload `manifestKey`, без `omitempty`.
4. **Конкуренция свидетелей** и значение `delimited`. Порядок ратифицированный: `\f``\n\n` на рабочей
копии → детекция по копии → откат на ОРИГИНАЛ, если шапки проиграли.
5. **EPUB `nav`/NCX** (`internal/chunk/epubtoc.go`): приоритет nav → NCX → spine; сплющивание вложенных
`ol`; схлопывание дублей; цели вне spine — игнор со счётом; nav с нулём разрешимых целей = отсутствует;
`linear="no"` не открывает главу; служебные страницы исключаются по `properties="nav"`, landmarks (EPUB 3)
И OPF `<guide>` (EPUB 2); непокрытые до первой цели — в главу 1, прочие — в предыдущую.
6.**Схлопнутые цели ПОНИЖАЮТ провенанс.** Книга, пакующая несколько глав в один документ с якорями,
объявляет больше глав, чем пак отдаёт: он группирует документы целиком и по якорям не режет. Назвать такой
разрез `declared` значило бы выдать своё, более грубое, за утверждение формата — та же ложь, что говорил
spine, в новом месте. Поэтому при `targets collapsed > 0` книга идёт как **`delimited`**, со счётом в WARN.
Четырёхзначного словаря для этого хватило: достраивать ничего не пришлось.
⚠ Резка xhtml по якорям в пак НЕ входит — отдельный заказ; цена названа: текст цел, гранулярность
потеряна.
7. **`title_raw`** аддитивно: строка шапки как в источнике (txt) / `navLabel` (EPUB). Понижение подзаголовка
не тронуто.
8. **Бамп `chunkerVersion`** v6 → v7 и пере-захват голдена — один раз, последним действием.
### Ревью-вопрос проекта — закрыт исполнением
«Заработает ли пара, которой в репозитории НЕТ, без правки Go?» — **да**, и это тест, а не утверждение.
`TestANewLanguageCutsFromItsDataFileAlone` (`internal/chunk`) проверяет ОБЕ половины на одном тексте и одном
бинаре: корейская книга без своего файла остаётся **одной** главой (CJK-дефолт даёт маркер 第, которого в
корейском нет), а с `ko/structure.txt`, объявляющим 제 и 장, режется на **три**, отдаёт `detected` и несёт
`title_raw` = `제2장 전개`. Единственная разница между половинами — файл данных.
Половина на уровне загрузчика — `TestALanguageAbsentFromTheRepositoryCutsFromDataAlone` (`internal/lang`),
она же пинует, что отпечаток берётся из ФАЙЛА, а не из дефолта: иначе правка чужой грамматики не пере-резала
бы книгу.
**ГРАНИЦА, КОТОРУЮ ЭТОТ ЖЕ ТЕСТ ВСКРЫЛ, и она честнее ответа «да».** Ингест режет только по грамматике
С ЮНИТОМ: `detectChapterUnit` итерирует `UnitOrdered`, и язык без юнита («Kapitel 12») сегодня НЕ режется —
хотя ЧАНКЕР такую грамматику уже умеет, и на это есть живой тест `TestStripHeadingGenericLatinMarker`
(`matchHeaderLine` юнита не требует). То есть половина не-CJK пути готова, а половина нет, и данные это
выразить могут, а детектор — нет. Не достраивал: §2.2 прямо говорит «римские/словесные читатели НЕ
строятся», объём читателей в паке 1 — ровно существующий. **Ревью-вопрос закрыт для языков С юнитом и
открыт для языков без него** — это кандидат в строку бэклога, не тихий пробел.
---
## 4. ОБЪЯВЛЕНИЯ ПО D39.183
Правки тестов, вызванные ЗАКАЗАННОЙ сменой поведения:
- `ingest_test.go` — кейс `"the format drew every boundary"` (`ГЛАВА A\fГЛАВА B`) ждал `StructureDeclared`,
стал `delimited`. **Единственный переехавший ассерт.** Сам тест переименован:
`TestTXTStructureIsDeclaredOnlyWhenTheFormatDrewEVERYBoundary``TestTXTProvenanceNamesTheWinningPath`
прежнее имя после конкуренции ни во что не разрешается (класс строки 299).
- `priceprojection_test.go` — кейс `"a form feed is the format speaking"` → `"a form feed is a separator, not
a chapter"`, ожидание `Declared` → `Delimited`.
- `ingest_encoding_test.go` — счёт глав сохранён (шапки победили), поправлен комментарий и текст ошибки:
разрез там больше не по `\f`.
- `chunker_heading_test.go` — `zhRuHeadingRule()` → `zhRuChapterRule()`, инвентарь больше не переписан
руками, а берётся из `DefaultCJKStructure()` (это и есть закрытие дрейфа).
- Механические правки сигнатур (`IngestEncoded` + структура, `SplitChunksWithChapters` + `keptIdx`,
`denseChapters`) в `bookbuild_test.go`, `contractblockers_test.go`, `chunker_manifest_test.go`,
`cuttag_test.go`.
- **Каталог:** `DF-declared-means-every-boundary` целила в предикат V2-7, который §1 приказал УДАЛИТЬ.
Заменена на `DF-competition-order-loses-a-chapter` — свойство, занявшее его место. `PP-structure-provenance`
пере-нацелена на новый `structureOf`. Обе прогнаны красными.
**Гарантия никуда не «уехала»:** свойство «стрей `\f` среди шапок не делает разрез `declared`» стало
недостижимым ПО ПОСТРОЕНИЮ (смешанного случая нет), а на его место встало свойство порядка операций, у
которого теперь есть и тест, и запись каталога.
---
## 5. НАХОДКИ ДО КОДА (все приняты оркестратором и внесены в промт)
1. **§2.4 заказывал цикл.** Решить «есть ли шапки» нельзя до нейтрализации `\f`: шапка сразу за ним лежит на
ОДНОЙ строке с прозой предыдущей главы (204 руны, начинается с 蛊) и отвергается двумя предикатами —
`ingest.go` длина > `chapterHeaderMaxRunes` и якорь `^`. Дороже цикла то, что **существующий тест этого
не ловил**: он проверял только `doc.Structure`, а ярлык при наивном порядке остаётся ВЕРНЫМ.
2. **Премисса «дрейф закрыл pair-14» неверна** — дрейф был живой: 章节節回話 против 章节節回.
3. **Риск красного незаказанного теста** `TestChapterUnitJaWebnovel` при сборке дефолта из пар-файла.
4. **Гейт был недостижим по построению:** требовал байт-в-байт манифест, а пак сам заказывает сдвиг
`cutTag`, перечеканивающий все unit-id (`manifest.go`, `manifestUnitID`).
5. **Пар-пак в чужой ветке:** `langpack_root` 20 конфигов из 31 указывает в рабочее дерево ветки `polygon`.
6. **Стенд отстал от движка:** стенд-конфиг даёт манифест ровно для 1 исходника из 8 (`{{genre}}` ретирован
и живёт в шести промтах polygon; `mined_delta`/`mined_rejects` ретированы и стоят в 17 конфигах из 31;
поля `stm_depth`/`overlap_tokens` исчезли).
### Находки о СВОЁМ коде, пойманные собственными тестами
- **`Marker` был одновременно классом рун и литеральным префиксом** — тот же дрейф в новой форме. «Chapter»
сортировался в набор букв. Исправлено моделью альтернатив + `regexp.QuoteMeta`.
- **Файловый парсер был СТРОЖЕ движка**: требовал `units`, хотя безъюнитная грамматика — ровно не-CJK случай,
ради которого пак и делается.
- **Группировка не приклеивала преамбулу**, а открывала ей отдельную главу.
- **Граница главы выводилась «первый или последний элемент группы»** — хрупко; сделана явным полем.
### Выжившая посадка, записанная с аргументом
Переворот сортировки `starts` в `resolveTOC` **не покрасил ничего**. Причина: `starts` читается как
МНОЖЕСТВО, группировка идёт по spine, и порядок целей на выход не влияет. Сортировка оставлена (она держит
истинным объявленный порядок поля), но комментарий теперь говорит это прямо, а не утверждает, что цели
«упорядочены по позиции в spine» как о несущем свойстве.
---
## 6. ЧТО НЕ ПРОВЕРЕНО И ПОЧЕМУ
- **Полный каталог (211 записей) не гонялся.** Прогнаны 14 записей, целящих в изменённое поведение. Причина —
цена: 124 записи гоняют `./internal/pipeline/`, чей набор идёт ~145 с, то есть полный прогон — часы при
загруженном хосте. Механически проверено другое и дешёвое: **у всех 211 записей цель ЖИВА** (греп `find`
по дереву), то есть протухших целей после пака не осталось.
- ⚠ **Эта строка УСТАРЕЛА и оставлена как след:** на момент первой редакции EPUB владельца через новый путь
не прогонялись. Они прогнаны позже — §8 и §10 несут их числа. Строку не удаляю, потому что она показывает,
в каком порядке появлялось знание.
- **Платная книга платформы** не трогалась — чужая зона.
- **Сквозного прогона не-CJK нет** — промтов пары для en/ja не существует (§5), проверка ограничена
уровнем ingest/manifest, $0.
- **`counts.py --lint`** не запускался — гейт оркестратора, доков с якорями я не писал.
- **Полная 蛊真人 (2283 главы)** через новый путь не прогонялась: у неё нет конфига, а её результат по §2.1
идёт числом в отчёт и гейтом не является.
---
## 7. ФИКСТУРА `delimited` ДЛЯ ПЛАТФОРМЫ (§6)
`~/tm-pins-0509/delimited/manifest.json` — реальный манифест, снятый движком:
`"structure": "delimited"`, три главы, `title_raw: ""` у каждой (источник заголовков не называет).
Источник — кириллический txt с двумя `\f` и без единой шапки: шапки проигрывают, побеждает разрыв страницы.
⚠ Фикстура лежит ВНЕ репозитория. Если она нужна платформенному тесту как файл — это решение оркестратора,
куда её положить: `platform/` мне не зона.
---
## 8. ДОФИКС ПО ПРИЁМКЕ (06.09)
Две линзы приёмки нашли шесть обязательных и пять сопутствующих. **Все проверены мной заново перед починкой
и все подтвердились** — ни одна не отклонена.
### Обязательные
| # | что было | чем закрыто |
|---|---|---|
| M-1 | роль `toc`/`cover`, объявленная С ФРАГМЕНТОМ (`c2.xhtml#pos`), исключала ВЕСЬ документ ⇒ молчаливая потеря прозы | объявление на ТОЧКЕ внутри документа больше не исключает файл; отказ считается (`ServiceRolesInsideDocuments`). Тесты `TestEPUBServiceRoleWithAFragmentDoesNotDeleteTheDocument` + обратный `…WholeDocumentServiceRoleStillExcludes` |
| M-2 | три счёта из пяти не доходили никуда; комментарий обещал WARN | ОДИН репортёр `Document.IngestNotes()` — и рефлексионный сторож `TestEveryIngestCountReachesTheAlarm`, который краснеет на любом новом неподключённом счётчике |
| M-3 | `manifestKey` не пинован ничем | `TestTheManifestKeyCarriesTheStructureGrammar`; проверен ровно мутацией верификатора |
| M-4 | EPUB-половина `title_raw` не покрыта; метки фикстур вырождены (`e`/`p` всем) | метки стали различимыми и НАЗЫВАЮТ свой источник (`NAV …`/`NCX …`); три теста, включая «заголовок берётся у ГРАНИЦЫ, а не у вклеенной впереди страницы» |
| M-5 | `unresolved` рос и на прекрасно разрешившихся служебных/нелинейных целях ⇒ любой обычный EPUB 2 отчитывался «сломанным оглавлением» | счётчик считает ровно то, что обещает; `TestEPUBOrdinaryBookDoesNotReportABrokenTOC` |
| M-6 | `mutations.json` переформатирован целиком (дифф 5080 строк) | отступ возвращён к 1; **дифф стал 62 строки**, правка видна |
### Сопутствующие
`m-5` — комментарий `bookfile/epub.go` был заказан §7 поимённо и мной пропущен; переписан. `m-6` — снят
моей же правкой M-2. `m-7` — «all three values» при четырёх (сказано, где пинуется `declared`) и шапка
`stripHeading` про `HeadingRule`/nil (условие no-op — ПУСТОЙ шаблон). `m-10` — счёт nav больше не теряется
при переходе на NCX; `TestEPUBDanglingNavCountSurvivesTheFallToNCX`. `L1` — два резолвера href сведены в
`hrefTarget`; **прежний `resolveHref` был неверен**: раскодировал ДО среза фрагмента, значит литеральный `#`
(приезжающий как `%23`) обрезал имя файла.
### m-8 — ОБЪЯВЛЕНИЕ ПО D39.183, замеренное
Регэксп ингеста стал допускать пробел между маркером и цифрой (`第 1章`), прежний не допускал. Это
одностороннее расширение, заказом не покрытое: сделано, чтобы ингест и чанкер читали заголовок ОДИНАКОВО —
`matchHeaderLine` пробел терпел всегда. **Цена замерена:**
| файл | старый регэксп | новый | разница |
|---|---|---|---|
| 蛊真人 (полная, 215 675 строк) | 2996 | 2996 | **0** |
| фикстура голдена | 8 | 8 | **0** |
| `isekai_majutsushi_jp.txt` | 4 | 4 | **0** |
| `enkan_no_hate_ja.txt` | 0 | 0 | **0** |
### Вырожденный разрез EPUB владельца — ЧИСЛОМ, замер мой
Прогнал все три EPUB через новый путь (ингест, $0; `tmctl manifest` на них не идёт — промтов `en-ru`/`ja-ru`
не существует, §5):
| книга | глав | structure | знаков min / медиана / max | глав < 200 знаков | счета |
|---|---|---|---|---|---|
| Kristoff (en) | 71 | declared | 18 / 29 / 397 878 | **64** | attached 54 · excluded 1 · **service-role-inside-doc 1** |
| isekai (ja) | 44 | declared | 72 / 6669 / 8999 | 9 | excluded 1 |
| fifty_shades (en) | 32 | declared | 0 / 32 276 / 45 511 | 3 | excluded 2 |
⛔ **Цена Kristoff названа прямо:** 64 единицы манифеста из 71 несут не текст книги, а строки её заднего
листинга, и пойдут в оплату как главы; окна банка получат на этой книге гранулярность 6. По словарю ярлык
честен (`nav` действительно объявил эти цели), и §2.3 называет такой исход ожидаемым для пака 1 — но цена
существует и это вопрос владельца, а не мой.
⚠ И `service-role-inside-doc 1` у Kristoff — **это ровно случай M-1 на живой книге**: там роль `toc`
объявлена с фрагментом, и до починки этот документ был бы вычеркнут целиком.
`title_raw` заполнен у **71 из 71**, **44 из 44** и **32 из 32** глав — EPUB-половина работает на реальных
книгах, а не только на фикстурах.
---
## 9. ВТОРОЙ КРУГ ДОФИКСА — И ОДНА ПОПРАВКА К ДОВОДУ, КОТОРЫЙ ЕГО ЗАКАЗАЛ
### Д-1 — ИНВАРИАНТ ПОСТРОЕН, но его область УЖЕ, чем было заявлено
Требование верное и принято: счета — это наблюдаемость, инвариант — гарантия. Построен и проверяется ДО
любого платного пути, нарушение — громкая ошибка движка:
> каждый читаемый документ spine либо исключён НАМЕРЕННО, либо попадает ровно в одну главу;
> и глава — это ровно её документы плюс вставленные между ними разделители.
⚠ **Но довод, которым он заказан, я обязан поправить, потому что проверил обе половины посадками.** Было
сказано: «инвариант ловит потерю текста ЛЮБЫМ путём — сегодняшним фрагментом и завтрашним неизвестным». По
замеру это не так:
| посадка | что делает | ловит ли инвариант |
|---|---|---|
| A | группировка молча роняет документ | **ДА** — `spine document "OEBPS/c1b.xhtml" is neither excluded nor part of a chapter … book text would be lost silently` |
| B | возврат дефекта M-1 (роль с фрагментом исключает файл) | **НЕТ** — падают только собственные тесты M-1 |
Причина простая: исключение **учтено**, поэтому сумма сходится. Инвариант ловит текст, ушедший из чтения
БЕЗ решения; неверное, но записанное решение он по построению увидеть не может. Это не довод против него —
класс, который он закрывает, счетами не ловился вовсе. Но «ловит любым путём» под ним не стоит.
### Д-2 — МОЙ ДОФИКС M-1 ОБМЕНЯЛ ПОТЕРЮ НА ШУМ, и на книге владельца это регресс
Принято целиком. До починки документ Kristoff, на который роль `toc` указывает с фрагментом, исключался
ЦЕЛИКОМ — и там это случайно оказалось верно: файл и есть чистое оглавление. После починки он **прикреплён
как текст книги** (`service-role-inside-doc 1` при `excluded 1`).
⛔ **Цена прямым текстом: листинг оглавления уедет модели, будет оплачен и покажется читателю внутри главы.**
Относительно состояния ДО дофикса на ЭТОЙ книге стало хуже; относительно общего случая — лучше, потому что
раньше терялась проза, а по `D39.205` потеря критична, а шум нет.
**Узкий ремонт (текст до якоря — книга, от якоря — служебное) НЕ делался**, и это осознанный отказ: он и есть
резка xhtml по якорям, которую пак выводит за границы и под которой лежат четыре пина байт-паритета
`extractXHTML`. Изобретать её в конце пака — та самая сделка, от которой пак предостерегает. Кандидат в пак 2.
### Д-3 — ТРЕВОГА ПОСТРОЕНА, НО ПОЛЬЗОВАТЕЛЮ БЕТЫ НЕ ВИДНА
`IngestNotes()` идёт в `r.Log.Warn`, то есть в лог ДВИЖКА. Платформа читает `events.jsonl`, не stderr.
⇒ **«тревога построена» не значит «тревога видна человеку»**: для пользователя беты её сегодня нет. Провод
не строился — это шов и чужая зона.
### Д-4 — ЦЕНА СХЛОПЫВАНИЯ, названная сознательно
`parseNavDoc` СПЛЮЩИВАЕТ nav: каждая `<a href>` любой глубины — граница (§2.3). Схлопнутые цели понижают
книгу до `delimited` (диспозиция 6). Вместе это значит: **любой EPUB, где внутри документа главы есть якоря
на разделы — частая форма хорошо собранной книги, — уходит целиком в `delimited`**, то есть в продажу по
знакам, а не по главам.
На трёх EPUB владельца `collapsed = 0`, поэтому не наблюдалось. Иерархия, по которой можно было бы отличить
«глава» от «раздел внутри главы», в nav ЕСТЬ и отбрасывается сплющиванием сознательно — значит будущий пак
может её использовать, не ломая словарь.
### Мелкое — исправлено, а не оговорено
`chapterTitleRaw` сканировал главу и на `delimited`-разрезе: единственная шапкообразная строка в прозе (одно
попадание, пол ≥2 не пройден) становилась `title_raw`. Теперь заголовок извлекается **только когда шапки
победили** — на `delimited` границы провёл разрыв страницы, и честный ответ `""`, а не уверенный неверный.
---
## 10. ПЕРЕ-ЗАМЕР ТРЁХ EPUB ВЛАДЕЛЬЦА ПОСЛЕ ВСЕХ ДОФИКСОВ
По норме, купленной этой же сменой: **экземпляр, которым дефект предъявлен, обязан быть пере-мерен ПОСЛЕ
починки.** Замер «до» доказывает, что дефект был, и ничего не говорит о том, что стало. Ниже — «после».
| книга | глав | structure | ИНВАРИАНТ | attached | excluded | roles-inside-doc | collapsed | toc-unresolved | рун в главах | рун исключено | title_raw |
|---|---|---|---|---|---|---|---|---|---|---|---|
| Kristoff (en) | 71 | declared | **СОШЁЛСЯ** | 54 | 1 | **1** | 0 | 0 | 1 626 362 | 0 | 71/71 |
| isekai (ja) | 44 | declared | **СОШЁЛСЯ** | 0 | 1 | 0 | 0 | 0 | 239 209 | 1659 | 44/44 |
| fifty_shades (en) | 32 | declared | **СОШЁЛСЯ** | 0 | 2 | 0 | 0 | 0 | 879 053 | 976 | 32/32 |
Инвариант проверяется ВНУТРИ ингеста и возвращает громкую ошибку — успешное чтение и ЕСТЬ утверждение, что
он сошёлся. Статистика глав (min/медиана/max, число коротких) совпала с замером «до» по всем трём книгам:
правки дофикса разрез не двигали.
### Именованный перечень исключений — и поправка к моему же отчёту
Перечень вместо числа сразу окупился: он показал, что **`excluded 1` у Kristoff — это НЕ документ оглавления.**
```
Kristoff: исключён titlepage.xhtml роль=cover рун=0
isekai: исключён EPUB/nav.xhtml роль=nav рун=1659
fifty_shades: исключён …_epub_cvi_r1.htm роль=cover рун=0
исключён …_epub_toc_r1.htm роль=toc рун=976
```
⛔ **В разделе 8 я связал у Kristoff `service-role-inside-doc 1` и `excluded 1` так, будто это одна сделка.
Это неверно, и перечень это вскрыл:** исключён пустой титульный лист (0 рун, потери нет), а документ с
ролью `toc` на фрагменте — `index_split_124.html` — НЕ исключён и лежит внутри главы. Число «исключено: 1»
позволяло прочитать это неправильно; имя и размер — нет.
### Цена Д-2 — теперь ЗАМЕРЕНА, а не предположена
```
ДОКУМЕНТ С guide-РОЛЬЮ toc НА ФРАГМЕНТЕ: index_split_124.html
рун = 2187
первые 120 знаков: "Table of Contents\n\n\n\nBOOK ONE\n\n\n\nBOOK TWO\n\n\n\nBOOK THREE\n\n\n\nBOOK FOUR…"
```
⇒ **2187 рун листинга оглавления уедут модели, будут оплачены и покажутся читателю внутри главы** книги
Kristoff. Это цена дофикса M-1 на этой книге, и она названа числом. До дофикса этих 2187 рун не было — но не
было и гарантии, что на следующей книге в том же месте не окажется глава.
---
## 11. ФИНАЛЬНЫЙ ПРОГОН — И ФОРМА, В КОТОРОЙ ОН СДЕЛАН
⚠ **`make battery` одной командой НЕ прошла: система дважды убила её по нехватке памяти** (хост держит
несколько сессий, своп занят на 3.3 ГБ; первый раз убита на тестах, второй — на линтере). Прогон сделан ПО
СТАДИЯМ, и отчитываюсь именно этими командами, а не «battery зелёная»:
```
go build ./... && go vet ./... && go vet -tags live ./...
go build -o bin/tmvet ./cmd/tmvet && go vet -vettool=bin/tmvet ./... && go vet -tags live -vettool=bin/tmvet ./...
GOGC=50 golangci-lint run --timeout=20m --concurrency=2 ./... → 0 issues
go test <16 лёгких пакетов> -race -count=1 -timeout=20m → ok 16
go test ./internal/store -race -count=1 → ok 29.5s
go test ./internal/archguard -race -count=1 → ok 52.2s
go test ./internal/pipeline -race -count=1 → ok 282.7s
go test ./... -count=1 -v → EXIT 0, ok 19, FAIL 0
```
Покрытие то же, что у `make battery`: те же цели, тот же `-race`, тот же сбор скипов. Разница только в том,
что стадии не складывают пик памяти. **Скипов 3**, поимённо те же, что и в базовой линии:
`TestMinerFullBookParity` · `TestHelperEventsRun` · `TestHelperKillLoop`. **Новых нет.**
### Регресс, пойманный ПЕРЕ-ЗАМЕРОМ — той самой нормой
Сужение `title_raw` до «только когда шапки победили» оказалось **слишком широким**, и я это поймал не
рассуждением, а пере-снятием пинов: у односоставного исходника `ch1` заголовок исчез (`заполнено 0/1`).
Причина: одна глава с одной шапкой не проходит пол ≥2, свидетель у неё `delimited`, и правило выбрасывало
её собственный настоящий заголовок.
**Правило разделено по половинам, которые тянут в разные стороны:**
| разрез | где ищется шапка | почему |
|---|---|---|
| шапки победили (`detected`) | вся глава | замороженное правило преамбулы прячет шапку главы 1 в её середине |
| любой другой | только ПЕРВАЯ строка | шапкообразная строка в прозе — одиночное попадание, не граница; но односоставная книга свой настоящий заголовок сохраняет |
Пин — `TestTitleRawIsTakenWhereTheCutAllowsIt`, три случая. После правки пины сошлись с базой полностью:
разрез идентичен и `title_raw` идентичен на всех восьми исходниках.
⚠ **Это второй раз за смену, когда правку ловил пере-замер, а не тест.** Первый — Д-2 (не пере-мерил книгу
владельца после починки). Норма работает ровно потому, что она про ЭКЗЕМПЛЯР, а не про мнение.
---
## 12. ВЫЧИТКА ПОСЛЕ ЛЕНДИНГА — ЧТО НАШЛОСЬ В СВОЁМ ЖЕ КОДЕ
Пак был уже заланден (`2f65d1d`), когда владелец спросил, всё ли проверено. Простое чтение собственного
дифа глазами — то, чего я за весь пак ни разу не сделал подряд, — дало находки за минуты и без единого агента.
### Дефекты кода (починены)
| # | что | почему это дефект |
|---|---|---|
| В-1 | **метка `nav` не триммилась**, а её сёстры триммят | один заголовок давал `"\n Chapter One\n "` через nav и `"Chapter One"` через NCX — два ответа на один вопрос, решаемые тем, какую таблицу собрал издатель. Едет на провод в `title_raw`. Фикстуры пака не ловили: они пишут метку без отступов |
| В-2 | **поиск заголовка в манифесте без нижней границы** | сосед шестью строками выше пишет `k >= 0 && k < len(ids)` и деградирует с WARN, «если плотная нумерация однажды перестанет держаться». Моя строка в том же случае даёт панику индекса на платном прогоне |
| В-3 | `ncxPath` — единственный резолвер href, не раскодирующий проценты | ровно та асимметрия, которую пак свёл в `hrefTarget`; я свёл два резолвера и не заметил третий |
Пин на В-1 — `TestNavLabelIsTrimmedLikeItsSiblings`, предъявлен посадкой (снять trim → красный).
### Протухшие носители (девять, починены)
`resolveTOC` обещал счёт нелинейных целей, которого я сам же лишил его при M-5 · `epubTOC.unresolved` ссылался
на `DocumentsExcluded`, переименованный в `Excluded` · `isChapterHeader` нёс имя `isCJKChapterHeader` ·
`Chunk.Heading` — «detected via the pair's lang.HeadingRule» · `stripHeading` в `SplitChunksWithChapters` —
«A nil rule» · шапка `ingest.go` и `chapterSep` — «form feed семантически граница главы» и «epub несёт
настоящую структуру глав в spine», то есть ровно те два убеждения, которые пак опровергает · `detectChapterUnit`
— четыре юнита и не тот источник · `buildManifest` — параметр `chapterTexts`, которого нет · `bookfile/model.go`
— «титульный документ был бы прочитан как отдельная глава» · комментарий сторожа в `ingestnotes_test.go`
обещал «любой тип», а `switch` покрывает два.
⛔ **Класс один и тот же: утверждение, верное на момент написания и не пере-прочитанное после правки.** Он же
дал шесть находок приёмки и четыре моих. Дешевле всего он ловится не тестом, а чтением подряд.
### Ошибки в ЭТОМ отчёте (исправлены)
Каталог 205 → **211** (число было от первого круга) · таблица §1 писала `make battery` в обеих строках и
противоречила §11 того же отчёта · «Счета формата» перечисляли пять, их шесть · строка §6 «EPUB владельца не
прогонялись» осталась после того, как §8 и §10 их прогнали.
### Перепись гейта была неверной — и я поймал её собственной нормой дня
Я считал конфиги **по ИМЕНИ файла** (`book.yaml`, `*.book.yaml`) и получил 31 конфиг / 8 исходников.
Счёт **по СОДЕРЖИМОМУ** (файл несёт `book_id` и `source_file`) даёт **39 конфигов и 12 различных
исходников**. Четыре источника семьи 蛊真人 под `books/gu-zhenren/exp15/` гейт не покрывал: их конфиги
называются `exp15-*.yaml`, и фильтр по имени их не видел.
**Это четвёртый за смену случай класса «имя против содержимого» — и первый, где на него наступил я сам,
уже объявив его нормой.**
Четыре пропущенных источника пинованы «до» и «после» отдельно:
| исходник | глав | structure до → после | разрез |
|---|---|---|---|
| `S2prime.txt` | 1 | none → none | байт-в-байт |
| `S2prime_big_flat.txt` | 1 | none → none | байт-в-байт |
| `S2prime_big_blanklines.txt` | 1 | none → none | байт-в-байт |
| `smoke.txt` | 1 | none → none | байт-в-байт |
Пробел последствий не имел, но он БЫЛ, и закрыт замером, а не доводом.
### Два заказа §2.1/§2.5, которых я не сделал и не назвал
⛔ **Полная 蛊真人 (2283 главы) через новый путь не прогонялась.** §2.1 велит: «Полную книгу гоняешь
ОТДЕЛЬНО, СВОИМ конфигом в своём каталоге; её результат идёт числом в отчёт». Я записал в §6 «у неё нет
конфига» — но отсутствие конфига и есть то, что заказ приказывал устранить. **Это не «не проверено», это
не сделано.**
⛔ **Машинная карта сдвигов не была построена и не была названа среди непроверенного.** §2.5: «Прозой
нельзя: на прозе не построить мигратор потом». Отчёт давал прозаическую таблицу восьми строк.
### ОБА ЗАКАЗА ИСПОЛНЕНЫ ПОСЛЕ ВЫЧИТКИ
**Полная 蛊真人 прогнана своим конфигом** (`~/tm-pins-0509/full/`, `tmctl manifest`, EXIT 0, 10 с, $0):
**2283 главы**, `structure = detected`, 4276 юнитов, 5071 чанк, `title_raw` заполнен у **2283 из 2283**.
Счёт глав сошёлся с независимым замером шапок шага 0 (2283 строки `第N节`) — два прибора, один ответ.
**Машинная карта сдвигов построена** — `backend/docs/CHAPTER_STRUCTURE_SHIFTMAP.json`, генератор рядом
(`chapter_structure_shiftmap.py`). Set-diff по id и номеру, `same/moved/gone/new`, по **тринадцати**
источникам: восемь пинов гейта, четыре пропущенных переписью, и полная книга.
| | глав | same | moved | gone | new |
|---|---|---|---|---|---|
| **все 13 источников** | **2355** | **2355** | **0** | **0** | **0** |
ветка (а) гейта §3 предъявлена не таблицей глазами, а машинным набором, и на нём строится мигратор,
если он понадобится. Это и было содержанием заказа.