diff --git a/docs/cli.md b/docs/cli.md index cb139a9..e721f2e 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -21,7 +21,7 @@ the managed Markdown report after final metadata is saved. weatherreporter --help weatherreporter generate daily [--config PATH] [--units VALUE] [--tz NAME] [--out PATH] [--date YYYY-MM-DD] weatherreporter generate tomorrow [--config PATH] [--units VALUE] [--tz NAME] [--out PATH] -weatherreporter generate near-term [--config PATH] [--units VALUE] [--tz NAME] [--out PATH] +weatherreporter generate hourly [--config PATH] [--units VALUE] [--tz NAME] [--out PATH] weatherreporter generate three-day [--config PATH] [--units VALUE] [--tz NAME] [--out PATH] weatherreporter generate weekend [--config PATH] [--units VALUE] [--tz NAME] [--out PATH] weatherreporter generate storm [--config PATH] [--units VALUE] [--tz NAME] [--out PATH] --start TIME --end TIME @@ -39,7 +39,7 @@ weatherreporter inspect sources [--config PATH] RUN_ID artifact, managed Markdown report, and metadata under the configured workspace. `--out` writes an extra Markdown copy for the operator; distributor notification uses the managed report path, not the extra copy. `generate -near-term` uses the current generation time, covers the next six hours in the +hourly` uses the current generation time, covers the next six hours in the effective report timezone, and does not accept date or event window flags. `generate storm` requires explicit event-window bounds with `--start` and `--end`. @@ -54,7 +54,7 @@ notification is enabled, batch summaries and status lines include notification status, accepted distributor run ID, or notification error fields for each attempted report. -Near-Term Report generation is explicit only; it is not included in `run +Hourly Report generation is explicit only; it is not included in `run morning` or `run evening`. `inspect` commands read existing workspace artifacts and emit JSON to stdout. @@ -83,7 +83,7 @@ are no distributor-specific CLI flags. ```sh weatherreporter generate tomorrow --out ./tomorrow.md -weatherreporter generate near-term --out ./near-term.md +weatherreporter generate hourly --out ./hourly.md weatherreporter generate three-day --out ./three-day.md weatherreporter generate weekend --out ./weekend.md weatherreporter generate storm --start 2026-05-29T18:00 --end 2026-05-30T06:00 --out ./storm.md diff --git a/docs/config.md b/docs/config.md index 406063a..c7e263c 100644 --- a/docs/config.md +++ b/docs/config.md @@ -181,10 +181,9 @@ snapshot exists and a threshold is crossed. `reports` optionally overrides the ordered deterministic modules declared by report definitions. Omit a report entry to use its default module order. -Supported report keys are `daily`, `tomorrow`, `near_term`, `near-term`, -`three_day`, `weekend`, and `storm`. Canonical report IDs such as -`daily_today` and `daily_tomorrow` are also accepted. `near_term` and -`near-term` are aliases for the same Near-Term Report override. +Supported report keys are `daily`, `tomorrow`, `hourly`, `three_day`, +`weekend`, and `storm`. Canonical report IDs such as `daily_today` and +`daily_tomorrow` are also accepted. Each report entry supports: diff --git a/docs/internal/briefing.md b/docs/internal/briefing.md index e19d425..337b1ed 100644 --- a/docs/internal/briefing.md +++ b/docs/internal/briefing.md @@ -38,7 +38,7 @@ Outputs: Every registered composition entry has a builder. Unknown or unimplemented module IDs fail validation instead of being skipped. -Near-Term Report supports source and valid-period modules that operate over its +Hourly Report supports source and valid-period modules that operate over its rolling six-hour period: `metadata`, `current_conditions`, `hourly_forecast`, `precip_timing`, `alert_digest`, `spc_convective_outlooks`, `area_forecast_discussion`, `spc_convective_discussion`, and `weather_story`. @@ -67,7 +67,7 @@ builders run. Configured `location` values are prompt context only; Weather API `sourceLocationId` and `sourceLocation` remain source provenance. `area_forecast_discussion` uses optional `sections` configuration to include a -subset of discussion fields. Near-Term Report defaults this module to +subset of discussion fields. Hourly Report defaults this module to `key_messages` and `short_term`. `spc_convective_outlooks` uses collected SPC run metadata and derived diff --git a/docs/internal/facts.md b/docs/internal/facts.md index 33bbfe9..f946bdb 100644 --- a/docs/internal/facts.md +++ b/docs/internal/facts.md @@ -27,7 +27,7 @@ Outputs: report-period SPC convective outlooks and discussions, daily summaries, daypart summaries, and Storm Report window summary -Near-Term Report uses the generic valid-period hourly and narrative selection +Hourly Report uses the generic valid-period hourly and narrative selection for its rolling six-hour window. Its derived facts include precipitation timing from the selected hourly periods, alert overlaps for the six-hour period, and SPC outlooks/discussions overlapping that period. It does not build daily @@ -68,7 +68,7 @@ and inspection. - Invalid timezone names return an error. - Missing required hourly forecast data returns the underlying forecast derivation error for reports that require daily summaries. -- Near-Term Report can derive its default module facts without daily or +- Hourly Report can derive its default module facts without daily or daypart summaries. - Missing optional narrative, alert, discussion, daily, or weather story data produces empty or nil derived fields. diff --git a/docs/internal/module.md b/docs/internal/module.md index 7f061b9..0eef985 100644 --- a/docs/internal/module.md +++ b/docs/internal/module.md @@ -47,9 +47,9 @@ The registry recognizes these IDs: Every registered module has a builder. Report composition entries that refer to unknown or unimplemented module IDs fail validation instead of being skipped. -## Near-Term Composition +## Hourly Composition -The default Near-Term Report module order is: +The default Hourly Report module order is: 1. `metadata` 2. `current_conditions` @@ -61,7 +61,7 @@ The default Near-Term Report module order is: 8. `spc_convective_discussion` 9. `weather_story` -Near-Term Report does not include daily or daypart summary modules by default. +Hourly Report does not include daily or daypart summary modules by default. Its `area_forecast_discussion` item is configured to include only `key_messages` and `short_term`. diff --git a/docs/internal/prompt-input.md b/docs/internal/prompt-input.md index 9173b63..7411559 100644 --- a/docs/internal/prompt-input.md +++ b/docs/internal/prompt-input.md @@ -68,7 +68,7 @@ Prompt-facing module intervals use local `period_begins` and `period_ends` labels; canonical report metadata and source timestamps remain structured timestamps where applicable. -Near-Term Report uses the same package schema and categories. Its default +Hourly Report uses the same package schema and categories. Its default package includes `precip_timing` under `derived_summaries`, alert and SPC outlooks under `applicable_risk_products`, AFD/SPC discussion/weather story under `narrative_products`, and current/hourly data under `raw_data`. It does @@ -112,7 +112,7 @@ None. ## Skip And Resume Behavior None. Recent Changes is always present as an `items` list and may be empty. -Near-Term Report currently writes an empty `items` list. +Hourly Report currently writes an empty `items` list. ## Failure Behavior diff --git a/docs/internal/report-registry.md b/docs/internal/report-registry.md index 6405a3b..8b651ed 100644 --- a/docs/internal/report-registry.md +++ b/docs/internal/report-registry.md @@ -31,7 +31,7 @@ Each report definition declares: | --- | --- | --- | --- | --- | --- | | Daily Today | `daily_today` | `weather.daily_report` | `daily` | `daily.md` | Daily Today, Daily Tomorrow | | Daily Tomorrow | `daily_tomorrow` | `weather.daily_report` | `daily` | `tomorrow.md` | Daily Today, Daily Tomorrow | -| Near-Term Report | `near_term` | `weather.near_term_report` | `near-term` | `near-term.md` | Near-Term Report | +| Hourly Report | `hourly` | `weather.hourly_report` | `hourly` | `hourly.md` | Hourly Report | | 3-Day Outlook | `three_day` | `weather.three_day_outlook` | `three-day` | `three-day.md` | 3-Day Outlook | | Weekend Outlook | `weekend` | `weather.weekend_outlook` | `weekend` | `weekend.md` | Weekend Outlook | | Storm Report | `storm` | `weather.storm_report` | `storm` | `storm.md` | Storm Report | @@ -43,7 +43,7 @@ All report definitions are eligible for generation. - Daily Today covers the selected local civil day, or the current local civil day when no date override is supplied. - Daily Tomorrow covers the next local civil day from generation time. -- Near-Term Report covers the half-open six-hour period from generation time in +- Hourly Report covers the half-open six-hour period from generation time in the effective report timezone. The duration is an internal report constant, not a configuration field. - 3-Day Outlook covers the interval from generation time through local midnight @@ -69,13 +69,12 @@ IDs, then uses the registry for report policy. The app supplies `weather_api.timezone` as a loaded `time.Location`. Batch output path copying uses batch output names from report definitions. Report -module overrides can use the `near_term` or `near-term` keys for Near-Term -Report. +module overrides can use the `hourly` key for Hourly Report. ## Batch Membership Morning batches include Daily Today, 3-Day Outlook, and Weekend Outlook except -on Sunday. Evening batches include Daily Tomorrow. Near-Term Report is not part +on Sunday. Evening batches include Daily Tomorrow. Hourly Report is not part of a scheduled batch. ## State And App Usage diff --git a/docs/operations.md b/docs/operations.md index 5b2f172..a2f962e 100644 --- a/docs/operations.md +++ b/docs/operations.md @@ -11,7 +11,7 @@ Generation commands: ```text weatherreporter generate daily --date 2026-05-29 weatherreporter generate tomorrow -weatherreporter generate near-term +weatherreporter generate hourly weatherreporter generate three-day weatherreporter generate weekend weatherreporter generate storm --start 2026-05-29T18:00 --end 2026-05-30T06:00 @@ -26,7 +26,7 @@ Markdown report after `scriptorium run` succeeds and final metadata is saved. `--out PATH` writes an extra Markdown copy for the current generated report; it is not used as the distributor upload source. -`generate near-term` is an explicit generation command. It covers the six-hour +`generate hourly` is an explicit generation command. It covers the six-hour rolling period from generation time in the effective report timezone and is not part of scheduled morning or evening batches. @@ -67,7 +67,7 @@ workspace/ YYYY-MM-DD/ .modules.json .metadata.json - near-term/ + hourly/ YYYY-MM-DD/ .modules.json .metadata.json @@ -85,7 +85,7 @@ workspace/ weekend/ YYYY-MM-DD/ .data_package.yaml - near-term/ + hourly/ YYYY-MM-DD/ .data_package.yaml storm/ @@ -101,7 +101,7 @@ workspace/ weekend/ YYYY-MM-DD/ .render.json - near-term/ + hourly/ YYYY-MM-DD/ .render.json storm/ @@ -117,7 +117,7 @@ workspace/ weekend/ YYYY-MM-DD/ .distributor.json - near-term/ + hourly/ YYYY-MM-DD/ .distributor.json storm/ @@ -130,7 +130,7 @@ workspace/ .md weekend/ .md - near-term/ + hourly/ .md storm/ .md @@ -238,7 +238,7 @@ Markdown or YAML text. Daily Today and Daily Tomorrow can compare with each other when they cover the same valid local date. 3-Day Outlook compares with prior compatible 3-Day snapshots for the same valid local date. Weekend Outlook compares with prior -compatible Weekend snapshots for the same weekend window. Near-Term Report and +compatible Weekend snapshots for the same weekend window. Hourly Report and Storm Report leave Recent Changes empty. When no prior comparable snapshot exists, or no configured threshold is crossed, diff --git a/docs/roadmap/generated-text.md b/docs/roadmap/generated-text.md index 826b52b..8445a62 100644 --- a/docs/roadmap/generated-text.md +++ b/docs/roadmap/generated-text.md @@ -4,8 +4,8 @@ This roadmap defines planned work to move selected reports from full Markdown LLM generation to deterministic Markdown rendering with structured LLM-filled -text slots. The first target report is the rolling next-hours report, now named -`hourly` rather than `near_term`. +text slots. The first target report is the rolling next-hours report, named +`hourly`. This feature is not implemented yet, so this document lives under `docs/roadmap/`. @@ -67,15 +67,14 @@ not become a second application logic layer. Schema dependency in the first implementation. - Do not generate schemas or templates from inline Go. - Start with the rolling next-hours report only. -- Rename the planned `near_term` report to `hourly`. -- Do not keep both `near_term` and `hourly` public report IDs. +- Use `hourly` as the only public report ID for the rolling next-hours + report. +- Do not keep compatibility aliases from retired roadmap terminology. - Do not add a generic workflow engine or plugin system. ## Hourly Report Target -The planned `near_term` report should become the `hourly` report. - -Target report identity: +The rolling next-hours report target identity is: - report ID: `hourly` - CLI command: `weatherreporter generate hourly` @@ -143,7 +142,7 @@ Keep the first schema small. Example shape: ```json { - "summary": "Brief near-term overview.", + "summary": "Brief hourly overview.", "timing": "Plain-language timing of notable changes.", "impacts": "Practical impacts for the next few hours.", "confidence": "Optional confidence or uncertainty note." diff --git a/docs/roadmap/implementation.md b/docs/roadmap/implementation.md index 625ff56..37ff421 100644 --- a/docs/roadmap/implementation.md +++ b/docs/roadmap/implementation.md @@ -7,8 +7,8 @@ This roadmap defines the concrete implementation sequence for will implement each stage in order. This is a future-work roadmap. Until a stage is implemented, non-roadmap docs -must not describe `GeneratedText`, embedded report templates, or -`weatherreporter generate hourly` as available behavior. +must not describe `GeneratedText`, embedded report templates, or other +unimplemented GeneratedText behavior as available. ## Source Feature Roadmap @@ -26,8 +26,8 @@ for reaching that shape. - Implement the first GeneratedText path for the rolling next-hours report, named `hourly`. -- Replace planned/public `near_term` naming with `hourly`; do not keep a - public `near-term` alias. +- Use `hourly` as the only planned public name for the rolling next-hours + report; do not keep compatibility aliases from retired roadmap terminology. - Use report ID `hourly`, artifact group `hourly`, batch output name `hourly.md`, and CLI command `weatherreporter generate hourly`. - Keep the rolling report explicit-generation only; do not add scheduled batch @@ -61,7 +61,7 @@ planned public name for the rolling next-hours report. Files to inspect: - `docs/roadmap/generated-text.md` -- `docs/roadmap/near-term.md` +- the superseded roadmap file for the retired rolling-report name - `docs/roadmap/future.md` - `docs/roadmap/implementation.md` - `internal/app/app.go` @@ -70,30 +70,32 @@ Files to inspect: Implementation: -- Replace `docs/roadmap/near-term.md` with a short superseded note pointing to - `docs/roadmap/generated-text.md`, or delete it if the repository policy at - implementation time prefers removing obsolete roadmap files. -- Ensure no roadmap instructs a future agent to implement public - `near_term` / `near-term` behavior. -- If partial `near_term` code already exists, rename it to `hourly` rather - than adding aliases: - - `ReportNearTerm` -> `ReportHourly`; - - `report.NearTerm` -> `report.Hourly`; - - CLI command `near-term` -> `hourly`; - - artifact group `near-term` -> `hourly`; +- Replace the superseded roadmap file for the retired rolling-report name with + a short note pointing to `docs/roadmap/generated-text.md`, or delete it if + the repository policy at implementation time prefers removing obsolete + roadmap files. +- Ensure no roadmap instructs a future agent to implement retired public + identifiers for the rolling report. +- If partial code using retired identifiers already exists, rename it to + `hourly` rather than adding aliases: + - app report kind -> `ReportHourly`; + - report ID constant -> `report.Hourly`; + - CLI command -> `hourly`; + - artifact group -> `hourly`; - prompt IDs and tests updated to hourly names. -- Do not add compatibility aliases for `near-term`. +- Do not add compatibility aliases for retired command or config names. Acceptance criteria: - Active roadmap docs use `hourly` for the rolling report. -- No active roadmap describes `near_term` as the target public report ID. +- No active roadmap describes a retired identifier as the target public report + ID. - Existing code compiles after any rename work. Suggested validation: ```bash -rg -n "near_term|near-term|NearTerm" docs internal +rg -n 'near[_-]term|Near''Term' docs internal go test ./internal/app ./internal/report ./internal/cli ``` @@ -208,7 +210,7 @@ Implementation: - no daily summaries, daypart summaries, or storm summary. - Add config report aliases: - `hourly` - - reject or do not recognize `near_term` / `near-term`. + - reject or do not recognize retired rolling-report config keys. - Add app report kind `ReportHourly = "hourly"` and map it to `report.Hourly`. - Add CLI parsing and help for: @@ -651,17 +653,17 @@ Files to inspect: Implementation: - Add CLI command tests for `generate hourly`. -- Reject `generate near-term`. +- Reject the retired rolling-report command name. - Reject hourly `--date`, `--start`, `--end`, and duration-like flags. - Add config override tests for `reports.hourly.deterministic_modules`. -- Ensure `near_term` and `near-term` report config keys are rejected. +- Ensure retired rolling-report config keys are rejected. - Add app/CLI workflow tests using fake Scriptorium structured output. - Update examples only if they enumerate report module overrides. Acceptance criteria: - `weatherreporter --help` lists `generate hourly`; -- `generate near-term` is not accepted; +- the retired rolling-report command name is not accepted; - hourly config overrides validate compatible modules; - incompatible daily/daypart modules fail clearly for hourly; - examples load successfully. @@ -693,13 +695,13 @@ Files to inspect and update: - new `docs/internal/reporttemplate.md` - `docs/integrations/scriptorium.md` - `docs/roadmap/future.md` -- `docs/roadmap/near-term.md` +- the superseded roadmap file for the retired rolling-report name - `examples/config.yml`, only if examples changed Documentation requirements: - Non-roadmap docs describe only implemented hourly and GeneratedText behavior. -- `docs/cli.md` documents `generate hourly`, not `generate near-term`. +- `docs/cli.md` documents `generate hourly`, not the retired command name. - `docs/config.md` documents `reports.hourly` only if override support is implemented. - Internal docs distinguish: @@ -711,20 +713,22 @@ Documentation requirements: - Scriptorium integration docs include the structured-output command actually used by the adapter. - State docs list generated-text artifacts and metadata links. -- `docs/roadmap/near-term.md` is deleted or marked superseded. +- the superseded roadmap file for the retired rolling-report name is deleted + or marked superseded. - Deferred migrations for other report types stay under roadmap docs only. Acceptance criteria: - No non-roadmap docs describe unimplemented generated-text migrations. -- No active docs describe `near_term` as the target public report. +- No active docs describe retired rolling-report identifiers as the target + public report. - Maintained examples remain valid. Suggested validation: ```bash go test ./internal/config -rg -n "near_term|near-term|NearTerm" docs internal examples +rg -n 'near[_-]term|Near''Term' docs internal examples git diff --check ``` @@ -745,7 +749,7 @@ git diff --check Manual checks: - `weatherreporter --help` lists `generate hourly`. -- `weatherreporter --help` does not list `generate near-term`. +- `weatherreporter --help` does not list the retired rolling-report command. - generated hourly Markdown has deterministic headings and section order. - GeneratedText JSON is persisted separately from deterministic facts. - render context JSON is persisted and readable. diff --git a/docs/roadmap/near-term.md b/docs/roadmap/near-term.md index 1fecbe0..671ea5b 100644 --- a/docs/roadmap/near-term.md +++ b/docs/roadmap/near-term.md @@ -1,363 +1,6 @@ -# Near-Term Report Roadmap +# Superseded Roadmap -## Purpose +This roadmap has been superseded by `docs/roadmap/generated-text.md`. -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. +The rolling next-hours report is named `hourly`. Use the GeneratedText roadmap +for current future-work guidance. diff --git a/internal/app/app.go b/internal/app/app.go index 2091cca..ca60aa8 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -30,7 +30,7 @@ type ReportKind string const ( ReportDaily ReportKind = "daily" ReportTomorrow ReportKind = "tomorrow" - ReportNearTerm ReportKind = "near-term" + ReportHourly ReportKind = "hourly" ReportThreeDay ReportKind = "three-day" ReportWeekend ReportKind = "weekend" ReportStorm ReportKind = "storm" @@ -387,8 +387,8 @@ func reportIDForCommand(kind ReportKind) (report.ID, error) { return report.DailyToday, nil case ReportTomorrow: return report.DailyTomorrow, nil - case ReportNearTerm: - return report.NearTerm, nil + case ReportHourly: + return report.Hourly, nil case ReportThreeDay: return report.ThreeDay, nil case ReportWeekend: diff --git a/internal/app/app_test.go b/internal/app/app_test.go index 5e2bf6b..e7d44fb 100644 --- a/internal/app/app_test.go +++ b/internal/app/app_test.go @@ -349,19 +349,19 @@ func TestGenerateReportOmitsSPCConvectiveDiscussionBelowThreshold(t *testing.T) } } -func TestGenerateNearTermReportWritesExpectedArtifacts(t *testing.T) { - server := nearTermBundleServer(t) +func TestGenerateHourlyReportWritesExpectedArtifacts(t *testing.T) { + server := hourlyBundleServer(t) cfg := dailyTestConfig(t, server) cfg.Workspace.Root = t.TempDir() resolved, err := ResolveGenerate(GenerateRequest{ Config: cfg, - Report: ReportNearTerm, + Report: ReportHourly, }, mustParse("2026-05-29T08:30:00-05:00")) if err != nil { t.Fatalf("ResolveGenerate() error = %v", err) } - outputPath := filepath.Join(t.TempDir(), "near-term.md") - renderer := successfulRenderer("# Near-Term Report\n") + outputPath := filepath.Join(t.TempDir(), "hourly.md") + renderer := successfulRenderer("# Hourly Report\n") result, err := GenerateReport(context.Background(), ReportRequest{ Config: cfg, @@ -373,8 +373,8 @@ func TestGenerateNearTermReportWritesExpectedArtifacts(t *testing.T) { t.Fatalf("GenerateReport() error = %v", err) } - if result.Metadata.ReportID != report.NearTerm || result.Metadata.PromptID != "weather.near_term_report" { - t.Fatalf("metadata report/prompt = %q/%q, want near-term", result.Metadata.ReportID, result.Metadata.PromptID) + if result.Metadata.ReportID != report.Hourly || result.Metadata.PromptID != "weather.hourly_report" { + t.Fatalf("metadata report/prompt = %q/%q, want hourly", result.Metadata.ReportID, result.Metadata.PromptID) } if got := result.Metadata.ValidPeriod.Start.Format(time.RFC3339); got != "2026-05-29T08:30:00-05:00" { t.Fatalf("valid period start = %s, want rolling window start", got) @@ -382,14 +382,14 @@ func TestGenerateNearTermReportWritesExpectedArtifacts(t *testing.T) { if got := result.Metadata.ValidPeriod.End.Format(time.RFC3339); got != "2026-05-29T14:30:00-05:00" { t.Fatalf("valid period end = %s, want six-hour rolling window end", got) } - if renderer.renderRequest.PromptID != "weather.near_term_report" || renderer.runRequest.PromptID != "weather.near_term_report" { - t.Fatalf("renderer prompt IDs = %q/%q, want near-term prompt", renderer.renderRequest.PromptID, renderer.runRequest.PromptID) + if renderer.renderRequest.PromptID != "weather.hourly_report" || renderer.runRequest.PromptID != "weather.hourly_report" { + t.Fatalf("renderer prompt IDs = %q/%q, want hourly prompt", renderer.renderRequest.PromptID, renderer.runRequest.PromptID) } - if !strings.Contains(result.ReportPath, filepath.Join("reports", "near-term")) { - t.Fatalf("ReportPath = %q, want near-term artifact group", result.ReportPath) + if !strings.Contains(result.ReportPath, filepath.Join("reports", "hourly")) { + t.Fatalf("ReportPath = %q, want hourly artifact group", result.ReportPath) } - if !strings.Contains(result.DataPackagePath, filepath.Join("data-packages", "near-term", "2026-05-29")) { - t.Fatalf("DataPackagePath = %q, want near-term artifact group", result.DataPackagePath) + if !strings.Contains(result.DataPackagePath, filepath.Join("data-packages", "hourly", "2026-05-29")) { + t.Fatalf("DataPackagePath = %q, want hourly artifact group", result.DataPackagePath) } assertPathsExist(t, result.ModuleSnapshotPath, result.DataPackagePath, result.PreflightPath, result.ReportPath, result.MetadataPath, outputPath) if result.OutputPath != outputPath { @@ -399,7 +399,7 @@ func TestGenerateNearTermReportWritesExpectedArtifacts(t *testing.T) { if err != nil { t.Fatalf("read output copy: %v", err) } - if !strings.Contains(string(copiedReport), "# Near-Term Report") { + if !strings.Contains(string(copiedReport), "# Hourly Report") { t.Fatalf("output copy missing rendered report:\n%s", string(copiedReport)) } @@ -423,11 +423,11 @@ func TestGenerateNearTermReportWritesExpectedArtifacts(t *testing.T) { t.Fatalf("decode hourly forecast: %v", err) } if !ok || len(hourly.Periods) != 5 { - t.Fatalf("hourly forecast = %#v, want five overlapping near-term periods", hourly) + t.Fatalf("hourly forecast = %#v, want five overlapping hourly periods", hourly) } hourlyJSON := mustMarshalString(t, hourly) if !strings.Contains(hourlyJSON, "Showers entering the area") || !strings.Contains(hourlyJSON, "Heavy rain") { - t.Fatalf("hourly forecast missing selected near-term periods:\n%s", hourlyJSON) + t.Fatalf("hourly forecast missing selected hourly periods:\n%s", hourlyJSON) } if strings.Contains(hourlyJSON, "Before-window storms") || strings.Contains(hourlyJSON, "After-window rain") { t.Fatalf("hourly forecast contains periods outside valid window:\n%s", hourlyJSON) @@ -438,13 +438,13 @@ func TestGenerateNearTermReportWritesExpectedArtifacts(t *testing.T) { t.Fatalf("decode precip timing: %v", err) } if !ok || precip.MaxPopPercent == nil || *precip.MaxPopPercent != 80 || len(precip.PrecipitationWindows) != 2 { - t.Fatalf("precip timing = %#v, want near-term precipitation windows", precip) + t.Fatalf("precip timing = %#v, want hourly precipitation windows", precip) } if precip.PrecipitationWindows[0].PeriodBegins != "2026-05-29 at 8:00 AM" || precip.PrecipitationWindows[0].PeriodEnds != "2026-05-29 at 9:00 AM" { t.Fatalf("first precip window = %#v, want first selected hour only", precip.PrecipitationWindows[0]) } if precip.PrecipitationWindows[1].PeriodBegins != "2026-05-29 at 10:00 AM" || precip.PrecipitationWindows[1].PeriodEnds != "2026-05-29 at 12:00 PM" { - t.Fatalf("second precip window = %#v, want late-morning near-term rain", precip.PrecipitationWindows[1]) + t.Fatalf("second precip window = %#v, want late-morning hourly rain", precip.PrecipitationWindows[1]) } alerts, ok, err := module.StanzaValue[briefing.AlertDigestModule](result.ModuleSnapshot, "alert_digest") @@ -452,7 +452,7 @@ func TestGenerateNearTermReportWritesExpectedArtifacts(t *testing.T) { t.Fatalf("decode alert digest: %v", err) } if !ok || !alerts.Checked || alerts.ActiveCount != 3 || alerts.RelevantCount != 1 || len(alerts.Relevant) != 1 || alerts.Relevant[0].Event != "Flood Watch" { - t.Fatalf("alert digest = %#v, want only near-term alert overlap relevant", alerts) + t.Fatalf("alert digest = %#v, want only hourly alert overlap relevant", alerts) } outlooks, ok, err := module.StanzaValue[briefing.SPCConvectiveOutlooksModule](result.ModuleSnapshot, "spc_convective_outlooks") @@ -460,13 +460,13 @@ func TestGenerateNearTermReportWritesExpectedArtifacts(t *testing.T) { t.Fatalf("decode SPC outlooks: %v", err) } if !ok || !outlooks.Checked || outlooks.OutlookCount != 1 || len(outlooks.Outlooks) != 1 || outlooks.Outlooks[0].Label != "SLGT" { - t.Fatalf("SPC outlooks = %#v, want one overlapping near-term outlook", outlooks) + t.Fatalf("SPC outlooks = %#v, want one overlapping hourly outlook", outlooks) } discussion, ok, err := module.StanzaValue[briefing.SPCConvectiveDiscussionModule](result.ModuleSnapshot, "spc_convective_discussion") if err != nil { t.Fatalf("decode SPC discussion: %v", err) } - if !ok || len(discussion.Discussions) != 1 || discussion.Discussions[0].Headline != "Near-term severe storms" { + if !ok || len(discussion.Discussions) != 1 || discussion.Discussions[0].Headline != "hourly severe storms" { t.Fatalf("SPC discussion = %#v, want discussion for retained overlapping outlook", discussion) } @@ -474,11 +474,11 @@ func TestGenerateNearTermReportWritesExpectedArtifacts(t *testing.T) { if err != nil { t.Fatalf("decode AFD: %v", err) } - if !ok || len(afd.KeyMessages) != 1 || afd.ShortTerm != "Short-term AFD narrative for near-term report." { + if !ok || len(afd.KeyMessages) != 1 || afd.ShortTerm != "Short-term AFD narrative for hourly report." { t.Fatalf("AFD = %#v, want key messages and short term", afd) } if afd.Product != "" || afd.LongTerm != "" { - t.Fatalf("AFD = %#v, want near-term defaults to omit product and long term", afd) + t.Fatalf("AFD = %#v, want hourly defaults to omit product and long term", afd) } if result.PriorSnapshot != nil || len(result.RecentChanges) != 0 || len(result.DataPackage.RecentChanges.Items) != 0 { @@ -487,8 +487,8 @@ func TestGenerateNearTermReportWritesExpectedArtifacts(t *testing.T) { data := readDataPackageForTest(t, result) text := string(data) for _, want := range []string{ - "id: near_term", - "prompt_id: weather.near_term_report", + "id: hourly", + "prompt_id: weather.hourly_report", "valid_period:", "recent_changes:", " items: []", @@ -505,9 +505,9 @@ func TestGenerateNearTermReportWritesExpectedArtifacts(t *testing.T) { t.Fatalf("data package missing %q:\n%s", want, text) } } - for _, omitted := range []string{"validPeriod:", "Before-window storms", "After-window rain", "Evening Advisory", "Day 2 outlook", "Long-term AFD narrative for near-term report."} { + for _, omitted := range []string{"validPeriod:", "Before-window storms", "After-window rain", "Evening Advisory", "Day 2 outlook", "Long-term AFD narrative for hourly report."} { if strings.Contains(text, omitted) { - t.Fatalf("data package contains %q, want near-term filtered/canonical output:\n%s", omitted, text) + t.Fatalf("data package contains %q, want hourly filtered/canonical output:\n%s", omitted, text) } } riskIndex := strings.Index(text, " applicable_risk_products:") @@ -1364,10 +1364,10 @@ func TestResolveGenerateMapsCommandToReportDefinition(t *testing.T) { requestDate: time.Time{}, }, { - name: "near-term", - kind: ReportNearTerm, - wantID: report.NearTerm, - wantPrompt: "weather.near_term_report", + name: "hourly", + kind: ReportHourly, + wantID: report.Hourly, + wantPrompt: "weather.hourly_report", wantStart: "2026-05-29T08:00:00-05:00", wantEnd: "2026-05-29T14:00:00-05:00", }, @@ -1488,7 +1488,7 @@ func dailyBundleServerWithConvectiveResponse(t *testing.T, convectiveResponse st return server } -func nearTermBundleServer(t *testing.T) *httptest.Server { +func hourlyBundleServer(t *testing.T) *httptest.Server { t.Helper() server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch r.URL.Path { @@ -1503,11 +1503,11 @@ func nearTermBundleServer(t *testing.T) *httptest.Server { case "/alerts/active": _, _ = w.Write([]byte(`{"data":{"alerts":[{"event":"Expired Advisory","headline":"Ends at valid start","severity":"Minor","effective":"2026-05-29T06:00:00-05:00","expires":"2026-05-29T08:30:00-05:00"},{"event":"Flood Watch","headline":"Flooding possible","severity":"Moderate","effective":"2026-05-29T11:00:00-05:00","expires":"2026-05-29T15:00:00-05:00"},{"event":"Evening Advisory","headline":"Starts at valid end","severity":"Minor","effective":"2026-05-29T14:30:00-05:00","expires":"2026-05-29T18:00:00-05:00"}]}}`)) case "/discussion": - _, _ = w.Write([]byte(`{"data":{"product":"discussion","issuedAt":"2026-05-29T08:05:00-05:00","keyMessages":["Storms are most likely late this morning."],"shortTerm":{"qualifier":"(Short Term)","text":"Short-term AFD narrative for near-term report."},"longTerm":{"qualifier":"(Long Term)","text":"Long-term AFD narrative for near-term report."}}}`)) + _, _ = w.Write([]byte(`{"data":{"product":"discussion","issuedAt":"2026-05-29T08:05:00-05:00","keyMessages":["Storms are most likely late this morning."],"shortTerm":{"qualifier":"(Short Term)","text":"Short-term AFD narrative for hourly report."},"longTerm":{"qualifier":"(Long Term)","text":"Long-term AFD narrative for hourly report."}}}`)) case "/weatherstories/latest": - _, _ = w.Write([]byte(`{"data":{"officeId":"LSX","startTime":"2026-05-29T13:00:00Z","endTime":"2026-05-29T20:00:00Z","updatedAt":"2026-05-29T13:05:00Z","title":"Near-Term Storm Chances","description":"Scattered showers and thunderstorms are possible.","altText":"Weather story graphic with rain chances.","priority":true,"order":1,"downloadUrl":"https://api.weather.gov/offices/LSX/weatherstories/download/near-term"}}`)) + _, _ = w.Write([]byte(`{"data":{"officeId":"LSX","startTime":"2026-05-29T13:00:00Z","endTime":"2026-05-29T20:00:00Z","updatedAt":"2026-05-29T13:05:00Z","title":"Hourly Storm Chances","description":"Scattered showers and thunderstorms are possible.","altText":"Weather story graphic with rain chances.","priority":true,"order":1,"downloadUrl":"https://api.weather.gov/offices/LSX/weatherstories/download/hourly"}}`)) case "/outlooks/convective": - _, _ = w.Write([]byte(`{"data":{"locationId":"home","locationName":"Brentwood","asOf":"2026-05-29T13:30:00Z","issuedAt":"2026-05-29T13:00:00Z","outlooks":[{"id":"day1-near-term","day":1,"outlookType":"categorical","label":"SLGT","labelText":"Slight Risk","severityRank":3,"validFrom":"2026-05-29T10:00:00-05:00","validTo":"2026-05-29T16:00:00-05:00","issuedAt":"2026-05-29T08:00:00-05:00","containsLocation":true},{"id":"day2-outside","day":2,"outlookType":"categorical","label":"ENH","labelText":"Day 2 outlook","severityRank":4,"validFrom":"2026-05-30T10:00:00-05:00","validTo":"2026-05-30T16:00:00-05:00","issuedAt":"2026-05-29T08:00:00-05:00","containsLocation":true}],"discussions":[{"day":1,"headline":"Near-term severe storms","summary":"Scattered severe storms are possible.","discussion":"Damaging winds may occur during the near-term window.","updatedAt":"2026-05-29T08:15:00-05:00"},{"day":2,"headline":"Day 2 discussion","summary":"Later period risk.","discussion":"This day 2 discussion should not be retained.","updatedAt":"2026-05-29T08:20:00-05:00"}]}}`)) + _, _ = w.Write([]byte(`{"data":{"locationId":"home","locationName":"Brentwood","asOf":"2026-05-29T13:30:00Z","issuedAt":"2026-05-29T13:00:00Z","outlooks":[{"id":"day1-hourly","day":1,"outlookType":"categorical","label":"SLGT","labelText":"Slight Risk","severityRank":3,"validFrom":"2026-05-29T10:00:00-05:00","validTo":"2026-05-29T16:00:00-05:00","issuedAt":"2026-05-29T08:00:00-05:00","containsLocation":true},{"id":"day2-outside","day":2,"outlookType":"categorical","label":"ENH","labelText":"Day 2 outlook","severityRank":4,"validFrom":"2026-05-30T10:00:00-05:00","validTo":"2026-05-30T16:00:00-05:00","issuedAt":"2026-05-29T08:00:00-05:00","containsLocation":true}],"discussions":[{"day":1,"headline":"hourly severe storms","summary":"Scattered severe storms are possible.","discussion":"Damaging winds may occur during the hourly window.","updatedAt":"2026-05-29T08:15:00-05:00"},{"day":2,"headline":"Day 2 discussion","summary":"Later period risk.","discussion":"This day 2 discussion should not be retained.","updatedAt":"2026-05-29T08:20:00-05:00"}]}}`)) default: http.NotFound(w, r) } diff --git a/internal/briefing/base_modules_test.go b/internal/briefing/base_modules_test.go index e51a877..0b8cd8c 100644 --- a/internal/briefing/base_modules_test.go +++ b/internal/briefing/base_modules_test.go @@ -97,10 +97,10 @@ func TestHourlyForecastModuleRejectsUnsupportedReports(t *testing.T) { } } -func TestHourlyForecastModuleBuildsForNearTerm(t *testing.T) { +func TestHourlyForecastModuleBuildsForHourly(t *testing.T) { registry := MustDefaultModuleRegistry() ctx := testModuleContext() - ctx.Resolved.Definition = report.DefaultRegistry().MustLookup(report.NearTerm) + ctx.Resolved.Definition = report.DefaultRegistry().MustLookup(report.Hourly) output, err := registry.BuildModule(ctx, module.ConfigItem{ID: module.HourlyForecast}) if err != nil { @@ -108,7 +108,7 @@ func TestHourlyForecastModuleBuildsForNearTerm(t *testing.T) { } value := moduleValue[HourlyForecastModule](t, output) if len(value.Periods) != 1 || value.Periods[0].TextDescription != "Showers likely." { - t.Fatalf("HourlyForecast = %#v, want near-term hourly period", value) + t.Fatalf("HourlyForecast = %#v, want hourly report period", value) } } @@ -335,10 +335,10 @@ func TestAreaForecastDiscussionModuleCanSelectSections(t *testing.T) { } } -func TestAreaForecastDiscussionModuleUsesNearTermDefaultSections(t *testing.T) { +func TestAreaForecastDiscussionModuleUsesHourlyDefaultSections(t *testing.T) { registry := MustDefaultModuleRegistry() ctx := testModuleContext() - ctx.Resolved.Definition = report.DefaultRegistry().MustLookup(report.NearTerm) + ctx.Resolved.Definition = report.DefaultRegistry().MustLookup(report.Hourly) var item module.ConfigItem for _, candidate := range ctx.Resolved.Definition.Modules { if candidate.ID == module.AreaForecastDiscussion { @@ -347,7 +347,7 @@ func TestAreaForecastDiscussionModuleUsesNearTermDefaultSections(t *testing.T) { } } if item.ID == "" { - t.Fatal("near-term default modules missing area_forecast_discussion") + t.Fatal("hourly default modules missing area_forecast_discussion") } output, err := registry.BuildModule(ctx, item) diff --git a/internal/briefing/derived_modules_test.go b/internal/briefing/derived_modules_test.go index a1b1144..2f6b0e1 100644 --- a/internal/briefing/derived_modules_test.go +++ b/internal/briefing/derived_modules_test.go @@ -138,7 +138,7 @@ func TestPrecipTimingModuleHandlesRainyAndDryForecasts(t *testing.T) { func TestPrecipTimingModuleUsesDerivedTimingWithoutDaypartSummaries(t *testing.T) { registry := MustDefaultModuleRegistry() - ctx := derivedModuleContext(report.NearTerm) + ctx := derivedModuleContext(report.Hourly) ctx.Derived.DailySummaries = nil ctx.Derived.DaypartSummaries = nil diff --git a/internal/briefing/modules.go b/internal/briefing/modules.go index 02a442d..d90b756 100644 --- a/internal/briefing/modules.go +++ b/internal/briefing/modules.go @@ -249,7 +249,7 @@ func (d ModuleDefinition) ValidateOptions(options any) error { } func defaultModuleDefinitions() []ModuleDefinition { - allReports := []report.ID{report.DailyToday, report.DailyTomorrow, report.NearTerm, report.ThreeDay, report.Weekend, report.Storm} + allReports := []report.ID{report.DailyToday, report.DailyTomorrow, report.Hourly, report.ThreeDay, report.Weekend, report.Storm} daypartReports := []report.ID{report.DailyToday, report.DailyTomorrow, report.ThreeDay, report.Weekend} return []ModuleDefinition{ { @@ -286,7 +286,7 @@ func defaultModuleDefinitions() []ModuleDefinition { DefaultOptions: module.HourlyForecastOptions{}, RequiredCollected: []module.FactRequirement{module.CollectedHourlyForecast}, RequiredDerived: []module.FactRequirement{module.RequiresDerivedHourlyPeriods}, - SupportedReports: []report.ID{report.DailyToday, report.DailyTomorrow, report.NearTerm}, + SupportedReports: []report.ID{report.DailyToday, report.DailyTomorrow, report.Hourly}, MissingData: module.MissingDataOmit, Builder: buildHourlyForecastModule, }, diff --git a/internal/briefing/modules_test.go b/internal/briefing/modules_test.go index 5648eac..5ad7931 100644 --- a/internal/briefing/modules_test.go +++ b/internal/briefing/modules_test.go @@ -54,8 +54,8 @@ func TestDefaultReportModulesBuildSnapshots(t *testing.T) { } } -func TestNearTermDefaultModuleOptions(t *testing.T) { - definition := report.DefaultRegistry().MustLookup(report.NearTerm) +func TestHourlyDefaultModuleOptions(t *testing.T) { + definition := report.DefaultRegistry().MustLookup(report.Hourly) var found bool for _, item := range definition.Modules { if item.ID != module.AreaForecastDiscussion { @@ -71,7 +71,7 @@ func TestNearTermDefaultModuleOptions(t *testing.T) { } } if !found { - t.Fatal("near-term default modules missing area_forecast_discussion") + t.Fatal("hourly default modules missing area_forecast_discussion") } } @@ -112,7 +112,7 @@ func TestModuleRegistryRejectsIncompatibleReports(t *testing.T) { } } -func TestModuleRegistryRejectsNearTermIncompatibleModules(t *testing.T) { +func TestModuleRegistryRejectsHourlyIncompatibleModules(t *testing.T) { registry := MustDefaultModuleRegistry() for _, id := range []module.ID{ module.NarrativeForecast, @@ -122,9 +122,9 @@ func TestModuleRegistryRejectsNearTermIncompatibleModules(t *testing.T) { module.TomorrowPlanning, } { t.Run(string(id), func(t *testing.T) { - err := registry.ValidateComposition(report.NearTerm, []module.ConfigItem{{ID: id}}) - if err == nil || !strings.Contains(err.Error(), `not compatible with report "near_term"`) { - t.Fatalf("ValidateComposition() error = %v, want incompatible near-term module", err) + err := registry.ValidateComposition(report.Hourly, []module.ConfigItem{{ID: id}}) + if err == nil || !strings.Contains(err.Error(), `not compatible with report "hourly"`) { + t.Fatalf("ValidateComposition() error = %v, want incompatible hourly module", err) } }) } diff --git a/internal/cli/root.go b/internal/cli/root.go index 5aff61a..911f7ae 100644 --- a/internal/cli/root.go +++ b/internal/cli/root.go @@ -19,7 +19,7 @@ Usage: weatherreporter --help weatherreporter generate daily [--config PATH] [--units VALUE] [--tz NAME] [--out PATH] [--date YYYY-MM-DD] weatherreporter generate tomorrow [--config PATH] [--units VALUE] [--tz NAME] [--out PATH] - weatherreporter generate near-term [--config PATH] [--units VALUE] [--tz NAME] [--out PATH] + weatherreporter generate hourly [--config PATH] [--units VALUE] [--tz NAME] [--out PATH] weatherreporter generate three-day [--config PATH] [--units VALUE] [--tz NAME] [--out PATH] weatherreporter generate weekend [--config PATH] [--units VALUE] [--tz NAME] [--out PATH] weatherreporter generate storm [--config PATH] [--units VALUE] [--tz NAME] [--out PATH] --start TIME --end TIME @@ -387,8 +387,8 @@ func reportKind(value string) (app.ReportKind, bool) { return app.ReportDaily, true case string(app.ReportTomorrow): return app.ReportTomorrow, true - case string(app.ReportNearTerm): - return app.ReportNearTerm, true + case string(app.ReportHourly): + return app.ReportHourly, true case string(app.ReportThreeDay): return app.ReportThreeDay, true case string(app.ReportWeekend): diff --git a/internal/cli/root_test.go b/internal/cli/root_test.go index 7121a1a..2035b6b 100644 --- a/internal/cli/root_test.go +++ b/internal/cli/root_test.go @@ -28,8 +28,8 @@ func TestRunHelpLongFlag(t *testing.T) { if !strings.Contains(stdout.String(), "generate daily") { t.Fatalf("help output missing generate command:\n%s", stdout.String()) } - if !strings.Contains(stdout.String(), "weatherreporter generate near-term") { - t.Fatalf("help output missing near-term generate command:\n%s", stdout.String()) + if !strings.Contains(stdout.String(), "weatherreporter generate hourly") { + t.Fatalf("help output missing hourly generate command:\n%s", stdout.String()) } removedInspectCommand := "inspect " + "briefing" if !strings.Contains(stdout.String(), "inspect modules") || strings.Contains(stdout.String(), removedInspectCommand) { @@ -812,7 +812,7 @@ func TestResolveGenerateCommands(t *testing.T) { }{ {name: "daily", args: []string{"daily", "--date", "2026-05-29"}, want: app.ReportDaily}, {name: "tomorrow", args: []string{"tomorrow"}, want: app.ReportTomorrow}, - {name: "near-term", args: []string{"near-term"}, want: app.ReportNearTerm}, + {name: "hourly", args: []string{"hourly"}, want: app.ReportHourly}, {name: "three-day", args: []string{"three-day"}, want: app.ReportThreeDay}, {name: "weekend", args: []string{"weekend"}, want: app.ReportWeekend}, {name: "storm", args: []string{"storm", "--start", "2026-05-29T18:00", "--end", "2026-05-30T06:00"}, want: app.ReportStorm}, @@ -831,20 +831,20 @@ func TestResolveGenerateCommands(t *testing.T) { } } -func TestResolveGenerateNearTermAppliesSharedFlags(t *testing.T) { +func TestResolveGenerateHourlyAppliesSharedFlags(t *testing.T) { runner := Runner{Clock: fixedClock()} configPath := filepath.Join(t.TempDir(), "config.yml") if err := os.WriteFile(configPath, []byte("weather_api:\n units: metric\n timezone: UTC\n"), 0o600); err != nil { t.Fatalf("write config: %v", err) } - req, err := runner.resolveGenerate([]string{"near-term", "--config", configPath, "--units", "us", "--tz", "America/Chicago", "--out", "./near-term.md"}) + req, err := runner.resolveGenerate([]string{"hourly", "--config", configPath, "--units", "us", "--tz", "America/Chicago", "--out", "./hourly.md"}) if err != nil { t.Fatalf("resolveGenerate() error = %v", err) } - if req.Report != app.ReportNearTerm { - t.Fatalf("Report = %q, want near-term", req.Report) + if req.Report != app.ReportHourly { + t.Fatalf("Report = %q, want hourly", req.Report) } if req.Config.WeatherAPI.Units != "us" { t.Fatalf("Units = %q, want us", req.Config.WeatherAPI.Units) @@ -852,21 +852,21 @@ func TestResolveGenerateNearTermAppliesSharedFlags(t *testing.T) { if req.Config.WeatherAPI.Timezone != "America/Chicago" { t.Fatalf("Timezone = %q, want America/Chicago", req.Config.WeatherAPI.Timezone) } - if req.OutputPath != "./near-term.md" { - t.Fatalf("OutputPath = %q, want ./near-term.md", req.OutputPath) + if req.OutputPath != "./hourly.md" { + t.Fatalf("OutputPath = %q, want ./hourly.md", req.OutputPath) } if !req.Date.IsZero() || !req.StormStart.IsZero() || !req.StormEnd.IsZero() { - t.Fatalf("date/storm bounds = %s/%s/%s, want unset for near-term", req.Date, req.StormStart, req.StormEnd) + t.Fatalf("date/storm bounds = %s/%s/%s, want unset for hourly", req.Date, req.StormStart, req.StormEnd) } } -func TestResolveGenerateNearTermRejectsDateAndStormBounds(t *testing.T) { +func TestResolveGenerateHourlyRejectsDateAndStormBounds(t *testing.T) { runner := Runner{Clock: fixedClock()} for _, args := range [][]string{ - {"near-term", "--date", "2026-05-29"}, - {"near-term", "--start", "2026-05-29T18:00"}, - {"near-term", "--end", "2026-05-29T20:00"}, + {"hourly", "--date", "2026-05-29"}, + {"hourly", "--start", "2026-05-29T18:00"}, + {"hourly", "--end", "2026-05-29T20:00"}, } { _, err := runner.resolveGenerate(args) if err == nil { diff --git a/internal/config/config_test.go b/internal/config/config_test.go index 7db698a..5ea372c 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -161,19 +161,10 @@ reports: } } -func TestLoadNearTermReportModuleOverrides(t *testing.T) { - tests := []struct { - name string - key string - }{ - {name: "UnderscoreAlias", key: "near_term"}, - {name: "HyphenAlias", key: "near-term"}, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - path := writeConfig(t, ` +func TestLoadHourlyReportModuleOverrides(t *testing.T) { + path := writeConfig(t, ` reports: - `+tt.key+`: + hourly: deterministic_modules: - metadata - hourly_forecast @@ -181,22 +172,20 @@ reports: - alert_digest `) - cfg, err := LoadFile(path) - if err != nil { - t.Fatalf("LoadFile() error = %v", err) - } - overrides := cfg.ReportModuleOverrides() - items := overrides[report.NearTerm] - if len(items) != 4 { - t.Fatalf("near-term override length = %d, want 4", len(items)) - } - if items[0].ID != module.Metadata || - items[1].ID != module.HourlyForecast || - items[2].ID != module.PrecipTiming || - items[3].ID != module.AlertDigest { - t.Fatalf("near-term override = %#v, want configured module order", items) - } - }) + cfg, err := LoadFile(path) + if err != nil { + t.Fatalf("LoadFile() error = %v", err) + } + overrides := cfg.ReportModuleOverrides() + items := overrides[report.Hourly] + if len(items) != 4 { + t.Fatalf("hourly override length = %d, want 4", len(items)) + } + if items[0].ID != module.Metadata || + items[1].ID != module.HourlyForecast || + items[2].ID != module.PrecipTiming || + items[3].ID != module.AlertDigest { + t.Fatalf("hourly override = %#v, want configured module order", items) } } @@ -284,27 +273,14 @@ reports: wantErr: "duplicates report override", }, { - name: "DuplicateNearTermAlias", + name: "HourlyIncompatibleDailyModule", yaml: ` reports: - near_term: - deterministic_modules: - - metadata - near-term: - deterministic_modules: - - current_conditions -`, - wantErr: "duplicates report override", - }, - { - name: "NearTermIncompatibleDailyModule", - yaml: ` -reports: - near_term: + hourly: deterministic_modules: - derived_daily_summary `, - wantErr: `not compatible with report "near_term"`, + wantErr: `not compatible with report "hourly"`, }, { name: "UnknownReportField", diff --git a/internal/config/reports.go b/internal/config/reports.go index 0e3500b..3dd041c 100644 --- a/internal/config/reports.go +++ b/internal/config/reports.go @@ -113,8 +113,8 @@ func reportIDForConfigKey(key string) (report.ID, error) { return report.DailyToday, nil case "tomorrow", "daily_tomorrow": return report.DailyTomorrow, nil - case "near_term": - return report.NearTerm, nil + case "hourly": + return report.Hourly, nil case "three_day", "three_day_outlook": return report.ThreeDay, nil case "weekend", "weekend_outlook": diff --git a/internal/facts/facts.go b/internal/facts/facts.go index 6d4defd..843e7b9 100644 --- a/internal/facts/facts.go +++ b/internal/facts/facts.go @@ -114,7 +114,7 @@ func BuildDerived(req BuildDerivedRequest) (DerivedFacts, error) { derived.PrecipTiming = forecast.BuildPrecipTiming(derived.ValidPeriodHourlyPeriods) switch req.Resolved.Definition.ID { - case report.NearTerm: + case report.Hourly: case report.DailyToday, report.DailyTomorrow: summary, err := forecast.BuildDailySummary(bundle, period.Start, location, req.Dayparts) if err != nil { diff --git a/internal/facts/facts_test.go b/internal/facts/facts_test.go index 3b85d1f..1c9ef8f 100644 --- a/internal/facts/facts_test.go +++ b/internal/facts/facts_test.go @@ -152,9 +152,9 @@ func TestBuildDerivedWeekendAndTomorrow(t *testing.T) { } } -func TestBuildDerivedNearTermUsesRollingWindowFacts(t *testing.T) { +func TestBuildDerivedHourlyUsesRollingWindowFacts(t *testing.T) { location := testLocation() - resolved := resolveForTest(t, report.NearTerm, mustParse("2026-05-29T08:30:00-05:00"), location) + resolved := resolveForTest(t, report.Hourly, mustParse("2026-05-29T08:30:00-05:00"), location) bundle := testBundle(location) bundle.Alerts = &weatherdata.AlertRun{Alerts: []json.RawMessage{ json.RawMessage(`{"event":"Expired Advisory","headline":"Ends at start","severity":"Minor","effective":"2026-05-29T05:00:00-05:00","expires":"2026-05-29T08:30:00-05:00"}`), @@ -184,7 +184,7 @@ func TestBuildDerivedNearTermUsesRollingWindowFacts(t *testing.T) { t.Fatalf("ValidPeriodNarrativePeriods length = %d, want overlapping narrative period", len(derived.ValidPeriodNarrativePeriods)) } if len(derived.DailySummaries) != 0 || len(derived.DaypartSummaries) != 0 || derived.StormWindowSummary != nil { - t.Fatalf("near-term summaries daily=%#v daypart=%#v storm=%#v, want none", derived.DailySummaries, derived.DaypartSummaries, derived.StormWindowSummary) + t.Fatalf("hourly summaries daily=%#v daypart=%#v storm=%#v, want none", derived.DailySummaries, derived.DaypartSummaries, derived.StormWindowSummary) } if derived.PrecipTiming.FirstPrecipitation == nil || derived.PrecipTiming.FirstPrecipitation.Time.Format(time.RFC3339) != "2026-05-29T08:00:00-05:00" { t.Fatalf("PrecipTiming.FirstPrecipitation = %#v, want first selected rainy hour", derived.PrecipTiming.FirstPrecipitation) @@ -193,13 +193,13 @@ func TestBuildDerivedNearTermUsesRollingWindowFacts(t *testing.T) { t.Fatalf("PrecipTiming.LastPrecipitation = %#v, want final selected rainy window end", derived.PrecipTiming.LastPrecipitation) } if len(derived.PrecipTiming.PrecipitationWindows) != 2 { - t.Fatalf("PrecipTiming.PrecipitationWindows = %#v, want two near-term windows", derived.PrecipTiming.PrecipitationWindows) + t.Fatalf("PrecipTiming.PrecipitationWindows = %#v, want two hourly windows", derived.PrecipTiming.PrecipitationWindows) } if len(derived.AlertOverlaps) != 1 || derived.AlertOverlaps[0].Event != "Flood Watch" { - t.Fatalf("AlertOverlaps = %#v, want only alert overlapping near-term period", derived.AlertOverlaps) + t.Fatalf("AlertOverlaps = %#v, want only alert overlapping hourly period", derived.AlertOverlaps) } if derived.AlertOverlaps[0].Overlap.End.Format(time.RFC3339) != "2026-05-29T14:30:00-05:00" { - t.Fatalf("Alert overlap end = %s, want clipped to near-term end", derived.AlertOverlaps[0].Overlap.End.Format(time.RFC3339)) + t.Fatalf("Alert overlap end = %s, want clipped to hourly end", derived.AlertOverlaps[0].Overlap.End.Format(time.RFC3339)) } if got, want := outlookIDs(derived.SPCConvectiveOutlooks), []string{"fri-high", "fri-storm", "fri-low", "fri-missing-rank", "fri-probabilistic"}; strings.Join(got, ",") != strings.Join(want, ",") { t.Fatalf("SPCConvectiveOutlooks IDs = %#v, want %#v", got, want) @@ -306,7 +306,7 @@ func TestBuildDerivedSelectsSPCConvectiveOutlooksByValidPeriod(t *testing.T) { func TestBuildDerivedUnsupportedReportReturnsActionableError(t *testing.T) { location := testLocation() - resolved := resolveForTest(t, report.NearTerm, mustParse("2026-05-29T08:00:00-05:00"), location) + resolved := resolveForTest(t, report.Hourly, mustParse("2026-05-29T08:00:00-05:00"), location) resolved.Definition.ID = report.ID("future_report") _, err := BuildDerived(BuildDerivedRequest{ diff --git a/internal/report/definition.go b/internal/report/definition.go index 81bfe67..42278cf 100644 --- a/internal/report/definition.go +++ b/internal/report/definition.go @@ -14,7 +14,7 @@ type ID string const ( DailyToday ID = "daily_today" DailyTomorrow ID = "daily_tomorrow" - NearTerm ID = "near_term" + Hourly ID = "hourly" ThreeDay ID = "three_day" Weekend ID = "weekend" Storm ID = "storm" diff --git a/internal/report/near_term_report.go b/internal/report/hourly_report.go similarity index 59% rename from internal/report/near_term_report.go rename to internal/report/hourly_report.go index 4a81479..8944fcc 100644 --- a/internal/report/near_term_report.go +++ b/internal/report/hourly_report.go @@ -7,24 +7,24 @@ import ( "gitea.maximumdirect.net/eric/weatherreporter/internal/timeutil" ) -const nearTermHours = 6 +const hourlyHours = 6 -func nearTermDefinition() Definition { +func hourlyDefinition() Definition { return Definition{ - ID: NearTerm, - Name: "Near-Term Report", - PromptID: "weather.near_term_report", + ID: Hourly, + Name: "Hourly Report", + PromptID: "weather.hourly_report", ComparisonStrategy: CompareRollingWindow, - ArtifactGroup: "near-term", - BatchOutputName: "near-term.md", + ArtifactGroup: "hourly", + BatchOutputName: "hourly.md", Generated: true, - CompatiblePriorIDs: []ID{NearTerm}, - Modules: nearTermModules(), - resolve: resolveNearTerm, + CompatiblePriorIDs: []ID{Hourly}, + Modules: hourlyModules(), + resolve: resolveHourly, } } -func nearTermModules() []module.ConfigItem { +func hourlyModules() []module.ConfigItem { return []module.ConfigItem{ {ID: module.Metadata}, {ID: module.CurrentConditions}, @@ -43,10 +43,10 @@ func nearTermModules() []module.ConfigItem { } } -func resolveNearTerm(req ResolveRequest) (timeutil.Period, error) { +func resolveHourly(req ResolveRequest) (timeutil.Period, error) { localNow := req.Now.In(req.Location) return timeutil.Period{ Start: localNow, - End: localNow.Add(nearTermHours * time.Hour), + End: localNow.Add(hourlyHours * time.Hour), }, nil } diff --git a/internal/report/period_test.go b/internal/report/period_test.go index 8b388a8..5c98fa6 100644 --- a/internal/report/period_test.go +++ b/internal/report/period_test.go @@ -58,19 +58,19 @@ func TestThreeDayPeriodCalculation(t *testing.T) { assertPeriod(t, resolved.ValidPeriod, "2026-05-29T05:00:00-05:00", "2026-06-01T00:00:00-05:00") } -func TestNearTermLookupAndPeriodCalculation(t *testing.T) { +func TestHourlyLookupAndPeriodCalculation(t *testing.T) { location := mustLoadLocation(t) now := mustParse("2026-05-29T05:15:00-05:00") - resolved, err := Resolve(NearTerm, ResolveRequest{Now: now, Location: location}) + resolved, err := Resolve(Hourly, ResolveRequest{Now: now, Location: location}) if err != nil { t.Fatalf("Resolve() error = %v", err) } - if resolved.Definition.ID != NearTerm { - t.Fatalf("ID = %q, want near_term", resolved.Definition.ID) + if resolved.Definition.ID != Hourly { + t.Fatalf("ID = %q, want hourly", resolved.Definition.ID) } - if resolved.Definition.PromptID != "weather.near_term_report" { - t.Fatalf("PromptID = %q, want weather.near_term_report", resolved.Definition.PromptID) + if resolved.Definition.PromptID != "weather.hourly_report" { + t.Fatalf("PromptID = %q, want weather.hourly_report", resolved.Definition.PromptID) } if resolved.Definition.ComparisonStrategy != CompareRollingWindow { t.Fatalf("ComparisonStrategy = %q, want rolling_window", resolved.Definition.ComparisonStrategy) @@ -78,25 +78,25 @@ func TestNearTermLookupAndPeriodCalculation(t *testing.T) { assertPeriod(t, resolved.ValidPeriod, "2026-05-29T05:15:00-05:00", "2026-05-29T11:15:00-05:00") } -func TestNearTermPeriodUsesEffectiveTimezone(t *testing.T) { +func TestHourlyPeriodUsesEffectiveTimezone(t *testing.T) { location, err := time.LoadLocation("America/New_York") if err != nil { t.Fatalf("load location: %v", err) } now := mustParse("2026-05-29T10:15:00Z") - resolved, err := Resolve(NearTerm, ResolveRequest{Now: now, Location: location}) + resolved, err := Resolve(Hourly, ResolveRequest{Now: now, Location: location}) if err != nil { t.Fatalf("Resolve() error = %v", err) } assertPeriod(t, resolved.ValidPeriod, "2026-05-29T06:15:00-04:00", "2026-05-29T12:15:00-04:00") } -func TestNearTermPeriodIsNotCivilDayTruncated(t *testing.T) { +func TestHourlyPeriodIsNotCivilDayTruncated(t *testing.T) { location := mustLoadLocation(t) now := mustParse("2026-05-29T22:30:00-05:00") - resolved, err := Resolve(NearTerm, ResolveRequest{Now: now, Location: location}) + resolved, err := Resolve(Hourly, ResolveRequest{Now: now, Location: location}) if err != nil { t.Fatalf("Resolve() error = %v", err) } @@ -203,7 +203,7 @@ func TestEveningBatchIncludesTomorrow(t *testing.T) { } } -func TestBatchesDoNotIncludeNearTerm(t *testing.T) { +func TestBatchesDoNotIncludeHourly(t *testing.T) { location := mustLoadLocation(t) req := ResolveRequest{Now: mustParse("2026-05-29T06:00:00-05:00"), Location: location} @@ -216,8 +216,8 @@ func TestBatchesDoNotIncludeNearTerm(t *testing.T) { t.Fatalf("BatchReports(evening) error = %v", err) } for _, resolved := range append(morning, evening...) { - if resolved.Definition.ID == NearTerm { - t.Fatalf("batch included %q, want near-term excluded", resolved.Definition.ID) + if resolved.Definition.ID == Hourly { + t.Fatalf("batch included %q, want hourly excluded", resolved.Definition.ID) } } } @@ -232,9 +232,9 @@ func TestRegistryLookupErrorIsActionable(t *testing.T) { } } -func TestRegistryAllIncludesNearTermInStableOrder(t *testing.T) { +func TestRegistryAllIncludesHourlyInStableOrder(t *testing.T) { ids := resolvedDefinitionIDs(DefaultRegistry().All()) - want := []string{"daily_today", "daily_tomorrow", "near_term", "three_day", "weekend", "storm"} + want := []string{"daily_today", "daily_tomorrow", "hourly", "three_day", "weekend", "storm"} if strings.Join(ids, ",") != strings.Join(want, ",") { t.Fatalf("All() ids = %#v, want %#v", ids, want) } @@ -277,11 +277,11 @@ func TestRegistryDefinitionsDeclarePathAndCompatibilityPolicy(t *testing.T) { comparisonStrategy: CompareSameValidDate, }, { - id: NearTerm, - artifactGroup: "near-term", - batchOutputName: "near-term.md", + id: Hourly, + artifactGroup: "hourly", + batchOutputName: "hourly.md", generated: true, - compatiblePriorIDs: []ID{NearTerm}, + compatiblePriorIDs: []ID{Hourly}, comparisonStrategy: CompareRollingWindow, }, { @@ -384,7 +384,7 @@ func TestRegistryDefinitionsDeclareDefaultModules(t *testing.T) { }, }, { - id: NearTerm, + id: Hourly, want: []module.ID{ module.Metadata, module.CurrentConditions, @@ -515,20 +515,20 @@ func TestResolvedMetadata(t *testing.T) { } } -func TestNearTermMetadataRunIDIncludesReportID(t *testing.T) { +func TestHourlyMetadataRunIDIncludesReportID(t *testing.T) { location := mustLoadLocation(t) - resolved, err := Resolve(NearTerm, ResolveRequest{Now: mustParse("2026-05-29T05:15:00-05:00"), Location: location}) + resolved, err := Resolve(Hourly, ResolveRequest{Now: mustParse("2026-05-29T05:15:00-05:00"), Location: location}) if err != nil { t.Fatalf("Resolve() error = %v", err) } metadata := resolved.Metadata() - if metadata.ReportID != NearTerm { - t.Fatalf("ReportID = %q, want near_term", metadata.ReportID) + if metadata.ReportID != Hourly { + t.Fatalf("ReportID = %q, want hourly", metadata.ReportID) } - if metadata.PromptID != "weather.near_term_report" { - t.Fatalf("PromptID = %q, want weather.near_term_report", metadata.PromptID) + if metadata.PromptID != "weather.hourly_report" { + t.Fatalf("PromptID = %q, want weather.hourly_report", metadata.PromptID) } - if !strings.Contains(metadata.RunID, "near_term") { + if !strings.Contains(metadata.RunID, "hourly") { t.Fatalf("RunID = %q, want report id", metadata.RunID) } } diff --git a/internal/report/registry.go b/internal/report/registry.go index 536207e..5ce2bee 100644 --- a/internal/report/registry.go +++ b/internal/report/registry.go @@ -14,7 +14,7 @@ func DefaultRegistry() Registry { definitions := []Definition{ dailyTodayDefinition(), dailyTomorrowDefinition(), - nearTermDefinition(), + hourlyDefinition(), threeDayDefinition(), weekendDefinition(), stormDefinition(), @@ -74,7 +74,7 @@ func (r Registry) MustLookup(id ID) Definition { } func (r Registry) All() []Definition { - ids := []ID{DailyToday, DailyTomorrow, NearTerm, ThreeDay, Weekend, Storm} + ids := []ID{DailyToday, DailyTomorrow, Hourly, ThreeDay, Weekend, Storm} out := make([]Definition, 0, len(ids)) for _, id := range ids { if definition, ok := r.definitions[id]; ok { diff --git a/internal/state/filesystem_test.go b/internal/state/filesystem_test.go index 6fdf6e9..420ab90 100644 --- a/internal/state/filesystem_test.go +++ b/internal/state/filesystem_test.go @@ -307,8 +307,8 @@ func TestFindPriorSnapshotSupportsNarrowedWeekendPeriod(t *testing.T) { func TestFindPriorSnapshotIgnoresRollingWindowReports(t *testing.T) { store := newTestStore(t) - first := resolveNearTermAt(t, "2026-05-29T05:00:00-05:00") - second := resolveNearTermAt(t, "2026-05-29T06:00:00-05:00") + first := resolveHourlyAt(t, "2026-05-29T05:00:00-05:00") + second := resolveHourlyAt(t, "2026-05-29T06:00:00-05:00") savePriorMetadata(t, store, first, stateBriefingMetadata(first)) prior, err := store.FindPriorSnapshot(context.Background(), second) @@ -405,7 +405,7 @@ func resolveWeekendAt(t *testing.T, value string) report.Resolved { return resolved } -func resolveNearTermAt(t *testing.T, value string) report.Resolved { +func resolveHourlyAt(t *testing.T, value string) report.Resolved { t.Helper() location, err := timeutil.LoadLocation("America/Chicago") if err != nil { @@ -415,7 +415,7 @@ func resolveNearTermAt(t *testing.T, value string) report.Resolved { if err != nil { t.Fatalf("parse time: %v", err) } - resolved, err := report.DefaultRegistry().Resolve(report.NearTerm, report.ResolveRequest{ + resolved, err := report.DefaultRegistry().Resolve(report.Hourly, report.ResolveRequest{ Now: now, Location: location, })