textmachine/docs/research/33-backend-debt.md

98 KiB
Raw Blame History

research/33 — Техдолг бэкенда: замер цены при изменении и что обязан перепроектировать перекрой глав

СТАТУС: ФАКТУРА, НЕ РАТИФИЦИРОВАНА. Написан контролёром 06.09 по заказу владельца («итог замера техдолга — ресёрч-документом, чтобы по нему принимать решения»). Отдельные утверждения отсюда становятся ратифицированными только собственной D-нотой; статус файла в целом — материал для решения. Строки бэклога по нему заводит оркестратор; сам файл ничего в дереве не меняет и не заказывает.

ЧЕМ СНЯТО. Ветка main, HEAD e69902a на момент письма. Замер шёл воркфлоу из 38 агентов (6 срезов-проб · 31 опровергатель · 1 сводный вердикт; 4.32 млн токенов, 1866 вызовов инструментов) на деревьях d8f723f/6457d77/8d61663/878cac5бэкенд на всех четырёх побайтово равен HEAD (git diff --stat <c> HEAD -- backend/ пуст, сверено). ⚠ Один опровергатель назвал деревом 535f60a (05.09), где backend/ отличается на 32 файла (пак структуры глав 2f65d1d); его адреса перепроверены по HEAD и сходятся. Каждую находку отдельный агент пытался опровергнуть с установкой «по умолчанию это не тормозит»; в документ вошли 11 переживших, 20 снятыхс доводом (§7), 8 косметических не оспаривались (§7.1). Поверх воркфлоу — моя сверка: адресные грепы по HEAD и четыре агента-воспроизводителя, гонявшие мутации на копиях дерева с полной батареей (дерево не трогалось).

ЧТО МЕРИЛИ — цену ПРИ ИЗМЕНЕНИИ, не красоту кода. Шесть срезов: вторые носители одного знания · цена одной правки (сцепление со снапшотом / хешем запроса / ключом манифеста, то есть с перепокупкой) · общность движка («заработает ли пара, которой в репо нет, без правки Go») · швы и ответственность · форма тестов как долг · инертность по git-истории.

КАК ЧИТАТЬ МЕТКИ. ✔ — сверено мной по HEAD (греп/чтение, адрес file:line живой) · ⟲ — воспроизведено исполнением на копии дерева моим агентом-верификатором 06.09 (мутация + полная батарея go test ./internal/... -count=1) · (агент) — взято из отчёта агента замера, мной не сверено · «вывод» — установлено рассуждением, не замером. Комментарий в коде поведения не доказывает: источником смысла здесь служат тест или прогон, комментарии цитируются как улика того, что проект ЗНАЕТ, а не что код ДЕЛАЕТ.

ПОПУЛЯЦИИ ЧИСЕЛ (одно «число тестов» дало у трёх наблюдателей 1157/1246/1252 — три разных популяции): 340 .go = 138 прод + 202 тест; 46 212 строк прод-кода / 54 489 тестов (1.18); ^func Test1157 в internal/, 1246 по всему backend/ (с cmd/), 1252 с Benchmark/Fuzz ✔. Батарея на HEAD e69902a: 18 ok + 2 без тестов, EXIT 0 ✔. internal/pipeline — 17 236 строк / 41 файл = 37 % прод-кода ✔.

ЭРРАТА 07.09 (оркестратор, D39.224 п.10): АДРЕСА §3 ЧАСТЬЮ ПРОТУХЛИ. Замер снят на e69902a; git diff --stat e69902a <HEAD> -- backend/ даёт 18 файлов (+1115/71) — лендинг движкового пака 07.09. Пере-снято: export.go:174/340/341:176/342/343 · terminologist.go:570:603 · PROGRESS.md:31:52. Контроль в другую сторону: якоря mining.go (234/241/244/269) не тронуты, порядок экспорта банка на стопе открыт ровно как описан. ⇒ адрес перед использованием пере-снимать командой, а не читать отсюда. Диспозиции по этому файлу ратифицированы D39.224: носители — строки 344350 и дописки в 86/114/119/182/224. ⚠ Там же (п.9) сняты ЧЕТЫРЕ утверждения, унаследованные из этого текста: наследование арм-конфигов НЕ ратифицировано D39.23 п.(г) (§7.13) · запрет двигать classifierVersionD39.195 п.5, не D39.199 п.5 (Д1) · ось «обязательность юнита» не в D39.207, а в строке 303 (Д3) · триггер Д7 — новая ИСХОДНАЯ пара, не строка 115. ⚠ Дописка 07.09, воспроизведением: §7.1 «последняя стадия двумя инлайнами при готовом finalStageWave()» называет НЕ ТУ функцию — та возвращает волну, а дублировалось ИМЯ стадии, и хелпера для него не было. §3 Д3: расхождение длинного заголовка воспроизводится ТОЛЬКО с сепаратором после юнита — без него оба предиката дают false на любой длине.


1. Ответ владельцу одной оценкой

Бэкенд НЕ техдолговый в том смысле, о котором спрашивает владелец: он не инертный, он недосторожённый. Разработку код не тормозит — тормозит одна НОРМА, наведённая денежной моделью (копить wire-правки одним касанием, §2.А.1). Из одиннадцати долгов, переживших проверку цены, вердикт «переписать» не получил ни один; все одиннадцать — «развязать точечно». Это не мягкость опровергателей: каждый из них доказал исполнением, что «переписать» здесь дороже развязки, и почти всегда — ровно на ту перепокупку оплаченных книг, которой владелец просил избежать.

Долг есть, он реальный, и он одного класса: гарантии держатся прозой, фикстурами и дисциплиной, а не гейтами. Шесть из одиннадцати находок (Д1, Д2, Д3, Д9, Д10, Д11) — «шов не сторожится ничем», и три из них доказаны до артефакта: мутант, убирающий восстановление заголовка из манифеста, оставляет батарею зелёной, а готовая книга теряет ВСЕ названия глав (Д10/Д11); мутант, сдвигающий заголовок на последний чанк, тоже зелёный; на нулевой главе 第零章 ингест режет, чанкер заголовок не снимает, и сырой CJK-маркер уезжает модели (Д3). Это риск КАЧЕСТВА, не риск скорости.

Второй противовес: долг в основном ЗАРЕГИСТРИРОВАН, а не спрятан. Бэклог — 230 строк ✔; у шести из одиннадцати находок есть строка или D-нота (Д2, Д4, Д5, Д6, Д8, Д9); у пяти носителя нет — Д1, Д3, Д7, Д10, Д11: четыре из них — непокрытый шов, Д7 — недостроенный канал данных; ни одна не архитектурная.

Здоровые части — поимённо, с уликой (не вежливость, замерено):

  • Граф импортов — DAG без циклов (go list -deps проходит ✔); internal/pipeline — ЛИСТ: его импортируют только 6 файлов cmd/tmctl и internal/archguard ✔. Практически: 17-тысячестрочный пакет — чердак, а не фундамент; перекрой внутри него не ломает ничего снаружи на этапе компиляции.
  • Архитектурные инварианты исполняет машина, не дока: internal/archguard — три go/analysis-анализатора (StageSeam/RequestSeam/EgressSeam, archguard.go:79-81 ✔) под go vet -vettool из make battery; в шапке честно перечислено, что НЕ ловится, и на каждый класс лежит фикстура без ожидания (агент).
  • Провод пинится побайтово: golden_test.go:23-29 ✔ — снапшот, каждый request_hash, тела запросов и итоговые тексты для свежего прогона И резюма; «a changed request_hash = a missed checkpoint = --resnapshot = the whole book re-billed (D15), which no code-health win can pay for».
  • Денежный контур сделан аккуратно: repin.go 329 · rebill.go 566 · snapshotdiff.go 184 строки ✔, 16 зелёных тестов (агент); предикат ре-пина консервативен по построению — незнакомое поле считается различием (repin.go:61-91 ✔); гейт согласия отказывает ДО единого вызова; стоп называет уехавшую ось.
  • Собственный мутационный каталог cmd/tmmutate/mutations.json — 211 мутаций на 43 файла ✔; проект уже практикует ту дисциплину, которой его мерили. Дыры ниже — незакрытые углы ВНУТРИ действующей дисциплины.
  • Грамматика главы — данные: <root>/<src>/structure.txt (lang/structure.go:142 ✔), и дежурный ревью-вопрос задан ТЕСТОМ, а не намерением: TestALanguageAbsentFromTheRepositoryCutsFromDataAlone (structure_newlang_test.go:16 ✔; корейский, которого в репо нет, режется одним файлом — ⟲ markers=["제"]).
  • Образец правильного разреза плоскостей уже стоит: lang/bankdata.go:14-35 ✔ — вторая эмбед-плоскость с собственным bankDataAlgoVersion, вне волнового снапшота, под стражем TestBankPlaneIsOutsideTheWaveFold (bankdata_test.go:13 ✔). Лечение Д4 — повторить этот разрез, а не изобретать.
  • Миграции — один append-only список, версия = длина списка (агент); идемпотентные шаги.
  • По оси общности переписывать нечего: в прод-Go 0 захардкоженных слагов моделей ✔, 0 маркеров TODO/FIXME/HACK ✔ (единственный хит — слово «hack» в комментарии quality.go:50), ветвление по языку в боевом коде одно и документированное (ingest.go:493, GB18030-проба — агент).
  • История не показывает буксования: rework-rate 14.1 % с учётом переименований, 7.4 % коммитов-исправлений, недельная выработка последних двух недель на уровне первых (агент, срез инертности). Раскол год-пакета (e33e3be, 44 файла) уже проведён и вышел с зелёным сьютом — механизм крупных перекроев отработан.

Единственный вектор, за которым стоит следить как за здоровьем: Runner — 16 полей на 25.07 (e33e3be) → 25 на 31.07 (8b1d8d1) → 39 на 06.09 ✔ (агент замера насчитал 18 на 25.07 — другой коммит того дня; направление то же), 178 методов в 29 файлах ✔, файл трогали 49 коммитов ✔. Но замеренная цена этого роста сегодня ровно одна и узкая — непиненный порядок предвычисления (Д9), и лечится она часами, не паком.


2. Два разных списка

2.А. Что тормозит РАЗРАБОТКУ сегодня (счёт выставляется сейчас)

  1. Норма «копить wire-правки и вносить одним касанием» (строка 182 ✔) — единственная измеренная НАВЕДЁННАЯ инертность. Снаружи текущего батча стоят и названы вслух: 131 (кап инъекции), 105(а) (вердикт-половина классификатора), 14 (ё-фолд), 119+124+115 (одним дизайн-паком), 160, 142 — часть из них чистая $0-гейтовая работа. Ждут не из-за риска, а потому что раздельный лендинг стоит денег. Корень механический: classifySnapshotMove знает ровно одно re-pinnable поле — const memoryVersionField (repin.go:54 ✔), всё остальное = moveOther = не пере-пиньится ничего (Д5).
  2. Лендинг данных заблокирован порядком. docs/PROGRESS.md:31 ✔ — стоячий запрет класть правку internal/lang/data/*.txt, пока платная книга живая. И прямо в дереве: lang/structure.go:108-110 ✔ — правка cjk-section.txt «would move EmbeddedVersion and re-cut every book alive for a refactor that changes no behaviour»; DefaultCJKStructure пришлось ВЫВОДИТЬ из файла, а не переписать, именно чтобы это обойти. Поведенчески-нейтральный рефактор в internal/lang физически дороже, чем должен быть (Д4).
  3. Порядок предвычисления не пиненный, и один счёт уже не оплачен. bookrun.go — 0 записей из 211 в собственном каталоге мутаций (то же у seeding.go, snapshot.go, bankexport.go, repin.go) при густом покрытии соседей (bankdecisions.go 2021, volume.go 11, waverun.go 811) ✔. Живое следствие — строки 224/253 ✔: mining.go:269 exportBank("bank-mining/signature-stop") стоит СНАРУЖИ ветки if !stopping (:234), а наполняющие банк writeAutoBank (:241) и seedGlossary (:244) — ВНУТРИ ✔; экран подписи читает пустой артефакт (total 0, signed 0 при 82 неразобранных поверхностях — найдено первым живым прогоном 04.09, строка 253 ✔). tmmutate не в батарее: Makefile:58 battery: build vet fmt lint test, слова mutate в Makefile нет ✔ (Д9).
  4. Шов заголовка главы не утверждается нигде. Три мутанта переживают всю батарею; причина — вырожденные фикстуры двух тестов, чьи имена обещают ровно эту гарантию (Д10/Д11).

НЕ тормозит — проверено историей, а не ощущением: «правка в четырёх местах при каждой смене стека» (ThinksOnWire, Д2) — стек моделей менялся, смены ушли в configs/models.yaml (capabilities.reasoning.control), а блок армов applyToBody побайтово одинаков во всех 6 коммитах файла с 04.07 ⟲. Закрытые словари классификатора (Д1) — на zh-ru-работе цена при изменении ноль. Нулевой канал langpack (Д7) — ни разу не оплачен: 0 файлов manifest.txt в configs/ ✔, каталог пары один (configs/langpacks = ru, zh, zh-ru ✔).

2.Б. Что удорожает БУДУЩИЙ перекрой структуры глав (сегодня не платится)

  1. embedded_version — один хеш на 8 файлов, три потребителя, три радиуса взрыва (Д4): snapshot.go:443 (провод/вердикт волн) + manifest.go:264 и :366 (CUT-тег и ключ манифеста) ✔. Правка target-ru.txt ($0-словарь вердикта) двигает CUT-тег, хотя TargetChecksFor не имеет ни одного call-site в internal/chunk и internal/text ✔ — то есть физически не может повлиять на нарезку.
  2. structure_version вне снапшота + projectRebill слеп к контентной/позиционной оси (Д6, «дыра Р6», rebill.go:33-36 ✔, строка 160 ✔): ревизия структуры через данные перекупила бы хвост МОЛЧА. Порядок величины назван проектом — ~$911 на 500 глав за вставку в середину (строка 185 ✔).
  3. Ординал главы несёт три семантики на одном int (Д8): позиция — PK chunk_status (migrate.go:126 ✔); время — spoilerBlocked (memory.go:678-686 ✔); граница — chapterNo++ (chunker.go:140 ✔). Радиус замерен: 183 не-тестовых площадки — membank 70 · store 47 · pipeline 43 · miner 7 · seed 6 · terminology 5 · chunk 3 · runevents 1 · obs 1 ✔. 64 % (117/183) лежит вне internal/pipeline — переписывание шины его не покупает.
  4. unit_resolutions платформы ключеваны позицией (строка 298 ✔) — любой перекрой сносит их целиком; сегодня цена нулевая (решения есть только у стенд-книги), после интейка — нет.
  5. Правило заголовка разъехалось по трём предикатам (длина 60 рун · осмысленность номера v<=0 · обязательность юнита), и ни один не пришпилен ни с одной стороны (Д3). Живой дефект уже сегодня: 第0章/第零章/第〇章 — ингест главу режет, чанкер заголовок не снимает ⟲, плюс номера главы и заголовка расходятся (第一章 становится Chapter 2, но рендерится «Глава 1» ⟲).

3. Поимённо: одиннадцать переживших долгов

Нумерация — по итогу воркфлоу. Для каждого: предмет · улика · цена при изменении · вердикт · двигает ли лекарство снапшот / хеш запроса / ключ манифеста / версию встроенных данных (= означает ли перепокупку уже оплаченных книг) · носитель. Д10 и Д11 — один шов, найденный двумя срезами (форма тестов и швы), разбираются вместе.

Д1. Закрытые словари классификатора живут в Go и словами в промте пары; для НОВОЙ пары гейта нет

  • Улика ✔: internal/terminology/classify.go:18 Types = {name, place, title, term}, :50 Genders, :26 CandidateTypes (+nickname); третья копия — internal/pipeline/banknote.go:80 bankTypeOK; те же слова прозой в prompts/zh-ru/classifier.md. Леджер меток промтов по построению смотрит только zh-ru: promptlabel_test.go:53 const pair = "zh-ru". Off-vocabulary строки ответа — только Log.WarnContext (terminologist.go:570), отказа нет. bankTypeOK не упомянут ни одним тестом (0 файлов) ✔.
  • Замер ⟲: переименование классов в zh-ru → красен ровно один тест TestPromptLabelsPinTheirBytes (сообщение — про сдвинувшийся sha, ни слова про словарь), документированный TM_UPDATE_PROMPT_LABELS=1 перезаписывает одну строку testdata/prompt-labels.json → батарея зелёная. Новая пара prompts/ja-ru/classifier.md с локализованными словами ответа (人名|地名|称号|用語) → батарея зелёная без единого шага автора (линт прошёл по 12 файлам, включая новый). Сквозной прогон с фейк-провайдером, отвечающим чужими словами: работа завершается, чекпойнт классификатора оплачен, Reclassified=0, реалия банкуется как place — ровно черновая эвристика, ради снятия которой фаза построена (агент, не воспроизводил).
  • Цена при изменении: сегодня на zh-ru — 0. Платится один раз, при второй паре, молчаливой потерей качества (тип неверен 1222 % и name/place форсирует транслитерацию — D39.65, агент) плюс мелким счётом (потолок фазы $1/книга).
  • Вердикт: развязать точечно. Линт КАНОНИЧЕСКОЙ формы промта (с снятыми комментариями — наивная версия даёт ложно-зелёное, слова есть в HTML-шапке) в уже существующем internal/config/prompt_lint_test.go (89 строк ✔, сегодня линтует <TAB>-плейсхолдеры и сам объявляет целью «ja-ru or en-ru pack authored by copying the zh-ru one»); прототип ~54 строки, краснеет на подложенном ja-ru, зелен на репо (агент). Плюс свести bankTypeOK к terminology.CandidateTypes (одна строка импорта; уже экспортирован).
  • Двигает ли: нет — только тест. Обратная дорога («переписать»: словарь в данные, байты промта) двигает prompt_sha256/RequestHash/classifierVersion, а D39.194 п.9 и D39.199 п.5 ратифицировали «classifierVersion НЕ двигать» ✔.
  • Носитель: нет (строки 120/258 несут КЛАСС «ни один тест не отвечает на ревью-вопрос», 204 — платформенная форма; агент). D39.70 санкционировала книжные примеры в classifier.md, про идентификаторы классов — ни слова.

Д2. Models.ThinksOnWire пересказывает Capability.applyToBody; копий три, каждая пришпилена к своей рукописной таблице

  • Улика ✔: config/models.go:389-407 ThinksOnWire — комментарий «Must mirror applyToBody exactly»; models.go:519-535 echoMineViolation — ЧЕТВЁРТАЯ версия той же таблицы, и это жёсткий fail-fast загрузки (агент: models.go:290 зовёт bad(...)); llm/capability.go:223 — арм if effort == "" || effort == "off"; enum-валидатор models.go:667-671 не пускает чужую строку — новый control нельзя сконфигурировать, не открыв тот же файл. Проза гниёт, обе штуки в дереве: capability.go:78 «effort "" leaves the provider default» — опровергается :223; models.go:404 «no switch reaches the wire» — ложно для ReasoningNone с явным уровнем (capability.go:236-238 пишет reasoning_effort).
  • Замер ⟲: сужение арма до if effort == "off" → падает только TestCapabilityWireBody/GLM_extra_body_disable:_UNSET_effort_also_disables… (capability_test.go:133); легальное обновление этого одного ожидания → полная батарея зелёная при разъехавшемся зеркале: провод оставляет thinking на дефолте провайдера, ThinksOnWire отвечает «подавляет», предупреждение об эхо-зоне врёт, и ни один тест не возражает.
  • Цена: единственный потребитель зеркала — sourceEchoExposurelogger.Warn (runner.go:384-385 ✔, «loud, not fatal»); врущее зеркало гасит РАННЕЕ предупреждение, перевод не портит. Частота ⟲: файл трогали 6 коммитов, блок switch c.Reasoning.Control байт-в-байт одинаков во всех шести с создания 04.07 (636919c); смены стека ушли в configs/models.yaml (capabilities.reasoning.control в строках 230/240/270/285/320). Прецедент был: предикат родился неверным 25.07 и чинился 02.08 — нашли паком, не гейтом (D39.91 п.4(б), агент).
  • Вердикт: развязать точечно, ~40 строк, полдня, вне окна перекроя. Экспортировать func (c Capability) SuppressesThinking(effort string) bool рядом с applyToBody; ThinksOnWire и switch в echoMineViolation — в делегацию; 8 строк таблицы config/capability_test.go перенести в таблицу llm, чтобы ОДИН кейс утверждал и тело провода, и предикат. Цикла нет: internal/llm не импортирует config.
  • Двигает ли: нет. Снапшот фолдит json.Marshal(r.Models.ResolveCapability(model)) (snapshot.go:535 ✔) — добавление МЕТОДА JSON-форму llm.Capability не меняет. Переписывание слоя (смена формы) двинуло бы снапшот каждой стадии.
  • Носитель: строка 114 ✔ (протухший комментарий висит с 02.08); зеркало как дизайн не ратифицировано.

Д3. Правило заголовка разъехалось по трём предикатам; третий («第0章») ломает пролог; ни один не сторожится

  • Улика ✔: ingest.go:307 const chapterHeaderMaxRunes = 60, применён в :323; в chunker.go RuneCountInString — 0 хитов; chunker.go:316 if !any || v <= 0 { return 0, false }parseSectionNumeral отвергает нулевой номер, который регекс ингеста принимает.
  • Замер ⟲: 第零章:序幕 / 第一章 起始 / 第二章 继续 через боевой IngestEncoded → 3 главы; SplitChunks даёт Chapter=1 Heading="" Text="第零章:序幕\n…" — сырой маркер уезжает модели; то же для 第0章; parseSectionNumeral("零"/"0"/"") = 0,false. Побочно: номера расходятся第一章 = Chapter 2 / «Глава 1», 第二章 = Chapter 3 / «Глава 2». Мутация А (страж длины В чанкер) → батарея зелёная; мутация Б (снять страж ИЗ ингеста) → зелёная. Живое расхождение на HEAD: строка-заголовок в 85 рун — ингест false, чанкер n=3 ok=true. Перекрёстного теста «оба видят одно» нет ни в одном из 97 тестов internal/chunk ✔⟲. Здоровая половина сужает предмет: классы числительных и content/separator сведены (агент: 0 расхождений на 194 528 рунах), и заявленная длинная ось на четырёх боевых книгах стоит ноль (агент: 250 778 строк, guzhenren 2283/2283).
  • Цена: сегодня на оплаченной книге — 0; на веб-новелле с прологом «第零章» — титульный хаос, ради снятия которого построена политика пак-13. Частота правок: ingest.go 9 коммитов, chunker.go 6, оба сразу 4 ✔ — платится ~раз в месяц и дёшево (последний акт 2f65d1d — сведение ДАННЫХ, не повторный вывод правила).
  • Вердикт: развязать точечно — ОДИН перекрёстный тест на реальных формах и сведение двух из трёх предикатов (длина, v<=0) к ингестовским стражам; ось обязательности юнита не трогать (D39.207 → вход в пак 2, строка 303 ✔). «Переписать» не покупает ничего: research/27 §6 п.2 ✔ — новый детектор = бамп chunkerVersion = обе волны на 100 %; там же §6 «что переиспользуется» ✔ оставляет matchHeaderLine + parseSectionNumeral живыми в роли готовой грамматики.
  • Двигает ли: ⚠ ДА — чанкерная половина. Идентичность главы не движется (chapter-id считается по ингестированному тексту ДО stripHeading, manifest.go:209, агент), но снятие заголовка меняет ТЕКСТ чанка, который уходит на провод, а chunkerVersion фолдится в снапшот (snapshot.go:344/428 ✔; собственная дока константы render.go:55-58 ✔: изменение того, где начинается глава, «is a deliberate re-translation and must not look current»). ⇒ по дисциплине проекта фикс — это бамп chunkerVersion → снапшот обеих волн → громкий --resnapshot. Сводный вердикт воркфлоу поставил этот фикс в шаг 0 как $0 — я с этим не согласен (§5): в шаг 0 идёт только ЗНАНИЕ и заготовка теста, сам фикс — в окно перенарезки, где chunkerVersion бампается всё равно. Единственный законный обход — ратифицировать, что бамп при байт-идентичности оплаченных книг не нужен; это решение владельца/оркестратора, не сессии (D39.183: заказанность решает заказ).
  • Носитель: только ось юнита (строка 303, D39.207). Длина и нулевой номер — нигде: isChapterHeader не встречается ни в одном живом доке, matchHeaderLine — в двух (строки 160/303 и research/27) ✔⟲.

Д4. embedded_version — один хеш на 8 файлов с тремя радиусами взрыва; правка $0-словаря двигает CUT-тег

  • Улика ✔: lang/embedded.go:22 — явный //go:embed на 8 файлов; :25-27 — бамп embedAlgoVersion только при смене набора/порядка. Потребителей три: snapshot.go:443 (безусловный фолд, D39.64 П0) + manifest.go:264 (cutInputs) + :366 (ключ манифеста). TargetChecksFor — call-site в bankmaterialize.go:307, runner.go:402, sanitizer.go:201; в internal/chunk/internal/text — 0 ✔. То есть target-ru.txt не может повлиять на нарезку, но двигает cutTag → ключ манифеста → на платформе delete from unit_resolutions where book_id (строка 198 ✔, PROGRESS.md:31 ✔). Хеш воспроизведён независимо и совпал с голденом capture.golden:3 (агент). lang/structure.go:108-110 ✔ — проект уже ЗАПИСАЛ, что поведенчески-нейтральный рефактор здесь стоит перенарезки всех книг.
  • Частота ✔: 8 коммитов в internal/lang/data/ за жизнь репо, все 24.0701.08; target-ru.txt — 6; с 01.08 — ни одного. Деньгами сегодня дёшево (D39.60: «$0.30.5 корпуса»; D39.64 «перекупок НЕТ» — потому что живых оплаченных книг тогда не было). Платится другим — порядком лендинга и робостью рефактора (§2.А.2).
  • Гейта нет, есть дисциплина: embedded_test.go:12 TestEmbeddedVersionMutation ✔ утверждает ОБРАТНОЕ желаемому (любой байт любого файла обязан двигать хеш); гасят цену только D39.62 Q3 ✔ («расщепление хеша пар-данных — НЕ делаем; дисциплина взамен: правки данных копятся в тот же resnapshot-батч») и ручной запрет PROGRESS.md:31.
  • Вердикт: развязать точечно — довести до ТРЁХ радиусов (нарезка / провод / вердикт) по образцу bankdata.go:14-35 ✔. Честная цена: 24 строки + три call-site + переписывание TestEmbeddedVersionMutation (обслуживание по D39.183 — ТОЛЬКО если пак это закажет, и объявляется в отчёте).
  • Двигает ли: ДА — сам ремонт. Смена набора/схемы хеширования требует бампа embedAlgoVersion (embedded.go:25-27 ✔) → EmbeddedVersion → снапшот + CUT-тег → одна глобальная перенарезка. Ремонт отдельным актом платит ровно ту цену, которую отменяет ⇒ единственный дешёвый маршрут — ВНУТРЬ уже ратифицированного единого пере-снапшота (D39.62 Q3, D39.190 п.4 ✔), тем же окном, что перекрой глав.
  • Носитель: строка 115 ✔ (триггер «первая новая целевая пара» — почти наступил), D39.64, D39.62 Q3.

Д5. Re-pin предикат знает только банк: правка $0-гейта не пере-пиньится, очередь строки 182

  • Улика ✔: repin.go:54 — одно поле; :61-91 — любое другое расхождение → moveOther; snapshot.go:447 StyleCheckVersion: checks.CheapGateVersion — версия правил, которые по собственному объявлению «never changes a chunk disposition» (cheapgates.go:23-27, агент), фолдится в денежный хеш. verdictSnapshotID — 6 упоминаний, все в комментариях, 0 определений ✔. Консервативность НАМЕРЕННА и прибита: TestRepinRefusesAnyMoveThatIsNotBankOnly (miningstop_join_test.go:1686 ✔); D15/D20 (агент): all-or-nothing приемлем для статичной книги Ф1.
  • Арифметическая поправка опровергателя (сверена ✔): buildSnapshotID маршалит ОДНУ структуру и берёт ОДИН sha256 (snapshot.go:499-503) — провод и вердикт лежат в одном объекте, перекрой даёт ОДИН новый id и ОДНУ перекупку. Довод пробы «пак заплатит дважды» — ложен.
  • Цена: не доллары (потолок $127/книга на 1500 глав, редактор $114 — archive/PROGRESS-2026-09-07-16-backend-packs.md:792, research/30:157 ✔; платная книга одна), а строка 182 ✔ — норма копить, и очередь за ней (§2.А.1). Бампы кучкуются на разработке гейтов: строка CheapGateVersion менялась 25.07/26.07/01.08/01.08, SanitizerVersion — последний 01.08; с тех пор ноль ✔.
  • Вердикт: развязать точечно — но НЕ наивной формой. ⚠ Моя поправка к сводному вердикту воркфлоу («превратить константу в множество с четырьмя вердикт-ключами»): resumeFromChunkStatus отдаёт СОХРАНЁННЫЕ Disposition/FlagReason (resume.go:24-26 ✔), пере-классификации в этой ветке нет. Объявить sanitizer/coverage/style_check_version/postcheck_gate/classifier_version re-pinnable без пере-вынесения вердикта = отдавать за $0 вердикты СТАРОГО алгоритма — ровно «HOLE (b)» спеки D15.2 §6 (агент) и тот класс «тихой пере-оценки на резюме», против которого снапшот построен. Честный дизайн — вердикт-ось с $0-переклассификацией сохранённых выходов (детерминированные гейты пере-гоняются по тексту чекпойнта, потом ре-пин); это проектирование (строки 49, 124 ось (в) ✔), не правка белого списка. (Пост-чек глоссария — исключение: он пересчитывается каждый прогон и в chunk_status не хранится — снятая находка §7.14.)
  • Двигает ли: сам предикат — нет (payload не меняется). Но любое РАСЩЕПЛЕНИЕ полей payload (например memory_version на банк и алгоритм) — смена формы = moveOther для всех книг в полёте ⇒ только внутри того же resnapshot-окна (§7.14).
  • Носитель: строки 49, 124, 182 ✔; D15, D20; D39.31 сознательно не строил (агент).

Д6. structure_version вне снапшота — зарегистрированная «дыра Р6»; лечение из пробы дороже зарегистрированного

  • Улика ✔: Structure: r.structure.Fingerprint() есть в cutInputs (manifest.go:265) и в ключе манифеста (:367); в snapshot.go слова structure — 0 хитов (контроль: соседи LangpackVersion, EmbeddedVersion там на месте, :442-443); Fingerprint() во всём internal/ — определение + 2 прод-вызова
    • 3 теста ✔. rebill.go:33-36 сам пишет: «WHAT IS NOT PROJECTED (documented, not silent): the CONTENT axis». --resnapshot сравнивает job.SnapshotID (stagerun.go:53, агент) — снапшот не двигается. Латентно: structure.txt в дереве 0 файлов (configs/ ✔; всё дерево 44 559 файлов — агент); грамматика идёт из DefaultCJKStructure, выведенной из cjk-section.txt, чьи байты в EmbeddedVersionсегодня правка грамматики громкая. Дыра открывается первым же шагом штатного пути «a source absent from this repository needs a directory, not a Go change».
  • Цена: после появления structure.txt правка грамматики двигает границы → ch.Text и плотные номера → msgsContentHash расходится → платный цикл БЕЗ --resnapshot и БЕЗ сметы согласия. Порядок: ~$911 на 500 глав (строка 185 ✔). Разработку не тормозит: обходов вокруг дыры в коде не наросло (6 хитов Fingerprint() всего ✔).
  • Вердикт: развязать точечно — зарегистрированное лечение (строка 160 ✔, research/27:73 ✔): read-path- расширение projectRebill на контентную/позиционную ось — ПОКАЗЫВАЕТ число, не заставляя платить. Плюс один явный пункт в дизайн-пак: structure_version не фолдится ни в снапшот, ни в Pack.Version() (агент) — решить, видит ли консент-гейт правку грамматики как отдельное событие.
  • Лечение из пробы («одна строка Structure в buildSnapshotID») — НЕВЕРНОЕ и дороже: repinnable() отвечает true только на bank-only move (repin.go:305-318 ✔), быстрый путь резюма требует ContentHash и (SnapshotID совпал ИЛИ repinnable) (stagerun.go:91-92 ✔) ⇒ внеси Structure в снапшот — и правка грамматики протухает ВСЕ строки chunk_status книги, включая главы, чей текст не двинулся ни на байт: адресная перепокупка становится полной.
  • Двигает ли: read-path — нет.
  • Носитель: строка 160 (отложена ПОРЯДКОМ — D39.190 п.2 ✔), 161, 185 ✔; D39.136 п.3 ✔ (сначала дизайн-пак).

Д7. Пар-данные нельзя завезти по частям: манифест требует ≥1 канал, оба канала — китайской формы

  • Улика ✔: каналов два, закрыты в Go (langpack.go:108-114: source-morphology, transliteration); :631-632 manifest declares no channel — omit the file to ship all channels, or list at least one; validate() при канале transliteration требует ПОИМЁННО пиньинь-категории palladius/initials, finals, yw, special_i + phonotactics/… (:519-527). Манифест — файл ЯЗЫКА-ИСТОЧНИКА (<root>/<src>/manifest.txt, ⟲; проба воркфлоу писала «langpack manifest» — форма та же). Отказ жёсткий: runner.go:349-352 ✔ — при gates.terminology.enabled и pack == nil прогон отказывает (RefusalBadConfig); в боевом pipeline-c1.yaml:151 enabled: true ✔.
  • Замер ⟲: ko/manifest.txt из одного комментария + ko-ru/heading.txt → ровно заявленная ошибка; пустой корень + ko/structure.txtLoadSourceStructure грузится, markers=["제"]. Агент воркфлоу: 11 таблиц по ОДНОМУ выдуманному токену — пак грузится (ровно запрещённый воркэраунд).
  • Цена: ни разу не оплачена (0 манифестов ✔, одна пара ✔). Наступает один раз — на второй паре на c1: либо фиктивные данные китайской формы, либо прогон без банка терминов (продуктовая цель № 1). К перекрою глав отношения не имеет: несущая половина — разрез — живёт вне канала (lang.LoadSourceStructure, structure.go:142 ✔) и под тестом; за гейтом остаётся только шаблон heading.txt, инертный по построению.
  • Вердикт: развязать точечно — разрешить нулевое число каналов (langpack.go:631-633, ~5 строк) внутри пака второй пары. ⚠ Крупнее и точечной правкой не закрывается: категории каналов зашиты в китайскую форму — пара, которой нужна СВОЯ транслитерация, выразить её не может. Носителя в бэклоге нет.
  • Двигает ли: нет. Байты манифеста фолдятся в хеш ТОЛЬКО при наличии файла (langpack.go:244-253 ✔), присутствующих манифестов 0 ⇒ Version() zh-ru не движется; голден пинит путь БЕЗ langpack (строка 41 ✔).
  • Носитель: нет; D39.64 П4 ратифицировала ОБРАТНОЕ намерение («источник без пиньинь-морфологии грузится без Palladius-файлов») — строка list at least one не дошла один шаг до собственной цели (агент).

Д8. Номер главы несёт три семантики на одном int — связка реальна, цена подписи завышена на порядок

  • Улика ✔: позиция — migrate.go:126 PK chunk_status, :38 jobs UNIQUE; время — spoilerBlocked (memory.go:678-686); граница — chapterNo++ (chunker.go:140), sticky-reset (wave.go:84-86, агент); ключ банка — TermID(src, sense, since, until) хеширует strconv.Itoa(since) (decisions.go:166-173). Радиус — 183 площадки, распределение в §2.Б.3 ✔.
  • Замер: термин с since_ch=0 инвариантен к пере-нумерации (агент, -overlay-тест). На реальном артефакте прогона (books/…/guzhenren-coldrun-a.db.mined-signature.yaml, только чтение ✔): 142 термина, 128 с since_ch: 0, окно у 14. В авторских сидах владельца ✔: guzhenren-seed.yaml (49 терминов) — 3 ненулевых since_ch + 1 until_ch; -v2.yaml (58) — 3 + 1. Личная плата владельца — ~8 чисел, не «все подписи».
  • Гейты уже стоят: manifestChapterID = SHA-256 по ингестированному тексту (manifest.go:197-215, агент), TestManifestChapterIDSurvivesAReCutAndAnEditElsewhere (contractblockers_test.go:245 ✔); TestEveryCutInputMovesTheTag (cuttag_test.go:26 ✔) — рефлексией по структуре, перекрой ГРОМКИЙ; перенос окон — класс moveBankOnly (research/27:84 ✔ «bank-only move — репиннимый класс»).
  • Цена: сегодня — нет улик, что платится; батарея зелёная, точка применения узкая (3 вызова Bank.Select, 5 spoilerBlocked, 14 файлов с SinceCh/UntilCh — агент). Мультиязычность не задета: ординал языконезависим.
  • Вердикт: развязать точечно — формула Chapter.ID + окна банка на chapter-ID, внутри дизайн-пака 161 (D39.136 п.3 ✔). Переписывать строго дороже: перенос окон при неподвижном крое = moveBankOnly = $0-репин; переписывание двигает сам крой = moveOther = полная перепокупка.
  • Двигает ли: окна банка → moveBankOnly ($0 для всего, чего правка не достаёт); развязка чанкера от глав → крой → moveOther ⇒ то же окно перенарезки, что Д4.
  • Носитель: строки 161, 298 ✔; контракт 14 StructureVersion (агент); D39.122 п.2(е) форма unit.id.

Д9. Runner — шина на 39 полей: цена платится не за размер, а за непиненный порядок предвычисления

  • Улика ✔: runner.go:42 — 39 полей; 178 методов в 29 файлах; рост 16 → 25 → 39 (§1). Каталог мутаций: bookrun.go/seeding.go/snapshot.go/bankexport.go/repin.go — 0 из 211; у mining.go 8, но ни одна не про порядок экспорта (FX23 — «map-before-memory»); Makefile:58 без tmmutate. Порядок в bookrun.go ⟲: seedGlossary :174 → exportBank("run-start/seeded") :180 → SplitChunksWithChapters :182 → persistManifest :194 → r.bankSrc :198 → precomputeSticky :207 → planVolume :219 → checkRebillConsent :229 → translateBookWaves :242. exportBank читает СТОР, не шину (bankexport.go:138 r.Store.GlossaryForBook ✔).
  • Замер ⟲: мутация Б (seedGlossary за консент-гейт, вплотную к волнам) → ровно 15 красных, среди них TestGoldenDeterminism, TestRebillProjectionIsPerWave, TestSingleSystemEndpointCarriesTheBankOnBothWaves, TestABoundedRunCannotOutrunTheConsentThreshold — денежный порядок держится, и громко. Мутация А (seedGlossary за exportBank("run-start/seeded") и r.bankSrc, до precomputeSticky) → батарея полностью зелёная, при этом «run-start/seeded» публикует банк ПРЕДЫДУЩЕГО прогона. Порядок пиненный ровно на денежном шве и голый на шве $0-проекций.
  • Счета уже уплачены дважды: строка 257 ✔ — тот же класс, найден мутационной пробой, СЕГОДНЯ закрыт пинами (volumeslot_test.go:866/:932, агент: WB60/WB64/FX23 → RED); строки 224/253 ✔ — ОТКРЫТЫ (mining.go:269 вне if !stopping, §2.А.3), срок «до первого внешнего пользователя».
  • Что заявлено и НЕ подтвердилось: «25 площадок присваиваний в 13 файлах» — 22 в 11 вне runner.go, греп ловил чужой ресивер sr. (агент); «read-path пишет в шину» (status.go:456) — натяжка: «pure read» там про стор, не про мемоизацию (агент). Шина — объявленный композит-корень (D39.23, тело в архиве ✔; 03-implementation-notes.md:5), толщина уже была замерена и использована как довод за процессный шов (D39.81, агент).
  • Вердикт: развязать точечно — часы, не пак. Записи в mutations.json на порядок предвычисления (bookrun.go, mining.go); tmmutate (или его порядковое подмножество) в make battery; закрыть строку 224 (экспорт на стопе — ПОСЛЕ наполнения банка). Это тот же механизм, которым проект закрыл строку 257.
  • Двигает ли: нет — тесты и содержимое артефакта, не провод.
  • Носитель: строки 224/253/313 ✔ (313: «каталог доказывает, что мутация существует, а не что она ловится»).

Д10/Д11. Шов «заголовок главы» не защищён ничем: три мутанта переживают всю батарею, под одним из них книга теряет все названия глав

  • Улика ✔: chunker.go:145 chapterChunks[0].Heading = headingText; manifest.go:664 if ch.ChunkIdx == 0 (восстановление заголовка на read-пути манифеста — tmctl export); export.go:174/:340/:341 u.Members[0].Heading; bookbuild.go:526-527 подставляет НОМЕР, когда Heading == "", :536 TrimPrefix. Тесты с правильными именами: TestSplitChunksHeadingCarried (chunker_heading_test.go:101) обещает «FIRST chunk only», негативной проверки нет; TestManifestChunksReproduceTheCut (contractblockers_test.go:294) — комментарий :298 «A long chapter so the cut produces several chunks» ✔.
  • Замер ⟲: обе фикстуры дают 1 чанк на главу (perChapter=map[1:1 2:1]; в чанкерной 2 непустых заголовка, в манифестной nonEmptyHeadings=0 и в разрезе, и в реконструкции — сравнение Heading там есть "" == ""; «длинная» глава в 5159 байт даёт 1 чанк / 1 юнит). Мутанты M1 (if i == 0 — заголовок на первый чанк КАЖДОГО юнита), M2 (if false), M5 (chapterChunks[len-1]) и M3 (export.go:340, а также все три места 174/340/341 разом) — каждый переживает полную батарею, 18 ok / 0 FAIL. Под M2 выданная книга (TranslateBook → Export → BuildBook) несёт «1», «2» вместо «Глава 1», «Глава 2» (агент; механизм подтверждён чтением bookbuild.go:526-527 ✔ — номер подставляется при пустом Heading); под M1 «Глава 1» повторяется в середине главы в артефакте tmctl export, который читает платформа (агент). На реальной книге 300 из 300 глав многочанковы (239×2, 57×3, 4×4) — набор упражняет случай, которого в проде нет (агент; главы резались по маркерам 第N节, не \f-препаратом — оговорка агента).
  • Поправки опровергателя (важны, чтобы не чинить здоровое): боевой код КОРРЕКТЕНChunkIdx сбрасывается на 0 в каждой главе, if ch.ChunkIdx == 0 ≡ «первый чанк главы» по построению, round-trip на невырожденной фикстуре 0 расхождений из 8; связь глава→чанки→edit-unit ЗАЩИЩЕНА (контрольная мутация «каждый чанк = свой юнит» убита 9 тестами; 27 тестов дают главе >1 чанка) (агент). Не защищено ровно одно — РАСПОЛОЖЕНИЕ заголовка; за все 570 разрезов батареи headingOnNonFirst = 0, но это свойство кода, не утверждение теста; упоминаний Heading в тестах 4148 в 57 файлах (счёт зависит от популяции) — ни одно про не-первый чанк; единственные проверки пустого заголовка — индекс 0 при nil-правиле (chunker_heading_test.go:130) и то самое "" == "" (contractblockers_test.go:316) ⟲.
  • Вердикт: развязать точечно, ДО дизайн-пака. (а) обеим фикстурам — главу, которая реально режется, и правило заголовка zh-ru; (б) утверждение «не-первый чанк главы несёт пустой Heading»; (в) M1/M2/M5 в mutations.json. Доказано исполнением: тело утверждения байт-в-байт + другая фикстура → под HEAD ok, под M1/M2 FAIL (агент). Класс назван проектом вчера — D39.208 п.5 ✔ («фикстура, в которой две различные величины совпадают, делает весь их класс расхождений НЕВИДИМЫМ»); последний коммит по обоим тест-файлам 2f65d1d (06.09) ✔ добавил 134 строки в mutations.json и оставил обе фикстуры вырожденными. В каталоге ни одна правка не содержит chapterChunks, ChunkIdx == 0, .Heading, Members[0], ApplyHeading ⟲.
  • Двигает ли: нет — боевой код не трогается.
  • Носитель: нет. chunker.go в каталоге мутаций отсутствует вовсе ✔.

4. Большой перекрой глав: дизайн-пак, не переписывание

Довод не вкусовой, а денежный и посчитанный проектом раньше этого замера: research/27 §6 п.2 ✔ — любой новый детектор границ бампает chunkerVersion, двигает ОБА волновых снапшота, $0-репин не работает (repin.go:61-91), перекупаются обе волны на 100 %; golden_test.go:23-29 ✔ пинит провод побайтово ровно потому, что сдвинутый request_hash = вся книга заново. Переписывание покупает ноль и стоит ровно того, чего владелец просил избежать. Под мандатом D39.216 ✔ («где форма не тянет — переписывать; „дешевле подпереть“ доводом не является») развилка выглядит так:

Спроектировать ЗАНОВО и отрефакторить без заплат (форма не тянет):

  • Формула Chapter.ID + развязка чанкера от глав (строка 161). Единственный пункт, который заплатой закрыть нельзя: 183 площадки, 64 % в membank+store. Половина фундамента стоит и запинена (manifestChapterID, TestManifestChapterIDSurvivesAReCutAndAnEditElsewhere). Сюда же — два числа главы: плотный Chapter (PK, адрес redrive) и номинал в заголовке (Heading рендерит исходное n) уже расходятся на неплотной нумерации и на прологе ⟲ — дизайн обязан назвать, каким числом адресуется глава.
  • Банк-окна since_ch/until_ch → chapter-ID (decisions.go:166-173, memory.go:678-686). Класс переноса moveBankOnly = $0-репин; миграция мелкая (14 из 142 добытых, ~8 чисел в сидах ✔).
  • Предикат осей classifySnapshotMove + вердикт-ось с $0-переклассификацией (Д5). Сегодня — одна строковая константа; обязан стать явной картой осей (провод / вердикт / банк / контент / позиция) с честным ответом, что делать на каждой: провод — купить, вердикт — пере-вынести из чекпойнта за $0, банк — ре-пин, контент — показать смету. От него зависит и очередь 182, и то, будет ли перекрой молчаливым.
  • projectRebill — контентная и позиционная оси (Д6, дыра Р6) — одним куском с предикатом выше: две стороны вопроса «какая ось уехала».
  • Расщепление EmbeddedVersion на три радиуса (Д4) — по образцу bankdata.go; сам ремонт двигает хеш ⇒ только внутри окна перенарезки.
  • structure_version как самостоятельная ось консент-гейта — решить в дизайне; НЕ через buildSnapshotID (Д6).
  • Правило заголовка — три предиката свести к одному источнику стражей и завести паритет-тест ингест↔чанкер (Д3); ось юнита — по D39.207 в пак 2.
  • unit_resolutions платформы (строка 298) — аддитивно, но в ТОМ ЖЕ дизайне: разрешать (chapter, unit) в идентификатор при приёме события (D39.202, агент).

Оставить и достроить (переписывать нечего):

  • matchHeaderLine + parseSectionNumeral — research/27 §6 ✔ прямо оставляет их в роли готовой грамматики / первого паттерн-пака индуктора.
  • Runner как композит-корень — объявленное решение (D39.23, статус «частично»), не недосмотр. Перекрой трогает в шине микроскопическую поверхность: r.structure 5 ссылок в 3 файлах, r.cutStructure 2, r.bankSrc 4 (агент). Денежный порядок держат 15 тестов и голден ⟲.
  • ThinksOnWire/echoMineViolation (Д2) — полдня, ВНЕ окна перекроя, снапшот-нейтрально.
  • Нулевой канал langpack (Д7) — пятистрочная правка внутри пака второй пары; к главам не относится.
  • Словарь классификатора (Д1) — линт в существующем файле, $0.
  • Шов заголовка (Д10/Д11) — фикстуры и мутации, боевой код цел.

5. Порядок развязки — что первым, чтобы остальное подешевело

Шаг 0 — часы, $0, снапшот не двигают, ДО дизайна. Иначе дизайн принимается без обратной связи ровно там, где режут:

  1. Невырожденные фикстуры chunker_heading_test.go / contractblockers_test.go + утверждение о пустом Heading не-первого чанка + M1/M2/M5 в mutations.json (Д10/Д11).
  2. Мутации на порядок предвычисления bookrun.go/mining.go + tmmutate в make battery + закрытие строки 224 (Д9).
  3. Линт словаря классификатора по канонической форме промта в prompt_lint_test.go + bankTypeOKCandidateTypes (Д1).
  4. ThinksOnWire → делегация в Capability.SuppressesThinking (Д2) — снапшот-нейтрально, в любое окно.
  5. По Д3 в шаг 0 идёт ЗНАНИЕ, не фикс: паритет-тест ингест↔чанкер на 第零章/85-рунной строке сегодня КРАСНЫЙ, а его зелень требует правки чанкера, которая по дисциплине бампает chunkerVersion (§3 Д3). Тест пишется сейчас, приземляется вместе с фиксом в шаге 3. (Сводный вердикт воркфлоу клал фикс в шаг 0 — здесь расхождение с ним, довод в Д3.)

Шаг 1 — первым в дизайне: предикат осей (classifySnapshotMove + projectRebill + вердикт-ось с переклассификацией). Он удешевляет всё остальное двумя способами: снимает норму «копить одним касанием» (то есть распечатывает очередь 182, включая $0-гейтовые правки) и превращает перекрой из «молчаливая перекупка» в «названная и согласованная». Вся обвязка вокруг него стоит и покрыта (1079 строк ✔, 16 тестов).

Шаг 2 — расщепление EmbeddedVersion по трём радиусамТЕМ ЖЕ окном, что перенарезка: ремонт сам двигает хеш, отдельным актом он платит цену, которую отменяет (D39.62 Q3, D39.190 п.4).

Шаг 3 — Chapter.ID + развязка чанкера от глав + перенос банк-окон + сведение правила заголовка (с фиксом 第0章 и стража длины)moveBankOnly для окон, одна перенарезка для остального.

Шаг 4 — аддитивно: контракт 14, unit_resolutions платформы, фронтовая половина строки 160.

Ключевое: шаги 2 и 3 — ОДНА перенарезка. Разнести их = заплатить дважды. Единственное дешёвое окно — пока платная книга одна (строка 161: «окно закрывается первой книгой внешнего пользователя»).


6. Чего делать НЕ надо — где рефакторинг дороже, чем терпеть

  • Не переписывать Runner и не дробить internal/pipeline дальше «ради здоровья». Норматив 12-go-style-notes.md §1: размер — не критерий сплита; D39.23 сплит по измеренной связности исполнен, остаток — композит-корень. Перенос seedGlossary за консент-гейт валит 15 тестов ⟲ — денежный порядок держится; 64 % радиуса перекроя лежит в membank+store ✔; переписывание шины двигает снапшот-байты.
  • Не вносить Structure в buildSnapshotID «одной строкой» — адресная перепокупка становится полной (Д6).
  • Не расширять белый список classifySnapshotMove вердикт-ключами как «быстрый выигрыш» — без переклассификации это отдача стухших вердиктов за $0 (Д5, resume.go:24-26).
  • Не строить детектор границ с нуля — research/27 §6 п.2: бамп chunkerVersion → 100 % обеих волн.
  • Не трогать ось «обязательность юнита» вне пака 2 — D39.207, строка 303.
  • Не менять байты промтов, словарей и classifierVersion ради стройности — D39.194 п.9, D39.199 п.5.
  • Не втаскивать снапшот-нейтральные мелочи (Д1, Д2, Д7) в окно перекроя — им там не место, они дешевле отдельно. И симметрично: не откладывать шаг 0 «до пака» — он $0 и он условие качества самого дизайна.
  • Не копить wire-правки дальше без шага 1 — копление и есть цена; после предиката осей оно перестанет быть нужным.
  • Не заводить второго носителя там, где опровергатель показал один источник: res.Flagged кормит exit-код, finished.outcome и CompletedWithFlags из одного счётчика (§7.11); словарь событий движок↔платформа аддитивен по построению (§7.16).

7. Что снято опровергателями — с доводом

Двадцать находок проб пали на проверке цены. Каждая — с решающим доводом, чтобы следующая смена не искала заново; побочные РЕАЛЬНЫЕ мелочи, которые опровергатели подняли попутно, помечены «остаток». ⚠ Доводы этого раздела — из отчётов опровергателей (каждый мерил на копии дерева исполнением); мной сверены только адреса с меткой ✔. Опровергатели работали на четырёх HEAD одного дня с побайтово равным backend/ (шапка).

  1. «StatusReport → render.go → golden: триада, через которую проходит каждая фича» — ОСТАВИТЬ. Нога «golden» ложна: golden_test.go не упоминает StatusReport/.Status() (0 хитов), добавление поля в StatusReport голден тронуть не может; renderStatusJSON — голый json.Encoder, новое поле на машинном пути стоит ноль; платформа читает status --json по аллоулисту на 11 полей. D39.181 ратифицировал класс «новое поле контракта шва». Гейт стоит там, куда перекрой садится: TestEveryCutInputMovesTheTag.
  2. «История уже оценила перекрой: 13 файлов, 5 пакетов, 0 миграций» — не долг. Правильный ответ владельцу (дизайн-пак), но цены не называет: вторая улика (0e69bc1) раздута ~4.5×; «ноль миграций» — потому что оба коммита ординал не двигали. Настоящая цена — бамп manifestVersion (manifest.go:709-711), и она уже погашена нормой: TestTheManifestDocumentVersionDoesNotMoveForAnAdditiveField.
  3. «Две идентичности главы: int в БД и контент-хеш в манифесте» — ОСТАВИТЬ. Шифт-мап пака структуры глав (backend/docs/CHAPTER_STRUCTURE_SHIFTMAP.json): 13 стенд-книг, same=2355, moved=0, ключ манифеста пере-минтился на 13/13 — форма двинулась, ни одна идентичность не переехала, $0. Две оси — ратифицированное различение (D39.122 п.2(е)), моста между ними нет (manifestChapterID только в manifest.go), гейт против молчаливой перепокупки стоит (TestTheCONSENTGateSeesAnInPlaceSourceEdit, rebillsource_test.go:138 ✔). Проба перепутала, где дорого: сдвиг границ = вся книга, перенумерация = хвост.
  4. «internal/pipeline — 17 тыс. строк, 31 % имён протекает между файлами» — ОСТАВИТЬ. 31 % — МЕДИАНА репо (store 47 %, miner 38 %, config 31 %); метрика меряет «это Go-пакет». Перекрой 06.09 (2f65d1d и соседи) принёс в internal/pipeline 138 строк в 6 файлах против 1197 в chunk и 261 в lang — семантика главы ЛОКАЛИЗОВАЛАСЬ в пакеты, созданные сплитом D39.23. LoadPipeline 353 стр. — другой пакет (config). Остаток: время прогона каталога мутаций (126 записей × 71 с) — строка 313, D39.214.
  5. «Стабильный chapter-ID движок не читает» — ОСТАВИТЬ. Читает ПЛАТФОРМА, для которой он построен (platform/internal/ingest/manifest.go:124-127, books.ordered_through_chapter_id, D39.208); внутри движка ординал — ратифицированная граница (D39.122 п.2(а)). Компилятор-пробы: смена типа даёт 25/10 и 44/14 ошибок, 100 % в одном пакете — подпись здоровой границы. Перевод окон банка на хеш невозможен по смыслу (spoilerBlocked — порядковый предикат; контракт 14 README:343).
  6. «Чанкер принимает решения четырёх слоёв в одной функции» — ОСТАВИТЬ. Композиция, не клубок: три из четырёх решений за своими швами (stripHeading, assignEditUnits, chapterDraftChunks), инлайном — две строки нумерации. Перекрой 06.09 взял с функции ~10 механических строк. Остаток → Д3 (паритет нуля) и «два числа главы» → §4.
  7. «status.go: 1183 строки, read-model и деструктивная команда на одном получателе» — ОСТАВИТЬ. Перекрой 2f65d1d тронул status.go одной строкой в 717 строках от ResetChunkStages; дизайн-док перекроя status.go не называет; редрайв под тремя мутациями падает четырьмя гейт-тестами (D17 п.5(в)(г) исполнены). Норматив против разреза по размеру. Остаток (доки): backend/README.md называет status.go read-only проекцией — ложь, там единственный деструктивный путь; status_test.go:376 обещает замок, который в одиночку не держит (с убитым гардом тест проходит, потому что первым отказывает rebill-гейт).
  8. «Новая ЦЕЛЬ требует правки Go (//go:embed) и пере-снапшотит все пары» — ОСТАВИТЬ. Механизм точен, цена не платится: гейт ловит громко (TestGoldenDeterminism красен ровно один из 21 пакета), событие не происходило ни разу (4 правки строки embed, все до 01.08 ✔), забуковано строкой 115 с триггером, соседняя форма разобрана владельцем в D39.62 Q2/Q3. Условный фолд измеренно снапшот-нейтрален (embed-v1-3df4329e3b9e байт-в-байт). Брать одним касанием со строкой 120 при второй цели. → влилось в Д4.
  9. «Ингест не режет по безъюнитной грамматике» — ОСТАВИТЬ. Недостижимо данными (0 structure.txt на 44 559 файлов); закрыта оговорка строки 303 — три живые книги владельца через настоящий IngestEncoded: EPUB 32/71 глав (declared), плоский en-txt — 1 глава, и заявленная правка на нём НИЧЕГО не меняет (заголовки словесными числительными в блоке оглавления, в теле их нет). Правка 24 строки, батарея зелёная, но делать СЕЙЧАС хуже, чем потом: ветка без корпуса для замера точности. ⚠ Агент честно сообщил заражение своей копии от соседней сессии (общий scratchpad) — перемерено в приватном каталоге.
  10. «Алфавит майнера зашит в unicode.Han» — ОСТАВИТЬ. Ратифицированная незастроенная фича с номером (D39.50 п.8, строка 35): нужен ВТОРОЙ детектор, не правка этого; молчание не тихое (пак без данных падает громко, TestEmissionFunnelIsReported). Развязка измерена: 50 строк, зелено, Pack.Version() не движется — но покупает ноль (en по-прежнему 0). Второй хардкод — allCyrillic в miner_palladius.go:128. ⚠ TestMinerFullBookParity на этой машине скипается (нет jieba-артефакта).
  11. «„Чем кончился прогон“ решается дважды: exit-код и finished.outcome» — ОСТАВИТЬ. Поле finished.outcome — write-only во всём репозитории: платформа кадр finished выбрасывает намеренно (sink.go:195), статус берёт из exit-кода; несущая половина загейчена (мутация exit-кода валит 3 теста + голден exit=2); источник ОДИН — res.Flagged. D39.181 §1 п.2 рассматривал биективность и отверг. Остаток: дешёвая дописка tracedCtx() во флаг-фикстуре.
  12. «Пять fold-тестов стерегут функцию, которой в проде нет» — снято, форма «развязать точечно». Волновое разделение защищено snapshot_wave_test.go (пропущен грепом пробы): три мутации — 23, 1 и 6 падений. И инверсия: TestBanknoteSnapshotFold — ЕДИНСТВЕННЫЙ сторож мутации TokenBudget в общем buildSnapshotID; удалить «мёртвые» тесты = снять защиту от тихой перепокупки. Остаток: snapshot.go:206-207/:218 — протухшие клаузы про последовательный драйвер (драйвер один, bookrun.go:242).
  13. «Пять pipeline-конфигов дублируют настройки; арм-шапки лгут» — снято, «развязать точечно». Механизм наследования ЕСТЬ и ратифицирован (D39.23 п.(г), configs/pairs/<пара>.yaml); segmentation — 0 раз в пяти конфигах, живёт в одном пар-файле; эмпирика опирается на пре-регистрированные книго-локальные армы, не на репо-армы. Остаток — живая мина с деньгами: шапки трёх армов «ЕДИНСТВЕННОЕ изменение» ложны на 1113 ключей; STACK.md:12 «флип = один арм-конфиг» молча снимает банкноту + терминолога + классификатор; bank_contour_test.go грузит только c1. Лечение — минуты.
  14. «memory_version — смешанная ось, ре-пин отдаёт стухшие вердикты за $0» — ОСТАВИТЬ. Опровергнуто исполнением в обе стороны: вердикт пост-чека НИКОГДА не пишется в chunk_status (export.go:250-256), живёт в retrieval_state и пересчитывается каждый прогон — 0 новых вызовов, $0, вердикт пошёл за новым алгоритмом. Ось — D15.2 §6 «HOLE (b)», строка 49. Расщепление поля = смена формы payload = moveOther для всех книг в полёте → только в окне 182. Остаток: snapshotdiff.go:157-159 при движении одного memory_version дописывает причину, которой не проверял («the auto-bank grew»).
  15. «Денежный контент-хеш снапшота без рефлексивного стража» — ОСТАВИТЬ. Посадки: удаление embedded_version из payload → TestGoldenDeterminism красен (56 из 65 json-тегов пинятся голденом); второй рубеж — ContentHash на резюме (rebillsource_test.go:18-22). Страж в 45 строк пишется без правки Go (payload уже возвращается строкой). Реально не прикрыты два ЛАТЕНТНЫХ слота — langpack_version (строка 41) и escalate_provider_* (строка 229).
  16. «Словарь событий движок↔платформа разъехался на 12 полей» — ОСТАВИТЬ. 12 = три аддитивных добавления (volume 8, money 3, shortfall); «только у платформы» — 0 из 24; 11 из 12 висят на кадре, который платформа выбрасывает намеренно; 12-е — сознательное не-зеркало с нотой (D39.203). Гейт есть: TestANewFieldInsideAKnownEventIsIgnored. chapters_total/units_total — поля МАНИФЕСТА, а он гейчен жёстко; unit_done покрыт эпохой structure_version/shape_epoch (D39.165).
  17. «Конвертер цен написан руками, CacheWritePerM выпадает бесшумно» — ОСТАВИТЬ. «День платной записи в кэш» исполнен на нетронутом коде: 3.75 дошло до леджера, CostUSD и EstimateUSD считают. Строгий декод KnownFields(true) ловит опечатку; ноль — вендор-факт (D39.137). Мутационный пробел на строке, которую никто не правит; закрывается одной фикстурой с ненулевым значением (~28 фикстур пишут явный 0).
  18. «Раскладка проекта собрана заново в 6 из 9 setup-хелперов и 13 файлах inline» — ОСТАВИТЬ. В YAML структуры глав НЕТ (0 ключей из 31); перекрой ведётся компилятором (переименование поля — 11 ошибок с адресами); худший случай (новый обязательный ключ, 407 падений) чинится одним sed за 0.013 с; риск перепокупки держит BriefHash, не фикстуры. Консолидация размыла бы мультиязычное разнообразие фикстур (D39.208).
  19. «Остаточная жёсткая письменность: 29 строк в 10 файлах» — ОСТАВИТЬ. Хвост ратифицированной карты D39.60 (149 сайтов / 45 файлов), носители — строки 79/35/115; три члена дизъюнкции в chunker.go:265 мёртвы (IsLetter покрывает); латинный класс уже закрыт TargetScriptNonLatin(); до 29 строк дело не доходит — раньше упирается в строку 115. Поправка в пользу пробы: для греческой/арабской цели детектор не нем, а ложно срабатывает — недостижимо, целевых данных одно target-ru.txt.
  20. «Два шаблона фолда гейта с противоположной ценой, ничто не заставляет выбрать дешёвый» — ОСТАВИТЬ. Заставляют четыре носителя: D39.60 §6.2 (условный фолд — ратифицированное правило), стиль-норма 12-go-style-notes.md:7, два зелёных теста, три рантайм-стопа; 3 из 3 гейтов после появления дешёвой формы выбрали её или не фолдятся. Линт отличить «опциональную фичу» от «оси у всех» не может — генератор ложных срабатываний.

7.1. Не оспаривались (косметика, 8 находок проб — носителей нет, цена копейки; всё «(агент)»)

  • Словарь родов — четыре носителя (memory.go:946, memseed.go:595, classify.go:50, промт), и текст собственной ошибки memseed.go:614 уже врёт о принимаемом множестве.
  • zh/numeral.txt уже эталона cjk-section.txt на 〇两兩 — вопрос владельцу: намеренно или недосмотр (потребитель — канал «числительное + ранг-слово» майнера).
  • unicode.Version вплетён в 5 версионных констант, 4 — в нерепинуемые поля: апгрейд Go = перепокупка всех незакрытых книг; stdlib-юникод прибит комментарием, x/text — механизмом.
  • 37 % не-тестовых строк — комментарии (16 048 против 27 585 кода); множитель цены каждого переименования.
  • «Последняя стадия» двумя одинаковыми инлайнами (export.go:279-282, quality.go:340-343) при готовом finalStageWave().
  • Реестр кодировок — закрытый Go-switch (Shift-JIS/EUC-KR требуют case; отказ громкий); резерв токенов банкноты выведен для кириллической цели (для кана-цели занижен ~2.1×, строка 40).
  • 97 из 200 тест-файлов зовут неэкспортируемые имена — переименование, не форма; в оценку перекроя не идёт.
  • 6 из 16 никогда не исполняемых функций — мёртвый код (SafeGo, TokenizeCyrillic, UnitScaleHard/Soft, Voices, Pairs); SafeGo с докой про паник-рекавери, которого никто не использует.

8. Чего замер НЕ покрыл — прямо

  • Скорость разработки как таковая. Метрики цикла в репо нет; срез инертности мерил rework-rate и недельную выработку по git, а один коммит здесь = один пак целой сессии, так что «файлов на коммит» сцепление НЕ измеряет (агент оговорил сам). Окно замера — 9 недель.
  • Долларовая цена перекроя на сегодняшнем корпусе. Есть только потолок $127/книга на 1500 глав (archive/PROGRESS-2026-09-07-16-backend-packs.md:792 ✔) и «100 % обеих волн» из research/27; сметы projectRebill по реальной книге под перекрой никто не снимал.
  • Не-CJK ингест на живой книге — покрыт частично одним опровергателем (§7.9: EPUB режется по оглавлению, плоский en-txt — одна глава); словесные числительные и книги без разметки глав в теле — не мерены нигде.
  • Мутационное покрытие prompts/ — ноль по построению (2 не-Go цели из 211, обе не в prompts/) ✔.
  • TestMinerFullBookParity скипается на этой машине (нет eval/exp16/data/): фрозен-паритет майнера с питоновским эталоном в зелени ни одного агента НЕ участвовал (агент).
  • Платформа, фронт, полигон — не мерились; платформа фигурирует только как потребитель швов.
  • Замер Д10/Д11 на реальной книге (300/300 многочанковых) сделан по маркерам 第N节, а не \f-препаратом пайплайна — распределение может слегка сдвинуться, вывод (даже кратчайшая глава даёт 2 чанка) — нет (агент).
  • Рантайм и производительность — не в предмете; единственное касание — время прогона каталога мутаций (строка 313).
  • Сквозные прогоны с фейк-провайдером (Д1) и git log -L по телам правил (Д3) — только у агентов замера, мной не воспроизведены.

9. Реестр сверки — что сверено лично, что агентами, чему доверяю меньше всего

Сверено мной по HEAD e69902a (✔): все file:line в §3 без метки «(агент)» перечитаны; счётчики файлов, строк, тестов (три популяции), Runner (поля/методы/рост по git show), каталог мутаций (211/43 и нули по файлам), Makefile, 183 площадки ординала с распределением, mining.go порядок, 8 коммитов lang/data и история строки embed, бампы CheapGateVersion/SanitizerVersion, TargetChecksFor, verdictSnapshotID, Fingerprint(), банк-артефакт и сиды (только чтение), строки бэклога 14/31/35/41/49/114/115/124/160/161/164/ 182/185/198/224/253/257/298/303/313 и число строк 230, тела D15/D20/D39.23/60/62 Q3/64/81/121/136 п.3/183/ 190 п.2,4/194 п.9/199 п.5/207/208 п.5/216, research/27 §6 п.2/п.4/«что переиспользуется»/этап 1, батарея на HEAD (EXIT 0), DAG без циклов, импортёры pipeline, анализаторы archguard (3), TODO-маркеры (0).

Воспроизведено исполнением моими агентами-верификаторами на копиях дерева (⟲): Д9 — мутации А (зелено) и Б (15 красных, поимённо); Д3 — 第零章/第0章 сквозь ингест→чанкер, parseSectionNumeral на нуле, обе мутации стража длины (зелено), отсутствие паритет-теста, упоминания в доках; Д7 — ошибка нулевого канала, загрузка ko/structure.txt из пустого корня, runner.go:349-352, pipeline-c1.yaml:151; Д10/Д11 — M1/M2/M3/M5 (каждый 18 ok / 0 FAIL), измеренная вырожденность обеих фикстур, отсутствие утверждения о не-первом чанке, отсутствие записей каталога по шву; Д1 — новая ja-ru пара с чужими словами (зелено), переименование в zh-ru (один красный, штатный рефреш, зелено), 0 тестов на terminology.Types/bankTypeOK, содержимое prompt_lint_test.go; Д2 — проба 1 (один сабтест), проба 2 (зелено при врущем зеркале), 6 коммитов файла с байт-идентичным блоком армов, YAML-контролы, строка 114 с 02.08. Ни одно из заказанных воспроизведений не опровергло утверждение; расхождения — только адреса на 12 строки, поправлены в тексте.

Взято с агентов замера, не сверял: сквозной прогон Д1 с фейк-провайдером; git log -L по телам правил заголовка; 300/300 многочанковых глав; шифт-мап и компилятор-пробы §7; всё в §7.1; «1079 строк» проверил, «16 тестов» — нет; «5 боевых call-site TargetChecksFor» — по моему грепу их 3 + определение (в chunk/text ноль в любом счёте).

Чему доверяю меньше всего:

  1. Утверждению, что расщепление EmbeddedVersion — «24 строки + три call-site» и что образец bankdata.go переносится один-в-один: три радиуса требуют решить, к какому относится каждый из 8 файлов (injection.txt едет на провод, cjk-section.txt/terminator.txt — резка, target-ru.txt — вердикт, refusal.txt, sentence-abbrev.txt, lang-script.txt, script-series.txt — не классифицированы ни одним агентом). Это дизайн-вопрос, а смета — оценка по чтению.
  2. Моей же поправке к Д5 (вердикт-ось требует переклассификации): resume.go:24-26 сверено, но какие именно стадийные вердикты зависят от каких версий (sanitizer — да; coverage/style_check — по комментариям «observability, never hard gates», то есть, возможно, ни от чего) — по коду не размечал. Направление уверенное, объём — нет.
  3. Частотному доводу Д3 из git log -L («тела правил заголовка двигались по 3 раза каждое»): переезд файлов из pipeline в chunk (e33e3be) ломает -L, и агент замера это не оговорил; мой счёт по файлам (9/6/4 коммита) — другая популяция. (Историю армов ThinksOnWire мой агент снял по всем 6 ревизиям файла целиком — ей доверяю.)
  4. Числу «~$911 на 500 глав» — это оценка самого проекта (строка 185), не замер этого воркфлоу.