# 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.
