# Отчёт: пак «ФОРМАТЫ И ЧЕСТНЫЙ СЛОВАРЬ» Сессия `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` с лестницей (`/<язык>/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 `` (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: каждая `` любой глубины — граница (§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 предъявлена не таблицей глазами, а машинным набором, и на нём строится мигратор, если он понадобится. Это и было содержанием заказа.