ADR-012 — Слоистость исполнения (модель реализует публичные контракты, а не внутренние типы)¶
| Поле | Значение |
|---|---|
| Статус | Принято |
| Версия | v.1 |
| Дата | 2026-06-14 |
| Владелец | Руслан Габитов |
| Уточняет | ADR-002 v.1 Extension Architecture |
Принято — реализовано через SRD-012 v.1. Чинит находку аудита 2.1:
pkg/model(публичный modeling-API) импортируетinternal/*и выставляет внутренние, неконструируемые пользователем типы в экспортируемых сигнатурах (Exec(ctx, renv.RuntimeEnvironment)). Решение сохраняет существующую форму исполнения — типы элементов модели по-прежнему несут свои методыExec/ data-binding, набор узлов остаётся закрытым (без пользовательских узлов) — но переносит контракты, которые эти методы реализуют и потребляют, в публичные пакеты, так что модель зависит только от публичных типов.internal/*хранит реализации. CI-правило depguard делает границу постоянной. Это фикс гигиены/инкапсуляции, а не фича расширяемости. Охват — концепция; SRD-012 v.1 приземлил файловый перенос (пять публичных пакетов —pkg/exec/pkg/renv/pkg/eventproc/pkg/interactor—pkg/modelсвободен от internal, правило depguard работает).
1. Контекст¶
1.1 Что требует чистый публичный API¶
pkg/model — публичная, стабильная поверхность моделирования gobpm, типы,
из которых пользователь строит процесс. Публичный пакет должен:
- не импортировать
internal/*движка (собственное правилоinternalв Go существует ровно для этого; публичный пакет, опирающийся на internals, связывает стабильную поверхность с волатильной машинерией), и - не выставлять внутренние типы в своих экспортируемых сигнатурах — метод, который вызывающий не может назвать или удовлетворить, на деле не часть API; это внутренняя сантехника в публичном пальто.
Рантайм зависит от модели (он её исполняет); модель должна зависеть только от опубликованных контрактов, никогда от внутренних реализаций.
1.2 Что у движка сейчас¶
Типы элементов pkg/model (tasks, events, gateways) реализуют execution-
интерфейсы рантайма напрямую против внутренних типов:
- интерфейс node-executor и synchronizing-join-маркер (
internal/exec); - per-execution-окружение, передаваемое в
Exec(internal/renv); - интерфейсы consumer/producer для data-binding и execution-frame, который
они получают (
internal/scope—LoadData/UploadDataпринимают*scope.Frameи зовут егоInstantiateInputs/InstantiateOutputs/LoadProperties/GetDataByID); - interaction-registrator (
internal/interactor, user task) и event-producer (internal/eventproc, события).
Так pkg/model импортирует пять пакетов internal/*, и экспортируемые методы
вроде Exec(ctx, renv.RuntimeEnvironment) несут внутренний, неконструируемый
пользователем параметр (аудит 2.1, оценён там как CRITICAL).
Это долг инкапсуляции, а не баг: оно компилируется, нет import-цикла, и эти
методы зовёт только движок. ADR-002 предвидел public/internal-split интерфейсов
(его §3.3) и определил per-execution RuntimeEnvironment (его §4.3); этот ADR
завершает этот split для контрактов, которые трогает модель.
1.3 Почему сейчас¶
Это последний открытый CRITICAL из аудита 2026-06-11, и он фундаментален для
очереди работ: припаркованные исполнители SendTask/ReceiveTask (ADR-014) иначе
добавили бы новые рёбра model→internal, а observability-ADR (ADR-013) хочет
чистую публичную поверхность. Сделав split контрактов первым, новые исполнители
реализуют публичные контракты с самого начала.
1.4 Чем это не является¶
gobpm предоставляет закрытый, определённый стандартом набор типов BPMN-узлов (SAD-001 §14, Process Execution Conformance). Цели дать пользователям добавлять свои виды узлов нет. Поэтому этот ADR намеренно не вводит реестр исполнителей, visitor или какую-либо машинерию расширяемости — диспетчеризация остаётся ровно такой, как есть (трек гоняет узел через интерфейс исполнителя, который тот реализует). Меняется только то, что интерфейс — и всё, что он вручает узлу — становятся публичными.
2. Решение¶
2.1 Модель реализует публичные контракты и не импортирует internal/*¶
Типы элементов pkg/model сохраняют свои методы исполнения (Exec, data-binding
LoadData/UploadData) — форма исполнения и закрытый набор узлов не меняются —
но каждый интерфейс, который они реализуют, и каждый тип, который они
потребляют, становится публичным. pkg/model импортирует ноль internal/*.
2.2 Execution-контракты переезжают в публичные пакеты¶
Контракты, которые трогает модель, переносятся в публичный(е) пакет(ы);
internal/* хранит только реализации, которые им удовлетворяют:
- Интерфейс node-executor (и synchronizing-join-маркер) — публичный; модель реализует, трек потребляет (тем же type-assert, что и сейчас — без реестра).
- Per-execution-окружение, которое получает исполнитель — публичный интерфейс
(runtime-facing-peer read-only
service.DataReaderиз SRD-011). Это «размещение публичных reader/node-executor-контрактов», которое ADR-011 v.5 оставил layering-ADR.internalхранит реализацию (per-execution-окружение экземпляра); публичен интерфейс, свободный от внутренних типов. - Поверхность data-binding — интерфейсы consumer/producer и операции
execution-frame, которые зовёт модель (
InstantiateInputs/…Outputs,LoadProperties,GetDataByID, commit) — публичные, при этомinternal/scopeхранит конкретный frame как реализацию. - Interaction-registrator (user task) и event-producer (события) — публичные интерфейсы, внутренние реализации.
Семантика не меняется: это переносит контракты и переключает на них модель; это не передизайн data-flow (ADR-010/011), событий или жизненного цикла трека.
2.3 Без реестра, без пользовательских узлов — диспетчеризация не меняется¶
Поскольку набор узлов закрыт (§1.4), рантайм продолжает диспетчеризовать узел, ассертя его к (теперь публичному) интерфейсу node-executor и вызывая — нет реестра kind→executor и нет публичной точки регистрации. Это держит изменение минимальным: оно про то, где живут контракты, а не про то, как диспетчеризуются узлы или кто может добавить узел.
2.4 Граница enforced в CI через depguard¶
Правило depguard запрещает pkg/model/** импортировать internal/** — правило,
которое ADR-003 §4.4 (правила направления импортов) требовал и которого нет.
Как только контракты публичны и модель чиста, правило делает незаметно
накопившийся регресс невозможным к повтору: файл модели, тянущийся к внутреннему
типу, валит make ci.
2.5 Не-цели и охват (фазами; каждая отсрочка названа)¶
- Реестр исполнителей / visitor / пользовательские виды узлов — явно вне охвата (§1.4); набор узлов закрыт.
- Передизайн семантики data-flow, событий или жизненного цикла — этот ADR только переносит контракты; семантика ADR-010/011 не тронута.
- Расщепление god-object'а
Instance(аудит 2.3) — sibling-рефакторинг, разделяющий дух «раздели роли», но это своё изменение. - Точный(е) публичный(е) пакет(ы), объединять ли per-execution-окружение и
data-binding-frame в один контракт или оставить два, и поэлементный порядок
переноса — решения реализации для SRD(ов), которые стейджат это (контракты
публичны → модель переключается поэлементно → правило depguard включается),
держа
make ciзелёным на каждом шаге.
3. Последствия¶
pkg/modelстановится чистым публичным API. Нет импортовinternal/*, нет внутренних типов в экспортируемых сигнатурах — стабильная поверхность перестаёт протекать машинерией и компилируется без загруженного рантайма.- Внутренняя машинерия снова свободна меняться. С моделью за публичными контрактами рефакторинг instance / data plane / event hub больше не рискует компиляцией или сигнатурами публичной модели (стабильность ADR-002 §4.7).
- Граница постоянна. depguard валит любое будущее ребро model→internal.
- ADR-014 и ADR-013 приземляются чисто. Новые исполнители реализуют публичные контракты с самого начала; observability-ADR имеет чистую поверхность для расширения.
- Цена: широкий, но механический перенос. Пять внутренних контрактов публикуются, ~шесть типов элементов + base task переключаются на них; без изменения поведения, без проектирования реестра. SRD стейджит это зелёным.
- Новая публичная поверхность, которую держать стабильной. Контракты executor / environment / data-binding присоединяются к публичному API под §4.7-версионированием ADR-002 — осознанное, задокументированное обязательство. Держится узкой, потому что набор узлов закрыт (нет сторонних реализаторов, лишь собственные у gobpm).
4. Рассмотренные альтернативы¶
- Полная инверсия с реестром исполнителей (модель = чистые данные, исполнители регистрируются по виду). Более тяжёлый «visitor/registry», который флоатил аудит. Его выигрыш — пользовательские виды узлов — которые SAD-001 §14 исключает (закрытый, определённый стандартом набор). Отвергнуто как over-engineering: оно вынесло бы всё поведение из модели и построило механизм регистрации ради расширяемости, которая не является целью. Более дешёвый перенос контрактов убирает внутреннюю связанность без этого.
- Принять связанность и задокументировать (won't-fix). gobpm владеет обоими
слоями, набор узлов закрыт,
Execзовёт только движок — так что связанность «всего лишь» публичный пакет, импортирующий internal и выставляющий внутренний тип. Отвергнуто:pkg/model— опубликованный API; экспортируемый метод, принимающий неконструируемый внутренний тип, — реальный (пусть скромный) дефект API, а правило depguard + чистый граф импортов — дешёвая страховка от худшей связанности позже. Фикс ограничен; жить с запахом на публичной поверхности — худший размен. - Опубликовать только интерфейс исполнителя, оставив
Execпринимающим внутреннее окружение. Чинит одну строку импорта, не проблему: сигнатура всё ещё несла бы внутренний тип. Отвергнуто — окружение (и frame, registrator, producer) тоже должны стать публичными, иначе протечка остаётся. - Сделать per-execution-окружение публичным, но оставить data-binding-frame
внутренним. Полфикса:
LoadData/UploadDataвсё ещё принимали бы*scope.Frame. Отвергнуто — все пять контрактов должны быть публичны, чтобы модель была свободна от internal; частичный перенос оставляет правило depguard падающим.
5. Рекомендации enterprise-готовности¶
Совещательно, не гейтинг — для приземляющих SRD(ов):
- Приземляйте правило depguard в том же изменении, что завершает перенос, чтобы граница была enforced в момент её корректности и каждый стейджевый шаг проверялся.
- Держите публичные контракты минимальными. Поскольку нет сторонних реализаторов (закрытый набор узлов), выставляйте ровно то, что нужно собственным исполнителям gobpm — узкую поверхность легче держать стабильной (ADR-002 §4.7).
- Добавьте model-only-сборку/тест, импортирующую
pkg/modelбез рантайма, как живое доказательство, что инверсия держится сверх правила depguard. - Предпочитайте один per-execution-контракт многим, если окружение и data-binding-frame можно объединить без расширения поверхности — меньше публичных контрактов для версионирования.
6. Открытые вопросы¶
- Нет. Сохранение формы исполнения и закрытого набора узлов (без реестра, §2.3),
перенос пяти execution-контрактов в публичные пакеты (§2.2) и enforce
pkg/model ↛ internalчерез depguard (§2.4) решены выше. Точная раскладка публичных пакетов, объединять ли контракты окружения и frame, и порядок стейджинга — заботы реализации для приземляющих SRD(ов).
7. Ссылки¶
- SAD-001 v.1 Vision & Architecture — §14 Conformance & Compliance Scope (закрытый, определённый стандартом набор узлов, который этот ADR предполагает); цели library-not-framework, модель-как-публичная-поверхность, которым он служит.
- ADR-001 v.5 Execution Model — жизненный цикл трек / исполнение-узла, чья диспетчеризация оставлена неизменной (§2.3).
- ADR-002 v.1 Extension Architecture —
public/internal-split (§3.3), per-execution
RuntimeEnvironment(§4.3) и §4.7-дисциплина версионирования, которую этот ADR уточняет и завершает для execution-контрактов. - ADR-003 Module Layout — §4.4
(правила направления импортов) предписал проверки depguard; этот ADR добавляет
недостающее правило
pkg/model ↛ internal. - ADR-011 v.5 Process Data Flow — §2.6 отложил «размещение публичных reader/node-executor-контрактов» layering-ADR — размещено здесь (§2.2).
- ADR-014 v.1 Message Handling — его исполнители SendTask/ReceiveTask реализуют публичные контракты, которые определяет этот ADR.
- Архитектурный аудит 2026-06-11 (
docs/audit/architecture-audit-2026-06-11.md) — находку 2.1 (слоистость) этот ADR ремедиирует; переоценена с CRITICAL до фикса инкапсуляции, когда расширяемость узлов исключена как не-цель.
История документа¶
| Версия | Дата | Автор | Изменение |
|---|---|---|---|
| v.1 | 2026-06-14 | Руслан Габитов | Draft. Чинит аудит 2.1 как инкапсуляционный фикс (не расширяемость): pkg/model сохраняет методы исполнения и закрытый набор узлов, но контракты, которые он реализует/потребляет — интерфейс node-executor (+ synchronizing-join-маркер), per-execution-окружение, consumer/producer + frame-операции data-binding, interaction-registrator, event-producer — переезжают в публичные пакеты; internal/* хранит реализации; pkg/model импортирует ноль internal/*. Правило depguard pkg/model ↛ internal (ADR-003 §4.4) делает это постоянным. Без реестра / visitor / пользовательских узлов (закрытый набор SAD-001 §14) — диспетчеризация не меняется (§2.3). Размещение публичных reader/node-executor-контрактов, отложенное ADR-011 v.5 §2.6 сюда, решено (§2.2). Вне охвата: передизайн data-flow/событий/жизненного цикла, расщепление god-object'а Instance (2.3), точная раскладка пакетов. Уточняет ADR-002 v.1; siblings ADR-001 v.5, ADR-003, ADR-011 v.5, ADR-014 v.1. |
| v.1 | 2026-06-15 | Руслан Габитов | Принято — реализовано через SRD-012 v.1 (5 milestone'ов; пять контрактов перенесены в pkg/exec/pkg/renv/pkg/eventproc/pkg/interactor; internal/exec+internal/renv упразднены; pkg/model импортирует ноль internal/*; правило depguard model-no-internal работает; без изменения поведения; make ci зелёный, 5 примеров exit 0). Без изменения содержания от Draft-концепции. |