diff --git a/docs/architecture/05-decisions-log.md b/docs/architecture/05-decisions-log.md index 15caa859..06f00b1f 100644 --- a/docs/architecture/05-decisions-log.md +++ b/docs/architecture/05-decisions-log.md @@ -1507,3 +1507,11 @@ API-529-долг закрыт: 8-осевой refute-by-default воркфлоу **2. Док-свип закрытия (норма «актуализация = полный свип»):** 5 сканеров по кластерам (корень · PROGRESS · architecture · research · experiments), ~40 находок применено: передача №13→№14 и счёт очереди скриптом (**109** строк · бэкенд **60** строго / **72** широко · «скоро» **24**) · supersede-указатель доктрины D39.104 в шапку этого файла и баннеры 09-target/10-prompt/06-registry/12-style/research 15·20·24/experiments README·09-pilot · глоссарий +«доктрина инжекта» · норма «без саги» вписана в промт оркестратора · **хроника №9/№10 и закрытые бэкенд-записи срезаны в `archive/PROGRESS-2026-08-01-02.md`** (PROGRESS ~356 → ~230 строк). Отложено до приёмки/лендинга P0 (зона живая, №14): 2 правки `platform/README.md` · правка «кода нет» в `14-api-contract/README` · архивация P0-промта. **3. Очередь №14 — CURRENT-STATE (первым — приёмка P0 платформы). Грабли преемнику:** перед запуском любого полигон-скрипта читать код на сетевые вызовы (`--score`/`score_f1` офлайн; голые запуски ПЛАТНЫЕ; `load_dotenv` на импорте — стабить при офлайн-ре-ранах) · перед пере-ранами — sha256-манифест сырья и бэкап харнесса (gold.jsonl пере-собирается байт-в-байт, детерминизм подтверждён) · пинг полигона может врать арифметикой при чистом леджере — сверять из сырья · счёт очереди ТОЛЬКО скриптом · git — только pathspec-форма · чужое незакоммиченное (`platform/*`) не трогать. + +## D39.106 — ШОВ: прогон переживает деплой платформы; журнал в каталоге книги + systemd-юнит-на-прогон; платформа НЕ родитель движка (05.08). ✅ + +**1. Решение владельца 05.08.** Верификация до ратификации: четыре чистых панели, ~18 агентов, две модели (research/25, там же отвергнутые альтернативы с причинами) — 0 голосов за «родитель+пайп», 0 за монолит, 0 за gRPC/брокер; атака на готовое решение: «доминирующей альтернативы нет». + +**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 — без изменений. + +**3. Обязательные механизмы при постройке — research/25 §«Обязательные механизмы» (реконсилятор на каждый выход юнита + перезапуск после ребута · intent/`uncertain` + ночная сверка с провайдером · пиннинг версии бинаря на прогон · гейт потолка пер-вызов). Следствия: PD-59 superseded; пайп-путь `supervisor.go` P1 = дев-режим; ответ PD-13 переезжает на cgroup юнита прогона. Строка 103 и денежные строки единого бэклога — при освобождении PROGRESS.md; задачи зонам — промтами.** diff --git a/docs/research/23-engine-platform-seam.md b/docs/research/23-engine-platform-seam.md index 09407379..28858a35 100644 --- a/docs/research/23-engine-platform-seam.md +++ b/docs/research/23-engine-platform-seam.md @@ -1,11 +1,11 @@ # 23 — Шов движок ↔ платформа: как индустрия интегрирует batch-движок с control plane -> **→ 05.08: холодное ревью этого шва — `25-seam-cold-review.md`.** Пять изолированных архитекторов, -> не знающих о проекте, получили задачу абстрактно. Несущие решения ниже воспроизведены ими -> независимо (потолок у движка вместо синхронной авторизации · hold→capture · идемпотентность по -> `(run_id, seq)` · запрет читать приватную БД движка · standalone тем же путём кода). Расходится -> ОДНО: они единогласно не делают платформу родителем процесса движка. Ничего не отменяет — читать -> перед правками транспорта (строка 103) и перед П-3. +> **→ 05.08 (D39.106): ТРАНСПОРТНАЯ часть §1 SUPERSEDED** — по холодному ревью `25-seam-cold-review.md` +> (4 панели, ~18 агентов, две модели) и решению владельца «прогон переживает деплой платформы». +> Поток идёт НЕ в stdout ребёнку-платформе, а в `events.jsonl` каталога книги (проекция коммитов +> SQLite движка); движок — systemd-юнит на прогон; платформа тейлит по курсору `(run_id, seq)`. +> Всё остальное этого дока (формат NDJSON/hello/seq · три канала · запрет на живой SQLite · деньги) +> подтверждено теми же панелями и В СИЛЕ. > **→ 04.08 (D39.99/101): контракт API v0 РАТИФИЦИРОВАН по этому шву** — `../architecture/14-api-contract/`; строка 95 закрыта, хвосты движка = блокеры 99/100/101/103/125; код платформы существует (P0, ждёт приёмки). Упоминания «строки 95» ниже — история. > **Ревью-шапка (оркестратор №9, 02.08.2026).** Статус: ПРИНЯТ, направление ратифицировано **D39.85**. Метод: 5-агентный воркфлоу — три веб-направления по первоисточникам (официальные доки/блоги авторов инструментов, вторичных статей в выводах нет) + репо-инвентаризация + независимый арбитраж репо-находок (все 9 вердиктов подтверждены, 3 мелкие поправки внесены сюда). Правила `internal` проверены ЖИВЫМИ сборками на go1.26.4, не пересказом доков. Стоимость $0. Вопрос ресёрча: как чисто организовать read-путь и статус-поток от движка (CLI, процесс-на-прогон, приватный SQLite под EXCLUSIVE flock) к платформе (отдельный Go-модуль, net/http + Postgres + River + SSE) — не нарушая инвариантов «сервер в backend не пишется» (D39.81) и «пользователи/квоты/HTTP не проникают в движок». diff --git a/docs/research/25-seam-cold-review.md b/docs/research/25-seam-cold-review.md index df2125fd..96523387 100644 --- a/docs/research/25-seam-cold-review.md +++ b/docs/research/25-seam-cold-review.md @@ -1,115 +1,87 @@ -# 25 · Холодное ревью шва движок↔платформа и топологии сервисов +# 25 · Шов движок↔платформа: ратифицированная форма и почему не иначе -> **Метод.** Задача шва (control plane + многочасовой воркер, деньги, standalone-режим) отдана -> панелям изолированных агентов в переодетом домене (сборка генома), без доступа к репозиторию. -> Три чистых плеча: базовое (`wf_6adb39d7-870`, 5 агентов) · с добавленным фактом «воркер дёшево -> резюмится» (`wf_2b45042d-115`, 5) · топологическое «сколько вообще нужно разворачиваемых единиц», -> без постулата разделения, с вебом (`wf_4ac43daf-ec5`, 6, три модели, включая seat-адвоката -> монолита). Изоляция проверена поагентно по журналам: ноль файловых вызовов, ноль следов -> репозитория. Первый прогон (`wf_b2b954de-e83`) загрязнён чтением репозитория и ОТБРОШЕН; -> правило для будущих панелей: запрещать файловые инструменты явно и проверять постфактум. +> **Что это.** Независимая верификация шва и топологии: задача в переодетом домене отдана четырём +> чистым панелям (~18 агентов, Opus и Fable, без доступа к репозиторию; изоляция проверена +> поагентно по журналам). Финальная тройка: атака на готовое решение + два абстрактных дизайна, +> которым явно разрешили REST/gRPC/брокер. Воркфлоу: `wf_6adb39d7-870` · `wf_2b45042d-115` · +> `wf_4ac43daf-ec5` · `wf_b7eae996-b9c` (первый прогон `wf_b2b954de-e83` загрязнён чтением +> репозитория и отброшен; правило: файловые инструменты панелям запрещать явно, проверять +> постфактум). > -> **Статус:** фактура. D39.85 в силе; служит входом для строки 103 единого бэклога, П-1/П-3 и -> решения по транспорту. Оркестратор №14, 05.08.2026. +> **Статус:** решение владельца 05.08 «прогон переживает деплой платформы» ратифицировано +> **D39.106**. Оркестратор №14. -## Вердикты панелей +## Форма -| Вопрос | Результат | -|---|---| -| Сколько разворачиваемых единиц | **2 — единогласно (6/6)**, включая seat-адвоката монолита; за монолит 0 | -| Платформа — родитель процесса движка + поток на stdout/fd | **0 выбрали, 10 отвергли** (оба плеча) | -| Дешёвый резюм воркера меняет расклад? | **Нет.** 5/5: «факт снял ограничения, но выбор не сделал»; при дорогом прерывании ушли бы в ту же сторону сильнее | -| Канал воркер→платформа | Append-only журнал в каталоге прогона, тейл по курсору — 8/10; unix-сокет-push упал в обоих плечах по недолговечности через рестарты платформы | -| Супервизор воркера | systemd, юнит-на-задачу, `Restart=no`, стоп SIGTERM — 10/10 | -| gRPC · брокер · общая БД · чтение приватной БД воркера · stdout/journald как источник событий · синхронная авторизация на каждый платный вызов | отвергнуты почти единогласно (чтение приватной БД: 1 голос «за» из 11 — единственное расхождение с D39.85 §4 за все плечи) | +- **Два Go-модуля** (движок · платформа), граница — модульная, D39.85-гард в силе. +- Движок — **транзиентный/шаблонный systemd-юнит на прогон** (`Restart=no`, стоп SIGTERM, щедрый + `TimeoutStopSec`), платформа его **не родитель**: её деплой прогоны не трогает. +- События — **`events.jsonl` в каталоге книги**, append-only, как **проекция** уже закоммиченных + строк SQLite движка (та же транзакция, что чекпойнт chunk×stage; outbox, не вторая запись). +- Платформа **тейлит** журнал; запись курсора — `(engine_run_id, seq)`, байтовое смещение — хинт; + курсор коммитится в той же Postgres-транзакции, что эффект события. +- Деньги: **холд + потолок движку = защита** (не зависит от живости платформы), события — свежесть; + hold→capture→release. `tmctl status --json` — ремонт, только по остановленному прогону (лок). +- **Реконсилятор** — обязательный компонент: на каждый выход юнита (D-Bus-сигнал + + `ExecStopPost=`-маркер) и на буте, где он ПЕРЕЗАПУСКАЕТ прерванные прогоны (транзиентные юниты + не переживают ребут VM). +- Формат потока (NDJSON · hello · версия · seq), D39.84, запрет читать живой SQLite движка — без + изменений; журнал им не является. -## Подтверждено вхолодную (совпало с нашими решениями) +## Отвергнутые альтернативы (кем и почему — кратко) -Названо 5/5 агентами, не знавшими о проекте: предавторизованный потолок, который воркер держит сам -(= холд + потолок движку, D39.84) · hold→capture→возврат остатка (`Hold`/`Settle`/`Release`) · -идемпотентное применение по `(run_id, seq)` · запрет читать приватную БД воркера (D39.85 §4) · -standalone тем же путём кода · `flock` вместо pid-файлов · запись намерения ДО платного вызова (Р7) -· целочисленные деньги (⚠ у нас — на платформе; в движок float64 входит раньше, см. «Деньги»). +| Вариант | Счёт | Причина отказа | +|---|---|---| +| Монолит (движок внутри платформы) | 0 из ~18 | Радиус поражения (леджер+вход в одном адресном пространстве с часто меняющимся движком); «одна транзакция решает деньги» ложна — БД прогона + леджер + провайдер = три хранилища при любом числе процессов. Пороги пересмотра: без cgo ✅ · деплой реже месяца ИЛИ p99 ≤ 2 ч ❌ · кросс-версионный резюм в CI ❌ | +| Платформа — родитель + пайп (stdout/fd) | 0 из 15 | Время жизни движка — подмножество платформы: деплой/рестарт убивает или осиротляет прогон; дешёвый резюм это НЕ лечит (проверено отдельным плечом: факт «прерывание стоит минуты» не сдвинул ни один голос) | +| gRPC/HTTP-сервис поверх движка | 0, отвергли явно | Изобретает жизненный цикл, которого у движка нет (одна задача, эксклюзивный лок); поток всё равно умирает на рестарте платформы — долговечный буфер нужен, а он и есть журнал | +| Push в платформу (unix-сокет/localhost HTTP) | 2 голоса в одном плече, затем 0 | Платформа лежит несколько раз в неделю ПО ЗАМЫСЛУ ⇒ push требует буфер+ретраи+дедуп = журнал; плюс неаутентифицированный локальный писатель в денежные таблицы | +| Брокер (NATS/Redis/Kafka) | 0 | Третий stateful-демон на одной VM для 1 продюсера и 1 консюмера; redelivery сам создаёт риск двойного списания | +| Чтение чужой БД (в обе стороны) | 1 голос из ~18 | Сварка с приватной схемой, меняющейся еженедельно; лок-конфликт с живым писателем | +| Выделенный fd 3 вместо stdout | 0 | Никому не понадобился при свободном выборе; отдельно замерено: fd 1/2 дают SIGPIPE-смерть сироты, fd 3 — нет | +| journald/stdout как источник событий | 0 | journald молча роняет под всплеском и ротирует сам; людям — да, контракту — нет | -**Два модуля подтверждены, но несущие доводы другие, чем в наших доках.** Всё «процессное» -(пиннинг версии, cgroups, независимый рестарт, пер-задачные журналы) даёт и один бинарь, -exec-ающий сам себя транзиентным юнитом. До «двух ПОСТАВОК» доживают ровно два довода: -**граница как ошибка компиляции** (модульный гард D39.85 — ровно это) и **независимые релизные -поезда** (фикс движка не ждёт CI платформы). Лучшая карта монолита — «одна транзакция решает -деньги» — фальшива: приватная БД задачи + глобальный леджер + провайдер = **три хранилища при -любом числе процессов**, outbox и сверка нужны в любом исходе. Пороги, при которых монолит стал бы -прав (все разом): движок без cgo ✅ · деплой реже месяца ИЛИ p99 задачи ≤ 2 ч ❌ · кросс-версионный -резюм в CI ❌ · команда ≤ 3 с одним владельцем обеих половин — сегодня не проходим, это и есть -триггер пересмотра. +## Подтверждено вхолодную (наши решения воспроизведены независимо) -## Настоящие драйверы против «платформа — родитель» (не зависят от стоимости прерывания) +Потолок у движка вместо синхронной авторизации · hold→capture→release · идемпотентность +`(run_id, seq)` · запрет читать приватную БД движка · standalone тем же путём кода (не режимом) · +`flock` вместо pid-файлов · запись намерения до платного вызова (Р7) · целочисленные деньги · +SSE, не WebSocket · `status` как ремонт, не поллинг. -1. **Несовпадение времён жизни:** деплой платформы несколько раз в неделю против прогона 4–30 ч. -2. **Плановая недоступность платформы делает долговечность требованием к каналу:** любой push-канал - требует буфера/ретраев/дедупа — а этот буфер и есть журнал. -3. **Атомарность денег:** «юнит сделан» и «юнит стоил $X» — одна транзакция в хранилище воркера. -4. **Радиус поражения:** леджер и аутентификация не делят адресное пространство и OOM-цель с - часто меняющимся воркером. -5. **Standalone не должен быть режимом:** непрогоняемая ветка `--no-server` гниёт. +Атака на готовое решение: вердикт **build-with-changes**, доминирующая альтернатива — **none**. -**Наше смягчение, которого у панелей не было:** при остановке платформы движок останавливается -тоже (сигнал группе; на stdout — SIGPIPE), окна «пишет без слушателя» нет, события не теряются. -Довод №2 бьёт по нам не потерей событий, а тем, что **каждый деплой платформы прерывает все идущие -переводы** ценой одного чанка каждый. +## Обязательные механизмы при постройке (носители в D39.106) -## Развилка владельцу +**Деньги:** +- write-ahead intent + состояние **`uncertain`** для окна «платный вызов прошёл, коммит не успел»: + смещение к списанию, запись по оценке, флаг человеку; молчаливой бесплатной работы не бывает; +- **ночная сверка с данными провайдера** — приоритетнее споров об округлении: в движок суммы + входят float64 по ручной таблице цен с ненулевым якорем незнакомого слага + (`backend/internal/ledger/pricing.go:27-52`), систематика процентная; +- **гейт потолка на каждый платный вызов**, не на юнит работы; +- state-machine строки прогона в Postgres (холд может утечь до создания каталога); поднятие + потолка — файлом через atomic rename на границах юнитов; кандидат: холд траншами (~часы + ожидаемого расхода), не на весь прогон. -**Должен ли идущий перевод переживать деплой платформы?** -- **Да** → журнал в каталоге прогона + движок отдельным юнитом; платформа перестаёт быть родителем. -- **Нет** → нынешняя форма (родитель + stdout) законна, но фиксируется как сознательная ставка с - явной ценой; формулировка «переезд не окупается» опровергнута. +**Эмиттер журнала (строка 103):** +- `events` (fsync, seq, деньги) ≠ `status` (atomic rename, косметика, не для биллинга); +- на старте движок ftruncate'ит хвост до последнего целого `\n` и дописывает с last-seq+1 — + побайтовая идентичность переэмиссии не требуется; +- **пиннинг прогона к версионированному пути бинаря**; резюм другой версией — только по явному + флагу (движок выкатывается чаще, чем идёт прогон); +- сброс буфера на всех путях выхода (`os.Exit` пропускает defer — теряются финальные события); +- живость = растущий `seq`, не удерживаемый лок; политика обратного давления выбирается явно. -Формат событий (NDJSON, hello-хендшейк, `(run_id, seq)`) не меняется ни в одном исходе — меняется -только кто спавнит и откуда читает. Решение владельца 05.08: одна машина пока что; при второй -машине умирают файловая шина/сокет/`systemctl start`, переживают outbox, денежная модель, -идемпотентность, SSE. Цена переезда мала, если платформа тейлит журнал, и велика, если она родитель. +**Опс:** privilege-модель создания юнитов (linger-пользователь либо узкое polkit-правило) решить до +постройки · состояние systemd — не истина (источник на буте = каталоги + Postgres) · диск ограничить +(`ENOSPC` на ФС леджера ломает денежные инварианты; квота на прогон, проверка места при допуске) · +ключи идемпотентности провайдеров протухают — replay это оптимизация, не гарантия · отдача SPA из +бинаря платформы (один origin) — вопрос к П-1, не решено. -## Деньги: обязательные механизмы, которых у нас нет +## Открытое -1. **Реконсилятор мёртвых воркеров** (компонент, не удобство): на старте платформы добрать хвост, - пометить прогон прерванным, вернуть остаток холда. Тестировать SIGKILL'ом реального воркера. -2. **Состояние `uncertain`** для окна «упали между платным вызовом и записью расчёта»: смещение в - сторону списания, запись по оценке, флаг человеку; молчаливой бесплатной работы не бывает. -3. **Сверка с данными/инвойсом провайдера** (ночной прогон). Приоритетнее споров об округлении: - в движке суммы считаются в float64 по ручной таблице цен с ненулевым дефолтным якорем для - незнакомого слага (`backend/internal/ledger/pricing.go:27-52`) — систематика процентная, а не - микродолларовая. -4. **Гейт на каждый платный вызов, а не на юнит работы** — «перерасход ≤ один юнит» без этого - утверждение, а не механизм. (Наши вызовы синхронные — оценка перерасхода держится, но гейт - всё равно пер-вызовный.) - -## Проектные требования к эмиттеру потока (задать до постройки, строка 103) - -- **Outbox правильно:** строка расхода — в БД задачи, в той же транзакции, что чекпойнт; - `events.jsonl` ПРОИЗВОДИТСЯ из неё релеем (иначе двойная запись — ловушка, в которую попали - 3 из 6 агентов, призывая сам паттерн). -- **`events` ≠ `status`:** долговечный денежный журнал (fsync, seq) отдельно от косметического - снапшота живости (atomic rename, ~2 с, из него никогда не биллить). -- **Перекос версий:** прогон может резюмить более новый движок, чем начал. Штамповать версию в - каталоге, резюм чужой версией — только по явному флагу. (В канон детерминизма, рядом с - `brief_hash`.) -- **Живость = растущий `seq`**, а не удерживаемый лок. -- **Сброс буфера на всех путях выхода:** `os.Exit`/`log.Fatal` пропускают defer и теряют финальные - события. -- **Обратное давление:** пайп/канал ограничен; политика на переполнение (блокировать воркер или - ронять с маркером `events_dropped`) выбирается явно, молчаливой третьей нет. - -## Прочее найденное - -- **Диск не ограничен нигде:** `ENOSPC` на ФС леджера ломает денежные инварианты разом. Леджер на - своей ФС, квота на задачу, проверка места при допуске. (В зону платформы при деплой-слое.) -- **Состояние systemd — не истина:** транзиентный юнит выгружается, VM перезагружается; источник - истины на старте = каталоги задач + БД платформы. -- **Ключи идемпотентности провайдеров протухают** (порядок суток) — replay как оптимизация ретрая, - не гарантия. -- **SPA из бинаря платформы** (один origin, нет перекоса версий) — 6/6 у панели; у нас не решено, - вопрос к П-1/фронту. -- SIGPIPE зависит от номера дескриптора (fd 1/2 — смерть процесса, прочие — EPIPE; замерено): - нынешний stdout даёт бесплатную остановку осиротевшего движка, переезд на fd 3 её ломает. -- Защита stdout от посторонних записей, если канал остаётся: dup-перехват на уровне дескриптора в - `main` движка, только в области потоковой команды (безусловный сломал бы `tmctl status --json`). +- Строка 103 единого бэклога и денежные строки — завести при освобождении `docs/PROGRESS.md` от + чужих незакоммиченных правок; задачи зонам — отдельными промтами. +- Вторая машина (решение владельца: одна пока что): при переезде умирают файловая шина и + `systemctl start` как глагол — заменяются хост-агентом, шлющим те же строки; outbox, деньги, + идемпотентность, SSE переживают без правки. Пиннинг книги к хосту уже в П-3.