ADR-006: Dual-Bus Event Architecture (PAOMBus + UXBus + LaneRouter)¶
Date: 2026-04-22
Status: Accepted
Decision-makers: FCC core maintainers · LAR (Lane-Router Architect) persona · OTO (OpenTelemetry Orchestrator) persona
Consulted: AURORA / Sky Parlour team (UX-lane consumer) · AOME / CRUCIBLE team (PAOM-lane consumer)
Informed: All fcc.plugins.subscribers plugin authors
Context and Problem Statement¶
By v1.4.1 the FCC framework had a single global EventBus serving 81
event types from three strongly different audiences:
- Back-office consumers (AOME auditor, governance pipeline, compliance subscriber, plugin bridge) that need deep, structured, high-fan-out event streams with correlation-id + workflow context.
- UX/visualization consumers (AURORA Sky Parlour frontend, demo + tutorial live views, search/KG/RAG query streams) that need low-latency, low-cardinality, browser-friendly event envelopes.
- Both (protocol-bridge errors, collaboration-maturity assessments) that genuinely belong on both lanes.
Running all three through one bus meant every UX subscriber saw every
governance event, every AOME subscriber saw every search-query ping,
and every plugin subscriber had to do heavy event-type filtering
client-side. Observability was also coarse — a single fcc.events
tracer scope could not distinguish "back-office" from "UX" in the OTel
span stream, so SLO dashboards conflated two very different workloads.
The problem was how to physically split the bus by audience without breaking any existing subscriber or losing the unified pub/sub/filter/replay semantics that shipped in v1.3.x.
Decision Drivers¶
- SLA separation. UX consumers need <100 ms event-to-visualization; back-office consumers tolerate >1 s delivery for audit durability.
- OTel attribute discipline. Every span needs a
laneattribute so dashboards can slice SLOs by audience. - Back-compat. All v1.3.x direct-
EventBus()construction sites must continue to function; no breaking API change in v1.4.x. - Event-type authority. The classification of each of the 85 event types must be centralised in one file, not scattered across call sites.
- Review-gate enforcement. New event types added in a PR must be classified at review time — no "unknown lane" events in main.
Considered Options¶
- Keep a single
EventBus; filter client-side. Simple but leaves every subscriber doing heavy filtering and gives no per-lane SLOs. - Topic-based routing on a single bus. Requires every publisher
to tag events; easy to forget, and still doesn't give per-lane
TracerProviderisolation for OTel. - Two physical
EventBusinstances wired by aLaneRouter. Every event passes through the router, which classifies it via an authoritative YAML map and forwards to one or both buses. Gives per-lane tracers, per-lane SLOs, and per-lane back-pressure. - Replace with an external message broker (Kafka / NATS). Gives
full lane separation but introduces deployment weight unacceptable
for a framework that must work in
pip install+ standalone mode.
Decision¶
We adopt option 3: a dual-bus architecture with a LaneRouter
classifier.
The implementation lands in src/fcc/messaging/lanes.py:
PAOMBus— back-office bus (workflow, simulation, governance, compliance, plugin lifecycle, MCP/A2A protocol, ecosystem events, ice_ext events; 73 event types).UXBus— UX bus (visualization, demo/tutorial live-view, search/KG/RAG query events; 10 event types).LaneRouter— classifies every event via the authoritative YAML map atsrc/fcc/data/messaging/event_lane_map.yamland forwards to the correct bus. Two event types (protocol-bridge error, collaboration-maturity assessed) classify asbothand fan to both buses.
Back-compat shim: direct EventBus() construction still works but
emits a DeprecationWarning naming the LaneRouter as the successor
and v1.6.0 as the removal target. All v1.3.x call sites continue to
function without source changes.
Review-gate enforcement: the PR template gained an "Does this PR add an EventType?" checkbox in v1.4.1 (before dual-bus); v1.4.2 added event-lane classification as the follow-on check.
OTel integration (ADR companion in observability/otel_bootstrap.py):
per-lane TracerProvider + MeterProvider with
service.name=fcc.paom vs service.name=fcc.ux, so OTLP exports
segregate SLO telemetry by audience. Every span carries lane,
event_type, correlation_id, and zachman_cell attributes.
Consequences¶
Positive¶
- Clean SLO separation. UX latency dashboards and back-office durability dashboards cannot be confused.
- Per-lane back-pressure. UX back-pressure doesn't block the governance pipeline (critical for compliance audit runs).
- Authoritative lane map.
event_lane_map.yamlis the single place to answer "is this event UX or back-office?" Every new event type must add an entry (CI gate). - No breaking change. v1.3.x code continues to work with a deprecation warning.
- OTel span attribute discipline. Per-lane tracers produce clean
OTLP exports that segregate by
service.name.
Negative¶
- Two bus objects to keep in sync. Bug classes like "subscriber registered on the wrong bus" can't exist in a single-bus world; they can now. Mitigated by adding the lane map as the discovery point — subscribers register at the router, which routes to the right bus automatically.
- Deprecation-path cost. The
EventBus()shim must stay through v1.5.x; removal in v1.6.0 is a scheduled breaking change. - Mental-model overhead. Contributors must learn "which lane does this event belong on?" The map file mitigates this — just edit the YAML, don't write routing code.
Confirmation¶
tests/test_lanes.py(46 tests) — 100% line + branch coverage onsrc/fcc/messaging/lanes.py; exhaustively coverslane: paom | ux | bothforwarding + back-compat + deprecation-warning emission.tests/test_otel_integration.py(71 tests) — per-lane tracer/meter provider isolation + span-attribute discipline.- CI contract test: adding a new EventType without a lane-map entry fails validation.
- The v1.4.2 release (commit
ebdd61c) shipped with the full 85 event types classified and no regressions against v1.4.1 consumers.
References¶
src/fcc/messaging/lanes.py—PAOMBus,UXBus,LaneRoutersrc/fcc/data/messaging/event_lane_map.yaml— 85-type authoritysrc/fcc/observability/otel_bootstrap.py— per-lane OTel providerssrc/fcc/protocols/ux_gateway.py— UX REST gateway on port 8901 (pairs with UXBus)- CHANGELOG
[1.4.2] — 2026-04-22— detailed delivery record tests/test_lanes.py,tests/test_otel_integration.py- New personas: LAR (Lane-Router Architect), OTO (OpenTelemetry Orchestrator)