Rename rolling report to hourly

This commit is contained in:
2026-06-14 04:26:05 +00:00
parent b3f7c9c1f2
commit 8d737395dc
28 changed files with 220 additions and 600 deletions

View File

@@ -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

View File

@@ -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:

View File

@@ -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

View File

@@ -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.

View File

@@ -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`.

View File

@@ -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

View File

@@ -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

View File

@@ -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/
<run_id>.modules.json
<run_id>.metadata.json
near-term/
hourly/
YYYY-MM-DD/
<run_id>.modules.json
<run_id>.metadata.json
@@ -85,7 +85,7 @@ workspace/
weekend/
YYYY-MM-DD/
<run_id>.data_package.yaml
near-term/
hourly/
YYYY-MM-DD/
<run_id>.data_package.yaml
storm/
@@ -101,7 +101,7 @@ workspace/
weekend/
YYYY-MM-DD/
<run_id>.render.json
near-term/
hourly/
YYYY-MM-DD/
<run_id>.render.json
storm/
@@ -117,7 +117,7 @@ workspace/
weekend/
YYYY-MM-DD/
<run_id>.distributor.json
near-term/
hourly/
YYYY-MM-DD/
<run_id>.distributor.json
storm/
@@ -130,7 +130,7 @@ workspace/
<run_id>.md
weekend/
<run_id>.md
near-term/
hourly/
<run_id>.md
storm/
<run_id>.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,

View File

@@ -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."

View File

@@ -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.

View File

@@ -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.

View File

@@ -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:

View File

@@ -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)
}

View File

@@ -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)

View File

@@ -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

View File

@@ -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,
},

View File

@@ -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)
}
})
}

View File

@@ -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):

View File

@@ -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 {

View File

@@ -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",

View File

@@ -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":

View File

@@ -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 {

View File

@@ -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{

View File

@@ -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"

View File

@@ -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
}

View File

@@ -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)
}
}

View File

@@ -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 {

View File

@@ -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,
})