79 KiB
Отчёт бэкенд-сессии: движковые блокеры контракта 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-v2c единым cutTag, cmp-проекции IDENTICAL; батарея-гейт лендинга EXIT=0. Ратификации — D39.122 п.2 (граница report · done=«разрешено волной» ·--ceiling-usd= книжный потолок в силе, пересчёт на платформе · девиации §8 · heading = временный рендер, страж — строка 160). Принятые NOTE без дофикса: тест границыmanifestMaxChunksпинит механизм, но не значение константы (absurd выводится из неё же) — значение сторожится ревью · две из четырёх точек банк-экспорта маскируют друг друга · атомарность против конкурентного читателя не наблюдаема из одного процесса (объявлено в §11б честно). Строки бэклога из приёмки: 163 (x/text|x/net двигают нарезку мимо всех ключей — класс чекпойнт-плоскости) · 164 (rebill-проекция с nil baseMemory завышает RebillUSD). Урок формы: соло-самопроверка сессии не увидела ничего из найденного тремя независимыми проходами; вывод сессии «мои клеймы покрытия проверять исполнением, а не читать» — подтверждён и разделён приёмкой (два клейма покрытия оказались пустыми при зелёной батарее).
Дата: 08.08.2026. Промт: docs/BACKEND_CONTRACT_BLOCKERS_SESSION_PROMPT.md (оркестратор №15, D39.119).
Деньги: $0 — платных вызовов не было ни одного; артефакты получены read-путями на стенд-книге и
мок-провайдером в тестах. Не коммичу — дерево готово к лендингу.
Каждое ЧИСЛО ниже дано с командой, которой получено. Каждый новый файл-артефакт открыт и прочитан глазами (образцы — §7).
0. Сводка
| Строка | Что построено | Форма артефакта |
|---|---|---|
| 99 | Пофазный прогресс draft N/M ∥ edit N/M в юнитах — книга и КАЖДАЯ глава |
поле progress в StatusReport/ChapterPassport (--json) + строка Phases (units resolved by wave) в человеческом выводе |
| 100 | Персист манифеста глав/чанков + стабильный id главы; read-пути перестали резать книгу | сайдкар <project_db>.manifest.json + новая $0-команда tmctl manifest |
| 101 | Машиночитаемая ПОЛНАЯ таблица подписи банка | сайдкар <project_db>.bank-stop.json рядом с текстовым .bank-stop.txt |
| 125 | Экспорт СОСТОЯНИЯ банка тремя статусами | сайдкар <project_db>.bank.json, обновляется на границах стадий |
| 145 | Потолок USD на прогон аргументом | флаг `tmctl translate |
Батарея: make 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):
"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
Решения, которые я принимал сам, и их обоснование:
- Единица обоих счётчиков — ВЫХОДНОЙ ЮНИТ, знаменатель общий (
TotalUnits). Альтернатива — считать черновую волну в ЧАНКАХ (её собственная гранулярность) — даёт два разных масштаба в одном объекте, и платформа, собирая «одну дробь» (контракт: «The phases exist for the DATA, not for the screen»), обязана была бы знать про разницу. Общий знаменатель делает(draft.done+edit.done)/(2·total)честной арифметикой без домыслов. done= РАЗРЕШЕНО волной, а не «ok». Юнит засчитывается, когда волна записала все строкиchunk_status, которые ему должна — ok, flagged, skipped безразлично. Причина: флагнутый юнит ЗАКОНЧЕН (без явногоredriveего никто не переатакует), и счёт только ok оставил бы индикатор навсегда ниже собственного знаменателя на любой книге с одним плохим чанком. Разделение ok/flagged уже несутdone/flagged/glossary_miss_flagged— они не сливаются. ⚠ Следствие, которое надо читать намеренно: пост-чек-ГЕЙТ флагает юнит ПОСЛЕ цикла стадий, его строкаeditпри этомok— значитedit.done ≥ doneровно на число гейт-флагнутых. Это честное чтение («редактура этот юнит закончила»), и оно задокументировано у типа.- Волны, которой у пайплайна нет,
total: 0, а не0/N. Draft-only-пайплайн иначе показывал быedit 0/Nвечно — сломанный по построению индикатор. - Процент от
progressНЕ считаю. Как две фазы складываются в одно число для человека — продуктовое решение (контракт: «No ready-made percentage is shipped»).
Замер на реальном состоянии (копия проекта coldrun-a в песочнице, чтобы не касаться эталона):
$ tmctl status --config <копия coldrun-a>/book.yaml
Progress: 0/14 units (0.0%) — done=0 in_progress=14 flagged=0 pending=0
Phases (units resolved by wave): draft 14/14 · edit 0/14
Это ровно дефект строки 99, показанный на настоящем прогоне: старый индикатор говорит «0%», новый — «черновая волна закрыта полностью».
Тесты: TestPhaseProgressSplitsTheWaves, TestPhaseProgressCountsAFlaggedUnitAsResolved,
TestPhaseProgressHasNoEditWaveOnADraftOnlyPipeline (internal/pipeline/contractblockers_test.go).
Миграции схемы нет — счёт идёт по уже существующим строкам chunk_status (migrate.go v2).
2. Строка 100 — персист манифеста глав/чанков
Код: новый backend/internal/pipeline/manifest.go (весь), новый backend/internal/pipeline/artifact.go
(атомарная запись), backend/internal/chunk/chunker.go:120 (SplitChunksWithChapters),
bookrun.go:115 (отпечаток до чтения) + bookrun.go:155 (запись на write-пути),
status.go:354 + export.go:150 (чтение), rebill.go (ленивый провайдер текста),
cmd/tmctl/main.go:243 + render.go:525 (команда tmctl manifest); документация — backend/README.md
(строка cmd/tmctl карты пакетов, строка internal/pipeline, блок примеров команд).
⚠ Якоря сверены грепом ПОСЛЕ фикс-раунда приёмки; после любой следующей правки они снова поплывут —
первичен код, не эти числа.
2.1. Форма артефакта
<project_db>.manifest.json — рядом с БД, как .mined-signature.yaml / .bank-stop.txt / .auto-bank.yaml
(состояние книги, вне git). Пример (реальный, стенд-книга 23 МБ, сокращён):
{
"manifest_version": "tm-manifest-v2",
"book_id": "guzhenren-manifest-bench",
"key": "7932c96e7f2e…", // сокращён; полный печатает `tmctl manifest`
"chunker_version": "chunker-v6-generic-heading+srcabbrev+u15.0.0",
"source_sha256": "758ce8d49660d4c8995d9d8d6e7ff6e3b38226232eb77a17407609c081817f5c",
"source_bytes": 23120865,
"source_lang": "zh", "target_lang": "ru", "encoding": "utf8",
"chapters": [
{ "id": "9c893d348b9480a8", "number": 1, "heading": "", "units_total": 3, "chunks_total": 6,
"units": [ { "id": "9c893d348b9480a8:cb440937:0", "first_chunk_idx": 0, "chunk_count": 2, "edit_unit_id": 0 },
{ "id": "9c893d348b9480a8:cb440937:2", "first_chunk_idx": 2, "chunk_count": 3, "edit_unit_id": 1 },
{ "id": "9c893d348b9480a8:cb440937:5", "first_chunk_idx": 5, "chunk_count": 1, "edit_unit_id": 2 } ] },
{ "id": "20862c52a0d456c3", "number": 2, "heading": "Глава 2", "units_total": 1, "chunks_total": 2,
"units": [ { "id": "20862c52a0d456c3:cb440937:0", "first_chunk_idx": 0, "chunk_count": 2, "edit_unit_id": 3 } ] }
],
"chapters_total": 2283, "units_total": 4276, "chunks_total": 5071
}
Размер файла на этой книге — 1 077 038 байт (ls -la ~/books/gu-zhenren/manifest-bench/).
2283 главы, все id уникальны (проверено скриптом: len({c['id']}) == 2283), заголовок непустой у 2282
(первая глава — предисловие без структурного маркера).
Текста в манифесте НЕТ намеренно. Он хранит СТРУКТУРУ; текст книги — это сам исходник, и дублировать 23 МБ в сайдкар значило бы разменять сэкономленный CPU на такой же объём диска и записи.
2.2. Стабильный id главы и ЧЕСТНАЯ граница (требование «а»)
id = первые 64 бита sha256(ИНГЕСТИРОВАННОГО текста главы), при дубликате текста — суффикс -2, -3.
Берётся текст ДО stripHeading, то есть ровно то, что выдал ingest.
Гарантируется (проверено тестом TestManifestChapterIDSurvivesAReCutAndAnEditElsewhere):
- пере-чанковка (смена
chunker_version,draft_budget_out/edit_ceiling_out, фертильности) — id глав НЕ двигаются: разрез не трогает текст главы; - правка эвристики заголовка (heading.txt пары) — не двигает: хешируется пред-strip текст;
- правка/вставка ДРУГОЙ главы — не двигает id остальных, хотя их НОМЕРА сдвигаются. Это главное, чего
номер (плотный,
chunker.go) дать не может, и ровно тот ущерб, который назвал §5 спутника контракта («разъезжаются открытые вкладки и закладки»).
ЧЕСТНО ОБЪЯВЛЯЮ границей (что именно инвалидируется):
| Что изменилось | Что инвалидируется |
|---|---|
| Текст КОНКРЕТНОЙ главы (правка внутри) | id ТОЛЬКО этой главы; её закладки/вкладки — да, остальные нет |
| Разрез (чанкер/бюджет/фертильность/форма пайплайна) | все unit.id без исключения — в id входит тег разреза, поэтому первая единица главы (её leader-индекс всегда 0) тоже получает новый id. ⚠ В первой редакции пака этого тега НЕ было, и граница была ЛОЖНОЙ: «<глава>:0» переживал любую пере-нарезку, указывая уже на другой текст, а потребитель читал «id на месте ⇒ якорь цел». Найдено приёмкой (F1) |
Нормализация ingest (text.NormalizeSource, NormVersion) |
все id глав — «текст главы» становится другим текстом |
Кодировка исходника (encoding) |
все id глав (декодируется другое) |
| Удаление ПЕРВОЙ из двух глав с байт-равным текстом | id второй (она перестаёт быть «-2») |
Плюс общий для движка класс: правки кода ingest версионируются конвенцией через chunkerVersion
(«This is an ingest rule → covered by chunkerVersion», internal/text/source.go:27) — конвенция унаследована,
не изобретена здесь; забытый бамп оставит протухший манифест выглядящим свежим ровно так же, как
оставляет протухшие чекпойнты сегодня.
2.3. Пере-строение и защита от протухшего (требование «а», вторая половина)
Протухание решает КЛЮЧ, а не флаг и не mtime. В ключ входят: manifest_version, book_id,
sha256 БАЙТ исходника + его размер, encoding, source_lang/target_lang, chunker_version,
segmentationSnap, pack.Version() (правило заголовка + данные майнера), lang.EmbeddedVersion()
(CJK-числительные/терминаторы → границы, аббревиатуры), text.NormVersion() и shipping_wave —
форма пайплайна: units[] документа есть проекция outputUnits, а она группирует чанки в edit-единицы
для edit-пайплайна и отдаёт по единице на чанк для draft-only (найдено ревью, §9-Б).
Отпечаток исходника снимается ДО чтения и пере-снимается ПОСЛЕ нарезки (найдено ревью, §9-А): структура выведена из байтов, которые прочитал ingest, и если файл сменился внутри этого окна (~1.4 с на книге 23 МБ), манифест НЕ пишется вовсе. Иначе документ описывал бы СТАРУЮ нарезку под НОВЫМ хешем, проходил бы валидацию вечно и не мог быть опознан как протухший.
- любой write-путь (
TranslateBook, значит иredrive) пере-строит манифест безусловно — то естьtranslate --resnapshot, будучи именно той громкой командой «чанкер поехал», пере-строит его тоже, но корректность на флаг НЕ завязана; - read-путь при несовпадении ключа манифест ИГНОРИРУЕТ и падает на полную нарезку (INFO-строка «the stored manifest is stale»). Состояния «подан устаревший знаменатель» не существует по построению.
Проверено исполнением: TestManifestIsIgnoredWhenTheSourceChanges (правка исходника → loadManifest()
возвращает nil, status видит новую главу).
2.4. Read-пути перестали резать книгу (требование «б») — ЗАМЕР
Стенд: ~/books/gu-zhenren/guzhenren-utf8.txt (23 120 865 байт, 2283 раздела, 4276 юнитов, 5071 чанк).
Книга-проект в песочнице (scratchpad/stand100/book.yaml), БД ~/books/gu-zhenren/manifest-bench/.
Команда замера: /usr/bin/time -f "wall=%e user=%U sys=%S" <бинарь> <cmd> --config <stand100>/book.yaml.
| Команда | ДО (бинарь HEAD 06f0342) |
ПОСЛЕ (финальный бинарь пака, манифест на месте) |
|---|---|---|
status |
wall 1.42 / 1.49 / 1.36 c · user 1.36 / 1.43 / 1.32 | wall 0.07 / 0.08 / 0.07 c · user 0.05 / 0.05 / 0.04 |
export (JSON по умолчанию) |
wall 1.39 / 1.40 / 1.40 c | wall 0.10 / 0.06 / 0.07 c |
report |
wall 1.38 / 1.41 c | wall 1.39 / 1.43 c — не изменился, см. ниже |
tmctl manifest (производитель) |
— | wall 1.79 c — это и есть работа: полный ingest+нарезка |
⚠ Числа — ОДИН прогон каждой команды, тройками. Пере-ран на финальном дереве (после фикс-листа и V2)
даёт status 0.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.
{
"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": [ … ] }
]
}
Решения:
- Поля — те же, что у текстовой, включая новые из D39.118:
signals,invented,conf,conventions,contradicts. conf— указатель,nullдля строки, о которой роль ничего не сказала. «Роль сказала 0%» и «роль промолчала» — противоположности (confOrAbsent, D39.102: уверенность = порядок чтения); ноль для обоих спрятал бы самую важную строку листа. Пин: тест требует наличия строки сconf: null.- Списки — списками, не склейкой. Текстовая таблица разделяет
", "/" | "/"; ", а эти последовательности встречаются внутри реальных рендерингов — разобрать назад нельзя. ⚠ Первая редакция нарушала это правило ровно в том поле, где оно важнее всего:variantsуезжал готовой строкой «<dst> ×<n> (proposed for <via>)» (найдено ревью, §9-В). Сейчас вариант едет тремя частями{dst, chunks, via}, а метку для человеческих таблиц собираетBankStopVariant.Label()— одно определение, поэтому stdout и текстовый сайдкар не могут описать вариант по-разному. ⚠ Пустая дельта ТЕПЕРЬ переписывает машинную таблицу пустой (§9-Г): «подписывать нечего» — это утверждение, а не отсутствие файла от прошлого прогона. - Пустой список —
[], неnull(одна форма у потребителя вместо двух). - Порядок строк — тот же, что у текстового сайдкара (порядок ключей источника): два документа читаются друг против друга построчно. Пин в тесте.
- Отдельный DTO, а не JSON-теги на
BankStopRow: имена в файле — опубликованная поверхность, имена в структуре — код.
⚠ Ловушка «подпись ≠ UPDATE» соблюдена: это ЧТЕНИЕ. Ни одного байта семантики подписи не тронуто —
seeding.go/glossary.go/механика promote/decline не изменялись (в диффе их нет).
Тест: TestBankStopMachineTableCarriesTheWholeTable (полнота против stop.Rows, поля арбитража,
null vs 0, совпадение порядка).
4. Строка 125 — экспорт-артефакт банка
Код: новый backend/internal/pipeline/bankexport.go; вызовы — bookrun.go:146 (после сида),
mining.go:226 (авто-континью), mining.go:242 (стоп на подпись), bookrun.go:193 (завершение
прогона), status.go (пере-сид внутри Redrive — добавлено фикс-листом приёмки, F2).
Форма: <project_db>.bank.json — ОДИН файл, весь банк, тремя статусами.
{
"bank_version": "tm-bank-v1", "book_id": "test-book", "total": 2, "signed": 1,
"terms": [
{ "id": "e1e2b5625d031d33", "src": "方源", "dst": "Фан Юань", "kind": "name",
"status": "draft", "origin": "seed", "sense": "", "since_chapter": 0, "until_chapter": 0, "aliases": [] },
{ "id": "fbe2fa3b1a0329e9", "src": "花海", "dst": "Море цветов", "kind": "place",
"status": "approved", "origin": "seed", "sense": "", "since_chapter": 0, "until_chapter": 0, "aliases": ["花"] }
]
}
Решения:
- Один файл, а не три по статусу. Канал
/bankконтракта — ОДНА коллекция со счётчикамиtotal/signedи полемstatusв строке; три файла заставили бы платформу сшивать их обратно. id— производный от ключа уникальности(src, sense, since_ch, until_ch), 64 бита sha256, разделитель U+001F. НЕglossary.id: тот — свежий автоинкремент на каждой пересборке банка (migrate.gov5), а банк пересобирается каждый прогон, то есть экспортированный автоинкремент пере-указывал бы под читателем между двумя чтениями НЕИЗМЕНИВШЕГОСЯ термина. Пин: тест гоняет два прогона и требует байт-равенства файла.kind= колонкаtype, пустая строка легитимна (ruby-кандидат не-имя/не-место не имеет типа,memseed.go); проекцию"" → nullделает платформа (это её работа по §2.8 спутника).origin= колонкаsource(провенанс). Имяsourceв файле не используется вовсе — иначе между схемами появляется ложный друг (то же решение, что в §2.8 контракта).- Момент записи — минимум промта плюс два дешёвых. Обязательные: банк-стоп (оба исхода — стоп на подпись и авто-континью) и завершение прогона. Добавил: сразу после сида в начале прогона — тогда книга, которая ничего не майнит, или прогон, умерший до первой волны, всё равно оставляют читаемый банк. Цена: одно чтение glossary + marshal (миллисекунды на банке в тысячи строк), платных вызовов ноль.
- Порядок детерминирован (по тому же ключу), алиасы сортируются: два прогона над неизменным банком дают побайтно один файл, значит no-op видно диффом.
- Пишется через
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
Решения:
- Флаг, не ENV. Промт разрешал «флаг и/или ENV — реши сам» и назвал входом
parseInvocation— это путь флага. Один вход = одна валидация; форма совпадает с остальными операторскими осями прогона (--verify-bank,--accept-rebill). Названный альтернативный довод — PD-99 реестра платформы («argv на INFO — деньги/book-id при воркере»): это свойство ЛОГИРОВАНИЯ платформы, а не форма входа движка (движок и так получает пути книги через argv). Если владелец решит иначе, ENV добавляется поверх одной строкой. - Применяю и к
redrive. Redrive переатакует флагнутые чанки настоящими вызовами; оставить его на книжном потолке значило бы оставить дыру ровно в той поверхности, которую строка 145 закрывает. - Семантика ровно как в промте: только этот прогон ·
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-Д). - Валидатор Р7 (
config/book.go:250) не тронут. Ноль/отрицательное/нефинитное аргументом = отказ ЗАПУСКА (fs.Visitотличает «флаг не передан» от «передан 0»). Итог: «выключено» не выразимо ни с одного конца — книга не может обойтись без потолка, аргумент не может обнулить его (D39.110). - Текст отказа называет ТОТ потолок, который в силе: передавшему
--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-путями на стенде и мок-провайдером через временный дамп-тест, удалённый после чтения):
manifest.jsonфикстуры (2 главы) и стенд-книги (2283 главы) — §2.1;bank.json— §4;bank-stop.json— §3;bank-stop.txt— сверен со своим JSON построчно (те же строки, тот же порядок);- вывод
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. Что оркестратору перепроверять в первую очередь
- Ключ годности манифеста — полон ли список входов (§2.3) ПОСЛЕ добавления
shipping_wave. Ревью нашло там два пропуска подряд; третьего искать стоит именно здесь, потому что забытый вход = тихо устаревшее дерево, а не громкая ошибка. - Семантика
--ceiling-usdкак КНИЖНОГО потолка, а не бюджета прогона (§5 п.3). Это то, что платформа будет ставить с экрана; если ратифицированная форма D39.110 подразумевает прирост, нужен либо пересчёт на стороне платформы, либо вторая ось — и второе есть новый денежный механизм. - Семантика
doneпофазных счётчиков (§1 п.2) — решение моё, следствиеedit.done ≥ doneнамеренное; если приёмка считает иначе, это правка одной строки предиката. - Граница
report(§2.4) — согласиться, что это граница, а не недоделка. - Форма
heading(§2.6) — вопрос владельцу/автору контракта. - Момент записи банк-экспорта (§4 п.5) — достаточно ли трёх точек, или нужен ещё какой-то рубеж.
- Числа §2.4 — пере-ранятся командой из шапки таблицы; бинарь HEAD пересобирается из
06f0342. .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).