Rename rolling report to hourly
This commit is contained in:
@@ -21,7 +21,7 @@ the managed Markdown report after final metadata is saved.
|
|||||||
weatherreporter --help
|
weatherreporter --help
|
||||||
weatherreporter generate daily [--config PATH] [--units VALUE] [--tz NAME] [--out PATH] [--date YYYY-MM-DD]
|
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 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 three-day [--config PATH] [--units VALUE] [--tz NAME] [--out PATH]
|
||||||
weatherreporter generate weekend [--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
|
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.
|
artifact, managed Markdown report, and metadata under the configured workspace.
|
||||||
`--out` writes an extra Markdown copy for the operator; distributor
|
`--out` writes an extra Markdown copy for the operator; distributor
|
||||||
notification uses the managed report path, not the extra copy. `generate
|
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.
|
effective report timezone, and does not accept date or event window flags.
|
||||||
`generate storm` requires explicit event-window bounds with `--start` and
|
`generate storm` requires explicit event-window bounds with `--start` and
|
||||||
`--end`.
|
`--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
|
status, accepted distributor run ID, or notification error fields for each
|
||||||
attempted report.
|
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`.
|
morning` or `run evening`.
|
||||||
|
|
||||||
`inspect` commands read existing workspace artifacts and emit JSON to stdout.
|
`inspect` commands read existing workspace artifacts and emit JSON to stdout.
|
||||||
@@ -83,7 +83,7 @@ are no distributor-specific CLI flags.
|
|||||||
|
|
||||||
```sh
|
```sh
|
||||||
weatherreporter generate tomorrow --out ./tomorrow.md
|
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 three-day --out ./three-day.md
|
||||||
weatherreporter generate weekend --out ./weekend.md
|
weatherreporter generate weekend --out ./weekend.md
|
||||||
weatherreporter generate storm --start 2026-05-29T18:00 --end 2026-05-30T06:00 --out ./storm.md
|
weatherreporter generate storm --start 2026-05-29T18:00 --end 2026-05-30T06:00 --out ./storm.md
|
||||||
|
|||||||
@@ -181,10 +181,9 @@ snapshot exists and a threshold is crossed.
|
|||||||
`reports` optionally overrides the ordered deterministic modules declared by
|
`reports` optionally overrides the ordered deterministic modules declared by
|
||||||
report definitions. Omit a report entry to use its default module order.
|
report definitions. Omit a report entry to use its default module order.
|
||||||
|
|
||||||
Supported report keys are `daily`, `tomorrow`, `near_term`, `near-term`,
|
Supported report keys are `daily`, `tomorrow`, `hourly`, `three_day`,
|
||||||
`three_day`, `weekend`, and `storm`. Canonical report IDs such as
|
`weekend`, and `storm`. Canonical report IDs such as `daily_today` and
|
||||||
`daily_today` and `daily_tomorrow` are also accepted. `near_term` and
|
`daily_tomorrow` are also accepted.
|
||||||
`near-term` are aliases for the same Near-Term Report override.
|
|
||||||
|
|
||||||
Each report entry supports:
|
Each report entry supports:
|
||||||
|
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ Outputs:
|
|||||||
Every registered composition entry has a builder. Unknown or unimplemented
|
Every registered composition entry has a builder. Unknown or unimplemented
|
||||||
module IDs fail validation instead of being skipped.
|
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`,
|
rolling six-hour period: `metadata`, `current_conditions`, `hourly_forecast`,
|
||||||
`precip_timing`, `alert_digest`, `spc_convective_outlooks`,
|
`precip_timing`, `alert_digest`, `spc_convective_outlooks`,
|
||||||
`area_forecast_discussion`, `spc_convective_discussion`, and `weather_story`.
|
`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.
|
`sourceLocationId` and `sourceLocation` remain source provenance.
|
||||||
|
|
||||||
`area_forecast_discussion` uses optional `sections` configuration to include a
|
`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`.
|
`key_messages` and `short_term`.
|
||||||
|
|
||||||
`spc_convective_outlooks` uses collected SPC run metadata and derived
|
`spc_convective_outlooks` uses collected SPC run metadata and derived
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ Outputs:
|
|||||||
report-period SPC convective outlooks and discussions, daily summaries,
|
report-period SPC convective outlooks and discussions, daily summaries,
|
||||||
daypart summaries, and Storm Report window summary
|
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
|
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
|
from the selected hourly periods, alert overlaps for the six-hour period, and
|
||||||
SPC outlooks/discussions overlapping that period. It does not build daily
|
SPC outlooks/discussions overlapping that period. It does not build daily
|
||||||
@@ -68,7 +68,7 @@ and inspection.
|
|||||||
- Invalid timezone names return an error.
|
- Invalid timezone names return an error.
|
||||||
- Missing required hourly forecast data returns the underlying forecast
|
- Missing required hourly forecast data returns the underlying forecast
|
||||||
derivation error for reports that require daily summaries.
|
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.
|
daypart summaries.
|
||||||
- Missing optional narrative, alert, discussion, daily, or weather story data
|
- Missing optional narrative, alert, discussion, daily, or weather story data
|
||||||
produces empty or nil derived fields.
|
produces empty or nil derived fields.
|
||||||
|
|||||||
@@ -47,9 +47,9 @@ The registry recognizes these IDs:
|
|||||||
Every registered module has a builder. Report composition entries that refer to
|
Every registered module has a builder. Report composition entries that refer to
|
||||||
unknown or unimplemented module IDs fail validation instead of being skipped.
|
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`
|
1. `metadata`
|
||||||
2. `current_conditions`
|
2. `current_conditions`
|
||||||
@@ -61,7 +61,7 @@ The default Near-Term Report module order is:
|
|||||||
8. `spc_convective_discussion`
|
8. `spc_convective_discussion`
|
||||||
9. `weather_story`
|
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
|
Its `area_forecast_discussion` item is configured to include only
|
||||||
`key_messages` and `short_term`.
|
`key_messages` and `short_term`.
|
||||||
|
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ Prompt-facing module intervals use local `period_begins` and `period_ends`
|
|||||||
labels; canonical report metadata and source timestamps remain structured
|
labels; canonical report metadata and source timestamps remain structured
|
||||||
timestamps where applicable.
|
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
|
package includes `precip_timing` under `derived_summaries`, alert and SPC
|
||||||
outlooks under `applicable_risk_products`, AFD/SPC discussion/weather story
|
outlooks under `applicable_risk_products`, AFD/SPC discussion/weather story
|
||||||
under `narrative_products`, and current/hourly data under `raw_data`. It does
|
under `narrative_products`, and current/hourly data under `raw_data`. It does
|
||||||
@@ -112,7 +112,7 @@ None.
|
|||||||
## Skip And Resume Behavior
|
## Skip And Resume Behavior
|
||||||
|
|
||||||
None. Recent Changes is always present as an `items` list and may be empty.
|
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
|
## Failure Behavior
|
||||||
|
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ Each report definition declares:
|
|||||||
| --- | --- | --- | --- | --- | --- |
|
| --- | --- | --- | --- | --- | --- |
|
||||||
| Daily Today | `daily_today` | `weather.daily_report` | `daily` | `daily.md` | Daily Today, Daily Tomorrow |
|
| 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 |
|
| 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 |
|
| 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 |
|
| Weekend Outlook | `weekend` | `weather.weekend_outlook` | `weekend` | `weekend.md` | Weekend Outlook |
|
||||||
| Storm Report | `storm` | `weather.storm_report` | `storm` | `storm.md` | Storm Report |
|
| 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
|
- Daily Today covers the selected local civil day, or the current local civil
|
||||||
day when no date override is supplied.
|
day when no date override is supplied.
|
||||||
- Daily Tomorrow covers the next local civil day from generation time.
|
- 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,
|
the effective report timezone. The duration is an internal report constant,
|
||||||
not a configuration field.
|
not a configuration field.
|
||||||
- 3-Day Outlook covers the interval from generation time through local midnight
|
- 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
|
The app supplies `weather_api.timezone` as a loaded `time.Location`. Batch
|
||||||
output path copying uses batch output names from report definitions. Report
|
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
|
module overrides can use the `hourly` key for Hourly Report.
|
||||||
Report.
|
|
||||||
|
|
||||||
## Batch Membership
|
## Batch Membership
|
||||||
|
|
||||||
Morning batches include Daily Today, 3-Day Outlook, and Weekend Outlook except
|
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.
|
of a scheduled batch.
|
||||||
|
|
||||||
## State And App Usage
|
## State And App Usage
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ Generation commands:
|
|||||||
```text
|
```text
|
||||||
weatherreporter generate daily --date 2026-05-29
|
weatherreporter generate daily --date 2026-05-29
|
||||||
weatherreporter generate tomorrow
|
weatherreporter generate tomorrow
|
||||||
weatherreporter generate near-term
|
weatherreporter generate hourly
|
||||||
weatherreporter generate three-day
|
weatherreporter generate three-day
|
||||||
weatherreporter generate weekend
|
weatherreporter generate weekend
|
||||||
weatherreporter generate storm --start 2026-05-29T18:00 --end 2026-05-30T06:00
|
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
|
`--out PATH` writes an extra Markdown copy for the current generated report; it
|
||||||
is not used as the distributor upload source.
|
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
|
rolling period from generation time in the effective report timezone and is not
|
||||||
part of scheduled morning or evening batches.
|
part of scheduled morning or evening batches.
|
||||||
|
|
||||||
@@ -67,7 +67,7 @@ workspace/
|
|||||||
YYYY-MM-DD/
|
YYYY-MM-DD/
|
||||||
<run_id>.modules.json
|
<run_id>.modules.json
|
||||||
<run_id>.metadata.json
|
<run_id>.metadata.json
|
||||||
near-term/
|
hourly/
|
||||||
YYYY-MM-DD/
|
YYYY-MM-DD/
|
||||||
<run_id>.modules.json
|
<run_id>.modules.json
|
||||||
<run_id>.metadata.json
|
<run_id>.metadata.json
|
||||||
@@ -85,7 +85,7 @@ workspace/
|
|||||||
weekend/
|
weekend/
|
||||||
YYYY-MM-DD/
|
YYYY-MM-DD/
|
||||||
<run_id>.data_package.yaml
|
<run_id>.data_package.yaml
|
||||||
near-term/
|
hourly/
|
||||||
YYYY-MM-DD/
|
YYYY-MM-DD/
|
||||||
<run_id>.data_package.yaml
|
<run_id>.data_package.yaml
|
||||||
storm/
|
storm/
|
||||||
@@ -101,7 +101,7 @@ workspace/
|
|||||||
weekend/
|
weekend/
|
||||||
YYYY-MM-DD/
|
YYYY-MM-DD/
|
||||||
<run_id>.render.json
|
<run_id>.render.json
|
||||||
near-term/
|
hourly/
|
||||||
YYYY-MM-DD/
|
YYYY-MM-DD/
|
||||||
<run_id>.render.json
|
<run_id>.render.json
|
||||||
storm/
|
storm/
|
||||||
@@ -117,7 +117,7 @@ workspace/
|
|||||||
weekend/
|
weekend/
|
||||||
YYYY-MM-DD/
|
YYYY-MM-DD/
|
||||||
<run_id>.distributor.json
|
<run_id>.distributor.json
|
||||||
near-term/
|
hourly/
|
||||||
YYYY-MM-DD/
|
YYYY-MM-DD/
|
||||||
<run_id>.distributor.json
|
<run_id>.distributor.json
|
||||||
storm/
|
storm/
|
||||||
@@ -130,7 +130,7 @@ workspace/
|
|||||||
<run_id>.md
|
<run_id>.md
|
||||||
weekend/
|
weekend/
|
||||||
<run_id>.md
|
<run_id>.md
|
||||||
near-term/
|
hourly/
|
||||||
<run_id>.md
|
<run_id>.md
|
||||||
storm/
|
storm/
|
||||||
<run_id>.md
|
<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
|
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
|
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
|
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.
|
Storm Report leave Recent Changes empty.
|
||||||
|
|
||||||
When no prior comparable snapshot exists, or no configured threshold is crossed,
|
When no prior comparable snapshot exists, or no configured threshold is crossed,
|
||||||
|
|||||||
@@ -4,8 +4,8 @@
|
|||||||
|
|
||||||
This roadmap defines planned work to move selected reports from full Markdown
|
This roadmap defines planned work to move selected reports from full Markdown
|
||||||
LLM generation to deterministic Markdown rendering with structured LLM-filled
|
LLM generation to deterministic Markdown rendering with structured LLM-filled
|
||||||
text slots. The first target report is the rolling next-hours report, now named
|
text slots. The first target report is the rolling next-hours report, named
|
||||||
`hourly` rather than `near_term`.
|
`hourly`.
|
||||||
|
|
||||||
This feature is not implemented yet, so this document lives under
|
This feature is not implemented yet, so this document lives under
|
||||||
`docs/roadmap/`.
|
`docs/roadmap/`.
|
||||||
@@ -67,15 +67,14 @@ not become a second application logic layer.
|
|||||||
Schema dependency in the first implementation.
|
Schema dependency in the first implementation.
|
||||||
- Do not generate schemas or templates from inline Go.
|
- Do not generate schemas or templates from inline Go.
|
||||||
- Start with the rolling next-hours report only.
|
- Start with the rolling next-hours report only.
|
||||||
- Rename the planned `near_term` report to `hourly`.
|
- Use `hourly` as the only public report ID for the rolling next-hours
|
||||||
- Do not keep both `near_term` and `hourly` public report IDs.
|
report.
|
||||||
|
- Do not keep compatibility aliases from retired roadmap terminology.
|
||||||
- Do not add a generic workflow engine or plugin system.
|
- Do not add a generic workflow engine or plugin system.
|
||||||
|
|
||||||
## Hourly Report Target
|
## Hourly Report Target
|
||||||
|
|
||||||
The planned `near_term` report should become the `hourly` report.
|
The rolling next-hours report target identity is:
|
||||||
|
|
||||||
Target report identity:
|
|
||||||
|
|
||||||
- report ID: `hourly`
|
- report ID: `hourly`
|
||||||
- CLI command: `weatherreporter generate hourly`
|
- CLI command: `weatherreporter generate hourly`
|
||||||
@@ -143,7 +142,7 @@ Keep the first schema small. Example shape:
|
|||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"summary": "Brief near-term overview.",
|
"summary": "Brief hourly overview.",
|
||||||
"timing": "Plain-language timing of notable changes.",
|
"timing": "Plain-language timing of notable changes.",
|
||||||
"impacts": "Practical impacts for the next few hours.",
|
"impacts": "Practical impacts for the next few hours.",
|
||||||
"confidence": "Optional confidence or uncertainty note."
|
"confidence": "Optional confidence or uncertainty note."
|
||||||
|
|||||||
@@ -7,8 +7,8 @@ This roadmap defines the concrete implementation sequence for
|
|||||||
will implement each stage in order.
|
will implement each stage in order.
|
||||||
|
|
||||||
This is a future-work roadmap. Until a stage is implemented, non-roadmap docs
|
This is a future-work roadmap. Until a stage is implemented, non-roadmap docs
|
||||||
must not describe `GeneratedText`, embedded report templates, or
|
must not describe `GeneratedText`, embedded report templates, or other
|
||||||
`weatherreporter generate hourly` as available behavior.
|
unimplemented GeneratedText behavior as available.
|
||||||
|
|
||||||
## Source Feature Roadmap
|
## Source Feature Roadmap
|
||||||
|
|
||||||
@@ -26,8 +26,8 @@ for reaching that shape.
|
|||||||
|
|
||||||
- Implement the first GeneratedText path for the rolling next-hours report,
|
- Implement the first GeneratedText path for the rolling next-hours report,
|
||||||
named `hourly`.
|
named `hourly`.
|
||||||
- Replace planned/public `near_term` naming with `hourly`; do not keep a
|
- Use `hourly` as the only planned public name for the rolling next-hours
|
||||||
public `near-term` alias.
|
report; do not keep compatibility aliases from retired roadmap terminology.
|
||||||
- Use report ID `hourly`, artifact group `hourly`, batch output name
|
- Use report ID `hourly`, artifact group `hourly`, batch output name
|
||||||
`hourly.md`, and CLI command `weatherreporter generate hourly`.
|
`hourly.md`, and CLI command `weatherreporter generate hourly`.
|
||||||
- Keep the rolling report explicit-generation only; do not add scheduled batch
|
- 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:
|
Files to inspect:
|
||||||
|
|
||||||
- `docs/roadmap/generated-text.md`
|
- `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/future.md`
|
||||||
- `docs/roadmap/implementation.md`
|
- `docs/roadmap/implementation.md`
|
||||||
- `internal/app/app.go`
|
- `internal/app/app.go`
|
||||||
@@ -70,30 +70,32 @@ Files to inspect:
|
|||||||
|
|
||||||
Implementation:
|
Implementation:
|
||||||
|
|
||||||
- Replace `docs/roadmap/near-term.md` with a short superseded note pointing to
|
- Replace the superseded roadmap file for the retired rolling-report name with
|
||||||
`docs/roadmap/generated-text.md`, or delete it if the repository policy at
|
a short note pointing to `docs/roadmap/generated-text.md`, or delete it if
|
||||||
implementation time prefers removing obsolete roadmap files.
|
the repository policy at implementation time prefers removing obsolete
|
||||||
- Ensure no roadmap instructs a future agent to implement public
|
roadmap files.
|
||||||
`near_term` / `near-term` behavior.
|
- Ensure no roadmap instructs a future agent to implement retired public
|
||||||
- If partial `near_term` code already exists, rename it to `hourly` rather
|
identifiers for the rolling report.
|
||||||
than adding aliases:
|
- If partial code using retired identifiers already exists, rename it to
|
||||||
- `ReportNearTerm` -> `ReportHourly`;
|
`hourly` rather than adding aliases:
|
||||||
- `report.NearTerm` -> `report.Hourly`;
|
- app report kind -> `ReportHourly`;
|
||||||
- CLI command `near-term` -> `hourly`;
|
- report ID constant -> `report.Hourly`;
|
||||||
- artifact group `near-term` -> `hourly`;
|
- CLI command -> `hourly`;
|
||||||
|
- artifact group -> `hourly`;
|
||||||
- prompt IDs and tests updated to hourly names.
|
- 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:
|
Acceptance criteria:
|
||||||
|
|
||||||
- Active roadmap docs use `hourly` for the rolling report.
|
- 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.
|
- Existing code compiles after any rename work.
|
||||||
|
|
||||||
Suggested validation:
|
Suggested validation:
|
||||||
|
|
||||||
```bash
|
```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
|
go test ./internal/app ./internal/report ./internal/cli
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -208,7 +210,7 @@ Implementation:
|
|||||||
- no daily summaries, daypart summaries, or storm summary.
|
- no daily summaries, daypart summaries, or storm summary.
|
||||||
- Add config report aliases:
|
- Add config report aliases:
|
||||||
- `hourly`
|
- `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
|
- Add app report kind `ReportHourly = "hourly"` and map it to
|
||||||
`report.Hourly`.
|
`report.Hourly`.
|
||||||
- Add CLI parsing and help for:
|
- Add CLI parsing and help for:
|
||||||
@@ -651,17 +653,17 @@ Files to inspect:
|
|||||||
Implementation:
|
Implementation:
|
||||||
|
|
||||||
- Add CLI command tests for `generate hourly`.
|
- 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.
|
- Reject hourly `--date`, `--start`, `--end`, and duration-like flags.
|
||||||
- Add config override tests for `reports.hourly.deterministic_modules`.
|
- 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.
|
- Add app/CLI workflow tests using fake Scriptorium structured output.
|
||||||
- Update examples only if they enumerate report module overrides.
|
- Update examples only if they enumerate report module overrides.
|
||||||
|
|
||||||
Acceptance criteria:
|
Acceptance criteria:
|
||||||
|
|
||||||
- `weatherreporter --help` lists `generate hourly`;
|
- `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;
|
- hourly config overrides validate compatible modules;
|
||||||
- incompatible daily/daypart modules fail clearly for hourly;
|
- incompatible daily/daypart modules fail clearly for hourly;
|
||||||
- examples load successfully.
|
- examples load successfully.
|
||||||
@@ -693,13 +695,13 @@ Files to inspect and update:
|
|||||||
- new `docs/internal/reporttemplate.md`
|
- new `docs/internal/reporttemplate.md`
|
||||||
- `docs/integrations/scriptorium.md`
|
- `docs/integrations/scriptorium.md`
|
||||||
- `docs/roadmap/future.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
|
- `examples/config.yml`, only if examples changed
|
||||||
|
|
||||||
Documentation requirements:
|
Documentation requirements:
|
||||||
|
|
||||||
- Non-roadmap docs describe only implemented hourly and GeneratedText behavior.
|
- 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
|
- `docs/config.md` documents `reports.hourly` only if override support is
|
||||||
implemented.
|
implemented.
|
||||||
- Internal docs distinguish:
|
- Internal docs distinguish:
|
||||||
@@ -711,20 +713,22 @@ Documentation requirements:
|
|||||||
- Scriptorium integration docs include the structured-output command actually
|
- Scriptorium integration docs include the structured-output command actually
|
||||||
used by the adapter.
|
used by the adapter.
|
||||||
- State docs list generated-text artifacts and metadata links.
|
- 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.
|
- Deferred migrations for other report types stay under roadmap docs only.
|
||||||
|
|
||||||
Acceptance criteria:
|
Acceptance criteria:
|
||||||
|
|
||||||
- No non-roadmap docs describe unimplemented generated-text migrations.
|
- 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.
|
- Maintained examples remain valid.
|
||||||
|
|
||||||
Suggested validation:
|
Suggested validation:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
go test ./internal/config
|
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
|
git diff --check
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -745,7 +749,7 @@ git diff --check
|
|||||||
Manual checks:
|
Manual checks:
|
||||||
|
|
||||||
- `weatherreporter --help` lists `generate hourly`.
|
- `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.
|
- generated hourly Markdown has deterministic headings and section order.
|
||||||
- GeneratedText JSON is persisted separately from deterministic facts.
|
- GeneratedText JSON is persisted separately from deterministic facts.
|
||||||
- render context JSON is persisted and readable.
|
- render context JSON is persisted and readable.
|
||||||
|
|||||||
@@ -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
|
The rolling next-hours report is named `hourly`. Use the GeneratedText roadmap
|
||||||
on the next several hours. The feature is not implemented yet, so this document
|
for current future-work guidance.
|
||||||
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.
|
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ type ReportKind string
|
|||||||
const (
|
const (
|
||||||
ReportDaily ReportKind = "daily"
|
ReportDaily ReportKind = "daily"
|
||||||
ReportTomorrow ReportKind = "tomorrow"
|
ReportTomorrow ReportKind = "tomorrow"
|
||||||
ReportNearTerm ReportKind = "near-term"
|
ReportHourly ReportKind = "hourly"
|
||||||
ReportThreeDay ReportKind = "three-day"
|
ReportThreeDay ReportKind = "three-day"
|
||||||
ReportWeekend ReportKind = "weekend"
|
ReportWeekend ReportKind = "weekend"
|
||||||
ReportStorm ReportKind = "storm"
|
ReportStorm ReportKind = "storm"
|
||||||
@@ -387,8 +387,8 @@ func reportIDForCommand(kind ReportKind) (report.ID, error) {
|
|||||||
return report.DailyToday, nil
|
return report.DailyToday, nil
|
||||||
case ReportTomorrow:
|
case ReportTomorrow:
|
||||||
return report.DailyTomorrow, nil
|
return report.DailyTomorrow, nil
|
||||||
case ReportNearTerm:
|
case ReportHourly:
|
||||||
return report.NearTerm, nil
|
return report.Hourly, nil
|
||||||
case ReportThreeDay:
|
case ReportThreeDay:
|
||||||
return report.ThreeDay, nil
|
return report.ThreeDay, nil
|
||||||
case ReportWeekend:
|
case ReportWeekend:
|
||||||
|
|||||||
@@ -349,19 +349,19 @@ func TestGenerateReportOmitsSPCConvectiveDiscussionBelowThreshold(t *testing.T)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestGenerateNearTermReportWritesExpectedArtifacts(t *testing.T) {
|
func TestGenerateHourlyReportWritesExpectedArtifacts(t *testing.T) {
|
||||||
server := nearTermBundleServer(t)
|
server := hourlyBundleServer(t)
|
||||||
cfg := dailyTestConfig(t, server)
|
cfg := dailyTestConfig(t, server)
|
||||||
cfg.Workspace.Root = t.TempDir()
|
cfg.Workspace.Root = t.TempDir()
|
||||||
resolved, err := ResolveGenerate(GenerateRequest{
|
resolved, err := ResolveGenerate(GenerateRequest{
|
||||||
Config: cfg,
|
Config: cfg,
|
||||||
Report: ReportNearTerm,
|
Report: ReportHourly,
|
||||||
}, mustParse("2026-05-29T08:30:00-05:00"))
|
}, mustParse("2026-05-29T08:30:00-05:00"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("ResolveGenerate() error = %v", err)
|
t.Fatalf("ResolveGenerate() error = %v", err)
|
||||||
}
|
}
|
||||||
outputPath := filepath.Join(t.TempDir(), "near-term.md")
|
outputPath := filepath.Join(t.TempDir(), "hourly.md")
|
||||||
renderer := successfulRenderer("# Near-Term Report\n")
|
renderer := successfulRenderer("# Hourly Report\n")
|
||||||
|
|
||||||
result, err := GenerateReport(context.Background(), ReportRequest{
|
result, err := GenerateReport(context.Background(), ReportRequest{
|
||||||
Config: cfg,
|
Config: cfg,
|
||||||
@@ -373,8 +373,8 @@ func TestGenerateNearTermReportWritesExpectedArtifacts(t *testing.T) {
|
|||||||
t.Fatalf("GenerateReport() error = %v", err)
|
t.Fatalf("GenerateReport() error = %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if result.Metadata.ReportID != report.NearTerm || result.Metadata.PromptID != "weather.near_term_report" {
|
if result.Metadata.ReportID != report.Hourly || result.Metadata.PromptID != "weather.hourly_report" {
|
||||||
t.Fatalf("metadata report/prompt = %q/%q, want near-term", result.Metadata.ReportID, result.Metadata.PromptID)
|
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" {
|
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)
|
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" {
|
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)
|
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" {
|
if renderer.renderRequest.PromptID != "weather.hourly_report" || renderer.runRequest.PromptID != "weather.hourly_report" {
|
||||||
t.Fatalf("renderer prompt IDs = %q/%q, want near-term prompt", renderer.renderRequest.PromptID, renderer.runRequest.PromptID)
|
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")) {
|
if !strings.Contains(result.ReportPath, filepath.Join("reports", "hourly")) {
|
||||||
t.Fatalf("ReportPath = %q, want near-term artifact group", result.ReportPath)
|
t.Fatalf("ReportPath = %q, want hourly artifact group", result.ReportPath)
|
||||||
}
|
}
|
||||||
if !strings.Contains(result.DataPackagePath, filepath.Join("data-packages", "near-term", "2026-05-29")) {
|
if !strings.Contains(result.DataPackagePath, filepath.Join("data-packages", "hourly", "2026-05-29")) {
|
||||||
t.Fatalf("DataPackagePath = %q, want near-term artifact group", result.DataPackagePath)
|
t.Fatalf("DataPackagePath = %q, want hourly artifact group", result.DataPackagePath)
|
||||||
}
|
}
|
||||||
assertPathsExist(t, result.ModuleSnapshotPath, result.DataPackagePath, result.PreflightPath, result.ReportPath, result.MetadataPath, outputPath)
|
assertPathsExist(t, result.ModuleSnapshotPath, result.DataPackagePath, result.PreflightPath, result.ReportPath, result.MetadataPath, outputPath)
|
||||||
if result.OutputPath != outputPath {
|
if result.OutputPath != outputPath {
|
||||||
@@ -399,7 +399,7 @@ func TestGenerateNearTermReportWritesExpectedArtifacts(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("read output copy: %v", err)
|
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))
|
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)
|
t.Fatalf("decode hourly forecast: %v", err)
|
||||||
}
|
}
|
||||||
if !ok || len(hourly.Periods) != 5 {
|
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)
|
hourlyJSON := mustMarshalString(t, hourly)
|
||||||
if !strings.Contains(hourlyJSON, "Showers entering the area") || !strings.Contains(hourlyJSON, "Heavy rain") {
|
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") {
|
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)
|
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)
|
t.Fatalf("decode precip timing: %v", err)
|
||||||
}
|
}
|
||||||
if !ok || precip.MaxPopPercent == nil || *precip.MaxPopPercent != 80 || len(precip.PrecipitationWindows) != 2 {
|
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" {
|
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])
|
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" {
|
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")
|
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)
|
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" {
|
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")
|
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)
|
t.Fatalf("decode SPC outlooks: %v", err)
|
||||||
}
|
}
|
||||||
if !ok || !outlooks.Checked || outlooks.OutlookCount != 1 || len(outlooks.Outlooks) != 1 || outlooks.Outlooks[0].Label != "SLGT" {
|
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")
|
discussion, ok, err := module.StanzaValue[briefing.SPCConvectiveDiscussionModule](result.ModuleSnapshot, "spc_convective_discussion")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("decode SPC discussion: %v", err)
|
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)
|
t.Fatalf("SPC discussion = %#v, want discussion for retained overlapping outlook", discussion)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -474,11 +474,11 @@ func TestGenerateNearTermReportWritesExpectedArtifacts(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("decode AFD: %v", err)
|
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)
|
t.Fatalf("AFD = %#v, want key messages and short term", afd)
|
||||||
}
|
}
|
||||||
if afd.Product != "" || afd.LongTerm != "" {
|
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 {
|
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)
|
data := readDataPackageForTest(t, result)
|
||||||
text := string(data)
|
text := string(data)
|
||||||
for _, want := range []string{
|
for _, want := range []string{
|
||||||
"id: near_term",
|
"id: hourly",
|
||||||
"prompt_id: weather.near_term_report",
|
"prompt_id: weather.hourly_report",
|
||||||
"valid_period:",
|
"valid_period:",
|
||||||
"recent_changes:",
|
"recent_changes:",
|
||||||
" items: []",
|
" items: []",
|
||||||
@@ -505,9 +505,9 @@ func TestGenerateNearTermReportWritesExpectedArtifacts(t *testing.T) {
|
|||||||
t.Fatalf("data package missing %q:\n%s", want, text)
|
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) {
|
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:")
|
riskIndex := strings.Index(text, " applicable_risk_products:")
|
||||||
@@ -1364,10 +1364,10 @@ func TestResolveGenerateMapsCommandToReportDefinition(t *testing.T) {
|
|||||||
requestDate: time.Time{},
|
requestDate: time.Time{},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "near-term",
|
name: "hourly",
|
||||||
kind: ReportNearTerm,
|
kind: ReportHourly,
|
||||||
wantID: report.NearTerm,
|
wantID: report.Hourly,
|
||||||
wantPrompt: "weather.near_term_report",
|
wantPrompt: "weather.hourly_report",
|
||||||
wantStart: "2026-05-29T08:00:00-05:00",
|
wantStart: "2026-05-29T08:00:00-05:00",
|
||||||
wantEnd: "2026-05-29T14: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
|
return server
|
||||||
}
|
}
|
||||||
|
|
||||||
func nearTermBundleServer(t *testing.T) *httptest.Server {
|
func hourlyBundleServer(t *testing.T) *httptest.Server {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
switch r.URL.Path {
|
switch r.URL.Path {
|
||||||
@@ -1503,11 +1503,11 @@ func nearTermBundleServer(t *testing.T) *httptest.Server {
|
|||||||
case "/alerts/active":
|
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"}]}}`))
|
_, _ = 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":
|
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":
|
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":
|
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:
|
default:
|
||||||
http.NotFound(w, r)
|
http.NotFound(w, r)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -97,10 +97,10 @@ func TestHourlyForecastModuleRejectsUnsupportedReports(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestHourlyForecastModuleBuildsForNearTerm(t *testing.T) {
|
func TestHourlyForecastModuleBuildsForHourly(t *testing.T) {
|
||||||
registry := MustDefaultModuleRegistry()
|
registry := MustDefaultModuleRegistry()
|
||||||
ctx := testModuleContext()
|
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})
|
output, err := registry.BuildModule(ctx, module.ConfigItem{ID: module.HourlyForecast})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -108,7 +108,7 @@ func TestHourlyForecastModuleBuildsForNearTerm(t *testing.T) {
|
|||||||
}
|
}
|
||||||
value := moduleValue[HourlyForecastModule](t, output)
|
value := moduleValue[HourlyForecastModule](t, output)
|
||||||
if len(value.Periods) != 1 || value.Periods[0].TextDescription != "Showers likely." {
|
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()
|
registry := MustDefaultModuleRegistry()
|
||||||
ctx := testModuleContext()
|
ctx := testModuleContext()
|
||||||
ctx.Resolved.Definition = report.DefaultRegistry().MustLookup(report.NearTerm)
|
ctx.Resolved.Definition = report.DefaultRegistry().MustLookup(report.Hourly)
|
||||||
var item module.ConfigItem
|
var item module.ConfigItem
|
||||||
for _, candidate := range ctx.Resolved.Definition.Modules {
|
for _, candidate := range ctx.Resolved.Definition.Modules {
|
||||||
if candidate.ID == module.AreaForecastDiscussion {
|
if candidate.ID == module.AreaForecastDiscussion {
|
||||||
@@ -347,7 +347,7 @@ func TestAreaForecastDiscussionModuleUsesNearTermDefaultSections(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if item.ID == "" {
|
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)
|
output, err := registry.BuildModule(ctx, item)
|
||||||
|
|||||||
@@ -138,7 +138,7 @@ func TestPrecipTimingModuleHandlesRainyAndDryForecasts(t *testing.T) {
|
|||||||
|
|
||||||
func TestPrecipTimingModuleUsesDerivedTimingWithoutDaypartSummaries(t *testing.T) {
|
func TestPrecipTimingModuleUsesDerivedTimingWithoutDaypartSummaries(t *testing.T) {
|
||||||
registry := MustDefaultModuleRegistry()
|
registry := MustDefaultModuleRegistry()
|
||||||
ctx := derivedModuleContext(report.NearTerm)
|
ctx := derivedModuleContext(report.Hourly)
|
||||||
ctx.Derived.DailySummaries = nil
|
ctx.Derived.DailySummaries = nil
|
||||||
ctx.Derived.DaypartSummaries = nil
|
ctx.Derived.DaypartSummaries = nil
|
||||||
|
|
||||||
|
|||||||
@@ -249,7 +249,7 @@ func (d ModuleDefinition) ValidateOptions(options any) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func defaultModuleDefinitions() []ModuleDefinition {
|
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}
|
daypartReports := []report.ID{report.DailyToday, report.DailyTomorrow, report.ThreeDay, report.Weekend}
|
||||||
return []ModuleDefinition{
|
return []ModuleDefinition{
|
||||||
{
|
{
|
||||||
@@ -286,7 +286,7 @@ func defaultModuleDefinitions() []ModuleDefinition {
|
|||||||
DefaultOptions: module.HourlyForecastOptions{},
|
DefaultOptions: module.HourlyForecastOptions{},
|
||||||
RequiredCollected: []module.FactRequirement{module.CollectedHourlyForecast},
|
RequiredCollected: []module.FactRequirement{module.CollectedHourlyForecast},
|
||||||
RequiredDerived: []module.FactRequirement{module.RequiresDerivedHourlyPeriods},
|
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,
|
MissingData: module.MissingDataOmit,
|
||||||
Builder: buildHourlyForecastModule,
|
Builder: buildHourlyForecastModule,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -54,8 +54,8 @@ func TestDefaultReportModulesBuildSnapshots(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestNearTermDefaultModuleOptions(t *testing.T) {
|
func TestHourlyDefaultModuleOptions(t *testing.T) {
|
||||||
definition := report.DefaultRegistry().MustLookup(report.NearTerm)
|
definition := report.DefaultRegistry().MustLookup(report.Hourly)
|
||||||
var found bool
|
var found bool
|
||||||
for _, item := range definition.Modules {
|
for _, item := range definition.Modules {
|
||||||
if item.ID != module.AreaForecastDiscussion {
|
if item.ID != module.AreaForecastDiscussion {
|
||||||
@@ -71,7 +71,7 @@ func TestNearTermDefaultModuleOptions(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if !found {
|
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()
|
registry := MustDefaultModuleRegistry()
|
||||||
for _, id := range []module.ID{
|
for _, id := range []module.ID{
|
||||||
module.NarrativeForecast,
|
module.NarrativeForecast,
|
||||||
@@ -122,9 +122,9 @@ func TestModuleRegistryRejectsNearTermIncompatibleModules(t *testing.T) {
|
|||||||
module.TomorrowPlanning,
|
module.TomorrowPlanning,
|
||||||
} {
|
} {
|
||||||
t.Run(string(id), func(t *testing.T) {
|
t.Run(string(id), func(t *testing.T) {
|
||||||
err := registry.ValidateComposition(report.NearTerm, []module.ConfigItem{{ID: id}})
|
err := registry.ValidateComposition(report.Hourly, []module.ConfigItem{{ID: id}})
|
||||||
if err == nil || !strings.Contains(err.Error(), `not compatible with report "near_term"`) {
|
if err == nil || !strings.Contains(err.Error(), `not compatible with report "hourly"`) {
|
||||||
t.Fatalf("ValidateComposition() error = %v, want incompatible near-term module", err)
|
t.Fatalf("ValidateComposition() error = %v, want incompatible hourly module", err)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ Usage:
|
|||||||
weatherreporter --help
|
weatherreporter --help
|
||||||
weatherreporter generate daily [--config PATH] [--units VALUE] [--tz NAME] [--out PATH] [--date YYYY-MM-DD]
|
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 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 three-day [--config PATH] [--units VALUE] [--tz NAME] [--out PATH]
|
||||||
weatherreporter generate weekend [--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
|
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
|
return app.ReportDaily, true
|
||||||
case string(app.ReportTomorrow):
|
case string(app.ReportTomorrow):
|
||||||
return app.ReportTomorrow, true
|
return app.ReportTomorrow, true
|
||||||
case string(app.ReportNearTerm):
|
case string(app.ReportHourly):
|
||||||
return app.ReportNearTerm, true
|
return app.ReportHourly, true
|
||||||
case string(app.ReportThreeDay):
|
case string(app.ReportThreeDay):
|
||||||
return app.ReportThreeDay, true
|
return app.ReportThreeDay, true
|
||||||
case string(app.ReportWeekend):
|
case string(app.ReportWeekend):
|
||||||
|
|||||||
@@ -28,8 +28,8 @@ func TestRunHelpLongFlag(t *testing.T) {
|
|||||||
if !strings.Contains(stdout.String(), "generate daily") {
|
if !strings.Contains(stdout.String(), "generate daily") {
|
||||||
t.Fatalf("help output missing generate command:\n%s", stdout.String())
|
t.Fatalf("help output missing generate command:\n%s", stdout.String())
|
||||||
}
|
}
|
||||||
if !strings.Contains(stdout.String(), "weatherreporter generate near-term") {
|
if !strings.Contains(stdout.String(), "weatherreporter generate hourly") {
|
||||||
t.Fatalf("help output missing near-term generate command:\n%s", stdout.String())
|
t.Fatalf("help output missing hourly generate command:\n%s", stdout.String())
|
||||||
}
|
}
|
||||||
removedInspectCommand := "inspect " + "briefing"
|
removedInspectCommand := "inspect " + "briefing"
|
||||||
if !strings.Contains(stdout.String(), "inspect modules") || strings.Contains(stdout.String(), removedInspectCommand) {
|
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: "daily", args: []string{"daily", "--date", "2026-05-29"}, want: app.ReportDaily},
|
||||||
{name: "tomorrow", args: []string{"tomorrow"}, want: app.ReportTomorrow},
|
{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: "three-day", args: []string{"three-day"}, want: app.ReportThreeDay},
|
||||||
{name: "weekend", args: []string{"weekend"}, want: app.ReportWeekend},
|
{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},
|
{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()}
|
runner := Runner{Clock: fixedClock()}
|
||||||
configPath := filepath.Join(t.TempDir(), "config.yml")
|
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 {
|
if err := os.WriteFile(configPath, []byte("weather_api:\n units: metric\n timezone: UTC\n"), 0o600); err != nil {
|
||||||
t.Fatalf("write config: %v", err)
|
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 {
|
if err != nil {
|
||||||
t.Fatalf("resolveGenerate() error = %v", err)
|
t.Fatalf("resolveGenerate() error = %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if req.Report != app.ReportNearTerm {
|
if req.Report != app.ReportHourly {
|
||||||
t.Fatalf("Report = %q, want near-term", req.Report)
|
t.Fatalf("Report = %q, want hourly", req.Report)
|
||||||
}
|
}
|
||||||
if req.Config.WeatherAPI.Units != "us" {
|
if req.Config.WeatherAPI.Units != "us" {
|
||||||
t.Fatalf("Units = %q, want us", req.Config.WeatherAPI.Units)
|
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" {
|
if req.Config.WeatherAPI.Timezone != "America/Chicago" {
|
||||||
t.Fatalf("Timezone = %q, want America/Chicago", req.Config.WeatherAPI.Timezone)
|
t.Fatalf("Timezone = %q, want America/Chicago", req.Config.WeatherAPI.Timezone)
|
||||||
}
|
}
|
||||||
if req.OutputPath != "./near-term.md" {
|
if req.OutputPath != "./hourly.md" {
|
||||||
t.Fatalf("OutputPath = %q, want ./near-term.md", req.OutputPath)
|
t.Fatalf("OutputPath = %q, want ./hourly.md", req.OutputPath)
|
||||||
}
|
}
|
||||||
if !req.Date.IsZero() || !req.StormStart.IsZero() || !req.StormEnd.IsZero() {
|
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()}
|
runner := Runner{Clock: fixedClock()}
|
||||||
|
|
||||||
for _, args := range [][]string{
|
for _, args := range [][]string{
|
||||||
{"near-term", "--date", "2026-05-29"},
|
{"hourly", "--date", "2026-05-29"},
|
||||||
{"near-term", "--start", "2026-05-29T18:00"},
|
{"hourly", "--start", "2026-05-29T18:00"},
|
||||||
{"near-term", "--end", "2026-05-29T20:00"},
|
{"hourly", "--end", "2026-05-29T20:00"},
|
||||||
} {
|
} {
|
||||||
_, err := runner.resolveGenerate(args)
|
_, err := runner.resolveGenerate(args)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
|
|||||||
@@ -161,19 +161,10 @@ reports:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestLoadNearTermReportModuleOverrides(t *testing.T) {
|
func TestLoadHourlyReportModuleOverrides(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, `
|
path := writeConfig(t, `
|
||||||
reports:
|
reports:
|
||||||
`+tt.key+`:
|
hourly:
|
||||||
deterministic_modules:
|
deterministic_modules:
|
||||||
- metadata
|
- metadata
|
||||||
- hourly_forecast
|
- hourly_forecast
|
||||||
@@ -186,17 +177,15 @@ reports:
|
|||||||
t.Fatalf("LoadFile() error = %v", err)
|
t.Fatalf("LoadFile() error = %v", err)
|
||||||
}
|
}
|
||||||
overrides := cfg.ReportModuleOverrides()
|
overrides := cfg.ReportModuleOverrides()
|
||||||
items := overrides[report.NearTerm]
|
items := overrides[report.Hourly]
|
||||||
if len(items) != 4 {
|
if len(items) != 4 {
|
||||||
t.Fatalf("near-term override length = %d, want 4", len(items))
|
t.Fatalf("hourly override length = %d, want 4", len(items))
|
||||||
}
|
}
|
||||||
if items[0].ID != module.Metadata ||
|
if items[0].ID != module.Metadata ||
|
||||||
items[1].ID != module.HourlyForecast ||
|
items[1].ID != module.HourlyForecast ||
|
||||||
items[2].ID != module.PrecipTiming ||
|
items[2].ID != module.PrecipTiming ||
|
||||||
items[3].ID != module.AlertDigest {
|
items[3].ID != module.AlertDigest {
|
||||||
t.Fatalf("near-term override = %#v, want configured module order", items)
|
t.Fatalf("hourly override = %#v, want configured module order", items)
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -284,27 +273,14 @@ reports:
|
|||||||
wantErr: "duplicates report override",
|
wantErr: "duplicates report override",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "DuplicateNearTermAlias",
|
name: "HourlyIncompatibleDailyModule",
|
||||||
yaml: `
|
yaml: `
|
||||||
reports:
|
reports:
|
||||||
near_term:
|
hourly:
|
||||||
deterministic_modules:
|
|
||||||
- metadata
|
|
||||||
near-term:
|
|
||||||
deterministic_modules:
|
|
||||||
- current_conditions
|
|
||||||
`,
|
|
||||||
wantErr: "duplicates report override",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "NearTermIncompatibleDailyModule",
|
|
||||||
yaml: `
|
|
||||||
reports:
|
|
||||||
near_term:
|
|
||||||
deterministic_modules:
|
deterministic_modules:
|
||||||
- derived_daily_summary
|
- derived_daily_summary
|
||||||
`,
|
`,
|
||||||
wantErr: `not compatible with report "near_term"`,
|
wantErr: `not compatible with report "hourly"`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "UnknownReportField",
|
name: "UnknownReportField",
|
||||||
|
|||||||
@@ -113,8 +113,8 @@ func reportIDForConfigKey(key string) (report.ID, error) {
|
|||||||
return report.DailyToday, nil
|
return report.DailyToday, nil
|
||||||
case "tomorrow", "daily_tomorrow":
|
case "tomorrow", "daily_tomorrow":
|
||||||
return report.DailyTomorrow, nil
|
return report.DailyTomorrow, nil
|
||||||
case "near_term":
|
case "hourly":
|
||||||
return report.NearTerm, nil
|
return report.Hourly, nil
|
||||||
case "three_day", "three_day_outlook":
|
case "three_day", "three_day_outlook":
|
||||||
return report.ThreeDay, nil
|
return report.ThreeDay, nil
|
||||||
case "weekend", "weekend_outlook":
|
case "weekend", "weekend_outlook":
|
||||||
|
|||||||
@@ -114,7 +114,7 @@ func BuildDerived(req BuildDerivedRequest) (DerivedFacts, error) {
|
|||||||
derived.PrecipTiming = forecast.BuildPrecipTiming(derived.ValidPeriodHourlyPeriods)
|
derived.PrecipTiming = forecast.BuildPrecipTiming(derived.ValidPeriodHourlyPeriods)
|
||||||
|
|
||||||
switch req.Resolved.Definition.ID {
|
switch req.Resolved.Definition.ID {
|
||||||
case report.NearTerm:
|
case report.Hourly:
|
||||||
case report.DailyToday, report.DailyTomorrow:
|
case report.DailyToday, report.DailyTomorrow:
|
||||||
summary, err := forecast.BuildDailySummary(bundle, period.Start, location, req.Dayparts)
|
summary, err := forecast.BuildDailySummary(bundle, period.Start, location, req.Dayparts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -152,9 +152,9 @@ func TestBuildDerivedWeekendAndTomorrow(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestBuildDerivedNearTermUsesRollingWindowFacts(t *testing.T) {
|
func TestBuildDerivedHourlyUsesRollingWindowFacts(t *testing.T) {
|
||||||
location := testLocation()
|
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 := testBundle(location)
|
||||||
bundle.Alerts = &weatherdata.AlertRun{Alerts: []json.RawMessage{
|
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"}`),
|
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))
|
t.Fatalf("ValidPeriodNarrativePeriods length = %d, want overlapping narrative period", len(derived.ValidPeriodNarrativePeriods))
|
||||||
}
|
}
|
||||||
if len(derived.DailySummaries) != 0 || len(derived.DaypartSummaries) != 0 || derived.StormWindowSummary != nil {
|
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" {
|
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)
|
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)
|
t.Fatalf("PrecipTiming.LastPrecipitation = %#v, want final selected rainy window end", derived.PrecipTiming.LastPrecipitation)
|
||||||
}
|
}
|
||||||
if len(derived.PrecipTiming.PrecipitationWindows) != 2 {
|
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" {
|
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" {
|
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, ",") {
|
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)
|
t.Fatalf("SPCConvectiveOutlooks IDs = %#v, want %#v", got, want)
|
||||||
@@ -306,7 +306,7 @@ func TestBuildDerivedSelectsSPCConvectiveOutlooksByValidPeriod(t *testing.T) {
|
|||||||
|
|
||||||
func TestBuildDerivedUnsupportedReportReturnsActionableError(t *testing.T) {
|
func TestBuildDerivedUnsupportedReportReturnsActionableError(t *testing.T) {
|
||||||
location := testLocation()
|
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")
|
resolved.Definition.ID = report.ID("future_report")
|
||||||
|
|
||||||
_, err := BuildDerived(BuildDerivedRequest{
|
_, err := BuildDerived(BuildDerivedRequest{
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ type ID string
|
|||||||
const (
|
const (
|
||||||
DailyToday ID = "daily_today"
|
DailyToday ID = "daily_today"
|
||||||
DailyTomorrow ID = "daily_tomorrow"
|
DailyTomorrow ID = "daily_tomorrow"
|
||||||
NearTerm ID = "near_term"
|
Hourly ID = "hourly"
|
||||||
ThreeDay ID = "three_day"
|
ThreeDay ID = "three_day"
|
||||||
Weekend ID = "weekend"
|
Weekend ID = "weekend"
|
||||||
Storm ID = "storm"
|
Storm ID = "storm"
|
||||||
|
|||||||
@@ -7,24 +7,24 @@ import (
|
|||||||
"gitea.maximumdirect.net/eric/weatherreporter/internal/timeutil"
|
"gitea.maximumdirect.net/eric/weatherreporter/internal/timeutil"
|
||||||
)
|
)
|
||||||
|
|
||||||
const nearTermHours = 6
|
const hourlyHours = 6
|
||||||
|
|
||||||
func nearTermDefinition() Definition {
|
func hourlyDefinition() Definition {
|
||||||
return Definition{
|
return Definition{
|
||||||
ID: NearTerm,
|
ID: Hourly,
|
||||||
Name: "Near-Term Report",
|
Name: "Hourly Report",
|
||||||
PromptID: "weather.near_term_report",
|
PromptID: "weather.hourly_report",
|
||||||
ComparisonStrategy: CompareRollingWindow,
|
ComparisonStrategy: CompareRollingWindow,
|
||||||
ArtifactGroup: "near-term",
|
ArtifactGroup: "hourly",
|
||||||
BatchOutputName: "near-term.md",
|
BatchOutputName: "hourly.md",
|
||||||
Generated: true,
|
Generated: true,
|
||||||
CompatiblePriorIDs: []ID{NearTerm},
|
CompatiblePriorIDs: []ID{Hourly},
|
||||||
Modules: nearTermModules(),
|
Modules: hourlyModules(),
|
||||||
resolve: resolveNearTerm,
|
resolve: resolveHourly,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func nearTermModules() []module.ConfigItem {
|
func hourlyModules() []module.ConfigItem {
|
||||||
return []module.ConfigItem{
|
return []module.ConfigItem{
|
||||||
{ID: module.Metadata},
|
{ID: module.Metadata},
|
||||||
{ID: module.CurrentConditions},
|
{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)
|
localNow := req.Now.In(req.Location)
|
||||||
return timeutil.Period{
|
return timeutil.Period{
|
||||||
Start: localNow,
|
Start: localNow,
|
||||||
End: localNow.Add(nearTermHours * time.Hour),
|
End: localNow.Add(hourlyHours * time.Hour),
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
@@ -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")
|
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)
|
location := mustLoadLocation(t)
|
||||||
now := mustParse("2026-05-29T05:15:00-05:00")
|
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 {
|
if err != nil {
|
||||||
t.Fatalf("Resolve() error = %v", err)
|
t.Fatalf("Resolve() error = %v", err)
|
||||||
}
|
}
|
||||||
if resolved.Definition.ID != NearTerm {
|
if resolved.Definition.ID != Hourly {
|
||||||
t.Fatalf("ID = %q, want near_term", resolved.Definition.ID)
|
t.Fatalf("ID = %q, want hourly", resolved.Definition.ID)
|
||||||
}
|
}
|
||||||
if resolved.Definition.PromptID != "weather.near_term_report" {
|
if resolved.Definition.PromptID != "weather.hourly_report" {
|
||||||
t.Fatalf("PromptID = %q, want weather.near_term_report", resolved.Definition.PromptID)
|
t.Fatalf("PromptID = %q, want weather.hourly_report", resolved.Definition.PromptID)
|
||||||
}
|
}
|
||||||
if resolved.Definition.ComparisonStrategy != CompareRollingWindow {
|
if resolved.Definition.ComparisonStrategy != CompareRollingWindow {
|
||||||
t.Fatalf("ComparisonStrategy = %q, want rolling_window", resolved.Definition.ComparisonStrategy)
|
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")
|
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")
|
location, err := time.LoadLocation("America/New_York")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("load location: %v", err)
|
t.Fatalf("load location: %v", err)
|
||||||
}
|
}
|
||||||
now := mustParse("2026-05-29T10:15:00Z")
|
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 {
|
if err != nil {
|
||||||
t.Fatalf("Resolve() error = %v", err)
|
t.Fatalf("Resolve() error = %v", err)
|
||||||
}
|
}
|
||||||
assertPeriod(t, resolved.ValidPeriod, "2026-05-29T06:15:00-04:00", "2026-05-29T12:15:00-04:00")
|
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)
|
location := mustLoadLocation(t)
|
||||||
now := mustParse("2026-05-29T22:30:00-05:00")
|
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 {
|
if err != nil {
|
||||||
t.Fatalf("Resolve() error = %v", err)
|
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)
|
location := mustLoadLocation(t)
|
||||||
req := ResolveRequest{Now: mustParse("2026-05-29T06:00:00-05:00"), Location: location}
|
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)
|
t.Fatalf("BatchReports(evening) error = %v", err)
|
||||||
}
|
}
|
||||||
for _, resolved := range append(morning, evening...) {
|
for _, resolved := range append(morning, evening...) {
|
||||||
if resolved.Definition.ID == NearTerm {
|
if resolved.Definition.ID == Hourly {
|
||||||
t.Fatalf("batch included %q, want near-term excluded", resolved.Definition.ID)
|
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())
|
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, ",") {
|
if strings.Join(ids, ",") != strings.Join(want, ",") {
|
||||||
t.Fatalf("All() ids = %#v, want %#v", ids, want)
|
t.Fatalf("All() ids = %#v, want %#v", ids, want)
|
||||||
}
|
}
|
||||||
@@ -277,11 +277,11 @@ func TestRegistryDefinitionsDeclarePathAndCompatibilityPolicy(t *testing.T) {
|
|||||||
comparisonStrategy: CompareSameValidDate,
|
comparisonStrategy: CompareSameValidDate,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: NearTerm,
|
id: Hourly,
|
||||||
artifactGroup: "near-term",
|
artifactGroup: "hourly",
|
||||||
batchOutputName: "near-term.md",
|
batchOutputName: "hourly.md",
|
||||||
generated: true,
|
generated: true,
|
||||||
compatiblePriorIDs: []ID{NearTerm},
|
compatiblePriorIDs: []ID{Hourly},
|
||||||
comparisonStrategy: CompareRollingWindow,
|
comparisonStrategy: CompareRollingWindow,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -384,7 +384,7 @@ func TestRegistryDefinitionsDeclareDefaultModules(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: NearTerm,
|
id: Hourly,
|
||||||
want: []module.ID{
|
want: []module.ID{
|
||||||
module.Metadata,
|
module.Metadata,
|
||||||
module.CurrentConditions,
|
module.CurrentConditions,
|
||||||
@@ -515,20 +515,20 @@ func TestResolvedMetadata(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestNearTermMetadataRunIDIncludesReportID(t *testing.T) {
|
func TestHourlyMetadataRunIDIncludesReportID(t *testing.T) {
|
||||||
location := mustLoadLocation(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 {
|
if err != nil {
|
||||||
t.Fatalf("Resolve() error = %v", err)
|
t.Fatalf("Resolve() error = %v", err)
|
||||||
}
|
}
|
||||||
metadata := resolved.Metadata()
|
metadata := resolved.Metadata()
|
||||||
if metadata.ReportID != NearTerm {
|
if metadata.ReportID != Hourly {
|
||||||
t.Fatalf("ReportID = %q, want near_term", metadata.ReportID)
|
t.Fatalf("ReportID = %q, want hourly", metadata.ReportID)
|
||||||
}
|
}
|
||||||
if metadata.PromptID != "weather.near_term_report" {
|
if metadata.PromptID != "weather.hourly_report" {
|
||||||
t.Fatalf("PromptID = %q, want weather.near_term_report", metadata.PromptID)
|
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)
|
t.Fatalf("RunID = %q, want report id", metadata.RunID)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ func DefaultRegistry() Registry {
|
|||||||
definitions := []Definition{
|
definitions := []Definition{
|
||||||
dailyTodayDefinition(),
|
dailyTodayDefinition(),
|
||||||
dailyTomorrowDefinition(),
|
dailyTomorrowDefinition(),
|
||||||
nearTermDefinition(),
|
hourlyDefinition(),
|
||||||
threeDayDefinition(),
|
threeDayDefinition(),
|
||||||
weekendDefinition(),
|
weekendDefinition(),
|
||||||
stormDefinition(),
|
stormDefinition(),
|
||||||
@@ -74,7 +74,7 @@ func (r Registry) MustLookup(id ID) Definition {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (r Registry) All() []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))
|
out := make([]Definition, 0, len(ids))
|
||||||
for _, id := range ids {
|
for _, id := range ids {
|
||||||
if definition, ok := r.definitions[id]; ok {
|
if definition, ok := r.definitions[id]; ok {
|
||||||
|
|||||||
@@ -307,8 +307,8 @@ func TestFindPriorSnapshotSupportsNarrowedWeekendPeriod(t *testing.T) {
|
|||||||
|
|
||||||
func TestFindPriorSnapshotIgnoresRollingWindowReports(t *testing.T) {
|
func TestFindPriorSnapshotIgnoresRollingWindowReports(t *testing.T) {
|
||||||
store := newTestStore(t)
|
store := newTestStore(t)
|
||||||
first := resolveNearTermAt(t, "2026-05-29T05:00:00-05:00")
|
first := resolveHourlyAt(t, "2026-05-29T05:00:00-05:00")
|
||||||
second := resolveNearTermAt(t, "2026-05-29T06:00:00-05:00")
|
second := resolveHourlyAt(t, "2026-05-29T06:00:00-05:00")
|
||||||
savePriorMetadata(t, store, first, stateBriefingMetadata(first))
|
savePriorMetadata(t, store, first, stateBriefingMetadata(first))
|
||||||
|
|
||||||
prior, err := store.FindPriorSnapshot(context.Background(), second)
|
prior, err := store.FindPriorSnapshot(context.Background(), second)
|
||||||
@@ -405,7 +405,7 @@ func resolveWeekendAt(t *testing.T, value string) report.Resolved {
|
|||||||
return resolved
|
return resolved
|
||||||
}
|
}
|
||||||
|
|
||||||
func resolveNearTermAt(t *testing.T, value string) report.Resolved {
|
func resolveHourlyAt(t *testing.T, value string) report.Resolved {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
location, err := timeutil.LoadLocation("America/Chicago")
|
location, err := timeutil.LoadLocation("America/Chicago")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -415,7 +415,7 @@ func resolveNearTermAt(t *testing.T, value string) report.Resolved {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("parse time: %v", err)
|
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,
|
Now: now,
|
||||||
Location: location,
|
Location: location,
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user