Accept the contract-blockers pack as D39.122 with review header and ratifications, close rows 99-145, add rows 163-164, archive the prompt, ping the platform with final artifact forms
This commit is contained in:
parent
0e69bc16c8
commit
1073184088
7 changed files with 990 additions and 11 deletions
4
.gitignore
vendored
4
.gitignore
vendored
|
|
@ -20,6 +20,10 @@ backend/dist/
|
|||
*.db-wal
|
||||
*.db-shm
|
||||
*.db.lock
|
||||
# Сайдкары движка рядом с БД (манифест, банк, стоп-таблицы — состояние книги, вне git; D39.122)
|
||||
*.db.*.json
|
||||
*.db.*.txt
|
||||
*.db.*.yaml
|
||||
|
||||
# Редакторы/ОС
|
||||
.vscode/
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -16,7 +16,7 @@
|
|||
- `research/` — фактура ресёрчей 01–26; у принятых — ревью-шапки, часть тел под ⚠ superseded: **читай баннер прежде содержимого**. Ключевые для навигации: 15 голос · 16 ридер-IDE · 17 внешняя критика · 18 рычаги качества · 19 нарезка · 20 банк-майнинг · 21 обзор транспорта · 22 доменные харнессы · 23 шов движок↔платформа (транспорт superseded D39.106) · 25 холодное ревью шва — форма D39.106, отвергнутые альтернативы, требования к эмиттеру (читать перед любым кодом стыка) · 24 арбитраж банка (ПРИНЯТ D39.102: консилиум закрыт классом, вход фикс-пака банка — §G) · 26 официальные практики Anthropic (D39.121: записка-план, сниппеты хендоффов, карта «уже делаем/перенять»).
|
||||
- [PROGRESS.md](PROGRESS.md) — журнал: CURRENT-STATE + **ЕДИНЫЙ БЭКЛОГ** (единственный трекер) + живой хвост хроники. НЕ источник решений.
|
||||
- `scripts/counts.py` — **производные числа доков считаются им, а не руками** (голова по трём носителям · счёт очереди и зон · вес открытых строк регистра платформы); `--check` даёт ненулевой код на расхождении. Его же зовёт зонный хук `scripts/githooks/pre-commit` при коммите, задевающем D-лог или PROGRESS — предупреждает, не блокирует. Заведено по D39.112 п.5б: голова отставала трижды у трёх разных оркестраторов.
|
||||
- Активные хендофф-промты сессий (состав обновляется при каждом лендинге — норма D39.80): [ORCHESTRATOR_SESSION_PROMPT.md](ORCHESTRATOR_SESSION_PROMPT.md) (роль/нормы; состояния не дублирует) · [POLYGON_PACKAGE4_SESSION_PROMPT.md](POLYGON_PACKAGE4_SESSION_PROMPT.md) (полигон, отложен) · **платформа: ОТРАБОТАН ЦЕЛИКОМ** — четыре сессии (P0 D39.107 · P1+P2 D39.109 · фикс-пак P3 D39.114); текст в архиве, [`archive/prompts/PLATFORM_P0_SESSION_PROMPT_2026-08-04.md`](archive/prompts/PLATFORM_P0_SESSION_PROMPT_2026-08-04.md), копия с баннером ещё лежит в зоне. **Платформа: [`../platform/docs/PLATFORM_RUNNER_SESSION_PROMPT.md`](../platform/docs/PLATFORM_RUNNER_SESSION_PROMPT.md)** (раннер: юниты/River/реконсилятор/тейлер + первые ручки /v0; D39.119, запуск = владелец) · **фронт: S3 ОТРАБОТАН и ПРИНЯТ D39.115** (контракт 0.2.0 в каноне, копии байт-равны, слой данных построен); текст в архиве, [`archive/prompts/FRONTEND_S3_SESSION_PROMPT_2026-08-04.md`](archive/prompts/FRONTEND_S3_SESSION_PROMPT_2026-08-04.md). **S4 ГЕЙЧЕН словом владельца о качестве оболочки (D39.119 п.4):** сначала список претензий → «S3.5 фикс-пак оболочки» (VS Code-вкладки + Ф-19) → S4. Зонные журналы фронта/платформы — `frontend-PROGRESS.md` / `platform-PROGRESS.md` в их зонах (решение владельца 04.08: прогресс зон только там). **бэкенд: фикс-пак банка ОТРАБОТАН и ПРИНЯТ D39.118** (код `bccf2d8`; отчёт с ревью-шапкой — [`archive/reports/BANK_CLUSTER_FIXPACK_2026-08-08.md`](archive/reports/BANK_CLUSTER_FIXPACK_2026-08-08.md); текст промта в архиве — [`archive/prompts/BACKEND_BANK_CLUSTER_FIXPACK_SESSION_PROMPT_2026-08-04.md`](archive/prompts/BACKEND_BANK_CLUSTER_FIXPACK_SESSION_PROMPT_2026-08-04.md)); **Бэкенд: [BACKEND_CONTRACT_BLOCKERS_SESSION_PROMPT.md](BACKEND_CONTRACT_BLOCKERS_SESSION_PROMPT.md)** (движковые блокеры контракта 99/100/101/125/145; D39.119, запуск = владелец; эмиттер 103 — следующим промтом) · **Полигон: [POLYGON_TENANT_PANEL_SESSION_PROMPT.md](POLYGON_TENANT_PANEL_SESSION_PROMPT.md)** (эксп-22 «панель жильцов»; санкция D39.117, ЗАПУЩЕН 08.08 — сессия живая) · **эксп-21 ОТРАБОТАН и ПРИНЯТ** (D39.117; отчёт `experiments/21-role-topology.md`), текст промта в архиве — [`archive/prompts/POLYGON_ROLE_TOPOLOGY_SESSION_PROMPT_2026-08-06.md`](archive/prompts/POLYGON_ROLE_TOPOLOGY_SESSION_PROMPT_2026-08-06.md). Очередь и состояние — только CURRENT-STATE.
|
||||
- Активные хендофф-промты сессий (состав обновляется при каждом лендинге — норма D39.80): [ORCHESTRATOR_SESSION_PROMPT.md](ORCHESTRATOR_SESSION_PROMPT.md) (роль/нормы; состояния не дублирует) · [POLYGON_PACKAGE4_SESSION_PROMPT.md](POLYGON_PACKAGE4_SESSION_PROMPT.md) (полигон, отложен) · **платформа: ОТРАБОТАН ЦЕЛИКОМ** — четыре сессии (P0 D39.107 · P1+P2 D39.109 · фикс-пак P3 D39.114); текст в архиве, [`archive/prompts/PLATFORM_P0_SESSION_PROMPT_2026-08-04.md`](archive/prompts/PLATFORM_P0_SESSION_PROMPT_2026-08-04.md), копия с баннером ещё лежит в зоне. **Платформа: [`../platform/docs/PLATFORM_RUNNER_SESSION_PROMPT.md`](../platform/docs/PLATFORM_RUNNER_SESSION_PROMPT.md)** (раннер: юниты/River/реконсилятор/тейлер + первые ручки /v0; D39.119, запуск = владелец) · **фронт: S3 ОТРАБОТАН и ПРИНЯТ D39.115** (контракт 0.2.0 в каноне, копии байт-равны, слой данных построен); текст в архиве, [`archive/prompts/FRONTEND_S3_SESSION_PROMPT_2026-08-04.md`](archive/prompts/FRONTEND_S3_SESSION_PROMPT_2026-08-04.md). **S4 ГЕЙЧЕН словом владельца о качестве оболочки (D39.119 п.4):** сначала список претензий → «S3.5 фикс-пак оболочки» (VS Code-вкладки + Ф-19) → S4. Зонные журналы фронта/платформы — `frontend-PROGRESS.md` / `platform-PROGRESS.md` в их зонах (решение владельца 04.08: прогресс зон только там). **бэкенд: фикс-пак банка ОТРАБОТАН и ПРИНЯТ D39.118** (код `bccf2d8`; отчёт с ревью-шапкой — [`archive/reports/BANK_CLUSTER_FIXPACK_2026-08-08.md`](archive/reports/BANK_CLUSTER_FIXPACK_2026-08-08.md); текст промта в архиве — [`archive/prompts/BACKEND_BANK_CLUSTER_FIXPACK_SESSION_PROMPT_2026-08-04.md`](archive/prompts/BACKEND_BANK_CLUSTER_FIXPACK_SESSION_PROMPT_2026-08-04.md)); **бэкенд: пак «блокеры контракта» ОТРАБОТАН и ПРИНЯТ D39.122** (код `0e69bc1`; отчёт с ревью-шапкой — [`archive/reports/CONTRACT_BLOCKERS_2026-08-08.md`](archive/reports/CONTRACT_BLOCKERS_2026-08-08.md); промт в архиве — [`archive/prompts/BACKEND_CONTRACT_BLOCKERS_SESSION_PROMPT_2026-08-08.md`](archive/prompts/BACKEND_CONTRACT_BLOCKERS_SESSION_PROMPT_2026-08-08.md); следующий бэкенд-промт — эмиттер 103, не выдан) · **Полигон: [POLYGON_TENANT_PANEL_SESSION_PROMPT.md](POLYGON_TENANT_PANEL_SESSION_PROMPT.md)** (эксп-22 «панель жильцов»; санкция D39.117, ЗАПУЩЕН 08.08 — сессия живая) · **эксп-21 ОТРАБОТАН и ПРИНЯТ** (D39.117; отчёт `experiments/21-role-topology.md`), текст промта в архиве — [`archive/prompts/POLYGON_ROLE_TOPOLOGY_SESSION_PROMPT_2026-08-06.md`](archive/prompts/POLYGON_ROLE_TOPOLOGY_SESSION_PROMPT_2026-08-06.md). Очередь и состояние — только CURRENT-STATE.
|
||||
- Зоны фронта (чужие, читать при касании стыка; каждая ведёт СВОЙ зонный бэклог — единый бэклог их строк не принимает, D39.84): [../frontend/](../frontend/) — веб-интерфейс: промт фронт-сессий S0–S7 + [STACK_DECISIONS.md](../frontend/docs/STACK_DECISIONS.md) (пины версий точными числами и ловушки, сверены с вебом 02.08) + [BACKLOG.md](../frontend/docs/BACKLOG.md) · [../platform/](../platform/) — SaaS control plane: README + [BACKLOG.md](../platform/BACKLOG.md) (П-1..П-5) + `docs/` (промт P0 · зонный журнал `platform-PROGRESS.md`).
|
||||
- `archive/` — история ([правила архива](archive/README.md)): закрытые промты (`prompts/`) · отчёты с ревью-шапками (`reports/` — на них ссылаются приёмки) · исполненные арх-доки (`architecture/`) · слайсы хроники `PROGRESS-*.md`. Инструкции оттуда не исполнять.
|
||||
- Диаграммы: [../backend/docs/components.puml](../backend/docs/components.puml) · [../backend/docs/pipeline.puml](../backend/docs/pipeline.puml) — дом рядом с кодом (D39.80), правятся бэкендом одним коммитом с кодом; вручную НЕ рендерить (владелец смотрит PlantUML-расширением VS Code).
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
# Журнал решений оркестратора — контракт D1–D39.121 (развязки 04.07 · пакеты 09–10.07 · приёмка/качество-первым/пивот/эмпирика 11–12.07 · арх-ресет+стройка пере-прогонного стека 13–19.07)
|
||||
# Журнал решений оркестратора — контракт D1–D39.122 (развязки 04.07 · пакеты 09–10.07 · приёмка/качество-первым/пивот/эмпирика 11–12.07 · арх-ресет+стройка пере-прогонного стека 13–19.07)
|
||||
|
||||
> **⟶ КАРТА АКТУАЛЬНОСТИ (ревизия D31, продлена до D38.2 [12.07]; исторические записи ниже НЕ переписываются — дисциплина D23.3).** Читая контракт целиком, держи под рукой, что чем перекрыто:
|
||||
> ⚠ **Навигация (актуализация 07.08, эра D39.1xx):** append-only-дисциплина (D23.3) означает, что
|
||||
|
|
@ -1710,3 +1710,15 @@ API-529-долг закрыт: 8-осевой refute-by-default воркфлоу
|
|||
**2. Ратифицировано (направление, не буква — оговорка D39.120 п.2 действует):** (а) **записка-план**: после тяжёлого ревью — структурированный файл «ID находки → статус → улика» (не проза: инцидент диспозиций 08.08 случился в прозе, и Anthropic это же говорит о Markdown против JSON), движение по записке, сдача гейтится сверкой «все ID имеют диспозицию»; (б) «заявление=команда» распространяется на клеймы ревью о себе; (в) комплектность против заказа — механически (пункт промта → исполнено/отказ), для паков среднего+ веса; (г) числовой гейт сторожит присутствие, не истинность — сравнительные/пороговые утверждения приёмка читает списком; (д) стандартный блок будущих кодовых хендоффов из официальных сниппетов (ground-progress-claims · «проверь последний абзац» · запрет подгонки тестов · интервальная самоверификация против явных критериев); (е) панелям — obstacle reporting обязательным полем схемы и анти-оверфлаг-рамки ревьюеру. Носители: промт оркестратора (два буллета) + CLAUDE.md (компакция-инструкция и запрет подгонки тестов — механизм-уровень, читается каждой сессией).
|
||||
|
||||
**3. Конвергенция зафиксирована** (research/26 §2): половина канона проекта совпала с официальными рекомендациями независимо («заявление=команда» = show-evidence · author≠reviewer = fresh-context verifiers · execute-first приёмка = grade-final-state · онбординг-диета = lean CLAUDE.md · «один носитель на факт» = JIT-указатели). Не перенимается: promptfoo · lock-файлы (нужны только при двух сессиях в одной зоне) · метапромпт как прод-механизм (§5 research/26). Вопрос владельца о «сайте prompts md» закрыт указателем — research/26 §4.
|
||||
|
||||
## D39.122 — ПАК «ДВИЖКОВЫЕ БЛОКЕРЫ КОНТРАКТА» ПРИНЯТ И ЗАЛЕНДЕН; ратификации приёмки; строки 99·100·101·125·145 закрыты; research/27 (главы) принят фактурой (09.08). ✅
|
||||
|
||||
**1. Приёмка (оркестратор №15, полный аппарат + новые нормы).** Три раунда: (а) 7-агентный воркфлоу — слепой дифф-ревьюер · **кросс-модельный опровергатель отчёта на Opus (первое применение D39.120)** · пере-ран заявлений (battery целиком, 717→738, формы артефактов на стенде, cmp проекций) · 7/7 посадок CAUGHT в копии зоны · два охотника вне карты · перф-замер на независимом стенде с бинарём «до» из `06f0342` (status 1.40–1.46→0.05–0.07 c — таблица §2.4 воспроизведена); (б) фикс-лист F1–F6 (несущее: **F1 MAJOR от опровергателя — `unit.id` первого юнита главы переживал любой разрез**, «граница инвалидации» отчёта была ложной; закрыт ТЕГОМ РАЗРЕЗА в id) + ре-чек с пере-мутациями (поймано: V2-1 не исполнен и не задекларирован · клейм покрытия F6 ложен · зазор МОЕЙ спеки cutTag без langpack/embedded/norm · manifestVersion не бампнут); (в) микро-раунд V2-1..4 исполнен, и сессия СВЕРХ заказа провела третий слепой аудит своих клеймов — нашла 4 непокрытых тестами оси (худшая: быстрый путь манифеста удалялся при зелёной батарее — тест сравнения проекций вырождался в «медленный против медленного»; проводка `--ceiling-usd` в CLI не пинилась) и запинила; призналась в холостых прогонах двух мутаций (`go test -run` без совпадений = ok) и добавила сторож. Итог: тестов **717→749** (+32/−0), battery EXIT=0 целиком, голден/снапшот/подпись банка нетронуты. (г) Финальный ре-чек V2+аудита (по слову владельца) — ВСЁ CONFIRMED исполнением: состав V2-диффа точен (5 файлов, ничего сверх), каждая V2-ось и все три §11б-теста перебиты мутациями заново (слепое пятно «ServesTheReadModelsIdentically зелёный при мёртвом быстром пути» воспроизведено буквально), счёт 749 поимённо, стенд `tm-manifest-v2` 2283/4276/5071 c единым cutTag, cmp-проекции IDENTICAL (0.08–0.16 c против 1.42–1.43 c). Одна MINOR-находка ре-чека принята БЕЗ дофикса: тест границы `manifestMaxChunks` пинит механизм, но не значение константы (absurd выводится из неё же — поднятие до 1<<62 тест переживает); диспозиция — значение константы сторожится ревью, NOTE в ревью-шапке отчёта.
|
||||
|
||||
**2. Ратификации приёмки.** (а) **Граница `report`/`export --pairs`/ре-билла на пере-нарезке — ГРАНИЦА, не долг**: `QualityReport` по построению требует исходный текст юнита (residual-скан), манифест текст не несёт намеренно. (б) **`done` пофазных счётчиков = «разрешено волной»** (ok/flagged/skipped); следствие `edit.done ≥ done` — честное, с оговоркой draft-only в доккоменте. (в) **`--ceiling-usd` = КНИЖНЫЙ потолок в силе, не бюджет прогона**: леджер сравнивает с накопленным committed+reserved книги; пересчёт «пользовательский прирост → абсолют» — обязанность ПЛАТФОРМЫ, данные уже отдаются (`committed_usd`/`reserved_usd` в `status --json`); вторая денежная ось НЕ заводится. (г) Все 8 девиаций §8 отчёта приняты (`tmctl manifest` · `--ceiling-usd` в redrive · счётчики на главу · `draftOnly` · ленивый провайдер · `BankStopVariant` · асимметрия таблиц · length-prefix ключа id). (д) **`heading` манифеста = ВРЕМЕННЫЙ рендер движка (решение владельца 09.08)**: противоречие с закрытым К-3 признано, настоящие заголовки — строка 160 (titleRaw, Этап 0 структуры глав). (е) **Форма артефактов для потребителей**: `unit.id = <chapterID>:<cutTag>:<firstChunkIdx>`, манифест `tm-manifest-v2` — платформе читать как новую форму (пинг в зонный журнал). (ж) `.gitignore` дополнен паттернами сайдкаров.
|
||||
|
||||
**3. Бэклог.** Строки 99·100·101·125·145 ЗАКРЫТЫ (удалены из таблицы). Новые: **163** — версии модулей x/text|x/net меняют нарезку, не двигая ни ключ/снапшот/чекпойнты (класс всей чекпойнт-плоскости, унаследован манифестом); **164** — rebill-проекция `status` с nil baseMemory может завышать `RebillUSD` против translate (преexisting, консервативная сторона). NOTE без строк: провести `--ceiling-usd` в `status` — по желанию платформы (в пинге зоны); fsync каталога; U+001F закрыт V2-1. Строки 160–162 (структура глав по research/27) заведены до лендинга тем же днём.
|
||||
|
||||
**4. research/27 «Определение глав» принят ФАКТУРОЙ** (веб-ресёрч · панель двух моделей · карта по коду · жизненный цикл дерева §3а · развилка парсинг-стадии закрыта словом владельца: оригинальные названия, платный перевод со старта прогона; мини-сессия названий §5а — редакторский слот · инъекция банка · двухфазность вокруг подписи). **Дизайн стройки НЕ ратифицирован** — отдельное решение при постановке строк 160–162 в очередь.
|
||||
|
||||
**5. Очередь дальше:** приёмка платформы-раннера (сессия жива) · лендинг эксп-22 (8 CONFIRM/DENY владельца) · промт эмиттера (103 + деньги шва) — эмиттер-сессии ПЕРЕД стартом прочесть PD-95/105/79/99/107/60/61 и D39.106 (09.08.2026, оркестратор №15). ✅
|
||||
|
|
|
|||
666
docs/archive/reports/CONTRACT_BLOCKERS_2026-08-08.md
Normal file
666
docs/archive/reports/CONTRACT_BLOCKERS_2026-08-08.md
Normal file
|
|
@ -0,0 +1,666 @@
|
|||
# Отчёт бэкенд-сессии: движковые блокеры контракта 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).
|
||||
|
|
@ -6,6 +6,22 @@
|
|||
|
||||
## Текущее состояние
|
||||
|
||||
- **P4 (08–09.08) построила РАННЕР целиком** (раздел «Сессия P4» ниже): транзиентный systemd-юнит на
|
||||
прогон · очередь River + воркер · реконсилятор (свип + бут) · тейлер `events.jsonl` с курсором ·
|
||||
первые пять ручек `/v0` по контракту 0.2.0 · дев-интейк книг. Батарея зелёная целиком с живым
|
||||
PostgreSQL 18.4 под `-race`, линтер 0 issues, `make vuln` чист, **скипов ноль**. Тестов
|
||||
**105 → 242** (удалённых ноль). **Дерево НЕ закоммичено — ждёт приёмки.**
|
||||
- ⚠ **Адверсариальное ревью (четыре независимых верификатора) нашло ТРИ денежные ошибки и три
|
||||
запирающие прогон — все закрыты в этом же дереве, с пинами и повторной живой пробой.** Раздел
|
||||
«Адверсариальное ревью» ниже; главная — расчёт брал пожизненную трату КНИГИ вместо траты прогона
|
||||
(PD-124), и её независимо воспроизвели два верификатора.
|
||||
- **Регистр после P4 — 143 строки** (`python3 docs/scripts/counts.py`): 107 закрыто · 3 приняты риском ·
|
||||
1 закрыт ратификацией · **32 открыто** (1 major — PD-113, 9 minor, 22 info). Закрыты в P4 шесть
|
||||
СТАРЫХ строк: PD-43 · PD-81 · PD-82 · PD-97 · PD-99 · **PD-105** (ратификация D39.119 +
|
||||
реализация). Заведены тридцать шесть новых, PD-108…PD-143: двадцать пять найдены И закрыты
|
||||
внутри пака (пять — собственной самопроверкой, двадцать — адверсариальным ревью), десять
|
||||
оставлены открытыми вопросами и гейтами.
|
||||
|
||||
- **P3 (08.08) отработала фикс-лист приёмки P2 ЦЕЛИКОМ — все восемь пунктов** (раздел «Сессия P3»
|
||||
ниже): PD-80 · PD-79 · PD-83/84/85 · PD-100 · PD-106 · PD-91 · PD-95 · PD-104 (часть док↔код).
|
||||
22 посадки, 22 поймано; батарея зелёная офлайн и с живым PG 18.4 под `-race`, линтер 0 issues,
|
||||
|
|
@ -50,6 +66,257 @@
|
|||
- Дизайн-ответы К-4 · К-7 · К-12 · форма П-5 — ниже, ПРЕДЛОЖЕНИЯМИ на ратификацию.
|
||||
- Контрактных ручек нет намеренно: они ждут ратификации К-4/К-7 (форма ответов) — это П-1.
|
||||
|
||||
## Сессия P4 (08–09.08): раннер — юнит-на-прогон, очередь, реконсилятор, тейлер, первые ручки /v0
|
||||
|
||||
**Каждое число ниже — с командой, которой получено.** Дерево не коммичено; в индексе ничего не держу.
|
||||
|
||||
### Работа 1 — транзиентный systemd-юнит на прогон
|
||||
|
||||
`internal/runner/runner.go` (юнит), `marker.go` (маркер выхода), `engine.go` (инвокация движка).
|
||||
|
||||
**Привилегия-модель решена ДО постройки, и второй вариант отвергнут по СВОЙСТВУ, а не по вкусу.**
|
||||
Выбран linger-пользователь: юниты создаются в СОБСТВЕННОМ менеджере платформы, новых привилегий в
|
||||
рантайме ноль. Polkit-вариант research/25 непригоден: **polkit авторизует ГЛАГОЛ, а не свойства** —
|
||||
`StartTransientUnit` отдаёт выбор `ExecStart=`/`User=` вызывающему, а транзиентный СИСТЕМНЫЙ юнит без
|
||||
`User=` идёт от root, то есть правило выдаёт сервису root и сузить это нечем. Плюс до systemd v257 в
|
||||
запрос авторизации не попадает даже ИМЯ юнита (systemd issue #17224 → PR #34651, влит 09.10.2024),
|
||||
так что на стенде (systemd 255, `systemctl --version`) «узкое правило» не узкое вовсе. Разбор —
|
||||
`STACK_DECISIONS` §15.
|
||||
|
||||
**Замерено на стенде, не выведено из доки** (все команды — `systemd-run --user`, стенд без root):
|
||||
|
||||
| Свойство | Как замерено | Результат |
|
||||
|---|---|---|
|
||||
| Юнит переживает того, кто его создал | спавнер-скрипт стартует юнит и выходит | `ActiveState=active`, маркер `success` |
|
||||
| `--collect` ⇒ состояние systemd не истина | `systemctl --user show` после выхода | `LoadState=not-found`, `ExecMainStatus=0` для прогона, вышедшего с кодом **3** |
|
||||
| Маркер несёт исход | `ExecStopPost` на трёх исходах | `exit-code/exited/3` · `success/killed/TERM` · `oom-kill/killed/TERM` |
|
||||
| **Лимиты в `app.slice` НЕ применяются** | `cat app.slice/cgroup.subtree_control` | **пусто**; лист без `memory.max`; процесс, потрогавший 400 МиБ, пережил `MemoryMax=64M` |
|
||||
| Лимиты в своём срезе применяются | то же в `tm-runs.slice` | `memory.max=67108864`, `pids.max=32`, тот же процесс — `oom-kill`, `Memory peak: 64.0M` |
|
||||
| `%`-спецификаторы в `--property=` не раскрываются | `ExecStopPost` с `100%_done`/`%n` | доехали буквально ⇒ экранировать `%` не нужно и было бы неверно |
|
||||
| `ProtectHome=yes` прячет `/run/user/<uid>` | проба под юнитом | `Permission denied` ⇒ своя же шина недостижима; `ProtectHome=tmpfs`+`BindPaths` ⇒ сокет виден |
|
||||
| Шине хватает `DBUS_SESSION_BUS_ADDRESS` | `env -u XDG_RUNTIME_DIR` | работает; без обоих — `Failed to connect to bus` |
|
||||
|
||||
**Ответ PD-13 живёт в cgroup ПРОГОНА и измерен.** ⚠ И там же найдено собственным флейком: `MemoryMax`
|
||||
БЕЗ `MemorySwapMax=0` не ограничивает прогон — ядро выдавливает страницы в своп (у среза
|
||||
`swap peak: 692.5M`), процесс выживает и тормозит до скорости диска. Для перевода это хуже отказа:
|
||||
тормозящий часами прогон продолжает платить за каждый прошедший вызов. Снято: `MemorySwapMax=0` идёт
|
||||
вместе с `MemoryMax` (`runner.go`), три прогона пина подряд зелёные, три посадки «убрать своп-лимит»
|
||||
подряд красные.
|
||||
|
||||
Пиннинг версии (строка 139): `run_attempts.engine_binary` пишется ДО создания юнита, новая попытка
|
||||
его НАСЛЕДУЕТ, резюм исполняет именно его, а переход на другую сборку требует явного
|
||||
`TM_PLATFORM_RESUME_MAY_CHANGE_ENGINE`. ⚠ Первая редакция закрывала эту строку НАПОЛОВИНУ — путь
|
||||
читался каналом ремонта и не исполнялся резюмом; поймано моей же пост-сверкой диффа с промтом, не
|
||||
батареей (обе половины компилировались и всё было зелёным). PD-143.
|
||||
|
||||
### Работа 2 — очередь River + воркер
|
||||
|
||||
`go.mod`: `github.com/riverqueue/river v0.42.0` + `riverdriver/riverpgxv5` (пин из `STACK_DECISIONS`
|
||||
подтверждён). `internal/runs/queue.go`.
|
||||
|
||||
**Холд берётся ДО спавна и в ОДНОЙ транзакции с созданием прогона, попытки и записи очереди**
|
||||
(`pgstore.StartRun`, `runs.go:66`): холд без прогона — деньги, зарезервированные ни за чем; прогон без
|
||||
холда — трата незарезервированного; запись очереди без обоих — воркер, спавнящий движок против книги,
|
||||
у которой нет бухгалтерии. Очередь вставляет своё задание через колбэк с той же `pgx.Tx`.
|
||||
|
||||
**Гард «один живой прогон на книгу» — исполнением:** частичный уникальный индекс
|
||||
`runs_one_live_per_book` мапится в доменную `ErrRunInFlight` (не сырой SQLSTATE), ручка отдаёт 409.
|
||||
Живая проба: второй `POST .../runs` на ту же книгу → **409**, `Reserved` не сдвинулся.
|
||||
|
||||
`MaxAttempts: 1` у задания — намеренно против рефлекса очередей: повтор здесь не доделывает работу, а
|
||||
спавнит ВТОРОЙ движок; восстановление — работа реконсилятора. Живая проба: повторная доставка задания
|
||||
на живой прогон не создала второго юнита (`len(started)` = 1).
|
||||
|
||||
### Работа 3 — реконсилятор
|
||||
|
||||
`internal/runs/reconcile.go`. На КАЖДЫЙ выход юнита — через **ExecStopPost-маркер**, не D-Bus: сигнал,
|
||||
посланный в момент, когда платформа лежит (а она лежит несколько раз в неделю по замыслу), теряется;
|
||||
файл — нет. На БУТЕ тот же свип перезапускает прерванные прогоны (строка 138): истина — каталоги книг
|
||||
и Postgres, systemd спрашивается только «жив ли юнит», и это улика, не вердикт.
|
||||
|
||||
Разграничитель «не стартовал ещё» / «умер без слова» — время (`spawnGrace` 60 с). Перезапуск даёт
|
||||
новой попытке ОСТАТОК бюджета (`budget − RunSpent`), иначе один прогон потратил бы потолок дважды;
|
||||
если остатка нет — `paused`/`credit_exhausted`, не `failed`.
|
||||
|
||||
**Расчёт — из фигуры движка, прочитанной ПОСЛЕ выхода процесса** (`status --json`, ратифицированный
|
||||
канал ремонта; лока нет, гадать не о чем). Не прочиталась — резервация остаётся ОТКРЫТОЙ и попадает в
|
||||
`UnsettledRuns` для следующего свипа. ⚠ Эскроу-половина research/25 (write-ahead intent, `uncertain`,
|
||||
`closing`) НЕ строилась: это строка 136 и её собственный промт.
|
||||
|
||||
**Резюнк — честно редкий и честно грубый:** по умолчанию 5 минут (`TM_PLATFORM_RESYNC_EVERY`), и
|
||||
только там, где чинить нечего — курсор не двигался ЛИБО материализация в карантине. Причина в цене:
|
||||
каждый вызов заново ингестит и режет исходник (1.4–1.5 с CPU на книге 23 МБ, строка 100).
|
||||
|
||||
### Работа 4 — тейлер `events.jsonl`
|
||||
|
||||
`internal/ingest/tail.go`. Курсор `(engine_run_id, seq)` коммитится в ОДНОЙ транзакции с эффектом
|
||||
(`pgstore.RunSink.Apply`), байтовое смещение — хинт. Файла ещё нет (эмиттер = строка 103) — тейлер
|
||||
спокойно ждёт (`ErrNoJournal` не ошибка свипа).
|
||||
|
||||
**PD-105 ратифицирован этим паком и реализован:** дубль `seq` — НОРМА, пропускается идемпотентно и
|
||||
без фатала; тот же `seq` с ДРУГИМ payload — `ErrPayloadConflict` → карантин ПРОЕКЦИИ (не прогона:
|
||||
движок тратит уже зарезервированные деньги, и наша неспособность читать его журнал — не повод это
|
||||
выбросить). Пропасть остаётся ошибкой. Строка PD-105 реестра обновлена.
|
||||
|
||||
Частичная последняя строка не читается как событие (писатель ещё пишет). Журнал пер-книжный и
|
||||
append-only, поэтому резюм дописывает ВТОРОЙ `hello`: читатель ведёт область и не судит чужие строки.
|
||||
|
||||
### Работа 5 — ручки `/v0` по контракту 0.2.0
|
||||
|
||||
`internal/httpapi/v0.go`: `GET /books` · `GET /books/{bookId}` · `GET /books/{bookId}/run-options` ·
|
||||
`POST /books/{bookId}/runs` · `GET /usage`. Прогресс/статус прогона отдаётся внутри карточки книги —
|
||||
отдельной ручки прогона в спеке НЕТ, и выдумывать её я не стал.
|
||||
|
||||
`CeilingBounds`: максимум = `Balance` КАК ЕСТЬ (холд — дебет в момент взятия), подрезан И остатком
|
||||
книги; `max_chapters: 0` легален. `default_chapters` = верх шкалы — продуктовая политика платформы
|
||||
(D39.115 §4), компромисс назван вслух в коде и вынесен вопросом ниже.
|
||||
|
||||
**Интейк:** дев-инструмент `tmplatformctl book add` (Go-подкоманда, а не шелл-скрипт: тестируется).
|
||||
`POST /books` (multipart) НЕ взят — обоснование строкой **П-9** зонного бэклога.
|
||||
|
||||
**Пересчёт «главы→доллары»** — константа платформы `$0.03`, провенанс: exp08 v2 ($10.94 на 500 глав =
|
||||
$0.0219) через ревизию D30.4 (+15–25% → $0.0252–0.0274), округление ВВЕРХ. Движковой поверхности
|
||||
оценки не выдумывал; потребность — строка **П-10**.
|
||||
|
||||
### Работа 6 — регистр
|
||||
|
||||
Закрыты: **PD-43** (денежный контур получил вызывающих) · **PD-81** · **PD-82** · **PD-97** ·
|
||||
**PD-99** · **PD-105**. Заведены **PD-108…PD-116** (из них PD-108/110/111/116 — дефекты, найденные и
|
||||
закрытые внутри этого же пака; PD-112…115 открыты вопросами). Строки эмиттер-стороны (PD-60/61) не тронуты.
|
||||
|
||||
### Сквозная проба на боевом бинаре (не тестами)
|
||||
|
||||
Поднят `tmplatformd` против живого PG, с фейковым `tmctl` ($0, говорит ровно две поверхности:
|
||||
инвокацию с потолком и `status --json`).
|
||||
|
||||
| Шаг | Наблюдение |
|
||||
|---|---|
|
||||
| `book add` + `GET /v0/books` | книга в библиотеке, `status: not_started` |
|
||||
| `GET run-options` | `min 1 / max 333 / default 333` — $10 / $0.03 = 333, подрезано 500 главами книги |
|
||||
| `POST runs` (100 глав) | `202`, форма `Run` по спеке; юнит `tm-run-<id>-1.service` **active running** |
|
||||
| потолок, дошедший до движка | `--ceiling-usd 3.000000` = 100 × $0.03 |
|
||||
| второй `POST` на ту же книгу | **409** |
|
||||
| `/v0/usage` при открытом холде | `remaining_percent: 70` (холд — дебет) |
|
||||
| прогресс из журнала | `draft 4/10, edit 0/10` — пофазно, без затирания резюнком |
|
||||
| выход юнита | книга и прогон → `ready`, `finished_at` проставлен |
|
||||
| расчёт | грант $10 → движок отчитался $0.42 → баланс **9.580000** (не потолок $3) |
|
||||
| гигиена логов | `grep -c 'ceiling-usd\|3.000000\|bk_' daemon.log` = **0** |
|
||||
| systemd после `--collect` | юнитов **0** |
|
||||
|
||||
**Ратифицированное свойство D39.106 — прогон переживает деплой — проверено убийством платформы:**
|
||||
`pkill -9` по имени процесса → API отвечает `000`, юнит `active running`, журнал за 6 с вырос
|
||||
`10 → 13` строк. Платформа поднята заново → карточка показала `draft 21/300` (догнала всё, что было
|
||||
записано, пока её не было), через 6 с — `24/300`, юнит ОДИН, в логе новой платформы
|
||||
`grep -c 'run unit started'` = **0** (второго движка не создано). Остановка через systemd → маркер
|
||||
`success/killed/TERM` → статус **`stopped`** (не `failed`).
|
||||
|
||||
### Батарея и самопроверка
|
||||
|
||||
- `make check` с `TM_PLATFORM_TEST_DSN` (живой PostgreSQL 18.4, `-race`): все 13 пакетов зелёные,
|
||||
линтер **0 issues**. Прогнана дважды подряд.
|
||||
- Скипы: `go test ./... -count=1 -v | grep -c -- '--- SKIP'` = **0**.
|
||||
- `make vuln`: **No vulnerabilities found**.
|
||||
- Дифф `^func Test` ИСПОЛНЕНИЕМ (`git grep '^func Test' HEAD -- 'platform/**/*.go'` против того же
|
||||
грепа по дереву): **105 → 203**, удалённых **0**, добавленных **98**.
|
||||
⚠ Две мои промежуточные редакции этого счёта были неверны, и это записано, а не подчищено:
|
||||
сначала pathspec не тот, потом грep без `*.go` — второй ловил КОПИЮ теста, вставленную в этот же
|
||||
журнал приёмкой P1 (`platform-PROGRESS.md:683`), и рисовал ложное «удалён
|
||||
`TestHoldAndSettleOnTheSameAttemptDoNotDeadlock`». Тест на месте: `credits_test.go:308`.
|
||||
- **Посадки (мутации): 12 из 12** в самопроверке; **139 в аудите ревью** — 107 поймано, 32 пережили
|
||||
(8 не ослабления), по ним написано **22 новых пина**, каждый проверен своей посадкой (сначала 9/10,
|
||||
затем 9/9 — две мои мутации были сформулированы мимо и переделаны). Итого посадок в паке — **33
|
||||
моих, все пойманы**, плюс аудит ревью., каждая восстанавливалась после прогона:
|
||||
холд вне транзакции допуска · мапинг `runs_one_live_per_book` · high-water mark в синке ·
|
||||
`greatest` у spend · payload-конфликт · детект пропасти · частичная строка как событие ·
|
||||
область чужого потока (обе стороны: `mine := true` и `mine := false`) · срез прогона ·
|
||||
половина шкалы (та самая ошибка «минус Reserved») · грация спавна · потолок как `failed` ·
|
||||
argv в INFO-логе · `MemorySwapMax` · compare-and-set права на спавн.
|
||||
⚠ **Одна посадка ПЕРЕЖИЛА первую редакцию пина** и это записано, а не подчищено: high-water mark
|
||||
проверялся событием `progress`, а `progress` — присваивание, то есть идемпотентен сам по себе.
|
||||
Настоящий пин — считающий эффект (`unit_done`): `TestARedeliveredCountingEventDoesNotCountTwice`.
|
||||
|
||||
### Девиации и то, чего не сделал
|
||||
|
||||
1. **`503` на старте прогона не входит в перечисленные спекой статусы операции** — PD-112, вопрос
|
||||
владельцу контракта. Каждый разрешённый код в этой ситуации соврал бы.
|
||||
2. **Стоп по потолку сегодня отдаётся как `failed`** — PD-113, контрактно видимо. Движок возвращает
|
||||
потолок ошибкой (`errReserveCeiling`, сверено в HEAD), события потолка нет (строка 103). Ветка под
|
||||
событие построена и запинена; выдумывать порог «сколько процентов потолка = стоп» я не стал.
|
||||
3. **`POST /books` не взят** — П-9, с обоснованием. PD-72 закрывается вместе с ним.
|
||||
4. **Эскроу/`uncertain`/`closing` не строились** — строка 136, следующий денежный промт.
|
||||
5. **Стоп/резюм прогона как ручки контракта** не в списке работ промта — не строились; банк-стоп
|
||||
заканчивает попытку и прогон, резюм = новый прогон.
|
||||
6. **`Supervisor.Status` дев-пути починен** (PD-108) — он в моей зоне и это мой канал ремонта.
|
||||
|
||||
### Адверсариальное ревью (author≠reviewer), и почему это главный результат пака
|
||||
|
||||
Четыре независимых верификатора, ни один не читал отчёт (его на тот момент не существовало):
|
||||
(1) промт+дифф вслепую · (2) охота за дефектами ВНЕ карты · (3) сверка провода с openapi 0.2.0 ·
|
||||
(4) сила пинов посадками. Каждому запрещено писать в репозиторий; эксперименты — в копиях.
|
||||
|
||||
**Нашли три ДЕНЕЖНЫЕ ошибки и три запирающие прогон. Все закрыты в этом дереве, каждая с пином.**
|
||||
|
||||
| Находка | Чем это было | Как закрыто |
|
||||
|---|---|---|
|
||||
| **PD-124** (major) | Расчёт брал `committed_usd` — ПОЖИЗНЕННУЮ сумму КНИГИ (`SUM(committed_usd) … WHERE book_id`) — как трату прогона. Второй прогон книги оплачивал первый заново; после того как сумма книги перерастает потолок, каждый прогон стоит ровно свой потолок. **Воспроизвели ДВА верификатора независимо** | Попытка пишет базовую линию книги ДО старта (миграция 00010) и платит разницу; линию не прочитать = не стартуем |
|
||||
| **PD-125** (major) | Перезапуск при отложенном расчёте брал ВТОРОЙ холд и терял первый навсегда: списки его не находили | Перезапуск спрашивает `AttemptReservationOpen`; список несведённых ключуется на завершённости ПОПЫТКИ |
|
||||
| **PD-126** (major) | Юнит, который не удалось создать, оставлял «право на спавн» — и каждый свип перезапускал прогон, съедая потолок по свипу за раз | Неудавшийся `Start` снимает право; повторяется та же попытка |
|
||||
| **PD-127** (major) | Одна нечитаемая строка журнала возвращала ошибку ДО чтения маркера: прогон навсегда `translating` с висящим холдом | Любая ошибка журнала = карантин ПРОЕКЦИИ, жизненный цикл продолжается |
|
||||
| **PD-128** | Грация спавна мерилась от старта ПРОГОНА ⇒ у перезапущенной попытки её не было | Мерится от старта попытки |
|
||||
| **PD-129** | Инверсия порядка блокировок `books`↔`runs` между материализатором и финишером ⇒ взаимоблокировки | Книга блокируется первой везде |
|
||||
| **PD-130/131** | Любая ошибка чтения журнала выглядела как «догнали»; хендшейк не обязан был нести `seq 1` | Только настоящий EOF; `seq != 1` = плохой хендшейк |
|
||||
| **PD-132** | Холд прогона, который так и не стартовал, не возвращался | Нет линии и нет юнита ⇒ холд возвращается целиком |
|
||||
| **PD-133/134/135** | Инстанс без движка не отдавал даже библиотеку; пиннинг версии писался и не читался; прерванный прогон вставал `paused` без причины | Чтения монтируются отдельно; `EngineBinary` читается и используется; `PauseRun` вместо `FinishRun` |
|
||||
| **PD-117…121** | Потолок ниже минимума схемы отвечал 409 вместо 400 · `Usage.paused_reason` недостижим через путь реконсилятора · карточка несла ревизию ПРОГОНА (отстающую) · чужой курсор пагинации принимался · «exhausted» при остатке, на который прогон стартует | Все пять закрыты, каждая с пином |
|
||||
| **PD-136/138** | Деплой-юнит нёс обе диспозиции сразу; `go.mod` не тидинут | Переписано; `go mod tidy` |
|
||||
| **PD-142** | **Аудит силы пинов: 139 посадок, 107 поймано, 32 пережили** (8 из них — не ослабления: эквивалентный код или страховка DDL). Пережившие — не дефекты кода, а отсутствующие пины, часть на свойствах, объявленных закрытыми | 22 новых пина, каждый проверен своей посадкой. Самые весомые: блокировка строки попытки под КОНКУРЕНЦИЕЙ (восемь горутин на одно событие — последовательная доставка посадку не ловила) и **CSRF на контрактной поверхности** (снятие переживало всё, а это старт платного прогона с амбиентной кукой) |
|
||||
|
||||
**Оставлено открытым и названо:** PD-122 (`Library.revision` не монотонна при удалении книги — сегодня
|
||||
недостижимо, ручки удаления нет; правильное решение — свой счётчик области, это своя миграция и
|
||||
несколько путей записи, поэтому НЕ сделано, а объявлено гейтом) · PD-137 (упорядочение на
|
||||
пользовательский менеджер — UID site-specific, инструкция в шапке юнита) · PD-139 (пути книг в
|
||||
ERROR-логах — диспозицию надо принять осознанно) · PD-140 (`Service.Stop` не подключён: ручек
|
||||
стопа/резюма в скоупе промта не было) · PD-141 · PD-112/113/123.
|
||||
|
||||
**Что ревью подтвердило, а не опровергло:** привилегия-модель (с одним уточнением, принятым:
|
||||
«никакое правило не сузит» верно для ТРАНЗИЕНТНЫХ юнитов; шаблонный системный юнит с фиксированным
|
||||
`User=`, запускаемый `StartUnit`, — вариант, которого мой разбор не касался) · арифметика
|
||||
`CeilingBounds` (баланс КАК ЕСТЬ, без второго вычитания холдов — проверено на живом PG пятью
|
||||
раскладами) · отсутствие кросс-аккаунтных чтений и стартов · инвариант `balance == SUM(ledger)`
|
||||
(сломать не удалось) · правила тейлера по областям и курсору.
|
||||
|
||||
**Возражение ревью, которое я принял только частично.** Верификатор предложил различать стоп по
|
||||
потолку через `ceiling_pct` из `status --json`. Не взял: `ceiling_pct` считается против
|
||||
`book_ceiling_usd`, то есть против потолка ИЗ `book.yaml`, а наш потолок приходит аргументом прогона
|
||||
(строка 145), которого в HEAD нет — значит смысл поля зависит от ещё не залендённой формы. Взять его
|
||||
сейчас значило бы построить дискриминатор на поведении, которого я не могу проверить. Записано в
|
||||
PD-113 как названная и оценённая альтернатива.
|
||||
|
||||
**Повторная сквозная проба на боевом бинаре после починок** (чистая база, фейковый движок с
|
||||
НАКОПИТЕЛЬНЫМ счётчиком книги, как у настоящего): грант $10.00 → прогон 1 (движок насчитал $0.40) →
|
||||
баланс **9.600000** → прогон 2 на ТОЙ ЖЕ книге (счётчик книги $0.40 → $0.80) → баланс **9.200000**,
|
||||
резервов ноль. До починки второй прогон списал бы $0.80.
|
||||
|
||||
### Вопросы (канал вопросов промта; НЕ тихая интерпретация)
|
||||
|
||||
1. **`503` вне перечня спеки** (PD-112) — вносить в спеку или назвать другой код?
|
||||
2. **`default_chapters` = верх шкалы.** Когда книга дороже баланса, верх шкалы резервирует ВЕСЬ
|
||||
баланс под одну книгу, и вторую начать нельзя (D39.110 §2в). Альтернатива — доля от максимума;
|
||||
какая именно — вопрос продуктовый, не инженерный.
|
||||
3. **Конфигурация (вопрос ВЛАДЕЛЬЦА 08.08, PD-114).** Ответ по существу: единого stdlib-пути в Go
|
||||
нет; мейнстрим — либо 12-factor «только окружение» (наш выбор, записан в доккомменте `config.go`,
|
||||
деплой уже использует systemd-нативные `EnvironmentFile=`+`LoadCredential=`), либо слоёная
|
||||
`флаги > env > файл > дефолты` (koanf/viper). Ниже нормы у нас другое: **27 переменных**
|
||||
(грепнуто), **ноль флагов** у демона, **нет печати эффективной конфигурации** при старте.
|
||||
Вопрос владельца при этом нашёл реальный дефект в этом же паке: дефолт «$/глава» лежал в двух
|
||||
местах — исправлено, резолвится только в `config`.
|
||||
4. **Абстрактный вопрос владельца, и он подтверждается фактом (PD-115).** `ENGINEERING_STANDARDS` §2
|
||||
называет внешнюю версионированную базовую линию ровно для ОДНОЙ оси — безопасности (ASVS 5.0 L2 +
|
||||
API Top-10 2023). Всё остальное — собственная проза зоны. Разница видна эмпирически: PD-57/PD-58
|
||||
нашлись именно сверкой с RFC 9700/9207 и NIST SP 800-63B. Там, где эталона нет, сверять не с чем —
|
||||
грепнуто на 08.08: метрик и трейсинга **ноль**, процедуры бэкапа/восстановления в `deploy/` нет,
|
||||
SLO не заданы. Предложение: §2 получает по эталону на ось; ратификация — оркестратора.
|
||||
|
||||
## Ратификация приёмкой P3 (оркестратор №15, 08.08)
|
||||
|
||||
**Вердикт: фикс-пак ПРИНЯТ и заленден.** Восемь пунктов фикс-листа отработаны, девять строк
|
||||
|
|
@ -1220,3 +1487,11 @@ floor остался 1.26.4, общий с движком). Плюс снят м
|
|||
не возвращает вовсе (падает), поэтому ветки её обработки убраны, а не оставлены изображать проверку.
|
||||
|
||||
Дерево не коммичено — лендит оркестратор.
|
||||
|
||||
---
|
||||
|
||||
**Пинг оркестратора №15 (09.08, D39.122): движковый пак «блокеры контракта» ПРИНЯТ и заленден `0e69bc1` — пять поверхностей для платформы существуют. ФИНАЛЬНЫЕ формы (менялись трижды за приёмку — старые в переписке игнорировать):**
|
||||
- **Манифест глав:** `<project_db>.manifest.json`, `manifest_version: "tm-manifest-v2"` (v1 движок сам отклоняет); id главы = 16 hex (стабилен через пере-нарезку); **`unit.id = <chapterID>:<cutTag>:<firstChunkIdx>`** — тег разреза 8 hex, при любой смене нарезки/данных пары unit.id УМИРАЮТ намеренно (id жив ⇒ якорь цел); поле `heading` — ВРЕМЕННЫЙ рендер движка «Глава N», НЕ метка книги (решение владельца 09.08; настоящие заголовки — строка 160 бэклога движка). $0-команда `tmctl manifest` строит дерево до первого прогона (первое касание создаёт БД проекта — то же поведение, что у status).
|
||||
- **Прогресс:** `status --json` → `progress: {draft:{done,total}, edit:{done,total}}` на книге и в каждом элементе `chapters`; `done` = «разрешено волной» (ok/flagged/skipped); волна, которой нет, — `total: 0`; процент не отгружается — собирает клиент.
|
||||
- **Банк:** `<project_db>.bank.json` (весь банк тремя статусами; id термов — длино-префиксированный хеш ключа уникальности, стабилен через пересборку) и `<project_db>.bank-stop.json` (полная таблица подписи; `conf: null` ≠ 0). Все сайдкары пишутся атомарно (temp+rename) — читать можно во время прогона.
|
||||
- **Потолок (строка 145): `tmctl translate|redrive --ceiling-usd <usd>` — это КНИЖНЫЙ ПОТОЛОК В СИЛЕ, не бюджет прогона** (ратифицировано D39.122): леджер сравнивает значение с накопленным committed+reserved книги. Пересчёт пользовательского «прирост в главах» (D39.110) в абсолют — ОБЯЗАННОСТЬ платформы: `аргумент = committed_usd + reserved_usd (из status --json) + прирост×оценка`. Значение ниже уже потраченного откажет первой же резервации. День-потолок не перекрывается. Опция по желанию зоны: движок готов провести `--ceiling-usd` и в `status`, чтобы мониторинг видел действующий потолок capped-прогона (сегодня status показывает книжный) — скажите, заведём строку.
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue