From b4149f00c52bdb6905d04237a101509ef4ea329e Mon Sep 17 00:00:00 2001 From: Eric Rakestraw Date: Thu, 11 Jun 2026 15:50:06 +0000 Subject: [PATCH] Refresh convective outlook roadmap docs --- docs/roadmap/implementation.md | 245 ++------------------------------- docs/roadmap/outlook.md | 137 ++---------------- 2 files changed, 19 insertions(+), 363 deletions(-) diff --git a/docs/roadmap/implementation.md b/docs/roadmap/implementation.md index 6d3dc23..9895c88 100644 --- a/docs/roadmap/implementation.md +++ b/docs/roadmap/implementation.md @@ -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. diff --git a/docs/roadmap/outlook.md b/docs/roadmap/outlook.md index e4cc711..9895c88 100644 --- a/docs/roadmap/outlook.md +++ b/docs/roadmap/outlook.md @@ -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.