diff options
Diffstat (limited to 'docs/superpowers/plans')
| -rw-r--r-- | docs/superpowers/plans/2026-08-10-go-rewrite.md | 133 |
1 files changed, 133 insertions, 0 deletions
diff --git a/docs/superpowers/plans/2026-08-10-go-rewrite.md b/docs/superpowers/plans/2026-08-10-go-rewrite.md new file mode 100644 index 0000000..1286edc --- /dev/null +++ b/docs/superpowers/plans/2026-08-10-go-rewrite.md @@ -0,0 +1,133 @@ +# prognosis Go rewrite — implementation plan + +> **For agentic workers:** implement task-by-task. Steps use checkbox (`- [ ]`) syntax. + +**Goal:** Replace the Python `prognosis` with a configurable Go implementation +that keeps every behaviour in the spec's 16-item parity contract. + +**Architecture:** `cmd/prognosis` wires flags + config into three API clients +(`openmeteo`, `imgw`) behind a `cache`, then hands a view model to `render`. +Rendering is pure: it takes data and a config, returns lines, touches no network. + +**Tech stack:** Go 1.24, standard library only. No third-party modules. + +## Global constraints + +- Module `github.com/lukaszkasprzak/prognosis`, Go 1.24, **stdlib only**. +- Colour: ANSI slots 0–15 only (codes 30–37, 90–97) plus attributes 1/2/4. +- Every column pad goes through `render.Pad`, never `len()` or rune count. +- IMGW thresholds compared in °C regardless of display units. +- Licence header not required per-file; `LICENSE` is GPLv3 at the repo root. +- Python `bin/prognosis-py` stays in the repo, off PATH, until parity is signed off. + +--- + +### Task 1: Scaffold + config package + +**Files:** +- Create: `go.mod`, `internal/config/config.go`, `internal/config/config_test.go` +- Modify: `Makefile` (add Go targets) + +**Interfaces produced:** +- `config.Config` struct with fields `Location, Units, Icons, Color string; + Hours, GraphHeight int; Graph, Warnings bool; Columns, Pollen []string` +- `config.Default() Config` +- `config.Load(path string) (Config, error)` — KEY=VALUE, `#` comments +- `config.Validate() error` — unknown column/icon/colour names +- `config.WriteDefault(path string) error` — commented defaults +- `config.ValidColumns() []string` + +- [ ] Write table tests: parse, precedence, unknown column error naming offender, unknown icons value, malformed line, comment/blank handling +- [ ] Run `go test ./internal/config/` — expect failure +- [ ] Implement +- [ ] Run tests — expect pass + +### Task 2: cache package + +**Files:** `internal/cache/cache.go`, `internal/cache/cache_test.go` + +**Interfaces produced:** +- `cache.Get(section, key string) (string, bool)` +- `cache.Put(section, key, value string) error` +- `cache.GetGeo(place string) (Geo, bool)` / `cache.PutGeo(place string, g Geo)` +- `cache.Geo{Lat, Lon float64; Label, Country string}` + +- [ ] Tests: round-trip, missing key, corrupt file self-heals to empty, atomic write leaves no `.tmp`, concurrent writers leave valid JSON +- [ ] Implement with temp file + `os.Rename` + +### Task 3: render width + colour primitives + +**Files:** `internal/render/width.go`, `internal/render/color.go`, plus tests + +**Interfaces produced:** +- `render.DisplayWidth(s string) int` +- `render.Pad(s string, w int) string` / `render.PadLeft(s string, w int) string` +- `render.Styler(colour bool) func(code, text string) string` +- `render.Paint(cells []render.Cell, c func(string, string) string) string` +- `render.Cell{Style, Text string}` + +- [ ] Tests: VS16 pair counts 1, `⛅` counts 2, combining mark counts 0, ASCII counts len, Nerd glyph counts 1; `Pad` reaches the requested display width for all three icon sets; `Paint` groups runs +- [ ] Implement + +### Task 4: openmeteo client + +**Files:** `internal/openmeteo/openmeteo.go`, `_test.go`, `testdata/*.json` + +**Interfaces produced:** +- `openmeteo.Geocode(place string) (cache.Geo, []string, error)` — second value is alternatives for the ambiguity note +- `openmeteo.Forecast(lat, lon float64, hours int, units string, fields []string) (*openmeteo.Data, error)` +- `openmeteo.Pollen(lat, lon float64, hours int, species []string) (map[string]float64, error)` +- `openmeteo.Data{TZ string; Rows []Row; Sun map[string][2]string; Daily map[string]float64}` +- `openmeteo.Row{When time.Time; Vals map[string]float64; Code int}` + +- [ ] Record fixtures once from the live API into `testdata/` +- [ ] Tests against fixtures: window starts at the current hour not 00:00, requested fields only, units mapping, pollen forward window ≥12h +- [ ] Implement + +### Task 5: imgw + gugik + +**Files:** `internal/imgw/imgw.go`, `internal/imgw/gugik.go`, tests, fixtures + +**Interfaces produced:** +- `imgw.Powiat(lat, lon float64) (code string, status imgw.Status, err error)` +- `imgw.Status` = `StatusOK | StatusOutside | StatusError` +- `imgw.Warnings(powiat string) ([]imgw.Warning, error)` +- `imgw.Warning{Event, Level, From, To, Probability, Text string}` + +- [ ] Tests: TERYT truncated to 4 digits; 0-result response ⇒ `StatusOutside`; transport failure ⇒ `StatusError`; expired warning dropped; unparseable date kept; radius parameter present +- [ ] Implement + +### Task 6: render table + chart + +**Files:** `internal/render/table.go`, `internal/render/chart.go`, `internal/render/render.go`, tests + +**Interfaces produced:** +- `render.View{Label, TZ string; Rows []openmeteo.Row; Sun map[string][2]string; Daily map[string]float64; Pollen map[string]float64; Warnings []imgw.Warning; WarnNote string; WarnFailed bool}` +- `render.Render(v View, cfg config.Config, width int, colour bool) string` +- `render.TempStyle(c float64) string` +- `render.PollenBand(species string, v float64) string` + +- [ ] Tests: temp band edges −15/30/35 exactly; rounded-value colouring (−14.6 ⇒ same as −15); dry window hides mm/rain; conditions repeat suppressed and reset per day; chart widen/downsample and `Nh/col`; axis labels skipped not truncated; all four warning states +- [ ] Implement + +### Task 7: cmd wiring + +**Files:** `cmd/prognosis/main.go` + +- [ ] Flags mirroring the spec's mapping table; flags > config > defaults +- [ ] Exit codes 0/1/2; notes to stderr +- [ ] Write default config on first run + +### Task 8: parity harness + +**Files:** `scripts/parity.sh`, Makefile target `parity` + +- [ ] Uses a bash/zsh **array** for arguments, never an unquoted string (zsh does not word-split; a string silently degrades every case into a usage error that compares equal) +- [ ] Runs both binaries back to back, compares layout: line count, column start positions, sections present, stderr and exit code exactly; tolerates numeric drift +- [ ] Covers the spec's matrix including `COLUMNS=53` and all three icon sets + +### Task 9: build, cross-compile, install + +- [ ] `make ci` clean: fmt, vet, test +- [ ] `make cross` produces `linux/amd64`, `linux/arm64`, `android/arm64` +- [ ] Verify the android/arm64 binary runs on the phone |
