# 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