Require explicit dates for daily generation

This commit is contained in:
2026-06-15 16:46:10 +00:00
parent 8c97788682
commit 696454cf34
6 changed files with 94 additions and 36 deletions

View File

@@ -20,7 +20,7 @@ Markdown report after final metadata is saved.
```text ```text
weatherreporter --help weatherreporter --help
weatherreporter generate daily [--config PATH] [--units VALUE] [--tz NAME] [--out PATH] [--date YYYY-MM-DD] weatherreporter generate daily --date YYYY-MM-DD [--config PATH] [--units VALUE] [--tz NAME] [--out PATH]
weatherreporter generate today [--config PATH] [--units VALUE] [--tz NAME] [--out PATH] [--date YYYY-MM-DD] weatherreporter generate today [--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 hourly [--config PATH] [--units VALUE] [--tz NAME] [--out PATH] weatherreporter generate hourly [--config PATH] [--units VALUE] [--tz NAME] [--out PATH]
@@ -40,17 +40,14 @@ weatherreporter inspect sources [--config PATH] RUN_ID
Implemented `generate` commands write a JSON module snapshot, YAML data package, Implemented `generate` commands write a JSON module snapshot, YAML data package,
preflight artifact, managed Markdown report, and metadata under the configured preflight artifact, managed Markdown report, and metadata under the configured
workspace. `--out` writes an extra Markdown copy for the operator; distributor workspace. `--out` writes an extra Markdown copy for the operator; distributor
notification uses the managed report path, not the extra copy. `generate today`, notification uses the managed report path, not the extra copy. `generate daily`,
`generate tomorrow`, and `generate hourly` write managed generated-text artifacts, `generate today`, `generate tomorrow`, and `generate hourly` write managed
validate structured text from Scriptorium, and render the managed Markdown generated-text artifacts, validate structured text from Scriptorium, and render
report from embedded templates. `generate hourly` covers the next six hours in the managed Markdown report from embedded templates. `generate daily` requires
the effective report timezone and does not accept date or event window flags. `--date YYYY-MM-DD` for the selected local civil day. `generate hourly` covers
`generate storm` requires explicit event-window bounds with `--start` and the next six hours in the effective report timezone and does not accept date or
`--end`. event window flags. `generate storm` requires explicit event-window bounds with
`--start` and `--end`.
`generate daily` remains the existing Daily Today report. It is separate from
`generate today`; the commands use different report IDs, artifact groups,
prompts, and config override keys.
`run morning` generates Today Report and the 3-Day Outlook, plus Weekend Outlook `run morning` generates Today Report and the 3-Day Outlook, plus Weekend Outlook
except on Sunday. `run evening` generates the Tomorrow Report. Batch except on Sunday. `run evening` generates the Tomorrow Report. Batch
@@ -76,7 +73,7 @@ They do not fetch weather data or invoke `scriptorium`.
- `--tz NAME`: override configured Weather API timezone for `generate` and `run`. - `--tz NAME`: override configured Weather API timezone for `generate` and `run`.
- `--out PATH`: write an extra Markdown report copy where supported by the `generate` command. - `--out PATH`: write an extra Markdown report copy where supported by the `generate` command.
- `--out-dir PATH`: write extra Markdown report copies for `run morning` and `run evening`. - `--out-dir PATH`: write extra Markdown report copies for `run morning` and `run evening`.
- `--date YYYY-MM-DD`: optional date for `generate daily` and `generate today`; defaults to the current local date in the configured timezone. - `--date YYYY-MM-DD`: required date for `generate daily`; optional date for `generate today`, defaulting to the current local date in the configured timezone.
- `--start TIME`: required start time for `generate storm`. - `--start TIME`: required start time for `generate storm`.
- `--end TIME`: required end time for `generate storm`. - `--end TIME`: required end time for `generate storm`.
- `--limit N`: maximum records for `inspect reports`; defaults to `20`, and `0` means no limit. - `--limit N`: maximum records for `inspect reports`; defaults to `20`, and `0` means no limit.

View File

@@ -91,7 +91,7 @@ Prior snapshot lookup reads stored metadata through the shared lookup path and
selects the latest earlier snapshot whose report ID is compatible with the selects the latest earlier snapshot whose report ID is compatible with the
current report definition. current report definition.
- Daily Today compares with prior Daily Today snapshots for the same valid - Daily Report compares with prior Daily Report snapshots for the same valid
local date. local date.
- Today Report compares with prior Today Report snapshots for the same valid - Today Report compares with prior Today Report snapshots for the same valid
local date. local date.

View File

@@ -24,16 +24,15 @@ a JSON module snapshot, build a YAML prompt input data package, run
workspace. Markdown-path reports then run `scriptorium run` directly to the workspace. Markdown-path reports then run `scriptorium run` directly to the
managed Markdown report path. managed Markdown report path.
`generate today`, `generate tomorrow`, and `generate hourly` use the `generate daily`, `generate today`, `generate tomorrow`, and `generate hourly`
generated-text-template workflow. They run structured `scriptorium run` to raw use the generated-text-template workflow. They run structured `scriptorium run`
GeneratedText JSON, validate the structured text, save a render context, and to raw GeneratedText JSON, validate the structured text, save a render context,
render the managed Markdown report from embedded templates. `generate today` and render the managed Markdown report from embedded templates. `generate
covers the selected or current local civil day. `generate hourly` covers the daily` requires `--date YYYY-MM-DD` for the selected local civil day.
six-hour rolling period from generation time in the effective report timezone `generate today` covers the selected or current local civil day. `generate
and is not part of scheduled morning or evening batches. hourly` covers the six-hour rolling period from generation time in the
effective report timezone and is not part of scheduled morning or evening
`generate daily` remains the existing Daily Today report and is independent of batches.
`generate today`.
When distributor notification is enabled, weatherreporter uploads the managed When distributor notification is enabled, weatherreporter uploads the managed
Markdown report after report rendering succeeds and final metadata is saved. Markdown report after report rendering succeeds and final metadata is saved.
@@ -293,13 +292,13 @@ source provenance and warnings without dumping full weather payloads.
Recent Changes are computed from structured module snapshots, not rendered Recent Changes are computed from structured module snapshots, not rendered
Markdown or YAML text. Markdown or YAML text.
Daily Today compares with prior Daily Today snapshots for the same valid local Daily Report compares with prior Daily Report snapshots for the same valid
date. Today Report compares with prior Today Report snapshots for the same local date. Today Report compares with prior Today Report snapshots for the
valid local date. Tomorrow Report compares with prior Tomorrow Report snapshots same valid local date. Tomorrow Report compares with prior Tomorrow Report
for the same valid local date. 3-Day Outlook compares with prior compatible snapshots for the same valid local date. 3-Day Outlook compares with prior
3-Day snapshots for the same valid local date. Weekend Outlook compares with compatible 3-Day snapshots for the same valid local date. Weekend Outlook
prior compatible Weekend snapshots for the same weekend window. Hourly Report compares with prior compatible Weekend snapshots for the same weekend window.
and Storm Report leave Recent Changes empty. Hourly Report and 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,
`recentChanges.items` is empty. `recentChanges.items` is empty.

View File

@@ -2061,6 +2061,22 @@ func TestResolveGenerateMapsCommandToReportDefinition(t *testing.T) {
} }
} }
func TestResolveGenerateDailyRequiresDate(t *testing.T) {
cfg := config.Defaults()
cfg.WeatherAPI.Timezone = "America/Chicago"
_, err := ResolveGenerate(GenerateRequest{
Config: cfg,
Report: ReportDaily,
}, mustParse("2026-05-29T08:00:00-05:00"))
if err == nil {
t.Fatal("ResolveGenerate() error = nil, want required date error")
}
if !strings.Contains(err.Error(), "requires an explicit date") {
t.Fatalf("ResolveGenerate() error = %q, want required date context", err.Error())
}
}
func TestResolveGenerateUsesConfiguredReportModules(t *testing.T) { func TestResolveGenerateUsesConfiguredReportModules(t *testing.T) {
path := filepath.Join(t.TempDir(), "config.yml") path := filepath.Join(t.TempDir(), "config.yml")
if err := os.WriteFile(path, []byte(` if err := os.WriteFile(path, []byte(`

View File

@@ -17,7 +17,7 @@ const helpText = `weatherreporter prepares weather reports from normalized forec
Usage: Usage:
weatherreporter --help weatherreporter --help
weatherreporter generate daily [--config PATH] [--units VALUE] [--tz NAME] [--out PATH] [--date YYYY-MM-DD] weatherreporter generate daily --date YYYY-MM-DD [--config PATH] [--units VALUE] [--tz NAME] [--out PATH]
weatherreporter generate today [--config PATH] [--units VALUE] [--tz NAME] [--out PATH] [--date YYYY-MM-DD] weatherreporter generate today [--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 hourly [--config PATH] [--units VALUE] [--tz NAME] [--out PATH] weatherreporter generate hourly [--config PATH] [--units VALUE] [--tz NAME] [--out PATH]
@@ -217,7 +217,15 @@ func (r Runner) resolveGenerate(args []string) (app.GenerateRequest, error) {
} }
switch reportKind { switch reportKind {
case app.ReportDaily, app.ReportToday: case app.ReportDaily:
if opts.Date == "" {
return app.GenerateRequest{}, fmt.Errorf("generate daily requires --date YYYY-MM-DD")
}
req.Date, err = timeutil.ParseLocalDate(opts.Date, location)
if err != nil {
return app.GenerateRequest{}, err
}
case app.ReportToday:
if opts.Date == "" { if opts.Date == "" {
req.Date = timeutil.LocalDate(r.Clock.Now(), location) req.Date = timeutil.LocalDate(r.Clock.Now(), location)
} else { } else {

View File

@@ -23,7 +23,7 @@ func TestRunHelpLongFlag(t *testing.T) {
t.Fatalf("Run() error = %v", err) t.Fatalf("Run() error = %v", err)
} }
if !strings.Contains(output.stdout, "generate daily") { if !strings.Contains(output.stdout, "weatherreporter generate daily --date YYYY-MM-DD") {
t.Fatalf("help output missing generate command:\n%s", output.stdout) t.Fatalf("help output missing generate command:\n%s", output.stdout)
} }
if !strings.Contains(output.stdout, "generate today") { if !strings.Contains(output.stdout, "generate today") {
@@ -571,6 +571,14 @@ func TestRunGenerateDailyWritesMarkdownReport(t *testing.T) {
if len(reportMatches) != 1 { if len(reportMatches) != 1 {
t.Fatalf("managed reports = %#v, want one", reportMatches) t.Fatalf("managed reports = %#v, want one", reportMatches)
} }
rawGeneratedTextPath := oneArtifact(t, workspaceRoot, "snapshots", "daily", "2026-05-29", "*.generated_text.raw.json")
validatedGeneratedTextPath := oneArtifact(t, workspaceRoot, "snapshots", "daily", "2026-05-29", "*.generated_text.json")
renderContextPath := oneArtifact(t, workspaceRoot, "snapshots", "daily", "2026-05-29", "*.render_context.json")
metadataPath := oneArtifact(t, workspaceRoot, "snapshots", "daily", "2026-05-29", "*.metadata.json")
assertFileContains(t, rawGeneratedTextPath, `"summary": "Showers are possible during the selected day."`)
assertFileContains(t, validatedGeneratedTextPath, `"summary":"Showers are possible during the selected day."`)
assertFileContains(t, renderContextPath, `"Title": "Friday's Weather"`)
assertFileContains(t, metadataPath, `"generatedTextSchemaId": "daily"`)
} }
func TestRunGenerateTodayWritesGeneratedTextReport(t *testing.T) { func TestRunGenerateTodayWritesGeneratedTextReport(t *testing.T) {
@@ -975,14 +983,44 @@ func TestResolveGenerateHourlyRejectsDateAndStormBounds(t *testing.T) {
} }
} }
func TestResolveGenerateDailyDefaultsDateInConfiguredTimezone(t *testing.T) { func TestResolveGenerateDailyRequiresDate(t *testing.T) {
runner := Runner{Clock: fixedClock()} runner := Runner{Clock: fixedClock()}
req, err := runner.resolveGenerate([]string{"daily"}) req, err := runner.resolveGenerate([]string{"daily"})
if err == nil {
t.Fatal("resolveGenerate() error = nil, want required date error")
}
if !strings.Contains(err.Error(), "generate daily requires --date YYYY-MM-DD") {
t.Fatalf("resolveGenerate() error = %q, want required date context", err.Error())
}
if !req.Date.IsZero() {
t.Fatalf("Date = %s, want unset on error", req.Date)
}
}
func TestResolveGenerateDailyRejectsMalformedDate(t *testing.T) {
runner := Runner{Clock: fixedClock()}
_, err := runner.resolveGenerate([]string{"daily", "--date", "bad-date"})
if err == nil {
t.Fatal("resolveGenerate() error = nil, want date parse error")
}
if !strings.Contains(err.Error(), `parse date "bad-date" as YYYY-MM-DD`) {
t.Fatalf("resolveGenerate() error = %q, want date parse context", err.Error())
}
}
func TestResolveGenerateDailyParsesDate(t *testing.T) {
runner := Runner{Clock: fixedClock()}
req, err := runner.resolveGenerate([]string{"daily", "--date", "2026-05-29"})
if err != nil { if err != nil {
t.Fatalf("resolveGenerate() error = %v", err) t.Fatalf("resolveGenerate() error = %v", err)
} }
if req.Report != app.ReportDaily {
t.Fatalf("Report = %q, want daily", req.Report)
}
if got := req.Date.Format(timeutil.DateLayout); got != "2026-05-29" { if got := req.Date.Format(timeutil.DateLayout); got != "2026-05-29" {
t.Fatalf("Date = %s, want 2026-05-29", got) t.Fatalf("Date = %s, want 2026-05-29", got)
} }
@@ -1024,7 +1062,7 @@ func TestResolveGenerateTodayDate(t *testing.T) {
func TestResolveGenerateAppliesSharedFlags(t *testing.T) { func TestResolveGenerateAppliesSharedFlags(t *testing.T) {
runner := Runner{Clock: fixedClock()} runner := Runner{Clock: fixedClock()}
req, err := runner.resolveGenerate([]string{"daily", "--units", "metric", "--tz", "UTC", "--out", "./daily.md"}) req, err := runner.resolveGenerate([]string{"daily", "--date", "2026-05-29", "--units", "metric", "--tz", "UTC", "--out", "./daily.md"})
if err != nil { if err != nil {
t.Fatalf("resolveGenerate() error = %v", err) t.Fatalf("resolveGenerate() error = %v", err)
} }