All checks were successful
ci/woodpecker/push/build-image Pipeline was successful
389 lines
18 KiB
Markdown
389 lines
18 KiB
Markdown
# Implement SPC Outlook V2 Location Filtering
|
|
|
|
## Summary
|
|
|
|
Implement the roadmap in `docs/roadmap/outlook.md` as a behavior-changing canonical contract update for SPC convective outlooks.
|
|
|
|
The implementation must make `weatherfeeder` emit location-relevant SPC outlook snapshots instead of all SPC polygons, and move SPC print-page prose from each polygon to run-level day discussions.
|
|
|
|
This plan is decision-complete. Do not preserve the old `weather.outlook.v1` output behavior in the SPC normalizer. Implement `weather.outlook.v2`, update the Postgres sink contract, add transition documentation, and update current-behavior docs after the code change lands.
|
|
|
|
## Required Target Behavior
|
|
|
|
- Raw SPC source behavior remains complete and unfiltered.
|
|
- Event kind remains `outlook`.
|
|
- Raw schema remains `raw.spc.convective_outlook.v1`.
|
|
- Canonical SPC outlook output changes from `weather.outlook.v1` to `weather.outlook.v2`.
|
|
- Canonical outlook runs include only polygons where the configured forecast location is inside or on the boundary.
|
|
- Canonical outlook runs are still emitted when no polygons apply locally.
|
|
- No local polygons means `outlooks: []` and `discussions: []`.
|
|
- Day discussions are run-level entries in `WeatherOutlookRun.discussions`.
|
|
- Discussions are included only for days represented by at least one retained outlook.
|
|
- Multiple retained outlooks for the same day share one run-level discussion entry.
|
|
- Polygon-level `headline`, `summary`, and `discussion` fields are removed from `model.WeatherOutlook`.
|
|
- `containsLocation` remains on `WeatherOutlook` and must be `true` for every v2 emitted outlook.
|
|
- Empty SPC `GeometryCollection` no-risk placeholders remain skipped and still contribute issue time.
|
|
- Supersession semantics are documented in permanent docs: latest-run semantics are preferred; historical row supersession uses `provider`, `product`, `outlookType`, `validFrom`, `validTo`, and greatest `issuedAt`; `day` and `label` are not identity fields.
|
|
|
|
## Guardrails
|
|
|
|
- Do not move point-in-polygon filtering into the source layer.
|
|
- Do not fetch additional SPC products, images, shapefiles, or Day 4-8 products.
|
|
- Do not make `weatherfeeder` an archive of national SPC polygons.
|
|
- Do not add a separate outlook-discussion event kind.
|
|
- Do not introduce feedkit changes.
|
|
- Do not use column-level compatibility migrations for the outlook table family. Existing SPC outlook history may be discarded, so the transition should drop and recreate outlook tables.
|
|
- Do not update non-roadmap current-behavior docs before the corresponding behavior is implemented.
|
|
- Keep implementation inside existing architecture boundaries: `model`, `standards`, SPC normalizer/provider helpers, and Postgres sink mapping.
|
|
|
|
## Stage 1: Standards And Canonical Model
|
|
|
|
### Code Changes
|
|
|
|
- Add `standards.SchemaWeatherOutlookV2 = "weather.outlook.v2"` in `standards/schema.go`.
|
|
- Keep `standards.SchemaWeatherOutlookV1` for historical references and any existing tests that still validate documented constants.
|
|
- Update `model/outlook.go`:
|
|
- add `WeatherOutlookDiscussion`;
|
|
- add `Discussions []WeatherOutlookDiscussion` to `WeatherOutlookRun` with JSON tag `json:"discussions"`;
|
|
- remove `Headline`, `Summary`, and `Discussion` fields from `WeatherOutlook`.
|
|
- Keep all other `WeatherOutlook` fields unchanged, including `ContainsLocation` and `Geometry`.
|
|
- Update any code or tests that construct `WeatherOutlook` values to remove the deleted polygon-level prose fields.
|
|
- Do not change raw SPC provider structs in this stage.
|
|
|
|
### Tests
|
|
|
|
- Update model documentation/consumer tests so `WeatherOutlookRun`, `WeatherOutlook`, and `WeatherOutlookDiscussion` are listed where applicable.
|
|
- Update standards documentation tests so `weather.outlook.v2` is expected.
|
|
- Add or update a JSON-shape test to verify:
|
|
- `WeatherOutlookRun` serializes `discussions`;
|
|
- `WeatherOutlook` no longer serializes polygon-level `headline`, `summary`, or `discussion`.
|
|
|
|
### Verification
|
|
|
|
```sh
|
|
go test ./model ./standards
|
|
```
|
|
|
|
## Stage 2: SPC Normalizer V2 Output
|
|
|
|
### Code Changes
|
|
|
|
- Update `internal/normalizers/spc/convective_outlook.go` so `Normalize` emits `standards.SchemaWeatherOutlookV2`.
|
|
- Keep matching raw input schema `standards.SchemaRawSPCConvectiveOutlookV1`.
|
|
- Keep parsing all required print-page discussions before mapping features.
|
|
- Keep decoding every configured GeoJSON product.
|
|
- Keep skipping empty `GeometryCollection` placeholders.
|
|
- Track latest GeoJSON issue time across all real features and empty placeholders, not only retained local polygons.
|
|
- For every real feature:
|
|
- parse required timestamps and label as today;
|
|
- compute `containsLocation` using existing `geo.ContainsPoint`;
|
|
- if `containsLocation == false`, do not append a canonical outlook;
|
|
- if `containsLocation == true`, append the canonical outlook with `ContainsLocation: true`.
|
|
- Do not attach `headline`, `summary`, or `discussion` to retained outlooks.
|
|
- After retained outlooks are built, build `WeatherOutlookRun.Discussions`:
|
|
- collect unique days present in retained outlooks;
|
|
- include one `WeatherOutlookDiscussion` per retained day;
|
|
- sort discussions by day ascending;
|
|
- map `Day`, `Headline`, `Summary`, `Discussion`, and `UpdatedAt` from parsed print-page data;
|
|
- omit discussions for days with no retained outlooks.
|
|
- Preserve existing product ordering for retained outlooks: day, outlook type order, then feature order.
|
|
- Preserve `asOf` and normalized event `effective_at` policy:
|
|
- latest valid GeoJSON issue time across the complete bundle;
|
|
- latest print-page update time;
|
|
- incoming event `effective_at`;
|
|
- incoming event `emitted_at`.
|
|
- Preserve `WeatherOutlookRun.IssuedAt` as latest valid GeoJSON issue time when available, even when `outlooks` is empty.
|
|
|
|
### Tests
|
|
|
|
Update `internal/normalizers/spc/convective_outlook_test.go`:
|
|
|
|
- Existing sample test should expect `out.Schema == standards.SchemaWeatherOutlookV2`.
|
|
- Existing in-location sample should expect only location-contained outlooks.
|
|
- Outside-location sample should normalize successfully with:
|
|
- `len(run.Outlooks) == 0`;
|
|
- `len(run.Discussions) == 0`;
|
|
- non-zero `run.AsOf` from latest product issue time;
|
|
- normalized event `EffectiveAt == run.AsOf`.
|
|
- Add a fixture/test case where only Day 2 contains the location and assert:
|
|
- all retained outlooks have `Day == 2`;
|
|
- `run.Discussions` length is 1;
|
|
- discussion day is 2;
|
|
- Day 1 and Day 3 discussions are absent.
|
|
- Add a fixture/test case where multiple Day 1 outlook types contain the location and assert:
|
|
- multiple retained Day 1 outlooks are present;
|
|
- exactly one Day 1 discussion is present.
|
|
- Assert every retained outlook has `ContainsLocation == true`.
|
|
- Keep the empty `GeometryCollection` regression test and update it for v2 shape.
|
|
- Keep malformed timestamp, malformed geometry, missing discussion, missing label, and product ordering tests.
|
|
- Replace old tests that expected all nine fixture polygons to be emitted with location-filtered expectations.
|
|
- Replace tests that asserted polygon-level discussion text with run-level discussion assertions.
|
|
|
|
### Verification
|
|
|
|
```sh
|
|
go test ./internal/providers/spc ./internal/normalizers/spc ./internal/geo
|
|
```
|
|
|
|
## Stage 3: Postgres Schema And Mapper
|
|
|
|
### Schema Changes
|
|
|
|
Update `internal/sinks/postgres/schema.go`:
|
|
|
|
- Add table constant `tableOutlookDiscussions = "outlook_discussions"`.
|
|
- Add `discussion_count INTEGER NOT NULL` to `outlook_runs`.
|
|
- Add `outlook_discussions` table:
|
|
- `run_event_id TEXT REFERENCES outlook_runs(event_id) ON DELETE CASCADE NOT NULL`;
|
|
- `discussion_index INTEGER NOT NULL`;
|
|
- `as_of TIMESTAMPTZ NOT NULL`;
|
|
- `day INTEGER NOT NULL`;
|
|
- `headline TEXT NULL`;
|
|
- `summary TEXT NULL`;
|
|
- `discussion TEXT NULL`;
|
|
- `updated_at TIMESTAMPTZ NULL`.
|
|
- Set primary key to `(run_event_id, discussion_index)`.
|
|
- Set prune column to `as_of`.
|
|
- Add index `idx_wf_outlook_discussions_day_as_of` on `(day, as_of)`.
|
|
- Add unique index `idx_wf_outlook_discussions_run_day` on `(run_event_id, day)` using `fksinks.PostgresIndex{Unique: true}`.
|
|
- Remove `headline`, `summary`, and `discussion` from the target `outlooks` schema. Do not carry these legacy nullable columns forward.
|
|
|
|
### Mapper Changes
|
|
|
|
Update `internal/sinks/postgres/map.go`:
|
|
|
|
- Route `standards.SchemaWeatherOutlookV2` to the outlook mapper.
|
|
- Remove `standards.SchemaWeatherOutlookV1` support from the active outlook mapper unless there is a compile-time reason to keep a clearly documented legacy test. Existing v1 outlook rows are intentionally discarded during the table reset and the SPC normalizer must not produce v1 events.
|
|
- Decode payload into the updated `model.WeatherOutlookRun`.
|
|
- Write `outlook_runs.discussion_count = len(run.Discussions)`.
|
|
- Write one `outlook_discussions` row for each `run.Discussions[i]`.
|
|
- Use `discussion_index` as the array position.
|
|
- Use parent `run.AsOf.UTC()` for `as_of`.
|
|
- Normalize `UpdatedAt` to UTC when present.
|
|
- Validate discussion entries before writing:
|
|
- `day` must be 1, 2, or 3;
|
|
- at least one of `headline`, `summary`, or `discussion` must be non-empty;
|
|
- duplicate discussion days in one run should fail before hitting the unique index.
|
|
- Continue validating outlook required fields:
|
|
- `id`, `provider`, `product`, `day`, `outlookType`, `label`, `validFrom`, `validTo`, `issuedAt`, `expiresAt`, and `geometry` remain required.
|
|
- Add a v2-specific invariant validation: every persisted v2 outlook must have `ContainsLocation == true`. If the mapper continues to accept v1, apply this invariant only to v2.
|
|
- Continue compacting geometry JSON and normalizing all timestamps to UTC.
|
|
|
|
### Tests
|
|
|
|
Update `internal/sinks/postgres/schema_test.go`:
|
|
|
|
- Assert `outlook_runs` includes `discussion_count`.
|
|
- Assert `outlook_discussions` exists with expected columns, primary key, prune column, day/as-of index, and unique run/day index.
|
|
- Assert `outlooks` does not include `headline`, `summary`, or `discussion`.
|
|
|
|
Update `internal/sinks/postgres/map_test.go`:
|
|
|
|
- Assert v2 outlook run writes:
|
|
- one parent row;
|
|
- one outlook row per retained outlook;
|
|
- one discussion row per run discussion;
|
|
- correct `outlook_count` and `discussion_count`.
|
|
- Assert empty local run writes parent row with `outlook_count = 0` and `discussion_count = 0` and no child rows.
|
|
- Assert discussion rows map day/headline/summary/discussion/updatedAt correctly.
|
|
- Assert duplicate discussion days fail with useful context.
|
|
- Assert invalid discussion day fails with useful context.
|
|
- Assert empty discussion content fails with useful context.
|
|
- Assert v2 outlook with `ContainsLocation == false` fails.
|
|
- Update existing tests that used polygon-level `Headline`, `Summary`, or `Discussion`.
|
|
- Keep compact geometry and UTC normalization tests.
|
|
|
|
### Verification
|
|
|
|
```sh
|
|
go test ./internal/sinks/postgres
|
|
```
|
|
|
|
## Stage 4: Normalizer Registry And Cross-Package Consistency
|
|
|
|
### Code Changes
|
|
|
|
- Confirm SPC normalizer registration remains unchanged except for output schema.
|
|
- Update any package-level tests under `internal/normalizers` that assert canonical schema routing or supported schema names.
|
|
- Update any source/registry documentation consistency tests that expect canonical schema strings.
|
|
- Search for stale `SchemaWeatherOutlookV1` usage:
|
|
|
|
```sh
|
|
rg "SchemaWeatherOutlookV1|weather\.outlook\.v1|headline|summary|discussion" model standards internal docs/consumers docs/integrations docs/internal
|
|
```
|
|
|
|
- Keep `SchemaWeatherOutlookV1` only where intentionally retained for historical docs or standards compatibility. Do not route new SPC normalized events to v1.
|
|
- Ensure no SPC normalizer test, source test, or current v2 documentation path still claims all polygons are emitted.
|
|
|
|
### Tests
|
|
|
|
```sh
|
|
go test ./internal/normalizers ./internal/normalizers/...
|
|
go test ./internal/sources ./cmd/weatherfeeder
|
|
```
|
|
|
|
## Stage 5: Transition Documentation
|
|
|
|
### Roadmap Transition Doc
|
|
|
|
Create `docs/roadmap/outlook-schema-transition.md` with operator transition guidance.
|
|
|
|
The document must include:
|
|
|
|
- Purpose: reset existing Postgres outlook tables from the `weather.outlook.v1` storage shape to the v2-compatible storage shape.
|
|
- Scope: drops only the outlook table family and lets updated `weatherfeeder` recreate it. Other weather tables are not affected.
|
|
- Explicit warning: these commands delete stored SPC outlook history.
|
|
- Deployment order:
|
|
1. Stop `weatherfeeder`.
|
|
2. Drop existing outlook tables.
|
|
3. Deploy updated `weatherfeeder`.
|
|
4. Start `weatherfeeder` so the Postgres sink recreates the v2 tables.
|
|
5. Deploy updated downstream consumers such as `weatherapi`.
|
|
- Transition SQL:
|
|
|
|
```sql
|
|
DROP TABLE IF EXISTS outlook_discussions;
|
|
DROP TABLE IF EXISTS outlooks;
|
|
DROP TABLE IF EXISTS outlook_runs;
|
|
```
|
|
|
|
- Verification SQL examples:
|
|
|
|
```sql
|
|
SELECT table_name
|
|
FROM information_schema.tables
|
|
WHERE table_name IN ('outlook_runs', 'outlooks', 'outlook_discussions')
|
|
ORDER BY table_name;
|
|
```
|
|
|
|
After the updated daemon has started and recreated tables:
|
|
|
|
```sql
|
|
SELECT column_name, is_nullable, data_type
|
|
FROM information_schema.columns
|
|
WHERE table_name = 'outlook_runs'
|
|
AND column_name = 'discussion_count';
|
|
|
|
SELECT indexname
|
|
FROM pg_indexes
|
|
WHERE tablename = 'outlook_discussions'
|
|
ORDER BY indexname;
|
|
|
|
SELECT column_name
|
|
FROM information_schema.columns
|
|
WHERE table_name = 'outlooks'
|
|
AND column_name IN ('headline', 'summary', 'discussion');
|
|
```
|
|
|
|
- State that the final verification query for legacy `outlooks` prose columns should return zero rows.
|
|
- State that v1 outlook rows are intentionally removed and downstream readers should be updated intentionally.
|
|
|
|
### Tests
|
|
|
|
No code tests are required for the roadmap transition doc, but documentation consistency tests may need updates if they check table names or schema strings.
|
|
|
|
## Stage 6: Permanent Documentation Updates
|
|
|
|
After the implementation is complete, update permanent current-behavior documentation. Do not leave these as roadmap-only notes.
|
|
|
|
### Required Docs
|
|
|
|
Update `docs/policy/architecture.md`:
|
|
|
|
- Add a core design principle that `weatherfeeder` is location-focused and canonical weather events should represent weather data relevant to the configured forecast location or configured provider object; it is not intended to archive all provider data for all places.
|
|
|
|
Update `docs/integrations/events.md`:
|
|
|
|
- Document `weather.outlook.v2`.
|
|
- Document `WeatherOutlookRun.discussions` and `WeatherOutlookDiscussion` fields.
|
|
- Document that `outlooks[]` is location-filtered.
|
|
- Document `containsLocation` as an invariant that is true for emitted v2 outlooks.
|
|
- Document empty local runs with `outlooks: []` and `discussions: []`.
|
|
- Document supersession semantics:
|
|
- latest-run semantics preferred;
|
|
- historical supersession key is `provider`, `product`, `outlookType`, `validFrom`, `validTo`;
|
|
- greatest `issuedAt` wins;
|
|
- `day` and `label` are not identity;
|
|
- preserve all polygons from the selected latest `issuedAt` group.
|
|
- Keep any v1 notes clearly marked legacy/historical if retained.
|
|
|
|
Update `docs/integrations/spc.md`:
|
|
|
|
- State raw SPC bundles remain complete.
|
|
- State canonical output is location-filtered.
|
|
- State discussions are day-level and only included for retained outlook days.
|
|
- Remove or replace claims that all polygons are emitted or discussion text is attached to every polygon.
|
|
- Add supersession guidance or link to the canonical section in `docs/integrations/events.md`.
|
|
|
|
Update `docs/integrations/postgres.md`:
|
|
|
|
- Add `outlook_runs.discussion_count`.
|
|
- Add `outlook_discussions` table, columns, keys, indexes, prune column, and mapping source.
|
|
- Remove `outlooks.headline`, `outlooks.summary`, and `outlooks.discussion` from the current table contract.
|
|
- Document v2 reader reconstruction: read `outlook_runs`, `outlooks`, and `outlook_discussions` by `run_event_id` ordered by child indexes.
|
|
|
|
Update `docs/internal/postgres-sink.md` and `internal/sinks/postgres/doc.go`:
|
|
|
|
- Document the new table and mapper validation rules.
|
|
|
|
Update `docs/consumers/pkg-model.md` and `docs/consumers/api.md`:
|
|
|
|
- Add `WeatherOutlookDiscussion`.
|
|
- Update model examples for run-level discussions.
|
|
- Remove polygon-level prose from v2 consumer examples.
|
|
|
|
Update `docs/config.md` only if SPC prose still describes all-polygons behavior.
|
|
|
|
Update `docs/operations.md` or `docs/troubleshooting.md` only if operational behavior or recovery instructions need adjustment.
|
|
|
|
### Tests
|
|
|
|
- Update docs consistency tests for `weather.outlook.v2`.
|
|
- Update docs consistency tests for `outlook_discussions` if such tests exist or are practical.
|
|
- Ensure no permanent doc still describes v1 all-polygons behavior as current behavior:
|
|
|
|
```sh
|
|
rg "All outlook polygons|all polygons|attached to every outlook|weather\.outlook\.v1" docs README.md
|
|
```
|
|
|
|
## Stage 7: Full Verification And Release Readiness
|
|
|
|
### Focused Tests
|
|
|
|
```sh
|
|
go test ./model ./standards
|
|
go test ./internal/providers/spc ./internal/normalizers/spc ./internal/normalizers ./internal/geo
|
|
go test ./internal/sinks/postgres
|
|
go test ./internal/sources ./cmd/weatherfeeder
|
|
```
|
|
|
|
### Full Test Suite
|
|
|
|
```sh
|
|
go test ./...
|
|
```
|
|
|
|
### Manual Checks
|
|
|
|
- Confirm `cmd/weatherfeeder/config.yml` still loads and source registry tests pass.
|
|
- Confirm no current-behavior docs outside `docs/roadmap/` describe unimplemented behavior.
|
|
- Confirm `docs/roadmap/outlook-schema-transition.md` transition SQL and verification steps match the final table names and `internal/sinks/postgres/schema.go`.
|
|
- Confirm the SPC normalizer emits `weather.outlook.v2`, not `weather.outlook.v1`.
|
|
- Confirm a no-local-risk SPC bundle emits a canonical run with empty outlook and discussion arrays.
|
|
- Confirm a local-risk SPC bundle emits only containing polygons and only matching day discussions.
|
|
- Confirm retained v2 outlooks all have `containsLocation: true`.
|
|
|
|
## Weatherapi Coordination Notes
|
|
|
|
This stage is not implemented in `weatherfeeder`, but the weatherfeeder release should call it out for downstream work.
|
|
|
|
- Release `weatherfeeder` with `weather.outlook.v2` before updating `weatherapi` dependency.
|
|
- Update `weatherapi` Postgres reads to load `outlook_discussions`.
|
|
- Update weatherapi endpoint semantics to prefer latest-run behavior for current outlook endpoints.
|
|
- Remove or revise weatherapi filters that depend on historical `containsLocation=false` rows.
|
|
- Consider keeping `/outlooks/convective/location` as a compatibility alias for latest local outlooks if already exposed.
|
|
|
|
## Open Questions
|
|
|
|
None. This plan chooses the long-term maintainable options from `docs/roadmap/outlook.md`: schema v2, location-filtered canonical output, run-level discussions, destructive reset of the outlook table family, and permanent documentation of supersession semantics.
|