From 2b9f0b668348cde4a8abbe8f2ffbcdeefb086ab5 Mon Sep 17 00:00:00 2001 From: Mats Rauhala Date: Thu, 20 Aug 2026 14:14:44 +0300 Subject: [PATCH] Add module split design spec --- .../specs/2026-08-20-module-split-design.md | 130 ++++++++++++++++++ 1 file changed, 130 insertions(+) create mode 100644 docs/superpowers/specs/2026-08-20-module-split-design.md diff --git a/docs/superpowers/specs/2026-08-20-module-split-design.md b/docs/superpowers/specs/2026-08-20-module-split-design.md new file mode 100644 index 0000000..57ddd82 --- /dev/null +++ b/docs/superpowers/specs/2026-08-20-module-split-design.md @@ -0,0 +1,130 @@ +# Module split & warning cleanup for `MyLib.hs` + +Date: 2026-08-20 +Status: Approved (pending spec review) + +## Goal + +Split `src/MyLib.hs` (352 lines, monolithic) into vertically-separated modules and fix all compiler warnings, committing between every change. + +Two concerns were named upfront: +1. AFRP (Mealy) internals — generic FRP machinery. +2. Home Assistant business logic. + +A third concern emerged during exploration: +3. IO runtime — websocket client, effect interpreter, `defaultMain`. + +## Module layout + +Three vertical modules with hierarchical naming. `MyLib` is removed; `app/Main.hs` and the cabal `exposed-modules` are updated. + +| Module | Concern | Depends on | +|---|---|---| +| `AFRP` | Generic Mealy/Event machinery — no HA knowledge | base, time | +| `HomeAssistant.Controller` | HA domain: effect type, entity parsers, controllers | `AFRP`, aeson, lens | +| `HomeAssistant.Runtime` | IO interpreter, websocket client, `defaultMain` | both, websockets | + +### Naming rationale + +`AFRP` lives at the top level (not `HomeAssistant.AFRP`) because the code is generic FRP machinery with zero HA references; nesting it under `HomeAssistant.*` would misrepresent it. It stays in-project per the user's decision, so a short top-level name is fine. + +### Future direction (deferred) + +When real controllers exist, split `HomeAssistant.Controller` further into sub-modules: +- `HomeAssistant.Controller.Effect` — `HASSEff`, `Service`, `HASS`, `callService` +- `HomeAssistant.Controller.Entity` — entity parsers +- `HomeAssistant.Controller` (or `.Controllers`) — actual controllers (`lightController`, etc.) + +Deferred now because there's only one proof-of-concept controller. + +## Module contents + +### `AFRP` (from MyLib.hs lines 29–91, 99–173) + +Exports: +- `Mealy(..)`, `eff` +- `Event(..)`, `hold`, `events`, `switch` +- accumulators: `preMapAccum`, `preMapAccumUTCTime`, `mapAccum`, `mapAccumUTCTime` +- `changes`, `whenA`, `filterA`, `thenA`, `(>>|)`, `toEvent` +- instances come along with `Mealy(..)` + +### `HomeAssistant.Controller` (from lines 32–42, 50–53, 175–256) + +Exports: +- `Service(..)`, `HASSEff(..)`, `HASS`, `callService` +- entity helpers: `entityChangeEvent`, `entityChangeEvent'`, `entityRead`, `entityRead'`, `entityBool`, `entityBool'` +- domain types/values: `Ruuvi(..)`, `ruuvi`, `ruuviTemperatures`, `ruuviPressures`, `DoorState(..)`, `door`, `light`, `lightController` + +### `HomeAssistant.Runtime` (from lines 259–268, 270–352) + +Exports: +- `defaultMain`, `app`, `step` +- `CallIdGen`, `mkCallIdGen`, `hassEval`, `dryRunHassEval`, `receiveJSON`, `wsCallService` + +## Warning fixes + +Per-warning, applied during the split-then-fix commit sequence: + +| Warning (line) | Fix | +|---|---| +| `numericDirection` (190) + `Direction`/`Increase`/`Decrease`/`Steady` (187) — unused, no sig | **Delete.** True experiment; used nowhere. | +| `entityChangeEvent` (224), `entityRead'` (236), `entityRead` (249) — unused top-binds | **Export** from `HomeAssistant.Controller`. Useful Either/Event-returning entity helpers the others build on; the split's export lists make them used. | +| `isEntity` (227) — unused local in `entityChangeEvent` | **Delete.** `entityChangeEvent` delegates to `entityChangeEvent' >>> toEvent`; its `isEntity` is a dead duplicate. | +| `state` (251) in `entityRead`, `state` (256) in `entityBool` — unused locals | **Delete.** Both functions delegate to their `'`-primed versions; the `where` clauses are dead duplicates. | +| `toBool` (244) — non-exhaustive (`"on"`/`"off"` only) | **Add catch-all `_ -> False`.** Conservative: treat unknown HA state as off rather than crashing. | +| `wsCallService` (327) + `conn` (346) — unused, paired | See option C below. | + +### `wsCallService` / `conn` — option C (chosen) + +The commented-out `wsCallService conn ...` call on line 350 made `conn` unused. Rather than uncomment (behavior change) or paper over with `_conn` (dishonest), expose both behaviors as named, exported functions: + +```haskell +hassEval :: CallIdGen -> WS.Connection -> HASSEff a -> IO a +hassEval gen conn = \case + CallService x -> do + callId <- generateCallId gen + wsCallService conn callId (serviceDomain x) (serviceName x) (serviceTarget x) + Pure a -> pure a + +dryRunHassEval :: CallIdGen -> HASSEff a -> IO a +dryRunHassEval gen = \case + CallService x -> do + callId <- generateCallId gen + print (callId, x) + Pure a -> pure a +``` + +Both `conn` and `wsCallService` become used by the real `hassEval`; the current print-only behavior lives in `dryRunHassEval`. `app` wires up **`dryRunHassEval`** to preserve current runtime behavior — flipping to real `hassEval` later is a one-word change. + +## Commit sequence + +Two commits, each building cleanly: + +1. **`Split MyLib into AFRP, Controller, Runtime`** + - Mechanical move of code into `src/AFRP.hs`, `src/HomeAssistant/Controller.hs`, `src/HomeAssistant/Runtime.hs`. + - Proper export lists (which naturally exports the "useful but unused" entity helpers, resolving those three unused-binding warnings). + - Delete `src/MyLib.hs`. + - Update `app/Main.hs` import (`MyLib` -> `HomeAssistant.Runtime`). + - Update cabal `exposed-modules`. + - Build still warns on remaining items. + +2. **`Fix warnings`** + - Delete `numericDirection`/`Direction` and dead `where` clauses (`isEntity` in `entityChangeEvent`, `state` in `entityRead`/`entityBool`). + - Add `_ -> False` catch-all to `toBool`. + - Implement real `hassEval` (uncommented `wsCallService`) + add `dryRunHassEval`; export both from `HomeAssistant.Runtime`. + - Wire `app` to `dryRunHassEval` to preserve current behavior. + - Build clean (no warnings). + +## Verification + +After each commit: +- `cabal build` succeeds. +- After commit 2: `cabal build --ghc-options="-Wall -Wincomplete-uni-patterns -Wincomplete-record-updates"` produces zero warnings. +- `app/Main.hs` still compiles and imports `defaultMain` from its new home. + +## Non-goals + +- Splitting `HomeAssistant.Controller` into Effect/Entity/Controller sub-modules (deferred until real controllers exist). +- Adding tests (test suite is a placeholder; out of scope). +- Changing `app` to call real `hassEval` (stays on `dryRunHassEval` to preserve behavior). +- Any behavioral changes beyond warning fixes and the dormant `wsCallService` activation (which is itself inert until `app` switches to `hassEval`).