Add module split design spec

This commit is contained in:
2026-08-20 14:14:44 +03:00
parent f82e179c4b
commit 2b9f0b6683
@@ -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 2991, 99173)
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 3242, 5053, 175256)
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 259268, 270352)
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`).