Refresh convective outlook roadmap docs
All checks were successful
ci/woodpecker/push/build-image Pipeline was successful

This commit is contained in:
2026-06-11 15:50:06 +00:00
parent 83cb9abcb9
commit b4149f00c5
2 changed files with 19 additions and 363 deletions

View File

@@ -1,239 +1,14 @@
# SPC Convective Outlook API Implementation Plan
# SPC Convective Outlook API Roadmap
Status: implemented. This file is retained as planning history; current
behavior is documented in [`docs/api.md`](../api.md) and the internal docs under
[`docs/internal/`](../internal/).
There are no active roadmap items for the SPC convective outlook API.
## Summary
Current behavior is documented in:
Implement the `weatherapi` SPC convective outlook API described in [`docs/roadmap/outlook.md`](outlook.md).
- [`docs/api.md`](../api.md)
- [`docs/internal/http-adapter.md`](../internal/http-adapter.md)
- [`docs/internal/presenters.md`](../internal/presenters.md)
- [`docs/internal/postgres-repository.md`](../internal/postgres-repository.md)
- [`docs/integrations/weatherfeeder-postgres.md`](../integrations/weatherfeeder-postgres.md)
This plan is execution-oriented. Implement stages in order. Keep `weatherapi` read-only: it must read `weatherfeeder` Postgres tables, reconstruct canonical model payloads, apply API filters, and present results over HTTP. Do not add ingestion, upstream SPC calls, table creation, migrations, or historical browsing.
The implementation target is `gitea.maximumdirect.net/ejr/weatherfeeder v0.11.0`, which provides the canonical outlook model and Postgres table contract this feature consumes.
Target routes:
- `GET /outlooks/convective`
- `GET /outlooks/convective/active`
- `GET /outlooks/convective/location`
All routes return the existing `data` envelope, support JSON/XML/text, and serve latest-run views only.
## Stage 1: Application Use Case And Filtering
Implement the core use case before touching HTTP or SQL.
Required changes:
- Extend `internal/app.Repository` with:
- `LatestConvectiveOutlookRun(context.Context) (*model.WeatherOutlookRun, error)`
- Add `internal/app.OutlookFilter` with:
- `Day *int`
- `OutlookType string`
- `ContainsLocation *bool`
- `ActiveAt *time.Time`
- Add `LatestConvectiveOutlook(ctx context.Context, filter OutlookFilter) (*model.WeatherOutlookRun, error)` to `internal/app.Service`.
- Implement `LatestConvectiveOutlook` by calling `repo.LatestConvectiveOutlookRun(ctx)`, returning `nil, nil` for no run, cloning the run, and filtering the cloned `Outlooks` slice.
- Filtering rules:
- `Day`: keep outlooks with `outlook.Day == *Day`.
- `OutlookType`: keep outlooks with exact normalized value matching `outlook.OutlookType`.
- `ContainsLocation`: keep outlooks with `outlook.ContainsLocation == *ContainsLocation`.
- `ActiveAt`: keep outlooks where `!ActiveAt.Before(outlook.ValidFrom)` and `ActiveAt.Before(outlook.ValidTo)`.
- Preserve original outlook order.
- Do not mutate the repository-returned run, slice, geometry, or pointer fields.
Tests:
- Add fake repository fields/methods in `internal/app/service_test.go`.
- Test repository delegation and no-data behavior.
- Test each filter independently.
- Test combined filters.
- Test active boundary behavior: includes exact `validFrom`, excludes exact `validTo`.
- Test filtered result keeps run metadata and can return `outlooks: []`.
- Test filtering does not mutate the original run or outlook slice.
Run:
```sh
go test ./internal/app
```
## Stage 2: Postgres Outlook Read Adapter
Add latest-run reconstruction from `weatherfeeder` outlook tables.
Required changes:
- Add `outlooks_queries.go`, `outlooks_rows.go`, `outlooks_mapper.go`, and `outlooks_read.go` under `internal/adapters/outbound/postgres`.
- Query latest parent from `outlook_runs`:
- selected columns: `event_id`, `location_id`, `location_name`, `latitude`, `longitude`, `as_of`, `issued_at`
- order: `as_of DESC, event_emitted_at DESC`
- limit: `1`
- Query child rows from `outlooks` by `run_event_id = $1`, ordered by `outlook_index ASC`.
- Select and map all canonical child columns:
- `outlook_index`, `outlook_id`, `provider`, `product`, `day`, `outlook_type`, `label`, `label_text`, `severity_rank`, `valid_from`, `valid_to`, `issued_at`, `expires_at`, `forecaster`, `headline`, `summary`, `discussion`, `source_url`, `image_url`, `contains_location`, `geometry_json`
- Map parent row into `model.WeatherOutlookRun` with UTC-normalized `AsOf` and `IssuedAt` pointer.
- Map child row into `model.WeatherOutlook` with UTC-normalized timestamps.
- Convert `geometry_json` into `json.RawMessage` without parsing or reserializing.
- Return `nil, nil` when the latest parent query returns `sql.ErrNoRows`.
- Wrap query, scan, iteration, and geometry decode/conversion failures with operation context.
- Confirm `var _ app.Repository = (*Repository)(nil)` still compiles.
Tests:
- Extend `internal/adapters/outbound/postgres/repository_test.go` or add outlook-specific mapper tests in the same package.
- Test parent nullable mapping and UTC normalization.
- Test child mapping for every canonical field, including `outlook_id`, `provider`, `contains_location`, and `geometry_json`.
- Test nullable fields map to nil/zero omitted model values consistently with existing helpers.
- Test geometry bytes are preserved exactly.
- Add read-path tests following existing repository test style if the package already has DB-backed query tests available; otherwise keep mapper-focused tests and rely on compile-time interface satisfaction.
Run:
```sh
go test ./internal/adapters/outbound/postgres
```
## Stage 3: HTTP Route Definitions And Query Binding
Add route wiring and strict query validation.
Required changes:
- Extend `internal/adapters/inbound/httpapi.Service` with:
- `LatestConvectiveOutlook(context.Context, app.OutlookFilter) (*model.WeatherOutlookRun, error)`
- Update HTTP test fakes to implement the new method.
- Add `outlooks_endpoint.go` under `internal/adapters/inbound/httpapi`.
- Register outlook definitions from `Definitions(svc)` after weather stories or before discussion; route order should not affect behavior.
- Add an outlook query binder that accepts common `format`, `units`, `tz`, `TZ`, plus outlook-specific filters.
- Reuse existing timezone parsing and common query normalization patterns.
- Validate:
- `day` must be integer `1`, `2`, or `3`.
- `outlookType` must be one of `categorical`, `tornado`, `hail`, `wind`; normalize case/space like other common values.
- `containsLocation` must parse as boolean when allowed.
- `precision` and unknown params are rejected.
- `containsLocation` is rejected on `/outlooks/convective/location`.
- Add package-level `outlookNow = time.Now` for active/location filters.
- Route filter construction:
- `/outlooks/convective`: use user filters only.
- `/outlooks/convective/active`: add `ActiveAt=outlookNow().UTC()` plus user filters.
- `/outlooks/convective/location`: add `ActiveAt=outlookNow().UTC()` and `ContainsLocation=true`; reject explicit `containsLocation`.
- Return `response.Envelope{Data: presenter.OutlookRunPayload(run, req.Units, req.Timezone)}`.
Tests:
- Route registration for all three paths.
- JSON success for each route.
- No repository data returns `data: null`.
- Filtered no-match run returns `outlooks: []`.
- `day`, `outlookType`, and `containsLocation` query params construct expected filters.
- Active and location endpoints use deterministic `outlookNow` in tests.
- `precision`, unknown params, invalid day/type/bool, invalid timezone, conflicting `tz`/`TZ`, and `containsLocation` on `/location` return `400 Bad Request`.
Run:
```sh
go test ./internal/adapters/inbound/httpapi
```
## Stage 4: Presenter And Text Template
Add outlook response presentation while preserving canonical shape.
Required changes:
- Add `presenter/outlook.go`.
- Implement `OutlookRunPayload(run *model.WeatherOutlookRun, _ Units, tz *time.Location) any`.
- Return nil for nil input.
- Copy run fields and outlook entries before timestamp conversion.
- Convert run `AsOf`, run `IssuedAt`, outlook `ValidFrom`, `ValidTo`, `IssuedAt`, and `ExpiresAt` with existing timezone helper behavior.
- Preserve `Latitude`, `Longitude`, `SeverityRank`, and `Geometry` without mutating source pointers or slices.
- Keep `units` accepted but ignored.
- Add `templates/outlooks_convective.txt.tmpl`.
- Template should render a clear no-data message when `.Data` is nil and concise outlook summaries when data exists.
Tests:
- Presenter nil input returns nil.
- Timezone conversion affects all run/outlook timestamps and preserves instant.
- Source model is not mutated.
- Geometry bytes are preserved exactly.
- Endpoint text response uses the new template.
- XML response renders without handler error.
Run:
```sh
go test ./internal/adapters/inbound/httpapi/presenter
go test ./internal/adapters/inbound/httpapi
```
## Stage 5: Current-Behavior Documentation
Update docs only after the endpoint behavior is implemented in code.
Required changes:
- Update `README.md` endpoint list with:
- `GET /outlooks/convective`
- `GET /outlooks/convective/active`
- `GET /outlooks/convective/location`
- Update `README.md` query parameter summary to mention outlook filters: `day`, `outlookType`, and `containsLocation`.
- Update `docs/api.md` with an Outlook endpoints section covering:
- route behavior
- supported query params and rejected params
- active filtering semantics
- `containsLocation` semantics
- response fields and nullability
- GeoJSON geometry coordinate order
- examples for full and location-filtered responses
- text format note
- Update `docs/internal/http-adapter.md`, `docs/internal/presenters.md`, `docs/internal/postgres-repository.md`, and `docs/integrations/weatherfeeder-postgres.md` only if the implementation adds details not already covered by existing internal/integration docs.
- Do not move implemented behavior into roadmap docs; roadmap docs should remain future/planning context until retired.
Tests:
- No documentation-specific tests are currently required unless existing tests check docs.
- Run full test suite after docs updates to catch template or route registration regressions.
Run:
```sh
go test ./...
```
## Stage 6: Final Verification And Cleanup
Before committing the implementation:
- Run focused tests:
```sh
go test ./internal/app
go test ./internal/adapters/outbound/postgres
go test ./internal/adapters/inbound/httpapi
go test ./internal/adapters/inbound/httpapi/presenter
```
- Run full tests:
```sh
go test ./...
```
- Check status and diff:
```sh
git status --short
git diff --stat
```
- Confirm no unrelated files are included.
- Confirm `go.mod` still requires `gitea.maximumdirect.net/ejr/weatherfeeder v0.11.0`.
- Confirm no weatherfeeder ingestion behavior, schema creation, migration logic, or upstream SPC calls were added.
- Confirm current-behavior docs no longer describe outlook endpoints as future work once implementation is complete, or leave roadmap docs clearly marked as historical/planning if they remain.
## Open Questions
None. The feature roadmap defines the route names, latest-run semantics, filters, adapter ownership, documentation timing, and validation behavior needed for implementation.
Future changes to convective outlook behavior should be proposed in a new
roadmap entry before implementation.

View File

@@ -1,133 +1,14 @@
# SPC Convective Outlook API Roadmap
Status: implemented. This file is retained as planning history; the current
public HTTP contract is documented in [`docs/api.md`](../api.md).
There are no active roadmap items for the SPC convective outlook API.
## Summary
Current behavior is documented in:
Add `weatherapi` support for SPC convective outlook data stored by `weatherfeeder`.
- [`docs/api.md`](../api.md)
- [`docs/internal/http-adapter.md`](../internal/http-adapter.md)
- [`docs/internal/presenters.md`](../internal/presenters.md)
- [`docs/internal/postgres-repository.md`](../internal/postgres-repository.md)
- [`docs/integrations/weatherfeeder-postgres.md`](../integrations/weatherfeeder-postgres.md)
This was roadmap-only content while the endpoint work was pending. Current
behavior now belongs in `README.md`, `docs/api.md`, and the relevant internal
and integration docs.
Target endpoints:
- `GET /outlooks/convective`
- `GET /outlooks/convective/active`
- `GET /outlooks/convective/location`
Each endpoint returns the standard weatherapi response envelope. Missing latest data returns `{"data": null}`. When a latest run exists but filters match no outlooks, return the run metadata with `outlooks: []`.
## Architecture And Boundary Decisions
- `weatherapi` remains read-only. It reads `weatherfeeder` Postgres tables and does not ingest SPC data, call SPC provider APIs, create tables, or run migrations.
- The application service owns latest-run filtering policy. The Postgres adapter reconstructs the latest stored canonical run; it should not encode HTTP query semantics.
- HTTP adapters own route registration, query binding, request validation, and filter construction.
- Presenters own timezone conversion and copy semantics. Repository methods normalize database timestamps to UTC.
- Geometry is returned as stored GeoJSON. `weatherapi` must not simplify, transform, or recompute polygons.
## Public API Contract
### Routes
- `/outlooks/convective`: latest convective outlook run with all stored outlook polygons.
- `/outlooks/convective/active`: latest run filtered to outlooks where `validFrom <= now < validTo`.
- `/outlooks/convective/location`: latest run filtered to currently active outlooks where `containsLocation=true`.
### Query Parameters
All routes support:
- `format=json|xml|text`
- `units=metric|us`, accepted for consistency and with no payload effect
- `tz` / `TZ`, affecting response timestamp rendering only
- `day=1|2|3`
- `outlookType=categorical|tornado|hail|wind`
`/outlooks/convective` and `/outlooks/convective/active` also support:
- `containsLocation=true|false`
Reject with `400 Bad Request`:
- `precision`
- unknown query parameters
- invalid `day`, `outlookType`, `containsLocation`, or timezone values
- conflicting `tz` and `TZ`
- `containsLocation` on `/outlooks/convective/location`
### Response Shape
Return a `model.WeatherOutlookRun`-compatible payload:
- run fields: `locationId`, `locationName`, `latitude`, `longitude`, `asOf`, `issuedAt`, `outlooks`
- outlook fields: `id`, `provider`, `product`, `day`, `outlookType`, `label`, `labelText`, `severityRank`, `validFrom`, `validTo`, `issuedAt`, `expiresAt`, `forecaster`, `headline`, `summary`, `discussion`, `sourceUrl`, `imageUrl`, `containsLocation`, `geometry`
Timezone conversion applies to run `asOf`, run `issuedAt`, and outlook `validFrom`, `validTo`, `issuedAt`, and `expiresAt`. Active filtering compares instants and is not timezone-dependent.
## Implementation Stages
### Stage 1: Application Use Case
- Extend `internal/app.Repository` with `LatestConvectiveOutlookRun(ctx)`.
- Add `app.OutlookFilter` with optional `Day`, `OutlookType`, `ContainsLocation`, and `ActiveAt` fields.
- Add `LatestConvectiveOutlook(ctx, filter)` to `app.Service`.
- Implement filtering by cloning the latest repository run and filtering the copied outlook slice while preserving order.
- Use an injectable request-time value from the HTTP adapter for active/location filters.
### Stage 2: Postgres Read Adapter
- Add outlook SQL, row DTOs, mapper, and read methods under `internal/adapters/outbound/postgres`, following the weather stories read pattern.
- Query the latest parent row from `outlook_runs` by `as_of DESC, event_emitted_at DESC`.
- Load child rows from `outlooks` by `run_event_id`, ordered by `outlook_index ASC`.
- Map all canonical outlook columns, including `outlook_id`, `provider`, `contains_location`, and `geometry_json`.
- Normalize timestamps to UTC and preserve `geometry_json` as `json.RawMessage`.
- Return `nil, nil` when no latest parent row exists.
### Stage 3: HTTP Adapter And Presenter
- Extend `internal/adapters/inbound/httpapi.Service` with `LatestConvectiveOutlook(ctx, app.OutlookFilter)`.
- Register the three outlook routes with JSON, XML, and text support.
- Add an outlook query binder for common query params plus `day`, `outlookType`, and `containsLocation` validation.
- Add `outlookNow`, defaulting to `time.Now`, for deterministic active/location endpoint tests.
- Add presenter helpers that copy the model, convert timestamps to the requested timezone, preserve geometry bytes, and never mutate repository-returned values.
- Add `templates/outlooks_convective.txt.tmpl` for all three outlook routes.
### Stage 4: Documentation After Implementation
Update current-behavior docs only in the same change that implements the routes:
- `README.md`: endpoint list and short query-parameter summary.
- `docs/api.md`: route family, query params, validation behavior, response fields, examples, `containsLocation` semantics, active filtering semantics, and GeoJSON longitude/latitude coordinate order.
- Internal or integration docs only if implementation changes repository assumptions or adapter boundaries beyond the planned latest-run reads.
## Test Plan
- App tests: delegation, no-data behavior, active/day/type/location filters, combined filters, and non-mutating clone behavior.
- Postgres tests: parent and child row mapping, nullable fields, UTC normalization, geometry preservation, child ordering, and no-row behavior.
- HTTP tests: route registration, JSON/XML/text responses, null data, empty filtered outlooks, timezone conversion, valid filters, rejected query params, invalid filter values, invalid timezone, and conflicting `tz`/`TZ`.
- Presenter tests: nil input, timezone conversion, copy semantics, and exact geometry preservation.
Verification commands:
```sh
go test ./internal/app
go test ./internal/adapters/outbound/postgres
go test ./internal/adapters/inbound/httpapi
go test ./internal/adapters/inbound/httpapi/presenter
go test ./...
```
## Assumptions And Defaults
- `weatherfeeder v0.11.0` or the active workspace module provides `model.WeatherOutlookRun` and `model.WeatherOutlook`.
- The first implementation serves only latest-run views; historical browsing remains future work.
- `/outlooks/convective` returns all stored latest-run polygons by default, including polygons that do not contain the configured location.
- `/outlooks/convective/location` means active and `containsLocation=true`.
- `units` is accepted but does not alter outlook payload values or field names.
- No `weatherapi` database migration is required.
## Open Questions
None. Route names, filtering semantics, adapter ownership, documentation timing, and verification expectations are decision-complete for implementation.
Future changes to convective outlook behavior should be proposed in a new
roadmap entry before implementation.