textmachine/docs/archive/reports/SMALLPACK_INGEST_PROMPTS_2026-07-30.md

38 KiB
Raw Blame History

Малая пачка: ingest грязных епабов (14а) + комментарии промпт-файлов (14б)

База: записка оркестратора №8 от 30.07, санкции владельца 26.07 (бэклог docs/PROGRESS.md, строки 14а/14б). Зона: backend/. Деньги сессии: $0 — ни одного вызова провайдера, вся приёмка на локальном сырье. Статус: построено и проверено исполнением. Сессия НЕ коммитит, лендинг — оркестратора.

Ревью-шапка (оркестратор №9, 30.07): ПРИНЯТО И ЗАЛЕНДЕНО, D39.54. Приёмка исполнением на неподвижных копиях (старая = HEAD, новая = деливерабл): собственный дампер old-vs-new по трём епабам стенда — 205 документов, дифф глав и ruby ПУСТ (3.29 МБ на сторону); полный сьют -race -count=1 14/14 зелёный, gofmt/vet чисто; четыре спот-мутации вне порядка сессии (D — SHA по сырому, M8 — нейтрализация вырезания, M7 — raw-text везде, M10 — flushRuby в no-op) — все КРАСНЫЕ, восстановление зелёное. Финальная сверка сессии (§2.8, несбалансированный ruby — третий улов ревью) вошла в приёмку: копии сняты ПОСЛЕ фикса, мутационная таблица сессии 17/17. Посылки перемерены независимо: 126 itemref (не 958) · 8/8 книг стенда txt · ровно 2 сущности из 252 расходятся (кросс-чек через WHATWG-таблицу python и GOROOT-таблицу xml.HTMLEntity) · цитата snapshot.go:138 дословна · grep terminolog по snapshot.go пуст. Диспозиции: CDATA (§7.4) — В СКОУПЕ (без неё переезд = регрессия на валидном XHTML); chunkerVersion НЕ поднят — ратифицировано D39.54 (экспозиция — пустое множество); mimetype — верность формату, не фикс поведения (§7.5 принят, в D-ноте так и записано). Фикс-лист (1): комментарий поля SHA256 в render.go:89 остался «hash of the raw file» — одна строка, на ближайшее касание. Кандидат §8 (<meta charset> без XML-объявления) — бэклог 34а.

Две поправки к посылкам записки — читать до §1. Обе найдены замером, обе меняют формулировки приёмки, ни одна не меняет решение строить.

  1. «Empire of the Dawn, 958 документов spine» — неверно. В книге 126 <itemref> (288 записей zip, 283 <item> манифеста, из них 154 .jpg). Число 958 не соответствует ничему в файле. Сравнение проведено на всех трёх епабах стенда — 205 документов суммарно.
  2. «Ingest выполняется при создании книги — существующие БД и снапшоты не трогаются» — обоснование неверно, вывод верен. TranslateBook вызывает chunk.IngestEncoded и SplitChunks на КАЖДОМ прогоне (bookrun.go:112,140; то же в status.go:190,194) — источник переизвлекается и перерезается каждый раз. Существующие БД защищены другим фактом: все книги стенда — txt/gb18030 (book.yaml восьми проектов gu-zhenren/*), а extractXHTML достижим только из ingestEPUB. Ни одна существующая книга епаб-путь не проходит. Подробно — §6.

§1 Что построено

Пункт Где
14а-1 extractXHTML на токенайзере x/net/html вместо encoding/xml chunk/ingest.go
14а-1а Паритетные помощники: voidTags, rawTextTags, tokenTagName, checkXHTMLCharset, unwrapCDATA, flushRuby там же
14а-2 Chapter.EntryName + два теста (percent-href, #fragment) chunk/chunktest/epub.go, chunk/ingest_test.go
14а-3 mimetype фикстуры через zip.Store + тест-пин OCF chunk/chunktest/epub.go, chunk/ingest_test.go
14б-1 Вырезание <!-- --> ДО сплита по ---USER--- pipeline/render.go (stripPromptComments)
14б-2 SHA по канонической (очищенной) форме там же, LoadPromptTemplate
14б-3 Шапка terminologist.md ужата 12 строк → 2 prompts/zh-ru/terminologist.md
14б-4 Тест-пины + мутации pipeline/promptcomments_test.go

Диффа: 5 отслеживаемых файлов +651/97 (из них ingest.go +216/70, ingest_test.go +374) плюс новый promptcomments_test.go (161 строка).


§2 14а — переезд на токенайзер

2.1 Четыре класса: КРАСНЫЕ до, ЗЕЛЁНЫЕ после

Приёмка «красное-до» снята не рассуждением, а исполнением оригинальной функции: старый extractXHTML извлечён дословно из git show HEAD:backend/internal/chunk/ingest.go и прогнан по тем же фикстурам рядом с новым. Старый на всех четырёх — жёсткая ошибка и пустой текст, то есть в ingestEPUB это return nil, err: один грязный документ убивал импорт всей книги.

Класс Старый encoding/xml Новый токенайзер
голый < в прозе XML syntax error on line 3: expected element name after < "Если a < b, то дальше.\n\n\n\nВторой абзац."
перехлёст тегов XML syntax error on line 3: unexpected end element </i> "жирный оба курсив хвост."
содержимое <script> XML syntax error on line 3: expected attribute name in element "Настоящий текст."
-- в комментарии XML syntax error on line 3: invalid sequence "--" not allowed in comments "Настоящий текст."

Пин — TestIngestEPUBDirtyXHTMLImports (4 подтеста): каждый проверяет и что проза выжила, и что шум (document.write, текст комментария, теги) в неё НЕ попал.

2.2 Пять решений, которых требует токенайзер (и почему именно так)

Токенайзер — лексер, а не парсер: он не достраивает дерево и не синтезирует закрывающие теги. Пять мест, где наивный перенос состояния ломается (пп. а–в найдены при проектировании, пп. г–д — ревью уже после сборки):

(а) Пропуск поддерева — ПО ИМЕНИ, а не по глубине. Старый код на encoding/xml считал skipDepth++ на каждом StartElement. У токенайзера <meta/>/<link/> в <head> — это SelfClosingTagToken без парного EndTagToken, а <meta> без слэша — StartTagToken без пары. Слепой счётчик никогда не вернулся бы в ноль и съел бы всю главу. Пропуск отслеживается по имени корня + глубина одноимённых; плюс правило «<body> закрывает незакрытый <head>», чтобы битая разметка не проглатывала текст.

(б) Void-элементы закрываются сами. xml.HTMLAutoClose синтезировал End для br/hr/img/meta/link и др., поэтому старый код для блочного <hr> писал разрыв ДВАЖДЫ ("\n\n\n\n"). Новый код на SelfClosingTagToken или элементе из voidTags вызывает end() сам — это и даёт байтовый паритет. Замерено: <p>A</p><hr/><p>B</p>"\n\nA\n\n\n\n\n\n\n\nB\n\n" в обеих реализациях, обе записи <hr/> и <hr> совпадают.

(в) Префикс пространства имён срезается. encoding/xml отдавал Name.Local (<epub:p>p), токенайзер отдаёт epub:p. Без среза префиксованная епаб3-глава схлопнулась бы в один абзац.

(г) Сырой текст — только там, где он нужен (§2.7) и (д) ruby как РЕЖИМ, а не глубина (§2.8) — оба найдены адверсариальной проверкой уже ПОСЛЕ первой сборки, оба критические, обоих сравнение по чистому корпусу не увидело бы вовсе (вместе с CDATA §2.4 — три регрессии).

Плюс сохранённый инвариант: экран объявленной кодировки (checkXHTMLCharset — не-UTF-8 объявление остаётся ГРОМКОЙ ошибкой, как делал CharsetReader).

2.3 Побайтовое сравнение на реальных епабах: расхождений НЕТ

Метод: дампер прогоняет extractXHTML по каждому документу spine ДО правки (снят с чистого дерева до первого редактирования) и ПОСЛЕ, и сравнивает по двум осям — сырые байты извлечения и «проводная» ось (text.NormalizeSourcesplitParagraphs, то есть ровно то, что склеивается в ch.Text: chunker.go:318 собирает чанк из TrimSpace-абзацев через \n\n).

Епаб Документов Расхождений сырых байт Расхождений на проводе Расхождений ruby
Empire of the Dawn 126 0 0 0
isekai_majutsushi_jp 45 0 0 0
fifty_shades_1_en 34 0 0 0
Итого 205 0 0 0

Поимённо перечислять нечего — список расхождений пуст. Это не «примерно совпало»: совпали сырые байты, включая 161 <hr/>, 1149 <br/> и 2200 &nbsp; в корпусе. Ошибок извлечения ноль в обеих реализациях (корпус стенда чистый — классы (iii)/(iv) в нём не встречаются вовсе: <script> 0, комментариев 0).

2.4 Улов В ХОДЕ стройки: CDATA была РЕГРЕССИЕЙ — починена

Сравнение только по корпусу дало бы ложное «всё чисто». Отдельный дифференциальный прогон по 24 конструкциям, которые старый ридер ПРИНИМАЛ (только такие и могут лежать в существующей БД), показал три расхождения, из них одно — настоящая регрессия:

*** CDATA in body:  old="До.\n\nсырой <текст>\n\nПосле."   new="До.\n\n]]>\n\nПосле."
*** CDATA wrapping markup: old="<p>не разметка</p>..."     new="не разметка\n\n]]>..."

У HTML-токенайзера CDATA нет — он лексит секцию как «bogus comment», теряя часть содержимого и вываливая ]]> в прозу. Добавлена распаковка unwrapCDATA до токенизации (содержимое CDATA — литеральный текст, экранируется и отдаётся токенайзеру обратно). После неё:

SUMMARY: 23 identical on the wire | 1 DIVERGED | 0 the old reader rejected outright

Реальная запись CDATA в корпусе (/*<![CDATA[*/ внутри <style>, 2 штуки в fifty_shades) остаётся отброшенной вместе с поддеревом — сравнение 205 документов после правки повторено, снова 0/0/0.

2.5 Оставшиеся расхождения — ДВА класса, оба про сущности

Свип по 24 конструкциям дал один вход ("A &amp G B"). Он оказался частным случаем более широкого класса: отдельная сверка всех 252 имён xml.HTMLEntity против токенайзера и сверка форм без точки с запятой (перемер третьим способом, независимо от свипа) показала ровно два класса:

(1) Legacy-сущности без точки с запятой. HTML5 разрешает &amp/&copy/&nbsp без ;; encoding/xml оставлял их буквой.

x&amp y   → old "x&amp y"   | new "x& y"          x&nbsp y → old "x&nbsp y" | new "x  y"
x&copy y  → old "x&copy y"  | new "x© y"          x&#38 y  → old "x&#38 y"  | new "x& y"
AT&T      → old "AT&T"      | new "AT&T"   ← СОВПАДАЮТ    R&D → совпадают

Обычная проза не задета: после & должно стоять начало известного имени, а T/D таковым не являются. Закреплено тестом TestExtractXHTMLBareAmpersandInProseSurvives.

(2) Ровно две именованные сущности из 252 расходятся значением:

&lang;  xml=U+2329 (〈)  tok=U+27E8 (⟨)      &rang;  xml=U+232A (〉)  tok=U+27E9 (⟩)

NFC их НЕ сводит (NFC(U+2329)=U+3008, NFC(U+27E8)=U+27E8), то есть text.NormalizeSource разницу не поглощает. Значение токенайзера — актуальное по HTML5; значение xml.HTMLEntity — устаревшая CJK-эквивалентная форма.

Ни один из двух классов не сработал ни на одном из 205 реальных документов. Оценка риска — §6.

2.7 Улов ПОСЛЕ сборки: сырой текст — КРИТИЧЕСКАЯ регрессия, починена

Найдено адверсариальным ревью (author≠reviewer), перепроверено мной прогоном старого ридера рядом с новым. Раз-мод токенайзера — то, что делает безобидным разметко-подобный JS (класс iii), — включается на целом семействе тегов, а не только на script/style. Два следствия, оба реальные:

(1) Раз-текстовые теги ВНЕ skipRoots (noscript, textarea, iframe, xmp, noembed, noframes, plaintext) отдавали содержимое одним куском с тегами внутри — литеральная разметка уезжала в ch.Text и на провод:

noscript:  old "\n\nВключите JS\n\n\n\nПроза.\n\n"   new "<p>Включите JS</p>\n\nПроза.\n\n"

(2) САМОЗАКРЫТЫЙ раз-текстовый тег — потеря остатка главы. <script src="x.js"/> — обычное написание в XHTML. Закрывающего тега не будет никогда, поэтому раз-мод шёл до конца файла:

self-closed script:  old "\n\nДо.\n\n\n\nПосле.\n\n"   new "\n\nДо.\n\n<p>После.</p></body></html>"

То есть весь остаток главы уходил модели сырой разметкой. Ни один из 205 документов корпуса этого не содержал — сравнение по корпусу дало бы чистое «0 расхождений» и пропустило бы дефект.

Правка: набор rawTextTags + z.NextIsNotRawText() для всех случаев, кроме единственного, где раз-мод нужен, — непустого <script>/<style>, поддерево которого мы и так выбрасываем. После правки все 11 форм байт-в-байт совпали со старым ридером, класс iii продолжает работать (оба свойства проверяются одним тестом TestExtractXHTMLRawTextElementsByteParity), сравнение 205 документов повторено — снова 0/0/0.

2.8 Второй улов ревью: несбалансированный <ruby> — КРИТИЧЕСКАЯ потеря текста, починена

Найдено вторым адверсариальным заходом, перепроверено мной прогоном старого ридера. HTML5 делает </rt>, </rp> и </rb> необязательными — их закрывает соседний элемент или </ruby>, — а токенайзер, в отличие от нестрогого encoding/xml, подразумеваемые закрывающие теги НЕ достраивает. Счётчики rtDepth/rpDepth из старого кода при первом же пропуске текли навсегда:

пропущен </rt>:   old "漢текст\n\n\n\n字ещё" ruby=[漢/かん, 字/じ]
                  new "漢текст\n\n\n\nещё"   ruby=[漢/かん]      ← база 字 УДАЛЕНА из прозы

То есть у КАЖДОГО последующего ruby в главе база уходила в буфер чтения вместо тела — текст пропадал с провода. Второе следствие: незакрытый <ruby> держал rubyDepth > 0 до конца документа, поэтому blockTags[name] && rubyDepth == 0 не срабатывал ни разу — все разрывы абзацев подавлялись, глава схлопывалась в один слипшийся блок:

незакрытый <ruby>: old "漢\n\n\n\nВторой абзац.\n\n\n\nТретий.\n\n"  (3 абзаца)
                   new "漢Второй абзац.Третий."                       (1 абзац, слова слиты)

Правка: под-элемент ruby стал РЕЖИМОМ (rubyBase|rubyRT|rubyRP), а не глубиной — режим сбрасывает любой сосед и любой </ruby>, поэтому течь ему некуда; плюс flushRuby() на границе блока (ruby — строчный элемент и не может пережить свой абзац). После правки обе формы совпадают со старым ридером байт-в-байт, вложенный ruby — тоже. Побочно: при пропущенном </rp> тело совпадает со старым, а чтение 東/とう, которое старый ридер ТЕРЯЛ, теперь захватывается — улучшение, и оно не трогает провод (ruby питает только сид глоссария, не ch.Text).

Оба улова (§2.7 и §2.8) были невидимы для сравнения по корпусу — 205 документов давали чистый ноль, пока код содержал обе регрессии. Это главный методический вывод пачки: приёмка «побайтово на реальной книге» показывает лишь то, что в книге есть; классы, которых в ней нет, надо строить руками и сверять со СТАРОЙ реализацией.

2.6 EntryName, ссылки и mimetype

resolveHref имел две ветки, которые ни один тест не мог достать: фикстура писала запись zip ровно по Href, поэтому ни percent-кодирование, ни #fragment не воспроизводились. Добавлено поле Chapter.EntryName (пусто → прежнее поведение; все 14 мест конструирования используют имена полей, позиционных литералов нет — ни один вызов не тронут). Ветка выбора «xhtml или ассет» переведена с Href на имя записи: у href с фрагментом расширение .xhtml#part2 не совпало бы ни с чем.

Два теста: percent-href (в т.ч. реальный случай — CJK-имя файла, %E7%AC%AC%E4%B8%80%E7%AB%A0.xhtml第一章.xhtml) и ch1.xhtml#part2. Оба убивают удаление своей ветки (§4).

mimetype пишется через zip.CreateHeader(… Method: zip.Store) — первым и несжатым, как требует OCF. Честно: ingest файл mimetype не читает вовсе (grep пуст) — это верность фикстуры формату, а не починка поведения; покупает она только то, что фикстура остаётся настоящим епабом по мере роста ридера. Подтверждение уместности: все три епаба стенда хранят mimetype именно так (compress_type=0, первая запись).


§3 14б — комментарии промпт-файлов

3.1 Утечка подтверждена и закрыта

LoadPromptTemplate не вырезал <!-- -->, и 12-строчная шапка terminologist.md уезжала модели в system каждого батча. Правка: вырезание до сплита (закомментированный ---USER--- не должен резать файл) и SHA по канонической форме.

3.2 Почему SHA именно по очищенной форме — обосновано доком самого снапшота

snapshot.go:138 формулирует назначение PromptSHA256 прямым текстом: «без этого правка промпта молча переиспользовала бы чекпойнты». То есть хеш отвечает на вопрос «изменился ли вход на проводе», а не «изменился ли файл». Комментарий после правки на провод не идёт — значит канонический хеш и есть буквальное исполнение контракта. Побочный эффект — ровно тот, что просил владелец: правка комментария перестаёт стоить денег.

3.3 Ось замерена: двигается ровно один файл

Пересчёт SHA всех промпт-файлов старым способом (sha256 сырых байт) против нового:

prompts/zh-ru/editor-mono.md            UNCHANGED   prompts/zh-ru/repair/dc1_time_units.md  UNCHANGED
prompts/zh-ru/editor.md                 UNCHANGED   prompts/zh-ru/repair/latin_residue.md   UNCHANGED
prompts/zh-ru/judge-selector.md         UNCHANGED   prompts/zh-ru/translator-banknote.md    UNCHANGED
prompts/zh-ru/repair/broken_word.md     UNCHANGED   prompts/zh-ru/translator.md             UNCHANGED
prompts/zh-ru/repair/dc1_fractional.md  UNCHANGED   prompts/zh-ru/terminologist.md          *** MOVED ***

9 из 10 — байт-в-байт прежние. Двигается только terminologist.md (единственный файл с комментарием).

Что это стоит. SHA терминолога в снапшот НЕ фолдится (grep terminolog по snapshot.go пуст — ни stageSnap, ни repairSnap его не видят), поэтому snapshot_id ни одной книги не меняется: волны draft/edit резюмируются за $0. Адрес батча терминолога — RequestHash{… Messages: msgs} (terminologist.go:395), а msgs несут отрендеренный system, поэтому батчи терминолога стендовой пробы перекупятся один раз (центы). Ровно та ось, что заявлена в записке; проверена двумя путями.

3.4 Незакрытый комментарий — громкая ошибка

Решение не в записке, обосную. Незакрытый <!-- — битый шаблон. Молча вырезать «до конца файла» значило бы съесть ---USER--- и выдать невнятную ошибку про отсутствующий разделитель (мутация F показала ровно это). Отказ на ЗАГРУЗКЕ, до любого биллинга — та же идиома, что уже применяет loadTerminologyTemplate («гейт, который не может отработать, отвергается на загрузке»).

3.5 Шапка ужата

12 строк → 2, обоснования оставлены ссылками на D39.46/47/52 и отчёты полигона (норма владельца: улики — в отчётах, не в файле).


§4 Мутации: все КРАСНЫЕ

Каждая правка восстанавливалась побайтно после прогона.

# Мутация Убита тестом
A убрать url.PathUnescape в resolveHref TestIngestEPUBPercentEncodedHref
B убрать срез #fragment TestIngestEPUBHrefFragmentIgnored
C mimetype обратно в Deflate TestEPUBFixtureMimetypeIsOCFConformant
D SHA по СЫРОМУ файлу TestPromptSHAIsOverTheCanonicalForm
E вырезать комментарии ПОСЛЕ сплита (только system) 4 теста, вкл. утечку в user-сообщение
F незакрытый комментарий глотает молча TestPromptUnterminatedCommentFailsLoud
M1 void/self-closing не закрывать самим TestExtractXHTMLVoidTagByteParity (hr)
M2 пропуск по слепой глубине вместо имени TestExtractXHTMLVoidTagByteParity (head без body)
M3 убрать unwrapCDATA TestExtractXHTMLUnwrapsCDATA
M4 убрать экран кодировки TestExtractXHTMLRejectsNonUTF8Charset
M5 убрать выход из <head> по <body> TestIngestEPUBVoidTagsInHeadDoNotSwallowChapter
M6 не срезать префикс пространства имён TestExtractXHTMLStripsNamespacePrefix
M7 убрать NextIsNotRawText (раз-текст везде) TestExtractXHTMLRawTextElementsByteParity
M8 нейтрализовать вырезание комментариев на боевых промптах TestShippedPromptsCarryNoCommentsOnTheWire (ловит и repair/, и terminologist.md)
M9 rt/rp обратно на текущие счётчики (без сброса соседом) TestIngestEPUBRubyCaptureAndBaseInBody
M10 убрать flushRuby() на границе блока TestExtractXHTMLUnbalancedRuby/unclosed <ruby>
M11 </ruby> не сбрасывает под-режим TestExtractXHTMLUnbalancedRuby/nested ruby

M1 и M2 сначала ВЫЖИЛИ — это улов самого гейта, а не формальность. M1 выжила потому, что разница <hr> ("\n\n\n\n" против "\n\n") поглощается splitParagraphs, и ни один тест не смотрел на сырые байты. M2 выжила потому, что правило «<body> закрывает <head>» маскирует поломку счётчика в любом документе с <body>. Добавлены: тест байтового паритета по 10 формам (эталон снят замером со СТАРОГО ридера) и случай «<head> без <body>», где маскировки нет. После этого обе красные.

Два теста 14б были почти вакуумны — тоже улов ревью, тоже настоящий:

  • пин «комментарий не в проводе» рендерил tpl.System, тогда как боевой путь сначала сплющивает few-shot (runner.go:315), поэтому канарейка из блока ---FEWSHOT--- ни во что не попадала. Теперь тест сплющивает через SystemFor(true) и проверяет посылку — что канарейки вообще есть в исходной фикстуре;
  • охранник боевых промптов ходил os.ReadDir по prompts/zh-ru/ и не видел repair/ — 6 файлов из 10. Теперь filepath.Walk + пол len(files) >= 10. Невакуумность доказана исполнением: внедрение комментария в repair/latin_residue.md при нейтрализованном вырезании — тест КРАСНЫЙ и называет оба файла.

§5 Гейты

Гейт Итог
go build ./... чисто
go vet ./... чисто
gofmt -l по моим файлам чисто (internal/llm/llm.go — до-паковый, в дереве не изменён, не трогал)
go test ./... -count=1 14/14 пакетов зелёные
go test ./... -count=1 -race 14/14 пакетов зелёные
golden (pipeline) зелёный в составе пакета
мутации 17/17 красные (§4); три из них выжили с первого раза и потребовали новых тестов
«перемерь другим способом» сравнение 205 документов по ДВУМ осям (перепрогнано после каждой правки); дифференциальный свип по 24 конструкциям; сверка ВСЕХ 252 имён xml.HTMLEntity против токенайзера; round-trip CDATA по 8 payload'ам; ось SHA пересчитана независимо от тестов
адверсариальная проверка (author≠reviewer) ТРИ дефекта найдены и починены ПОСЛЕ первой сборки: §2.7 раз-текст (критический), §2.8 несбалансированный ruby (критический), §4 два почти-вакуумных теста 14б

Замечание по гигиене: субагенты-ревьюеры, вопреки выданной им инструкции «репозиторий только на чтение», оставили в internal/chunk/ восемь временных zz*_test.go (часть не компилировалась и роняла сборку). Все удалены, дерево проверено git status — в диффе только мои файлы.

Временные харнессы (дампер, red-before, свип) жили в internal/chunk/ только на время прогона и удалены; git status чист от них.


§6 Что висит на владельце: chunkerVersion НЕ поднят

Факт. chunkerVersion (render.go:52) документирован как версия «правил сегментации/ингеста (включая… слой ingest txt/epub)», и его смысл — сделать изменение поведения ингеста громким --resnapshot, а не молчаливой расходящейся переоплатой. Прецедент в самом доке: v3→v4 подняли в том числе за «<br> внутри <ruby> больше не течёт переводом строки в тело» — правку уже той же породы. Я его не поднял. Обоснование и остаточный риск:

  1. Ингест действительно переисполняется на каждом прогоне (bookrun.go:112) — то есть механизм риска реален, посылка записки в этой части неверна (см. шапку).
  2. Но extractXHTML достижим только из ingestEPUB, а все восемь книг стенда — txt/gb18030. Ни одна существующая БД епаб-путь не проходит, поэтому двигать нечего.
  3. Для епаб-книг, которых ещё нет, поднятие версии не даёт ничего: они будут созданы уже на новом коде.
  4. Остаточная экспозиция — два класса сущностей (§2.5): книга, в чьей прозе стоит legacy-сущность без точки с запятой (&amp/&copy/&nbsp…) либо &lang;/&rang;. Такая книга — и только епаб-книга, уже имеющая БД, — при следующем прогоне перерезалась бы молча. На стенде таких книг нет (п.2).
  5. Цена поднятия несимметрична: новый snapshot_id меняет request_hash каждого чанка, то есть полная переоплата всех книг стенда — за изменение, которое их байтов не касается. Гейт согласия на перепокупку (checkRebillConsent) остановил бы это суммой, но платить всё равно не за что.

Рекомендация: не поднимать. Если владелец предпочитает строгое правило («любая правка ингеста — громкий resnapshot, без рассуждений о достижимости») — это одна строка, и гейт согласия делает подъём безопасным. Решение денежное, поэтому оставлено подписи, а не принято сессией.


§7 Пинги оркестратору (расхождения с записями)

  1. «958 документов spine» в записке — в файле 126. Число ни на что в епабе не отображается; строку бэклога 14а стоит поправить, чтобы будущая сессия не искала недостающие 832 документа.
  2. Ось «ingest при создании книги» сформулирована неверно (переисполняется каждый прогон); вывод «существующие БД не трогаются» верен по другой причине (все книги txt). См. §6.
  3. Заявка «4 класса падений» подтвердилась полностью — все четыре были жёсткими ошибками, теряющими главу целиком (а с ней — импорт книги).
  4. CDATA не была в скоупе записки, но без неё переезд был бы регрессией на валидном XHTML (§2.4). Считаю это частью «переносится с пинами», а не расширением скоупа; если оркестратор считает иначе — правка изолирована в unwrapCDATA + один тест и снимается отдельно.
  5. mimetype через zip.Store не чинит поведениеingest его не читает. Записано, чтобы пункт не пошёл в D-лог как исправление импорта.

§8 Чего сессия НЕ делала

  • Не коммитила (по записке).
  • Не трогала docs/BACKEND_PACK19_VOICE_STATE_SESSION_PROMPT.md и START_PROMT.MD — в дереве были чужие незакоммиченные правки (на момент старта; pack-19 позже стал чистым).
  • Не форматировала internal/llm/llm.go (до-паковое состояние, чужая зона диффа).
  • Не расширяла экран кодировки на <meta charset>: старый ридер смотрел только XML-объявление, сохранён паритет. Книга с <meta charset="gb18030"> и без XML-объявления по-прежнему прочлась бы как UTF-8 — это до-паковая дыра, не регрессия; кандидат в бэклог, если владелец захочет.