From e14a7db4ffe3c4e0f15f6b37a980501a8d74d26b Mon Sep 17 00:00:00 2001 From: Lukasz Kasprzak Date: Thu, 13 Aug 2026 13:04:10 +0200 Subject: Initial commit: prognosis, the Go implementation An hour-by-hour forecast for the terminal, with official IMGW warnings for Polish locations. Replaces the Python version, whose cache file format it keeps so the two can coexist until this reaches parity. Open-Meteo provides the forecast, geocoding and pollen; GUGiK turns coordinates into a TERYT powiat code; IMGW supplies the warnings, filtered to that powiat rather than the whole country. Only the two lookups that never change are cached. Forecasts never are. Silence is never allowed to read as all-clear: "no warnings in force" and "the check failed" are reported as distinct states. Place names are resolved without guessing. A name matching several places is refused with a numbered list carrying each candidate's region and coordinates, and -pick N chooses one and remembers it. A stray positional beside -l is an error, so an unquoted "Wiry, PL" cannot silently resolve to somewhere else. The cache is written one entry per line with sorted keys, and treated as disposable but not worthless: an entry that will not parse is skipped and the rest kept, and a file that will not parse at all is moved to cache.json.bad rather than overwritten. No third-party dependencies. `make ci` is the gate: gofmt clean, vet, tests. --- docs/superpowers/plans/2026-08-10-go-rewrite.md | 133 ++++++++++++++++++++++++ 1 file changed, 133 insertions(+) create mode 100644 docs/superpowers/plans/2026-08-10-go-rewrite.md (limited to 'docs/superpowers/plans') 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 -- cgit v1.3