diff options
| author | Lukasz Kasprzak <lukas@labunix.xyz> | 2026-08-13 13:04:10 +0200 |
|---|---|---|
| committer | Lukasz Kasprzak <lukas@labunix.xyz> | 2026-08-13 13:04:10 +0200 |
| commit | e14a7db4ffe3c4e0f15f6b37a980501a8d74d26b (patch) | |
| tree | 670ef0897839871a64d3a3bb2e17e242e7d6c385 /docs/superpowers/plans | |
| download | prognosis-e14a7db4ffe3c4e0f15f6b37a980501a8d74d26b.tar.gz prognosis-e14a7db4ffe3c4e0f15f6b37a980501a8d74d26b.zip | |
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.
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 |
