# Architecture Principles ## Standalone Subsystems Each class should behave like a small standalone subsystem. It should own one clear responsibility, expose a narrow public interface, and avoid hidden dependencies on the internals of other classes. Good subsystem boundaries: - accept explicit inputs - return explicit outputs - keep internal state private - avoid reaching into global state - avoid performing unrelated work - can be tested with recorded or fixture data Examples: - a weather client fetches forecast data - a weather parser converts API payloads into forecast points - a weather builder normalizes external forecast records for storage - a storage class persists records - Gibil makes decisions from snapshots ## Data Models Between Subsystems Subsystems should communicate through shared data models rather than through source-specific payloads. For example: - Open-Meteo JSON should become `WeatherForecastRun` - Modbus register reads should become `Observation` - HASS entity state should become `Observation` - Gibil should reason from `Snapshot` This keeps the edges messy and the core calm. ## Side Effects At The Edges Network calls, database writes, MQTT publishes, and filesystem writes should live at clear boundaries. Core reasoning classes should generally be pure or nearly pure: - input data in - answer out - no surprise I/O Stateful classes are allowed, but their state should be deliberate and inspectable. ## Grow By Composition Astrape should grow by connecting small subsystems together, not by building one large object that knows everything. The desired shape is: ```text source client -> parser -> model -> storage -> query/snapshot -> Gibil -> publisher ``` Each part should be replaceable without rewriting the others. ## Prefer Working Slices Build one thin working path at a time. A thin slice may start with empty storage or recorded source data, but it should still follow the real subsystem boundaries. For example, the weather slice can start with: ```text Open-Meteo forecast run -> WeatherBuilder -> clean forecast records ``` Then grow into: ```text Open-Meteo -> parser -> WeatherBuilder -> TimescaleDB -> weather_predictor.py ```