From 696454cf34dc9c92d4bb21583d689d84eb17b7df Mon Sep 17 00:00:00 2001 From: Eric Rakestraw Date: Mon, 15 Jun 2026 16:46:10 +0000 Subject: [PATCH] Require explicit dates for daily generation --- docs/cli.md | 23 +++++++++----------- docs/internal/state.md | 2 +- docs/operations.md | 33 ++++++++++++++--------------- internal/app/app_test.go | 16 ++++++++++++++ internal/cli/root.go | 12 +++++++++-- internal/cli/root_test.go | 44 ++++++++++++++++++++++++++++++++++++--- 6 files changed, 94 insertions(+), 36 deletions(-) diff --git a/docs/cli.md b/docs/cli.md index 7d8266b..baccc56 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -20,7 +20,7 @@ Markdown report after final metadata is saved. ```text 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 tomorrow [--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, preflight artifact, managed Markdown report, and metadata under the configured workspace. `--out` writes an extra Markdown copy for the operator; distributor -notification uses the managed report path, not the extra copy. `generate today`, -`generate tomorrow`, and `generate hourly` write managed generated-text artifacts, -validate structured text from Scriptorium, and render the managed Markdown -report from embedded templates. `generate hourly` covers the next six hours in -the effective report timezone and does not accept date or event window flags. -`generate storm` requires explicit event-window bounds with `--start` and -`--end`. - -`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. +notification uses the managed report path, not the extra copy. `generate daily`, +`generate today`, `generate tomorrow`, and `generate hourly` write managed +generated-text artifacts, validate structured text from Scriptorium, and render +the managed Markdown report from embedded templates. `generate daily` requires +`--date YYYY-MM-DD` for the selected local civil day. `generate hourly` covers +the next six hours in the effective report timezone and does not accept date or +event window flags. `generate storm` requires explicit event-window bounds with +`--start` and `--end`. `run morning` generates Today Report and the 3-Day Outlook, plus Weekend Outlook 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`. - `--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`. -- `--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`. - `--end TIME`: required end time for `generate storm`. - `--limit N`: maximum records for `inspect reports`; defaults to `20`, and `0` means no limit. diff --git a/docs/internal/state.md b/docs/internal/state.md index e54596f..9f6b550 100644 --- a/docs/internal/state.md +++ b/docs/internal/state.md @@ -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 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. - Today Report compares with prior Today Report snapshots for the same valid local date. diff --git a/docs/operations.md b/docs/operations.md index ba1893a..f6db08a 100644 --- a/docs/operations.md +++ b/docs/operations.md @@ -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 managed Markdown report path. -`generate today`, `generate tomorrow`, and `generate hourly` use the -generated-text-template workflow. They run structured `scriptorium run` to raw -GeneratedText JSON, validate the structured text, save a render context, and -render the managed Markdown report from embedded templates. `generate today` -covers the selected or current local civil day. `generate hourly` covers the -six-hour rolling period from generation time in the effective report timezone -and is not part of scheduled morning or evening batches. - -`generate daily` remains the existing Daily Today report and is independent of -`generate today`. +`generate daily`, `generate today`, `generate tomorrow`, and `generate hourly` +use the generated-text-template workflow. They run structured `scriptorium run` +to raw GeneratedText JSON, validate the structured text, save a render context, +and render the managed Markdown report from embedded templates. `generate +daily` requires `--date YYYY-MM-DD` for the selected local civil day. +`generate today` covers the selected or current local civil day. `generate +hourly` covers the six-hour rolling period from generation time in the +effective report timezone and is not part of scheduled morning or evening +batches. When distributor notification is enabled, weatherreporter uploads the managed 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 Markdown or YAML text. -Daily Today compares with prior Daily Today snapshots for the same valid local -date. Today Report compares with prior Today Report snapshots for the same -valid local date. Tomorrow Report compares with prior Tomorrow Report snapshots -for the same valid local date. 3-Day Outlook compares with prior compatible -3-Day snapshots for the same valid local date. Weekend Outlook compares with -prior compatible Weekend snapshots for the same weekend window. Hourly Report -and Storm Report leave Recent Changes empty. +Daily Report compares with prior Daily Report snapshots for the same valid +local date. Today Report compares with prior Today Report snapshots for the +same valid local date. Tomorrow Report compares with prior Tomorrow Report +snapshots for the same valid local date. 3-Day Outlook compares with prior +compatible 3-Day snapshots for the same valid local date. Weekend Outlook +compares with prior compatible Weekend snapshots for the same weekend window. +Hourly Report and Storm Report leave Recent Changes empty. When no prior comparable snapshot exists, or no configured threshold is crossed, `recentChanges.items` is empty. diff --git a/internal/app/app_test.go b/internal/app/app_test.go index fcbd11e..c2f3403 100644 --- a/internal/app/app_test.go +++ b/internal/app/app_test.go @@ -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) { path := filepath.Join(t.TempDir(), "config.yml") if err := os.WriteFile(path, []byte(` diff --git a/internal/cli/root.go b/internal/cli/root.go index 7125bdd..f3852fc 100644 --- a/internal/cli/root.go +++ b/internal/cli/root.go @@ -17,7 +17,7 @@ const helpText = `weatherreporter prepares weather reports from normalized forec Usage: 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 tomorrow [--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 { - 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 == "" { req.Date = timeutil.LocalDate(r.Clock.Now(), location) } else { diff --git a/internal/cli/root_test.go b/internal/cli/root_test.go index 106cdef..c130ea0 100644 --- a/internal/cli/root_test.go +++ b/internal/cli/root_test.go @@ -23,7 +23,7 @@ func TestRunHelpLongFlag(t *testing.T) { 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) } if !strings.Contains(output.stdout, "generate today") { @@ -571,6 +571,14 @@ func TestRunGenerateDailyWritesMarkdownReport(t *testing.T) { if len(reportMatches) != 1 { 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) { @@ -975,14 +983,44 @@ func TestResolveGenerateHourlyRejectsDateAndStormBounds(t *testing.T) { } } -func TestResolveGenerateDailyDefaultsDateInConfiguredTimezone(t *testing.T) { +func TestResolveGenerateDailyRequiresDate(t *testing.T) { runner := Runner{Clock: fixedClock()} 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 { 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" { t.Fatalf("Date = %s, want 2026-05-29", got) } @@ -1024,7 +1062,7 @@ func TestResolveGenerateTodayDate(t *testing.T) { func TestResolveGenerateAppliesSharedFlags(t *testing.T) { 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 { t.Fatalf("resolveGenerate() error = %v", err) }