# Weatherfeeder Outlook V2 Support ## Summary Update `weatherapi` to read and serve the new `weatherfeeder` SPC outlook contract introduced by `weather.outlook.v2`. `weatherfeeder` now emits location-filtered convective outlook runs, removes polygon-level prose fields from outlook polygons, and stores day-level outlook discussions in a new `outlook_discussions` table. `weatherapi` must update its dependency, Postgres read adapter, application filtering, presenters, templates, endpoint tests, and documentation to match that contract. This roadmap preserves the existing public route family unless a later API roadmap explicitly changes it: - `GET /outlooks/convective` - `GET /outlooks/convective/active` - `GET /outlooks/convective/location` ## Target Behavior - All outlook endpoints read the latest `weather.outlook.v2` run from weatherfeeder-owned Postgres tables. - The repository loads `outlook_runs`, `outlooks`, and `outlook_discussions`. - `outlooks[]` contains only location-relevant polygons written by weatherfeeder. - `containsLocation` remains in responses and should normally be `true` for every returned outlook. - `discussions[]` contains run-level SPC day discussions for days represented by returned outlooks. - If no latest run exists, responses continue returning `{ "data": null }`. - If a latest run exists but filters remove every outlook, responses return the run with `outlooks: []` and `discussions: []`. - `/outlooks/convective` returns the latest run with optional user filters. - `/outlooks/convective/active` filters the latest run to outlooks active at request time. - `/outlooks/convective/location` remains available for API compatibility and applies the active filter; because v2 storage is already location-filtered, it is effectively the current active local-outlook endpoint. - Public response timestamps continue honoring `tz` / `TZ` presentation conversion. - `units=metric|us` remains accepted for consistency and has no payload effect. - `precision` and unknown query parameters remain rejected. ## Upstream Dependency Update `go.mod` to the first released `gitea.maximumdirect.net/ejr/weatherfeeder` version that contains: - `standards.SchemaWeatherOutlookV2`; - `model.WeatherOutlookRun.Discussions`; - `model.WeatherOutlookDiscussion`; - `model.WeatherOutlook` without polygon-level `Headline`, `Summary`, or `Discussion` fields. Do not commit a local `replace` directive for weatherfeeder. If implementation begins before the upstream release is tagged, stop and tag/release weatherfeeder first or use a temporary local replace only outside the final committed diff. ## Stage 1: Module And Compile Contract ### Changes - Bump the `weatherfeeder` dependency in `go.mod` to the released version containing outlook v2. - Run `go mod tidy`. - Fix compile errors caused by removed `model.WeatherOutlook.Headline`, `Summary`, and `Discussion` fields. - Update any references to `standards.SchemaWeatherOutlookV1` in outlook-specific code to use `SchemaWeatherOutlookV2` only where schema constants are needed. ### Expected Compile Hotspots - `internal/adapters/outbound/postgres/outlooks_rows.go` - `internal/adapters/outbound/postgres/outlooks_mapper.go` - `internal/adapters/outbound/postgres/outlooks_queries.go` - `internal/adapters/outbound/postgres/outlooks_read.go` - `internal/adapters/inbound/httpapi/presenter/outlook.go` - `internal/app/service.go` - endpoint and presenter tests that construct `model.WeatherOutlook` - `docs/integrations/weatherfeeder-postgres.md` - `docs/api.md` ### Verification ```sh go test ./internal/app ./internal/adapters/outbound/postgres ./internal/adapters/inbound/httpapi/presenter ``` ## Stage 2: Postgres Repository V2 Reads ### Queries And Rows Update the Postgres adapter to match the v2 table shape. Parent query: - Continue selecting latest parent row from `outlook_runs` ordered by `as_of DESC, event_emitted_at DESC`. - Include `discussion_count` only if useful for tests or sanity checks; the read model does not need to expose it. Outlook child query: - Remove columns that no longer exist on `outlooks`: - `headline` - `summary` - `discussion` - Continue selecting child rows ordered by `outlook_index ASC`. - Continue validating/copying `geometry_json` as JSON. - Continue normalizing timestamps to UTC. Discussion child query: - Add `queryOutlookDiscussionsForRun` selecting from `outlook_discussions`: - `discussion_index` - `day` - `headline` - `summary` - `discussion` - `updated_at` - Filter by `run_event_id = $1`. - Order by `discussion_index ASC`. Row structs: - Remove `Headline`, `Summary`, and `Discussion` from `outlookRow`. - Add `outlookDiscussionRow` with nullable string/time fields. Read flow: - `LatestConvectiveOutlookRun` should load parent, outlook rows, and discussion rows. - Attach `run.Outlooks` and `run.Discussions` before returning. - Missing latest parent still returns `nil, nil`. - Child query/scan/iteration errors should include contextual wrapping. ### Mapper Behavior - `mapOutlookRow` should map only polygon fields present in v2. - Add `mapOutlookDiscussionRow` returning `model.WeatherOutlookDiscussion`. - Normalize `updated_at` to UTC when present. - Preserve nil/zero semantics from the canonical model. - Preserve geometry byte copy behavior. ### Tests Update Postgres mapper/read tests to cover: - parent mapping still normalizes `asOf` and optional `issuedAt` to UTC; - v2 outlook row maps all polygon fields and no polygon-level prose fields; - nullable optional outlook fields map to omitted/zero canonical values; - invalid `geometry_json` returns a mapper error; - discussion row maps `day`, `headline`, `summary`, `discussion`, and `updatedAt`; - nullable discussion fields map to omitted/zero values; - latest run loads outlook children by `outlook_index ASC`; - latest run loads discussion children by `discussion_index ASC`; - no parent row returns `nil, nil`; - query errors and scan errors remain context-wrapped. ### Verification ```sh go test ./internal/adapters/outbound/postgres ``` ## Stage 3: Application Filtering Semantics ### Changes Update `internal/app` outlook filtering so discussions stay coherent after endpoint filters. Current filtering should continue to clone the repository-returned run before mutation. Extend cloning and filtering to include `Discussions`: - Deep-copy `WeatherOutlookRun.Discussions`. - After filtering `Outlooks`, rebuild `Discussions` to include only days still represented by retained outlooks. - Preserve discussion order from the repository for retained days. - If retained outlooks are empty, set `Discussions` to an empty non-nil slice when the original slice was non-nil or when the endpoint needs stable JSON empty-array behavior. Route implications: - `/outlooks/convective?day=2` should return only Day 2 outlooks and only the Day 2 discussion. - `/outlooks/convective?outlookType=tornado` should return discussions only for days with retained tornado outlooks. - `/outlooks/convective/active` should remove discussions for days with no active retained outlooks. - `/outlooks/convective/location` should remain active plus local semantics. With v2 data, the explicit `containsLocation=true` filter is redundant but harmless. - `containsLocation=false` on routes that allow the parameter should return an empty outlook/discussion run with v2 data. ### Query Parameter Policy Preserve current public query behavior unless endpoint tests reveal a direct conflict: - `day=1|2|3` accepted on all outlook routes. - `outlookType=categorical|tornado|hail|wind` accepted case-insensitively on all outlook routes. - `containsLocation=true|false` accepted on `/outlooks/convective` and `/outlooks/convective/active` for backward-compatible filtering. - `containsLocation` rejected on `/outlooks/convective/location`. - `format`, `units`, and `tz` / `TZ` remain supported. - `precision` and unknown query parameters remain rejected. ### Tests Update app tests to cover: - repository delegation still happens once; - filtering by day also filters discussions to that day; - filtering by outlook type filters discussions to days with retained outlooks; - active filtering filters discussions to days with active retained outlooks; - no matching outlooks returns `outlooks: []` and `discussions: []`; - clone behavior does not mutate repository-owned `Outlooks`, `Discussions`, severity pointers, geometry bytes, or time pointers. ### Verification ```sh go test ./internal/app ``` ## Stage 4: Presenter, Templates, And HTTP Responses ### Presenter Changes Update `internal/adapters/inbound/httpapi/presenter/outlook.go`: - Copy `run.Discussions` into the presented payload. - Convert `WeatherOutlookDiscussion.UpdatedAt` into the requested timezone. - Continue converting `run.AsOf`, `run.IssuedAt`, and outlook `validFrom`, `validTo`, `issuedAt`, and `expiresAt`. - Remove references to polygon-level `Headline`, `Summary`, and `Discussion`. - Preserve geometry copy behavior. - Return `nil` for nil input. ### Text Template Changes Update `templates/outlooks_convective.txt.tmpl`: - Render run-level discussions, grouped/listed by day. - Do not reference polygon-level `.Headline`, `.Summary`, or `.Discussion`. - Keep output useful when `outlooks` is empty but `data` is present. - Keep no-data text for `data: null`. ### HTTP Tests Update endpoint tests to cover: - JSON response includes `discussions` at run level. - JSON response no longer includes polygon-level `headline`, `summary`, or `discussion`. - XML response renders run-level discussions without errors. - Text response renders run-level discussion content. - `tz` / `TZ` converts `asOf`, `issuedAt`, outlook times, and discussion `updatedAt`. - `/outlooks/convective`, `/active`, and `/location` still register and route. - `data: null` remains unchanged when no run exists. - filtered no-match response returns `outlooks: []` and `discussions: []`. - query validation behavior remains unchanged for supported/rejected params. ### Presenter Tests Update presenter tests to cover: - copied run includes copied discussions; - discussion `updatedAt` timezone conversion; - input run is not mutated; - geometry bytes remain copied; - nil input returns nil. ### Verification ```sh go test ./internal/adapters/inbound/httpapi ./internal/adapters/inbound/httpapi/presenter ``` ## Stage 5: Documentation Updates After code behavior is updated, update permanent docs in the same change. ### Public API Docs Update `docs/api.md`: - State that outlook endpoints serve weatherfeeder `weather.outlook.v2` data. - Add `discussions` to run fields. - Add `WeatherOutlookDiscussion` / discussion field definitions: - `day` - `headline` - `summary` - `discussion` - `updatedAt` - Remove polygon-level `headline`, `summary`, and `discussion` from outlook fields. - State that v2 outlooks are already location-filtered by weatherfeeder. - State `containsLocation` is expected to be true for v2 outlooks. - Clarify that `/outlooks/convective/location` remains active local-outlook behavior and is mostly a compatibility route under v2. - Document that filters also filter `discussions` to retained outlook days. - Document latest-run semantics: current endpoints read the latest run and do not accumulate active historical outlook rows from previous runs. - Update JSON and text examples to include run-level `discussions`. ### Integration And Internal Docs Update `docs/integrations/weatherfeeder-postgres.md`: - Update weatherfeeder dependency version. - Add `outlook_discussions` to the table family. - Add `discussion_count` to `outlook_runs` if the doc lists columns read or storage assumptions. - Remove `headline`, `summary`, and `discussion` from `outlooks` columns. - Add `outlook_discussions` columns and ordering by `discussion_index ASC`. - State that `weatherapi` expects the v2 table reset/migration to have been applied by operators/weatherfeeder deployment. Update `docs/internal/postgres-repository.md`: - State `LatestConvectiveOutlookRun` loads `outlook_runs`, `outlooks`, and `outlook_discussions`. - State child order for outlook discussions. Update `docs/internal/presenters.md`: - State outlook presenter copies and timezone-converts run-level discussions. Update `docs/internal/http-adapter.md`: - Clarify that outlook filters also trim run-level discussions to retained days. Update `README.md` only if its outlook summary implies the old all-polygon behavior. Update `docs/roadmap/implementation.md` after implementation is complete if this repository continues using that file as the active implementation checklist. ### Documentation Tests If docs consistency tests exist or are added, assert stable identifiers only: - `outlook_discussions` appears in `docs/integrations/weatherfeeder-postgres.md`. - `weather.outlook.v2` appears in `docs/api.md` or the integration docs. ## Stage 6: Full Verification 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 the full suite: ```sh go test ./... ``` Manual verification against a database populated by weatherfeeder v2 outlooks: - `GET /outlooks/convective` returns latest run with run-level `discussions`. - `GET /outlooks/convective/active` returns only active outlooks and matching discussions. - `GET /outlooks/convective/location` works and returns active local outlooks. - `GET /outlooks/convective?containsLocation=false` returns an empty run for v2 data. - Text and XML formats render successfully. ## Assumptions - `weatherfeeder` has been released with outlook v2 before final implementation is committed. - Weatherfeeder-owned Postgres outlook tables have been reset/recreated according to weatherfeeder's transition documentation. - `weatherapi` remains read-only and does not create, migrate, or repair weatherfeeder tables. - The existing outlook route family remains public and should not be removed in this compatibility update. - Latest-run semantics are the correct public API behavior for current outlook endpoints. ## Open Questions None. The roadmap preserves current route names and query compatibility while updating storage and response handling to the new weatherfeeder v2 outlook contract.