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

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

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

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) {
path := filepath.Join(t.TempDir(), "config.yml")
if err := os.WriteFile(path, []byte(`

View File

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

View File

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