Add `weatherfeeder` support for Storm Prediction Center convective outlooks as a new canonical outlook domain. The feature should poll SPC GeoJSON outlook products, enrich them with Day 1-3 print-page discussion text, compute whether the configured forecast point is inside each active outlook polygon, normalize the result into a provider-independent schema, and persist it through existing sinks.
This is a new domain, not an extension of `weather.alert.v1`. SPC outlooks describe probabilistic/categorical risk areas over a validity window; alerts describe active actionable hazard messages.
## Public Contract
Add schema constants:
- Raw schema: `raw.spc.convective_outlook.v1`
- Canonical schema: `weather.outlook.v1`
Add source driver:
-`spc_convective_outlook`
Add event kind:
-`outlook`
Add canonical model types:
-`model.WeatherOutlookRun`
-`model.WeatherOutlook`
Recommended canonical run fields:
-`locationId`, `locationName`
-`latitude`, `longitude`
-`asOf`
-`issuedAt`
-`outlooks`
Recommended canonical outlook fields:
-`id`
-`provider`
-`product`
-`day`
-`outlookType`
-`label`
-`labelText`
-`severityRank`
-`validFrom`
-`validTo`
-`issuedAt`
-`expiresAt`
-`forecaster`
-`headline`
-`summary`
-`discussion`
-`sourceUrl`
-`imageUrl`
-`containsLocation`
-`geometry`
Contract defaults:
-`product` should be `convective`.
-`outlookType` should be one of `categorical`, `tornado`, `hail`, `wind`.
-`day` should be `1`, `2`, or `3` for this first implementation.
-`containsLocation` is computed against configured forecast coordinates.
-`geometry` should preserve compact GeoJSON geometry for auditability and future API use.
- Day 4-8 outlooks are out of scope for this first implementation. They have different product semantics and should be added in a later schema-compatible extension only after Day 1-3 support is proven.
The source should fetch a bundle of SPC products in one poll cycle and emit one raw event containing fetched GeoJSON products, fetched Day 1-3 print pages, optional RSS metadata, configured point metadata, and per-product fetch metadata. The source may decode minimal timestamps for event identity/effective time, but provider-to-canonical mapping belongs in the normalizer.
- Use print pages as the authoritative source for Day 1-3 outlook discussion text.
- Preserve source URL, page title/header where available, page-level `Updated:` timestamp where parseable, and the raw HTML or extracted `<pre>` product block in the raw bundle.
- Extract canonical `headline`, `summary`, `discussion`, and discussion metadata in the normalizer or shared SPC provider helper.
- Treat the GeoJSON files, not print pages, as authoritative for polygons and validity windows.
- Ignore page risk tables and image assets for v1 unless they are needed to support discussion parsing.
- Use a default cadence of `30m`, not daily/twice-daily. SPC current outlook files can update several times per day, and a 30-minute poll with HTTP caching is low cost and less likely to miss updates.
- Rely on ETag/Last-Modified handling from the HTTP source layer where available.
The source should own default SPC URLs, with optional params to override GeoJSON URLs, print-page URLs, and the RSS URL for tests or future provider changes.
- Preserve partial fetch errors as source errors for the first implementation; do not emit incomplete outlook bundles unless a later explicit degraded-mode policy is added.
- Enrich narrative fields from the matching Day 1-3 print page:
- parse the main product text from the `<pre>` block
- strip embedded tags and scripts before text parsing
- preserve line breaks in discussion text
- parse page/title metadata, product title, issued line, valid line, correction markers such as `CORR 1`, forecaster, summary, and full discussion where practical
- use print-page text for canonical `headline`, `summary`, and `discussion`
- Use RSS only as optional supplemental metadata when present; missing or incomplete RSS must not prevent GeoJSON plus print-page normalization.
- Set run `asOf` to the latest valid `issuedAt` across features, falling back to print-page `Updated:` timestamps, RSS `lastBuildDate`, then input event time.