Ratify D39.106 seam decision, rewrite research 25 from arm chronology into current form with brief rejection reasons, supersede the transport banner in research 23

This commit is contained in:
heaven 2026-08-05 21:37:11 +03:00
parent 39032e5148
commit 481b2ba72b
3 changed files with 86 additions and 106 deletions

View file

@ -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; задачи зонам — промтами.**

View file

@ -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 не проникают в движок».

View file

@ -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. **Несовпадение времён жизни:** деплой платформы несколько раз в неделю против прогона 430 ч.
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.