666 lines
79 KiB
Markdown
666 lines
79 KiB
Markdown
# Отчёт бэкенд-сессии: движковые блокеры контракта 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.40–1.46→0.05–0.07 c), формы подтверждены; фикс-лист **F1–F6**, несущее — **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.06–0.10 c и `export` 0.06–0.13 c: тот же порядок, разброс — шум файлового кэша. Приёмке
|
||
при пере-ране читать порядок величины, а не совпадение цифр.
|
||
|
||
Первый замер до правок (HEAD-поведение в начале сессии) дал `status` wall 1.71 / 1.51 / 1.51 c — та же
|
||
величина, что в теле строки 100 (1.42–1.51 c CPU).
|
||
|
||
Разбор стоимости, откуда берётся выигрыш (микро-замер, временный тест, удалён):
|
||
`ingest=831.9 мс · split=528.3 мс` на этой книге. Валидация манифеста платит только sha256 исходника —
|
||
`time sha256sum guzhenren-utf8.txt` → 0.01–0.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.54–1.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).
|