Add ADRs

2026-03-17 23:35:33 -07:00
parent 5c74cfc8e4
commit 941a67db97
6 changed files with 257 additions and 0 deletions

@@ -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

@@ -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

@@ -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

@@ -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

@@ -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

@@ -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