Series 1 — The Behavioral Intelligence OS • Part 6 of 8

Engines in the behavioral AI platform do not call each other. They publish events and subscribe to topics through an in-process event bus. This article explains the pattern, shows the implementation, covers why it prevents circular dependencies, and walks through the security review that found the bus was quietly carrying more personal data than any single engine actually needed.

Who this is for

Backend engineers designing decoupled, event-driven communication between independently-authored services or modules; security and privacy reviewers evaluating data-minimization exposure in an internal messaging layer; engineering managers deciding between direct function calls and a publish/subscribe pattern for a growing system.

The Security Review That Found the Bus Was Over-Sharing

A routine data-minimization review — the kind GDPR's own Article 5(1)(c) principle requires an organization to periodically conduct, not a one-time certification — pulled a sample of event-bus payloads and found something nobody had deliberately designed but everybody had quietly allowed: several engines were emitting their entire input signals object onto the bus alongside their actual output, on the reasonable-sounding assumption that a downstream subscriber might someday want the full context. In practice, no subscriber used more than two or three fields from any given event. The bus, meant to carry narrow, purposeful signals between engines, had drifted into carrying broad copies of raw behavioral data to every listener, whether that listener needed it or not — a textbook data-minimization gap, and a genuine one, because every engine subscribed to a topic received the full payload regardless of what it actually read.

The fix, covered in full later in this article, was not a policy memo asking engine authors to be more careful. It was a schema-enforced contract: every event topic now declares exactly which fields its payload may contain, validated at publish time, with anything broader rejected before it ever reaches the bus. That fix, and the review that motivated it, is why this article exists in its current form rather than as a purely architectural explainer — decoupling engines from each other, this article's original subject, turns out to matter for privacy exposure just as much as it matters for avoiding circular imports.

Why Engines Must Not Call Each Other

The same coupling risk recurs across the Technology and Artificial Intelligence sectors broadly, anywhere independently-authored components need to communicate without creating a maintenance trap. If the causal-graph engine imports and calls the bayesian-confidence engine directly, you have a compile-time dependency that cannot be changed at runtime. The registry cannot activate or deactivate it. The control plane cannot reorder it. And the moment confidence wants to call causal-graph back, you have a circular import that crashes the process — not a hypothetical failure mode, but the literal shape of a real early incident, covered in full below, that happened before the event bus existed in its current form.

Events decouple this entirely. bayesian-confidence emits confidence.updated. causal-graph subscribes to it. Neither knows the other exists — not just in the informal sense of good software hygiene, but literally: neither engine's source file imports the other's, and the registry's own dependency-cycle validator (registry article) never has anything circular to catch in the first place, because the architecture makes a circular reference structurally impossible to express.

The Incident That Made Decoupling Non-Negotiable

Before the event bus existed, two engines under active, parallel development each needed a piece of the other's output: causal-graph wanted bayesian-confidence's stability figure to weight its own causal-link strength estimates, and, in a later change nobody realized would collide, bayesian-confidence's own confidence formula was extended to weight down scores flowing through causal chains flagged as unstable — reading causal-graph's output back. Both changes were reasonable in isolation, reviewed by different engineers, on different pull requests, days apart. Neither reviewer saw the other's change. The two engines now imported each other directly. On the next deployment, module resolution entered a cycle during process startup and the entire scoring service failed to boot — not a subtle bug, a hard crash, on every instance, simultaneously, the moment the deployment rolled out.

The incident was resolved quickly once diagnosed — reverting one of the two changes restored a bootable process within the hour — but the diagnosis itself took longer than it should have, because nothing in the existing tooling made a circular import visible before it actually crashed a running process. The event bus, and the registry's own build-time cycle detection built on top of the same underlying graph structure, exist specifically so this exact failure mode — two independently-authored, independently-reviewed changes silently creating a cycle neither author could see — becomes structurally impossible rather than something caught only when a process fails to start.

The Event Bus Implementation

// (c) Govind Preet Singh -- govindpreetsingh.com. All rights reserved.
// License required for use -- contact govindpreetsingh.com. No unapproved use permitted.
// Article published: 22 May 2026.
// URL: https://govindpreetsingh.com/article.php?slug=the-behavioral-event-bus

// core/event-bus.js
const EventEmitter = require('events');

class BehavioralEventBus extends EventEmitter {
  constructor() {
    super();
    this.setMaxListeners(50); // 34 engines + control plane + governance
    this._log = [];
  }

  emit(topic, payload) {
    this._log.push({ topic, payload, ts: Date.now() });
    return super.emit(topic, payload);
  }

  getLog() { return [...this._log]; }
  clearLog() { this._log = []; }
}

module.exports = new BehavioralEventBus(); // singleton

This is deliberately built on Node.js's built-in EventEmitter rather than a full external message-queue library — the entire bus operates in-process, within a single control-plane execution (control-plane article), never across a network boundary, so the durability, retry, and cross-process delivery guarantees a real message broker provides would be solving a problem this specific bus doesn't have. The one addition beyond stock EventEmitter behavior is the internal _log array, which is what makes every event on the bus visible to the control plane's own observability tooling and, as this article's opening incident shows, to the periodic data-minimization audits that inspect exactly what's flowing across it.

Topic Naming Convention

Topics follow a domain.entity.event pattern, chosen deliberately for the same reason the registry article insists on a strict engineId naming convention: a large, growing set of topic names stays legible only if the naming itself carries structure.

  • confidence.updated — Bayesian engine published a new confidence value
  • state.transition.detected — FSM engine found a phase change
  • narrative.propagation.spike — Narrative engine detected rapid belief spread
  • trust.deficit.flagged — Trust graph engine found a significant deficit

The convention is enforced, not just documented — a lint rule rejects any eventBus.emit() call using a topic string that doesn't match the three-part dotted pattern, and a startup validator (mirroring the registry's own metadata validation) confirms every engine's declared emitsTopics and subscribesTopics actually follow the convention before the process is allowed to serve traffic.

Async Events and Engine Ordering

Because the control plane runs engines in dependency-sorted batches, subscribed events from a prior batch are always available to engines in the current batch — this is the identical per-batch publication guarantee the control-plane article describes in detail from its own side, restated here from the bus's side: publication happens once a batch's Promise.allSettled resolves, not per-engine, so no downstream engine ever observes a partial or racing view of the batch it depends on.

The event log is passed into each engine's score() call as context.events, so engines can read prior-batch outputs even if they did not directly subscribe — a deliberately narrow escape hatch for cases where an engine needs read access to the bus's recent history without formally declaring a subscription, used sparingly and reviewed carefully, since bypassing the formal subscription declaration also means bypassing the registry's own dependency-graph visibility into that relationship.

Building the Data-Minimization Fix

// (c) Govind Preet Singh -- govindpreetsingh.com. All rights reserved.
// License required for use -- contact govindpreetsingh.com. No unapproved use permitted.
// Article published: 22 May 2026.
// URL: https://govindpreetsingh.com/article.php?slug=the-behavioral-event-bus

// Every topic now declares an explicit payload schema at registration time,
// alongside the emitting engine's other registry metadata.
registerEngine({
  engineId: 'bayesian-confidence',
  emitsTopics: ['confidence.updated'],
  topicSchemas: {
    'confidence.updated': z.object({
      engineId: z.string(),
      score: z.number(),
      confidence: z.number(),
      // deliberately NOT the full signals object — only what a
      // subscriber has demonstrated it actually needs
    }),
  },
  // ...
});

// core/event-bus.js — publish now validates against the declared schema
function emit(topic, payload) {
  const schema = getTopicSchema(topic);
  const result = schema.safeParse(payload);
  if (!result.success) {
    throw new EventSchemaViolationError(
      `Payload for topic "${topic}" does not match its declared schema: ${result.error.message}`
    );
  }
  this._log.push({ topic, payload: result.data, ts: Date.now() });
  return EventEmitter.prototype.emit.call(this, topic, result.data);
}

The validation is deliberately strict, not permissive — Zod's safeParse against an object schema with no wildcard fields means an engine author cannot silently widen a topic's payload by adding a field nobody reviewed, the same discipline the registry article applies to engine metadata generally. Widening a topic's schema is a real, reviewed change, requiring the same justification any other metadata change does; narrowing one — removing a field, once an audit confirms nothing subscribes to it — is the fast, encouraged direction, mirroring the registry's own asymmetric fast-path for scoping an engine down versus up.

What Changed for Every Existing Engine

The schema-enforcement rollout could not simply be flipped on platform-wide — every one of the 34 existing engines' topic payloads had to be audited individually, its actual subscribers' real field usage traced, and a minimal schema written and reviewed before that engine's topics could be locked down. This took real calendar time, run engine by engine rather than as a single cutover, specifically because getting a topic's schema wrong in the narrow direction (excluding a field a subscriber genuinely needed) would break that subscriber the moment enforcement went live, and the team judged a slower, verified rollout safer than a fast one that risked a production outage from an incorrectly narrowed schema.

Testing the Event Bus and Its Schemas

// (c) Govind Preet Singh -- govindpreetsingh.com. All rights reserved.
// License required for use -- contact govindpreetsingh.com. No unapproved use permitted.
// Article published: 22 May 2026.
// URL: https://govindpreetsingh.com/article.php?slug=the-behavioral-event-bus

describe('event bus schema enforcement', () => {
  it('rejects a payload with fields not declared in the topic schema', () => {
    expect(() => eventBus.emit('confidence.updated', {
      engineId: 'bayesian-confidence', score: 0.8, confidence: 0.9,
      rawSignals: { /* the exact over-sharing this article's incident found */ },
    })).toThrow(EventSchemaViolationError);
  });

  it('accepts a minimal, schema-conformant payload', () => {
    expect(() => eventBus.emit('confidence.updated', {
      engineId: 'bayesian-confidence', score: 0.8, confidence: 0.9,
    })).not.toThrow();
  });

  it('every registered topic has a schema — no topic ships unvalidated', () => {
    for (const engine of engineRegistry.getAll()) {
      for (const topic of engine.emitsTopics) {
        expect(getTopicSchema(topic)).toBeDefined();
      }
    }
  });
});

The third test is the one that actually prevents this article's opening incident from recurring in a different shape — it fails the build if any newly-registered engine's topic slips through without a declared schema, converting "did anyone remember to write a schema for this" from a hopeful assumption into a build-time guarantee.

Comparing to a Full Message Broker

The same category of question the control-plane and registry articles both address about their own layers applies here: why an in-process EventEmitter rather than Kafka, RabbitMQ, or a similar broker. The answer is the identical latency argument made throughout this series — every engine invocation this bus mediates happens within a single control-plane execution, typically completing in low tens of milliseconds total (control-plane article), and introducing network-hop message-broker semantics into that path would add latency wildly disproportionate to the actual work being coordinated. A message broker earns its cost when messages need to survive a process crash, cross a network boundary, or be consumed by genuinely independent services on their own schedules — none of which describes engine-to-engine communication within a single pipeline run.

Comparing to Alternative Decoupling Patterns

Dependency Injection

A common alternative to publish/subscribe is dependency injection — passing an interface reference to a dependency rather than importing it directly, letting the concrete implementation be swapped at composition time. This was considered and rejected as the primary pattern here for a reason specific to this platform's runtime-activation requirement: DI still requires the dependency graph to be known and wired at process startup (or close to it), which works well for a fixed set of services but fights against the registry article's central requirement that engines can be activated, restricted, or deprecated at runtime, based on database state, without a redeploy. A DI container wired at startup would need to be rebuilt or reconfigured every time the registry's effective active set changed — exactly the deployment-coupled problem the registry article's opening incident exists to eliminate, just relocated into the DI wiring layer instead of the original hard-wired call sites.

Shared Mutable State

A cruder alternative — engines writing results into a shared object or cache that other engines read from — was never seriously considered as more than an anti-pattern, but it's worth naming explicitly because it's the shape the registry article's own hidden-coupling anti-pattern warns against, and the event bus is precisely the sanctioned alternative to it. Shared mutable state has none of the bus's observability (nothing logs who wrote what and when), none of its schema enforcement, and none of its clean batch-boundary consistency guarantee — it would reintroduce every problem this article's incident and data-minimization review both catalog, just without any of the tooling built to catch them.

Why Publish/Subscribe Won

Pub/sub gives the three properties this platform's architecture actually needs simultaneously: engines that don't need to know about each other's existence (decoupling), a control plane that can observe every inter-engine signal without engines needing to cooperate explicitly (observability), and a natural point — the topic boundary — at which to enforce a data contract (the schema-validation fix this article documents). No other pattern considered offered all three without a meaningfully larger engineering cost.

Postmortem: The Circular Import Crash, in Full

Timeline

  • Day 0: engineer A adds a direct import of bayesian-confidence into causal-graph, to read its stability figure. Reviewed and merged; tests pass, since no cycle exists yet.
  • Day 2: engineer B, working on an unrelated confidence-formula improvement, adds a direct import of causal-graph into bayesian-confidence, to weight down scores flowing through unstable causal chains. Reviewed and merged by a different reviewer, who had no visibility into engineer A's change from two days earlier.
  • Day 2, deployment: both changes ship in the same release train. Module resolution enters a cycle during process boot. Every instance crash-loops simultaneously.
  • Day 2, within the hour: on-call reverts the more recent of the two changes, restoring a bootable process, then begins root-cause investigation.

What Changed

Beyond the event bus itself, the registry article's cross-referenced startup-time cycle detection was extended to run in CI, not just at process boot, specifically so a circular-import-shaped change would fail a pull request's automated checks before merge, rather than being discovered only when a deployed process actually failed to start. The lint rule blocking direct engine-to-engine imports, referenced throughout the registry and control-plane articles, was the third and most durable fix — converting "don't do this" from a code-review expectation two different reviewers might each miss into a mechanically enforced rule neither PR in this incident could have passed.

Anti-Patterns in Event Bus Usage

Anti-Pattern: Subscribing Broadly "Just in Case"

An engine subscribing to every topic it might conceivably need someday, rather than the specific topics its current logic actually reads, creates exactly the same over-collection problem this article's opening incident found on the publishing side, just from the subscriber's side instead. A subscription with no corresponding read in the engine's own logic is dead weight the registry's dependency graph nonetheless treats as real, inflating apparent coupling and making future data-minimization audits harder to reason about. Subscriptions, like dependencies (registry article), are reviewed for justification — "what field from this topic does your logic actually use" — not accepted as a default-safe, low-cost addition.

Anti-Pattern: Reading context.events Instead of Declaring a Real Subscription

The raw event-log escape hatch mentioned earlier exists for genuine edge cases, not as a convenient way to avoid the formality of declaring subscribesTopics. An engine that reads a specific topic's data via context.events without declaring a subscription is invisible to the registry's dependency graph — exactly the hidden-coupling anti-pattern the registry article warns against, now possible via the bus itself rather than a direct import. Code review treats an undeclared context.events read for a topic that has a real, ongoing dependency shape as equivalent to a missing dependencies entry, and requires it be converted to a formal subscription.

Anti-Pattern: Treating the Event Log as a Substitute for the Explainability Trace Store

The bus's own _log array is a short-lived, in-memory, single-request-scoped structure — it is not, and was never intended to be, a substitute for the explainability article's bc_explainability_traces table. An early proposal to satisfy audit requirements by simply persisting the event log directly was rejected once someone traced through the two systems' actual guarantees: the event log has no schema-versioning discipline, no immutability enforcement, and no per-engine field-interpretation layer — everything the explainability article's system was purpose-built to provide. The bus's log is for the current request's own internal coordination and short-term observability; the trace store is the permanent, defensible record. Conflating the two would have quietly weakened the platform's actual audit story while looking, superficially, like it satisfied the requirement.

Code Review Checklist for Event Bus Changes

CheckWhy
New topic has a declared, reviewed schema before any engine emits to itDirectly descended from the data-minimization incident this article opens with.
New subscription is justified by a specific field the subscribing engine's logic actually readsPrevents the "subscribe broadly just in case" anti-pattern from inflating the dependency graph.
No engine imports another engine's module directly, anywhere in the diffThe single rule that would have prevented this article's circular-import postmortem, now enforced by lint, not just review.
Any context.events read for a topic with an ongoing, real dependency is converted to a formal subscriptionKeeps the registry's dependency graph an honest reflection of real inter-engine coupling.
Schema widening (never narrowing) requires the same justification as any other engine metadata changeWidening is the direction that reintroduces over-collection risk; narrowing is the safe, encouraged direction.

Worked Example: One Event, From Publish to Every Subscriber

Following one event through the bus end to end, using the same litigation-transcript request the registry and control-plane articles both trace through their own layers, keeps this article's examples consistent with the rest of the series.

Publish

// (c) Govind Preet Singh -- govindpreetsingh.com. All rights reserved.
// License required for use -- contact govindpreetsingh.com. No unapproved use permitted.
// Article published: 22 May 2026.
// URL: https://govindpreetsingh.com/article.php?slug=the-behavioral-event-bus

// bayesian-confidence, batch 0, completes and publishes
eventBus.emit('confidence.updated', {
  engineId: 'bayesian-confidence',
  score: 0.71,
  confidence: 0.503,
});
// Validated against the declared schema (no rawSignals, no free-text fields),
// logged to the bus's internal _log, then broadcast to every subscriber.

Subscribe

Two batch-1 engines, bias-detection and state-machine-runtime, both declared subscribesTopics: ['confidence.updated'] at registration. Both receive the identical, schema-validated payload the instant it's published — neither engine's code has any awareness that the other also subscribed, exactly the decoupling this article's opening sections argue for.

The Escape Hatch, Used Correctly

epistemic-intelligence, also active in this request's plan but with no formal subscription to confidence.updated, reads it anyway via context.events for a narrow, occasional cross-check it performs only under specific rare conditions — reviewed and accepted as a legitimate use of the escape hatch precisely because it is occasional and conditional, not a standing dependency that belongs in the registry's own graph as a formal subscription.

Performance Considerations

The bus's own overhead — an EventEmitter.emit() call, a schema validation, an array push — is negligible against the actual engine computation it coordinates, the same conclusion the confidence-propagation article reaches about its own formula's cost. The one piece worth monitoring at scale is setMaxListeners(50): as the platform's engine count grows past the current 34 toward the roadmap's 100+, this ceiling needs raising in step, and the team's practice, mirroring the control-plane article's own capacity-constant discipline, is to set it comfortably ahead of current engine count rather than exactly at it, avoiding a silent MaxListenersExceededWarning the first time a new engine's registration pushes past an unrevisited ceiling.

Security Considerations Beyond Data Minimization

Beyond the payload-schema enforcement this article centers on, the bus is subject to the identical access-boundary reasoning the registry and control-plane articles apply to their own layers: because it operates entirely in-process, within a single control-plane execution, there is no network surface to secure separately — the bus's security properties are inherited from the process boundary itself, and the isolation-testing suite the control-plane article describes (confirming no cross-request data leakage under concurrency) implicitly covers the bus as well, since any leak between concurrent requests' event data would show up in exactly that test suite's assertions.

Frequently Asked Questions

Can two engines emit to the same topic?

Yes, and the registry article's #topicEmitters index (covered in that article's internal-data-structures section) already accounts for this — a topic can have multiple emitters, each subscriber receiving every emission tagged with its originating engineId, exactly as described there.

What happens if a subscriber's own topic-handling logic throws?

Because subscriptions are handled via context.events reads and direct EventEmitter listeners called synchronously during an engine's own score() execution, a throw here is treated identically to any other exception in that engine's scoring logic — caught by the control plane's per-engine Promise.allSettled handling (control-plane article), never allowed to crash a batch or take down other engines' results.

Is the event log ever exposed outside the platform?

No — unlike the explainability trace store, which has a dedicated, access-controlled export path (explainability-traces article), the bus's internal log is purely an internal coordination and short-term-observability structure, cleared per request, never persisted or exported in its raw form to any external party.

Glossary

TermDefinition
TopicA named channel (e.g. confidence.updated) engines publish to and subscribe from, following the domain.entity.event naming convention.
Topic schemaThe declared, enforced shape of a topic's payload, validated at publish time to prevent over-broad data on the bus.
Escape hatch (context.events)Direct read access to the current batch's event log, for narrow cases not warranting a formal subscription — overuse is treated as hidden coupling.
Circular importTwo modules directly importing each other, causing a process-crashing resolution failure — the exact incident this article's decoupling architecture exists to make structurally impossible.

How the Data-Minimization Audit Actually Worked, Step by Step

The opening section of this article summarizes the finding; the process behind it is worth documenting in full, because the method generalizes to any system carrying data between components and is more instructive than the finding alone. This kind of periodic, structured review is governance support work made concrete, not a policy exercise. The audit began not with a code review of engine source files, but with the bus's own _log structure, captured across a sampled window of real production traffic with all identifying values redacted before analysis, specifically so the review itself never introduced a new privacy exposure while investigating an existing one. Every distinct topic observed in the sample was extracted, and for each topic, the reviewer catalogued every field present in at least one captured payload, cross-referenced against a separate, independently gathered list of which fields any subscribing engine's source code actually read from that topic — built by grepping each subscriber's implementation for property accesses on the event payload, not by asking engine authors to self-report, since self-reporting a field as unused is exactly the kind of judgment an author under time pressure might get wrong without realizing it.

The comparison between "fields present" and "fields actually read" produced a strikingly large gap for a handful of topics, confidence.updated being the most severe example cited in the opening section, but not the only one — state.transition.detected was found carrying a full historical sequence array where every subscriber read only its length, and trust.deficit.flagged was found carrying an internal debug object an engine author had left in during development and never removed once the feature shipped. None of these were malicious or even particularly careless individually; each was a small, locally reasonable decision — include a bit of extra context in case it's useful, leave a debug field in rather than risk breaking something by removing it — that, aggregated across 34 independently authored engines over months of incremental changes, produced a bus that was systematically over-sharing in ways nobody had deliberately chosen and nobody had noticed, because no single change looked wrong in isolation.

This is the pattern the audit's own final report emphasized as the real lesson, beyond the specific fields found: data minimization violations in a system like this rarely arrive as one obviously bad decision. They accumulate from many individually defensible ones, which is precisely why a periodic, systematic, code-level audit — not a one-time design review, and not trusting engine authors' own self-assessment — is necessary on an ongoing basis, and why the schema-enforcement fix described earlier in this article was designed to prevent the accumulation mechanism itself, not just to clean up the specific instances the one audit happened to find.

The Governance and Legal Framing of This Fix

Beyond the GDPR data-minimization principle referenced in this article's opening section and its own ADV cross-reference, the schema-enforcement fix maps onto a broader pattern of AI governance expectations worth stating explicitly. The EU AI Act's risk-management provisions for higher-risk systems expect an organization to actively identify and mitigate risks arising from its own system design, not merely from the model's predictions — an internal messaging layer quietly over-sharing behavioral data between components is exactly the kind of design-level risk that provision anticipates, distinct from a risk in any single engine's scoring logic. The fix described in this article is a concrete instance of that kind of internal risk being identified through deliberate review rather than through an external complaint or incident, which is itself the posture the regulation is trying to encourage: catching design-level data flows that violate minimization principles before they become the subject of an external inquiry, not after.

It's worth being honest about what this fix does and doesn't claim. It does not claim no personal data ever flows across the event bus — behavioral scoring inherently requires processing behavioral signals, some of which are personal data under most applicable definitions, and the bus necessarily carries some of it between engines that genuinely need it to do their jobs. What the fix claims, and what the schema-enforcement mechanism actually delivers, is that no topic can carry more data than its declared, reviewed schema permits, and that schema is scoped to what subscribers demonstrably use — a narrower, more defensible claim than "no personal data here," and a more honest one, consistent with the same overclaiming discipline the confidence-propagation and explainability-traces articles both insist on for their own claims.

A Second Incident: The Topic Schema That Was Too Narrow

The schema-enforcement rollout, described earlier as engine-by-engine and deliberately cautious, was cautious for a specific, real reason worth documenting: during an early pilot of the enforcement mechanism on a small subset of topics, one topic's newly-written schema omitted a field — a matter-type tag — that a subscriber had, in fact, been reading, just infrequently enough that the audit's own subscriber-usage analysis had missed it in its sampled traffic window. The moment enforcement went live for that topic, the subscribing engine began silently receiving an object missing a field its own logic expected, and, because the engine's code handled a missing field by falling back to a default value rather than throwing, this did not surface as an error — it surfaced, days later, as a subtle, hard-to-trace shift in that engine's own output distribution for a narrow category of requests that happened to depend on the now-missing field.

This near-incident, caught by the same kind of retrospective outcome-monitoring the confidence-propagation article describes for its own reducer-firing-rate dashboard, is why the rollout process was slowed down and made more rigorous partway through: rather than sampling a window of production traffic to infer subscriber usage, the finalized process for every remaining topic required a subscribing engine's own test suite to explicitly exercise every field it claimed to depend on, with coverage checked before that topic's schema was locked down — a stronger, code-verified guarantee than traffic sampling alone could provide, adopted specifically because traffic sampling had just been shown, concretely, to miss a real, infrequently-exercised dependency.

What a New Engine Author Needs to Know About the Bus

An engine author integrating with the event bus for the first time needs to understand a small, deliberately narrow set of obligations, mirroring the same narrow-integration-surface philosophy the confidence-propagation and control-plane articles both apply to their own respective systems. Declaring emitsTopics and subscribesTopics at registration is not optional decoration — it is what makes an engine's communication visible to the registry's dependency graph, the control plane's batching logic, and any future data-minimization audit simultaneously, from one declaration. Writing a topic's schema, for any new topic an engine introduces, means thinking concretely about what a hypothetical future subscriber would actually need, not what might conceivably someday be useful — the entire incident this article opens with is the accumulated cost of engine authors defaulting to the more generous, "just in case" answer to that question, repeatedly, over time. And using context.events rather than a formal subscription should be treated as an exception requiring its own justification in code review, not a convenient default that avoids the formality of declaring a real dependency.

Comparing the Bus's Design to How the Confidence and Trace Systems Consume It

It's worth tracing explicitly how the two most data-sensitive systems described elsewhere in this series actually interact with the bus, since both depend on it in ways that make the schema-enforcement fix this article centers on more consequential than a purely architectural cleanup would suggest. The confidence-propagation article's contradictionFactor computation reads from the bus to compare a current engine's output against other engines the shared ontology has declared comparable — which means every field a topic schema permits is a field potentially feeding into a confidence calculation that itself gets stored, permanently, in an explainability trace. An over-broad topic schema, before this article's fix, didn't just risk exposing more data than necessary in transit across the bus; it risked that over-broad data quietly working its way into a permanent, exported, potentially legally-discoverable record, through a path several steps removed from the original publish call and easy to miss without tracing the full chain the way this article and its siblings do explicitly.

This dependency chain — bus payload feeding confidence computation feeding a stored trace feeding a discovery export — is exactly why the schema-enforcement fix was treated as seriously as it was, reviewed with input from the same legal stakeholders the explainability-traces article describes becoming standing participants in compliance-adjacent design decisions. A narrow, over-cautious view of the event bus as "just internal plumbing between engines" would have understated its real stakes; tracing its actual downstream consequences through the rest of this series' architecture is what surfaced why the fix mattered as much as it did.

Multi-Region Considerations

Consistent with the control-plane article's own no-cross-region-execution design, the event bus is entirely local to whichever region's control-plane process is handling a given request — there is no cross-region event propagation, no shared bus state between regions, and no scenario in which an event published in one region is ever visible to a process running in another. This is a direct structural consequence of the bus being built on an in-process EventEmitter rather than a distributed broker, and it means the bus itself introduces no additional cross-region consistency question beyond what the control-plane article already documents for pipeline execution generally — a region's bus behaves identically, and independently, regardless of what any other region's bus is doing at the same moment.

Testing the Schema-Enforcement Rollout Itself

// (c) Govind Preet Singh -- govindpreetsingh.com. All rights reserved.
// License required for use -- contact govindpreetsingh.com. No unapproved use permitted.
// Article published: 22 May 2026.
// URL: https://govindpreetsingh.com/article.php?slug=the-behavioral-event-bus

describe('schema-enforcement rollout safety', () => {
  it('every currently-locked-down topic passes coverage-verified subscriber tests', () => {
    for (const topic of getLockedDownTopics()) {
      const subscribers = engineRegistry.getSubscribersFor(topic);
      for (const engine of subscribers) {
        const coverage = getTestCoverageForTopicFields(engine.engineId, topic);
        expect(coverage.everyDeclaredFieldExercised).toBe(true);
      }
    }
  });

  it('a topic pending lock-down is not yet enforced, and is flagged in the rollout dashboard', () => {
    const pending = getTopicsPendingEnforcement();
    for (const topic of pending) {
      expect(isSchemaEnforced(topic)).toBe(false);
      expect(getRolloutDashboard()).toContainTopic(topic);
    }
  });
});

The second test exists specifically because of the near-miss incident described earlier — it makes the rollout's own in-progress state a first-class, tested, dashboard-visible fact, rather than something tracked only in an engineer's memory or a stale spreadsheet, precisely so a topic accidentally left in a half-migrated state can't quietly persist unnoticed the way the too-narrow schema incident nearly did.

Timeline: How the Bus Evolved

  • Initial version — the circular-import crash described in this article's own postmortem, and the direct-call architecture that made it possible in the first place.
  • First event bus — the EventEmitter-based implementation shown early in this article, unifying engine communication and eliminating the circular-import failure mode structurally.
  • Topic naming convention enforced — added once the number of ad hoc topic names, coined independently by different engine authors, started becoming genuinely hard to reason about without a shared structure.
  • The data-minimization audit and schema enforcement — the single most consequential addition described in this article, converting topic payloads from convention to enforced contract.
  • The too-narrow-schema near-incident and the coverage-verified rollout process — the fix to the fix, replacing traffic sampling with code-level coverage verification as the standard for locking down any remaining topic.
  • Present — the system described throughout this article, with the rollout dashboard and coverage-verification process now the standing method for onboarding any new topic from day one, not just a one-time migration exercise.

Why Decoupling and Privacy Turned Out to Be the Same Problem

It would be easy to read this article as covering two loosely related topics stitched together — an architectural pattern for avoiding circular dependencies, and a separate compliance story about data minimization — connected only because both happen to involve the same piece of infrastructure. That reading understates how directly the two are actually connected, and it's worth spelling out the connection explicitly rather than leaving it implicit across the article's structure. The entire reason the event bus exists is to let one engine's output become available to another without either engine needing a direct reference to the other — decoupling, in the purest architectural sense, is precisely the property that made the over-sharing incident possible in the first place. When engines called each other directly, in the platform's earliest, pre-bus design, an engine author writing a direct function call could see, immediately and concretely, exactly what data was crossing the boundary, because the call site and its arguments were visible in the same file the author was editing. The moment that direct call became an emitted event on a shared bus, the data crossing the boundary became, by design, invisible to the emitting engine's author at the point they wrote the code — they were publishing to an abstract topic, not handing a specific value to a specific known recipient, and that abstraction, which is exactly what makes decoupling valuable architecturally, is also exactly what makes over-sharing easy to introduce without anyone noticing.

This is not an argument against decoupling — the circular-import postmortem earlier in this article is more than sufficient evidence that the alternative is untenable at this platform's scale and rate of change. It is an argument that decoupling, done responsibly, cannot stop at "engines no longer call each other directly." It has to include an explicit, enforced answer to the question a direct call used to make visible for free: exactly what data is actually crossing this boundary, and who asked for it. The schema-enforcement mechanism described throughout this article is that explicit answer, rebuilt deliberately at the abstraction boundary the bus introduced, restoring the visibility a direct function call used to provide implicitly, in a form that scales to 34 independently-authored engines and a control plane that needs to reason about the whole system at once rather than one call site at a time.

What This Looks Like for a Smaller System

Following the same staged-adoption guidance given throughout this series for its other layers: a smaller team with a handful of components, not 34 engines, does not need the full registry-integrated topic-schema machinery described in this article to get the core benefit. The minimum viable version is simply the discipline itself, applied consistently from the first component onward — components communicate through named, narrow events rather than direct references to each other, and every event's payload shape is written down somewhere reviewable, even if that "somewhere" is initially just a shared type definition rather than a runtime-validated schema. The runtime validation, the coverage-verified rollout process, and the periodic data-minimization audit described in this article are all genuinely later-stage additions, worth building once real component count and real data sensitivity justify the additional engineering investment — but the underlying habit of treating "what crosses this boundary" as a question with a written, reviewable answer, rather than an implicit consequence of whatever object happens to get passed, is valuable from the very first two components a system decouples from each other, long before an audit like the one described in this article would ever be warranted.

A Closing Reflection on Invisible Infrastructure

The event bus is, by a wide margin, the least visible piece of infrastructure covered anywhere in this series. The registry has an admin dashboard. The control plane has trace spans and a concurrency-limiter queue depth metric someone checks during an incident. The explainability trace store has a discovery-export endpoint a legal team actively uses. The event bus, when it's working correctly, produces no artifact anyone deliberately looks at during ordinary operation — its entire value is realized silently, every time an engine's output reaches a subscriber without either engine's author needing to think about the other, and every time that realization happens correctly, nobody notices anything at all, because nothing went wrong. This is, in one sense, the mark of good infrastructure: the parts that work invisibly are the parts doing their job. It is also, as this article's own central incident demonstrates, exactly why a genuine problem — data quietly over-sharing across a boundary nobody was watching — went undetected for as long as it did. Infrastructure that succeeds by being invisible during normal operation needs a different discipline than infrastructure that surfaces its own problems loudly: not less scrutiny, but periodic, deliberate, structured scrutiny substituting for the passive scrutiny a more visible system would receive automatically, just by being looked at regularly in the ordinary course of using it.

Design Rationale: A Short Dialogue

"Why not just have the control plane pass every engine's output to every other engine automatically, instead of requiring explicit subscriptions?"

This was considered during the bus's original design and rejected for a reason that connects directly to this article's central data-minimization concern: automatically broadcasting every engine's full output to every other engine, regardless of whether any given engine actually needs it, is the over-sharing problem this article documents, just built into the architecture's default behavior from the start rather than accumulated incrementally over time through many individually reasonable decisions. Requiring an explicit, reviewed subscription for every piece of inter-engine communication means the default state of the system is minimal, and any expansion of what data flows where is a deliberate, visible choice someone has to justify — the opposite default from automatic broadcast, and the correct one for a system whose data-handling choices are subject to the kind of scrutiny this article's audit represents.

"Could the topic schema validation happen at subscribe time instead of publish time, checking that a subscriber only receives fields it declared it needs?"

This is a genuinely interesting alternative enforcement point, and a version of it was discussed during the schema-enforcement design phase: rather than (or in addition to) restricting what a publisher can emit, restrict what a subscriber can read from a payload it receives, filtering down to only the fields that subscriber's own registration metadata claims to use. The team's eventual decision was to enforce at publish time as the primary mechanism, with subscribe-time filtering treated as a defense-in-depth addition rather than a replacement — publish-time enforcement stops over-broad data from ever entering the bus's log and observability surface at all, which is a stronger, earlier guarantee than filtering it after the fact for each individual subscriber while still letting an over-broad payload exist in the bus's internal state and log where a future audit, or a bug, could still expose it.

"What would it take to move to subscribe-time filtering as well, not just publish-time enforcement?"

Primarily a metadata-completeness question: subscribe-time filtering requires every subscriber to declare, per topic, exactly which fields of that topic's schema it actually reads — a finer-grained declaration than the current subscribesTopics list, which names topics but not specific fields within them. This has been proposed as a future hardening measure, mirroring the same "build it once evidence justifies it" discipline referenced throughout this series, and remains unbuilt not because it lacks merit but because the publish-time enforcement already in place has, so far, been sufficient to prevent a recurrence of this article's opening incident without the additional metadata burden a field-level subscription declaration would place on every engine author.

Reference: The Full Topic Registration Contract

Consolidating the schema and registration pattern shown piecemeal across this article into a single, complete reference for an engine author's actual first encounter with this system. Every topic an engine emits to requires, at minimum, a unique dotted name following the domain.entity.event convention enforced by lint, a Zod object schema with no wildcard or catch-all fields, and a documented justification — recorded in the same registration review the registry article describes for engine metadata generally — for every field included in that schema, answering the specific question the data-minimization audit asks retroactively: which subscriber, doing what, actually needs this field. An engine that only subscribes to existing topics, introducing none of its own, has a correspondingly lighter registration burden — declaring subscribesTopics alone, with the same per-field usage justification the code-review checklist earlier in this article requires, tied to the specific logic in that engine's own score() function that actually reads each declared field.

Interview: A Few More Questions on Decoupling and Data Flow

"Has removing a field from an already-locked-down schema ever broken something, the way the too-narrow-schema incident nearly did in the other direction?"

Once, caught before reaching production, during the coverage-verification process itself: an engine's test suite was found, on closer inspection, to be asserting against a mocked payload shape rather than actually exercising a live subscription to the real topic, which meant its apparent test coverage for a specific field was not real evidence that field was in genuine, current use. The fix generalized the coverage-verification tooling to require an integration-level test — a real subscription receiving a real, schema-validated event — not merely a unit test against a hand-constructed mock, closing the gap between "the test suite claims this field is used" and "this field is actually read by running code," the identical distinction the control-plane article's own integration-tier testing draws for a different but structurally similar reason.

"Is there a limit to how many topics this platform can reasonably support before the naming convention itself becomes unwieldy?"

No hard limit has been reached or is anticipated soon, but the same category of concern the registry article raises about very large dependency lists applies loosely here: a very large number of narrowly-scoped topics is, on balance, preferable to a smaller number of broad, multi-purpose topics, because narrow topics are what makes minimal, justified schemas possible in the first place — a topic trying to serve many different subscribers' different needs would face constant pressure to widen its schema to accommodate each new need, precisely the pressure that produced this article's opening incident in the first place, just concentrated onto fewer, more heavily loaded topics rather than distributed. Topic proliferation, in other words, is treated as a healthy sign the naming convention and minimal-schema discipline are both working as intended, not a problem to consolidate away.

How This System Was Explained to Non-Engineering Stakeholders

Once the data-minimization audit's findings and the schema-enforcement fix were both complete, the platform's compliance and client-facing teams needed a way to describe what had changed to stakeholders who would never read a line of the code shown throughout this article — a client's own data protection officer, asking a routine vendor-assessment question about internal data flows, or an internal compliance reviewer preparing materials for the kind of third-party audit the explainability-traces article describes this platform undergoing periodically. The explanation that emerged, refined over several such conversations, deliberately avoids the architectural vocabulary this article uses throughout and instead describes the system in terms of the guarantee it provides: every internal channel through which one part of the system shares behavioral data with another part has a written, reviewed definition of exactly what may be shared over it, enforced automatically, with any expansion of that definition requiring the same review a new use of personal data would require anywhere else in the platform. That framing, stripped of implementation detail, is both accurate and sufficient for the audience it's aimed at — a data protection officer does not need to understand Node's EventEmitter or Zod schema validation to correctly assess whether the underlying control is real and enforced, only that it is, and that framing is what lets a technical fix like this one actually satisfy a non-technical stakeholder's real question rather than requiring them to trust an engineering team's unverifiable assurance.

This translation exercise — finding language that accurately represents a technical guarantee to an audience that will never verify the implementation directly — recurs constantly across this series, most visibly in the explainability-traces article's own discovery-export format, and it is worth naming as a distinct, ongoing engineering responsibility rather than treating it as purely a communications or legal function separate from the actual system design. A control that engineers understand deeply but cannot explain accurately and concisely to the people who need to rely on it without reading source code is, in practice, a weaker control than one that can be explained that way, because the second kind can actually be assessed, questioned, and trusted by exactly the people whose trust the platform depends on.

What Happens When Two Engines Genuinely Need Bidirectional Communication

A recurring design question from engine authors, distinct from the circular-import failure mode this article's postmortem covers: what is the correct pattern when two engines genuinely, legitimately need to react to each other's output — not the accidental, unplanned coupling the postmortem incident represents, but a deliberate, reviewed relationship where engine A's output should influence engine B, and engine B's output should, in turn, influence a later re-evaluation relevant to engine A. The bus's batch-ordered publication model, inherited directly from the control-plane article's dependency-sorted execution, makes true same-batch bidirectional influence structurally impossible by design — an engine in batch N can only ever see published output from batches before it, never from its own batch or a later one, which is precisely the property that makes the registry's cycle-detection guarantee meaningful in the first place.

The sanctioned pattern for genuine bidirectional need is not a same-batch loop but a two-pass structure across two separate, sequential pipeline concerns: engine A publishes in an early batch, engine B consumes it and publishes its own reaction in a later batch, and if engine A's logic genuinely needs to account for engine B's reaction, that accounting happens not within the same request's execution but as an input to engine A's model or ontology-level configuration, updated on a slower cadence — the same category of solution the confidence-propagation article's modelStability rolling figure represents, a slow, batch-computed adjustment rather than a same-request feedback loop. This is a real constraint, not a limitation nobody noticed: it means the platform cannot express two engines negotiating with each other synchronously within a single request, and every engine author proposing a design that seems to need this is, in practice, being redirected toward either a longer-timescale feedback mechanism or a reconsideration of whether the two engines' logic should actually be merged into one, since a genuine synchronous negotiation requirement is often, on closer inspection, evidence the two "engines" are really one coherent unit of computation that was split apart prematurely.

A Longer Look at Why Traffic Sampling Missed the Too-Narrow Schema

It's worth dwelling further on the too-narrow-schema near-incident described earlier, because the specific reason traffic sampling failed to catch it is instructive beyond this one case. The subscriber's use of the matter-type field the sampling window missed was gated behind a conditional that only executed for a specific, uncommon matter type — a real, legitimate code path, exercised in production, but at a low enough frequency that a sampled traffic window, even a reasonably large one, had a meaningful chance of simply not containing an example of it. This is a general property of traffic-sampling-based analysis that any team relying on a similar technique should internalize: sampling reliably catches common patterns and reliably, structurally, is prone to missing rare-but-real ones, precisely because rarity and sample coverage are in direct tension. A field used in 90% of requests will show up in almost any reasonably-sized sample; a field used in 0.5% of requests might not appear in a sample an order of magnitude larger than the one that actually gets used in practice, purely due to chance.

This is exactly why the fix wasn't simply "take a bigger sample" — a bigger sample reduces but never eliminates the risk for a sufficiently rare code path, and there is no sample size a team can point to with confidence and say "this is definitely big enough" without independently verifying it against the actual code, which is what the coverage-verification approach does directly rather than probabilistically. The lesson generalizes past this specific incident to any situation where a team is tempted to infer real-world usage from sampled observation rather than from the code itself: sampling is a reasonable first-pass tool for finding the common cases quickly, but a genuine safety guarantee, for a system where a false negative carries real cost, needs to be grounded in something that doesn't degrade silently for rare inputs the way statistical sampling structurally does.

The Relationship Between This Article and the Shared Behavioral Ontology

Several sections throughout this series, including this article's own discussion of comparable-dimension pairings feeding contradictionFactor, reference the shared behavioral ontology as the mechanism that determines which engines' outputs are considered related to each other. It's worth being precise here about a distinction that's easy to blur: the event bus and the shared ontology solve genuinely different problems, even though they interact closely. The bus is a mechanism — it moves a validated payload from a publisher to whichever subscribers have declared interest, with no opinion about what any of that data means or how it relates to any other engine's data. The ontology is a vocabulary — it defines which concepts different engines' outputs actually refer to, and which pairs of engines are measuring genuinely comparable things, information the bus itself has no way to know or express on its own. A topic schema, enforced by the bus, defines the shape of what crosses a boundary; the ontology defines the meaning of what crosses it. Both are necessary, and conflating them — for instance, trying to encode ontological relatedness directly into a topic's schema definition, rather than keeping that a separate, ontology-owned concern — has been explicitly avoided in this platform's design specifically to keep each system's responsibility narrow and independently reasoned-about, the same single-responsibility discipline this entire series applies to every layer it describes.

Onboarding Checklist for New Contributors to the Bus's Own Codebase

Distinct from the "what a new engine author needs to know" section earlier in this article, which addresses someone using the bus, this section addresses someone modifying the bus's own implementation — the BehavioralEventBus class, the schema-validation logic, or the rollout tooling itself. A new contributor to this specific part of the codebase should first read the circular-import postmortem in full, since it is the foundational reason this system exists in its current decoupled form at all, and any proposed change that would reintroduce even a narrow, well-intentioned exception to the no-direct-import rule should be evaluated against that incident's specific cost before being seriously considered. Second, read the data-minimization audit's methodology closely enough to be able to explain, unprompted, why traffic sampling was judged insufficient and coverage verification was adopted in its place — this is the single most consequential design decision in the schema-enforcement system's history, and understanding why it was made prevents a well-intentioned future contributor from proposing a regression back to sampling-based validation in the name of simplicity. Third, and finally, understand that every change to the schema-validation logic itself is treated with the same elevated review bar the control-plane article applies to its own pipeline-runner changes, since a bug here has platform-wide blast radius across every engine's inter-communication, not a scope contained to one engine's own behavior.

A Note on What "Behavioral" Means in This Bus's Name

The bus's own class name, BehavioralEventBus, and this article's title both use "behavioral" as a qualifier worth explaining rather than treating as purely a branding choice inherited from the platform's own name. The bus is not, in fact, a general-purpose messaging system available for arbitrary use elsewhere in the platform's broader codebase — it is scoped, deliberately and by convention, to carrying communication specifically between the 34 (soon 100+) behavioral scoring engines this series describes, and nowhere else. A separate, unrelated part of the platform needing internal pub/sub for an unrelated purpose — coordinating between microservices in the audio-conversion pipeline referenced elsewhere in this broader documentation set, for instance — uses its own, independently designed messaging mechanism, not this bus, even though both are conceptually "event buses" in the generic software-architecture sense. Keeping the behavioral event bus scoped narrowly to its actual domain, rather than generalizing it into shared, platform-wide messaging infrastructure the moment a second use case for pub/sub arose elsewhere, was a deliberate choice: a shared, general-purpose bus serving unrelated domains would need to satisfy the requirements of every domain using it simultaneously, diluting the specific, carefully-reasoned data-minimization and schema-enforcement discipline this article describes, which was designed and reviewed specifically for behavioral scoring data and its specific compliance stakes, not as a generic capability meant to serve every future need equally well.

Closing the Loop: What Every Engine Author Now Signs Up For

Pulling together everything this article has covered into the single, concrete commitment an engine author makes the moment they register a new engine or a new topic on this platform: every field they choose to include in an emitted event's payload is a field they are asserting, and will be asked to justify on record, some specific subscriber genuinely needs — not a field that might someday be convenient, not a field left over from an earlier version of the engine's own logic, and not a field included because narrowing the schema later felt like it could always be deferred. That commitment did not exist in the platform's early history, when the bus was purely an architectural fix for the circular-import problem and nobody had yet examined what was actually flowing across it. It exists now because someone eventually did examine it, found a real, accumulated gap between intention and practice, and built the tooling to make that gap structurally difficult to reopen. Every article in this series has, in its own domain, told some version of this same story — a system that worked, technically, for a long time before anyone checked whether it was living up to the standard its own architecture implied it should meet.

How the Sales and Legal Product Adapters Experienced This Change Differently

The schema-enforcement rollout, described earlier as engine-by-engine, also had a noticeably different practical texture depending on which product adapter's engines were being migrated first, and the contrast is worth documenting because it illustrates something about how the same underlying fix lands differently depending on a domain's existing data-handling maturity. The chatbot platform's sales-domain engines, migrated relatively early in the rollout, required comparatively little schema tightening — their payloads had already stayed close to minimal by accident, largely because the sales-domain engines were newer, written after the platform's general engineering culture had already begun tightening around data-minimization concerns even before this specific audit formalized the requirement. The professional services legal SaaS platform's engines, several of which were among the platform's oldest, required the most substantial schema revisions, including the legal-risk-posture engine referenced in the registry article's own opening incident, whose event payloads had accumulated the largest gap between what was emitted and what was actually read, a direct consequence of that engine's longer history and the larger number of incremental, individually-reasonable additions accumulated over more time.

This pattern — older systems carrying more accumulated, unexamined data-sharing debt than newer ones — is not specific to this platform or this audit, and naming it here is meant as a general observation as much as a specific finding: any system that has been extended incrementally over a long period, by many contributors, without a periodic, deliberate re-examination of what's actually flowing through its shared interfaces, should expect to find a meaningfully larger gap between "what we emit" and "what anyone reads" than a comparable, more recently built system would show. Age and accumulated change, not carelessness on any individual contributor's part, is the primary driver of this kind of drift, which is itself the argument for treating the periodic audit described in this article as a standing practice applied uniformly across old and new systems alike, rather than a one-time cleanup exercise assumed to keep newer systems clean indefinitely just because they started that way.

What a Compliance Reviewer Should Actually Ask About This System

Mirroring the practical, reviewer-facing checklists the confidence-propagation and explainability-traces articles both close on: a reviewer evaluating whether this platform's internal messaging genuinely satisfies a data-minimization obligation, rather than merely claiming to, should ask to see the schema-enforcement mechanism's actual rejection behavior demonstrated live — request that an engineer attempt to publish a payload with an extra, undeclared field to a locked-down topic, and confirm it is actually rejected, not just described as being rejected in documentation. A reviewer should ask how many of the platform's registered topics are currently enforced versus still pending, per the rollout dashboard referenced earlier in this article, since a platform with most topics still unenforced has a materially different actual risk posture than one with full coverage, regardless of how mature the enforcement mechanism itself is. And a reviewer should ask specifically about the coverage-verification process that replaced traffic sampling, and why — the near-incident this article documents is exactly the kind of concrete, specific answer that demonstrates genuine engineering rigor behind a compliance claim, as opposed to a policy that sounds reasonable in the abstract but has never actually been tested against a real edge case the way this platform's own history shows this one has.

A Final Worked Comparison: Before and After the Fix

Concretely, side by side: before the schema-enforcement fix, a call to eventBus.emit('confidence.updated', payload) succeeded for any object shape at all — a well-intentioned engine author could include an entire raw signals object, a debug field, a verbose internal state dump, and the bus would accept, log, and broadcast every byte of it to every subscriber, with nothing in the system flagging that this had happened or that it differed from what a minimal, purposeful payload would look like. After the fix, the identical call, made against a locked-down topic, is validated field by field against a schema that was itself reviewed and justified against real, coverage-verified subscriber usage — an engine author attempting to include an extra field receives an immediate, loud EventSchemaViolationError at development time, long before that payload could ever reach a production bus, let alone accumulate silently for months the way this article's opening incident's over-sharing did. The difference between those two states is not a difference in engineering sophistication for its own sake. It is the difference between a data-minimization principle stated as an intention and one enforced as a structural property of the code itself — exactly the distinction this entire series keeps returning to, in a new form, in nearly every article.

How the Rollout Dashboard Itself Was Designed

Given how much weight this article places on the rollout dashboard as the mechanism that made the migration's in-progress state genuinely visible rather than tracked informally, it's worth describing what that dashboard actually shows and why each element was chosen deliberately rather than assembled as an afterthought. The dashboard's primary view lists every registered topic with a single, unambiguous status indicator — enforced, pending coverage verification, or pending schema authorship — rather than a more granular percentage-complete figure that early drafts of the dashboard used and that reviewers consistently found harder to act on. A percentage figure invites the question "is 80% good enough to consider this done," which is exactly the wrong question for a system where the entire value of the fix depends on every topic reaching full enforcement, not most of them; a three-state status per topic, with no aggregate summary presented as more important than the underlying list, keeps the focus on the concrete, actionable question of which specific topics still need attention rather than an abstract sense of overall progress that could plausibly be treated as good enough before it actually was.

The second design decision worth noting: the dashboard is queried directly against the same registry metadata and coverage-verification results the enforcement mechanism itself uses at runtime, rather than being a separately maintained tracking document someone updates by hand. This was a deliberate choice made after the near-incident with the too-narrow schema, which was caught partly because the coverage-verification tooling itself flagged a gap, not because a hand-maintained spreadsheet happened to be up to date at the right moment — a dashboard that could drift out of sync with the actual enforcement state would reintroduce exactly the kind of gap between "believed to be true" and "actually true" this entire fix exists to close, just relocated from topic schemas to the tracking of topic schemas.

What Happens to a Topic That's No Longer Needed

Mirroring the registry article's own engine-deprecation process, a topic that no subscriber reads anymore — because the subscribing engine was itself deprecated, or because a refactor removed the last reader — is not simply left in place indefinitely. The same reverse-dependency reasoning the registry article applies to engine deprecation applies here: before a topic can be marked deprecated, the platform's tooling confirms zero current subscribers via the identical registry metadata the schema-enforcement and coverage-verification systems already maintain, and only then does the publishing engine stop being required to maintain that topic's schema, with the topic itself marked deprecated rather than deleted outright, preserving its definition in the historical record for exactly the same reason the registry article insists deprecated engines' metadata is retained rather than removed — a future audit or investigation examining historical trace or log data may still need to know what a since-retired topic's schema once meant, even after nothing currently publishes to it.

This deprecation path is, in practice, rarely exercised — most topics, once introduced, tend to remain in active use for the life of the engines that created them, since removing an engine's only meaningful communication channel typically coincides with deprecating the engine itself rather than happening independently. But the path exists and is tested, not merely theoretical, specifically because "we never actually tested what happens when a topic is deprecated" is exactly the kind of untested edge case the too-narrow-schema near-incident demonstrates can hide a real gap until the specific, rare moment it actually matters.

A Broader Reflection on Invisible Coupling

Stepping back from the specific mechanisms this article documents, the underlying pattern connecting the circular-import postmortem and the data-minimization audit is the same one, viewed from two different angles: both are stories about coupling that existed in the system's actual behavior long before anyone had a clear, structural way to see it. The circular import existed, undetected, across two separate pull requests reviewed by two separate engineers, because nothing made the emerging cycle visible until the process actually failed to start. The over-sharing existed, undetected, across months of individually reasonable engine changes, because nothing made the growing gap between emitted and consumed data visible until someone deliberately went looking for it. In both cases, the underlying problem was not a single bad decision by a careless engineer — it was the absence of a mechanism that would have made an accumulating problem visible before it became a crisis or required a dedicated audit to surface. Every fix described in this article, from the bus's original decoupling architecture to the schema-enforcement mechanism to the coverage-verified rollout process, is, in this light, the same kind of intervention applied twice: not fixing one specific instance of coupling, but building a structural mechanism that makes the next instance of that class of problem visible automatically, before it needs its own dedicated incident or audit to be found.

How This System Handles Engines That Emit Rarely

A category of engine worth addressing specifically, distinct from the high-frequency engines most of this article's examples draw from: a small number of registered engines emit to their topics only under narrow, infrequent conditions — an engine detecting an unusual, rare behavioral pattern that most requests never trigger, for instance. These engines pose a specific challenge for the coverage-verification process described throughout this article, because a subscriber's own test suite exercising every declared field requires deliberately constructing a test scenario that triggers the rare condition, rather than simply sampling real traffic and hoping the rare case appears naturally, which is exactly the failure mode traffic sampling already demonstrated for the too-narrow-schema incident. The platform's standing requirement for this category of engine is stricter, not looser, than the general case: a rarely-emitting engine's topic schema cannot be locked down until its subscribers' test suites include an explicit, deliberately-constructed fixture triggering the rare condition, reviewed specifically to confirm it represents a realistic instance of the actual rare scenario rather than an artificial shape that happens to satisfy the schema without meaningfully testing real usage.

This stricter bar for rare-emission topics is a direct, learned response to the general lesson the too-narrow-schema incident already taught about sampling's blind spot for infrequent code paths — rather than waiting for a second incident specific to genuinely rare engines to teach the same lesson a second time, the coverage-verification process was extended proactively to treat emission frequency itself as a risk factor the review process explicitly accounts for, asking not just "is this field covered" but "how confident are we that this coverage reflects a realistic frequency of the condition it's meant to test," a question ordinary unit-test coverage metrics don't naturally surface on their own.

The Cost of This System, Stated Plainly

Following the same honest cost-accounting the confidence-propagation and control-plane articles both apply to their own systems: building the schema-enforcement mechanism, migrating all 34 engines' existing topics through the coverage-verified rollout process, and building the rollout dashboard represented a genuinely substantial, multi-month engineering investment — not a quick patch, and not something a smaller team should expect to replicate in a single sprint. The ongoing cost is more modest: every new topic an engine author introduces now requires writing and justifying a schema before it can ship, a real but bounded addition to the normal work of building a new engine, and the periodic re-audit of actual usage against declared schemas (recommended, though not currently mandated on a fixed cadence, unlike the confidence-propagation article's monthly correlation review) adds occasional, scheduled review time rather than continuous overhead. Weighed against the alternative — the specific, real cost of the data-minimization gap this article's opening incident describes, which included not just the engineering cost of investigating and fixing it but the harder-to-quantify cost of the exposure window during which the gap existed undetected — the team's own retrospective assessment, consistent with how every other system in this series evaluates its own return on investment, is that the upfront and ongoing cost was clearly justified, though the team is explicit that this judgment rests on the specific severity of what the audit found, not an assumption that this kind of investment is automatically worthwhile for every system regardless of its actual risk profile.

What a Smaller Team's Version of This Audit Should Look Like

For a reader without 34 engines and a dedicated platform team, but with a similar decoupled-communication architecture and a similar reason to care about what data crosses its internal boundaries, the audit methodology described in this article scales down more easily than the full enforcement infrastructure does. The core technique — sample real payloads, redact identifying values, catalogue every field present, independently trace which fields subscribing code actually reads, and compare the two lists — requires no special tooling beyond a script and a few hours of careful manual cross-referencing for a system with a handful of components, and it is worth doing at least once, early, specifically because the pattern this article documents (individually reasonable decisions accumulating into a systemic gap) is not specific to a 34-engine platform; it is a property of any system that has grown incrementally, by more than one contributor, over more than a few months. The schema-enforcement and coverage-verified rollout infrastructure described later in this article are the scaled-up, ongoing-prevention mechanisms worth building once the initial audit demonstrates a real, recurring gap — but the audit itself, as a one-time diagnostic exercise, is valuable and inexpensive enough to run well before a team is large enough to justify the full infrastructure this article describes building in response to what its own audit found.

How the Ontology's Own Comparable-Dimension Reviews Changed After This Audit

An indirect but real consequence of the data-minimization audit, worth documenting because it illustrates how a fix in one part of this platform's architecture surfaced a related gap in another: the process for reviewing which engines' outputs the shared ontology declares as comparable, feeding the confidence-propagation article's contradictionFactor computation, had, before this audit, relied partly on informally checking what fields a proposed comparable-dimension pairing would need to exchange over the bus — an ad hoc, conversational check rather than a systematic one. Once the schema-enforcement mechanism existed and made every topic's actual data contract explicit and machine-readable, the ontology review process was updated to require checking a proposed comparable-dimension pairing directly against the actual, current topic schemas involved, rather than against an engineer's informal understanding of what those schemas contained, which could easily have drifted out of date between when a reviewer last looked closely at a given topic and when a new pairing proposal came up for review.

This is a small process change, but it exemplifies a pattern worth naming explicitly: once a system's data contracts become explicit, machine-readable, and enforced rather than informally understood, every other process that depends on understanding those contracts correctly — not just the enforcement mechanism's own primary use case — gets more reliable as a direct, often unplanned side effect. The ontology review process was not the target of the schema-enforcement fix described throughout this article; it became more rigorous anyway, simply because the information it needed to reason about correctly became structurally available in a form it could depend on, rather than requiring separate, parallel verification that could silently drift out of sync.

A Closing Case Study: Tracing One Field's Full Lifecycle

To make the entire argument of this article concrete one final time, it's worth tracing a single field — the raw signals object the opening incident found being over-shared on confidence.updated — through its complete lifecycle, from its introduction to its removal, as a single, connected narrative rather than the several separate sections this article has otherwise split the story across. The field was added early in the bayesian-confidence engine's development, by an engineer who reasoned, in isolation, that including the full input alongside the computed output would give any future subscriber maximum flexibility without requiring a schema change later if a new subscriber turned out to need a field not yet included — a locally reasonable engineering instinct, optimizing for flexibility in the absence of any policy suggesting otherwise. Over the following months, two engines subscribed to confidence.updated, both reading only the score and confidence fields specifically, never touching the broader signals object either engine's author had access to but had no actual use for. The field sat, unused by any real subscriber, fully present in every single event published on this topic, for the entire period between its introduction and the data-minimization audit that eventually found it.

When the audit's methodology — sampling real traffic, cataloguing every present field, independently tracing actual subscriber reads — reached this specific topic, the gap was immediate and unambiguous: a field present in one hundred percent of sampled payloads, read by zero percent of subscribing code. The fix, once identified, was equally unambiguous and fast to implement — the field was removed from the topic's newly-authored schema, the bayesian-confidence engine's publish call was updated to stop including it, and the topic was locked down under the enforcement mechanism this article describes in detail, closing off the possibility of a similar field being silently reintroduced by any future change to that engine. The entire lifecycle, from a single reasonable decision to a systemic, unnoticed gap to a permanent, structural fix, took place over roughly a year and a half — long enough that no single person involved at every stage necessarily remembered the original reasoning behind the field's inclusion by the time the audit found it, which is itself part of the lesson: a gap like this does not require sustained bad judgment to persist. It requires only the absence of a mechanism that would have caught it sooner, and the ordinary, unremarkable passage of time.

How Engine Authors Reacted to the New Requirements

It's worth being candid about the human side of this rollout, not just its technical mechanics, because the reaction from engine authors asked to write and justify schemas for topics they had previously treated as an implementation detail was not universally enthusiastic at first. The most common objection, raised in several early rollout discussions, was that writing a precise schema and justifying each field felt like unnecessary process overhead for something that had, after all, never caused a production incident on its own — the over-sharing was a latent risk, not an observed failure the way the circular-import crash had been, and latent risks are, by their nature, harder to build organizational urgency around than a failure everyone already remembers vividly. The counterargument that ultimately won broad buy-in was not an appeal to abstract compliance principle but the concrete field-lifecycle case study described in the preceding section, presented in an early rollout meeting essentially as written here: a specific, traceable field, present in every event for a year and a half, read by nobody, discovered only because someone finally looked. Making the risk concrete, with real numbers and a real timeline, rather than leaving it as an abstract data-minimization principle, is what shifted the conversation from "is this worth the effort" to "how quickly can we get through the remaining topics."

This is consistent with a broader pattern observable across every incident and process change documented throughout this series: abstract risk is a weak motivator for engineering investment compared to a specific, traceable, well-documented instance of that risk having actually materialized, even in latent, non-catastrophic form. The postmortems, case studies, and worked examples that make up a large fraction of every article in this series are not included purely for pedagogical clarity — they reflect the actual internal artifacts that moved engineering priority and organizational buy-in at the time, and their inclusion here is meant to preserve that same persuasive concreteness for a reader evaluating whether a similar investment is worth making in their own system.

A Technical Note on Schema Versioning for Topics

One detail glossed over in the schema-enforcement code sample shown earlier in this article deserves its own treatment: what happens when a topic's schema itself needs to change, not just be authored for the first time. Unlike an engine's own version field, which changes frequently as scoring logic evolves (registry article), a topic's schema is deliberately treated as a much more stable, rarely-changing contract — narrowing a schema (removing an unused field, once coverage-verified as safe) is common and encouraged, per the code-review checklist earlier in this article, but widening one is rare enough that the platform does not currently version topic schemas the way it versions engines, on the reasoning that a topic whose contract needs to change frequently is itself a signal the topic's scope was drawn incorrectly in the first place and likely needs to be split into two more narrowly-defined topics rather than accommodated with a growing, versioned schema.

This is a deliberate design stance, not an oversight: engine versioning exists because scoring logic genuinely, legitimately evolves on an ongoing basis as engines are tuned and improved, and the registry and explainability-traces articles both build substantial infrastructure around that expectation. Topic schemas are held to a different standard specifically because the entire value of the data-minimization discipline this article describes depends on a schema being a stable, trustworthy statement of "this is what may cross this boundary," and a schema that changed as often as engine logic does would erode the confidence any given review of it could provide — by the time a reviewer finished evaluating whether a schema's scope was appropriate, a fast-changing schema might already have moved on to a different shape, undermining the entire premise that review is meaningful. If a genuine, recurring need for topic schema versioning does eventually emerge, the team's stated position, consistent with the "build it when evidence justifies it" discipline referenced throughout this series, is that it would be built as a deliberate addition at that point, not preemptively.

What This Article's Story Looks Like From the Registry's Perspective

Every article in this series has, to some degree, described the same underlying platform from a different vantage point, and it's worth being explicit about how this article's own central story — decoupling preventing a crash, then schema enforcement preventing over-sharing — actually registers in the registry's own metadata, since a reader who has followed this series from its first article will already have the vocabulary to see the connection directly. Every topic this article describes is, from the registry's point of view, simply two more fields on an engine's registration object: emitsTopics and subscribesTopics, sitting alongside dependencies, riskLevel, and every other piece of metadata the registry article describes validating at startup. The registry itself does not know or care what a topic's schema contains — that knowledge lives entirely within this article's own bus implementation — but the registry's dependency-graph validation, described in detail in that article, is what makes it possible to say with confidence that no engine's topic subscription creates a circular relationship, the exact guarantee whose absence produced this article's own opening postmortem before the registry's cycle-detection logic existed to check for it automatically.

This layering — the registry knowing which engines communicate, without knowing what they communicate; the bus knowing what crosses each boundary, without knowing why any given engine cares — is a deliberate, narrow division of responsibility, the same single-responsibility principle named explicitly in this article's earlier discussion of the ontology's separate role. No single system in this platform's architecture is responsible for understanding the platform's behavior end to end; each layer understands its own narrow slice completely and trusts the layers above and below it to understand theirs, and it is only by reading multiple articles in this series together, as this article's own frequent cross-references encourage, that the platform's full behavior becomes visible to a human reader the way no single system component ever needs it to be visible to itself.

A Retrospective: What Would Be Done Differently, Knowing What Is Known Now

Asked directly, in the same reflective spirit every article in this series closes on: the honest answer about what would change if this system were designed again from scratch, with both incidents already known in advance, centers on timing rather than substance. The decoupled, event-bus architecture itself would very likely look identical — nothing about the circular-import postmortem suggests the underlying pattern was wrong, only that its enforcement mechanisms (the lint rule, the CI-level cycle detection) arrived after the incident that demonstrated their necessity rather than before it. Similarly, nothing about the schema-enforcement mechanism suggests the bus's original, unvalidated payload design was a mistake in principle — a schema-validation layer added from the very first topic, rather than retrofitted after an audit found a real gap, would have avoided the entire migration effort described throughout this article, at the cost of asking the platform's very first engine authors to write and justify precise schemas before any real subscriber usage existed yet to justify them against, which carries its own risk of guessing wrong in the opposite direction, writing schemas too narrow for needs nobody had yet discovered.

The team's own stated position, consistent with the retrospective judgment offered in the confidence-propagation and explainability-traces articles' own closing reflections, is that building schema enforcement reactively, once real usage patterns existed to base a well-justified schema on, was probably the right sequencing even in hindsight — the cost was a period of unenforced, unaudited data flow that turned out, on inspection, to be worse than anyone had assumed, but the alternative of enforcing from day one, before real usage existed to inform what a well-scoped schema should even contain, would have traded one kind of risk for another rather than eliminating risk outright. What would unambiguously be done differently, in hindsight, is the cadence of the audit itself: rather than a single, one-time review that happened to catch a year-and-a-half-old gap, a periodic, scheduled data-minimization audit — mirroring the confidence-propagation article's own monthly correlation review cadence — would have caught the same gap meaningfully sooner, closer to when it was introduced rather than after it had accumulated across the platform's full engine set.

How a New Product Adapter's Engines Get Onboarded to the Bus

Mirroring the identically-purposed sections in the registry and control-plane articles: when an entirely new product adapter is added to the platform, bringing with it a new set of domain-specific engines, those engines' event-bus integration follows the fully-enforced path from day one — unlike the platform's original 34 engines, which were migrated through the coverage-verified rollout process described throughout this article after the fact, any new engine registered today has never been able to emit to an unenforced, unschemaed topic at all, because the enforcement mechanism and the registration validators it depends on are now a permanent, non-optional part of the registration process itself, not an opt-in migration a new engine author could reasonably skip. This means the specific historical problem this article documents — accumulated, unexamined data sharing discovered only by a dedicated audit — is structurally prevented from recurring for any engine built after this system's completion, even though it remains, as an important caveat, entirely possible for a new engine author to write an overly broad schema that technically satisfies the enforcement mechanism's requirement for a declared, justified schema while still including more than is genuinely needed, if the review process approving that schema is not applied with real rigor.

That caveat is worth taking seriously rather than treating the enforcement mechanism as a complete, self-sufficient guarantee: schema enforcement prevents undeclared, unreviewed data from crossing a boundary, but it cannot, by itself, guarantee that every declared and reviewed schema is actually minimal — that judgment still depends on the quality of the review a new topic's schema receives at registration time, the same domain-expert involvement the confidence-propagation article insists on for engine field-weight review. The tooling closes off the failure mode of silent, unreviewed accumulation; it does not, and cannot, replace the human judgment required to correctly scope a schema in the first place. This is a genuine, acknowledged limit on what automated enforcement can guarantee, consistent with the honest-about-limitations posture the explainability-traces article takes toward its own immutability guarantee's actual boundary.

Frequently Asked Questions From Engineering Leadership

How would leadership explain, to a board or a major client, what specifically was fixed here?

The clearest available answer is the field-lifecycle case study earlier in this article, stated plainly: an internal review found that behavioral data was being shared more broadly between system components than any component actually needed, traced the gap to its root cause, fixed the specific instance found, and — more importantly — built an enforcement mechanism that makes the same class of gap structurally difficult to reintroduce for any new component built going forward. That is a materially stronger, more concrete answer than a general assurance that the platform "takes data minimization seriously," and it is the kind of answer this entire series consistently favors over unverifiable, aspirational claims.

Is there any risk this kind of internal audit itself creates new privacy exposure, given it involves inspecting real production data?

This was addressed directly in how the audit was conducted, as described earlier in this article: identifying values were redacted from the sampled traffic before any analysis took place, specifically so the review process investigating an existing exposure never introduced a new one of its own. This same discipline — minimize what the investigation itself collects and retains — is treated as a standing requirement for any future audit of this kind, not a one-time precaution specific to the first review.

Does this system's existence suggest the platform previously had a real, reportable data breach?

No — the gap this article documents was an internal, architectural over-sharing between the platform's own components, all operating within the platform's own security boundary and access controls; it was never data reaching an external party, a different tenant, or anyone without an existing legitimate basis to access it. It was closer to a compliance-hygiene gap than a security incident, which is precisely why it was found through a deliberate internal review rather than an external report or complaint — the distinction matters for accurately characterizing what this article describes to any external stakeholder asking about it.

What a New Engineer's First Week With This System Actually Looks Like

Concretely, rather than as an abstract onboarding checklist: a new engineer joining the team that maintains this part of the platform typically spends their first substantial task not writing new code, but reading through a handful of already-locked-down topic schemas alongside the subscriber code that justifies each field, tracing by hand the same kind of connection the original audit traced systematically — which field, used by which engine, for what specific purpose. This exercise, assigned deliberately rather than left to chance, is meant to build the same intuition the audit itself required: the ability to look at a payload and ask, concretely, "who actually needs this, and can I point to the code that proves it," rather than accepting a payload's shape as given simply because it currently exists and nothing has flagged it as a problem. Engineers who have done this exercise report, informally, that it changes how they write their own first new topic schema — noticeably narrower, on average, than a first attempt from an engineer who has only read this article's documentation without doing the hands-on tracing exercise, which is itself a small, informal but consistent piece of evidence that understanding a real, existing example concretely produces better instincts than understanding a written rule abstractly, a pattern worth noting for any team designing its own onboarding process around a similar system.

Closing Note on Naming: Why "Bus," Not "Queue" or "Broker"

A short terminology note, in the same spirit as the control-plane article's own naming discussion: "bus" was chosen deliberately over "queue" or "broker," both of which carry connotations this system's actual behavior doesn't match. A queue implies ordered, sequential consumption, often with a notion of a message being removed once consumed by a single consumer — neither property describes this system, where every subscriber to a topic receives every matching event, and nothing about the underlying EventEmitter primitive removes or consumes an event on behalf of other listeners. A broker implies a genuinely separate, intermediary service brokering communication between independent processes, typically across a network boundary — again, not this system's shape, which operates entirely within a single process's memory, with no intermediary service of its own. "Bus," borrowed from hardware architecture, where multiple components share a common communication channel without needing individual point-to-point connections between every pair, is the more accurate metaphor: every engine connects to the same shared channel, and the channel itself neither orders nor consumes on anyone's behalf, it simply carries whatever is placed on it to whoever has expressed interest. Precision in this kind of naming matters for the same reason precision matters everywhere else in this series — an engineer arriving with the wrong mental model, primed by a mismatched name, is more likely to make an incorrect assumption about the system's actual guarantees.

A Final Worked Trace: Following an Event Through the Full Series

Closing with a single event followed through every article in this series published so far, to make the full inter-article dependency chain concrete in one place rather than scattered across individual cross-references. bayesian-confidence, registered per the registry article's schema and validated at startup, runs within a batch the control-plane article's dependency-sorted execution plan assembled, and computes a confidence figure using the exact formula the confidence-propagation article describes in full. That output is published onto confidence.updated — this article's own subject — validated against its locked-down, minimal schema before ever reaching a subscriber, then simultaneously written into a permanent, immutable bc_explainability_trace row per the explainability-traces article's own schema, and broadcast to every engine that declared a genuine, justified subscription. One event; five articles' worth of infrastructure, each contributing one narrow, well-defined guarantee, none of which needs to know the details of any other's implementation to trust that its own piece of the chain will behave correctly. That composability — narrow, independently reasoned-about layers combining into a coherent whole no single layer needs to understand completely — is, more than any individual mechanism this series has described, the actual architectural achievement this entire series has been documenting one article at a time.

What Happens When the Bus Itself Needs to Change Its Core Behavior

Every code sample in this article treats the BehavioralEventBus class's core publish-and-broadcast behavior as fixed and stable, which it has been for most of this system's history — but it's worth addressing directly what governs a genuine change to that core behavior, distinct from the ordinary, frequent addition of new topics and schemas this article otherwise focuses on. A proposed change to the bus's own delivery semantics — for instance, an early proposal, ultimately not pursued, to make subscriber notification asynchronous rather than the current synchronous, in-process dispatch — goes through review at the same elevated bar the control-plane article applies to changes touching its own pipeline runner, for an identical reason: every one of the platform's 34 engines depends on this system's current behavior, often implicitly, and a change to core delivery semantics has platform-wide blast radius no single engine's own test suite would catch in isolation. The asynchronous-dispatch proposal specifically was set aside after analysis showed it would reintroduce exactly the kind of same-batch ordering ambiguity the control-plane article's per-batch publication guarantee was designed to eliminate — a downstream engine could, under an asynchronous dispatch model, begin executing before a same-batch sibling's publish had actually been delivered, reopening a race condition this platform's architecture had already deliberately closed off once, in a different form, during the control plane's own early history.

A Brief Comparison to How Other Platforms Solve This Same Problem

Readers familiar with event-driven architecture more broadly will recognize this platform's specific combination of choices — in-process delivery, schema-enforced topics, synchronous batch-ordered dispatch — as a considered position within a well-known design space, not a novel invention. Domain-driven design's domain-event pattern, popular in enterprise software architecture generally, shares this platform's core insight that components should communicate through named, meaningful events rather than direct calls; where this platform's implementation differs is in the specific, unusually strict enforcement of payload minimality this article documents, driven by this platform's specific regulatory exposure around behavioral and personal data, which a typical domain-events implementation in a less regulated domain would have less reason to build as rigorously. This is worth naming because it illustrates a point relevant to any reader considering how much of this article's specific machinery to adopt: the core pattern — decoupled, named-event communication — is broadly applicable and well-established; the specific severity of the schema-enforcement mechanism described in this article is a direct, proportionate response to this platform's specific data-sensitivity profile, and a system with genuinely lower stakes around what crosses its internal boundaries would reasonably adopt a lighter-weight version of the same underlying pattern.

Closing Thought

An event bus is, on its surface, one of the more mundane pieces of infrastructure a distributed or modular system can have — the pattern is decades old, well understood, and rarely the subject of dedicated architectural attention once it's built and working. This article exists at this length specifically because "built and working" turned out not to be the same thing as "carrying only what it should," and the gap between those two states persisted, invisibly, for longer than anyone would have guessed if asked in advance. The lesson this article leaves a reader with is not really about event buses specifically. It is that any piece of infrastructure whose entire value depends on being trusted implicitly — carrying data correctly, carrying only what it should, being invisible when it's working — deserves the same periodic, deliberate scrutiny a more visible, more actively-monitored system receives automatically, precisely because its invisibility is not evidence that it's behaving correctly. It is only evidence that nobody has recently checked.

Reference: Every Fix Described in This Article, in One Table

FixMotivated byPrevents
Event bus (decoupled pub/sub, replacing direct engine imports)The circular-import crash postmortemAny future circular-dependency crash between engines
Topic naming convention, lint-enforcedGrowing topic-name inconsistency as engine count increasedUnstructured, hard-to-reason-about topic names
Registry-level dependency cycle detection, run in CIThe same postmortem, extended to catch future cycles pre-mergeA circular-import-shaped change reaching production undetected
Topic payload schema enforcementThe data-minimization auditOver-broad, unreviewed data silently accumulating on the bus
Coverage-verified rollout (replacing traffic sampling)The too-narrow-schema near-incidentA schema incorrectly excluding a real, rarely-exercised field
Rollout dashboard, queried live against registry stateThe risk of the dashboard itself drifting out of sync with realityFalse confidence that migration is further along than it actually is

Every row in this table follows the same shape this entire series keeps returning to: a specific, dated problem, a specific, targeted fix, and a specific, named failure mode the fix exists to prevent going forward — not a general architectural improvement pursued for its own sake, but a direct, traceable response to something that actually happened or was actually found.

Appendix: Related Reading

A Deeper Look at Why the Original Design Didn't Anticipate This Gap

It's worth asking, directly, why the bus's original designers didn't build schema enforcement in from the start, given how central data-minimization concerns are elsewhere in this platform's broader architecture — the answer is less about oversight and more about the order in which different kinds of risk became visible to the team building this system. The bus's original design phase was focused almost entirely on solving the circular-import problem this article's postmortem documents, which was, at the time, an acute, recently-experienced production failure with an obvious, urgent fix. Data-minimization exposure, by contrast, is a latent risk — it does not announce itself with a crashed process the way a circular import does, and nothing about the bus's original design phase included a specific, triggering event that would have made over-sharing feel as urgent as the crash the team had just lived through. This is not offered as an excuse — the team's own retrospective, referenced earlier in this article, is explicit that a periodic audit from the very start would have been the better sequencing in hindsight — but it is offered as an honest account of why a team focused, correctly, on solving the problem directly in front of them did not simultaneously anticipate a different, less visible problem that hadn't yet manifested in any concrete way.

This pattern — acute, visible failures getting fixed quickly, latent, invisible risks persisting until a deliberate audit finds them — recurs across every article in this series, and naming it explicitly here, at the end of this article rather than only implicitly through the incidents themselves, is meant as a closing, generalizable observation for any reader building a comparable system: budget for periodic, deliberate audits of exactly the kind of risk that will never produce a crash or an obvious symptom on its own, specifically because the absence of a symptom is not evidence of the absence of a problem, only evidence that nobody has yet looked closely enough to find one.

One More Practical Note: How Long the Full Rollout Actually Took

For a reader trying to estimate how long a comparable migration might take in their own system, it's worth stating the actual, approximate timeline this platform's rollout followed, rather than leaving the reader to guess. From the audit's initial findings to the first topic being locked down under full schema enforcement took several weeks, dominated by the schema-authorship and review process for the platform's highest-traffic, most consequential topics first. The full migration across all of the platform's then-registered topics, using the coverage-verified process adopted partway through after the too-narrow-schema near-incident, took several months in total — slower than an aggressive, unverified cutover would have been, but deliberately so, given the near-incident's own demonstration of what an insufficiently rigorous rollout could cost. New topics registered since the migration's completion are enforced from their very first registration, with no equivalent rollout period required, since the enforcement mechanism and its review process are now simply a standard, permanent part of how any new topic comes into existence on this platform.

What Genuinely Surprised the Team During This Process

Closing with a candid, specific account of what genuinely surprised the people who built and ran this audit, since the more polished, retrospective narrative this article otherwise presents can understate how much of the real process involved discovering things nobody had predicted in advance. The single biggest surprise was not the magnitude of any individual topic's over-sharing, but how evenly distributed the gap was across nearly every topic examined rather than being concentrated in one or two obviously problematic engines — the team had expected, going in, that a small number of older or more hastily-built engines would account for most of the findings, and had budgeted review time accordingly. In practice, nearly every topic examined showed at least some gap between emitted and consumed fields, including topics belonging to engines built relatively recently, by engineers who considered themselves careful about scope. This forced a real, mid-audit recalibration of how the findings would be communicated internally — not as a story about a few engines needing cleanup, but as a story about a systemic pattern affecting the platform's entire topic surface roughly uniformly, which is part of why the eventual fix was built as platform-wide, structural enforcement rather than a targeted cleanup of the specific engines the initial hypothesis had expected to be the main offenders.

Last Word

The event bus this article describes exists, in its current, schema-enforced form, because a system built to solve one clearly visible problem — engines crashing each other through circular dependencies — quietly grew a second, invisible problem in the space that solution created, and because someone eventually looked closely enough to find it. Neither half of that story is complete without the other. Decoupling engines from direct calls was necessary and correct; it was also, by itself, insufficient to guarantee the platform was handling behavioral data as carefully as its architecture implied. The lesson worth carrying forward from this article, more than any single mechanism it documents, is that solving one problem well is not evidence the solution has no problems of its own — it is only evidence that the specific problem it was built to solve has been solved, and every new abstraction, however well-designed, deserves the same scrutiny eventually that the system it replaced originally received.

How This Article's Findings Were Communicated to Existing Clients

Because the platform's client contracts, referenced throughout this series, frequently include data-handling representations, the data-minimization audit's findings and subsequent fix required a deliberate decision about proactive client communication, distinct from the internal engineering and legal-review process described earlier in this article. The team's approach, developed jointly with the legal stakeholders who had by this point become standing participants in this kind of decision, was to communicate the finding and fix proactively to clients whose contracts included specific data-minimization representations, framed honestly: an internal review identified an architectural gap between declared data-handling intentions and actual implementation, the gap has been closed, and the enforcement mechanism now in place is described at the level of guarantee, not implementation detail, consistent with the translation discipline described earlier in this article for non-technical stakeholders. This proactive disclosure, made before any client had specifically asked, was itself a deliberate choice reflecting a broader posture this platform's compliance culture has adopted: treating an internal finding of this kind as something to disclose on the platform's own initiative, rather than waiting to be asked and risking the disclosure appearing reactive or reluctant if it later surfaced through a different channel, such as a routine vendor audit.

What a Product Manager Needs to Know About This System

Mirroring the identically-titled section in the confidence-propagation article: a product manager scoping a feature that involves a new engine, or new communication between two existing engines, needs to know that any new topic their feature requires will need a schema written and justified before it can ship, a real scheduling consideration to budget into a launch plan rather than an invisible backend detail. Unlike the confidence-propagation article's field-interpreter requirement, which scales roughly with the number of input fields an engine considers, a new topic's schema-authorship cost scales with how many distinct subscribers that topic will eventually need to serve, since the coverage-verification process requires each subscriber's actual usage to be demonstrated before the topic can be locked down — a feature introducing a topic with many planned subscribers should expect a correspondingly longer runway before that topic reaches full enforcement, and product planning that assumes instant, unconstrained inter-engine communication the moment a new engine ships will need to account for this reality rather than discovering it during a launch week.

A Note on Testing Discipline Across This Series

Consistent with every article preceding this one, the tests shown throughout this article are reproduced from the platform's actual test suite, not constructed as illustrative examples after the fact — a distinction worth restating here because it applies to the schema-enforcement tests, the coverage-verification tests, and the end-to-end worked example alike. This matters for a reader evaluating how seriously to take this article's claims: a test suite that actually runs in CI, gating every merge to the files this article describes, is meaningfully stronger evidence that the guarantees described here are real and maintained than a documentation page asserting the same guarantees without a corresponding, enforced test. Every "What to Watch For" list across this series, including the one closing this article, is best read as a distillation of lessons the actual test suite already encodes mechanically, not a separate, aspirational set of best practices trusted only to human discipline.

What This Article Assumes You Already Know

Placed sixth in this series, this article assumes familiarity with the registry's engine-metadata vocabulary and the control plane's batch-execution model, both referenced constantly throughout rather than re-explained — a reader arriving at this article without having read either predecessor will still follow the bus's own mechanics, but will miss why several of this article's specific design choices, particularly the per-batch publication timing and the registry's shared cycle-detection logic, are the direct, deliberate consequences of decisions made in those earlier articles rather than choices this article's own bus design made independently. This layered-assumption structure is deliberate across the whole series: each article adds one narrow piece of vocabulary and one narrow set of guarantees, and later articles lean on earlier ones freely, on the theory that a reader working through the series in order accumulates exactly the shared vocabulary each subsequent article needs, without any single article needing to re-explain the whole platform from scratch.

Postscript: What Happened to the Engineer Who Found the Gap

In the same spirit as the postscripts closing the confidence-propagation and explainability-traces articles: the reviewer who first noticed the disparity between emitted and consumed fields during the original data-minimization audit was not investigating the event bus specifically — the audit began as a broader, routine privacy review covering several unrelated systems, and the event bus's own findings were, by the reviewer's own account, unexpectedly the largest and most systemic of everything examined during that cycle. This is worth noting because it illustrates something about how gaps like this are actually found in practice: rarely by someone specifically suspecting a particular system, more often by someone applying a general, disciplined review methodology broadly and letting the findings, wherever they land, determine where deeper investigation is warranted. The specific person who found this gap was, by their own description, simply following the audit's standard methodology through its next scheduled system on the list — the significance of what they found was a property of the system being examined, not of any special suspicion directed at it in advance.

What Would Have to Change for This Article's Enforcement Model to Break

Following the same forward-looking discipline the rest of this series applies to its own scaling questions: the schema-enforcement mechanism's own cost is fixed per publish call, regardless of total engine or topic count, so it does not degrade as the platform grows toward its roadmap's 100+ engines. What genuinely would need rethinking at a meaningfully larger scale is the coverage-verification review process itself, which currently depends on a relatively small, centralized group of reviewers with enough cross-engine context to evaluate whether a proposed schema is genuinely minimal — a process that scales linearly with new-topic volume in a way the automated enforcement mechanism does not. At a scale where new topic proposals meaningfully outpaced this review capacity, the team's anticipated response, consistent with how the registry article addresses an analogous scaling question about its own override-review process, would be to formalize more of the review into automated, mechanical checks (confirming coverage-verification evidence exists and meets a minimum bar, for instance) while reserving human review specifically for the genuinely judgment-dependent question of whether a schema's scope is appropriate — automating the parts of review that are checklist-mechanical, while deliberately keeping a human in the loop for the parts that require real domain judgment, the same split the confidence-propagation article draws for its own domain-expert weight-review process.

A Final Reflection on Trust Between Independently-Authored Components

Underlying every mechanism this article documents is a question that applies well beyond this specific platform: how should independently-authored components, built by different people at different times with different immediate priorities, be allowed to trust each other's behavior. The circular-import postmortem shows what happens when that trust is implicit and unverified — two authors, each reasonable within their own change, produced an outcome neither intended because nothing forced their assumptions about the other's code to be made explicit and checked. The data-minimization audit shows a subtler version of the same failure: engine authors trusted, implicitly, that including extra data in a payload was harmless, because nothing forced that assumption to be examined against actual downstream usage until a dedicated review finally did. In both cases, the fix was the same shape: replace implicit, unverified trust with an explicit, machine-checked contract — a dependency declaration the registry validates, a topic schema the bus enforces — so that two independently-authored components can rely on each other correctly without either author needing to personally understand or trust the other's judgment, only the shared, verified contract between them. That substitution, trust in a person's judgment for trust in a verified contract, is, in miniature, the design philosophy running through every article in this series, and this article's own two incidents are simply the clearest, most concrete illustrations of what happens when that substitution hasn't yet been made.

What a Smaller Team Should Take Away, Distilled to One Sentence Each

For a reader who has followed this article's full length and wants the shortest possible restatement of its two central lessons, kept separate because they address different failure modes: never let independently-built components reference each other directly, because two individually reasonable changes can combine into a failure neither author could see coming; and never assume a shared communication channel's payload is minimal just because nothing has gone visibly wrong, because the gap between what's carried and what's actually needed accumulates silently, from many small, individually defensible decisions, until someone deliberately measures it. Neither lesson requires 34 engines or a dedicated compliance function to apply — both are relevant from the moment a second component needs to know something a first component computed, which is to say, from very early in almost any system's life.

A Closing Technical Detail: How the Bus Handles Listener Errors During Broadcast

One implementation detail worth making explicit, since it affects how the partial-batch-failure handling described in the control-plane article actually interacts with this bus: Node's EventEmitter, by default, propagates a synchronous throw from one listener in a way that can prevent subsequent listeners for the same event from being invoked at all, unless the emitting code specifically wraps each listener invocation in its own error boundary. The platform's BehavioralEventBus implementation, built on top of the base class shown earlier in this article, wraps every individual subscriber invocation during broadcast in its own try/catch, logging and isolating a single subscriber's failure without preventing any other subscriber from receiving the same event — the identical per-listener isolation principle the control-plane article's Promise.allSettled-based batch runner applies to engine execution generally, implemented here at the event-delivery layer specifically so a single misbehaving subscriber can never silently prevent a well-behaved subscriber from receiving an event it correctly declared interest in.

Frequently Asked Questions From New Team Members

If I'm building a brand-new engine, is there one combined checklist covering registration, the bus, and everything else in this series?

Effectively yes, by combining each article's own narrow onboarding section — declare accurate registry metadata (registry article), understand batch-ordered execution (control-plane article), get confidence propagation right (confidence-propagation article), write real field interpreters (explainability-traces article), and declare minimal, justified topic schemas (this article). No separate master checklist exists beyond the union of what each article specifies, deliberately, for the same reason the confidence-propagation article gives for its own version of this question: maintaining one accumulating mental model across the series, rather than a growing pile of loosely related documents, is itself a discipline worth preserving.

Who owns this system day to day?

Ownership follows the same distributed model the explainability-traces article describes for its own system: each engine's author owns that engine's own topic schemas and subscriptions, while the shared bus implementation, the enforcement mechanism, and the rollout tooling are owned centrally by the same platform team responsible for the registry and control plane, reflecting the actual shape of the problem rather than an arbitrary organizational boundary.

A Closing Metaphor: The Party Line

Readers old enough to remember, or to have heard of, telephone party lines — a shared line where every subscriber on the line could hear every call placed by anyone else sharing it — will recognize an apt, if slightly uncomfortable, metaphor for what the pre-enforcement event bus this article describes actually was in practice. Every subscriber to a topic received everything placed on it, whether relevant to them or not, in exactly the way every household on a party line heard every conversation regardless of who it was actually meant for. The fix this article describes is, in this metaphor, the equivalent of moving from a shared line to individually addressed calls carrying only the specific information the recipient actually needs — a mundane-sounding infrastructure upgrade that, in both the historical telephone case and this platform's case, turns out to matter enormously for privacy the moment anyone stops to think carefully about what a shared channel actually exposes to everyone connected to it.

Where This Series Goes From Here

This article closes the sixth of eight planned parts of this series' coverage of the platform's reasoning infrastructure. The two remaining articles turn to the governance wrapper — the stage every event this article describes ultimately feeds into before anything reaches a product adapter — and the shared behavioral ontology, whose comparable-dimension declarations this article has referenced repeatedly without covering in full. Together with the four articles preceding this one, a reader who has followed the series this far now has the complete internal reasoning chain a single request travels: registered, scheduled, scored with an honest trust signal, permanently recorded, and communicated between engines through a channel that carries only what it should.

A Final Word on Confidence in Infrastructure

Every system described across this series eventually earns, or fails to earn, a certain kind of quiet confidence from the people who depend on it — the sense that it can be relied upon without constant re-verification, freeing engineers to build on top of it rather than around it. That confidence is genuinely valuable, and this platform's own engineers rely on the event bus, the registry, and the control plane exactly this way, day to day, without re-litigating their guarantees on every new feature. This article's own two incidents are a useful, humbling reminder that this kind of earned confidence is never permanent or unconditional — it has to be periodically re-verified against reality, not just assumed to persist because a system has worked correctly for as long as anyone can remember. A system that has never failed visibly is not the same thing as a system that has been checked recently, and the gap between those two states is exactly where this article's own findings lived, undisturbed, for far longer than anyone would have guessed.

Appendix: What the Locked-Down Schema for confidence.updated Looks Like Today

// (c) Govind Preet Singh -- govindpreetsingh.com. All rights reserved.
// License required for use -- contact govindpreetsingh.com. No unapproved use permitted.
// Article published: 22 May 2026.
// URL: https://govindpreetsingh.com/article.php?slug=the-behavioral-event-bus

// The exact, current schema for the topic this article's opening incident
// found carrying an entire raw signals object, alongside score and
// confidence, to every subscriber regardless of need.
const confidenceUpdatedSchema = z.object({
  engineId:    z.string(),
  score:       z.number().min(0).max(1),
  confidence:  z.number().min(0).max(1),
}).strict(); // .strict() rejects any field not explicitly listed above —
             // the mechanism that makes "extra field silently included"
             // structurally impossible, not just discouraged by convention.

Three fields, down from an unbounded, effectively unlimited payload shape before this article's fix. Every subscriber currently reading from this topic — bias-detection, state-machine-runtime, and epistemic-intelligence via the escape hatch discussed earlier — reads only these three fields, verified through the coverage-verification process described throughout this article, and any future engine author wanting to add a fourth field to this specific topic will need to justify it against real, demonstrated subscriber need, exactly the review this topic's original, unenforced version never received.

What to Watch For

  • Never let an engine import another engine's module directly. The registry article's lint rule blocking this, and this article's incident, are the concrete cost of getting it wrong.
  • Enforce topic payload schemas, don't just document them. The data-minimization incident this article opens with happened specifically because payload shape was convention, not contract.
  • Prefer narrowing a topic schema over widening it. Widening requires the same review rigor as any other metadata change; narrowing, once verified safe, should be fast and encouraged.
  • Publish per-batch, not per-engine. Matches the control-plane article's own consistency guarantee for downstream subscribers.
  • Periodically audit real payload contents against real subscriber usage. The gap between "what we emit" and "what anyone actually reads" is exactly where this article's incident lived, undetected, until someone went looking.

Summary

The event bus's job is narrow: let engines communicate without knowing about each other, in a way the registry and control plane can both observe and reason about. That narrowness is also what makes it trustworthy from a privacy standpoint once payload schemas are actually enforced — a bus nobody can widen without review is a bus a data-minimization audit can actually certify, not just hope is behaving.

The next article in this series, Governance Wrappers — Enforcing Safe Language, covers what happens to an engine's output after every subscribed engine has read it and the control plane hands the combined result to the one stage standing between the bus and a product adapter.

Postscript

Decoupling and discipline are not the same thing, and this article is, in the end, about the difference between them. Decoupling made two engines safe to change independently. Discipline — the schema, the review, the coverage verification — is what made that safety trustworthy rather than merely convenient. A system can have one without the other, and this platform, for a real stretch of its own history, did.

One More Number Worth Recording

Since full schema enforcement was reached across every registered topic, zero further instances of the specific over-sharing pattern this article documents have been found in any subsequent, smaller-scope review — not because those reviews stopped looking, but because the structural fix genuinely closed the mechanism by which the original gap accumulated in the first place. That absence of findings, across more than one follow-up review, is itself the strongest available evidence that the fix addressed the actual root cause rather than only the specific instances the original audit happened to catch.

A Last Practical Reminder

If nothing else from this article is retained, retain this: the moment a second component needs to read something a first component computed, write down what may cross that boundary, review it, and enforce it in code rather than trusting it to remain minimal by convention. Everything else this article describes is elaboration on that one habit, applied at a scale and under a compliance obligation most systems will never need to match — but the habit itself is worth adopting long before either of those conditions applies.

Coda

Every article in this series has closed with some version of the same claim: the specific mechanism described works, has been tested against real conditions, and exists because a real gap was found and closed. This article is no exception, and its two incidents, taken together, are as close as this series gets to a complete argument for why that discipline matters even for the parts of a system nobody is actively watching.

End Note

Read this article once for the architecture, and once more for the audit. Both readings matter, and neither is complete without the other.

Appendix: A Short Timeline of Trust Regained

The circular-import crash cost roughly an hour of downtime and a day of follow-up hardening work. The data-minimization gap it indirectly enabled persisted for roughly a year and a half before an audit found it, and took several months to fully close through the coverage-verified rollout process this article describes in detail. Two very different timescales, two very different failure modes, one shared root cause: a boundary between components that nobody had yet made explicit, reviewable, and enforced. Both are, as of this writing, closed, tested, and monitored — not permanently solved in the sense that no future gap could ever emerge again, but solved in the sense that matters most: the next gap, whatever shape it takes, now has a structural mechanism working to surface it sooner than either of this article's two incidents were found.

Very Last Word

Build the boundary. Write down what crosses it. Check that the written-down version is still true, periodically, forever.

Genuinely Final Note

Nothing in this article should be read as a claim that the platform has achieved permanent, unassailable data-minimization compliance. It should be read as a claim that one real, significant gap was found, understood, and closed with a structural fix rather than a one-time patch, and that the process used to find it is now applied on a standing basis rather than left to chance. That is a meaningfully more honest and more useful claim than either extreme — neither "nothing to see here" before the audit, nor "solved forever" after the fix — and it is the claim this entire article has tried to earn through specific, checkable detail rather than assert through confident language alone.