From 941a67db976eb6bfacc3f6d23f8c2559346867b1 Mon Sep 17 00:00:00 2001 From: David Gwilliam Date: Tue, 17 Mar 2026 23:35:33 -0700 Subject: [PATCH] Add ADRs --- ...-Capability-Based-Dependency-Resolution.md | 35 ++++++++++++ ADR-002-Stage-Based-Pipeline-Architecture.md | 35 ++++++++++++ ADR-003-DataSource-Abstraction.md | 40 +++++++++++++ ADR-004-Display-Protocol-Pattern.md | 41 ++++++++++++++ ADR-005-Sensor-Framework.md | 56 +++++++++++++++++++ ADR-006-Preset-TOML-Format.md | 50 +++++++++++++++++ 6 files changed, 257 insertions(+) create mode 100644 ADR-001-Capability-Based-Dependency-Resolution.md create mode 100644 ADR-002-Stage-Based-Pipeline-Architecture.md create mode 100644 ADR-003-DataSource-Abstraction.md create mode 100644 ADR-004-Display-Protocol-Pattern.md create mode 100644 ADR-005-Sensor-Framework.md create mode 100644 ADR-006-Preset-TOML-Format.md diff --git a/ADR-001-Capability-Based-Dependency-Resolution.md b/ADR-001-Capability-Based-Dependency-Resolution.md new file mode 100644 index 0000000..135ae81 --- /dev/null +++ b/ADR-001-Capability-Based-Dependency-Resolution.md @@ -0,0 +1,35 @@ +# ADR-001: Capability-Based Dependency Resolution + +**Date:** March 2026 +**Status:** Accepted + +## Context + +The pipeline needed a way for stages to auto-connect without hardcoding dependencies. Legacy system required explicit stage ordering. + +## Decision + +Stages declare capabilities (what they provide) and dependencies (what they need). Pipeline resolves dependencies using prefix matching. + +```python +class DataSourceStage(Stage): + capabilities = ["data"] # provides "data" + dependencies = [] # no dependencies + +class FontStage(Stage): + capabilities = ["rendered"] # provides "rendered" + dependencies = ["data"] # needs "data" +``` + +Prefix matching: dependency "source" matches capability "source.headlines", "source.poetry", etc. + +## Consequences + +- **Positive:** New stages integrate automatically without modifying controller +- **Positive:** Flexible composition - swap implementations by changing capabilities +- **Negative:** Debugging dependency chains requires understanding prefix resolution + +## References + +- `engine/pipeline/core.py`: Stage base class +- `engine/pipeline/controller.py`: Resolution logic diff --git a/ADR-002-Stage-Based-Pipeline-Architecture.md b/ADR-002-Stage-Based-Pipeline-Architecture.md new file mode 100644 index 0000000..3a4d5bb --- /dev/null +++ b/ADR-002-Stage-Based-Pipeline-Architecture.md @@ -0,0 +1,35 @@ +# ADR-002: Stage-Based Pipeline Architecture + +**Date:** March 2026 +**Status:** Accepted + +## Context + +Legacy used monolithic RenderStage that coupled fetching, rendering, and display. Hard to extend or test. + +## Decision + +Replace with composable Stage classes, each with single responsibility: + +- **DataSourceStage**: Wraps DataSource, provides raw items +- **SourceItemsToBufferStage**: Converts items to display buffer +- **EffectPluginStage**: Applies effects chain +- **DisplayStage**: Renders to display backend +- **ViewportFilterStage**: Filters to visible region + +Each Stage implements: +- `capabilities`: What it provides +- `dependencies`: What it needs +- `process(buf, ctx)`: Transform the buffer + +## Consequences + +- **Positive:** Single responsibility - easier to test and debug +- **Positive:** Replace any stage without affecting others +- **Positive:** Pipeline introspection shows clear data flow +- **Negative:** More classes to maintain + +## References + +- `engine/pipeline/core.py`: Stage ABC +- `engine/pipeline/adapters.py`: Concrete stage implementations diff --git a/ADR-003-DataSource-Abstraction.md b/ADR-003-DataSource-Abstraction.md new file mode 100644 index 0000000..14cc692 --- /dev/null +++ b/ADR-003-DataSource-Abstraction.md @@ -0,0 +1,40 @@ +# ADR-003: DataSource Abstraction + +**Date:** March 2026 +**Status:** Accepted + +## Context + +Headlines, poetry, and pipeline data all had different interfaces. Pipeline stages needed consistent access. + +## Decision + +Abstract DataSource class with get_items(): + +```python +class DataSource(ABC): + @abstractmethod + def get_items(self) -> list[SourceItem]: + ... + +class SourceRegistry: + def get(self, name: str) -> DataSource: ... + def list_available(self) -> list[str]: ... +``` + +Implementations: +- **HeadlinesDataSource**: Static RSS feed, cached +- **PoetryDataSource**: Static poetry feed, cached +- **ListDataSource**: Wraps pre-fetched items (testing) +- **PipelineDataSource**: Dynamic, re-fetches each cycle + +## Consequences + +- **Positive:** Consistent interface across all source types +- **Positive:** SourceRegistry enables discovery +- **Negative:** Runtime overhead for dynamic sources + +## References + +- `engine/data_sources/sources.py`: DataSource, SourceRegistry +- `tests/test_data_sources.py`: Comprehensive tests diff --git a/ADR-004-Display-Protocol-Pattern.md b/ADR-004-Display-Protocol-Pattern.md new file mode 100644 index 0000000..aa837c7 --- /dev/null +++ b/ADR-004-Display-Protocol-Pattern.md @@ -0,0 +1,41 @@ +# ADR-004: Display Protocol Pattern + +**Date:** March 2026 +**Status:** Accepted + +## Context + +Different display targets (terminal, browser, pygame) needed unified interface. Hard to add new backends. + +## Decision + +Display protocol with common interface: + +```python +class Display(Protocol): + def setup(self, width: int, height: int) -> None: ... + def draw(self, buf: Buffer) -> None: ... + def refresh(self) -> None: ... + def cleanup(self) -> None: ... +``` + +DisplayRegistry auto-discovers backends from `engine/display/backends/`: + +- **terminal.py**: ANSI escape sequences +- **websocket.py**: HTML5 Canvas broadcast +- **sixel.py**: Sixel graphics +- **kitty.py**: Kitty protocol +- **pygame.py**: Pygame window +- **null.py**: Headless (testing) +- **multi.py**: Forward to multiple backends + +## Consequences + +- **Positive:** Add new backend by implementing protocol +- **Positive:** MultiDisplay enables simultaneous outputs +- **Positive:** NullDisplay enables headless testing + +## References + +- `engine/display/__init__.py`: Display, DisplayRegistry +- `engine/display/backends/`: Backend implementations diff --git a/ADR-005-Sensor-Framework.md b/ADR-005-Sensor-Framework.md new file mode 100644 index 0000000..d8c3f0a --- /dev/null +++ b/ADR-005-Sensor-Framework.md @@ -0,0 +1,56 @@ +# ADR-005: Sensor Framework + +**Date:** March 2026 +**Status:** Accepted + +## Context + +Effects should react to real-time input (microphone, pipeline metrics) without hardcoding sensor logic. + +## Decision + +Sensor ABC with read() returning SensorValue: + +```python +class Sensor(ABC): + name: str + unit: str = "" + + @abstractmethod + def read(self) -> SensorValue | None: ... + +@dataclass +class SensorValue: + sensor_name: str + value: float + timestamp: float + unit: str = "" +``` + +Implementations: +- **MicSensor**: Microphone RMS dB (via sounddevice) +- **PipelineMetricsSensor**: FPS, frame-time +- **OscillatorSensor**: Test sine wave generator + +SensorStage provides values to effects. Effects declare param_bindings: + +```python +class GlitchEffect(EffectPlugin): + param_bindings = { + "intensity": {"sensor": "mic", "transform": "linear"}, + } +``` + +Transforms: linear, exponential, threshold + +## Consequences + +- **Positive:** Extensible - add new sensors without touching effects +- **Positive:** Effects react to any sensor uniformly +- **Negative:** Audio sensors require additional dependencies + +## References + +- `engine/sensors/__init__.py`: Sensor ABC, SensorRegistry +- `engine/sensors/mic.py`: MicSensor +- `engine/pipeline/adapters.py`: SensorStage diff --git a/ADR-006-Preset-TOML-Format.md b/ADR-006-Preset-TOML-Format.md new file mode 100644 index 0000000..ae4736d --- /dev/null +++ b/ADR-006-Preset-TOML-Format.md @@ -0,0 +1,50 @@ +# ADR-006: Preset TOML Format + +**Date:** March 2026 +**Status:** Accepted + +## Context + +Pipeline presets needed configuration format. Avoid external dependencies (no YAML, JSON, etc.). + +## Decision + +TOML format with validation: + +```toml +[preset] +name = "firehose" +display = "terminal" + +[[stages]] +type = "datasource" +source = "headlines" + +[[stages]] +type = "effect" +name = "glitch" +intensity = 0.5 + +[stages.config] +param_bindings = { intensity = { sensor = "mic", transform = "linear" } } +``` + +Locations (in order of priority): +1. `./presets.toml` (local override) +2. `~/.config/mainline/presets.toml` (user config) +3. `engine/presets.toml` (built-in) + +PresetLoader validates structure and signal paths (detects circular deps). + +## Consequences + +- **Positive:** No external dependencies (TOML built into Python 3.11+) +- **Positive:** Human-readable, easy to edit +- **Positive:** Validation prevents runtime errors +- **Negative:** Must document TOML schema + +## References + +- `engine/pipeline/preset_loader.py`: Loading and validation +- `engine/pipeline/presets.py`: Preset dataclasses +- `engine/presets.toml`: Built-in presets