textmachine/docs/architecture/17-seam-inbound-law.md

129 lines
16 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.

# 17 — Закон входной двери шва движок ↔ платформа
> **СТАТУС: РАТИФИЦИРОВАНО D39.156 (слово владельца 23.08).** Это НОРМА, ссылаться на неё можно. Собрано оркестратором №19 по консилиуму
> со сторонним архитектором 2223.08.2026. Ратифицирована одной D-нотой; с этого момента двери встают паками без
> возврата к владельцу — ровно так, как код выхода `13` встал в ратифицированную полосу 1019 без
> нового решения ВЛАДЕЛЬЦА (D39.131 п.2а → D39.132 п.2г → D39.134: код 13 прошёл через направление и
> финализацию приёмкой, то есть через ратификации ОРКЕСТРАТОРА — именно это свойство норма и обещает).
>
> **Зачем понадобилось.** Контракт фронт↔платформа ратифицирован (`14-api-contract/`), а контракт
> платформа↔движок — нет: исследование шва дошло до «события пишутся в `events.jsonl`» и дальше
> отвечало на вопрос «как запускать движок» (`research/23`, транспортная часть §1 superseded
> D39.106; `research/25` — ратифицированная форма и отвергнутые альтернативы). В итоге НАРУЖУ из
> движка идёт богатый поток — журнал событий по курсору, полоса кодов выхода, exit-маркер,
> версионированные артефакты-проекции, — а ВНУТРЬ не идёт ничего, кроме запуска с флагами. Данные,
> которыми владеет пользователь продукта, движок принимать не умеет вообще, поэтому каждый внешний
> участник вынужден имперсонировать человека-оператора с текстовым редактором.
## Почему НОРМА, а не каталог команд
За осознанный контракт говорят три прожитых инцидента гниения нератифицированных входных конвенций:
дубль дефолта `<book_id>.db` у платформы (строка бэклога **213**) · ручная константа версии API,
не поднятая при синке (D39.153) · три мины формата карты подписи, вскрытые только трассировкой
строки 199.
Против ПОЛНОГО каталога — то, что половина будущих дверей гейтится нерешёнными развилками: строка
**192** прямо запрещена к проектированию до полигонных итогов (D39.144 п.1), **161**/**185** ждут
дизайн-пака, **170** — «после беты формы Б». Каталогизировать их сейчас значит выдумывать.
Прецедент, разрешающий спор, — исходящее направление: там выиграла ратификация ПОЛОСЫ и ДИСЦИПЛИНЫ,
а не перечня.
## Закон: семь пунктов
1. **Вход — это аргументы при спавне и файлы, названные аргументами.** Данные, которыми владеет
пользователь продукта, доезжают движковым ГЛАГОЛОМ; движок — единственный писатель файлов своей
схемы и владелец их путей и конвенций. Платформа читает только версионированные артефакты по
путям, которые ей СООБЩЕНЫ ДВИЖКОМ **ИЛИ РАТИФИЦИРОВАНЫ КОНТРАКТОМ ШВА**, и никогда не выводит путь сама.
**Два выноса, без которых буква закона объявляет вне закона уже ратифицированное:**
**(а) форма Б** — одноразовый рендер стартового `book.yaml` при рождении книги (D39.130 п.2б): платформа пишет пять
ключей идентичности, всё остальное проходит из шаблона непрочитанным
(`platform/internal/books/render.go:29-33`=`comes from the template unread and unjudged`); живёт до формы В (строка 170).
**(б) фикс-имена ратифицированной конвенции шва** — `events.jsonl` никто платформе не «сообщает», это константа
(`platform/internal/ingest/tail.go:17`=`const JournalFile = "events.jsonl"`, D39.106); без этого выноса буква ломает главный канал самого шва.
*Прецеденты:* потолок аргументом (D39.110 п.2б`--ceiling-usd`, D39.122) · `--verify-bank`
флагом (`platform/internal/runner/engine.go:83-87`=`func TranslateArgs(workdir string, verifyBank bool`) · дефолт `project_db` по конвенции каталога
(`backend/internal/config/book.go:163-167`=`b.ProjectDB = filepath.Join(dir, b.BookID+".db")`) · конвенционное опциональное чтение `.auto-bank.yaml`
(`backend/internal/pipeline/mining.go:644`=`.auto-bank.yaml`).
2. **Посреди прогона внутрь идёт ТОЛЬКО сигнал.** `SIGTERM` = «дочти чанк и отпусти лок»; всё
остальное — на границах процессов. Арбитр — эксклюзивный flock проекта на всю жизнь прогона
(`backend/internal/store/store.go:55`=`EXCLUSIVE flock on`), мутирующий глагол берёт его неблокирующе и при занятости
отказывает **ровно классом 12** (`platform/internal/ingest/exit.go:58`=`ExitProjectLocked = 12`), а НЕ 10:
полоса различает их не косметически — 10 (`platform/internal/ingest/exit.go:51`=`ExitConfigInvalid = 10`) значит «деплой сломан,
чинит человек, книги ждут», 12 значит «ничего не произошло, подожди и повтори» (D39.134 п.2а). Запись через слэш
позволила бы исполнителю выбрать 10, и платформа отнесла бы занятый лок к операторским ошибкам.
*Почему жёстко:* движок пере-читает сид ПОСРЕДИ прогона (`mining.go:221`), запись в живой прогон
въехала бы в снапшот недетерминированно. Потолок посреди прогона менять НЕЛЬЗЯ — он едет со
стартом и после не меняется, лекарство — новый прогон
(`platform/internal/runs/reconcile.go:1222-1227`=`answering 202 with the run in the state`). Дверь «менять потолок файлом» закрыта;
не выдумывать её заново из `research/25`.
3. **Каждый JSON-выход глагола несёт версию документа.** Сегодня асимметрия: `tm-bank-v1`
(`pipeline/bankexport.go:33`=`const bankExportVersion = "tm-bank-v1"`) и `tm-manifest-v2` (`pipeline/manifest.go:46`=`const manifestVersion = "tm-manifest-v2"`) версию несут,
а `status --json` — нет. Закрыть.
4. **Незнакомый флаг = громкий отказ при спавне.** Сегодня это так по устройству разбора аргументов
(`backend/cmd/tmctl/invocation.go:104,120`=`flag.NewFlagSet(cmd, flag.ContinueOnError)`); зафиксировать законом, а не полагаться на случайность.
5. **Глагол зовётся ТЕМ ЖЕ пиннутым бинарём, что и возобновляемая попытка** (`runs/spawn.go:240-248`=`engineBinary is the path an attempt is PINNED to`,
строка бэклога 139). Иначе файл, записанный новым бинарём, читается старым — а при нестрогом
загрузчике сида (строка **212**) новое поле теряется МОЛЧА.
6. **Словарь движка ↔ словарь контракта — одна таблица, два направления, один файл.**
Сегодня перевод односторонний, на чтении (`platform/internal/ingest/bank.go:15-21`=`Every vocabulary crossing here is TRANSLATED and none is passed through`). Запись
делает таблицу двунаправленной, и две копии разъедутся.
7. **Мутирующий глагол сначала даёт проекцию, потом мутирует.** «Почём ДО подтверждения».
*Прецеденты:* rebill-смета с порогом согласия (D20.2-Q2, `pipeline/rebill.go`) · строка 185
(«сколько глав и почём до подтверждения») · `--dry-run` строки 124(в).
## Совместимость без второго рукосинхронизируемого контракта
Версий у КОМАНД нет и заводить их не нужно: совместимость выводится из трёх уже существующих ног —
пиннинг попытки к бинарю (строка 139) · незнакомый флаг роняет спавн громко (п.4) · версионированный
конверт на каждом JSON-выходе (п.3). Плюс одна асимметричная норма: движок не меняет семантику флага
или поля без мажора, новые флаги едут «движок-первым» — расширение уже ратифицированного порядка
деплоя `migrate` (D39.134 п.3).
Хендшейк или `capabilities`-глагол движка — НЕ строить: один хост, «least mechanism»
(`12-go-style-notes.md` §1), а судьбу ручной константы показал инцидент `ContractVersion`.
⚠ Предположение, не механизм: окно «старая платформа + новый движок» считается покрытым обратной
совместимостью флагов. Если деплои разъедутся сильнее — вернуться к вопросу.
## Двери, УЖЕ подразумеваемые живыми строками
| дверь | откуда следует |
|---|---|
| приём правок банка движковым глаголом | строка **199(а)** — развилка владения `book.yaml` растворяется этим |
| `--keys-file` (или иной явный путь к ключам) | строка **211** — блокер строки 202 |
| путь артефакта в ответе движка | строка **213** — снимает дубль у платформы |
| `resnapshot --dry-run` | строка **124(в)**, глагол поименован там дословно |
| движковая оценка $/глава | строка **166**; форма (поле в `manifest --json` против нового глагола) — за проектировщиком пака |
| consent-флаги `--resnapshot`/`--accept-rebill` от платформы | строка **199(б)**, гейчена 192 |
| `tmctl init` (форма В) | строка **170**; приоритет ПАДАЕТ после двери 199(а) — та забирает её главный довод |
**Не установлено, проверить при постановке:** строка **198** (апгрейд стирает замечания) — возможно, подразумевает
глагол пере-анонса или бэкфилла, потому что движок анонсирует юнит один раз за жизнь книги
(`backend/internal/pipeline/events.go:399-400`=`func (e *emitter) unitOnceKey(k unitWave) string`); направление фикса консилиум НЕ установил. Записано третьей
категорией, чтобы читатель ратификации не счёл свип дырявым.
**Из чего двери НЕ следует** (экономия паков): **202** — потребитель всего набора, своих команд не
несёт · **208** — фикс транспорта внутри движка · **210** — развилка данных/промпта, вайр-батч 182 ·
**164** — не команда, а починка: смета уже едет в `status --json`.
## Чего НЕ строить
- **Инбокс-каталог входящих команд** как зеркало `events.jsonl`. Все трассированные нужды садятся на
границы процессов; единственный мид-ран кандидат закрыт каноном (п.2); инбокс-файл возвращает
гонку, ради исключения которой существует flock.
**Эррата, которую ратифицирующая нота обязана поставить на `research/25`:** там «поднятие потолка файлом через
atomic rename» отнесено к носителям D39.106 — в теле D39.106 этого механизма НЕТ вовсе (проверено по ЖИВОМУ телу `architecture/05-decisions-log.md:232`=`ШОВ: прогон переживает деплой платформы`; ⚠ испр. 23.08 — прежняя редакция ссылалась на слайс
`archive/architecture/05-decisions-D39-106-123.md`, где тела D39.106 НЕТ, и греп по нему подтвердил бы эррату вакуумно; первый хит темы — только в D39.110). Строка была КАНДИДАТОМ и
никогда не ратифицировалась, а живой канон решил ось иначе (п.2).
- **Пере-открытие транспорта** (gRPC/сокет/брокер/пуш) — `research/25`, у каждой альтернативы 0
голосов из ~18, решение владельца принято.
- **Общий Go-пакет типов на две системы** — сварка независимых деплоев против D39.81/85; перевод
словаря на шве — сознательный контракт, а не недоделка.
- **Быстрый poll статуса** — `status` дорог по построению (пере-ингест книги), свежесть даёт журнал
событий; «status — это ремонт, а не поллинг» подтверждено вхолодную независимыми панелями.