Add roadmap and staged plan for implementing SPC convective outlook support
This commit is contained in:
591
docs/roadmap/implementation.md
Normal file
591
docs/roadmap/implementation.md
Normal file
@@ -0,0 +1,591 @@
|
|||||||
|
# SPC Convective Outlook Implementation Roadmap
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
|
||||||
|
This roadmap defines the concrete implementation sequence for
|
||||||
|
`docs/roadmap/outlook.md`. It is written for an LLM coding agent that will
|
||||||
|
implement the stages in order.
|
||||||
|
|
||||||
|
This is a future-work roadmap. Until a stage is implemented, non-roadmap docs
|
||||||
|
must not describe SPC convective outlook behavior as available.
|
||||||
|
|
||||||
|
## Source Feature Roadmap
|
||||||
|
|
||||||
|
Use `docs/roadmap/outlook.md` as the authoritative feature roadmap for intent,
|
||||||
|
target shape, and policy decisions. This implementation roadmap is the
|
||||||
|
step-by-step work plan. If the two documents conflict, update the feature
|
||||||
|
roadmap first when the target behavior changes, then update this implementation
|
||||||
|
plan.
|
||||||
|
|
||||||
|
## Locked Implementation Decisions
|
||||||
|
|
||||||
|
- Preserve existing CLI syntax, generated artifact paths, report IDs, prompt
|
||||||
|
IDs, distributor behavior, and Scriptorium invocation.
|
||||||
|
- Use source key and module IDs based on `spc_convective_outlooks`.
|
||||||
|
- Add two modules:
|
||||||
|
- `spc_convective_outlooks` in `applicable_risk_products`;
|
||||||
|
- `spc_convective_discussion` in `narrative_products`.
|
||||||
|
- Fetch the upstream Weather API source once in
|
||||||
|
`internal/adapters/weatherapi`; module builders must not fetch upstream data.
|
||||||
|
- Use a Weather API adapter constant for the path:
|
||||||
|
`/outlooks/convective`.
|
||||||
|
- Initial query parameters for the outlook endpoint are `format=json` and the
|
||||||
|
configured `tz`; do not send `precision`.
|
||||||
|
- Treat the source as optional under existing missing-source policy.
|
||||||
|
- Treat `data: null` as no latest run and therefore missing/unavailable
|
||||||
|
optional source data.
|
||||||
|
- Treat a non-null run with empty `outlooks` and `discussions` arrays as
|
||||||
|
checked, non-missing empty data.
|
||||||
|
- Preserve GeoJSON geometry in collected facts and persisted bundle artifacts,
|
||||||
|
but omit geometry from prompt-facing module output.
|
||||||
|
- Filter outlooks by overlap with the resolved report valid period in Go.
|
||||||
|
- Do not depend on `/outlooks/convective/active` or
|
||||||
|
`/outlooks/convective/location` for initial behavior.
|
||||||
|
- Include SPC discussion text only when at least one retained report-period
|
||||||
|
outlook has `severity_rank >= 3`.
|
||||||
|
- Define the discussion threshold as an internal constant, initially `3`, not a
|
||||||
|
config field.
|
||||||
|
|
||||||
|
## Stage 1: Weather Data Contract
|
||||||
|
|
||||||
|
Goal: add typed collected SPC outlook facts without changing fetching or prompt
|
||||||
|
output yet.
|
||||||
|
|
||||||
|
Files to inspect:
|
||||||
|
|
||||||
|
- `internal/weatherdata/bundle.go`
|
||||||
|
- `internal/facts/facts.go`
|
||||||
|
- `internal/module/module.go`
|
||||||
|
- `internal/briefing/modules.go`
|
||||||
|
- `internal/weatherdata` and `internal/facts` tests
|
||||||
|
|
||||||
|
Implementation:
|
||||||
|
|
||||||
|
- Add `ConvectiveOutlookRun` to `internal/weatherdata`.
|
||||||
|
- Add `ConvectiveOutlook` with fields matching the consumed Weather API
|
||||||
|
outlook fields:
|
||||||
|
- `id`
|
||||||
|
- `provider`
|
||||||
|
- `product`
|
||||||
|
- `day`
|
||||||
|
- `outlookType`
|
||||||
|
- `label`
|
||||||
|
- `labelText`
|
||||||
|
- `forecaster`
|
||||||
|
- `severityRank`
|
||||||
|
- `validFrom`
|
||||||
|
- `validTo`
|
||||||
|
- `issuedAt`
|
||||||
|
- `expiresAt`
|
||||||
|
- `sourceUrl`
|
||||||
|
- `imageUrl`
|
||||||
|
- `containsLocation`
|
||||||
|
- `geometry`
|
||||||
|
- Store `geometry` as `json.RawMessage` or an equivalent JSON-preserving type;
|
||||||
|
do not introduce a GeoJSON dependency.
|
||||||
|
- Add `ConvectiveOutlookDiscussion` with:
|
||||||
|
- `day`
|
||||||
|
- `headline`
|
||||||
|
- `summary`
|
||||||
|
- `discussion`
|
||||||
|
- `updatedAt`
|
||||||
|
- Add `SPCConvectiveOutlooks *ConvectiveOutlookRun` to
|
||||||
|
`weatherdata.Bundle`.
|
||||||
|
- Add matching fields to `facts.CollectedFacts`, `BuildCollected`, and
|
||||||
|
`CollectedFacts.Bundle`.
|
||||||
|
- Add module constants in `internal/module`:
|
||||||
|
- `SPCConvectiveOutlooks ID = "spc_convective_outlooks"`
|
||||||
|
- `SPCConvectiveDiscussion ID = "spc_convective_discussion"`
|
||||||
|
- Add option structs:
|
||||||
|
- `SPCConvectiveOutlooksOptions struct{}`
|
||||||
|
- `SPCConvectiveDiscussionOptions struct{}`
|
||||||
|
- Add fact requirement constants:
|
||||||
|
- `CollectedSPCConvectiveOutlooks`
|
||||||
|
- `RequiresDerivedSPCConvectiveOutlooks`
|
||||||
|
- Wire the new collected requirement into
|
||||||
|
`briefing.collectedFactAvailable`.
|
||||||
|
|
||||||
|
Acceptance criteria:
|
||||||
|
|
||||||
|
- No Weather API request is added in this stage.
|
||||||
|
- No report default module list changes in this stage.
|
||||||
|
- No prompt package shape changes in this stage.
|
||||||
|
- Existing tests pass.
|
||||||
|
|
||||||
|
Suggested validation:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go test ./internal/weatherdata ./internal/facts ./internal/module ./internal/briefing
|
||||||
|
```
|
||||||
|
|
||||||
|
This stage is small enough for one implementation prompt.
|
||||||
|
|
||||||
|
## Stage 2: Weather API Adapter Fetch
|
||||||
|
|
||||||
|
Goal: fetch `/outlooks/convective`, decode it into the new weather data
|
||||||
|
contract, and preserve source provenance.
|
||||||
|
|
||||||
|
Files to inspect:
|
||||||
|
|
||||||
|
- `internal/adapters/weatherapi/client.go`
|
||||||
|
- `internal/adapters/weatherapi/client_test.go`
|
||||||
|
- `internal/adapters/weatherapi/testdata/`
|
||||||
|
- `docs/roadmap/outlook.md`
|
||||||
|
- upstream Weather API docs under the Convective Outlooks section
|
||||||
|
|
||||||
|
Implementation:
|
||||||
|
|
||||||
|
- Add a package-level adapter constant, for example:
|
||||||
|
`convectiveOutlooksEndpoint = "/outlooks/convective"`.
|
||||||
|
- Add source-name constant or narrowly scoped source key for
|
||||||
|
`spc_convective_outlooks`.
|
||||||
|
- Add fan-out fetch for the new optional source.
|
||||||
|
- Build the query as:
|
||||||
|
- `format=json`
|
||||||
|
- `tz=<configured weather_api.timezone>`
|
||||||
|
- Do not send `precision` to this endpoint.
|
||||||
|
- Do not send `units` to this endpoint. If existing query helpers add units by
|
||||||
|
default, add a route-specific option such as `omitUnits` so the final request
|
||||||
|
remains `format` plus `tz`.
|
||||||
|
- Decode the Weather API envelope and payload into
|
||||||
|
`weatherdata.ConvectiveOutlookRun`.
|
||||||
|
- Preserve `data: null` as optional missing/unavailable source data through
|
||||||
|
existing missing-source policy.
|
||||||
|
- Preserve a non-null run with empty arrays as checked non-missing source data.
|
||||||
|
- Record source provenance:
|
||||||
|
- source name `spc_convective_outlooks`;
|
||||||
|
- endpoint constant path;
|
||||||
|
- exact query parameters sent;
|
||||||
|
- fetched time;
|
||||||
|
- issued time from run `issuedAt` when present, otherwise `asOf`;
|
||||||
|
- source hash over compact raw `data` JSON;
|
||||||
|
- source warnings when missing-source policy emits them.
|
||||||
|
- Add `internal/adapters/weatherapi/testdata/convective_outlooks.json`.
|
||||||
|
|
||||||
|
Acceptance criteria:
|
||||||
|
|
||||||
|
- Complete fixture fetch includes `bundle.SPCConvectiveOutlooks`.
|
||||||
|
- Source record is present and non-missing when the payload has a non-null run.
|
||||||
|
- `data: null` follows optional missing-source policy.
|
||||||
|
- Non-null empty arrays do not produce a missing-source warning.
|
||||||
|
- Adapter tests assert request path and query, including absence of
|
||||||
|
`precision`.
|
||||||
|
- Existing hourly required-source behavior is unchanged.
|
||||||
|
|
||||||
|
Suggested validation:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go test ./internal/adapters/weatherapi ./internal/weatherdata ./internal/facts
|
||||||
|
```
|
||||||
|
|
||||||
|
This stage is small enough for one implementation prompt.
|
||||||
|
|
||||||
|
## Stage 3: Report-Period Outlook Filtering
|
||||||
|
|
||||||
|
Goal: derive report-scoped SPC outlook facts by valid-period overlap.
|
||||||
|
|
||||||
|
Files to inspect:
|
||||||
|
|
||||||
|
- `internal/facts/facts.go`
|
||||||
|
- `internal/forecast/derive.go`
|
||||||
|
- `internal/timeutil/periods.go`
|
||||||
|
- `internal/report/*_report.go`
|
||||||
|
- existing forecast/facts tests for period slicing
|
||||||
|
|
||||||
|
Implementation:
|
||||||
|
|
||||||
|
- Add a report-scoped derived value for retained outlooks and discussions.
|
||||||
|
Recommended shape:
|
||||||
|
- `DerivedFacts.SPCConvectiveOutlooks []weatherdata.ConvectiveOutlook`
|
||||||
|
- `DerivedFacts.SPCConvectiveDiscussions []weatherdata.ConvectiveOutlookDiscussion`
|
||||||
|
- Add a small deterministic helper under `internal/facts`; this first
|
||||||
|
implementation is report-scoped selection of already-collected facts, not a
|
||||||
|
broader meteorological derivation.
|
||||||
|
- Select outlooks whose half-open valid interval overlaps the resolved report
|
||||||
|
valid period.
|
||||||
|
- Treat missing `severityRank` as lower than the discussion threshold, but do
|
||||||
|
not drop the outlook from the risk-product module solely because rank is
|
||||||
|
missing.
|
||||||
|
- Retain discussion records only for days represented by retained outlooks.
|
||||||
|
- Sort retained outlooks deterministically by:
|
||||||
|
- day;
|
||||||
|
- outlook type;
|
||||||
|
- severity rank descending when present;
|
||||||
|
- valid start;
|
||||||
|
- label;
|
||||||
|
- id.
|
||||||
|
- Sort retained discussions by day, then updated time when present.
|
||||||
|
- Keep empty retained slices distinct from a missing collected source.
|
||||||
|
|
||||||
|
Acceptance criteria:
|
||||||
|
|
||||||
|
- Daily Today, Daily Tomorrow, 3-Day, Weekend, and Storm valid periods select
|
||||||
|
expected outlooks by overlap.
|
||||||
|
- Tomorrow and multi-day reports do not depend on server-current active
|
||||||
|
filtering.
|
||||||
|
- Empty retained results are still available to modules as checked empty data
|
||||||
|
when the collected source exists.
|
||||||
|
|
||||||
|
Suggested validation:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go test ./internal/facts ./internal/forecast ./internal/report
|
||||||
|
```
|
||||||
|
|
||||||
|
This stage is small enough for one implementation prompt.
|
||||||
|
|
||||||
|
## Stage 4: Prompt Category Plumbing
|
||||||
|
|
||||||
|
Goal: prepare prompt-package category placement without registering
|
||||||
|
builderless modules.
|
||||||
|
|
||||||
|
Files to inspect:
|
||||||
|
|
||||||
|
- `internal/module/module.go`
|
||||||
|
- `internal/promptinput/package.go`
|
||||||
|
- `internal/promptinput/package_test.go`
|
||||||
|
|
||||||
|
Implementation:
|
||||||
|
|
||||||
|
- Add prompt input category mapping:
|
||||||
|
- `spc_convective_outlooks` -> `applicable_risk_products`;
|
||||||
|
- `spc_convective_discussion` -> `narrative_products`.
|
||||||
|
- Use synthetic module snapshots in tests if needed; do not add module
|
||||||
|
definitions to `defaultModuleDefinitions` until the real builders are added
|
||||||
|
in Stages 5 and 6.
|
||||||
|
|
||||||
|
Acceptance criteria:
|
||||||
|
|
||||||
|
- Prompt category tests prove both new stanzas route to the intended groups.
|
||||||
|
- The module registry still rejects unknown or builderless modules.
|
||||||
|
- No report default includes the new modules yet.
|
||||||
|
|
||||||
|
Suggested validation:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go test ./internal/module ./internal/promptinput
|
||||||
|
```
|
||||||
|
|
||||||
|
This stage is small enough for one implementation prompt.
|
||||||
|
|
||||||
|
## Stage 5: SPC Convective Outlooks Module
|
||||||
|
|
||||||
|
Goal: add the prompt-facing risk-product module.
|
||||||
|
|
||||||
|
Files to inspect:
|
||||||
|
|
||||||
|
- `internal/briefing/alert_digest_module.go`
|
||||||
|
- `internal/briefing/weather_story_module.go`
|
||||||
|
- `internal/briefing/module_format_helpers.go`
|
||||||
|
- `internal/briefing/base_modules_test.go`
|
||||||
|
- `docs/roadmap/outlook.md`
|
||||||
|
|
||||||
|
Implementation:
|
||||||
|
|
||||||
|
- Add `internal/briefing/spc_convective_outlooks_module.go`.
|
||||||
|
- Add the `SPCConvectiveOutlooks` module definition to
|
||||||
|
`defaultModuleDefinitions` in the same change as its real builder.
|
||||||
|
- Register stanza `spc_convective_outlooks`.
|
||||||
|
- Require:
|
||||||
|
- `CollectedSPCConvectiveOutlooks`;
|
||||||
|
- `RequiresDerivedSPCConvectiveOutlooks`.
|
||||||
|
- Use `MissingDataEmpty` so checked empty data can emit an explicit empty
|
||||||
|
risk-product stanza.
|
||||||
|
- Build from collected source metadata plus derived retained outlooks.
|
||||||
|
- Emit concise prompt-facing fields:
|
||||||
|
- `checked`;
|
||||||
|
- `as_of`;
|
||||||
|
- `issued_at`;
|
||||||
|
- `location_id`;
|
||||||
|
- `location_name`;
|
||||||
|
- `outlook_count`;
|
||||||
|
- `outlooks`.
|
||||||
|
- For each outlook, emit:
|
||||||
|
- `day`;
|
||||||
|
- `outlook_type`;
|
||||||
|
- `label`;
|
||||||
|
- `label_text`;
|
||||||
|
- `severity_rank`;
|
||||||
|
- `valid_start`;
|
||||||
|
- `valid_end`;
|
||||||
|
- `issued_at`;
|
||||||
|
- `expires_at`;
|
||||||
|
- `contains_location`;
|
||||||
|
- `source_url`;
|
||||||
|
- `image_url`.
|
||||||
|
- Use human-readable local time helpers consistent with current modules.
|
||||||
|
- Do not emit GeoJSON geometry.
|
||||||
|
- If the source was checked and no retained outlooks overlap the report
|
||||||
|
period, emit `checked: true`, `outlook_count: 0`, and an empty or omitted
|
||||||
|
`outlooks` list according to the existing YAML style for empty lists.
|
||||||
|
|
||||||
|
Acceptance criteria:
|
||||||
|
|
||||||
|
- Module output is deterministic and omits geometry.
|
||||||
|
- Checked empty data produces an explicit checked-empty stanza.
|
||||||
|
- Missing collected source follows registry missing-data behavior.
|
||||||
|
- Module tests cover populated, checked-empty, and missing cases.
|
||||||
|
|
||||||
|
Suggested validation:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go test ./internal/briefing ./internal/module ./internal/promptinput
|
||||||
|
```
|
||||||
|
|
||||||
|
This stage is small enough for one implementation prompt.
|
||||||
|
|
||||||
|
## Stage 6: SPC Convective Discussion Module
|
||||||
|
|
||||||
|
Goal: add optional SPC discussion narrative context with a severity threshold.
|
||||||
|
|
||||||
|
Files to inspect:
|
||||||
|
|
||||||
|
- `internal/briefing/area_forecast_discussion_module.go`
|
||||||
|
- `internal/briefing/weather_story_module.go`
|
||||||
|
- `internal/briefing/module_format_helpers.go`
|
||||||
|
- `internal/briefing/base_modules_test.go`
|
||||||
|
|
||||||
|
Implementation:
|
||||||
|
|
||||||
|
- Add `internal/briefing/spc_convective_discussion_module.go`.
|
||||||
|
- Add the `SPCConvectiveDiscussion` module definition to
|
||||||
|
`defaultModuleDefinitions` in the same change as its real builder.
|
||||||
|
- Register stanza `spc_convective_discussion`.
|
||||||
|
- Require:
|
||||||
|
- `CollectedSPCConvectiveOutlooks`;
|
||||||
|
- `RequiresDerivedSPCConvectiveOutlooks`.
|
||||||
|
- Use `MissingDataOmit` so unavailable or below-threshold discussion text is
|
||||||
|
omitted.
|
||||||
|
- Define a package-private constant near the module, for example:
|
||||||
|
`defaultSPCConvectiveDiscussionMinimumSeverityRank = 3`.
|
||||||
|
- Build from derived retained outlooks and discussions.
|
||||||
|
- Include discussion text only when at least one retained outlook has
|
||||||
|
`severityRank >= defaultSPCConvectiveDiscussionMinimumSeverityRank`.
|
||||||
|
- When the threshold is not met, return `nil` output so the stanza is omitted.
|
||||||
|
- When threshold is met, include discussions for retained outlook days with:
|
||||||
|
- `day`;
|
||||||
|
- `headline`;
|
||||||
|
- `summary`;
|
||||||
|
- `discussion`;
|
||||||
|
- `updated_at`.
|
||||||
|
- Include a concise reason field such as:
|
||||||
|
`included_because: "severity_rank >= 3"`.
|
||||||
|
|
||||||
|
Acceptance criteria:
|
||||||
|
|
||||||
|
- Slight Risk or higher retained outlooks include matching discussion records
|
||||||
|
when available.
|
||||||
|
- Lower-risk retained outlooks still appear in `spc_convective_outlooks` but
|
||||||
|
do not emit `spc_convective_discussion`.
|
||||||
|
- Missing discussion text omits the stanza without failing report generation.
|
||||||
|
- Tests cover threshold below, threshold equal, threshold above, and missing
|
||||||
|
discussion cases.
|
||||||
|
|
||||||
|
Suggested validation:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go test ./internal/briefing ./internal/promptinput
|
||||||
|
```
|
||||||
|
|
||||||
|
This stage is small enough for one implementation prompt.
|
||||||
|
|
||||||
|
## Stage 7: Report Composition And Config Examples
|
||||||
|
|
||||||
|
Goal: add the implemented modules to default report definitions and maintained
|
||||||
|
examples.
|
||||||
|
|
||||||
|
Files to inspect:
|
||||||
|
|
||||||
|
- `internal/report/daily_report.go`
|
||||||
|
- `internal/report/three_day_report.go`
|
||||||
|
- `internal/report/weekend_report.go`
|
||||||
|
- `internal/report/storm_report.go`
|
||||||
|
- `internal/config/reports.go`
|
||||||
|
- `internal/config/config_test.go`
|
||||||
|
- `examples/config.yml`
|
||||||
|
|
||||||
|
Implementation:
|
||||||
|
|
||||||
|
- Add `spc_convective_outlooks` to default report module lists for:
|
||||||
|
- Daily Today;
|
||||||
|
- Daily Tomorrow;
|
||||||
|
- 3-Day;
|
||||||
|
- Weekend;
|
||||||
|
- Storm.
|
||||||
|
- Place `spc_convective_outlooks` immediately after `alert_digest` when
|
||||||
|
`alert_digest` is present.
|
||||||
|
- Add `spc_convective_discussion` immediately after
|
||||||
|
`area_forecast_discussion` when `area_forecast_discussion` is present.
|
||||||
|
- Update maintained example config module overrides if they enumerate module
|
||||||
|
lists.
|
||||||
|
- Keep CLI syntax and config field names unchanged.
|
||||||
|
|
||||||
|
Acceptance criteria:
|
||||||
|
|
||||||
|
- All default report module compositions validate.
|
||||||
|
- Example config loads successfully.
|
||||||
|
- Config override tests can include both new module IDs.
|
||||||
|
- Existing report IDs, prompt IDs, output names, and valid-period behavior are
|
||||||
|
unchanged.
|
||||||
|
|
||||||
|
Suggested validation:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go test ./internal/report ./internal/config ./internal/briefing ./internal/app
|
||||||
|
```
|
||||||
|
|
||||||
|
This stage is small enough for one implementation prompt.
|
||||||
|
|
||||||
|
## Stage 8: App And Prompt Workflow Coverage
|
||||||
|
|
||||||
|
Goal: prove the end-to-end generated data package contains the new stanzas in
|
||||||
|
the intended categories when fixture data warrants them.
|
||||||
|
|
||||||
|
Files to inspect:
|
||||||
|
|
||||||
|
- `internal/app/app_test.go`
|
||||||
|
- `internal/promptinput/package_test.go`
|
||||||
|
- Weather API test server fixtures in `internal/adapters/weatherapi`
|
||||||
|
- `internal/state` artifact path behavior if tests inspect saved files
|
||||||
|
|
||||||
|
Implementation:
|
||||||
|
|
||||||
|
- Extend app-level Weather API fixtures to serve convective outlook data.
|
||||||
|
- Add or update workflow tests so saved YAML contains:
|
||||||
|
- `briefing.applicable_risk_products.spc_convective_outlooks`;
|
||||||
|
- `briefing.narrative_products.spc_convective_discussion` when severity rank
|
||||||
|
is at least `3`;
|
||||||
|
- no geometry in prompt-facing YAML.
|
||||||
|
- Add a workflow case where outlook data is checked but empty and the
|
||||||
|
risk-product stanza remains explicit.
|
||||||
|
- Add a workflow case where severity rank is below `3` and the discussion
|
||||||
|
stanza is omitted.
|
||||||
|
- Keep existing Recent Changes behavior unchanged unless a later roadmap adds
|
||||||
|
comparisons for SPC outlooks.
|
||||||
|
|
||||||
|
Acceptance criteria:
|
||||||
|
|
||||||
|
- Module snapshot and YAML data package remain deterministic.
|
||||||
|
- Prompt category grouping preserves module order within categories.
|
||||||
|
- No Scriptorium or distributor behavior changes are required.
|
||||||
|
|
||||||
|
Suggested validation:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go test ./internal/app ./internal/promptinput ./internal/state
|
||||||
|
```
|
||||||
|
|
||||||
|
This stage is small enough for one implementation prompt.
|
||||||
|
|
||||||
|
## Stage 9: Implemented Documentation
|
||||||
|
|
||||||
|
Goal: update non-roadmap docs after the feature is implemented.
|
||||||
|
|
||||||
|
Files to inspect and update:
|
||||||
|
|
||||||
|
- `docs/integrations/weatherapi.md`
|
||||||
|
- `docs/internal/weather-data.md`
|
||||||
|
- `docs/internal/facts.md`
|
||||||
|
- `docs/internal/module.md`
|
||||||
|
- `docs/internal/briefing.md`
|
||||||
|
- `docs/internal/prompt-input.md`
|
||||||
|
- `docs/config.md` if example module override behavior changes
|
||||||
|
- `examples/config.yml` if not already updated in Stage 7
|
||||||
|
|
||||||
|
Documentation requirements:
|
||||||
|
|
||||||
|
- Describe only the implemented SPC behavior outside `docs/roadmap/`.
|
||||||
|
- In the Weather API integration doc, include only the route, query, response
|
||||||
|
fields, and missing/empty semantics used by weatherreporter.
|
||||||
|
- In internal docs, distinguish:
|
||||||
|
- collected source facts and geometry/provenance;
|
||||||
|
- derived report-period filtering;
|
||||||
|
- prompt-facing module output that omits geometry.
|
||||||
|
- In prompt-input docs, list:
|
||||||
|
- `spc_convective_outlooks` under `applicable_risk_products`;
|
||||||
|
- `spc_convective_discussion` under `narrative_products`.
|
||||||
|
- Keep deferred route choices, geometry presentation, and user-configurable
|
||||||
|
threshold ideas under roadmap docs only.
|
||||||
|
|
||||||
|
Acceptance criteria:
|
||||||
|
|
||||||
|
- Non-roadmap docs do not describe deferred SPC behavior as current behavior.
|
||||||
|
- Maintained examples load.
|
||||||
|
- Documentation links and module ID lists are consistent with code.
|
||||||
|
|
||||||
|
Suggested validation:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go test ./internal/config
|
||||||
|
git diff --check
|
||||||
|
```
|
||||||
|
|
||||||
|
This stage is small enough for one implementation prompt.
|
||||||
|
|
||||||
|
## Stage 10: Final Validation
|
||||||
|
|
||||||
|
Goal: validate the complete feature and guard against regressions.
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go test ./internal/adapters/weatherapi ./internal/weatherdata ./internal/facts ./internal/forecast ./internal/briefing ./internal/module ./internal/report ./internal/config ./internal/app ./internal/promptinput
|
||||||
|
go test ./...
|
||||||
|
go run ./cmd/weatherreporter --help
|
||||||
|
git diff --check
|
||||||
|
```
|
||||||
|
|
||||||
|
Manual review:
|
||||||
|
|
||||||
|
- Confirm `/outlooks/convective` is referenced through an adapter constant.
|
||||||
|
- Confirm Weather API outlook requests do not send `precision`.
|
||||||
|
- Confirm no module builder performs Weather API calls.
|
||||||
|
- Confirm prompt YAML omits GeoJSON geometry.
|
||||||
|
- Confirm checked-empty outlook data is not represented as missing data.
|
||||||
|
- Confirm SPC discussion text appears only at severity rank `3` or higher.
|
||||||
|
- Confirm public CLI syntax, output paths, distributor upload behavior, and
|
||||||
|
Scriptorium argv remain unchanged.
|
||||||
|
|
||||||
|
This stage is small enough for one implementation prompt.
|
||||||
|
|
||||||
|
## Deferred Work
|
||||||
|
|
||||||
|
Out of scope for the initial implementation:
|
||||||
|
|
||||||
|
- use of `/outlooks/convective/active`;
|
||||||
|
- use of `/outlooks/convective/location`;
|
||||||
|
- per-report Weather API filters such as `day` or `outlookType`;
|
||||||
|
- user-configurable SPC discussion severity threshold;
|
||||||
|
- prompt-facing GeoJSON geometry;
|
||||||
|
- polygon distance, area, map summaries, or rendered images;
|
||||||
|
- Mesoscale Discussions, watches, WPC outlooks, radar, QPF, or other risk
|
||||||
|
products;
|
||||||
|
- Recent Changes comparisons for SPC outlook changes;
|
||||||
|
- module-owned upstream fetching.
|
||||||
|
|
||||||
|
## Global Validation Checklist
|
||||||
|
|
||||||
|
Before considering the feature complete:
|
||||||
|
|
||||||
|
- all focused package tests pass;
|
||||||
|
- `go test ./...` passes;
|
||||||
|
- `go run ./cmd/weatherreporter --help` still matches documented CLI syntax;
|
||||||
|
- `git diff --check` passes;
|
||||||
|
- examples load through config tests;
|
||||||
|
- non-roadmap docs describe only implemented behavior;
|
||||||
|
- no secret values or large raw geometry are introduced into prompt-facing
|
||||||
|
output;
|
||||||
|
- source provenance and warnings remain inspectable through existing metadata
|
||||||
|
and state artifacts.
|
||||||
|
|
||||||
|
## Open Questions
|
||||||
|
|
||||||
|
No question blocks implementation.
|
||||||
|
|
||||||
|
The recommended approach is to implement the locked decisions exactly as
|
||||||
|
described above. The main viable alternative is to query
|
||||||
|
`/outlooks/convective/active` or `/outlooks/convective/location`, but that
|
||||||
|
would make tomorrow, multi-day, weekend, and event reports depend on
|
||||||
|
server-current active filtering rather than report valid periods. That
|
||||||
|
alternative should be deferred unless fixture payloads from the base latest-run
|
||||||
|
route prove too large or too irrelevant for prompt use.
|
||||||
190
docs/roadmap/outlook.md
Normal file
190
docs/roadmap/outlook.md
Normal file
@@ -0,0 +1,190 @@
|
|||||||
|
# SPC Convective Outlook Roadmap
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
|
||||||
|
This roadmap defines future work to add SPC convective outlook support to
|
||||||
|
`weatherreporter`. The feature is not implemented yet, so current user,
|
||||||
|
operator, integration, and internal documentation must not describe it as
|
||||||
|
available behavior until the implementation lands.
|
||||||
|
|
||||||
|
The goal is to consume location-filtered SPC convective outlook facts from the
|
||||||
|
Weather API once per report run, preserve source provenance, and expose concise
|
||||||
|
prompt-facing risk and narrative stanzas without making modules responsible for
|
||||||
|
upstream fetching.
|
||||||
|
|
||||||
|
## Upstream Contract
|
||||||
|
|
||||||
|
The Weather API currently documents these convective outlook routes:
|
||||||
|
|
||||||
|
- `GET /outlooks/convective`
|
||||||
|
- `GET /outlooks/convective/active`
|
||||||
|
- `GET /outlooks/convective/location`
|
||||||
|
|
||||||
|
The initial `weatherreporter` integration should use the latest-run route,
|
||||||
|
`/outlooks/convective`, because report valid periods may target tomorrow,
|
||||||
|
multi-day, weekend, or event windows. The `/active` and `/location` routes
|
||||||
|
filter using the server's current UTC time, which is useful for "active right
|
||||||
|
now" views but is too narrow for report-period-oriented generation.
|
||||||
|
|
||||||
|
Important response semantics:
|
||||||
|
|
||||||
|
- `data: null` means no latest outlook run exists.
|
||||||
|
- a non-null `data` object with empty `outlooks` and `discussions` arrays means
|
||||||
|
the endpoint was checked successfully and no matching outlooks were present.
|
||||||
|
- `precision` and unknown query parameters are rejected by the upstream
|
||||||
|
outlook routes.
|
||||||
|
- outlook GeoJSON coordinates use longitude, latitude order.
|
||||||
|
- `format`, `units`, and `tz` are supported by the upstream contract, but the
|
||||||
|
initial weatherreporter request should send only values needed for JSON
|
||||||
|
decoding and local-time presentation.
|
||||||
|
|
||||||
|
## Locked Decisions
|
||||||
|
|
||||||
|
- Add the source as an optional Weather API source named
|
||||||
|
`spc_convective_outlooks`.
|
||||||
|
- Define the Weather API endpoint path with a package-level constant in the
|
||||||
|
Weather API adapter rather than embedding a string literal throughout the
|
||||||
|
implementation.
|
||||||
|
- Start with the base latest-run route, not `/active` or `/location`.
|
||||||
|
- Fetch outlook facts once in the Weather API adapter and expose them through
|
||||||
|
`weatherdata.Bundle`, `facts.CollectedFacts`, and report-scoped derived
|
||||||
|
filtering.
|
||||||
|
- Do not let module builders make Weather API calls.
|
||||||
|
- Keep GeoJSON geometry in collected facts and persisted bundle/debug artifacts,
|
||||||
|
but omit geometry from prompt-facing module output by default.
|
||||||
|
- Add two prompt-facing modules backed by the same collected source:
|
||||||
|
`spc_convective_outlooks` and `spc_convective_discussion`.
|
||||||
|
- Place `spc_convective_outlooks` under `applicable_risk_products`.
|
||||||
|
- Place `spc_convective_discussion` under `narrative_products`, immediately
|
||||||
|
after `area_forecast_discussion` in report module order when both are present.
|
||||||
|
- Include SPC outlook discussion text only when at least one retained outlook
|
||||||
|
for the report valid period has `severity_rank >= 3`.
|
||||||
|
- Define that threshold as an internal constant so it can be adjusted later
|
||||||
|
without searching through module code.
|
||||||
|
- Treat a non-null run with empty arrays as checked empty data, not missing
|
||||||
|
data.
|
||||||
|
- Treat `data: null`, HTTP errors, and malformed payloads as optional-source
|
||||||
|
missing or malformed conditions using the configured missing-source policy.
|
||||||
|
|
||||||
|
## Target Internal Shape
|
||||||
|
|
||||||
|
Add normalized collected facts to `internal/weatherdata`:
|
||||||
|
|
||||||
|
- `ConvectiveOutlookRun`
|
||||||
|
- `ConvectiveOutlook`
|
||||||
|
- `ConvectiveOutlookDiscussion`
|
||||||
|
|
||||||
|
The run should include upstream run metadata, ordered outlooks, ordered
|
||||||
|
discussions, and enough raw/provenance data for inspection. The bundle should
|
||||||
|
gain a field similar to:
|
||||||
|
|
||||||
|
```go
|
||||||
|
SPCConvectiveOutlooks *weatherdata.ConvectiveOutlookRun
|
||||||
|
```
|
||||||
|
|
||||||
|
`facts.CollectedFacts` should expose the same collected source. Derived facts
|
||||||
|
should provide report-period-filtered outlooks and discussions, or a small
|
||||||
|
forecast/facts helper should perform that filtering before module builders
|
||||||
|
shape prompt output. The filtering rule should use overlap with the resolved
|
||||||
|
report valid period, not server-current active status.
|
||||||
|
|
||||||
|
## Prompt-Facing Shape
|
||||||
|
|
||||||
|
The risk-product module should be concise and location-oriented:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
briefing:
|
||||||
|
applicable_risk_products:
|
||||||
|
spc_convective_outlooks:
|
||||||
|
checked: true
|
||||||
|
as_of: "2026-06-12 at 7:00 AM"
|
||||||
|
issued_at: "2026-06-12 at 6:00 AM"
|
||||||
|
outlooks:
|
||||||
|
- day: 1
|
||||||
|
outlook_type: categorical
|
||||||
|
label: SLGT
|
||||||
|
label_text: Slight Risk
|
||||||
|
severity_rank: 3
|
||||||
|
valid_start: "2026-06-12 at 8:00 AM"
|
||||||
|
valid_end: "2026-06-13 at 7:00 AM"
|
||||||
|
contains_location: true
|
||||||
|
source_url: "https://..."
|
||||||
|
image_url: "https://..."
|
||||||
|
```
|
||||||
|
|
||||||
|
The discussion module should be separate narrative context:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
briefing:
|
||||||
|
narrative_products:
|
||||||
|
area_forecast_discussion: {}
|
||||||
|
spc_convective_discussion:
|
||||||
|
included_because: "severity_rank >= 3"
|
||||||
|
discussions:
|
||||||
|
- day: 1
|
||||||
|
headline: "Severe storms possible"
|
||||||
|
summary: "Scattered severe storms are possible."
|
||||||
|
discussion: "SPC discussion text."
|
||||||
|
updated_at: "2026-06-12 at 6:30 AM"
|
||||||
|
```
|
||||||
|
|
||||||
|
If outlook data is checked successfully and no report-period outlooks apply,
|
||||||
|
the risk-product stanza should make that explicit with `checked: true` and an
|
||||||
|
empty outlook count or empty list. The discussion stanza should be omitted when
|
||||||
|
the severity threshold is not met or no relevant discussion is available.
|
||||||
|
|
||||||
|
## Implementation Plan
|
||||||
|
|
||||||
|
The staged implementation plan for this feature lives in
|
||||||
|
`docs/roadmap/implementation.md`. This document remains the feature roadmap:
|
||||||
|
it defines the target state, user intent, and policy decisions that future
|
||||||
|
implementation work should preserve.
|
||||||
|
|
||||||
|
## Test Plan
|
||||||
|
|
||||||
|
Add focused coverage for:
|
||||||
|
|
||||||
|
- Weather API decode of run metadata, outlooks, discussions, and geometry;
|
||||||
|
- endpoint path/query construction using the endpoint constant;
|
||||||
|
- `data: null` optional-source policy behavior;
|
||||||
|
- non-null empty `outlooks`/`discussions` as checked empty data;
|
||||||
|
- source provenance and data hash recording;
|
||||||
|
- valid-period overlap filtering for today, tomorrow, 3-day, weekend, and
|
||||||
|
storm windows;
|
||||||
|
- risk-product module output, omitted geometry, checked-empty behavior, and
|
||||||
|
prompt category;
|
||||||
|
- discussion module severity threshold behavior and prompt category;
|
||||||
|
- report default composition validation;
|
||||||
|
- app or prompt-input workflow proving generated YAML contains the new stanzas
|
||||||
|
when fixture data warrants them.
|
||||||
|
|
||||||
|
Run at minimum:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go test ./internal/adapters/weatherapi ./internal/weatherdata ./internal/facts ./internal/briefing ./internal/module ./internal/report ./internal/app ./internal/promptinput
|
||||||
|
go test ./...
|
||||||
|
go run ./cmd/weatherreporter --help
|
||||||
|
git diff --check
|
||||||
|
```
|
||||||
|
|
||||||
|
## Deferred Work
|
||||||
|
|
||||||
|
Do not include these in the first implementation unless a separate roadmap
|
||||||
|
expands the scope:
|
||||||
|
|
||||||
|
- calling `/outlooks/convective/active` or `/outlooks/convective/location`;
|
||||||
|
- user-configurable SPC discussion severity threshold;
|
||||||
|
- prompt-facing GeoJSON geometry;
|
||||||
|
- polygon distance, area, or map-rendered risk summaries;
|
||||||
|
- Mesoscale Discussions, watches, WPC outlooks, radar, QPF, or other risk
|
||||||
|
products;
|
||||||
|
- module-owned upstream fetching;
|
||||||
|
- custom per-report Weather API query filters such as `day` or `outlookType`.
|
||||||
|
|
||||||
|
## Open Questions
|
||||||
|
|
||||||
|
No open question blocks implementation. The recommended defaults above should
|
||||||
|
be used for the first pass. If fixture testing shows that the base latest-run
|
||||||
|
route includes too much irrelevant data, the viable alternative is to add
|
||||||
|
report-aware adapter query filters later, but that should be driven by observed
|
||||||
|
payload size or prompt quality rather than by the initial design.
|
||||||
@@ -21,7 +21,7 @@ notify:
|
|||||||
token_env: DISTRIBUTOR_UPLOAD_TOKEN
|
token_env: DISTRIBUTOR_UPLOAD_TOKEN
|
||||||
timeout: 30s
|
timeout: 30s
|
||||||
failure_policy: error
|
failure_policy: error
|
||||||
pipeline_id_template: "weatherreporter.{artifact_group}"
|
pipeline_id_template: "weatherreporter.{report_id}"
|
||||||
bundle_id_template: "weatherreporter.{location_id}.{report_id}"
|
bundle_id_template: "weatherreporter.{location_id}.{report_id}"
|
||||||
idempotency_key_template: "{bundle_id}.{run_id}"
|
idempotency_key_template: "{bundle_id}.{run_id}"
|
||||||
report_path_templates:
|
report_path_templates:
|
||||||
|
|||||||
@@ -85,7 +85,7 @@ func TestLoadExampleConfig(t *testing.T) {
|
|||||||
if cfg.Location.ID != "home" || cfg.Location.Name != "Brentwood" || cfg.Location.Region != "St. Louis Metro" {
|
if cfg.Location.ID != "home" || cfg.Location.Name != "Brentwood" || cfg.Location.Region != "St. Louis Metro" {
|
||||||
t.Fatalf("Location = %#v, want example location", cfg.Location)
|
t.Fatalf("Location = %#v, want example location", cfg.Location)
|
||||||
}
|
}
|
||||||
if cfg.Notify.Distributor.PipelineIDTemplate != "weatherreporter.{artifact_group}" {
|
if cfg.Notify.Distributor.PipelineIDTemplate != "weatherreporter.{report_id}" {
|
||||||
t.Fatalf("PipelineIDTemplate = %q, want example pipeline template", cfg.Notify.Distributor.PipelineIDTemplate)
|
t.Fatalf("PipelineIDTemplate = %q, want example pipeline template", cfg.Notify.Distributor.PipelineIDTemplate)
|
||||||
}
|
}
|
||||||
if len(cfg.Notify.Distributor.ReportPathTemplates) != 1 {
|
if len(cfg.Notify.Distributor.ReportPathTemplates) != 1 {
|
||||||
@@ -599,7 +599,7 @@ func TestLoadFileLoadsSecretsBeforeReturningNotifyConfig(t *testing.T) {
|
|||||||
"notify:\n" +
|
"notify:\n" +
|
||||||
" distributor:\n" +
|
" distributor:\n" +
|
||||||
" enabled: true\n" +
|
" enabled: true\n" +
|
||||||
" pipeline_id_template: weatherreporter.{artifact_group}\n"
|
" pipeline_id_template: weatherreporter.{report_id}\n"
|
||||||
if err := os.WriteFile(path, []byte(configYAML), 0o600); err != nil {
|
if err := os.WriteFile(path, []byte(configYAML), 0o600); err != nil {
|
||||||
t.Fatalf("write config fixture: %v", err)
|
t.Fatalf("write config fixture: %v", err)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user