# Эмиттер прогонных событий (шов движок→платформа) — сборка-норматив (строки 103+165, докс-пак 167) > ⚠ **ЭМИТТЕР ПОСТРОЕН И ПРИНЯТ (D39.131, `9cfe080`, 14.08):** `events.jsonl` (StreamVersion 1.1) · exit 4/5/полоса 10–19 · пер-вызовный репэйр-гейт; развилки §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). (б) *Деньги шва*: пока события нет, «экран покажет „ошибка“ там, где верно „остановлено: лимиты“» (PD-113, `DEFECT_REGISTER.md:18`), причём после PD-158 «PD-113 остаётся ЕДИНСТВЕННЫМ открытым major до эмиттера; … консервативный потолок останавливает ровно на исчерпании холда, и „деньги кончились“ неотличимо от „инфраструктура упала“» (D39.123 п.2(ж)). **Что уже есть со стороны движка.** 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) · write-ahead intent / `uncertain`-эскроу / `closing` — отдельная строка 136 бэклога · пиннинг версии бинаря на прогон — «исполняется и резюмом — PD-143/134 закрыты» (D39.123 п.3) · движковая оценка $/глава — строка 166 бэклога · тейлер/декодер платформы — уже построены (пакет `platform/internal/ingest/`), эмиттер пишет ПОД их контракт. ## 2. Ратифицированная форма (D39.106 · research/25) Канон — D39.106 п.2, дословно: > «**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; `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(б)) · `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, дословно: > «доставка событий шва = at-least-once; ДУБЛЬ `seq` — норма повторного чтения, декодер обязан принимать идемпотентно без фатала; тот же `seq` с ДРУГИМ payload = карантин прогона (research/25)». ⚠ **Расхождение, разрешённое позже**: «карантин прогона» (D39.119 п.3 и `research/25:77-78` «тот же seq с другим payload = карантин прогона, не тихий no-op») уточнён D39.123 п.2(в): «**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), плюс полоса отказов **10–19**; действующий словарь читать в коде движка и в `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(в): > «**`--ceiling-usd` = КНИЖНЫЙ потолок в силе, не бюджет прогона**: леджер сравнивает с накопленным committed+reserved книги; пересчёт „пользовательский прирост → абсолют“ — обязанность ПЛАТФОРМЫ, данные уже отдаются (`committed_usd`/`reserved_usd` в `status --json`); вторая денежная ось НЕ заводится». Поправлено D39.123 п.2(б) для стороны платформы: > «**ПОПРАВКА к 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 в `/events.ndjson`, платформа тейлит его с чекпойнтом смещения в Postgres». Пункт (б) уже ОТВЕЧЕН ратифицированной формой (журнал файлом, D39.106); пункт (а) — живое требование к эмиттеру. ⚠ Имя `/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/.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 каталога»; решить в дизайне записи (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: «промт эмиттера (103 + деньги шва; читать PD-95/105/79/99/107/60/61 + D39.106 + новые 165)»; продублирован в CURRENT-STATE. | # | Что | Где | Зачем | |---|---|---|---| | 1 | Этот док | `docs/architecture/16-events-emitter.md` | сборка всего ниже | | 2 | D39.106 (форма шва) | грепать `D39.106` (тело — слайс) | канон транспорта | | 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-лог/слайсы).