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

666 lines
79 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Отчёт бэкенд-сессии: движковые блокеры контракта 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|redrive --ceiling-usd <usd>` |
Батарея: `make battery`**EXIT=0**, `golangci-lint`**0 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):**
```json
"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 МБ, сокращён):
```json
{
"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` остался на пере-нарезке.** Половина `report``QualityReport`
считает 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.
```json
{
"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` — ОДИН файл, весь банк, тремя статусами.
```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` создаёт БД при первом касании проекта** (падение `OpenReadOnly``Open`) — верно,
доккоммент «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).