Add a roadmap and implementation plan for rolling near-term forecast reports

This commit is contained in:
2026-06-12 12:24:07 -05:00
parent e6a4bb2d16
commit 0b1423c90d
3 changed files with 771 additions and 692 deletions

View File

@@ -1,519 +1,442 @@
# SPC Convective Outlook Implementation Roadmap
# Near-Term Report 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.
`docs/roadmap/near-term.md`. It is written for an LLM coding agent that will
implement each stage 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.
must not describe `near_term` 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.
Use `docs/roadmap/near-term.md` as the authoritative feature roadmap for user
intent, target behavior, and policy choices. This document is the implementation
plan. If the target behavior changes, update `near-term.md` first, then update
this roadmap.
## 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
categorical outlook has `severity_rank >= 3`.
- Define the discussion threshold as an internal constant, initially `3`, not a
config field.
- Add report ID `near_term` and Go constant `report.NearTerm`.
- Add CLI command `weatherreporter generate near-term`.
- Do not add `--date`, `--start`, `--end`, or duration flags for this report.
- Do not add `near_term` to morning or evening batches in the first
implementation.
- Define the valid-period duration with a package-owned constant, initially
`6` hours.
- Resolve the valid period as `[generation_time, generation_time + 6h)` in the
effective report timezone.
- Use prompt ID `weather.near_term_report`.
- Use artifact group `near-term` and batch output name `near-term.md`.
- Add comparison strategy constant `CompareRollingWindow = "rolling_window"`.
- Declare `CompatiblePriorIDs: []report.ID{report.NearTerm}`, but do not emit
Recent Changes for `near_term` in the first implementation.
- Keep state prior lookup returning `nil` for rolling-window reports until a
future comparison algorithm is designed.
- Default module order is:
1. `metadata`
2. `current_conditions`
3. `hourly_forecast`
4. `precip_timing`
5. `alert_digest`
6. `spc_convective_outlooks`
7. `area_forecast_discussion`
8. `spc_convective_discussion`
9. `weather_story`
- Configure the near-term `area_forecast_discussion` module with only
`key_messages` and `short_term` sections.
- Do not include daily/daypart modules in the default near-term composition.
- Alert and SPC modules must use existing valid-period overlap behavior.
- Preserve existing public behavior for all current report types.
## Stage 1: Weather Data Contract
## Stage 1: Report Identity And Period Resolution
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`;
- `period_begins`;
- `period_ends`;
- `issued_at`;
- `contains_location`;
- `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 categorical 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`;
- `period_begins`;
- `period_ends`;
- `headline`;
- `summary`;
- `discussion`;
- `updated_at`.
- Include a concise reason field such as:
`included_because: "categorical severity_rank >= 3"`.
Acceptance criteria:
- Slight Risk or higher retained categorical 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.
Goal: add the `near_term` report definition, constants, registry entry, and
rolling six-hour valid-period resolver.
Files to inspect:
- `internal/report/definition.go`
- `internal/report/registry.go`
- `internal/report/period.go`
- `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`
- `internal/report/period_test.go`
- `internal/state/filesystem.go`
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.
- Add `NearTerm ID = "near_term"` in `internal/report/definition.go`.
- Add `CompareRollingWindow ComparisonStrategy = "rolling_window"`.
- Add `internal/report/near_term_report.go`.
- Define a package-local duration constant in that file, for example:
Acceptance criteria:
```go
const nearTermHours = 6
```
- 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.
- Add `nearTermDefinition()` returning:
- `ID: NearTerm`
- `Name: "Near-Term Report"`
- `PromptID: "weather.near_term_report"`
- `ComparisonStrategy: CompareRollingWindow`
- `ArtifactGroup: "near-term"`
- `BatchOutputName: "near-term.md"`
- `Generated: true`
- `CompatiblePriorIDs: []ID{NearTerm}`
- `Modules: nearTermModules()`
- no `Morning` or `Evening` membership
- `resolve: resolveNearTerm`
- Implement `resolveNearTerm` as generation-time anchored:
Suggested validation:
```go
localNow := req.Now.In(req.Location)
return timeutil.Period{
Start: localNow,
End: localNow.Add(nearTermHours * time.Hour),
}, nil
```
- Add `nearTermDefinition()` to `DefaultRegistry()`.
- Add `NearTerm` to `Registry.All()` in a stable order after
`DailyTomorrow` and before `ThreeDay`.
- Do not change `BatchReports`.
- Leave `state.FindPriorSnapshot` behavior unchanged for
`CompareRollingWindow`; it should return `nil` because it only supports
same-date and weekend lookup.
Tests:
- Add report period tests for:
- lookup succeeds for `NearTerm`;
- `Registry.All()` includes `NearTerm`;
- fixed generation time resolves to exactly six hours;
- timezone-aware start and end use the effective location;
- valid period is not civil-day truncated;
- metadata RunID includes `near_term`;
- batch membership remains unchanged.
- Update registry metadata/path tests to include:
- artifact group `near-term`;
- batch output name `near-term.md`;
- generated `true`;
- compatible prior IDs `[]ID{NearTerm}`;
- comparison strategy `CompareRollingWindow`.
Validation:
```bash
go test ./internal/report ./internal/config ./internal/briefing ./internal/app
go test ./internal/report ./internal/state
```
This stage is small enough for one implementation prompt.
## Stage 8: App And Prompt Workflow Coverage
## Stage 2: Module Compatibility And Default Composition
Goal: prove the end-to-end generated data package contains the new stanzas in
the intended categories when fixture data warrants them.
Goal: make existing modules compatible with `near_term` where appropriate and
declare the default module composition.
Files to inspect:
- `internal/report/near_term_report.go`
- `internal/briefing/modules.go`
- `internal/module/module.go`
- `internal/briefing/modules_test.go`
- `internal/briefing/base_modules_test.go`
- `internal/briefing/derived_modules_test.go`
Implementation:
- Add `nearTermModules()` in `internal/report/near_term_report.go`.
- Use explicit module items in this order:
- `module.Metadata`
- `module.CurrentConditions`
- `module.HourlyForecast`
- `module.PrecipTiming`
- `module.AlertDigest`
- `module.SPCConvectiveOutlooks`
- `module.AreaForecastDiscussion` with options:
```go
module.AreaForecastDiscussionOptions{
Sections: []string{"key_messages", "short_term"},
}
```
- `module.SPCConvectiveDiscussion`
- `module.WeatherStory`
- Expand module `SupportedReports` in `internal/briefing/modules.go`:
- include `report.NearTerm` in `allReports`;
- include `report.NearTerm` for `HourlyForecast`;
- do not include `report.NearTerm` for `NarrativeForecast`;
- do not include `report.NearTerm` in `daypartReports`;
- do not include `report.NearTerm` for `DerivedDailySummary`,
`DerivedDaypartSummaries`, `OutdoorWindows`, or `TomorrowPlanning`.
- Keep `PrecipTiming`, `AlertDigest`, `SPCConvectiveOutlooks`,
`AreaForecastDiscussion`, `SPCConvectiveDiscussion`, and `WeatherStory`
compatible through `allReports`.
- Do not add a new module ID in this stage.
Tests:
- Add or update module registry tests proving:
- default near-term composition validates;
- all default near-term modules have builders;
- daily/daypart-only modules reject `report.NearTerm`;
- `HourlyForecast` builds for `report.NearTerm`;
- AFD options for the near-term default include only key messages and short
term.
- Add a focused AFD module test that near-term options omit long term when the
source provides it.
Validation:
```bash
go test ./internal/report ./internal/briefing ./internal/module
```
This stage is small enough for one implementation prompt.
## Stage 3: Derived Facts For Rolling Windows
Goal: teach `internal/facts` to build the facts needed by the near-term module
set without requiring daily summaries or daypart summaries.
Files to inspect:
- `internal/facts/facts.go`
- `internal/facts/facts_test.go`
- `internal/forecast`
- `internal/timeutil`
- `internal/briefing/modules.go`
Implementation:
- Add `report.NearTerm` handling in `facts.BuildDerived`.
- For near-term:
- populate `ValidPeriodHourlyPeriods` from the resolved six-hour valid
period;
- populate `ValidPeriodNarrativePeriods` if the existing generic selection
already does so, but do not require it for default near-term modules;
- build `PrecipTiming` from `ValidPeriodHourlyPeriods`;
- select alert overlaps using the near-term valid period;
- select SPC outlooks and discussions using the near-term valid period;
- do not build or require `DailySummaries`;
- do not build or require `DaypartSummaries`;
- do not build `StormWindowSummary`.
- Preserve existing daily, tomorrow, three-day, weekend, and storm derivation.
- If any existing helper assumes civil-day coverage, keep near-term on the
generic valid-period hourly path instead of reusing that helper.
Tests:
- Add facts tests for:
- valid-period hourly selection over a rolling six-hour window;
- precipitation timing based only on the near-term hourly slice;
- alert overlap inclusion/exclusion by near-term period;
- SPC outlook inclusion/exclusion by near-term period;
- SPC discussion records retained only for retained overlapping SPC days;
- no daily/daypart facts required.
- Add a regression test that an unsupported future report still returns an
actionable derivation error.
Validation:
```bash
go test ./internal/facts ./internal/forecast ./internal/timeutil
```
This stage is small enough for one implementation prompt.
## Stage 4: App Report Mapping And CLI Command
Goal: add explicit `generate near-term` support while preserving existing CLI
syntax and app behavior.
Files to inspect:
- `internal/app/app.go`
- `internal/app/app_test.go`
- `internal/cli/root.go`
- `internal/cli/root_test.go`
- `cmd/weatherreporter/main.go`
Implementation:
- Add app report kind:
```go
ReportNearTerm ReportKind = "near-term"
```
- Map `ReportNearTerm` to `report.NearTerm` in `reportIDForCommand`.
- Add `near-term` to CLI generate report parsing.
- Add help usage line:
```text
weatherreporter generate near-term [--config PATH] [--units VALUE] [--tz NAME] [--out PATH]
```
- Reuse existing generate command flags:
- allow `--config`;
- allow `--units`;
- allow `--tz`;
- allow `--out`;
- do not allow or require `--date`;
- do not allow or require storm `--start` / `--end`.
- Ensure the CLI returns an actionable error for unknown report names as today.
- Do not change batch commands.
Tests:
- Add CLI parser tests for:
- `generate near-term`;
- `generate near-term --config PATH`;
- `generate near-term --units us --tz America/Chicago --out PATH`;
- rejected `generate near-term --date YYYY-MM-DD`;
- rejected storm-only `--start` / `--end` on near-term if current parser
behavior supports this distinction.
- Add app resolution tests proving `ReportNearTerm` resolves `report.NearTerm`.
- Update help tests to assert `generate near-term` appears.
- Existing generate and run command tests must remain unchanged.
Validation:
```bash
go test ./internal/app ./internal/cli
go run ./cmd/weatherreporter --help
```
This stage is small enough for one implementation prompt.
## Stage 5: End-To-End Generation And Artifact Behavior
Goal: prove a near-term report can run through the app workflow and persist the
expected artifacts.
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
- `internal/state`
- `internal/promptinput`
- Weather API fixture server helpers in app tests
- distributor notification tests if report paths are asserted
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.
- Add an app-level test that generates `ReportNearTerm` with fixture weather
data and fake Scriptorium.
- Assert:
- `ReportResult.Metadata.ReportID == report.NearTerm`;
- prompt ID is `weather.near_term_report`;
- managed report path uses artifact group `near-term`;
- data package path uses artifact group `near-term`;
- module snapshot contains the near-term module list in order;
- data package categories are unchanged;
- `hourly_forecast` contains only periods overlapping the six-hour window;
- `precip_timing` reflects only the six-hour window;
- `alert_digest` and SPC stanzas respect overlap behavior;
- AFD includes key messages and short term but not long term;
- `recent_changes.items` is empty when no rolling-window comparison exists.
- Add an app-level case for distributor notification only if existing tests
assert report-specific path rendering. The expected distributor templates
should work through existing `{artifact_group}`, `{report_id}`,
`{valid_start_date}`, and `{valid_start_time}` values without special
near-term behavior.
- Do not add `near_term` to scheduled batch tests.
Acceptance criteria:
Tests:
- Module snapshot and YAML data package remain deterministic.
- Prompt category grouping preserves module order within categories.
- No Scriptorium or distributor behavior changes are required.
- Add stale-key checks if generated YAML is inspected:
- module intervals use `period_begins` / `period_ends`;
- top-level metadata keeps canonical `valid_period`.
- Ensure optional output copy via `--out` continues to use existing app copy
behavior for generated reports.
Suggested validation:
Validation:
```bash
go test ./internal/app ./internal/promptinput ./internal/state
go test ./internal/app ./internal/state ./internal/promptinput
```
This stage is small enough for one implementation prompt.
## Stage 9: Implemented Documentation
## Stage 6: Config Overrides And Examples
Goal: update non-roadmap docs after the feature is implemented.
Goal: allow configured module overrides for `near_term` while keeping
maintained examples valid.
Files to inspect:
- `internal/config/reports.go`
- `internal/config/config_test.go`
- `docs/config.md` after implementation
- `examples/config.yml`
Implementation:
- Add report config key aliases:
- `near_term`
- `near-term`
- Map both aliases to `report.NearTerm`.
- Validate near-term module overrides through the existing module registry.
- Ensure incompatible modules fail clearly, for example
`derived_daily_summary` should not be compatible with `near_term`.
- Update config tests for:
- successful `reports.near_term.deterministic_modules`;
- successful `reports.near-term.deterministic_modules`;
- duplicate canonical near-term aliases rejected if both are present;
- incompatible daily-only module rejected.
- Update `examples/config.yml` only if it enumerates all report module
overrides. If it does not need a near-term override, do not add one just to
demonstrate the feature.
Validation:
```bash
go test ./internal/config ./internal/report ./internal/briefing
```
This stage is small enough for one implementation prompt.
## Stage 7: Implemented Documentation
Goal: update non-roadmap documentation after the feature exists.
Files to inspect and update:
- `docs/integrations/weatherapi.md`
- `docs/internal/weather-data.md`
- `docs/internal/facts.md`
- `docs/cli.md`
- `docs/config.md`
- `docs/operations.md`
- `docs/internal/report-registry.md`
- `docs/internal/module.md`
- `docs/internal/facts.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
- `examples/config.yml`, only if changed in Stage 6
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.
- Describe `generate near-term` in CLI docs after implementation.
- Document that the first version is explicit generation only and is not part
of scheduled batches.
- Document the six-hour rolling valid period and that the duration is an
internal constant, not a config field.
- Document near-term report identity, artifact group, batch output name, prompt
ID, and module composition in internal docs.
- Document near-term config override keys only if Stage 6 implements them.
- Keep deferred items under roadmap docs only:
- configurable duration;
- batch membership;
- dedicated `derived_near_term_summary`;
- near-term Recent Changes comparison output.
Acceptance criteria:
- Non-roadmap docs do not describe deferred SPC behavior as current behavior.
- Non-roadmap docs describe only implemented behavior.
- Docs do not imply a duration config field or scheduled batch behavior.
- Maintained examples load.
- Documentation links and module ID lists are consistent with code.
Suggested validation:
Validation:
```bash
go test ./internal/config
@@ -522,71 +445,54 @@ git diff --check
This stage is small enough for one implementation prompt.
## Stage 10: Final Validation
## Stage 8: Final Validation
Goal: validate the complete feature and guard against regressions.
Goal: run the complete project validation after implementation and docs are
updated.
Run:
Commands:
```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:
Manual checks:
- 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 for categorical outlooks at severity
rank `3` or higher.
- Confirm public CLI syntax, output paths, distributor upload behavior, and
Scriptorium argv remain unchanged.
- `weatherreporter --help` lists `generate near-term`.
- No existing command syntax changed.
- `near_term` is absent from morning and evening batch membership.
- No non-roadmap docs describe unimplemented deferred near-term work.
- No config examples include secrets or invalid module IDs.
- Distributor bundle path rendering remains template-driven and does not need
report-specific branching.
This stage is small enough for one implementation prompt.
## Deferred Work
Out of scope for the initial implementation:
Do not include these in the first 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.
- user-configurable near-term duration;
- scheduled near-term batch membership or a new high-frequency batch command;
- dedicated `derived_near_term_summary`;
- narrative forecast periods in the default near-term module list;
- separate AFD section modules;
- rolling-window Recent Changes comparison output;
- custom CLI duration flags;
- distributor-specific behavior for near-term reports.
## Open Questions
No question blocks implementation.
None block 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.
Recommendation: keep the first version explicit and narrow: `generate
near-term`, six-hour constant, no batch membership, no Recent Changes output.
This fits the existing registry/module architecture and lets prompt quality be
tested before adding scheduler or comparison complexity.
Viable alternative: implement rolling-window Recent Changes immediately by
finding the most recent prior `near_term` report with an overlapping or adjacent
window. That could be useful later, but it needs a well-defined comparison
contract and should not block the first report implementation.

363
docs/roadmap/near-term.md Normal file
View File

@@ -0,0 +1,363 @@
# Near-Term Report Roadmap
## Purpose
This roadmap defines planned work to add a rolling `near_term` report focused
on the next several hours. The feature is not implemented yet, so this document
lives under `docs/roadmap/`.
The goal is a frequently generated report that helps readers understand what
matters in the immediate future: current conditions, hourly evolution,
precipitation timing, applicable hazards, applicable SPC risk products, and the
most relevant short-term narrative context.
## Target Behavior
Add a generated report with:
- report ID: `near_term`
- display name: `Near-Term Report`
- prompt ID: `weather.near_term_report`
- artifact group: `near-term`
- batch output name: `near-term.md`
- default valid-period length: 6 hours
- valid period: `[generation_time, generation_time + nearTermHours)`
The report should use a package-owned constant for the valid-period length, for
example:
```go
const nearTermHours = 6
```
Do not make the duration configurable in the first implementation. The constant
exists so the value can be changed later to 4 or 8 hours without changing
valid-period logic in multiple places.
## Locked Decisions
- The report ID is `near_term`.
- The report is rolling and generation-time anchored, not civil-day anchored.
- The first implementation covers the next 6 hours through a constant.
- The report should be generated explicitly by CLI command before deciding
whether it belongs in scheduled batches.
- The first implementation should declare `rolling_window` comparison policy
but should not emit Recent Changes output for `near_term`.
- Alert and SPC products should be included only when their valid periods
overlap the resolved near-term report period.
- SPC discussion text should keep the existing categorical-risk threshold rule
and should still require overlap with the near-term report period.
- The AFD stanza should include key messages and short term text by default.
- Daily-only modules should not be forced into this report.
## Report Definition
Add a report definition under `internal/report`, preferably in a focused
`near_term_report.go` file.
Definition fields:
- `ID`: `NearTerm`
- `Name`: `Near-Term Report`
- `PromptID`: `weather.near_term_report`
- `ComparisonStrategy`: `rolling_window`
- `ArtifactGroup`: `near-term`
- `BatchOutputName`: `near-term.md`
- `Generated`: `true`
- `CompatiblePriorIDs`: `[]ID{NearTerm}`
- `Modules`: near-term module list below
- `resolve`: rolling near-term resolver
Valid-period resolver:
```go
func resolveNearTerm(req ResolveRequest) (timeutil.Period, error) {
localNow := req.Now.In(req.Location)
return timeutil.Period{
Start: localNow,
End: localNow.Add(nearTermHours * time.Hour),
}, nil
}
```
The implementation should use idiomatic package-local constants and avoid
duplicating duration literals in tests or app code.
## Default Module Composition
Default module order should be:
1. `metadata`
2. `current_conditions`
3. `hourly_forecast`
4. `precip_timing`
5. `alert_digest`
6. `spc_convective_outlooks`
7. `area_forecast_discussion`
8. `spc_convective_discussion`
9. `weather_story`
`area_forecast_discussion` should use options equivalent to:
```yaml
sections:
- key_messages
- short_term
```
Do not include these daily/daypart-oriented modules initially:
- `derived_daily_summary`
- `derived_daypart_summaries`
- `outdoor_windows`
- `tomorrow_planning`
If the report needs deterministic summary facts later, add a purpose-built
module such as `derived_near_term_summary` rather than stretching daily modules
into a rolling sub-daily context.
## Prompt Package Shape
The existing prompt-input category layout should remain unchanged:
- `applicable_risk_products`
- `derived_summaries`
- `narrative_products`
- `raw_data`
Near-term output is expected to emphasize:
- `current_conditions` and `hourly_forecast` under `raw_data`
- `precip_timing` under `derived_summaries`
- `alert_digest` and `spc_convective_outlooks` under
`applicable_risk_products`
- `area_forecast_discussion`, `spc_convective_discussion`, and
`weather_story` under `narrative_products`
Module interval fields should use the existing prompt-facing
`period_begins` / `period_ends` convention.
## Overlap And Filtering Rules
Hourly forecast:
- use only hourly periods overlapping the near-term valid period;
- preserve hourly period order;
- do not include the full daily forecast.
Narrative products:
- AFD key messages and short term text may be included because the AFD is an
official short-term forecast discussion product;
- narrative forecast periods are not included by default unless a later prompt
test shows they improve near-term output.
Alerts:
- include active alert overlaps only when the alert overlaps the near-term
valid period;
- if alerts were checked successfully and no alerts overlap, emit checked empty
alert context through existing module behavior.
SPC outlooks:
- include only retained outlooks whose valid periods overlap the near-term
valid period;
- do not include non-overlapping outlooks even if they are severe;
- keep current prompt-facing field exclusions for geometry, severity rank,
expiration time, and source URL.
SPC discussion:
- include discussion only for SPC days where a retained overlapping categorical
outlook has severity rank at least `3`;
- do not include discussion for low-risk, non-categorical-only, or
non-overlapping outlooks.
Weather story:
- include when available under current optional-source behavior;
- do not require the story valid period to exactly match the near-term period
unless later testing shows stale stories are a problem.
## Implementation Stages
### Stage 1: Report Registry
Goal: add the `near_term` report definition and valid-period resolver.
Files to inspect or update:
- `internal/report/definition.go`
- `internal/report/registry.go`
- `internal/report/period.go`
- new `internal/report/near_term_report.go`
- `internal/report/period_test.go`
- `docs/internal/report-registry.md` after implementation
Acceptance criteria:
- `report.DefaultRegistry().Lookup(report.NearTerm)` succeeds.
- `report.Registry.All()` includes the report in a stable order.
- resolving the report at a fixed generation time produces a half-open
six-hour period.
- report metadata and RunID include `near_term`.
### Stage 2: Module Compatibility And Defaults
Goal: allow existing relevant modules to build for `near_term` and define the
default near-term composition.
Files to inspect or update:
- `internal/briefing/modules.go`
- `internal/report/near_term_report.go`
- `internal/briefing/*_module_test.go`
- `internal/module/module_test.go`
Acceptance criteria:
- all default near-term modules validate and build from appropriate test facts;
- daily-only modules remain incompatible unless intentionally expanded;
- `area_forecast_discussion` defaults to key messages and short term only for
this report.
### Stage 3: Derived Facts
Goal: make `internal/facts` derive report-period facts for `near_term`.
Files to inspect or update:
- `internal/facts/facts.go`
- `internal/facts/facts_test.go`
- `internal/forecast` selection helpers, if needed
Acceptance criteria:
- valid-period hourly periods are sliced to the six-hour window;
- precipitation timing is built from that six-hour hourly slice;
- alert overlaps use the near-term valid period;
- SPC outlook and discussion derivation use the near-term valid period;
- no daily summaries or daypart summaries are required for the default
near-term module set.
### Stage 4: CLI And App Wiring
Goal: add explicit generation support without changing existing commands.
Files to inspect or update:
- `internal/app`
- `internal/cli`
- `cmd/weatherreporter/main.go`
- `docs/cli.md` after implementation
Expected command:
```bash
weatherreporter generate near-term [--config PATH] [--units VALUE] [--tz NAME] [--out PATH]
```
Acceptance criteria:
- command maps to `report.NearTerm`;
- no `--date`, `--start`, or `--end` flags are added for this report;
- existing `generate daily`, `generate tomorrow`, `generate three-day`,
`generate weekend`, and `generate storm` behavior is unchanged;
- CLI help lists the new command.
### Stage 5: Prompt Input And State Coverage
Goal: prove the managed module snapshot and data package contain the intended
near-term stanzas.
Files to inspect or update:
- `internal/app/app_test.go`
- `internal/promptinput`
- `internal/state`
Acceptance criteria:
- generated module snapshot includes the near-term default module list in
order;
- data package uses existing categories;
- alert/SPC stanzas are omitted or checked-empty according to current module
behavior and valid-period overlap;
- report metadata preserves canonical `valid_period`;
- module interval labels use `period_begins` and `period_ends`.
### Stage 6: Documentation And Examples
Goal: document implemented behavior only after the code exists.
Files to inspect or update after implementation:
- `docs/cli.md`
- `docs/internal/report-registry.md`
- `docs/internal/module.md`
- `docs/internal/facts.md`
- `docs/internal/briefing.md`
- `docs/config.md` only if report module overrides need an example update
- `examples/config.yml` only if it lists report module overrides
Acceptance criteria:
- non-roadmap docs describe `near_term` only after implementation;
- future or deferred near-term summary modules remain under roadmap docs;
- examples, if updated, load through existing config tests.
## Test Plan
Focused tests:
```bash
go test ./internal/report ./internal/briefing ./internal/facts
go test ./internal/app ./internal/cli ./internal/promptinput
```
Full validation:
```bash
go test ./...
go run ./cmd/weatherreporter --help
git diff --check
```
Important coverage:
- six-hour valid-period resolution from a fixed generation time;
- timezone-aware period start and end;
- no civil-day truncation;
- hourly periods selected by overlap with the rolling window;
- alert overlap filtering;
- SPC outlook filtering by overlap;
- SPC discussion filtering by overlap plus categorical severity threshold;
- AFD key messages and short term included;
- daily-only modules rejected or absent from default near-term composition;
- distributor bundle path templates render sensibly for a sub-daily report,
especially valid start time variables.
## Deferred Work
Do not include these in the first implementation:
- user-configurable near-term duration;
- adding `near_term` to scheduled morning or evening batches;
- a dedicated `derived_near_term_summary` module;
- narrative forecast periods by default;
- separate AFD section modules;
- near-term-specific Recent Changes comparison output;
- CLI flags for custom near-term duration.
## Open Questions
None block the roadmap.
Recommendation: implement the first version as explicit `generate near-term`
only, with no scheduled batch membership. That keeps the user-visible behavior
small while the prompt and generated report are tested.
Viable alternative: add `near_term` to a frequent scheduled batch immediately.
That may be useful operationally, but it should wait until the report cadence
and downstream distributor behavior are clear.

View File

@@ -1,190 +0,0 @@
# 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
categorical 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
period_begins: "2026-06-12 at 8:00 AM"
period_ends: "2026-06-13 at 7:00 AM"
contains_location: true
image_url: "https://..."
```
The discussion module should be separate narrative context:
```yaml
briefing:
narrative_products:
area_forecast_discussion: {}
spc_convective_discussion:
included_because: "categorical severity_rank >= 3"
discussions:
- day: 1
period_begins: "2026-06-12 at 8:00 AM"
period_ends: "2026-06-13 at 7:00 AM"
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.