Files
weatherapi/docs/roadmap/outlook.md
Eric Rakestraw 0135eb1153
Some checks failed
ci/woodpecker/push/build-image Pipeline failed
Add roadmap and implementation plan to support the new outlook schema
2026-06-12 07:47:01 -05:00

14 KiB

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

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

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

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

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:

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:

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.