aboutsummaryrefslogtreecommitdiff
path: root/docs/superpowers/plans/2026-08-10-go-rewrite.md
blob: 1286edc5618d73e92ca5b9050d343af404af92ce (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
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