aboutsummaryrefslogtreecommitdiff
path: root/docs/superpowers/plans/2026-08-10-go-rewrite.md
diff options
context:
space:
mode:
Diffstat (limited to 'docs/superpowers/plans/2026-08-10-go-rewrite.md')
-rw-r--r--docs/superpowers/plans/2026-08-10-go-rewrite.md133
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