textmachine/docs/architecture/16-events-emitter.md

132 lines
33 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

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

# Эмиттер прогонных событий (шов движок→платформа) — сборка-норматив (строки 103+165, докс-пак 167)
> ⚠ **ЭМИТТЕР ПОСТРОЕН И ПРИНЯТ (D39.131, `9cfe080`, 14.08):** `events.jsonl` (StreamVersion 1.1) · exit 4/5/полоса 1019 · пер-вызовный репэйр-гейт; развилки §5 решены (одиночный JSONL · PD-60 = громкая деградация · fsync каталога один раз · outbox «факт → outbox → файл», `spend` внутри settle-транзакции). Док остаётся справочником формы; фактический код первичен (`backend/internal/runevents/` · `pipeline/events.go` · `store/outbox.go`). **Платформенная половина ПОСТРОЕНА P6 и принята D39.132 (15.08) — шов закрыт с обеих сторон, PD-113 закрыт**; упоминания «PD-113 открыт / остаток П-15» в теле ниже — историческая фактура эпохи стройки.
>
> **Статус: СБОРКА-НОРМАТИВ.** Этот док ничего не решает сам — он собирает в одно место уже ратифицированные решения об эмиттере `events.jsonl` (строка 103 бэклога), разбросанные по D-нотам, `research/25`, реестру дефектов платформы и коду зоны ingest. **Источники первичны; при любом конфликте этого дока с `05-decisions-log.md` побеждает D-лог.** Каждое утверждение несёт указатель; дословные цитаты — в кавычках-ёлочках или блоках.
>
> **Для кого:** обязательное пре-чтение бэкенд-сессии эмиттера (строка 103 + деньги шва) — до промта и до первой строчки кода. Вторичный читатель — оркестратор при приёмке пака.
## 1. Что строим и зачем
**Что.** Строка 103 бэклога (`docs/PROGRESS.md:149`), дословно ядро ячейки:
> «**NDJSON-эмиттер прогонных событий со стабильным словарём** (машинный канал живого статуса для платформы, `research/23` §2): NDJSON-логи уже есть (`LOG_FORMAT=json`, авто-оси trace/book/chapter/stage/role — `obs/logging.go:15-54`), но сообщения прозой, version-хендшейка нет, а **событие потолка отсутствует вовсе** (деньги только в тексте ошибки `stagerun.go:472-494`); нужен словарь-enum + version-хендшейк первой строкой (образец terraform -json: минор = ignore-unknown, мажор = reject) + событие потолка; все границы — единичные call-sites в pipeline рядом с готовыми slog; человеческие логи уже на stderr — stdout-дисциплина соблюдена. **Форма ратифицирована D39.106 (research/25 §эмиттер):** `events.jsonl` каталога книги = outbox-проекция коммитов SQLite (та же транзакция, что чекпойнт) · fsync/seq/hello-версия · события денег и потолка · развилка формата (JSONL против пофайловых/CRC-фрейминга) — решить дизайном · `status --json` отдельным каналом (ремонт, тот же flock)».
**Зачем (две ноги).** (а) *Свежесть статуса*: по слову владельца 08.08 «эмиттер нужен СВЕЖЕСТИ статуса и деньгам шва, не первым экранам — резерв на это время = периодический `status --json`-резюнк (построен в P1)» (D39.119 п.1, `05-decisions-log.md:390`). (б) *Деньги шва*: пока события нет, «экран покажет „ошибка“ там, где верно „остановлено: лимиты“» (PD-113, `DEFECT_REGISTER.md:18`), причём после PD-158 «PD-113 остаётся ЕДИНСТВЕННЫМ открытым major до эмиттера; … консервативный потолок останавливает ровно на исчерпании холда, и „деньги кончились“ неотличимо от „инфраструктура упала“» (D39.123 п.2(ж), `05-decisions-log.md:428`).
**Что уже есть со стороны движка.** NDJSON-инфраструктура логов: `obs.NewLogger` пишет slog в **stderr**, `LOG_FORMAT=json` даёт JSONHandler, UTC-время (`backend/internal/obs/logging.go:15-31`); оси trace_id/book/chapter/chunk/stage/role навешиваются из контекста автоматически (`logging.go:41-54`). Деньги при стопе по потолку существуют только в тексте ошибки: `" (committed=$%.6f reserved=$%.6f, denied estimate=$%.6f, ch%d/chunk%d/%s)"` и `"pipeline: book USD ceiling reached ($%g)%s — %s or stop: %w"` / `"pipeline: daily USD ceiling reached ($%g)%s: %w"` с сентинелом `errReserveCeiling` (`backend/internal/pipeline/stagerun.go:489-513`, сентинел — `escalation.go:49`). Trace id чеканится на каждый вызов в `cmd/tmctl/main.go:67-70` — это сегодняшний кандидат в `engine_run_id` (так и записано платформой, `platform/internal/ingest/events.go:75`).
**Что эмиттер НЕ решает** (не тащить в пак): реконсилятор и свип — построены платформой, строки 138·139 закрыты (D39.123 п.3, `05-decisions-log.md:430`) · write-ahead intent / `uncertain`-эскроу / `closing` — отдельная строка 136 (`docs/PROGRESS.md:85`) · пиннинг версии бинаря на прогон — «исполняется и резюмом — PD-143/134 закрыты» (D39.123 п.3) · движковая оценка $/глава — строка 166 (`docs/PROGRESS.md:114`) · тейлер/декодер платформы — уже построены (пакет `platform/internal/ingest/`), эмиттер пишет ПОД их контракт.
## 2. Ратифицированная форма (D39.106 · research/25)
Канон — D39.106 п.2 (`05-decisions-log.md:228`), дословно:
> «**2. Форма (заменяет ТРАНСПОРТ D39.85 §1; остальное D39.85 в силе):** движок = транзиентный systemd-юнит на прогон (`Restart=no`, SIGTERM); события = `events.jsonl` каталога книги как проекция коммитов SQLite движка (outbox, та же транзакция, что чекпойнт); платформа тейлит, курсор `(engine_run_id, seq)` коммитится с эффектом в одной Postgres-транзакции; `status --json` — ремонт по остановленному прогону. Формат NDJSON/hello/seq, деньги (холд+потолок = защита, события = свежесть, D39.84/D39.100) и запрет на живой SQLite — без изменений.»
Разворачивание по `research/25` (`docs/research/25-seam-cold-review.md`):
- **Журнал = outbox, не вторая запись**: «События — **`events.jsonl` в каталоге книги**, append-only, как **проекция** уже закоммиченных строк SQLite движка (та же транзакция, что чекпойнт chunk×stage; outbox, не вторая запись)» (`25:21-22`). Следствие для эмиттера: строка события рождается в той же SQLite-транзакции, что чекпойнт, — не «после», не «параллельно».
- **Тейл с курсором**: «Платформа **тейлит** журнал; запись курсора — `(engine_run_id, seq)`, байтовое смещение — хинт; курсор коммитится в той же Postgres-транзакции, что эффект события» (`25:23-24`). Смещение — хинт и может быть отброшено; решает курсор (`platform/internal/ingest/tail.go:31-35`).
- **events ≠ status**: «`events` (fsync, seq, деньги) ≠ `status` (atomic rename, косметика, не для биллинга)» (`25:73`). `status --json` — ремонт-канал по остановленному прогону, «берёт тот же flock — проверка „остановлен“ не есть взаимоисключение (GPT)» (`25:79`).
- **Файл — пер-КНИГА, поток — пер-процесс**: имя журнала ратифицировано кодом платформы: `const JournalFile = "events.jsonl"` — «the engine's event journal, in the BOOK's directory (D39.106 §2)» (`tail.go:16-17`). `Seq` — «per PROCESS and starts at 1»; ключ идемпотентности — `(Hello.EngineRunID, seq)`, НЕ прогон платформы: «a resumed run is a new process whose seq restarts» (`events.go:62-70`). Резюм дописывает в тот же файл новый hello; тейлер пропускает строки чужого `engine_run_id` (`tail.go:58-60`).
- **Сброс буфера на всех путях выхода**: «сброс буфера на всех путях выхода (`os.Exit` пропускает defer — теряются финальные события)» (`25:82`); то же PD-61(а) — см. §4.
- **Живость**: «живость = растущий `seq`, не удерживаемый лок; политика обратного давления выбирается явно» (`25:83`).
**Отвергнутые альтернативы** (`25:33-44`; повторены в доккомменте зоны, «so that none of them comes back as a fresh idea» — `events.go:21-23`): платформа-родитель + пайп stdout/fd — **0 из 15** («время жизни движка — подмножество платформы: деплой/рестарт убивает или осиротляет прогон», `25:38`) · выделенный fd 3 — **0** (`25:43`) · stdout/journald как источник событий — **0** («journald молча роняет под всплеском и ротирует сам; людям — да, контракту — нет», `25:44`) · gRPC/HTTP поверх движка — 0 (`25:39`) · push в платформу — 2→0 (`25:40`) · брокер — 0 (`25:41`) · монолит — 0 из ~18 (`25:37`). Пайп-путь `supervisor.go` — ДЕВ-режим и только (D39.106 п.3 `05-decisions-log.md:230`; `events.go:23`).
**Словарь событий — ПРЕДЛОЖЕНИЕ платформы, записанное кодом**: «The vocabulary below is therefore the platform's PROPOSAL, written as code so the engine zone can answer it with a diff» (`events.go:5-6`). Состав (`events.go:37-147`): `StreamVersion = "1.0"` · типы `hello` / `progress` / `unit_done` / `bank_stop` / `ceiling` / `spend` / `finished` · конверт `{seq, type, time, data}` · `Hello{stream_version, engine_run_id, book_id, chunker_version}` · `Progress` — пофазные счётчики draft/edit + `eta_seconds` (семантика `done` = «разрешено волной», ok/flagged/skipped — D39.122 п.2(б), `05-decisions-log.md:416`) · `UnitDone{chapter, unit, wave, shipped, flagged, reason}` · `BankStop{terms_proposed}` (полная таблица — артефактом, не потоком: `events.go:117-118`) · `Ceiling{halted}` · `Spend{committed_micro_usd}` · `Finished{outcome: clean|flagged|bank_stop|failed}`.
## 3. Контракт декодера и дублей (PD-105 · D39.123 п.2(в))
Ратификация — D39.119 п.3 (`05-decisions-log.md:394`), дословно:
> «доставка событий шва = at-least-once; ДУБЛЬ `seq` — норма повторного чтения, декодер обязан принимать идемпотентно без фатала; тот же `seq` с ДРУГИМ payload = карантин прогона (research/25)».
**Расхождение, разрешённое позже**: «карантин прогона» (D39.119 п.3 и `research/25:77-78` «тот же seq с другим payload = карантин прогона, не тихий no-op») уточнён D39.123 п.2(в) (`05-decisions-log.md:428`): «**PD-105-реализация: карантин ПРОЕКЦИИ (попытки), не прогона** — уточнение D39.119; деньги прогона сеттлятся ремонт-каналом». Побеждает D39.123 как более свежая нота.
Фактическое поведение читателя (текущий текст PD-105, `platform/docs/DEFECT_REGISTER.md:192` — закрыт ратификацией и реализацией):
> «Транспорт — тейл `events.jsonl` с курсором, поэтому повторное чтение строк НОРМА: `ingest.Tail` пропускает `seq <= last_seq` идемпотентно и не возвращает ошибку, а `pgstore.RunSink.Apply` пере-проверяет тот же high-water mark ВНУТРИ транзакции эффекта. Тот же `seq` с ДРУГИМ payload = `ErrPayloadConflict` → карантин ПОПЫТКИ, то есть её проекции: материализация останавливается, а жизненный цикл прогона продолжается — движок тратит зарезервированные деньги, и наша неспособность прочитать журнал не повод их выбросить (`pgstore.Quarantine` пишет только `run_attempts.quarantine_reason`, свежесть переходит на ре-синк). Сверка по sha256 строки (`run_attempts.last_line_sha256`). Пропасть (`seq > last+1`) осталась ошибкой — строки потеряны, читать дальше нечего. … Фатальный `ErrStreamGap` на дубле в `decoder.go` остаётся только на ДЕВ-пути пайпа, где передоставки нет».
Что это значит для эмиттера, по коду читателя:
- **hello — всегда первая строка потока, seq=1** (`decoder.go:64`: «hello carries seq 1, want 1»); версия — правило terraform, ратифицировано D39.85: «a MINOR bump adds fields and event types — unknown ones are ignored; a MAJOR bump is refused» (`events.go:34-37`); мажор-несовпадение = `ErrUnsupportedVersion` (`events.go:150-163`).
- **Дубль строки безопасен, значит строка обязана быть повторимой байт-в-байт** — сверка дублей идёт по sha256 сырой строки (`tail.go:44-49`); тот же seq с другим payload = `ErrPayloadConflict` (`tail.go:20-24, 181`). Эмиттер не должен пере-генерировать уже записанную строку иначе (например, с другим timestamp при реплее).
- **Пропуск seq фатален для чтения** (`tail.go:190`, `ErrStreamGap`) — нумерация плотная, без дыр.
- **Недописанная последняя строка — не ошибка**: тейлер «deliberately left alone» строку без `\n` — это строка в процессе записи (`tail.go:51-55`). Это смягчает, но не снимает формат-развилку §5 (битая запись С валидным `\n` остаётся слепым пятном JSONL — `research/25:74-75`).
- **Ключ — engine_run_id из hello**, не прогон платформы; чужие run id в том же файле тейлер пропускает (`events.go:63-65`, `tail.go:58-60`).
## 4. Деньги и потолок
**Роль событий денег — свежесть, НЕ защита.** Канон D39.106 п.2: «деньги (холд+потолок = защита, события = свежесть, D39.84/D39.100)». Доккоммент `Spend` прямо запрещает обратное: «Building enforcement on this event is forbidden — the stream is at-least-once and a crash truncates it» (`events.go:129-132`). Форма `Spend`: «CUMULATIVE, not a delta: a redelivered or duplicated line is then harmless … Integer micro-USD: money never travels as a float, and the engine's ledger is a lower bound, so the conversion at the seam rounds up» (`events.go:134-137`); это внутренний канал в приватную таблицу — D39.84 правит провод/экран/INFO ПОЛЬЗОВАТЕЛЯ, их это не касается (`events.go:137-138`).
**Событие потолка — факт без цифр.** `Ceiling{halted}`: «It carries the FACT and nothing else: money never reaches the platform's wire or its INFO logs (D39.84), and the stop is resumable, so it is not a failure» (`events.go:123-127`).
**PD-113 (открытый major, закрывается этим паком)** — текущий текст `DEFECT_REGISTER.md:18`:
> «**Стоп по потолку сегодня НЕразличим от инфраструктурного отказа, и контракт при этом запрещает называть его `failed`.** Движок возвращает потолок ошибкой (`errReserveCeiling`, сверено в HEAD), а `exitCode` мапит всё нераспознанное в 1 — значит по коду выхода „деньги кончились“ и „упало“ это одно и то же число; события потолка не существует (строка 103). Платформа честно ставит `failed`, хотя `BookStatus` требует `paused` и „никогда не `failed`“, потому что стоп резюмируем. Единственный путь, которым платформа СЕГОДНЯ узнаёт о потолке, — событие потока, которого нет; ветка под него построена и запинена (`TestACeilingHaltPausesTheRunWithItsReason`, `TestWhatTheUnitDidBecomesTheProductStatus`, случай „a ceiling halt survives any exit“). Закрывается приходом эмиттера (строка 103); ⚠ до тех пор экран покажет „ошибка“ там, где верно „остановлено: лимиты“».
**Строка 165 — различимые exit-коды, идёт ВМЕСТЕ с эмиттером** (`docs/PROGRESS.md:113`):
> «**Различимые exit-коды tmctl: потолочная остановка и graceful-stop** (сегодня оба = exit 1 ⇒ платформа отдаёт `failed` и на штатный стоп, и на исчерпание потолка — PD-113 платформы; после поправки формулы потолка (D39.123, PD-158) стоп по потолку = ровно исчерпание холда, цена неразличимости выросла; вскрыто приёмкой раннера на РЕАЛЬНОМ tmctl — фейк пробы умирал от сигнала)».
**ЭТОТ АБЗАЦ — СНИМОК ДО ПАКА, а не действующий контракт (испр. 22.08 аудитом доков; исходный текст оставлен, потому что документ — заказ эмиттер-сессии, а не справочник).** Пак D39.131 РАСШИРИЛ словарь ровно так, как этот раздел и просил: добавлены **exit 4** (потолочная остановка) и **exit 5** (graceful stop), плюс полоса отказов **1019**; действующий словарь читать в коде движка и в `platform/internal/ingest/exit.go`. Ниже — состояние на момент постановки задачи: 0 clean · 2 completed-with-flags (`*pipeline.CompletedWithFlags`) · 3 signature-stop банка (`*pipeline.WaveSignatureStop`) · 1 — infra failure и всё прочее (`backend/cmd/tmctl/main.go:30-52`).
**Семантика `--ceiling-usd`** (эмиттер-сессия трогает потолочную окрестность — знать обязана). D39.122 п.2(в) (`05-decisions-log.md:416`):
> «**`--ceiling-usd` = КНИЖНЫЙ потолок в силе, не бюджет прогона**: леджер сравнивает с накопленным committed+reserved книги; пересчёт „пользовательский прирост → абсолют“ — обязанность ПЛАТФОРМЫ, данные уже отдаются (`committed_usd`/`reserved_usd` в `status --json`); вторая денежная ось НЕ заводится».
Поправлено D39.123 п.2(б) (`05-decisions-log.md:428`) для стороны платформы:
> «**ПОПРАВКА к D39.122(в)/пингу: формула аргумента потолка = `committed + прирост×ставка`, БЕЗ reserved (PD-158)** — `store.Open` движка зануляет утёкший reserved до первой судимой резервации, read-only `status` этого не делает ⇒ reserved из status всегда leftover».
Следствие: платформа выставляет потолок впритык к холду, стоп по потолку = ровно исчерпание холда — потому событие потолка и различимый exit-код перестали быть «косметикой» (D39.123 п.2(ж)).
**PD-79 (закрыт; вход для денежной дисциплины шва)**`DEFECT_REGISTER.md:90`: «**Строковый `"null"` читается как НОЛЬ денег.** … **закрыто:** литерал `null` судится ДО раскавычивания и оставляет значение нетронутым; кавычки снимает `encoding/json`, а не `strings.Trim` — слово `null`, пустая строка и экранированная цифра выходят тем, чем являются». Урок для эмиттера: денежные поля шва — целые micro-USD (`events.go:139-141`), без строковых форм и float.
**PD-60 (открыт; пере-постановка «что делает движок, когда журнал не пишется»)**`DEFECT_REGISTER.md:24`: «⚠ **ПЕРЕ-ДИСПОЗИЦИЯ (эррата №15, 07.08): постановка строки устарела.** Она рассуждает про 64 КиБ пайпа, а ратифицированный транспорт (D39.106 п.2) — тейл `events.jsonl` с курсором: у файла обратного давления в этом смысле нет вовсе, зато появляются свои свойства (fsync-политика, ротация, отставание тейлера, поведение при заполненном диске). Строка живёт, но переформулируется вместе со строкой 103 — не „блокировать движок или ронять события“, а „что делает движок, когда журнал не пишется“». Политика выбирается ЯВНО, «молчаливой третьей нет» (там же); ср. `research/25:83`.
**PD-61 (открыт; два свойства ДО постройки)**`DEFECT_REGISTER.md:72`: «**(а) Сброс буфера на выходе:** `bufio.Writer` вокруг потока плюс `os.Exit`/`log.Fatal` пропускает `defer` и теряет последние события — ровно те, что сообщают об окончании прогона. **(б) Хвост при падении платформы:** … ответ — НЕ сокет …, а журнал файлом: движок дописывает NDJSON в `<jobdir>/events.ndjson`, платформа тейлит его с чекпойнтом смещения в Postgres». Пункт (б) уже ОТВЕЧЕН ратифицированной формой (журнал файлом, D39.106); пункт (а) — живое требование к эмиттеру. ⚠ Имя `<jobdir>/events.ndjson` в PD-61 — до-ратификационное; канон — `events.jsonl` каталога книги (D39.106 п.2, `tail.go:17`).
**PD-99 (закрыт; дисциплина логов вокруг спавна)**`DEFECT_REGISTER.md:110`: «INFO-строка старта несёт имя команды и НЕ несёт argv …, поэтому ни id книги, ни потолок в долларах в поток INFO не попадают». Для эмиттер-сессии это норма-зеркало: деньги и book id не текут в INFO и на провод (D39.84); в журнале событий деньги едут только внутри `spend` (приватная таблица).
**PD-107 (открыт; гейт, смежный с деньгами шва)**`DEFECT_REGISTER.md:118`: «**Удаление аккаунта обходит защиту PD-25** … прогон, идущий против этого холда, останется без того, кто его закроет. Кода удаления аккаунта в дереве нет вовсе (грепнуто) ⇒ строка = гейт перед появлением такой операции». Прямых обязанностей эмиттеру не назначает; в чек-листе — потому что входит в ратифицированный список пре-чтения (D39.123 п.4).
> ⚠ **§5 и §6 ниже — ОТРАБОТАННЫЙ ХЕНДОФФ** (пометка 22.08): эмиттер-сессия исполнена и принята (D39.131, `9cfe080`), все пять развилок решены, чек-лист входного чтения исполнен. Разделы оставлены как провенанс заказа — исполнять их не нужно.
## 5. Открытые развилки дизайна — отданы эмиттер-сессии
1. **Формат журнала** — главная развилка, «решить дизайном» (строка 103). `research/25:74-76` (GPT): «**формат — развилка строки 103 (GPT):** одиночный JSONL с ftruncate-ремонтом имеет гонку живого читателя и слеп к битой записи с валидным `\n`; альтернативы — пофайловые события (`events/<seq>.json`, temp+rename) либо CRC-фрейминг; побайтовая идентичность не требуется». ⚠ Учесть: тейлер и декодер платформы УЖЕ построены под одиночный `events.jsonl` (`tail.go:17`); выбор пофайловой/CRC-формы = согласованное изменение обеих сторон, пингом через оркестратора.
2. **Политика «журнал не пишется»** (PD-60): fsync-политика · ротация · отставание тейлера · заполненный диск; блокировать или деградировать — выбрать и записать явно, «обе позиции законны, молчаливой третьей нет» (`DEFECT_REGISTER.md:24`). Смежное: «диск ограничить (`ENOSPC` на ФС леджера ломает денежные инварианты…)» (`research/25:87-88`) — опс-слой, но поведение эмиттера при ENOSPC — его дизайн.
3. **fsync каталога** — NOTE без строки при приёмке D39.122: «NOTE без строк: … fsync каталога» (`05-decisions-log.md:418`); решить в дизайне записи (durability rename/append на ext4).
4. **Значение нового exit-кода** для потолка/graceful-stop (строка 165): 0/2/3 заняты (`main.go:30-52`) — выбрать и внести в shell-контракт.
5. **Словарь событий**: форма `events.go` — предложение платформы, «so the engine zone can answer it with a diff» (`events.go:5-6`); движок вправе ответить диффом (состав полей `progress`/`unit_done`, `eta_seconds`, `chunker_version` и т.д.), расхождения — пингом, не молча.
## 6. Чек-лист входного чтения эмиттер-сессии
Ратифицированный список — D39.123 п.4 (`05-decisions-log.md:432`): «промт эмиттера (103 + деньги шва; читать PD-95/105/79/99/107/60/61 + D39.106 + новые 165)»; продублирован в CURRENT-STATE (`docs/PROGRESS.md:5`).
| # | Что | Где | Зачем |
|---|---|---|---|
| 1 | Этот док | `docs/architecture/16-events-emitter.md` | сборка всего ниже |
| 2 | D39.106 (форма шва) | `docs/architecture/05-decisions-log.md:224-230` | канон транспорта |
| 3 | D39.119 п.3 · D39.122 п.2(в) · D39.123 п.2(б,в), п.3 | там же `:394`, `:416`, `:428`, `:430` | at-least-once · семантика `--ceiling-usd` · формула PD-158 · карантин проекции |
| 4 | research/25 | `docs/research/25-seam-cold-review.md` §Форма (:16-31) · §эмиттер (:72-83) · отвергнутое (:33-44) | механизмы + чтобы отвергнутое не вернулось |
| 5 | PD-95 | `platform/docs/DEFECT_REGISTER.md:180` | история двух устаревших транспортных текстов зоны; что НЕ строить |
| 6 | PD-105 | там же `:192` | контракт дублей/пропасти/sha256 |
| 7 | PD-79 | `:174` | денежная дисциплина шва (null/строки) |
| 8 | PD-99 | `:92` | argv/деньги не в INFO |
| 9 | PD-107 | `:56` | гейт удаления аккаунта против холда |
| 10 | PD-60 · PD-61 | `:24` · `:46` | «журнал не пишется» + сброс буфера на выходе |
| 11 | PD-113 | `:18` | major, который пак обязан закрыть |
| 12 | Строки 103 · 165 бэклога | `docs/PROGRESS.md:149`, `:113` | сам заказ + exit-коды |
| 13 | Код читателя | `platform/internal/ingest/events.go` (словарь) · `decoder.go` · `tail.go` | писать ПОД существующий контракт чтения |
| 14 | Своя сторона | `backend/internal/obs/logging.go:15-54` · `backend/internal/pipeline/stagerun.go:489-513` · `backend/cmd/tmctl/main.go:30-52` | готовые call-sites, текст потолка, exit-контракт |
## 7. Оговорки сборки (снять при постройке)
1. **research/23 §2 (событийная секция) со словарём `events.go` НЕ сверялся** — строка 103 на него ссылается; расхождение словарей снять при дизайне, диффом к `events.go`, не молча.
2. **«Строка события в той же SQLite-транзакции, что чекпойнт» — ратифицированная ФОРМУЛА (D39.106 п.2), не факт кода:** как она ложится на текущие транзакции `backend/internal/store` — первая проектная работа сессии.
3. Знаменатели голосов панелей research/25 различаются между вариантами (0/15 · 0/~18) — это разные плечи панелей, не усреднять.
4. Тела D39.84/D39.85/D39.100 в сборку вошли через цитаты D39.106 и доккомменты `events.go` — при сомнении открыть первоисточники (живой D-лог/слайсы).