Перейти к содержанию

ADR 0099: IDE DataBus — типизированные события и проекции состояния

Статус: Accepted · Implemented
Дата: 2026-04-25

Связанные ADR

ADR Роль
0094 шина доставки и backpressure
0097 CCU: свёртка в DTO канала
0036 канал → CDS → композиция поверхности
0095 уровни Workspace/Solution/IDE
0004 UI marshaling
0007 сигналы и связность
## Резюме
  • IDE DataBus: типизированные события в процессе IDE.
  • Развязка источников и проекций; не подменяет 0094 и 0097.


Контекст

В коде уже есть рабочие элементы шины и свёртки:

  • транспорт и батчинг вывода (ADR 0094);
  • вычислительные блоки CCU и каналовая композиция (ADR 0097, ADR 0036).

Но пока нет единого прикладного контракта «событие домена IDE → подписчики → проекция состояния».
Из-за этого часть сигналов продолжает идти через прямые делегаты и ручную склейку в ViewModel.


Решение

Ввести в архитектуру слой IDE DataBus как in-process typed event bus.

  1. Контракт: IDataBus с минимальным API:
  2. Publish<TEvent>(TEvent evt)
  3. Subscribe<TEvent>(Action<TEvent> handler) (с disposable-отпиской)

  1. События типизированные (не object/string):
  2. BuildStateChanged
  3. TestsStateChanged
  4. DebugStateChanged
  5. GitStateChanged
  6. (по мере надобности) ScopeDecisionChanged и т.д.

  1. DataBus не подменяет 0094:
    Channel<T>/ingestion остаётся транспортом потока; DataBus — слой распространения нормализованных событий домена.

  1. DataBus не подменяет 0097/0036:
    CCU и compositor по-прежнему отвечают за свёртку/проекцию. DataBus доставляет входные события к местам, где строятся снимки/DTO.

  1. Базовая реализация v1: синхронный in-memory bus в одном процессе IDE, без внешнего брокера/IPC.

  1. Git в IDE Health: один продуктовый путь — после обновления git-строк в UiChromeViewModel вызывается AfterGitWorkspaceHealthSummaryApplied (в конце RefreshGitSummaryAsync на UI-потоке), что в MainWindowViewModel привязано к PublishGitToIdeDataBusAndRebuildIdeHealth (публикация GitStateChanged + RebuildIdeHealth). Сид начального состояния: SeedIdeHealthDataBus() в конструкторе (startup + первый GitStateChanged), без PropertyChanged на отдельные поля git.

  1. Снимок канала и UI: IdeHealthSnapshotUnit.Build вызывается только из MainWindowViewModel.IdeHealth (RebuildIdeHealth); результат кэшируется в _lastIdeHealthInputSnapshot, геттеры строк в MainWindowViewModel.Presentation читают кэш. Roslyn CASCOPE019 фиксирует эту границу.

  1. Жизненный цикл: IdeHealthSnapshotUnit реализует IDisposable (отписка от шины); при закрытии главного окна — ReleaseWorkspaceHealthChannel().

  1. Порядок для IDE Health (внедрено): прикладной InMemoryDataBus главного окна — синхронная диспетчеризация (asynchronousDispatch: false), чтобы подписчики IdeHealthSnapshotUnit отработали до возврата из Publish, а RebuildIdeHealth() читал согласованный снимок. Сборка из UI: сначала BuildStateChanged (старт/финиш), затем IsBuilding — чтобы NotifyPropertyChangedForRebuildIdeHealth не обходил обновление _buildSnapshot. Публикации с фона MCP — через UiScheduler.InvokeAsync в PublishToIdeDataBusAndRebuild (тот же UI-поток, что и свёртка).

Принципы обмена

  1. Неблокирующий транспорт между слоями:
    ни IDS, ни CDS, ни CCU не должны зависеть от синхронного ответа друг друга в runtime-цепочке.
    Публикация выполняется как «fire-and-forward» в соответствующий канал/шину, обработка — по готовности потребителя.

  2. Строгая типизация сообщений:
    никаких object/dynamic в каналах домена.
    Используются типизированные события и явные контракты сообщений (record/иерархия типов; discriminated-union-стиль через pattern matching C#).

  3. Backpressure и политика потерь по классу данных:

  4. для критичных сигналов (ошибки, жизненный статус IDE, safety/health) — режим без потерь (unbounded, bounded+wait или отдельный приоритетный контур);
  5. для тяжёлых/высокочастотных сигналов (например графовые срезы для Skia) — BoundedChannel с политикой вроде DropOldest/«latest wins», чтобы не копить устаревшие кадры.

  6. Изоляция доменов:
    CCU получает вход из своего typed input-потока (сенсоры/источники) и публикует отдельный typed output-поток (индикация/проекции).
    Ошибки в контуре отрисовки/потребления не должны валить анализ/вычисление.


Границы

  • Можно: использовать DataBus для развязки источников и проекций (UI/MCP/cockpit snapshot).
  • Нельзя: смешивать в одном типе транспортные механики (Channel<T>, backpressure) и бизнес-события.
  • Нельзя: переносить рендер/UI-логику в обработчики событий шины.

Strangler-план

  1. Пилотный vertical slice: BuildStateChanged от источника сборки до IdeHealth snapshot.
  2. Затем TestsStateChanged и DebugStateChanged.
  3. После стабилизации — расширить на Git/прочие доменные сигналы.
  4. Закрепить границы в CascadeIDE.ArchitectureAnalyzers: CASCOPE019 — запрет прямого _workspaceHealth.Build вне MainWindowViewModel.IdeHealth (и прежние правила pipeline для устаревших API, см. README анализаторов).

Последствия

  • Меньше связности в MainWindowViewModel.
  • Проще тестировать куски по событиям (publish → проверка проекции).
  • Проще добавлять новые каналы/снимки без каскадной правки существующих сервисов.
  • Появляется риск «event spaghetti» при слабой дисциплине именования/границ — гасится typed-событиями и ADR-гайдлайнами.

Не цели

  • Внешний message broker, распределённая шина или межпроцессный transport.
  • Унификация всех потоков в один универсальный envelope на первом шаге.
  • Массовая миграция всех существующих сигналов в один коммит.