Skip to content

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:

  1. 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.
  2. 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.
  3. 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 lane attribute 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

  1. Keep a single EventBus; filter client-side. Simple but leaves every subscriber doing heavy filtering and gives no per-lane SLOs.
  2. Topic-based routing on a single bus. Requires every publisher to tag events; easy to forget, and still doesn't give per-lane TracerProvider isolation for OTel.
  3. Two physical EventBus instances wired by a LaneRouter. 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.
  4. 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 at src/fcc/data/messaging/event_lane_map.yaml and forwards to the correct bus. Two event types (protocol-bridge error, collaboration-maturity assessed) classify as both and 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.yaml is 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 on src/fcc/messaging/lanes.py; exhaustively covers lane: paom | ux | both forwarding + 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.pyPAOMBus, UXBus, LaneRouter
  • src/fcc/data/messaging/event_lane_map.yaml — 85-type authority
  • src/fcc/observability/otel_bootstrap.py — per-lane OTel providers
  • src/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)