Add roadmap and implementation plan to support the new outlook schema
Some checks failed
ci/woodpecker/push/build-image Pipeline failed

This commit is contained in:
2026-06-12 07:47:01 -05:00
parent b4149f00c5
commit 0135eb1153
2 changed files with 831 additions and 20 deletions

View File

@@ -1,14 +1,501 @@
# SPC Convective Outlook API Roadmap
# Implement Weatherfeeder Outlook V2 Support
There are no active roadmap items for the SPC convective outlook API.
## Summary
Current behavior is documented in:
Implement `weatherapi` support for the `weatherfeeder` SPC outlook v2 contract described in `docs/roadmap/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 is a compatibility update for the existing convective outlook route family. Do not add or remove public routes. Update dependency, repository reads, app filtering, presenters, templates, tests, and current-behavior docs so `weatherapi` reads `weather.outlook.v2` data from the new weatherfeeder Postgres table shape.
Future changes to convective outlook behavior should be proposed in a new
roadmap entry before implementation.
## Required End State
- `go.mod` depends on the released `weatherfeeder` version containing `weather.outlook.v2` support.
- Existing public routes remain available:
- `GET /outlooks/convective`
- `GET /outlooks/convective/active`
- `GET /outlooks/convective/location`
- `weatherapi` remains read-only and does not create, migrate, or repair weatherfeeder tables.
- The Postgres repository reads `outlook_runs`, `outlooks`, and `outlook_discussions`.
- The repository no longer expects polygon-level `headline`, `summary`, or `discussion` columns on `outlooks`.
- Returned `WeatherOutlookRun` values include run-level `Discussions` loaded from `outlook_discussions`.
- App-level filtering trims both `Outlooks` and `Discussions` so discussions are present only for days with retained outlooks.
- Presenters copy and timezone-convert run-level discussion `updatedAt` values.
- JSON/XML/text responses expose run-level `discussions` and do not expose polygon-level outlook prose fields.
- `data: null` semantics for no latest run are preserved.
- Filtered no-match responses return a non-null run with `outlooks: []` and `discussions: []`.
- Public query behavior remains compatible: `format`, `units`, `tz` / `TZ`, `day`, `outlookType`, and allowed `containsLocation` behavior are preserved; `precision` and unknown params remain rejected.
## Guardrails
- Do not implement weatherfeeder table creation or migration in weatherapi.
- Do not call SPC or any upstream weather provider from weatherapi.
- Do not introduce historical browsing or cross-run accumulation in this change.
- Do not remove `/outlooks/convective/location`; keep it as an API-compatible active local-outlook route.
- Do not keep stale v1 SQL reads for `outlooks.headline`, `outlooks.summary`, or `outlooks.discussion`.
- Do not commit a `replace` directive for `weatherfeeder`.
- Keep SQL in `*_queries.go`, row DTOs in `*_rows.go`, row mapping in `*_mapper.go`, and request/presentation policy in HTTP/presenter packages.
## Stage 1: Dependency And Compile Boundary
### Changes
1. Update `go.mod` to the released `gitea.maximumdirect.net/ejr/weatherfeeder` version that includes:
- `standards.SchemaWeatherOutlookV2`;
- `model.WeatherOutlookRun.Discussions`;
- `model.WeatherOutlookDiscussion`;
- `model.WeatherOutlook` without `Headline`, `Summary`, or `Discussion`.
2. Run `go mod tidy`.
3. Fix compile errors from removed polygon-level outlook prose fields.
4. Search for outlook v1 and polygon prose references:
```sh
rg "SchemaWeatherOutlookV1|weather\.outlook\.v1|\.Headline|\.Summary|\.Discussion|headline|summary|discussion" internal docs templates README.md
```
5. Treat matches carefully:
- Keep unrelated alert and forecast-discussion uses.
- Remove or update outlook polygon-level prose references.
- Replace outlook-specific schema references with v2 where applicable.
### Expected Files
- `go.mod`
- `go.sum`
- `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`
- outlook-related tests under `internal/app`, `internal/adapters/outbound/postgres`, `internal/adapters/inbound/httpapi`, and presenter tests
### Verification
At this stage, run a compile-oriented focused set. It may fail until later stages are complete, but failures should identify remaining v1/prose references.
```sh
go test ./internal/app ./internal/adapters/outbound/postgres ./internal/adapters/inbound/httpapi/presenter
```
## Stage 2: Postgres Query, Row, And Mapper Updates
### Query Changes
Update `internal/adapters/outbound/postgres/outlooks_queries.go`.
Parent query:
- Keep latest run ordering:
```sql
ORDER BY as_of DESC, event_emitted_at DESC
LIMIT 1
```
- Continue selecting parent fields needed by `WeatherOutlookRun`.
- Optionally select `discussion_count` for test/sanity visibility, but do not expose it in the API response model.
Outlook child query:
- Remove these v1 columns from the `SELECT` list:
- `headline`
- `summary`
- `discussion`
- Continue selecting:
- `outlook_index`
- `outlook_id`
- `provider`
- `product`
- `day`
- `outlook_type`
- `label`
- `label_text`
- `severity_rank`
- `valid_from`
- `valid_to`
- `issued_at`
- `expires_at`
- `forecaster`
- `source_url`
- `image_url`
- `contains_location`
- `geometry_json`
- Keep `WHERE run_event_id = $1`.
- Keep `ORDER BY outlook_index ASC`.
Discussion child query:
- Add `queryOutlookDiscussionsForRun`:
```sql
SELECT
discussion_index,
day,
headline,
summary,
discussion,
updated_at
FROM outlook_discussions
WHERE run_event_id = $1
ORDER BY discussion_index ASC
```
### Row DTO Changes
Update `internal/adapters/outbound/postgres/outlooks_rows.go`:
- Remove `Headline`, `Summary`, and `Discussion` from `outlookRow`.
- Add `DiscussionCount` to `outlookRunParentRow` only if selected by parent query.
- Add `outlookDiscussionRow`:
- `DiscussionIndex int`
- `Day int`
- `Headline sql.NullString`
- `Summary sql.NullString`
- `Discussion sql.NullString`
- `UpdatedAt sql.NullTime`
### Mapper Changes
Update `internal/adapters/outbound/postgres/outlooks_mapper.go`:
- `mapOutlookRow` maps only v2 polygon fields.
- Keep geometry validation with `json.Valid` and return contextual mapper errors for invalid JSON.
- Continue copying geometry bytes with `append([]byte(nil), geometry...)`.
- Continue normalizing outlook timestamps to UTC.
- Add `mapOutlookDiscussionRow` returning `model.WeatherOutlookDiscussion`:
- `Day` from row day;
- string fields via existing `stringValue` helper;
- `UpdatedAt` via existing `timePtr` helper, ensuring UTC normalization.
### Read Flow Changes
Update `internal/adapters/outbound/postgres/outlooks_read.go`:
- `LatestConvectiveOutlookRun` loads parent row as today.
- After parent row:
- call `loadOutlooks(ctx, row.EventID)`;
- call `loadOutlookDiscussions(ctx, row.EventID)`;
- attach both to the run.
- Add `loadOutlookDiscussions` mirroring `loadOutlooks` style:
- query with context;
- scan rows;
- map rows;
- return iteration errors;
- wrap query, scan, map, and iteration errors with operation context.
- Preserve `nil, nil` on missing parent row.
### Tests
Update or add tests in `internal/adapters/outbound/postgres`:
- `mapOutlookRow` maps v2 polygon fields and no longer expects polygon-level prose.
- `mapOutlookRow` rejects invalid `geometry_json`.
- nullable label/forecaster/source/image/severity fields map correctly.
- `mapOutlookDiscussionRow` maps day/headline/summary/discussion/updatedAt and normalizes time to UTC.
- nullable discussion fields map to empty/omitted canonical values.
- latest run read loads outlooks in `outlook_index ASC` order.
- latest run read loads discussions in `discussion_index ASC` order.
- missing latest parent returns `nil, nil`.
- query/scan/iteration failures remain context-wrapped.
### Verification
```sh
go test ./internal/adapters/outbound/postgres
```
## Stage 3: Application Filtering And Copy Semantics
### Changes
Update `internal/app/service.go`.
Clone behavior:
- `cloneOutlookRun` must deep-copy `Discussions` in addition to `Outlooks`.
- Ensure outlook severity pointers, geometry bytes, latitude/longitude pointers, and issuedAt pointers remain copied.
- Add a helper such as `cloneOutlookDiscussion` if useful.
Filtering behavior:
- Filter `Outlooks` as today using `OutlookFilter`.
- After filtering outlooks, filter `Discussions` to only days represented by retained outlooks.
- Preserve discussion order from the repository result.
- If no outlooks remain, set discussions to an empty slice, not stale unfiltered discussions.
- Preserve nil/missing run behavior: repository `nil, nil` still returns `nil, nil`.
Recommended helper shape:
```go
func filterOutlookDiscussions(discussions []model.WeatherOutlookDiscussion, outlooks []model.WeatherOutlook) []model.WeatherOutlookDiscussion
```
Rules:
- Build a set of retained outlook days.
- If the set is empty, return `[]model.WeatherOutlookDiscussion{}` when discussions was non-nil or when consistent empty-array output is desired.
- Include each discussion only if `discussion.Day` is in the set.
- Do not synthesize discussions.
### Query Compatibility
Do not change query binder behavior in this stage unless tests reveal compile fallout from the model update. Public query behavior remains:
- `day` accepted on all outlook routes.
- `outlookType` accepted on all outlook routes.
- `containsLocation` accepted on `/outlooks/convective` and `/outlooks/convective/active`.
- `containsLocation` rejected on `/outlooks/convective/location`.
- `format`, `units`, and `tz` / `TZ` accepted.
- `precision` and unknown params rejected.
### Tests
Update `internal/app/service_test.go`:
- Delegation test still asserts one repository call.
- Day filter retains matching outlooks and only matching day discussions.
- Outlook type filter retains discussions only for days with retained outlooks.
- Active filter retains discussions only for days with active retained outlooks.
- `containsLocation=false` returns an empty outlook/discussion run for v2-style test data.
- Filtered no-match result has empty `Outlooks` and empty `Discussions`.
- Mutating returned outlooks/discussions does not mutate repository-owned data.
- Geometry byte copy and pointer copy behavior remains covered.
### Verification
```sh
go test ./internal/app
```
## Stage 4: Presenter And Text Template Updates
### Presenter Changes
Update `internal/adapters/inbound/httpapi/presenter/outlook.go`:
- Include `Discussions` when constructing the presented `model.WeatherOutlookRun`.
- Convert `WeatherOutlookDiscussion.UpdatedAt` using `inLocationTimePtr`.
- Deep-copy discussions so presenter output mutation cannot affect repository/app input.
- Remove `Headline`, `Summary`, and `Discussion` assignments from `copyOutlook`.
- Continue converting:
- run `AsOf`;
- run `IssuedAt`;
- outlook `ValidFrom`;
- outlook `ValidTo`;
- outlook `IssuedAt`;
- outlook `ExpiresAt`.
- Continue preserving geometry copy behavior.
- Continue returning `nil` for nil input.
Recommended helper:
```go
func copyOutlookDiscussion(d model.WeatherOutlookDiscussion, tz *time.Location) model.WeatherOutlookDiscussion
```
### Template Changes
Update `templates/outlooks_convective.txt.tmpl`:
- Render run-level discussions from `.Data.Discussions`.
- Do not reference outlook-level `.Headline`, `.Summary`, or `.Discussion`.
- Keep no-data output when `.Data` is nil.
- Keep useful output when `.Data` exists and `.Data.Outlooks` is empty.
- Include discussion day/headline/summary/discussion fields in a readable, conditional way.
- Avoid printing zero timestamps or `<nil>` values for absent `updatedAt`.
### Presenter Tests
Update `internal/adapters/inbound/httpapi/presenter/payload_test.go`:
- `OutlookRunPayload(nil, ...)` returns nil.
- Output includes copied `Discussions`.
- Discussion `UpdatedAt` is converted to requested timezone.
- Mutating output discussions does not mutate input discussions.
- Mutating output geometry does not mutate input geometry.
- Outlook timestamps still convert to requested timezone.
- Polygon-level prose fields are not expected in output structs/tests.
### Verification
```sh
go test ./internal/adapters/inbound/httpapi/presenter
```
## Stage 5: HTTP Endpoint Behavior And Tests
### Handler Behavior
Keep route registration and handler structure unchanged unless compile updates require mechanical edits.
Ensure endpoint responses reflect the updated presenter/app behavior:
- `/outlooks/convective` returns latest run with v2 shape.
- `/outlooks/convective/active` applies active filter and trims discussions.
- `/outlooks/convective/location` applies active local filter and trims discussions.
- `data: null` remains unchanged when no run exists.
- Filtered no-match response returns data object with empty outlook and discussion arrays.
### Endpoint Tests
Update `internal/adapters/inbound/httpapi/endpoints_test.go`:
- test fakes build `model.WeatherOutlookRun` with `Discussions`.
- JSON success asserts run-level `discussions` are present.
- JSON success asserts outlook polygons do not include polygon-level prose.
- XML success renders run-level discussions without errors.
- Text success renders run-level discussion content.
- Timezone test checks conversion of:
- run `asOf`;
- run `issuedAt`;
- outlook times;
- discussion `updatedAt`.
- Null data test remains unchanged.
- Filtered no-match test expects both `outlooks` and `discussions` empty.
- Query acceptance still covers `units`, `format`, `tz` / `TZ`, `day`, `outlookType`, and allowed `containsLocation`.
- Query rejection still covers `precision`, unknown params, invalid day, invalid outlookType, invalid containsLocation, invalid timezone, conflicting `tz` / `TZ`, and `containsLocation` on `/location`.
- Active/location route tests continue using injectable `outlookNow`.
### Verification
```sh
go test ./internal/adapters/inbound/httpapi
```
## Stage 6: Documentation Updates
After code behavior is updated, update current-behavior documentation. Do not leave v2 behavior only in roadmap files.
### Public Docs
Update `docs/api.md`:
- State outlook endpoints serve weatherfeeder `weather.outlook.v2` data.
- Run fields include `discussions`.
- Add discussion field table:
- `day`;
- `headline`;
- `summary`;
- `discussion`;
- `updatedAt`.
- Remove polygon-level `headline`, `summary`, and `discussion` from outlook fields.
- State v2 outlooks are already location-filtered by weatherfeeder.
- State `containsLocation` is expected to be true for v2 outlooks.
- Explain `/outlooks/convective/location` remains an active local-outlook compatibility route under v2.
- State endpoint filters also filter `discussions` to days represented by retained outlooks.
- State current endpoints use latest-run semantics and do not accumulate historical active outlook rows.
- Update examples to show run-level `discussions`.
- Ensure examples use valid GeoJSON polygon or multipolygon, not a point, if demonstrating SPC geometry.
Update `README.md` only if its overview implies all-polygons or old response shape.
### Integration/Internal Docs
Update `docs/integrations/weatherfeeder-postgres.md`:
- Update dependency version.
- Table family for convective outlook includes `outlook_runs`, `outlooks`, and `outlook_discussions`.
- Latest run selection remains `as_of DESC, event_emitted_at DESC`.
- Child ordering includes:
- outlooks by `outlook_index ASC`;
- outlook discussions by `discussion_index ASC`.
- `outlook_runs` includes `discussion_count` if listed.
- `outlooks` no longer includes `headline`, `summary`, or `discussion`.
- Add `outlook_discussions` columns.
- State weatherapi assumes weatherfeeder's outlook v2 table reset has already been applied.
Update `docs/internal/postgres-repository.md`:
- `LatestConvectiveOutlookRun` loads parent, outlook children, and discussion children.
- Add outlook discussion child ordering.
Update `docs/internal/presenters.md`:
- Outlook presenter copies and timezone-converts run-level discussions.
- Outlook presenter no longer handles polygon-level prose.
Update `docs/internal/http-adapter.md`:
- Outlook filters trim discussions to retained outlook days.
- `/location` is retained for compatibility and active local-outlook behavior.
Update `docs/operations.md` and `docs/troubleshooting.md` only if they mention the old weatherfeeder table shape or should warn operators that weatherapi requires weatherfeeder v2 outlook tables.
### Roadmap Docs
After implementation is complete, replace or update `docs/roadmap/outlook.md` and `docs/roadmap/implementation.md` according to repository convention:
- Either mark the roadmap complete and point to current docs.
- Or move any remaining deferred ideas to a future roadmap file.
Do not leave roadmap files claiming unimplemented work after the implementation has shipped.
### Documentation Checks
Run searches for stale outlook v1/current behavior:
```sh
rg "weather\.outlook\.v1|outlooks\.headline|outlooks\.summary|outlooks\.discussion|polygon-level|all stored outlook|all polygons|headline, summary, discussion" docs README.md internal templates
```
Allowed matches:
- explicit historical/legacy notes, if any;
- unrelated alert or forecast discussion prose fields;
- roadmap files that are intentionally historical.
## Stage 7: Full Verification
Run focused tests:
```sh
go test ./internal/app
go test ./internal/adapters/outbound/postgres
go test ./internal/adapters/inbound/httpapi/presenter
go test ./internal/adapters/inbound/httpapi
```
Run the full suite:
```sh
go test ./...
```
Optional build check:
```sh
go build ./cmd/weatherapi
```
Manual verification against a database populated by weatherfeeder outlook v2 tables:
```http
GET /outlooks/convective
GET /outlooks/convective/active
GET /outlooks/convective/location
GET /outlooks/convective?day=1
GET /outlooks/convective?outlookType=tornado
GET /outlooks/convective?containsLocation=false
GET /outlooks/convective?format=text&tz=America/Chicago
```
Expected manual results:
- Latest run includes run-level `discussions`.
- Active route filters outlooks and discussions together.
- Location route returns active local outlooks and remains valid.
- `containsLocation=false` returns an empty run for v2 data.
- Text format renders discussions without template errors.
- XML format renders without errors.
- No endpoint queries historical active rows across older runs.
## Assumptions
- A weatherfeeder release containing outlook v2 exists before final implementation is committed.
- Operators have reset/recreated weatherfeeder outlook tables according to weatherfeeder transition docs.
- Existing weatherapi route names remain stable for external consumers.
- Keeping `containsLocation` query support on non-location routes is useful backward compatibility even though v2 data should always be local.
- Latest-run semantics are correct for current convective outlook endpoints.
## Open Questions
None. This plan fixes the route, dependency, storage, filtering, presentation, and documentation decisions required to support weatherfeeder outlook v2 while preserving weatherapi's existing public route family.

View File

@@ -1,14 +1,338 @@
# SPC Convective Outlook API Roadmap
# Weatherfeeder Outlook V2 Support
There are no active roadmap items for the SPC convective outlook API.
## Summary
Current behavior is documented in:
Update `weatherapi` to read and serve the new `weatherfeeder` SPC outlook contract introduced by `weather.outlook.v2`.
- [`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)
`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.
Future changes to convective outlook behavior should be proposed in a new
roadmap entry before implementation.
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.