ADR 0099: IDE DataBus - Typed Events and State Projections¶
Status: Accepted · Implemented
Date: 2026-04-25
Related ADRs¶
| ADR | Role |
|---|---|
| 0094 | tire delivery and backpressure |
| 0097 | CCU: convolution in channel DTO |
| 0036 | channel → CDS → surface composition |
| 0095 | Workspace/Solution/IDE levels |
| 0004 | UI marshaling |
| 0007 | signals and connectivity |
| ## Summary |
- IDE DataBus: typed events in the IDE process.
- Decoupling of sources and projections; does not replace 0094 and 0097.
Context¶
The code already has bus and convolution work elements:
- transport and output batching (
ADR 0094); - CCU computing units and channel composition (
ADR 0097,ADR 0036).
But there is no single application contract “IDE domain event → subscribers → state projection” yet.
Because of this, some signals continue to go through direct delegates and manual gluing in the ViewModel.
Solution¶
Introduce the IDE DataBus layer into the architecture as an in-process typed event bus.
- Contract:
IDataBuswith minimal API: Publish<TEvent>(TEvent evt)Subscribe<TEvent>(Action<TEvent> handler)(with disposable unsubscribe)
- Typed events (not
object/string): BuildStateChangedTestsStateChangedDebugStateChangedGitStateChanged- (as needed)
ScopeDecisionChanged, etc.
- DataBus does not replace 0094:
Channel<T>/ingestion remains the stream transport; DataBus is a distribution layer for normalized domain events.
- DataBus does not replace 0097/0036:
CCU and compositor are still responsible for convolution/projection. DataBus delivers input events to the locations where snapshots/DTOs are built.
- Basic implementation v1: synchronous in-memory bus in one IDE process, without external broker/IPC.
- Git in IDE Health: one product path - after updating git lines in
UiChromeViewModel,AfterGitWorkspaceHealthSummaryAppliedis called (at the end ofRefreshGitSummaryAsyncon the UI thread), which inMainWindowViewModelis tied toPublishGitToIdeDataBusAndRebuildIdeHealth(publicationGitStateChanged+RebuildIdeHealth). Initial state seed:SeedIdeHealthDataBus()in the constructor (startup + firstGitStateChanged), withoutPropertyChangedon individual git fields.
- Channel Snapshot and UI:
IdeHealthSnapshotUnit.Buildis called only fromMainWindowViewModel.IdeHealth(RebuildIdeHealth); the result is cached in_lastIdeHealthInputSnapshot, row getters inMainWindowViewModel.Presentationread the cache. Roslyn CASCOPE019 captures this boundary.
- Lifecycle:
IdeHealthSnapshotUnitimplementsIDisposable(bus unsubscribe); when closing the main window -ReleaseWorkspaceHealthChannel().
- Order for IDE Health (implemented): applied
InMemoryDataBusmain window - synchronous dispatch (asynchronousDispatch: false) so that subscribers ofIdeHealthSnapshotUnitwork before returning fromPublish, andRebuildIdeHealth()reads the agreed snapshot. Build from the UI: firstBuildStateChanged(start/finish), thenIsBuilding- so thatNotifyPropertyChangedFor→RebuildIdeHealthdoes not bypass the_buildSnapshotupdate. Publishing from the MCP background - viaUiScheduler.InvokeAsyncinPublishToIdeDataBusAndRebuild(same UI thread as fold).
Exchange principles¶
- Non-blocking transport between layers:
neither IDS, nor CDS, nor CCU should depend on each other's synchronous response in the runtime chain.
Publishing is performed as “fire-and-forward” to the appropriate channel/bus, processing is done when the consumer is ready. -
Strict message typing:
noobject/dynamicin domain channels.
Typed events and explicit message contracts are used (record/type hierarchy; discriminated-union style via pattern matching C#). -
Backpressure and loss policy by data class:
- for critical signals (errors, IDE vital status, safety/health) - lossless mode (unbounded, bounded+wait or a separate priority circuit);
-
for heavy/high-frequency signals (for example, graph slices for Skia) -
BoundedChannelwith a policy likeDropOldest/“latest wins”, so as not to accumulate outdated frames. -
Domain isolation:
The CCU receives input from its typed input stream (sensors/sources) and publishes a separate typed output stream (indication/projections).
Errors in the rendering/consuming loop should not crash the analysis/computation.
Boundaries¶
- You can: use DataBus to decouple sources and projections (UI/MCP/cockpit snapshot).
- You cannot: mix transport mechanics (
Channel<T>, backpressure) and business events in one type. - You cannot: transfer rendering/UI logic to bus event handlers.
Strangler-plan¶
- Pilot vertical slice:
BuildStateChangedfrom the build source to the IdeHealth snapshot. - Then
TestsStateChangedandDebugStateChanged. - After stabilization, expand to Git/other domain signals.
- Fix boundaries in
CascadeIDE.ArchitectureAnalyzers: CASCOPE019 - prohibit direct_workspaceHealth.BuildoutsideMainWindowViewModel.IdeHealth(and previous pipeline rules for legacy APIs, see README of analyzers).
Consequences¶
- Less cohesion in
MainWindowViewModel. - It’s easier to test pieces based on events (publish → check projection).
- It’s easier to add new channels/images without cascading editing of existing services.
- The risk of “event spaghetti” appears with weak naming/boundary discipline - it is extinguished by typed events and ADR guidelines.
Not goals¶
- External message broker, distributed bus or interprocess transport.
- Unification of all streams into one universal envelope in the first step.
- Mass migration of all existing signals into one commit.