Skip to content

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/interactorpkg/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/scopeLoadData/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-концепции.