001. Layered Architecture with Protocol-Based Boundaries

Status

Accepted

Context

The diagnostic output system needs to support multiple usage patterns: developers want simple one-line debug statements, libraries need non-intrusive registration, and applications require fine-grained control over output formatting and routing. Existing approaches either tightly couple components (hard to extend) or require extensive boilerplate (poor developer experience).

The system must accommodate:

  • Multiple output targets: stderr (default), files, logging integration, custom sinks

  • Multiple formatting strategies: plain text, Rich-colorized, custom layouts

  • Library-friendly configuration: per-module settings without global pollution

  • Optional dependencies: Rich library should enhance but not be required

  • Testing support: output capture, deterministic formatting

Decision

Implement a four-layer architecture with protocol-based boundaries:

Layer 1 - Dispatcher: Entry point managing reporter selection and activation control. Provides attribute-based access (ctrl.note(...)) and caches reporter instances per address + flavor combination.

Layer 2 - Reporter: Coordination layer binding compositor to printer for specific address + flavor. Handles active/inactive state and packages user content into structured records.

Layer 3 - Compositor: Transforms records into formatted text lines using composed subsystems (introducer for prefixes, linearizers for content). Receives column constraints from printer layer.

Layer 4 - Printer: Abstracts output targets. Provides textualization control information (columns, colorization capability) and writes formatted output.

Layers communicate through protocols (structural typing) rather than inheritance, enabling independent implementation and testing.

Alternatives

Single Monolithic Class

Combine dispatcher, formatting, and output in one class with template methods for customization.

Rejected: Violates single responsibility principle. Hard to test formatting without output. Configuration becomes unwieldy (one class with 20+ template methods). Cannot independently replace formatting vs output strategies.

Observer Pattern with Event Bus

Route messages through event bus to registered handlers.

Rejected: Over-engineering for diagnostic output. Async complexity not needed. Per-module configuration becomes awkward (need module-scoped subscriptions). Testing harder (must mock event bus infrastructure).

Plugin Architecture with Entry Points

Use setuptools entry points for formatters and outputs.

Rejected: Runtime configuration more important than install-time plugins. Adds packaging complexity. Testing requires filesystem manipulation. Per-module configuration unclear (how to specify which module uses which plugin).

Consequences

Positive Consequences

  • Independent evolution: Layers can change implementation without affecting others as long as protocols are maintained.

  • Testing clarity: Each layer tests independently with mock implementations of dependencies.

  • Configuration flexibility: Factory pattern enables per-address, per-flavor customization at each layer.

  • Optional dependencies: Rich integration isolated in compositor layer with graceful fallback.

  • Clear extension points: Protocols document exactly what custom implementations must provide.

Negative Consequences

  • Indirection overhead: Four layers add call stack depth compared to monolithic approach (negligible for diagnostic output).

  • Protocol verbosity: Each protocol requires TypedDict or Protocol definition plus documentation.

  • Learning curve: Developers must understand layer responsibilities to customize effectively.

Neutral Consequences

  • Factory pattern ubiquity: Almost every component uses factory pattern for instantiation (consistent but more conceptual overhead).

  • Protocol over ABC: Structural subtyping more flexible but less explicit than abstract base classes.