textmachine/backend/docs/CHAPTER_STRUCTURE_REPORT.md

47 KiB
Raw Blame History

Отчёт: пак «ФОРМАТЫ И ЧЕСТНЫЙ СЛОВАРЬ»

Сессия 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:0c3aa4cbbb53de134: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. Единственный переехавший ассерт. Сам тест переименован: TestTXTStructureIsDeclaredOnlyWhenTheFormatDrewEVERYBoundaryTestTXTProvenanceNamesTheWinningPath — прежнее имя после конкуренции ни во что не разрешается (класс строки 299).
  • priceprojection_test.go — кейс "a form feed is the format speaking""a form feed is a separator, not a chapter", ожидание DeclaredDelimited.
  • ingest_encoding_test.go — счёт глав сохранён (шапки победили), поправлен комментарий и текст ошибки: разрез там больше не по \f.
  • chunker_heading_test.gozhRuHeadingRule()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 предъявлена не таблицей глазами, а машинным набором, и на нём строится мигратор, если он понадобится. Это и было содержанием заказа.