textmachine/docs/archive/reports/CONTRACT_BLOCKERS_2026-08-08.md

79 KiB
Raw Permalink Blame History

Отчёт бэкенд-сессии: движковые блокеры контракта API (строки 99 · 100 · 101 · 125 · 145)

РЕВЬЮ-ШАПКА ПРИЁМКИ (оркестратор №15, 09.08.2026; первична над телом). ПРИНЯТ И ЗАЛЕНДЕН 0e69bc1, D39.122. Приёмка тремя раундами, все клеймы пере-раны исполнением. Раунд 1 (7 агентов: слепой дифф · кросс-модельный опровергатель-Opus · пере-ран · 7/7 посадок CAUGHT · 2 охотника · перф на независимом стенде с бинарём «до» из 06f0342): перф §2.4 воспроизведён (status 1.401.46→0.050.07 c), формы подтверждены; фикс-лист F1F6, несущее — F1 MAJOR: unit.id первого юнита главы переживал любой разрез («честная граница» §2.2 была ложной; закрыт тегом разреза). Раунд 2 (ре-чек дофикса с пере-мутациями): поймано — V2-1 не исполнен и пропуск не задекларирован · клейм покрытия F6 ложен (границу не тестировало ничто) · cutTag без langpack/embedded/norm (зазор спеки приёмки) · manifestVersion не бампнут → микро-лист V2-1..V2-4. Раунд 3 (финальный, по слову владельца): состав V2-диффа точен, каждая ось перебита мутацией заново, слепое пятно §11б воспроизведено буквально, счёт 717→749 поимённо, стенд tm-manifest-v2 c единым cutTag, cmp-проекции IDENTICAL; батарея-гейт лендинга EXIT=0. Ратификации — D39.122 п.2 (граница report · done=«разрешено волной» · --ceiling-usd = книжный потолок в силе, пересчёт на платформе · девиации §8 · heading = временный рендер, страж — строка 160). Принятые NOTE без дофикса: тест границы manifestMaxChunks пинит механизм, но не значение константы (absurd выводится из неё же) — значение сторожится ревью · две из четырёх точек банк-экспорта маскируют друг друга · атомарность против конкурентного читателя не наблюдаема из одного процесса (объявлено в §11б честно). Строки бэклога из приёмки: 163 (x/text|x/net двигают нарезку мимо всех ключей — класс чекпойнт-плоскости) · 164 (rebill-проекция с nil baseMemory завышает RebillUSD). Урок формы: соло-самопроверка сессии не увидела ничего из найденного тремя независимыми проходами; вывод сессии «мои клеймы покрытия проверять исполнением, а не читать» — подтверждён и разделён приёмкой (два клейма покрытия оказались пустыми при зелёной батарее).

Дата: 08.08.2026. Промт: docs/BACKEND_CONTRACT_BLOCKERS_SESSION_PROMPT.md (оркестратор №15, D39.119). Деньги: $0 — платных вызовов не было ни одного; артефакты получены read-путями на стенд-книге и мок-провайдером в тестах. Не коммичу — дерево готово к лендингу.

Каждое ЧИСЛО ниже дано с командой, которой получено. Каждый новый файл-артефакт открыт и прочитан глазами (образцы — §7).


0. Сводка

Строка Что построено Форма артефакта
99 Пофазный прогресс draft N/M ∥ edit N/M в юнитах — книга и КАЖДАЯ глава поле progress в StatusReport/ChapterPassport (--json) + строка Phases (units resolved by wave) в человеческом выводе
100 Персист манифеста глав/чанков + стабильный id главы; read-пути перестали резать книгу сайдкар <project_db>.manifest.json + новая $0-команда tmctl manifest
101 Машиночитаемая ПОЛНАЯ таблица подписи банка сайдкар <project_db>.bank-stop.json рядом с текстовым .bank-stop.txt
125 Экспорт СОСТОЯНИЯ банка тремя статусами сайдкар <project_db>.bank.json, обновляется на границах стадий
145 Потолок USD на прогон аргументом флаг `tmctl translate

Батарея: make batteryEXIT=0, golangci-lint0 issues, go test ./... -race зелёный целиком. Тесты: 717 → 749 (^func Test, посчитано исполнением, см. §6). Удалённых нет.

⚠ Отчёт описывает состояние ПОСЛЕ адверсариального ревью сессии (§9) И ПОСЛЕ фикс-листа приёмки (§11). Ревью — два независимых верификатора, оба СЛЕПЫЕ к этому отчёту (один судил промт против диффа, второй охотился за дефектами вне карты отчёта). Они СОШЛИСЬ на одном и том же несущем дефекте, которого соло-проход не увидел; всего исправлено семь находок, каждая из них пере-проверена убийством мутации. Всё, что ниже, — уже исправленный код; что именно было сломано и как поймано — §9.


1. Строка 99 — пофазный прогресс

Код: backend/internal/pipeline/status.go:39-72 (типы WaveCounter/PhaseProgress), status.go:82 (поле главы), status.go:133 (поле книги), status.go:441 (счёт по волнам); backend/cmd/tmctl/render.go:588 (человеческая строка) + render.go:646 (waveCell).

Форма JSON (называю явно — платформа собирает Progress контракта из неё 1:1):

"progress": {
  "draft": { "done": 1234, "total": 4276 },
  "edit":  { "done": 0,    "total": 4276 }
}

То же поле progress лежит внутри каждого элемента chapters (это и есть цена, которую К-10 контракта назвал «пофазные счётчики НА ГЛАВУ в строке 99»; продуктовую половину К-10 — что показывать в дереве — решает автор контракта, я её не решал).

Человеческий вывод (строка добавлена после существующей Progress:):

Progress: 0/14 units (0.0%) — done=0 in_progress=14 flagged=0 pending=0
Phases (units resolved by wave): draft 14/14 · edit 0/14

Решения, которые я принимал сам, и их обоснование:

  1. Единица обоих счётчиков — ВЫХОДНОЙ ЮНИТ, знаменатель общий (TotalUnits). Альтернатива — считать черновую волну в ЧАНКАХ (её собственная гранулярность) — даёт два разных масштаба в одном объекте, и платформа, собирая «одну дробь» (контракт: «The phases exist for the DATA, not for the screen»), обязана была бы знать про разницу. Общий знаменатель делает (draft.done+edit.done)/(2·total) честной арифметикой без домыслов.
  2. done = РАЗРЕШЕНО волной, а не «ok». Юнит засчитывается, когда волна записала все строки chunk_status, которые ему должна — ok, flagged, skipped безразлично. Причина: флагнутый юнит ЗАКОНЧЕН (без явного redrive его никто не переатакует), и счёт только ok оставил бы индикатор навсегда ниже собственного знаменателя на любой книге с одним плохим чанком. Разделение ok/flagged уже несут done/flagged/glossary_miss_flagged — они не сливаются. ⚠ Следствие, которое надо читать намеренно: пост-чек-ГЕЙТ флагает юнит ПОСЛЕ цикла стадий, его строка edit при этом ok — значит edit.done ≥ done ровно на число гейт-флагнутых. Это честное чтение («редактура этот юнит закончила»), и оно задокументировано у типа.
  3. Волны, которой у пайплайна нет, total: 0, а не 0/N. Draft-only-пайплайн иначе показывал бы edit 0/N вечно — сломанный по построению индикатор.
  4. Процент от progress НЕ считаю. Как две фазы складываются в одно число для человека — продуктовое решение (контракт: «No ready-made percentage is shipped»).

Замер на реальном состоянии (копия проекта coldrun-a в песочнице, чтобы не касаться эталона):

$ tmctl status --config <копия coldrun-a>/book.yaml
Progress: 0/14 units (0.0%) — done=0 in_progress=14 flagged=0 pending=0
Phases (units resolved by wave): draft 14/14 · edit 0/14

Это ровно дефект строки 99, показанный на настоящем прогоне: старый индикатор говорит «0%», новый — «черновая волна закрыта полностью».

Тесты: TestPhaseProgressSplitsTheWaves, TestPhaseProgressCountsAFlaggedUnitAsResolved, TestPhaseProgressHasNoEditWaveOnADraftOnlyPipeline (internal/pipeline/contractblockers_test.go).

Миграции схемы нет — счёт идёт по уже существующим строкам chunk_status (migrate.go v2).


2. Строка 100 — персист манифеста глав/чанков

Код: новый backend/internal/pipeline/manifest.go (весь), новый backend/internal/pipeline/artifact.go (атомарная запись), backend/internal/chunk/chunker.go:120 (SplitChunksWithChapters), bookrun.go:115 (отпечаток до чтения) + bookrun.go:155 (запись на write-пути), status.go:354 + export.go:150 (чтение), rebill.go (ленивый провайдер текста), cmd/tmctl/main.go:243 + render.go:525 (команда tmctl manifest); документация — backend/README.md (строка cmd/tmctl карты пакетов, строка internal/pipeline, блок примеров команд). ⚠ Якоря сверены грепом ПОСЛЕ фикс-раунда приёмки; после любой следующей правки они снова поплывут — первичен код, не эти числа.

2.1. Форма артефакта

<project_db>.manifest.json — рядом с БД, как .mined-signature.yaml / .bank-stop.txt / .auto-bank.yaml (состояние книги, вне git). Пример (реальный, стенд-книга 23 МБ, сокращён):

{
  "manifest_version": "tm-manifest-v2",
  "book_id": "guzhenren-manifest-bench",
  "key": "7932c96e7f2e…",  // сокращён; полный печатает `tmctl manifest`
  "chunker_version": "chunker-v6-generic-heading+srcabbrev+u15.0.0",
  "source_sha256": "758ce8d49660d4c8995d9d8d6e7ff6e3b38226232eb77a17407609c081817f5c",
  "source_bytes": 23120865,
  "source_lang": "zh", "target_lang": "ru", "encoding": "utf8",
  "chapters": [
    { "id": "9c893d348b9480a8", "number": 1, "heading": "", "units_total": 3, "chunks_total": 6,
      "units": [ { "id": "9c893d348b9480a8:cb440937:0", "first_chunk_idx": 0, "chunk_count": 2, "edit_unit_id": 0 },
                 { "id": "9c893d348b9480a8:cb440937:2", "first_chunk_idx": 2, "chunk_count": 3, "edit_unit_id": 1 },
                 { "id": "9c893d348b9480a8:cb440937:5", "first_chunk_idx": 5, "chunk_count": 1, "edit_unit_id": 2 } ] },
    { "id": "20862c52a0d456c3", "number": 2, "heading": "Глава 2", "units_total": 1, "chunks_total": 2,
      "units": [ { "id": "20862c52a0d456c3:cb440937:0", "first_chunk_idx": 0, "chunk_count": 2, "edit_unit_id": 3 } ] }
  ],
  "chapters_total": 2283, "units_total": 4276, "chunks_total": 5071
}

Размер файла на этой книге — 1 077 038 байт (ls -la ~/books/gu-zhenren/manifest-bench/). 2283 главы, все id уникальны (проверено скриптом: len({c['id']}) == 2283), заголовок непустой у 2282 (первая глава — предисловие без структурного маркера).

Текста в манифесте НЕТ намеренно. Он хранит СТРУКТУРУ; текст книги — это сам исходник, и дублировать 23 МБ в сайдкар значило бы разменять сэкономленный CPU на такой же объём диска и записи.

2.2. Стабильный id главы и ЧЕСТНАЯ граница (требование «а»)

id = первые 64 бита sha256(ИНГЕСТИРОВАННОГО текста главы), при дубликате текста — суффикс -2, -3. Берётся текст ДО stripHeading, то есть ровно то, что выдал ingest.

Гарантируется (проверено тестом TestManifestChapterIDSurvivesAReCutAndAnEditElsewhere):

  • пере-чанковка (смена chunker_version, draft_budget_out/edit_ceiling_out, фертильности) — id глав НЕ двигаются: разрез не трогает текст главы;
  • правка эвристики заголовка (heading.txt пары) — не двигает: хешируется пред-strip текст;
  • правка/вставка ДРУГОЙ главы — не двигает id остальных, хотя их НОМЕРА сдвигаются. Это главное, чего номер (плотный, chunker.go) дать не может, и ровно тот ущерб, который назвал §5 спутника контракта («разъезжаются открытые вкладки и закладки»).

ЧЕСТНО ОБЪЯВЛЯЮ границей (что именно инвалидируется):

Что изменилось Что инвалидируется
Текст КОНКРЕТНОЙ главы (правка внутри) id ТОЛЬКО этой главы; её закладки/вкладки — да, остальные нет
Разрез (чанкер/бюджет/фертильность/форма пайплайна) все unit.id без исключения — в id входит тег разреза, поэтому первая единица главы (её leader-индекс всегда 0) тоже получает новый id. ⚠ В первой редакции пака этого тега НЕ было, и граница была ЛОЖНОЙ: «<глава>:0» переживал любую пере-нарезку, указывая уже на другой текст, а потребитель читал «id на месте ⇒ якорь цел». Найдено приёмкой (F1)
Нормализация ingest (text.NormalizeSource, NormVersion) все id глав — «текст главы» становится другим текстом
Кодировка исходника (encoding) все id глав (декодируется другое)
Удаление ПЕРВОЙ из двух глав с байт-равным текстом id второй (она перестаёт быть «-2»)

Плюс общий для движка класс: правки кода ingest версионируются конвенцией через chunkerVersion («This is an ingest rule → covered by chunkerVersion», internal/text/source.go:27) — конвенция унаследована, не изобретена здесь; забытый бамп оставит протухший манифест выглядящим свежим ровно так же, как оставляет протухшие чекпойнты сегодня.

2.3. Пере-строение и защита от протухшего (требование «а», вторая половина)

Протухание решает КЛЮЧ, а не флаг и не mtime. В ключ входят: manifest_version, book_id, sha256 БАЙТ исходника + его размер, encoding, source_lang/target_lang, chunker_version, segmentationSnap, pack.Version() (правило заголовка + данные майнера), lang.EmbeddedVersion() (CJK-числительные/терминаторы → границы, аббревиатуры), text.NormVersion() и shipping_wave — форма пайплайна: units[] документа есть проекция outputUnits, а она группирует чанки в edit-единицы для edit-пайплайна и отдаёт по единице на чанк для draft-only (найдено ревью, §9-Б).

Отпечаток исходника снимается ДО чтения и пере-снимается ПОСЛЕ нарезки (найдено ревью, §9-А): структура выведена из байтов, которые прочитал ingest, и если файл сменился внутри этого окна (~1.4 с на книге 23 МБ), манифест НЕ пишется вовсе. Иначе документ описывал бы СТАРУЮ нарезку под НОВЫМ хешем, проходил бы валидацию вечно и не мог быть опознан как протухший.

  • любой write-путь (TranslateBook, значит и redrive) пере-строит манифест безусловно — то есть translate --resnapshot, будучи именно той громкой командой «чанкер поехал», пере-строит его тоже, но корректность на флаг НЕ завязана;
  • read-путь при несовпадении ключа манифест ИГНОРИРУЕТ и падает на полную нарезку (INFO-строка «the stored manifest is stale»). Состояния «подан устаревший знаменатель» не существует по построению.

Проверено исполнением: TestManifestIsIgnoredWhenTheSourceChanges (правка исходника → loadManifest() возвращает nil, status видит новую главу).

2.4. Read-пути перестали резать книгу (требование «б») — ЗАМЕР

Стенд: ~/books/gu-zhenren/guzhenren-utf8.txt (23 120 865 байт, 2283 раздела, 4276 юнитов, 5071 чанк). Книга-проект в песочнице (scratchpad/stand100/book.yaml), БД ~/books/gu-zhenren/manifest-bench/. Команда замера: /usr/bin/time -f "wall=%e user=%U sys=%S" <бинарь> <cmd> --config <stand100>/book.yaml.

Команда ДО (бинарь HEAD 06f0342) ПОСЛЕ (финальный бинарь пака, манифест на месте)
status wall 1.42 / 1.49 / 1.36 c · user 1.36 / 1.43 / 1.32 wall 0.07 / 0.08 / 0.07 c · user 0.05 / 0.05 / 0.04
export (JSON по умолчанию) wall 1.39 / 1.40 / 1.40 c wall 0.10 / 0.06 / 0.07 c
report wall 1.38 / 1.41 c wall 1.39 / 1.43 c — не изменился, см. ниже
tmctl manifest (производитель) wall 1.79 c — это и есть работа: полный ingest+нарезка

⚠ Числа — ОДИН прогон каждой команды, тройками. Пере-ран на финальном дереве (после фикс-листа и V2) даёт status 0.060.10 c и export 0.060.13 c: тот же порядок, разброс — шум файлового кэша. Приёмке при пере-ране читать порядок величины, а не совпадение цифр.

Первый замер до правок (HEAD-поведение в начале сессии) дал status wall 1.71 / 1.51 / 1.51 c — та же величина, что в теле строки 100 (1.421.51 c CPU).

Разбор стоимости, откуда берётся выигрыш (микро-замер, временный тест, удалён): ingest=831.9 мс · split=528.3 мс на этой книге. Валидация манифеста платит только sha256 исходника — time sha256sum guzhenren-utf8.txt → 0.010.022 c (тёплый кэш).

Байт-равенство проекций доказано исполнением, а не рассуждением. С манифестом и без него (файл временно убран):

$ cmp status-с-манифестом.txt status-без-манифеста.txt  → IDENTICAL
$ cmp export-с-манифестом.json export-без-манифеста.json → IDENTICAL

плюс тест TestManifestServesTheReadModelsIdentically сравнивает reflect.DeepEqual целых структур StatusReport и BookExport на обоих путях.

ЧЕГО НЕ СДЕЛАЛ И ПОЧЕМУ — report остался на пере-нарезке. Половина reportQualityReport — считает residual-скан checks.RepairCandidates(unitSource[k], normText, …) (quality.go:373), а unitSource есть ИСХОДНЫЙ ТЕКСТ каждого обработанного юнита (quality.go:207-209). Это вход сравнительных детекторов по построению, и манифест его не несёт. Три варианта были: (1) класть текст в манифест — это +23 МБ на книгу и дубль исходника; (2) убрать сигнал — это решение о КАЧЕСТВЕ, не гигиена; (3) оставить как есть и объявить. Взял (3). Первая половина report (renderReport) чанков не трогала и до пака. Если владелец захочет report на быстром пути — цена названа: это отказ от repair-residual-замера, и решать её не мне. Пере-нарезку сохранили ещё два места, оба намеренно: export --pairs (эмитит КОЛОНКУ ИСХОДНИКА — пустая колонка хуже медленной, пин TestManifestServesTheReadModelsIdentically) и контент-ветка ре-билл-проекции (rebill.go — ей надо пере-рендерить инъекцию; получает ленивый провайдер и вызывается, только когда банк-only сдвиг снапшота реально случился).

2.5. tmctl manifest — почему появилась команда

Дерево глав нужно ДО первого прогона: библиотека обязана показывать разобранную, но не запущенную книгу (BookStatus: not_started = «parsed, never run»), а единственный другой производитель структуры — платный translate. Команда $0: ingest+чанкер, без ключей провайдера, без записи в БД, ноль вызовов (пин: TestManifestCommandProducesATreeBeforeAnyRun проверяет rec.count() == 0). Печатает сводку; --json печатает сам документ.

$ tmctl manifest --config <stand100>/book.yaml
=== MANIFEST: guzhenren-manifest-bench ===
chapters=2283 units=4276 chunks=5071
source: 23120865 bytes (sha256 758ce8d49660) · encoding utf8 · zh→ru
cut by: chunker-v6-generic-heading+srcabbrev+u15.0.0 · validity key 7932c96e7f2e
written: /home/ubuntu/books/gu-zhenren/manifest-bench/manifest-bench.db.manifest.json
(wall 1.541.79 c — полная нарезка, она тут и есть работа)

2.6. ВОПРОС ОРКЕСТРАТОРУ (не тихая интерпретация): поле heading

Манифест несёт heading = детерминированный рендер движка из правила пары (heading.txt: template Глава {n}) — ровно ту строку, которую движок приклеивает к первой единице главы на экспорте (chunk.ApplyHeading). Другой метки у движка НЕТ: исходный маркер вырезается из текста для модели, а подзаголовок главы остаётся телом прозы и на момент манифеста не переведён.

Контракт при этом требует обратного: Chapter.heading — «label as it comes from the data of the book», и «A client MUST NOT synthesize a label from a template such as "Chapter {n}"». Развилка открыта как К-2 (автор контракта + бэкенд) и К-3 (владелец, продуктовое).

Что сделал: отдал то, что у движка есть, и подписал у поля прямо в коде, что это рендер движка, а не метка книги. Что НЕ делал: не менял чанкер, чтобы сохранять подзаголовок отдельным полем, и не решал за контракт, что показывать в дереве. Строка вопроса продублирована в пинг PROGRESS.


3. Строка 101 — машиночитаемая таблица подписи банка

Код: backend/internal/pipeline/mining.go:280+ (пути, версия, DTO, проекция, запись, clearBankStopTableJSON), mining.go:202 (вызов на стопе) + mining.go:155 (очистка на пустой дельте) + mining.go:53 (очистка осиротевшего стола), waverun.go (поле в сентинеле), cmd/tmctl/render.go:87 (строка «Machine table:»).

Форма: <project_db>.bank-stop.json, рядом с текстовым .bank-stop.txt, пишется на КАЖДОМ прогоне, что мининг сделал непустую дельту (как и текстовый — «signed or not»). ПОЛНАЯ таблица; кап 20 остаётся только у stdout.

{
  "table_version": "tm-bank-stop-v1",
  "book_id": "test-book",
  "terms": 2,
  "rows": [
    { "src": "方源", "dst": "Гуюэ Фан Юань", "origin": "both", "type": "name",
      "freq": 12, "spread": 2, "conventions": 1, "conf": 40, "invented": true,
      "signals": ["freq", "conform"], "contradicts": [],
      "variants": [ { "dst": "Фан Юань", "chunks": 2 } ],
      "evidence": ["surname:方", "formant_prefix:方"],
      "contexts": ["方源来到青茅山。…"] },
    { "src": "花家", "dst": "", "origin": "mined", "type": "name",
      "freq": 12, "spread": 0, "conventions": 0, "conf": null, "invented": false,
      "signals": [], "contradicts": [], "variants": [], "evidence": ["surname:花", ], "contexts": [  ] }
  ]
}

Решения:

  1. Поля — те же, что у текстовой, включая новые из D39.118: signals, invented, conf, conventions, contradicts.
  2. conf — указатель, null для строки, о которой роль ничего не сказала. «Роль сказала 0%» и «роль промолчала» — противоположности (confOrAbsent, D39.102: уверенность = порядок чтения); ноль для обоих спрятал бы самую важную строку листа. Пин: тест требует наличия строки с conf: null.
  3. Списки — списками, не склейкой. Текстовая таблица разделяет ", " / " | " / "; ", а эти последовательности встречаются внутри реальных рендерингов — разобрать назад нельзя. ⚠ Первая редакция нарушала это правило ровно в том поле, где оно важнее всего: variants уезжал готовой строкой «<dst> ×<n> (proposed for <via>)» (найдено ревью, §9-В). Сейчас вариант едет тремя частями {dst, chunks, via}, а метку для человеческих таблиц собирает BankStopVariant.Label() — одно определение, поэтому stdout и текстовый сайдкар не могут описать вариант по-разному. ⚠ Пустая дельта ТЕПЕРЬ переписывает машинную таблицу пустой (§9-Г): «подписывать нечего» — это утверждение, а не отсутствие файла от прошлого прогона.
  4. Пустой список — [], не null (одна форма у потребителя вместо двух).
  5. Порядок строк — тот же, что у текстового сайдкара (порядок ключей источника): два документа читаются друг против друга построчно. Пин в тесте.
  6. Отдельный DTO, а не JSON-теги на BankStopRow: имена в файле — опубликованная поверхность, имена в структуре — код.

⚠ Ловушка «подпись ≠ UPDATE» соблюдена: это ЧТЕНИЕ. Ни одного байта семантики подписи не тронуто — seeding.go/glossary.go/механика promote/decline не изменялись (в диффе их нет).

Тест: TestBankStopMachineTableCarriesTheWholeTable (полнота против stop.Rows, поля арбитража, null vs 0, совпадение порядка).


4. Строка 125 — экспорт-артефакт банка

Код: новый backend/internal/pipeline/bankexport.go; вызовы — bookrun.go:146 (после сида), mining.go:226 (авто-континью), mining.go:242 (стоп на подпись), bookrun.go:193 (завершение прогона), status.go (пере-сид внутри Redrive — добавлено фикс-листом приёмки, F2).

Форма: <project_db>.bank.json — ОДИН файл, весь банк, тремя статусами.

{
  "bank_version": "tm-bank-v1", "book_id": "test-book", "total": 2, "signed": 1,
  "terms": [
    { "id": "e1e2b5625d031d33", "src": "方源", "dst": "Фан Юань", "kind": "name",
      "status": "draft", "origin": "seed", "sense": "", "since_chapter": 0, "until_chapter": 0, "aliases": [] },
    { "id": "fbe2fa3b1a0329e9", "src": "花海", "dst": "Море цветов", "kind": "place",
      "status": "approved", "origin": "seed", "sense": "", "since_chapter": 0, "until_chapter": 0, "aliases": ["花"] }
  ]
}

Решения:

  1. Один файл, а не три по статусу. Канал /bank контракта — ОДНА коллекция со счётчиками total/signed и полем status в строке; три файла заставили бы платформу сшивать их обратно.
  2. id — производный от ключа уникальности (src, sense, since_ch, until_ch), 64 бита sha256, разделитель U+001F. НЕ glossary.id: тот — свежий автоинкремент на каждой пересборке банка (migrate.go v5), а банк пересобирается каждый прогон, то есть экспортированный автоинкремент пере-указывал бы под читателем между двумя чтениями НЕИЗМЕНИВШЕГОСЯ термина. Пин: тест гоняет два прогона и требует байт-равенства файла.
  3. kind = колонка type, пустая строка легитимна (ruby-кандидат не-имя/не-место не имеет типа, memseed.go); проекцию "" → null делает платформа (это её работа по §2.8 спутника).
  4. origin = колонка source (провенанс). Имя source в файле не используется вовсе — иначе между схемами появляется ложный друг (то же решение, что в §2.8 контракта).
  5. Момент записи — минимум промта плюс два дешёвых. Обязательные: банк-стоп (оба исхода — стоп на подпись и авто-континью) и завершение прогона. Добавил: сразу после сида в начале прогона — тогда книга, которая ничего не майнит, или прогон, умерший до первой волны, всё равно оставляют читаемый банк. Цена: одно чтение glossary + marshal (миллисекунды на банке в тысячи строк), платных вызовов ноль.
  6. Порядок детерминирован (по тому же ключу), алиасы сортируются: два прогона над неизменным банком дают побайтно один файл, значит no-op видно диффом.
  7. Пишется через writeFileAtomic (temp+rename): артефакт читают, ПОКА идёт прогон.

Отличие от строки 101 держу явным (и в доккоменте файла): 101 — таблица РЕШЕНИЙ на одном стопе (предложения, эвиденс, «почему такой рендеринг»), существует только у прогона, который что-то намайнил; 125 — СОСТОЯНИЕ банка целиком, включая сид-строки, о которых никто ничего не решал.

Тест: TestBankExportCarriesEveryStatusWithStableIDs (все три статуса, поля, стабильность id через пересборку банка) + проверка обновления на стопе внутри теста строки 101.


5. Строка 145 — потолок на прогон аргументом

Код: cmd/tmctl/invocation.go:114 + :129-145 (флаг и отказ), cmd/tmctl/main.go (проводка в translate и redrive), internal/pipeline/runner.go:78 (поле CeilingUSD), runner.go:356 (bookCeilingUSD — ЕДИНСТВЕННОЕ определение «потолок в силе»), stagerun.go:480 (Reserve — единственная точка допуска), stagerun.go:507 (текст отказа), status.go.

Форма: --ceiling-usd <usd>.

tmctl translate --config book.yaml --ceiling-usd 0.5

Решения:

  1. Флаг, не ENV. Промт разрешал «флаг и/или ENV — реши сам» и назвал входом parseInvocation — это путь флага. Один вход = одна валидация; форма совпадает с остальными операторскими осями прогона (--verify-bank, --accept-rebill). Названный альтернативный довод — PD-99 реестра платформы («argv на INFO — деньги/book-id при воркере»): это свойство ЛОГИРОВАНИЯ платформы, а не форма входа движка (движок и так получает пути книги через argv). Если владелец решит иначе, ENV добавляется поверх одной строкой.
  2. Применяю и к redrive. Redrive переатакует флагнутые чанки настоящими вызовами; оставить его на книжном потолке значило бы оставить дыру ровно в той поверхности, которую строка 145 закрывает.
  3. Семантика ровно как в промте: только этот прогон · book.yaml не пишется · перекрывает book_usd. Дневной потолок НЕ перекрывается — он аккаунт-широкий страж, а не свойство прогона. ⚠ ЭТО КНИЖНЫЙ ПОТОЛОК В СИЛЕ, А НЕ БЮДЖЕТ ПРОГОНА — платформе читать внимательно. Леджер сравнивает его с НАКОПЛЕННЫМ committed+reserved книги (store/ledger.go:67: bookTotal + estimate > BookUSD), то есть аргумент говорит «книга вправе дойти до $X, пока я работаю», а не «этот прогон вправе потратить $X». Кто хочет выдать ПРИРОСТ, передаёт «уже потрачено + прирост»; значение ниже уже потраченного отказывает первой же резервации. Это та же величина, что и ceilings.book_usd, — именно поэтому перекрытие когерентно; вторая, пер-процессная ось бюджета была бы НОВЫМ денежным механизмом, а не этой строкой. Первая редакция доккоммента translate формулировала это неверно («caps what THIS run may spend») — найдено ревью (§9-Д).
  4. Валидатор Р7 (config/book.go:250) не тронут. Ноль/отрицательное/нефинитное аргументом = отказ ЗАПУСКА (fs.Visit отличает «флаг не передан» от «передан 0»). Итог: «выключено» не выразимо ни с одного конца — книга не может обойтись без потолка, аргумент не может обнулить его (D39.110).
  5. Текст отказа называет ТОТ потолок, который в силе: передавшему --ceiling-usd больше не советуют править ceilings.book_usd — файл, который не действует.

Цена нулевая — предъявлено ИСПОЛНЕНИЕМ, не грепом: TestCeilingsAreAbsentFromEveryHashInput рендерит payload обоих волновых снапшотов и требует отсутствия ceilings / book_usd / day_usd / самого значения, плюс двигает Ceilings в памяти и требует неизменности BriefHash. (Греп для протокола: Ceilings в каноне BriefHash нет — config/book.go:280-297, комментарий там же: «Wiring fields (paths, ceilings, db) deliberately excluded»; в buildSnapshotID (snapshot.go:341-450) поля потолка нет.) ⚠ Наивный греп по подстроке ceiling даёт ЛОЖНОЕ срабатывание: edit_ceiling_out — это бюджет сегментации. Первая редакция теста на этом и упала; тест ищет имена, а не подстроку.

Приор промта проверен и подтверждён: resume после ceiling-стопа с БОЛЬШИМ аргументом — штатное поднятие потолка, отдельной правки не потребовалось. TestRunCeilingMovesNoHashAndResumesWhenRaised: тесный потолок останавливает прогон; повторный запуск с --ceiling-usd 10 доводит книгу до конца, и суммарно за оба прогона ровно 6 вызовов провайдера (3 юнита × 2 стадии) — оплаченное первым прогоном переиграно из чекпойнтов, не куплено заново.

Тесты: TestRunCeilingArgumentOverridesTheBookCeiling (перекрытие, книжный потолок цел, book.yaml байт-равен до и после), TestRunCeilingMovesNoHashAndResumesWhenRaised, TestCeilingsAreAbsentFromEveryHashInput, TestParseCeilingUSD (CLI: отсутствие/значение/обе формы записи/отказ на 0, -1, NaN, Inf).


6. Самопроверка

Батарея целиком, исполнением:

$ make battery ; echo EXIT=$?
…
golangci-lint run --timeout=15m ./...
0 issues.
go test ./... -race -count=1   → все пакеты ok
--- did NOT run (no stand data; see battery-stand) ---
--- SKIP: TestHelperKillLoop (0.00s)
EXIT=0

Единственный SKIP — TestHelperKillLoop (вспомогательный процесс kill9-теста, скипается всегда, до пака тоже). Корпус-гейченные тесты (TM_MINER_PARITY, TM_CHECKER_LABELS) в голой батарее не идут — это известное свойство make battery, а не следствие пака.

Дифф ^func Test — ИСПОЛНЕНИЕМ (git grep -h "^func Test" HEAD -- 'backend/**_test.go' против грепа по рабочему дереву): 717 → 749, добавлено 32, удалено 0.

Добавлено
TestPhaseProgressSplitsTheWaves · TestPhaseProgressCountsAFlaggedUnitAsResolved · TestPhaseProgressHasNoEditWaveOnADraftOnlyPipeline
TestManifestServesTheReadModelsIdentically · TestManifestIsIgnoredWhenTheSourceChanges · TestManifestChapterIDSurvivesAReCutAndAnEditElsewhere · TestManifestChunksReproduceTheCut · TestManifestCommandProducesATreeBeforeAnyRun
TestSplitChunksWithChaptersLeavesTheSplitUnchanged · TestSplitChunksWithChaptersReturnsPreStripText
TestBankStopMachineTableCarriesTheWholeTable
TestBankExportCarriesEveryStatusWithStableIDs
TestRunCeilingArgumentOverridesTheBookCeiling · TestRunCeilingMovesNoHashAndResumesWhenRaised · TestCeilingsAreAbsentFromEveryHashInput · TestParseCeilingUSD
TestRunLeavesTheContractArtifacts (страховка от артефакта, который пишется на пути, куда не ходит ни один тест)
По находкам ревью (§9): TestManifestIsNotWrittenWhenTheSourceMovesUnderTheRead · TestManifestKeyFollowsThePipelineShape · TestManifestWithBrokenCountersIsRefused · TestBankStopMachineTableIsClearedWhenNothingAwaitsSignature
По фикс-листу приёмки (§11): TestManifestUnitIDDiesWithTheCut · TestBankExportIsRefreshedWhenARedriveReSeedsAndAborts · TestBankStopMachineTableIsClearedWhenMiningIsUnconfigured · TestBankStopMachineTableIsNotCreatedForABookThatNeverMines
По микро-фикс-листу V2 (§11а): TestBankTermIDIsUnambiguous · TestManifestWithAbsurdButConsistentCountersIsRefused · TestCutTagFollowsTheLangpack · TestManifestOfAnOlderDocumentVersionIsRefused
По слепому аудиту клеймов (§11б): TestManifestFastPathIsActuallyTaken · TestTranslateAndRedriveWireTheRunCeiling · TestWriteFileAtomicLeavesNoLitter

Голден не пере-захватывался (golden_test.go зелёный без правок). Ни снапшот, ни RequestHash пак не двигает: новые поля — read-модель и файлы-проекции, потолок в хеши не входит (§5), SplitChunks доказанно байт-равен (TestSplitChunksWithChaptersLeavesTheSplitUnchanged).

Инварианты соблюдены: сетевых поверхностей не появилось (новых слушателей/HTTP в диффе нет); процесс-на-прогон и EXCLUSIVE flock не тронуты; read-пути по-прежнему открывают store query_only.

Гардрейлы: .env не читал; PUML не рендерил (правил backend/docs/pipeline.puml текстом, проверил две ловушки синтаксиса — внутренних ; в многострочных метках нет, строк, начинающихся на / | < > ] }, внутри меток нет).

Стенд и чужие зоны: эталон ~/books/gu-zhenren/coldrun-a/ только читался, и то один раз — для замеров сделана КОПИЯ в песочнице, чтобы даже -shm эталона не трогать. Замерная БД лежит в ~/books/gu-zhenren/manifest-bench/ (новый каталог, вне git). eval/, platform/, frontend/, docs/ (кроме этого отчёта и своего пинга) не трогал — параллельно живы полигон эксп-22 и сессия раннера платформы.


7. Прочитанные глазами артефакты

Все пять открыты и прочитаны целиком (сгенерированы $0: read-путями на стенде и мок-провайдером через временный дамп-тест, удалённый после чтения):

  1. manifest.json фикстуры (2 главы) и стенд-книги (2283 главы) — §2.1;
  2. bank.json — §4;
  3. bank-stop.json — §3;
  4. bank-stop.txt — сверен со своим JSON построчно (те же строки, тот же порядок);
  5. вывод tmctl manifest и tmctl status/status --json — §1, §2.5.

Что нашёл чтением и починил по ходу: conf рендерился бы нулём для неупомянутой роли строки (стало null); пустые списки рендерились null (стало []); в манифесте юнит-id мог схлопнуться в :0 при теоретическом рассинхроне номера главы и текста (добавлен детерминированный fallback + WARN).

Оговорка, поставленная слепым аудитом (§11б): утверждение «прочитано глазами» было верно для ПЕРВОГО раунда, а после V2-1 образец bank.json в §4 остался с прежними id — то есть файл, документирующий правку, после этой правки перечитан НЕ был. Артефакт пере-сгенерирован и перечитан; id в §4 теперь взяты из реальных байт. Тот же класс — размер манифеста в §2.1 (был снят до тега разреза) и якоря mining.go.


8. Девиации от промта и чего не сделал

# Что Почему
1 report НЕ переведён на манифест (строка 100, требование «б») QualityReport по построению нуждается в исходном тексте юнита для residual-скана; варианты и цена — §2.4. Считаю это границей, а не долгом: закрывать её значит либо дублировать 23 МБ, либо снимать сигнал
2 Добавлена команда tmctl manifest (промт её не называл) Дерево глав нужно ДО прогона, а всё остальное, что строит структуру, — платный путь; §2.5
3 --ceiling-usd проведён и в redrive (промт называл translate) Redrive тратит деньги теми же вызовами; §5 п.2
4 Пофазные счётчики добавлены и НА ГЛАВУ (промт просил в StatusReport) ChapterPassport — часть StatusReport, а К-10 контракта прямо называет это ценой строки 99. Продуктовую половину К-10 не решал
5 В общую тест-фикстуру projectOpts добавлено поле draftOnly Иначе поведение «у пайплайна нет edit-волны» непроверяемо. Дефолт false — все прежние фикстуры байт-равны (батарея зелёная)
6 Сигнатура projectRebill получила ленивый провайдер текста Иначе быстрый путь status пришлось бы кормить бестекстовыми чанками в ветку, которая рендерит контент-хеши, — то есть тихо считать хеши от пустых строк. Провайдер делает это невозможным
7 BankStopRow.Variants сменил тип []string[]BankStopVariant Правка по находке ревью (§9-В). Человеческие таблицы рендерят метку через Label(), поэтому stdout и текстовый сайдкар байт-равны прежним; строковый тип оставался бы единственным полем, которое машинная поверхность не может отдать частями
8 Пустая дельта майнинга переписывает МАШИННУЮ таблицу пустой, текстовую — нет Правка по находке ревью (§9-Г). Асимметрия намеренная и названа: машинную читает экран (ему нужно утверждение «подписывать нечего»), текстовую читает человек, который знает, на какой прогон смотрит. Менять поведение текстового сайдкара — трогать принятое D39.118

Открытый вопрос — один, продублирован в пинг: поле heading манифеста против К-2/К-3 контракта (§2.6). Остальные работы он не блокирует и не блокировал.


9. Адверсариальное ревью: что оно нашло и что с этим сделано

Механика. Два независимых верификатора, оба СЛЕПЫЕ к этому отчёту (им прямо запрещено было его читать — иначе первый же абзац задаёт им карту и они проверяют её, а не работу). Первый выносил вердикт по ПРОМТУ против ДИФФА, построчно с грунтом; второй охотился за дефектами ВНЕ карты отчёта по заданным направлениям (бестекстовый манифест · протухание · конкурентность и крах · потолок · счётчики фаз · экспорт банка · регрессии). Оба писали ноль файлов. Оба независимо вынесли ОДИН И ТОТ ЖЕ несущий дефект (А ниже), которого соло-проход не увидел.

Каждая правка пере-проверена УБИЙСТВОМ МУТАЦИИ — фикс временно ломался, тест обязан был падать:

# Дефект Как поймано Правка Мутация убита
А Ключ годности снимался ВТОРЫМ чтением исходника, уже после ingest+нарезки. Файл, переписанный внутри этого окна (~1.4 c на 23 МБ; переzаливка, правка оператора), давал документ со СТАРОЙ структурой под НОВЫМ хешем — он проходил валидацию вечно и не мог быть опознан как протухший. Это опровергало заявление «состояния «подан устаревший знаменатель» не существует по построению» — моё собственное оба верификатора, независимо, первым пунктом отпечаток снимается ДО чтения и пере-снимается ПОСЛЕ нарезки; расхождение = манифест НЕ пишется TestManifestIsNotWrittenWhenTheSourceMovesUnderTheRead
Б units[] документа — проекция outputUnits, а она ветвится по форме пайплайна; формы в ключе не было. Снятие редакторской стадии не двигало ни одного входа ключа ⇒ сохранённое дерево единиц продолжало валидироваться, описывая группы, которых прогон больше не отгружает оба, вторым пунктом shipping_wave в ключе TestManifestKeyFollowsThePipelineShape
В variants[] машинной таблицы уезжал готовой строкой «<dst> ×<n> (proposed for <via>)» — нарушение правила, которое доккоммент этого же DTO объявляет двумя абзацами выше: «×» и скобка встречаются внутри реальных рендерингов оба вариант едет {dst, chunks, via}; метку собирает BankStopVariant.Label() TestBankStopMachineTableCarriesTheWholeTable (пин на отсутствие «×» в dst). Мутация (вернуть склеенную метку) убита
Г Машинная таблица писалась только при НЕПУСТОЙ дельте: подписав все термы, владелец оставлял на диске байт-равный файл прошлого прогона, и экран подписи пере-предлагал бы уже подписанное слепой верификатор пустая дельта переписывает машинную таблицу ПУСТОЙ TestBankStopMachineTableIsClearedWhenNothingAwaitsSignature
Д Доккоммент translate заявлял «--ceiling-usd caps what THIS run may spend», тогда как леджер сравнивает потолок с НАКОПЛЕННЫМ committed+reserved книги. Для платформы, которая будет ставить потолок на прогон, это разница между «дойти до $X» и «потратить $X» охотник вне карты доккоммент и help-строка переписаны, инвариант вынесен в §5 п.3 документация; кода не касается
Е readModelChunks возвращал bool withText, который ОБА вызывателя выбрасывали — то есть гард, объявленный доккоммантом, в коде не существовал; плюс Status без манифеста резал книгу ДВАЖДЫ (падение на полную нарезку, а затем r.bookChunks провайдером в ре-билл) охотник вне карты вторым результатом стал ПРОВАЙДЕР тех же чанков с текстом: на быстром пути — полная нарезка по требованию, на медленном — уже материализованный срез Обе ветки readModelChunks исполняются TestManifestServesTheReadModelsIdentically, но САМ фикс мутацией НЕ покрывается: реверт дал бы медленнее, а не неверно (лишняя пере-нарезка) плюс вернул бы гард, объявленный доккоммантом и не существующий в коде. За проверенное исполнением не выдаю
Ж loadManifest проверял ПРОВЕНАНС (версия+ключ), но не внутреннюю согласованность, а счётчики документа идут прямо в make(...) и в границу цикла: chunks_total: -1 в отредактированном руками сайдкаре роняет tmctl status паникой охотник вне карты selfConsistent() перед ключом TestManifestWithBrokenCountersIsRefused

Плюс f.Sync() перед rename в artifact.go (краш хоста мог оставить усечённый документ под настоящим именем; манифест это переживает — падает на парсе, — а экспорт банка и стоп-таблица валидации не имеют).

Что верификаторы нашли и что я НЕ менял, с причиной:

  • report остаётся на пере-нарезкеоба назвали это (слепой пометил строку 100 PARTIAL). Согласен с квалификацией; причина — §2.4, и это решение о качестве, а не гигиена.
  • --ceiling-usd парсится всеми подкомандами, действует у двух — свойство общего FlagSet, ровно как --verify-bank на status. Трогать общий разбор ради этого — больше механизма, чем пользы.
  • Переименование стадии даёт трём поверхностям три ответа — класс существовал до пака (export показывает всё pending, resolveChunkState считает по диспозиции); мои счётчики дают третье чтение. Условие ловит ConfigDrift. Дописал прямо в доккоммент PhaseProgress, чтобы читатель не выводил это сам.
  • Новые сайдкары не в .gitignore — верно, и это тот же класс, что у .bank-stop.txt / .mined-signature.yaml / .auto-bank.yaml. .gitignore — КОРНЕВОЙ файл, зона оркестратора; сам не правил. Предлагаю строку *.db.*.json + *.db.*.txt + *.db.*.yaml.
  • kind экспорта банка — свободная строка, а контракт держит enum и null — движок не валидирует type сида ни на одном пути, так что закрытый словарь в экспорте соврал бы о том, что там лежит. Проекция "" → null — работа платформы (§2.8 спутника); словарь открыт — дописал в доккоммент поля.
  • tmctl manifest создаёт БД при первом касании проекта (падение OpenReadOnlyOpen) — верно, доккоммент «no store write» был неточен и исправлен. Это ровно то поведение, которое status имеет всегда; заводить ради команды второй путь открытия store я не стал.
  • sourceSHA256 на каждом чтении (~20 мс на 23 МБ) — цена валидации; названа в §2.4.
  • Контракт держит потолок в ГЛАВАХ (ceiling_chapters), а строка 145 и этот пак — в USD — верно, и это вопрос контрактной стороны (пересчёт «главы → деньги» живёт на платформе, D39.84), не девиация реализации. Промт назвал USD прямо.

К утверждениям самого ревью — та же дисциплина. Два пункта верификаторов я проверил и НЕ принял как дефекты: (1) «run-finished экспорт банка не сработает на exit-2» — TranslateBook возвращает nil при флагах (сентинел CompletedWithFlags минтится в CLI), охотник это сам пере-проверил и снял; (2) «перф-клейм puml не воспроизводится» — верификатору было запрещено писать файлы, а замер требует записать манифест; числа §2.4 сняты мной с командой в шапке таблицы и пере-ранятся приёмкой.

10. Что оркестратору перепроверять в первую очередь

  1. Ключ годности манифеста — полон ли список входов (§2.3) ПОСЛЕ добавления shipping_wave. Ревью нашло там два пропуска подряд; третьего искать стоит именно здесь, потому что забытый вход = тихо устаревшее дерево, а не громкая ошибка.
  2. Семантика --ceiling-usd как КНИЖНОГО потолка, а не бюджета прогона (§5 п.3). Это то, что платформа будет ставить с экрана; если ратифицированная форма D39.110 подразумевает прирост, нужен либо пересчёт на стороне платформы, либо вторая ось — и второе есть новый денежный механизм.
  3. Семантика done пофазных счётчиков (§1 п.2) — решение моё, следствие edit.done ≥ done намеренное; если приёмка считает иначе, это правка одной строки предиката.
  4. Граница report (§2.4) — согласиться, что это граница, а не недоделка.
  5. Форма heading (§2.6) — вопрос владельцу/автору контракта.
  6. Момент записи банк-экспорта (§4 п.5) — достаточно ли трёх точек, или нужен ещё какой-то рубеж.
  7. Числа §2.4 — пере-ранятся командой из шапки таблицы; бинарь HEAD пересобирается из 06f0342.
  8. .gitignore для новых сайдкаров (§9) — корневой файл, зона оркестратора.

11. Фикс-лист приёмки (исполнен)

# Что Правка Проверка
F1 MAJOR Объявленная в §2.2 граница была ЛОЖНОЙ: unit.id = <chapterID>:<FirstChunkIdx>, а leader-индекс первой единицы главы всегда 0 ⇒ «<глава>:0» переживал любую пере-нарезку, указывая на другой текст. Потребитель читает «id на месте ⇒ якорь цел» и ошибается в unit.id вошёл ТЕГ РАЗРЕЗА (cutTag = хеш chunker_version + сегментация + форма пайплайна), manifest.go:129-166; доккоммент ManifestUnit.ID и таблица границ §2.2 переписаны TestManifestUnitIDDiesWithTheCut — два бюджета над одним текстом: id глав равны, НИ ОДИН unit.id не выжил. Мутация (убрать тег) убита
F2 minor Абортированный redrive оставлял .bank.json устаревшим: банк уже пере-сеян, но экспорт вызывался только внутри TranslateBook, до которого команда не доходила r.exportBank(ctx, "redrive/re-seeded") сразу после пере-сида в Redrive (status.go) TestBankExportIsRefreshedWhenARedriveReSeedsAndAborts (редрайв падает на дрифт-гарде, экспорт описывает новый банк). Мутация убита
F3 minor «Майнинг не сконфигурирован» не чистил .bank-stop.json — осиротевший стол подписи жил вечно clearBankStopTableJSON на раннем возврате (mining.go:53), с гардом «только если файл есть» — книга, которая никогда не майнит, сайдкар не заводит ✔ два теста: очистка осиротевшего и НЕ-создание у не-майнящей книги. Обе мутации убиты (прямая и обратная)
F4 ЧЕТЫРЕ ложные формулировки, а не три: (а) «no store write» у tmctl manifest (первое касание проекта создаёт БД) · (б) «Edit.Done ≥ Done ровно на гейт-флагнутых» (причин три, а для draft-only соотношение вообще не в ту сторону) · (в) «книга не может без потолка» (Р7 разрешает day-only) · (г) гарантия «U+001F не встречается» в bankexport.go (а)(б)(в) — доккомменты manifest.go, status.go (PhaseProgress), invocation.go + runner.go. (г) в первом дофикс-раунде НЕ исполнен, и пропуск не был задекларирован — файл остался байт-идентичным, а строка отчёта перечисляла три пункта так, будто список полон. Поймано ре-чеком приёмки, закрыто как V2-1 (§11а) (а)(б)(в) — документация, мутацией не покрывается и за проверенное не выдаю; (г) — см. V2-1
F5 Дрейф file:line ссылок отчёта после фикс-раунда якоря §1/§2/§3/§4/§5 пере-сняты грепом по текущему дереву ✔ греп, вывод в логе сессии
F6 backend/README.md не назван в списке правленых файлов; в selfConsistent не было верхней границы README назван в §2; manifestMaxChunks = 10 000 000 + проверка суммы и per-unit Клейм «покрыто TestManifestWithBrokenCountersIsRefused» был ЛОЖЕН и опровергнут исполнением: все счётчики в том тесте НЕсогласованны, они умирают на равенствах и до границы не доходят — верификатор выключил границу, батарея осталась зелёной. Код был верен, тест отсутствовал. Закрыто как V2-2 (§11а)

В1 (главы) — решение оркестратора принято: поле heading остаётся рендером движка как ВРЕМЕННОЕ, пак принимается, настоящие заголовки уезжают отдельной строкой бэклога (правка чанкера — зона нарезки и снапшотов, хвостом в этот пак не тащится; при её приезде все книги получают заголовки ретроактивно, без пере-перевода). В коде поле уже подписано как «рендер движка, не метка книги» (manifest.go:76-82) — менять там нечего; страж от «временное приживётся» — строка бэклога, её ведёт оркестратор.

11а. Микро-фикс-лист V2 (хвосты ре-чека приёмки, исполнен)

# Что Правка Проверка
V2-1 (из F4г) bankTermID склеивал ключ уникальности через U+001F и утверждал коммантом, что разделитель «не может встретиться» — предположение о данных, которые движок НИГДЕ не валидирует: src/sense копируются из сид-YAML, и контрольный символ там ничем не запрещён части ключа длино-префиксированы, кодировка инъективна ПО ПОСТРОЕНИЮ — утверждению больше не нужно быть истинным (и не нужно пере-проверяться при появлении нового источника сида); коммент переписан TestBankTermIDIsUnambiguous — два РАЗНЫХ ключа, чьи separator-склейки байт-равны, обязаны дать разные id. Мутация (вернуть склейку) убита
V2-2 (из F6) Границу manifestMaxChunks не тестировало ничто тест на СОГЛАСОВАННО-абсурдный сайдкар: все равенства проходят, отказать может только граница; утверждение останавливается на loadManifest, потому что при снятой границе реконструкция аллоцировала бы заявленное число чанков и уронила бы машину, а не тест TestManifestWithAbsurdButConsistentCountersIsRefused. Мутация (выключить границу) убита
V2-3 (зазор спеки приёмки, не брак сессии) cutTag не включал версии langpack/embedded/norm — а они двигают нарезку (правило заголовка решает, съедает ли номер глава-из-одного-хедера; embedded несёт CJK-числительные и терминаторы; нормализация решает, что вообще режется). Пере-строенный манифест отчеканил бы ТЕ ЖЕ unit.id над другим текстом — класс F1, узкий триггер три версии добавлены в cutTag (в ключе они лежали рядом) TestCutTagFollowsTheLangpack (единственная из трёх осей, варьируемая в рантайме; embedded/norm — константы бинаря, как и chunkerVersion, который тест сегментации тоже не варьирует — сказано прямо, за исполненное не выдаю). Мутация убита
V2-4 tm-manifest-v1 не бампнут: сайдкар от до-дофиксного бинаря структурно валиден и валидировался бы вечно, отдавая читателю id в СТАРОЙ форме — ровно той, чей дефект закрывает тег версия документа → tm-manifest-v2 TestManifestOfAnOlderDocumentVersionIsRefused (фикстура — настоящий v1-документ со старой формой id, не пере-клеенный v2). Мутация убита

Про сам мутационный прогон. Первые два прогона V2-1 и V2-3 «прошли» вхолостую: go test -run с шаблоном, которому НИЧЕГО не соответствует, возвращает ok. Для V2-1 теста тогда не существовало вовсе, для V2-3 существующий тест варьирует только сегментацию. Оба написаны, мутации перебиты, а в скрипт добавлен сторож «ни один тест не выбран шаблоном = прогон вхолостую».

11б. Слепой аудит клеймов (исполнен)

После фикс-листа и V2 запущен ещё один независимый проход — СЛЕПОЙ и с одной задачей: искать ЛОЖНЫЕ УТВЕРЖДЕНИЯ, потому что именно на них сессия срывалась дважды. Он пере-убил все 23 заявленные мутации (ни одна не оказалась холостой) и подтвердил числа, формы артефактов и все «не тронут». Нашёл он другое:

# Что Правка
A4 (несущее) ЧЕТЫРЕ изменения кода не держал НИ ОДИН тест, и в списке «мутацией не покрывается» они не значились. Худшие два: сам быстрый путь манифеста (его удаление проходило всю батарею, потому что TestManifestServesTheReadModelsIdentically сравнивает «с манифестом» против «без манифеста» — обе стороны становились медленным путём и тест проходил ВХОЛОСТУЮ; вся строка 100(б) держалась на замере в отчёте) и проводка --ceiling-usd в CLI (TestParseCeilingUSD кончается на разобранном инвокейшне, а пайплайн-тесты ставят поле на Runner напрямую — провод между ними не пинило ничто) добавлены TestManifestFastPathIsActuallyTaken (пин по НАБЛЮДАЕМОМУ признаку: быстрый путь отдаёт чанки БЕЗ текста, медленный — с текстом), TestTranslateAndRedriveWireTheRunCeiling (обе команды доводятся до реального стопа по потолку; для redrive книга сперва доводится до флагнутого чанка, иначе резервации нет и потолок недостижим), TestWriteFileAtomicLeavesNoLitter. Все четыре мутации убиты
A1/A2/A5 Числа и образцы, снятые ДО раунда V2 и не пере-снятые: размер манифеста (1 038 554 → 1 077 038, ровно 4276 юнитов × 9 байт тега), два id в образце bank.json (это единственное поле, которое V2-1 и менял), время tmctl manifest и sha256sum все пере-сняты с реальных байт; §7 получил оговорку
A3 Клейм F5 «якоря пере-сняты» ✔ был верен для §1/§2/§5 и устарел для §3/§4 на 8 строк якоря mining.go исправлены и сверены
A7 Список входов ключа в пинге и в pipeline.puml не называл shipping_wave — тот самый вход, который добавила находка Б дописан в обоих; в puml добавлена строка про смертность unit.id

Не покрыто тестом и объявлено (полный список, а не выборка): доккоменты Д и F4(абв) · оси embedded/norm в cutTag · сам факт, что реверт withText-провайдера даёт медленнее, а не неверно · две из четырёх точек записи банк-экспорта (run-start/seeded и run-finished маскируют друг друга: удаление любой ОДНОЙ оставляет батарею зелёной, потому что артефакт всё равно пишется другой; отдельный пин потребовал бы прогона, где банк меняется МЕЖДУ ними, а это только путь майнинга — и он пинится) · атомарность записи против конкурентного читателя (из одного процесса не наблюдаема; тест пинит то, что наблюдаемо: замену целиком, отсутствие temp-мусора и права 0644).