Replace legacy daily report with generated text daily report

This commit is contained in:
2026-06-15 16:43:06 +00:00
parent 5203440ba0
commit 8c97788682
28 changed files with 326 additions and 194 deletions

View File

@@ -110,7 +110,7 @@ weatherreporter inspect modules 20260529T100000.000000000Z_today
weatherreporter inspect data-package 20260529T100000.000000000Z_today weatherreporter inspect data-package 20260529T100000.000000000Z_today
weatherreporter inspect prior 20260529T100000.000000000Z_today weatherreporter inspect prior 20260529T100000.000000000Z_today
weatherreporter inspect sources 20260529T100000.000000000Z_today weatherreporter inspect sources 20260529T100000.000000000Z_today
weatherreporter inspect metadata 20260529T100000.000000000Z_daily_today weatherreporter inspect metadata 20260529T100000.000000000Z_daily
``` ```
`inspect reports` lists recent generated runs with artifact paths and source `inspect reports` lists recent generated runs with artifact paths and source

View File

@@ -183,12 +183,12 @@ report definitions. Omit a report entry to use its default module order.
Supported report keys are `daily`, `today`, `tomorrow`, `hourly`, Supported report keys are `daily`, `today`, `tomorrow`, `hourly`,
`three_day`, `weekend`, and `storm`. Canonical report IDs and accepted aliases `three_day`, `weekend`, and `storm`. Canonical report IDs and accepted aliases
are also valid, including `daily_today`, `three_day_outlook`, are also valid, including `three_day_outlook`, `weekend_outlook`, and
`weekend_outlook`, and `storm_report`. Hyphens and underscores are treated `storm_report`. Hyphens and underscores are treated equivalently in report
equivalently in report keys. keys. `daily_today` is not a supported report key.
`reports.today` applies only to the Today Report. `reports.daily` and `reports.today` applies only to the Today Report. `reports.daily` applies only
`reports.daily_today` apply only to the existing Daily Today report. to the dated Daily Report.
Each report entry supports: Each report entry supports:

View File

@@ -39,6 +39,10 @@ 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.
Daily Report supports the Daily-style civil-day modules plus `daily_planning`
and `hourly_forecast`; those outputs feed the dated Daily GeneratedText prompt
package and embedded Markdown template.
Tomorrow Report supports the Daily-style civil-day modules plus Tomorrow Report supports the Daily-style civil-day modules plus
`tomorrow_planning` and `hourly_forecast`; those outputs feed the Tomorrow `tomorrow_planning` and `hourly_forecast`; those outputs feed the Tomorrow
GeneratedText prompt package and embedded Markdown template. GeneratedText prompt package and embedded Markdown template.
@@ -53,8 +57,8 @@ late-day change-watch fields. It is compatible with `report.Today` only.
`daily_planning` is a dated Daily deterministic planning stanza with morning `daily_planning` is a dated Daily deterministic planning stanza with morning
readiness, commute/school/workday concerns, and overnight change-watch fields. readiness, commute/school/workday concerns, and overnight change-watch fields.
It is compatible only with the `daily` report ID value. No active default It is compatible only with the `daily` report ID value. The default Daily
report composition includes it yet. Report composition includes it.
Hourly 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`,

View File

@@ -49,6 +49,28 @@ 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.
## Daily Composition
The default Daily Report module order is:
1. `metadata`
2. `current_conditions`
3. `narrative_forecast`
4. `derived_daily_summary`
5. `derived_daypart_summaries`
6. `precip_timing`
7. `alert_digest`
8. `spc_convective_outlooks`
9. `area_forecast_discussion`
10. `spc_convective_discussion`
11. `weather_story`
12. `outdoor_windows`
13. `daily_planning`
14. `hourly_forecast`
The embedded Daily template uses selected deterministic fields from these
module outputs after GeneratedText validation.
## Today Composition ## Today Composition
The default Today Report module order is: The default Today Report module order is:
@@ -98,7 +120,7 @@ module outputs after GeneratedText validation.
`daily_planning` emits dated daily planning facts for the `daily` report ID. `daily_planning` emits dated daily planning facts for the `daily` report ID.
Its output stanza is also named `daily_planning`. The module is supported only Its output stanza is also named `daily_planning`. The module is supported only
by that report ID and depends on daily summaries for the selected local civil by that report ID and depends on daily summaries for the selected local civil
day. No active default report composition includes it yet. day. The default Daily Report composition includes it.
The output uses this shape: The output uses this shape:

View File

@@ -68,9 +68,9 @@ 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.
Today Report, Tomorrow Report, and Hourly Report module snapshots use the same Daily Report, Today Report, Tomorrow Report, and Hourly Report module snapshots
package schema and categories when converted into prompt input. The default use the same package schema and categories when converted into prompt input.
hourly module list places The default hourly module list places
`precip_timing` under `derived_summaries`, alert and SPC outlooks under `precip_timing` under `derived_summaries`, alert and SPC outlooks under
`applicable_risk_products`, AFD/SPC discussion/weather story under `applicable_risk_products`, AFD/SPC discussion/weather story under
`narrative_products`, and current/hourly data under `raw_data`. It does not `narrative_products`, and current/hourly data under `raw_data`. It does not
@@ -78,10 +78,11 @@ include civil-day summary stanzas. Generated-text and render context artifacts
are produced later in app orchestration and are not part of the YAML data are produced later in app orchestration and are not part of the YAML data
package. package.
The default Today and Tomorrow module lists include civil-day summary stanzas, The default Daily, Today, and Tomorrow module lists include civil-day summary
planning stanzas, and `hourly_forecast` in the data package before structured stanzas, planning stanzas, and `hourly_forecast` in the data package before
GeneratedText is requested from Scriptorium. Today uses `today_planning`; structured GeneratedText is requested from Scriptorium. Daily uses
Tomorrow uses `tomorrow_planning`. `daily_planning`, Today uses `today_planning`, and Tomorrow uses
`tomorrow_planning`.
Current categories are: Current categories are:

View File

@@ -29,25 +29,25 @@ Each report definition declares:
Report-owned helpers map public command names and config keys to report IDs. Report-owned helpers map public command names and config keys to report IDs.
The generate command names are `daily`, `today`, `tomorrow`, `hourly`, The generate command names are `daily`, `today`, `tomorrow`, `hourly`,
`three-day`, `weekend`, and `storm`. Config keys also accept underscore and `three-day`, `weekend`, and `storm`. Config keys also accept selected
legacy descriptive aliases such as `daily_today`, `three_day_outlook`, underscore and descriptive aliases such as `three_day_outlook`,
`weekend_outlook`, and `storm_report`. `weekend_outlook`, and `storm_report`.
`daily` resolves to the existing Daily Today report ID `daily_today`. `daily` resolves to the dated Daily Report ID `daily`. `today` resolves to the
`today` resolves to the independent Today report ID `today`. `reports.today` independent Today report ID `today`. `reports.today` is not an alias for
is not an alias for `reports.daily` or `reports.daily_today`. `reports.daily`, and `reports.daily_today` is not a supported config key.
Markdown report definitions use the `scriptorium_markdown` generation mode. Markdown report definitions use the `scriptorium_markdown` generation mode.
Their template and structured-text schema identifiers are empty. Today Report, Their template and structured-text schema identifiers are empty. Daily Report,
Tomorrow Report, and Hourly Report declare `generated_text_template`; the app Today Report, Tomorrow Report, and Hourly Report declare
uses their template and schema identifiers to validate generated text and render `generated_text_template`; the app uses their template and schema identifiers
embedded Markdown templates. to validate generated text and render embedded Markdown templates.
## Reports ## Reports
| Report | ID | Prompt | Generation mode | Artifact group | Batch copy | Prior compatibility | | Report | ID | Prompt | Generation mode | Artifact group | Batch copy | Prior compatibility |
| --- | --- | --- | --- | --- | --- | --- | | --- | --- | --- | --- | --- | --- | --- |
| Daily Today | `daily_today` | `weather.daily_report` | `scriptorium_markdown` | `daily` | `daily.md` | Daily Today | | Daily Report | `daily` | `weather.daily_generated_text` | `generated_text_template` | `daily` | `daily.md` | Daily Report |
| Today Report | `today` | `weather.today_generated_text` | `generated_text_template` | `today` | `today.md` | Today Report | | Today Report | `today` | `weather.today_generated_text` | `generated_text_template` | `today` | `today.md` | Today Report |
| Tomorrow Report | `tomorrow` | `weather.tomorrow_generated_text` | `generated_text_template` | `tomorrow` | `tomorrow.md` | Tomorrow Report | | Tomorrow Report | `tomorrow` | `weather.tomorrow_generated_text` | `generated_text_template` | `tomorrow` | `tomorrow.md` | Tomorrow Report |
| Hourly Report | `hourly` | `weather.hourly_generated_text` | `generated_text_template` | `hourly` | `hourly.md` | Hourly Report | | Hourly Report | `hourly` | `weather.hourly_generated_text` | `generated_text_template` | `hourly` | `hourly.md` | Hourly Report |
@@ -59,8 +59,8 @@ All report definitions are eligible for generation.
## Valid Periods ## Valid Periods
- Daily Today covers the selected local civil day, or the current local civil - Daily Report covers the selected local civil day and requires an explicit
day when no date override is supplied. date.
- Today Report covers the selected local civil day, or the current local civil - Today Report 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.
- Tomorrow Report covers the next local civil day from generation time. - Tomorrow Report covers the next local civil day from generation time.
@@ -91,14 +91,13 @@ report override keys.
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 short keys such as `today`, `tomorrow`, and `hourly`, module overrides can use short keys such as `daily`, `today`, `tomorrow`, and
canonical report IDs such as `daily_today`, or descriptive names such as `hourly`, or descriptive names such as `three_day_outlook`.
`three_day_outlook`.
## Batch Membership ## Batch Membership
Morning batches include Today Report, 3-Day Outlook, and Weekend Outlook Morning batches include Today Report, 3-Day Outlook, and Weekend Outlook
except on Sunday. Evening batches include Tomorrow Report. Daily Today and except on Sunday. Evening batches include Tomorrow Report. Daily Report and
Hourly Report are not part of a scheduled batch. Hourly Report are not part of a scheduled batch.
## State And App Usage ## State And App Usage

View File

@@ -196,7 +196,7 @@ period do not overwrite each other.
RunIDs are based on generation time plus report ID: RunIDs are based on generation time plus report ID:
```text ```text
20260529T100000.123456789Z_daily_today 20260529T100000.123456789Z_daily
20260529T100000.123456789Z_today 20260529T100000.123456789Z_today
``` ```

View File

@@ -1107,7 +1107,7 @@ func recentChanges(ctx context.Context, store state.Store, priorSnapshot *state.
PrecipTimingShiftMinutes: cfg.PrecipTimingShiftMinutes, PrecipTimingShiftMinutes: cfg.PrecipTimingShiftMinutes,
} }
switch reportID { switch reportID {
case report.DailyToday, report.Today, report.Tomorrow: case report.Daily, report.Today, report.Tomorrow:
return changes.CompareDaily(previous, current, thresholds) return changes.CompareDaily(previous, current, thresholds)
case report.ThreeDay: case report.ThreeDay:
return changes.CompareThreeDay(previous, current, thresholds) return changes.CompareThreeDay(previous, current, thresholds)

View File

@@ -105,6 +105,11 @@ func TestGenerateReportWritesReportAndPreflight(t *testing.T) {
ExitCode: 0, ExitCode: 0,
OutputPath: "", OutputPath: "",
}, },
structuredRunResult: &scriptorium.StructuredRunResult{
Command: []string{"scriptorium", "run"},
Stderr: "wrote generated text",
ExitCode: 0,
},
runBody: "# Daily Report\n\nRain this morning.\n", runBody: "# Daily Report\n\nRain this morning.\n",
} }
outputPath := filepath.Join(t.TempDir(), "daily.md") outputPath := filepath.Join(t.TempDir(), "daily.md")
@@ -128,25 +133,28 @@ func TestGenerateReportWritesReportAndPreflight(t *testing.T) {
if renderer.renderCalls != 1 { if renderer.renderCalls != 1 {
t.Fatalf("render calls = %d, want 1", renderer.renderCalls) t.Fatalf("render calls = %d, want 1", renderer.renderCalls)
} }
if renderer.runCalls != 1 { if renderer.structuredRunCalls != 1 {
t.Fatalf("run calls = %d, want 1", renderer.runCalls) t.Fatalf("structured run calls = %d, want 1", renderer.structuredRunCalls)
} }
if renderer.renderRequest.PromptID != "weather.daily_report" { if renderer.runCalls != 0 {
t.Fatalf("render PromptID = %q, want weather.daily_report", renderer.renderRequest.PromptID) t.Fatalf("markdown run calls = %d, want none", renderer.runCalls)
}
if renderer.renderRequest.PromptID != "weather.daily_generated_text" {
t.Fatalf("render PromptID = %q, want weather.daily_generated_text", renderer.renderRequest.PromptID)
} }
if renderer.renderRequest.DataPackagePath != result.DataPackagePath { if renderer.renderRequest.DataPackagePath != result.DataPackagePath {
t.Fatalf("render DataPackagePath = %q, want managed path %q", renderer.renderRequest.DataPackagePath, result.DataPackagePath) t.Fatalf("render DataPackagePath = %q, want managed path %q", renderer.renderRequest.DataPackagePath, result.DataPackagePath)
} }
if renderer.runRequest.DataPackagePath != result.DataPackagePath { if renderer.structuredRunRequest.DataPackagePath != result.DataPackagePath {
t.Fatalf("run DataPackagePath = %q, want managed path %q", renderer.runRequest.DataPackagePath, result.DataPackagePath) t.Fatalf("structured run DataPackagePath = %q, want managed path %q", renderer.structuredRunRequest.DataPackagePath, result.DataPackagePath)
} }
if renderer.runRequest.OutputPath != result.ReportPath { if renderer.structuredRunRequest.OutputPath != result.GeneratedTextRawPath {
t.Fatalf("run OutputPath = %q, want managed report path %q", renderer.runRequest.OutputPath, result.ReportPath) t.Fatalf("structured run OutputPath = %q, want raw generated text path %q", renderer.structuredRunRequest.OutputPath, result.GeneratedTextRawPath)
} }
if got, want := strings.Join(store.calls, ","), "module_snapshot,data_package,preflight,metadata,prepare_report,metadata"; !strings.HasPrefix(got, want) { if got, want := strings.Join(store.calls, ","), "module_snapshot,data_package,preflight,metadata,generated_text_result,metadata,generated_text,metadata,render_context,metadata,prepare_report,metadata"; !strings.HasPrefix(got, want) {
t.Fatalf("store calls = %v, want prefix %s", store.calls, want) t.Fatalf("store calls = %v, want prefix %s", store.calls, want)
} }
assertPathsExist(t, result.ModuleSnapshotPath, result.DataPackagePath, result.PreflightPath, result.ReportPath, result.MetadataPath, outputPath) assertPathsExist(t, result.ModuleSnapshotPath, result.DataPackagePath, result.PreflightPath, result.GeneratedTextRawPath, result.GeneratedTextResultPath, result.GeneratedTextPath, result.RenderContextPath, result.ReportPath, result.MetadataPath, outputPath)
snapshotData, err := os.ReadFile(result.ModuleSnapshotPath) snapshotData, err := os.ReadFile(result.ModuleSnapshotPath)
if err != nil { if err != nil {
t.Fatalf("read module snapshot: %v", err) t.Fatalf("read module snapshot: %v", err)
@@ -252,7 +260,7 @@ func TestGenerateReportWritesReportAndPreflight(t *testing.T) {
if err != nil { if err != nil {
t.Fatalf("read report output: %v", err) t.Fatalf("read report output: %v", err)
} }
if !strings.Contains(string(report), "# Daily Report") { if !strings.Contains(string(report), "# Friday's Weather") {
t.Fatalf("report output missing markdown:\n%s", string(report)) t.Fatalf("report output missing markdown:\n%s", string(report))
} }
} }
@@ -754,8 +762,7 @@ func TestGenerateReportSavesFinalMetadataForMarkdownAndGeneratedTextReports(t *t
cfg.Workspace.Root = t.TempDir() cfg.Workspace.Root = t.TempDir()
resolved, err := ResolveGenerate(GenerateRequest{ resolved, err := ResolveGenerate(GenerateRequest{
Config: cfg, Config: cfg,
Report: ReportDaily, Report: ReportThreeDay,
Date: mustParse("2026-05-29T12:00:00-05:00"),
}, mustParse("2026-05-29T05:00:00-05:00")) }, mustParse("2026-05-29T05:00:00-05:00"))
if err != nil { if err != nil {
t.Fatalf("ResolveGenerate() error = %v", err) t.Fatalf("ResolveGenerate() error = %v", err)
@@ -764,7 +771,7 @@ func TestGenerateReportSavesFinalMetadataForMarkdownAndGeneratedTextReports(t *t
result, err := GenerateReport(context.Background(), ReportRequest{ result, err := GenerateReport(context.Background(), ReportRequest{
Config: cfg, Config: cfg,
Resolved: resolved, Resolved: resolved,
Renderer: successfulRenderer("# Daily Report\n"), Renderer: successfulRenderer("# 3-Day Outlook\n"),
}) })
if err != nil { if err != nil {
t.Fatalf("GenerateReport() error = %v", err) t.Fatalf("GenerateReport() error = %v", err)
@@ -1125,7 +1132,7 @@ func TestGenerateReportNotifiesManagedReportPath(t *testing.T) {
if req.PipelineID != "weatherreporter.daily" { if req.PipelineID != "weatherreporter.daily" {
t.Fatalf("notification PipelineID = %q, want rendered pipeline", req.PipelineID) t.Fatalf("notification PipelineID = %q, want rendered pipeline", req.PipelineID)
} }
if req.BundleID != "weatherreporter.home.daily_today" { if req.BundleID != "weatherreporter.home.daily" {
t.Fatalf("notification BundleID = %q, want default template", req.BundleID) t.Fatalf("notification BundleID = %q, want default template", req.BundleID)
} }
if req.IdempotencyKey != req.BundleID+"."+result.Metadata.RunID { if req.IdempotencyKey != req.BundleID+"."+result.Metadata.RunID {
@@ -1208,10 +1215,9 @@ func TestGenerateReportDoesNotNotifyAfterRenderOrRunFailure(t *testing.T) {
{ {
name: "Run", name: "Run",
renderer: &recordingRenderer{ renderer: &recordingRenderer{
renderResult: &scriptorium.RenderResult{ExitCode: 0}, renderResult: &scriptorium.RenderResult{ExitCode: 0},
runResult: &scriptorium.RunResult{ExitCode: 2, Stderr: "run failed"}, structuredRunResult: &scriptorium.StructuredRunResult{ExitCode: 2, Stderr: "run failed"},
runErr: errors.New("run failed"), structuredRunErr: errors.New("run failed"),
runBody: "# Daily Report\n",
}, },
}, },
} }
@@ -1350,12 +1356,11 @@ func TestGenerateReportReturnsRunErrorAfterPreflight(t *testing.T) {
} }
renderer := &recordingRenderer{ renderer := &recordingRenderer{
renderResult: &scriptorium.RenderResult{ExitCode: 0}, renderResult: &scriptorium.RenderResult{ExitCode: 0},
runResult: &scriptorium.RunResult{ structuredRunResult: &scriptorium.StructuredRunResult{
Stderr: "validation failed", Stderr: "validation failed",
ExitCode: 2, ExitCode: 2,
}, },
runErr: errors.New("scriptorium run exited with code 2: validation failed"), structuredRunErr: errors.New("scriptorium run exited with code 2: validation failed"),
runBody: "# Daily Report\n",
} }
_, err = GenerateReport(context.Background(), ReportRequest{ _, err = GenerateReport(context.Background(), ReportRequest{
@@ -1366,8 +1371,8 @@ func TestGenerateReportReturnsRunErrorAfterPreflight(t *testing.T) {
if err == nil { if err == nil {
t.Fatal("GenerateReport() error = nil, want run error") t.Fatal("GenerateReport() error = nil, want run error")
} }
if renderer.renderCalls != 1 || renderer.runCalls != 1 { if renderer.renderCalls != 1 || renderer.structuredRunCalls != 1 || renderer.runCalls != 0 {
t.Fatalf("calls render=%d run=%d, want one of each", renderer.renderCalls, renderer.runCalls) t.Fatalf("calls render=%d structured=%d run=%d, want render and structured run only", renderer.renderCalls, renderer.structuredRunCalls, renderer.runCalls)
} }
store, err := state.NewFilesystemStore(cfg.Workspace) store, err := state.NewFilesystemStore(cfg.Workspace)
if err != nil { if err != nil {
@@ -1380,8 +1385,11 @@ func TestGenerateReportReturnsRunErrorAfterPreflight(t *testing.T) {
if _, err := os.Stat(paths.Metadata); err != nil { if _, err := os.Stat(paths.Metadata); err != nil {
t.Fatalf("expected metadata for failed run %q: %v", paths.Metadata, err) t.Fatalf("expected metadata for failed run %q: %v", paths.Metadata, err)
} }
if _, err := os.Stat(paths.RenderedReport); err != nil { if _, err := os.Stat(paths.GeneratedTextRaw); err != nil {
t.Fatalf("expected report from validation exit %q: %v", paths.RenderedReport, err) t.Fatalf("expected raw generated text from failed run %q: %v", paths.GeneratedTextRaw, err)
}
if _, err := os.Stat(paths.RenderedReport); !os.IsNotExist(err) {
t.Fatalf("rendered report exists after failed generated-text run: %v", err)
} }
} }
@@ -2552,6 +2560,10 @@ func validTodayGeneratedTextJSON() string {
return `{"summary":"Today starts with showers before improving.","forecast_discussion":["Morning showers should taper as drier air arrives.","Afternoon conditions trend quieter."],"precipitation_timing":"The best rain chance is during the morning."}` return `{"summary":"Today starts with showers before improving.","forecast_discussion":["Morning showers should taper as drier air arrives.","Afternoon conditions trend quieter."],"precipitation_timing":"The best rain chance is during the morning."}`
} }
func validDailyGeneratedTextJSON() string {
return `{"summary":"Showers are possible during the selected day.","forecast_discussion":["A front will keep rain chances in the forecast.","Temperatures stay seasonable by afternoon."],"precipitation_timing":"Rain is most likely during the afternoon.","confidence":"Medium"}`
}
func generateDailyReportForTest(t *testing.T, cfg config.Config) *ReportResult { func generateDailyReportForTest(t *testing.T, cfg config.Config) *ReportResult {
t.Helper() t.Helper()
cfg.Workspace.Root = t.TempDir() cfg.Workspace.Root = t.TempDir()
@@ -2835,9 +2847,10 @@ func (s *recordingStore) SaveMetadata(ctx context.Context, metadata state.Metada
func successfulRenderer(body string) *recordingRenderer { func successfulRenderer(body string) *recordingRenderer {
return &recordingRenderer{ return &recordingRenderer{
renderResult: &scriptorium.RenderResult{ExitCode: 0}, renderResult: &scriptorium.RenderResult{ExitCode: 0},
runResult: &scriptorium.RunResult{ExitCode: 0}, runResult: &scriptorium.RunResult{ExitCode: 0},
runBody: body, structuredRunResult: &scriptorium.StructuredRunResult{ExitCode: 0},
runBody: body,
} }
} }
@@ -2913,6 +2926,9 @@ func (r *selectiveRenderer) StructuredRun(_ context.Context, req scriptorium.Str
if req.PromptID == "weather.tomorrow_generated_text" { if req.PromptID == "weather.tomorrow_generated_text" {
body = validTomorrowGeneratedTextJSON() body = validTomorrowGeneratedTextJSON()
} }
if req.PromptID == "weather.daily_generated_text" {
body = validDailyGeneratedTextJSON()
}
if err := os.WriteFile(req.OutputPath, []byte(body), 0o600); err != nil { if err := os.WriteFile(req.OutputPath, []byte(body), 0o600); err != nil {
return nil, err return nil, err
} }
@@ -2942,8 +2958,12 @@ func (r *recordingRenderer) Run(_ context.Context, req scriptorium.RunRequest) (
func (r *recordingRenderer) StructuredRun(_ context.Context, req scriptorium.StructuredRunRequest) (*scriptorium.StructuredRunResult, error) { func (r *recordingRenderer) StructuredRun(_ context.Context, req scriptorium.StructuredRunRequest) (*scriptorium.StructuredRunResult, error) {
r.structuredRunCalls++ r.structuredRunCalls++
r.structuredRunRequest = req r.structuredRunRequest = req
if r.structuredRunBody != "" { body := r.structuredRunBody
if err := os.WriteFile(req.OutputPath, []byte(r.structuredRunBody), 0o600); err != nil { if body == "" {
body = validGeneratedTextJSONForPrompt(req.PromptID)
}
if body != "" {
if err := os.WriteFile(req.OutputPath, []byte(body), 0o600); err != nil {
return nil, err return nil, err
} }
} }
@@ -2952,3 +2972,18 @@ func (r *recordingRenderer) StructuredRun(_ context.Context, req scriptorium.Str
} }
return r.structuredRunResult, r.structuredRunErr return r.structuredRunResult, r.structuredRunErr
} }
func validGeneratedTextJSONForPrompt(promptID string) string {
switch promptID {
case "weather.daily_generated_text":
return validDailyGeneratedTextJSON()
case "weather.today_generated_text":
return validTodayGeneratedTextJSON()
case "weather.tomorrow_generated_text":
return validTomorrowGeneratedTextJSON()
case "weather.hourly_generated_text":
return validHourlyGeneratedTextJSON()
default:
return ""
}
}

View File

@@ -202,7 +202,7 @@ func TestMetadataModuleUsesPromptSafeSourceWarningSummary(t *testing.T) {
t.Fatalf("BuildModule() error = %v", err) t.Fatalf("BuildModule() error = %v", err)
} }
value := moduleValue[MetadataModule](t, output) value := moduleValue[MetadataModule](t, output)
if value.RunID == "" || value.ReportID != report.DailyToday || value.PromptID != "weather.daily_report" { if value.RunID == "" || value.ReportID != report.Daily || value.PromptID != "weather.daily_generated_text" {
t.Fatalf("metadata = %#v, want report identity", value) t.Fatalf("metadata = %#v, want report identity", value)
} }
if value.Location == nil || value.Location.Name != "Brentwood" { if value.Location == nil || value.Location.Name != "Brentwood" {
@@ -395,7 +395,7 @@ func TestAreaForecastDiscussionModuleUsesHourlyDefaultSections(t *testing.T) {
func testModuleContext() ModuleContext { func testModuleContext() ModuleContext {
generatedAt := mustParseModuleTime("2026-05-29T08:00:00-05:00") generatedAt := mustParseModuleTime("2026-05-29T08:00:00-05:00")
definition := report.DefaultRegistry().MustLookup(report.DailyToday) definition := report.DefaultRegistry().MustLookup(report.Daily)
resolved := report.Resolved{ resolved := report.Resolved{
Definition: definition, Definition: definition,
GeneratedAt: generatedAt, GeneratedAt: generatedAt,

View File

@@ -16,7 +16,7 @@ import (
func TestDerivedDailySummaryModulePackagesOrdinaryForecast(t *testing.T) { func TestDerivedDailySummaryModulePackagesOrdinaryForecast(t *testing.T) {
registry := MustDefaultModuleRegistry() registry := MustDefaultModuleRegistry()
ctx := derivedModuleContext(report.DailyToday) ctx := derivedModuleContext(report.Daily)
output, err := registry.BuildModule(ctx, module.ConfigItem{ID: module.DerivedDailySummary}) output, err := registry.BuildModule(ctx, module.ConfigItem{ID: module.DerivedDailySummary})
if err != nil { if err != nil {
@@ -67,7 +67,7 @@ func TestDerivedDailySummaryModulePackagesOrdinaryForecast(t *testing.T) {
func TestDerivedDailySummaryModuleFallsBackWithoutNarrativeFacts(t *testing.T) { func TestDerivedDailySummaryModuleFallsBackWithoutNarrativeFacts(t *testing.T) {
registry := MustDefaultModuleRegistry() registry := MustDefaultModuleRegistry()
ctx := derivedModuleContext(report.DailyToday) ctx := derivedModuleContext(report.Daily)
ctx.Derived.DailySummaries[0].NarrativePeriods = nil ctx.Derived.DailySummaries[0].NarrativePeriods = nil
output, err := registry.BuildModule(ctx, module.ConfigItem{ID: module.DerivedDailySummary}) output, err := registry.BuildModule(ctx, module.ConfigItem{ID: module.DerivedDailySummary})
@@ -89,7 +89,7 @@ func TestDerivedDailySummaryModuleFallsBackWithoutNarrativeFacts(t *testing.T) {
func TestPrecipTimingModuleHandlesRainyAndDryForecasts(t *testing.T) { func TestPrecipTimingModuleHandlesRainyAndDryForecasts(t *testing.T) {
registry := MustDefaultModuleRegistry() registry := MustDefaultModuleRegistry()
ctx := derivedModuleContext(report.DailyToday) ctx := derivedModuleContext(report.Daily)
output, err := registry.BuildModule(ctx, module.ConfigItem{ID: module.PrecipTiming}) output, err := registry.BuildModule(ctx, module.ConfigItem{ID: module.PrecipTiming})
if err != nil { if err != nil {
@@ -154,7 +154,7 @@ func TestPrecipTimingModuleUsesDerivedTimingWithoutDaypartSummaries(t *testing.T
func TestDerivedDaypartSummariesExposeConfiguredKeysAndHazards(t *testing.T) { func TestDerivedDaypartSummariesExposeConfiguredKeysAndHazards(t *testing.T) {
registry := MustDefaultModuleRegistry() registry := MustDefaultModuleRegistry()
ctx := derivedModuleContext(report.DailyToday) ctx := derivedModuleContext(report.Daily)
output, err := registry.BuildModule(ctx, module.ConfigItem{ID: module.DerivedDaypartSummaries}) output, err := registry.BuildModule(ctx, module.ConfigItem{ID: module.DerivedDaypartSummaries})
if err != nil { if err != nil {
@@ -380,7 +380,7 @@ func TestDailyPlanningModulePackagesPlanningFields(t *testing.T) {
func TestDailyPlanningModuleRejectsUnsupportedReports(t *testing.T) { func TestDailyPlanningModuleRejectsUnsupportedReports(t *testing.T) {
registry := MustDefaultModuleRegistry() registry := MustDefaultModuleRegistry()
for _, id := range []report.ID{report.DailyToday, report.Today, report.Tomorrow, report.Hourly, report.ThreeDay, report.Weekend, report.Storm} { for _, id := range []report.ID{report.Today, report.Tomorrow, report.Hourly, report.ThreeDay, report.Weekend, report.Storm} {
t.Run(string(id), func(t *testing.T) { t.Run(string(id), func(t *testing.T) {
ctx := derivedModuleContext(id) ctx := derivedModuleContext(id)
_, err := registry.BuildModule(ctx, module.ConfigItem{ID: module.DailyPlanning}) _, err := registry.BuildModule(ctx, module.ConfigItem{ID: module.DailyPlanning})
@@ -453,7 +453,7 @@ func TestTodayPlanningModulePackagesPlanningFields(t *testing.T) {
func TestTodayPlanningModuleRejectsUnsupportedReports(t *testing.T) { func TestTodayPlanningModuleRejectsUnsupportedReports(t *testing.T) {
registry := MustDefaultModuleRegistry() registry := MustDefaultModuleRegistry()
for _, id := range []report.ID{report.Tomorrow, report.DailyToday} { for _, id := range []report.ID{report.Tomorrow, report.Daily} {
t.Run(string(id), func(t *testing.T) { t.Run(string(id), func(t *testing.T) {
ctx := derivedModuleContext(id) ctx := derivedModuleContext(id)
_, err := registry.BuildModule(ctx, module.ConfigItem{ID: module.TodayPlanning}) _, err := registry.BuildModule(ctx, module.ConfigItem{ID: module.TodayPlanning})
@@ -484,7 +484,7 @@ func TestTodayPlanningModuleHandlesMissingDailySummary(t *testing.T) {
func TestDerivedModulesHandleMissingData(t *testing.T) { func TestDerivedModulesHandleMissingData(t *testing.T) {
registry := MustDefaultModuleRegistry() registry := MustDefaultModuleRegistry()
ctx := derivedModuleContext(report.DailyToday) ctx := derivedModuleContext(report.Daily)
ctx.Derived.DailySummaries = nil ctx.Derived.DailySummaries = nil
ctx.Derived.DaypartSummaries = nil ctx.Derived.DaypartSummaries = nil
@@ -576,7 +576,7 @@ func derivedModuleContext(id report.ID) ModuleContext {
} }
func todayModuleContext() ModuleContext { func todayModuleContext() ModuleContext {
ctx := derivedModuleContext(report.DailyToday) ctx := derivedModuleContext(report.Daily)
ctx.Resolved.Definition = report.Definition{ ctx.Resolved.Definition = report.Definition{
ID: report.Today, ID: report.Today,
Name: "Today Report", Name: "Today Report",
@@ -588,7 +588,7 @@ func todayModuleContext() ModuleContext {
func dailyModuleContext() ModuleContext { func dailyModuleContext() ModuleContext {
ctx := derivedModuleContext(report.Tomorrow) ctx := derivedModuleContext(report.Tomorrow)
ctx.Resolved.Definition = report.Definition{ ctx.Resolved.Definition = report.Definition{
ID: report.ID("daily"), ID: report.Daily,
Name: "Daily Report", Name: "Daily Report",
PromptID: "weather.daily_generated_text", PromptID: "weather.daily_generated_text",
} }

View File

@@ -249,9 +249,8 @@ func (d ModuleDefinition) ValidateOptions(options any) error {
} }
func defaultModuleDefinitions() []ModuleDefinition { func defaultModuleDefinitions() []ModuleDefinition {
dailyReportID := report.ID("daily") allReports := []report.ID{report.Daily, report.Today, report.Tomorrow, report.Hourly, report.ThreeDay, report.Weekend, report.Storm}
allReports := []report.ID{report.DailyToday, report.Today, report.Tomorrow, report.Hourly, report.ThreeDay, report.Weekend, report.Storm} daypartReports := []report.ID{report.Daily, report.Today, report.Tomorrow, report.ThreeDay, report.Weekend}
daypartReports := []report.ID{report.DailyToday, report.Today, report.Tomorrow, report.ThreeDay, report.Weekend}
return []ModuleDefinition{ return []ModuleDefinition{
{ {
ID: module.Metadata, ID: module.Metadata,
@@ -277,7 +276,7 @@ func defaultModuleDefinitions() []ModuleDefinition {
DefaultOptions: module.NarrativeForecastOptions{}, DefaultOptions: module.NarrativeForecastOptions{},
RequiredCollected: []module.FactRequirement{module.CollectedNarrativeForecast}, RequiredCollected: []module.FactRequirement{module.CollectedNarrativeForecast},
RequiredDerived: []module.FactRequirement{module.RequiresDerivedNarrativePeriods}, RequiredDerived: []module.FactRequirement{module.RequiresDerivedNarrativePeriods},
SupportedReports: []report.ID{report.DailyToday, report.Today, report.Tomorrow}, SupportedReports: []report.ID{report.Daily, report.Today, report.Tomorrow},
MissingData: module.MissingDataOmit, MissingData: module.MissingDataOmit,
Builder: buildNarrativeForecastModule, Builder: buildNarrativeForecastModule,
}, },
@@ -287,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.Today, report.Tomorrow, report.Hourly}, SupportedReports: []report.ID{report.Daily, report.Today, report.Tomorrow, report.Hourly},
MissingData: module.MissingDataOmit, MissingData: module.MissingDataOmit,
Builder: buildHourlyForecastModule, Builder: buildHourlyForecastModule,
}, },
@@ -296,7 +295,7 @@ func defaultModuleDefinitions() []ModuleDefinition {
StanzaName: "derived_daily_summary", StanzaName: "derived_daily_summary",
DefaultOptions: module.DerivedDailySummaryOptions{}, DefaultOptions: module.DerivedDailySummaryOptions{},
RequiredDerived: []module.FactRequirement{module.RequiresDerivedDailySummaries, module.RequiresDerivedPrecipTiming}, RequiredDerived: []module.FactRequirement{module.RequiresDerivedDailySummaries, module.RequiresDerivedPrecipTiming},
SupportedReports: []report.ID{report.DailyToday, report.Today, report.Tomorrow}, SupportedReports: []report.ID{report.Daily, report.Today, report.Tomorrow},
MissingData: module.MissingDataError, MissingData: module.MissingDataError,
Builder: buildDerivedDailySummaryModule, Builder: buildDerivedDailySummaryModule,
}, },
@@ -398,7 +397,7 @@ func defaultModuleDefinitions() []ModuleDefinition {
StanzaName: "daily_planning", StanzaName: "daily_planning",
DefaultOptions: module.DailyPlanningOptions{}, DefaultOptions: module.DailyPlanningOptions{},
RequiredDerived: []module.FactRequirement{module.RequiresDerivedDailySummaries}, RequiredDerived: []module.FactRequirement{module.RequiresDerivedDailySummaries},
SupportedReports: []report.ID{dailyReportID}, SupportedReports: []report.ID{report.Daily},
MissingData: module.MissingDataEmpty, MissingData: module.MissingDataEmpty,
Builder: buildDailyPlanningModule, Builder: buildDailyPlanningModule,
}, },

View File

@@ -77,7 +77,7 @@ func TestHourlyDefaultModuleOptions(t *testing.T) {
func TestModuleRegistryRejectsUnknownModule(t *testing.T) { func TestModuleRegistryRejectsUnknownModule(t *testing.T) {
registry := MustDefaultModuleRegistry() registry := MustDefaultModuleRegistry()
err := registry.ValidateComposition(report.DailyToday, []module.ConfigItem{{ID: module.ID("unknown")}}) err := registry.ValidateComposition(report.Daily, []module.ConfigItem{{ID: module.ID("unknown")}})
if err == nil || !strings.Contains(err.Error(), `unknown module "unknown"`) { if err == nil || !strings.Contains(err.Error(), `unknown module "unknown"`) {
t.Fatalf("error = %v, want unknown module", err) t.Fatalf("error = %v, want unknown module", err)
} }
@@ -85,7 +85,7 @@ func TestModuleRegistryRejectsUnknownModule(t *testing.T) {
func TestModuleRegistryRejectsDuplicateModuleIDs(t *testing.T) { func TestModuleRegistryRejectsDuplicateModuleIDs(t *testing.T) {
registry := MustDefaultModuleRegistry() registry := MustDefaultModuleRegistry()
err := registry.ValidateComposition(report.DailyToday, []module.ConfigItem{ err := registry.ValidateComposition(report.Daily, []module.ConfigItem{
{ID: module.Metadata}, {ID: module.Metadata},
{ID: module.Metadata}, {ID: module.Metadata},
}) })
@@ -106,8 +106,8 @@ func TestModuleRegistryRejectsDuplicateStanzaNames(t *testing.T) {
func TestModuleRegistryRejectsIncompatibleReports(t *testing.T) { func TestModuleRegistryRejectsIncompatibleReports(t *testing.T) {
registry := MustDefaultModuleRegistry() registry := MustDefaultModuleRegistry()
err := registry.ValidateComposition(report.DailyToday, []module.ConfigItem{{ID: module.TomorrowPlanning}}) err := registry.ValidateComposition(report.Daily, []module.ConfigItem{{ID: module.TomorrowPlanning}})
if err == nil || !strings.Contains(err.Error(), `module "tomorrow_planning" is not compatible with report "daily_today"`) { if err == nil || !strings.Contains(err.Error(), `module "tomorrow_planning" is not compatible with report "daily"`) {
t.Fatalf("error = %v, want incompatible report", err) t.Fatalf("error = %v, want incompatible report", err)
} }
} }
@@ -117,7 +117,7 @@ func TestModuleRegistryValidatesTodayPlanningSupport(t *testing.T) {
if err := registry.ValidateComposition(report.Today, []module.ConfigItem{{ID: module.TodayPlanning}}); err != nil { if err := registry.ValidateComposition(report.Today, []module.ConfigItem{{ID: module.TodayPlanning}}); err != nil {
t.Fatalf("ValidateComposition(today) error = %v", err) t.Fatalf("ValidateComposition(today) error = %v", err)
} }
for _, id := range []report.ID{report.Tomorrow, report.DailyToday} { for _, id := range []report.ID{report.Tomorrow, report.Daily} {
t.Run(string(id), func(t *testing.T) { t.Run(string(id), func(t *testing.T) {
err := registry.ValidateComposition(id, []module.ConfigItem{{ID: module.TodayPlanning}}) err := registry.ValidateComposition(id, []module.ConfigItem{{ID: module.TodayPlanning}})
if err == nil || !strings.Contains(err.Error(), `module "today_planning" is not compatible with report`) { if err == nil || !strings.Contains(err.Error(), `module "today_planning" is not compatible with report`) {
@@ -129,10 +129,10 @@ func TestModuleRegistryValidatesTodayPlanningSupport(t *testing.T) {
func TestModuleRegistryValidatesDailyPlanningSupport(t *testing.T) { func TestModuleRegistryValidatesDailyPlanningSupport(t *testing.T) {
registry := MustDefaultModuleRegistry() registry := MustDefaultModuleRegistry()
if err := registry.ValidateComposition(report.ID("daily"), []module.ConfigItem{{ID: module.DailyPlanning}}); err != nil { if err := registry.ValidateComposition(report.Daily, []module.ConfigItem{{ID: module.DailyPlanning}}); err != nil {
t.Fatalf("ValidateComposition(daily) error = %v", err) t.Fatalf("ValidateComposition(daily) error = %v", err)
} }
for _, id := range []report.ID{report.DailyToday, report.Today, report.Tomorrow, report.Hourly, report.ThreeDay, report.Weekend, report.Storm} { for _, id := range []report.ID{report.Today, report.Tomorrow, report.Hourly, report.ThreeDay, report.Weekend, report.Storm} {
t.Run(string(id), func(t *testing.T) { t.Run(string(id), func(t *testing.T) {
err := registry.ValidateComposition(id, []module.ConfigItem{{ID: module.DailyPlanning}}) err := registry.ValidateComposition(id, []module.ConfigItem{{ID: module.DailyPlanning}})
if err == nil || !strings.Contains(err.Error(), `module "daily_planning" is not compatible with report`) { if err == nil || !strings.Contains(err.Error(), `module "daily_planning" is not compatible with report`) {
@@ -204,7 +204,7 @@ func TestModuleRegistryRejectsUnsupportedMissingDataWarn(t *testing.T) {
func TestModuleRegistryRejectsInvalidOptionShapes(t *testing.T) { func TestModuleRegistryRejectsInvalidOptionShapes(t *testing.T) {
registry := MustDefaultModuleRegistry() registry := MustDefaultModuleRegistry()
err := registry.ValidateComposition(report.DailyToday, []module.ConfigItem{ err := registry.ValidateComposition(report.Daily, []module.ConfigItem{
{ID: module.Metadata, Options: module.CurrentConditionsOptions{}}, {ID: module.Metadata, Options: module.CurrentConditionsOptions{}},
}) })
if err == nil || !strings.Contains(err.Error(), `module "metadata" options have type module.CurrentConditionsOptions, want module.MetadataOptions`) { if err == nil || !strings.Contains(err.Error(), `module "metadata" options have type module.CurrentConditionsOptions, want module.MetadataOptions`) {
@@ -214,7 +214,7 @@ func TestModuleRegistryRejectsInvalidOptionShapes(t *testing.T) {
func TestModuleRegistryAcceptsTypedOptions(t *testing.T) { func TestModuleRegistryAcceptsTypedOptions(t *testing.T) {
registry := MustDefaultModuleRegistry() registry := MustDefaultModuleRegistry()
err := registry.ValidateComposition(report.DailyToday, []module.ConfigItem{ err := registry.ValidateComposition(report.Daily, []module.ConfigItem{
{ID: module.Metadata, Options: module.MetadataOptions{}}, {ID: module.Metadata, Options: module.MetadataOptions{}},
{ID: module.CurrentConditions, Options: &module.CurrentConditionsOptions{}}, {ID: module.CurrentConditions, Options: &module.CurrentConditionsOptions{}},
}) })

View File

@@ -185,7 +185,7 @@ func alertStatus(bundle *weatherdata.Bundle) *AlertStatus {
func variantForReport(id report.ID) string { func variantForReport(id report.ID) string {
switch id { switch id {
case report.DailyToday, report.Today: case report.Daily, report.Today:
return "today" return "today"
case report.Tomorrow: case report.Tomorrow:
return "tomorrow" return "tomorrow"

View File

@@ -251,7 +251,7 @@ func TestBatchOutputIncludesNotificationDetails(t *testing.T) {
Failed: 1, Failed: 1,
Reports: []app.BatchReportResult{ Reports: []app.BatchReportResult{
{ {
ReportID: "daily_today", ReportID: "daily",
Status: "succeeded", Status: "succeeded",
OutputPath: "/tmp/daily.md", OutputPath: "/tmp/daily.md",
NotificationStatus: "accepted", NotificationStatus: "accepted",
@@ -299,11 +299,11 @@ func TestBatchOutputDoesNotExposeSecretLikeNotificationErrors(t *testing.T) {
Failed: 1, Failed: 1,
Reports: []app.BatchReportResult{ Reports: []app.BatchReportResult{
{ {
ReportID: "daily_today", ReportID: "daily",
Status: "failed", Status: "failed",
Error: "notify report daily_today: upload failed: [redacted]", Error: "notify report daily: upload failed: [redacted]",
NotificationStatus: "failed", NotificationStatus: "failed",
NotificationError: "notify report daily_today: upload failed: [redacted]", NotificationError: "notify report daily: upload failed: [redacted]",
}, },
}, },
} }
@@ -526,7 +526,7 @@ func TestRunGenerateDailyWritesMarkdownReport(t *testing.T) {
if err != nil { if err != nil {
t.Fatalf("read report: %v", err) t.Fatalf("read report: %v", err)
} }
if !strings.Contains(string(report), "# Daily Report") { if !strings.Contains(string(report), "# Friday's Weather") {
t.Fatalf("report output missing markdown:\n%s", string(report)) t.Fatalf("report output missing markdown:\n%s", string(report))
} }
dataPackageMatches, err := filepath.Glob(filepath.Join(workspaceRoot, "data-packages", "daily", "2026-05-29", "*.data_package.yaml")) dataPackageMatches, err := filepath.Glob(filepath.Join(workspaceRoot, "data-packages", "daily", "2026-05-29", "*.data_package.yaml"))
@@ -540,7 +540,7 @@ func TestRunGenerateDailyWritesMarkdownReport(t *testing.T) {
if err != nil { if err != nil {
t.Fatalf("read managed data package: %v", err) t.Fatalf("read managed data package: %v", err)
} }
if !strings.Contains(string(data), "schema_version: weatherreporter.data_package.v2") || !strings.Contains(string(data), "id: daily_today") { if !strings.Contains(string(data), "schema_version: weatherreporter.data_package.v2") || !strings.Contains(string(data), "id: daily") {
t.Fatalf("data package output missing expected content:\n%s", string(data)) t.Fatalf("data package output missing expected content:\n%s", string(data))
} }
if !strings.Contains(string(data), "location:") || if !strings.Contains(string(data), "location:") ||
@@ -1299,6 +1299,21 @@ JSON
], ],
"precipitation_timing": "The best rain chance is during the morning." "precipitation_timing": "The best rain chance is during the morning."
} }
JSON
printf 'wrote generated text\n' >&2
exit 0
fi
if [ "$prompt" = "weather.daily_generated_text" ]; then
cat > "$out" <<'JSON'
{
"summary": "Showers are possible during the selected day.",
"forecast_discussion": [
"A front will keep rain chances in the forecast.",
"Temperatures stay seasonable by afternoon."
],
"precipitation_timing": "Rain is most likely during the afternoon.",
"confidence": "Medium"
}
JSON JSON
printf 'wrote generated text\n' >&2 printf 'wrote generated text\n' >&2
exit 0 exit 0
@@ -1345,6 +1360,21 @@ if [ "$1" = "run" ]; then
"precipitation_timing": "A cold front is moving into the region.", "precipitation_timing": "A cold front is moving into the region.",
"confidence": "Medium" "confidence": "Medium"
} }
JSON
printf 'wrote generated text\n' >&2
exit 0
fi
if [ "$prompt" = "weather.daily_generated_text" ]; then
cat > "$out" <<'JSON'
{
"summary": "Showers are possible during the selected day.",
"forecast_discussion": [
"A front will keep rain chances in the forecast.",
"Temperatures stay seasonable by afternoon."
],
"precipitation_timing": "Rain is most likely during the afternoon.",
"confidence": "Medium"
}
JSON JSON
printf 'wrote generated text\n' >&2 printf 'wrote generated text\n' >&2
exit 0 exit 0

View File

@@ -163,7 +163,7 @@ reports:
if err != nil { if err != nil {
t.Fatalf("ReportModuleOverrides() error = %v", err) t.Fatalf("ReportModuleOverrides() error = %v", err)
} }
items := overrides[report.DailyToday] items := overrides[report.Daily]
if len(items) != 6 { if len(items) != 6 {
t.Fatalf("daily override length = %d, want 6", len(items)) t.Fatalf("daily override length = %d, want 6", len(items))
} }
@@ -220,8 +220,8 @@ reports:
t.Fatalf("today override[%d] = %s, want %s", i, items[i].ID, id) t.Fatalf("today override[%d] = %s, want %s", i, items[i].ID, id)
} }
} }
if _, ok := overrides[report.DailyToday]; ok { if _, ok := overrides[report.Daily]; ok {
t.Fatalf("daily override = %#v, want today override to stay distinct", overrides[report.DailyToday]) t.Fatalf("daily override = %#v, want today override to stay distinct", overrides[report.Daily])
} }
} }
@@ -320,12 +320,12 @@ func TestValidateReportModuleAliasesDirectly(t *testing.T) {
wantErr string wantErr string
}{ }{
{ {
name: "DuplicateAlias", name: "RetiredDailyReportID",
reports: map[string]ReportConfig{ reports: map[string]ReportConfig{
"daily": {}, "daily": {},
"daily_today": {}, "daily_today": {},
}, },
wantErr: "duplicates report override", wantErr: "reports.daily_today",
}, },
{ {
name: "TodayAndDailyAreDistinct", name: "TodayAndDailyAreDistinct",
@@ -425,7 +425,7 @@ reports:
deterministic_modules: deterministic_modules:
- tomorrow_planning - tomorrow_planning
`, `,
wantErr: `not compatible with report "daily_today"`, wantErr: `not compatible with report "daily"`,
}, },
{ {
name: "RemovedPlaceholderModule", name: "RemovedPlaceholderModule",
@@ -461,7 +461,7 @@ reports:
wantErr: "options are invalid", wantErr: "options are invalid",
}, },
{ {
name: "DuplicateReportAlias", name: "RetiredDailyReportID",
yaml: ` yaml: `
reports: reports:
daily: daily:
@@ -471,7 +471,7 @@ reports:
deterministic_modules: deterministic_modules:
- current_conditions - current_conditions
`, `,
wantErr: "duplicates report override", wantErr: "reports.daily_today",
}, },
{ {
name: "HourlyIncompatibleDailyModule", name: "HourlyIncompatibleDailyModule",

View File

@@ -115,7 +115,7 @@ func BuildDerived(req BuildDerivedRequest) (DerivedFacts, error) {
switch req.Resolved.Definition.ID { switch req.Resolved.Definition.ID {
case report.Hourly: case report.Hourly:
case report.DailyToday, report.Today, report.Tomorrow: case report.Daily, report.Today, report.Tomorrow:
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 {
return DerivedFacts{}, err return DerivedFacts{}, err

View File

@@ -56,7 +56,7 @@ func TestBuildCollectedCopiesBundleFactsAndKeepsSourcesSeparate(t *testing.T) {
func TestBuildDerivedDailySlicesDaypartsAndAlerts(t *testing.T) { func TestBuildDerivedDailySlicesDaypartsAndAlerts(t *testing.T) {
location := testLocation() location := testLocation()
resolved := resolveForTest(t, report.DailyToday, mustParse("2026-05-29T08:00:00-05:00"), location) resolved := resolveForTest(t, report.Daily, mustParse("2026-05-29T08:00:00-05:00"), location)
derived, err := BuildDerived(BuildDerivedRequest{ derived, err := BuildDerived(BuildDerivedRequest{
Resolved: resolved, Resolved: resolved,
Timezone: location.String(), Timezone: location.String(),
@@ -253,7 +253,7 @@ func TestBuildDerivedSelectsSPCConvectiveOutlooksByValidPeriod(t *testing.T) {
}{ }{
{ {
name: "daily today", name: "daily today",
resolved: resolveForTest(t, report.DailyToday, now, location), resolved: resolveForTest(t, report.Daily, now, location),
wantOutlookIDs: []string{"fri-high", "fri-storm", "fri-low", "fri-missing-rank", "fri-probabilistic"}, wantOutlookIDs: []string{"fri-high", "fri-storm", "fri-low", "fri-missing-rank", "fri-probabilistic"},
wantDiscussion: []string{"day1 early", "day1 late"}, wantDiscussion: []string{"day1 early", "day1 late"},
}, },
@@ -322,7 +322,7 @@ func TestBuildDerivedUnsupportedReportReturnsActionableError(t *testing.T) {
func TestBuildDerivedSPCConvectiveOutlooksDistinguishesMissingAndCheckedEmpty(t *testing.T) { func TestBuildDerivedSPCConvectiveOutlooksDistinguishesMissingAndCheckedEmpty(t *testing.T) {
location := testLocation() location := testLocation()
resolved := resolveForTest(t, report.DailyToday, mustParse("2026-05-29T08:00:00-05:00"), location) resolved := resolveForTest(t, report.Daily, mustParse("2026-05-29T08:00:00-05:00"), location)
bundle := testBundle(location) bundle := testBundle(location)
bundle.SPCConvectiveOutlooks = nil bundle.SPCConvectiveOutlooks = nil
@@ -465,7 +465,11 @@ func testDayparts() []forecast.DaypartDefinition {
func resolveForTest(t *testing.T, id report.ID, now time.Time, location *time.Location) report.Resolved { func resolveForTest(t *testing.T, id report.ID, now time.Time, location *time.Location) report.Resolved {
t.Helper() t.Helper()
resolved, err := report.Resolve(id, report.ResolveRequest{Now: now, Location: location}) req := report.ResolveRequest{Now: now, Location: location}
if id == report.Daily {
req.Date = now
}
resolved, err := report.Resolve(id, req)
if err != nil { if err != nil {
t.Fatalf("resolve %s: %v", id, err) t.Fatalf("resolve %s: %v", id, err)
} }

View File

@@ -121,7 +121,7 @@ func TestCatalogLookupSupportsTodayDefinition(t *testing.T) {
func TestCatalogLookupSupportsDailyDefinitionAssets(t *testing.T) { func TestCatalogLookupSupportsDailyDefinitionAssets(t *testing.T) {
definition := report.Definition{ definition := report.Definition{
ID: report.ID("daily"), ID: report.Daily,
GenerationMode: report.GenerationModeGeneratedTextTemplate, GenerationMode: report.GenerationModeGeneratedTextTemplate,
GeneratedTextSchemaID: "daily", GeneratedTextSchemaID: "daily",
TemplateID: "daily", TemplateID: "daily",
@@ -206,7 +206,7 @@ func TestCatalogValidationDispatchSupportsKnownSchemas(t *testing.T) {
} }
dailyHandler, err := LookupDefinition(report.Definition{ dailyHandler, err := LookupDefinition(report.Definition{
ID: report.ID("daily"), ID: report.Daily,
GenerationMode: report.GenerationModeGeneratedTextTemplate, GenerationMode: report.GenerationModeGeneratedTextTemplate,
GeneratedTextSchemaID: "daily", GeneratedTextSchemaID: "daily",
TemplateID: "daily", TemplateID: "daily",
@@ -281,7 +281,7 @@ func TestCatalogBuildRenderContextRejectsMismatchedGeneratedText(t *testing.T) {
} }
dailyHandler, err := LookupDefinition(report.Definition{ dailyHandler, err := LookupDefinition(report.Definition{
ID: report.ID("daily"), ID: report.Daily,
GenerationMode: report.GenerationModeGeneratedTextTemplate, GenerationMode: report.GenerationModeGeneratedTextTemplate,
GeneratedTextSchemaID: "daily", GeneratedTextSchemaID: "daily",
TemplateID: "daily", TemplateID: "daily",
@@ -303,7 +303,7 @@ func TestCatalogBuildRenderContextRejectsMismatchedGeneratedText(t *testing.T) {
func TestCatalogBuildRenderContextSupportsDaily(t *testing.T) { func TestCatalogBuildRenderContextSupportsDaily(t *testing.T) {
handler, err := LookupDefinition(report.Definition{ handler, err := LookupDefinition(report.Definition{
ID: report.ID("daily"), ID: report.Daily,
GenerationMode: report.GenerationModeGeneratedTextTemplate, GenerationMode: report.GenerationModeGeneratedTextTemplate,
GeneratedTextSchemaID: "daily", GeneratedTextSchemaID: "daily",
TemplateID: "daily", TemplateID: "daily",

View File

@@ -290,7 +290,7 @@ func TestBuildDailyRenderContext(t *testing.T) {
if !ctx.Report.ValidPeriod.Start.Equal(metadata.ValidPeriod.Start) || !ctx.Report.ValidPeriod.End.Equal(metadata.ValidPeriod.End) || ctx.Report.Timezone != "America/Chicago" { if !ctx.Report.ValidPeriod.Start.Equal(metadata.ValidPeriod.Start) || !ctx.Report.ValidPeriod.End.Equal(metadata.ValidPeriod.End) || ctx.Report.Timezone != "America/Chicago" {
t.Fatalf("period/timezone = %#v/%q, want metadata passthrough", ctx.Report.ValidPeriod, ctx.Report.Timezone) t.Fatalf("period/timezone = %#v/%q, want metadata passthrough", ctx.Report.ValidPeriod, ctx.Report.Timezone)
} }
if ctx.Modules.Metadata == nil || ctx.Modules.Metadata.ReportID != report.ID("daily") { if ctx.Modules.Metadata == nil || ctx.Modules.Metadata.ReportID != report.Daily {
t.Fatalf("Modules.Metadata = %#v, want daily metadata", ctx.Modules.Metadata) t.Fatalf("Modules.Metadata = %#v, want daily metadata", ctx.Modules.Metadata)
} }
if ctx.Modules.CurrentConditions == nil || ctx.Modules.HourlyForecast == nil { if ctx.Modules.CurrentConditions == nil || ctx.Modules.HourlyForecast == nil {
@@ -544,7 +544,7 @@ func testDailyMetadata() briefing.Metadata {
generatedAt := time.Date(2026, 6, 13, 14, 14, 0, 0, time.UTC) generatedAt := time.Date(2026, 6, 13, 14, 14, 0, 0, time.UTC)
return briefing.Metadata{ return briefing.Metadata{
RunID: "run-daily", RunID: "run-daily",
ReportID: report.ID("daily"), ReportID: report.Daily,
PromptID: "weather.daily_generated_text", PromptID: "weather.daily_generated_text",
GeneratedAt: generatedAt, GeneratedAt: generatedAt,
Units: "imperial", Units: "imperial",
@@ -763,7 +763,7 @@ func testDailySnapshot(t *testing.T) module.Snapshot {
StanzaName: string(module.Metadata), StanzaName: string(module.Metadata),
Value: briefing.MetadataModule{ Value: briefing.MetadataModule{
RunID: "run-daily", RunID: "run-daily",
ReportID: report.ID("daily"), ReportID: report.Daily,
PromptID: "weather.daily_generated_text", PromptID: "weather.daily_generated_text",
GeneratedAt: testDailyMetadata().GeneratedAt, GeneratedAt: testDailyMetadata().GeneratedAt,
Units: "imperial", Units: "imperial",

View File

@@ -40,6 +40,7 @@ var briefingStanzaCategories = map[string]string{
string(module.DerivedDaypartSummaries): categoryDerivedSummaries, string(module.DerivedDaypartSummaries): categoryDerivedSummaries,
string(module.PrecipTiming): categoryDerivedSummaries, string(module.PrecipTiming): categoryDerivedSummaries,
string(module.OutdoorWindows): categoryDerivedSummaries, string(module.OutdoorWindows): categoryDerivedSummaries,
string(module.DailyPlanning): categoryDerivedSummaries,
string(module.TodayPlanning): categoryDerivedSummaries, string(module.TodayPlanning): categoryDerivedSummaries,
string(module.TomorrowPlanning): categoryDerivedSummaries, string(module.TomorrowPlanning): categoryDerivedSummaries,
string(module.NarrativeForecast): categoryNarrativeProducts, string(module.NarrativeForecast): categoryNarrativeProducts,

View File

@@ -19,11 +19,11 @@ func TestBuildDailyDataPackage(t *testing.T) {
if pkg.SchemaVersion != SchemaVersion { if pkg.SchemaVersion != SchemaVersion {
t.Fatalf("SchemaVersion = %q, want %q", pkg.SchemaVersion, SchemaVersion) t.Fatalf("SchemaVersion = %q, want %q", pkg.SchemaVersion, SchemaVersion)
} }
if pkg.RunID != "20260529T100000Z_daily_today" { if pkg.RunID != "20260529T100000Z_daily" {
t.Fatalf("RunID = %q, want metadata run id", pkg.RunID) t.Fatalf("RunID = %q, want metadata run id", pkg.RunID)
} }
if pkg.Report.PromptID != "weather.daily_report" { if pkg.Report.PromptID != "weather.daily_generated_text" {
t.Fatalf("PromptID = %q, want weather.daily_report", pkg.Report.PromptID) t.Fatalf("PromptID = %q, want weather.daily_generated_text", pkg.Report.PromptID)
} }
if pkg.Report.CurrentLocalDate != "2026-05-29" { if pkg.Report.CurrentLocalDate != "2026-05-29" {
t.Fatalf("CurrentLocalDate = %q, want 2026-05-29", pkg.Report.CurrentLocalDate) t.Fatalf("CurrentLocalDate = %q, want 2026-05-29", pkg.Report.CurrentLocalDate)
@@ -252,10 +252,10 @@ func TestMarshalYAMLRejectsUncategorizedStanza(t *testing.T) {
func TestLoadYAMLRejectsMisplacedStanza(t *testing.T) { func TestLoadYAMLRejectsMisplacedStanza(t *testing.T) {
data := []byte(` data := []byte(`
schema_version: weatherreporter.data_package.v2 schema_version: weatherreporter.data_package.v2
run_id: 20260529T100000Z_daily_today run_id: 20260529T100000Z_daily
report: report:
id: daily_today id: daily
prompt_id: weather.daily_report prompt_id: weather.daily_generated_text
generated_at: 2026-05-29T10:00:00Z generated_at: 2026-05-29T10:00:00Z
timezone: America/Chicago timezone: America/Chicago
current_local_date: "2026-05-29" current_local_date: "2026-05-29"
@@ -264,7 +264,7 @@ report:
end: 2026-05-30T05:00:00Z end: 2026-05-30T05:00:00Z
briefing: briefing:
metadata: metadata:
run_id: 20260529T100000Z_daily_today run_id: 20260529T100000Z_daily
raw_data: raw_data:
alert_digest: alert_digest:
checked: true checked: true
@@ -283,10 +283,10 @@ func validBuildRequest(t *testing.T) BuildRequest {
generatedAt := time.Date(2026, 5, 29, 10, 0, 0, 0, time.UTC) generatedAt := time.Date(2026, 5, 29, 10, 0, 0, 0, time.UTC)
return BuildRequest{ return BuildRequest{
Metadata: Metadata{ Metadata: Metadata{
RunID: "20260529T100000Z_daily_today", RunID: "20260529T100000Z_daily",
ReportID: report.DailyToday, ReportID: report.Daily,
Variant: "today", Variant: "today",
PromptID: "weather.daily_report", PromptID: "weather.daily_generated_text",
GeneratedAt: generatedAt, GeneratedAt: generatedAt,
Timezone: "America/Chicago", Timezone: "America/Chicago",
ValidPeriod: timeutil.Period{ ValidPeriod: timeutil.Period{
@@ -295,7 +295,7 @@ func validBuildRequest(t *testing.T) BuildRequest {
}, },
}, },
Modules: snapshotWithOutputs(t, Modules: snapshotWithOutputs(t,
module.Output{ID: module.Metadata, StanzaName: "metadata", Value: map[string]string{"run_id": "20260529T100000Z_daily_today"}}, module.Output{ID: module.Metadata, StanzaName: "metadata", Value: map[string]string{"run_id": "20260529T100000Z_daily"}},
module.Output{ID: module.CurrentConditions, StanzaName: "current_conditions", Value: map[string]string{"condition_text": "Partly cloudy"}}, module.Output{ID: module.CurrentConditions, StanzaName: "current_conditions", Value: map[string]string{"condition_text": "Partly cloudy"}},
module.Output{ID: module.DerivedDailySummary, StanzaName: "derived_daily_summary", Value: map[string]string{"date": "2026-05-29"}}, module.Output{ID: module.DerivedDailySummary, StanzaName: "derived_daily_summary", Value: map[string]string{"date": "2026-05-29"}},
module.Output{ID: module.AlertDigest, StanzaName: "alert_digest", Value: map[string]bool{"checked": true}}, module.Output{ID: module.AlertDigest, StanzaName: "alert_digest", Value: map[string]bool{"checked": true}},

View File

@@ -1,24 +1,27 @@
package report package report
import ( import (
"fmt"
"gitea.maximumdirect.net/eric/weatherreporter/internal/module" "gitea.maximumdirect.net/eric/weatherreporter/internal/module"
"gitea.maximumdirect.net/eric/weatherreporter/internal/timeutil" "gitea.maximumdirect.net/eric/weatherreporter/internal/timeutil"
) )
func dailyTodayDefinition() Definition { func dailyDefinition() Definition {
return Definition{ return Definition{
ID: DailyToday, ID: Daily,
Name: "Daily Report", Name: "Daily Report",
PromptID: "weather.daily_report", PromptID: "weather.daily_generated_text",
GenerationMode: GenerationModeScriptoriumMarkdown, GenerationMode: GenerationModeGeneratedTextTemplate,
ComparisonStrategy: CompareSameValidDate, TemplateID: "daily",
ArtifactGroup: "daily", GeneratedTextSchemaID: "daily",
BatchOutputName: "daily.md", ComparisonStrategy: CompareSameValidDate,
Generated: true, ArtifactGroup: "daily",
CompatiblePriorIDs: []ID{DailyToday}, BatchOutputName: "daily.md",
Modules: dailyTodayModules(), Generated: true,
Morning: true, CompatiblePriorIDs: []ID{Daily},
resolve: resolveDailyToday, Modules: dailyModules(),
resolve: resolveDaily,
} }
} }
@@ -41,7 +44,7 @@ func tomorrowDefinition() Definition {
} }
} }
func dailyTodayModules() []module.ConfigItem { func dailyModules() []module.ConfigItem {
return moduleItems( return moduleItems(
module.Metadata, module.Metadata,
module.CurrentConditions, module.CurrentConditions,
@@ -55,6 +58,7 @@ func dailyTodayModules() []module.ConfigItem {
module.SPCConvectiveDiscussion, module.SPCConvectiveDiscussion,
module.WeatherStory, module.WeatherStory,
module.OutdoorWindows, module.OutdoorWindows,
module.DailyPlanning,
module.HourlyForecast, module.HourlyForecast,
) )
} }
@@ -78,11 +82,11 @@ func tomorrowModules() []module.ConfigItem {
) )
} }
func resolveDailyToday(req ResolveRequest) (timeutil.Period, error) { func resolveDaily(req ResolveRequest) (timeutil.Period, error) {
if !req.Date.IsZero() { if req.Date.IsZero() {
return timeutil.CivilDay(req.Date, req.Location), nil return timeutil.Period{}, fmt.Errorf("daily report requires an explicit date")
} }
return timeutil.CivilDay(req.Now, req.Location), nil return timeutil.CivilDay(req.Date, req.Location), nil
} }
func resolveTomorrow(req ResolveRequest) (timeutil.Period, error) { func resolveTomorrow(req ResolveRequest) (timeutil.Period, error) {

View File

@@ -12,13 +12,13 @@ import (
type ID string type ID string
const ( const (
DailyToday ID = "daily_today" Daily ID = "daily"
Today ID = "today" Today ID = "today"
Tomorrow ID = "tomorrow" Tomorrow ID = "tomorrow"
Hourly ID = "hourly" Hourly ID = "hourly"
ThreeDay ID = "three_day" ThreeDay ID = "three_day"
Weekend ID = "weekend" Weekend ID = "weekend"
Storm ID = "storm" Storm ID = "storm"
) )
type ComparisonStrategy string type ComparisonStrategy string

View File

@@ -21,7 +21,7 @@ const (
func IDForCommandName(name string) (ID, error) { func IDForCommandName(name string) (ID, error) {
switch name { switch name {
case CommandNameDaily: case CommandNameDaily:
return DailyToday, nil return Daily, nil
case CommandNameToday: case CommandNameToday:
return Today, nil return Today, nil
case CommandNameTomorrow: case CommandNameTomorrow:
@@ -54,8 +54,8 @@ func CommandNames() []string {
func IDForConfigKey(key string) (ID, error) { func IDForConfigKey(key string) (ID, error) {
normalized := strings.ReplaceAll(strings.TrimSpace(strings.ToLower(key)), "-", "_") normalized := strings.ReplaceAll(strings.TrimSpace(strings.ToLower(key)), "-", "_")
switch normalized { switch normalized {
case "daily", "daily_today": case "daily":
return DailyToday, nil return Daily, nil
case "today": case "today":
return Today, nil return Today, nil
case "tomorrow": case "tomorrow":

View File

@@ -14,11 +14,14 @@ func TestDailyValidPeriod(t *testing.T) {
location := mustLoadLocation(t) location := mustLoadLocation(t)
now := mustParse("2026-05-29T17:45:00-05:00") now := mustParse("2026-05-29T17:45:00-05:00")
resolved, err := Resolve(DailyToday, ResolveRequest{Now: now, Location: location}) resolved, err := Resolve(Daily, ResolveRequest{Now: now, Location: location})
if err != nil { if err == nil {
t.Fatalf("Resolve() error = %v", err) t.Fatal("Resolve() error = nil, want explicit date requirement")
} }
assertPeriod(t, resolved.ValidPeriod, "2026-05-29T00:00:00-05:00", "2026-05-30T00:00:00-05:00") if !strings.Contains(err.Error(), "requires an explicit date") {
t.Fatalf("Resolve() error = %v, want explicit date requirement", err)
}
_ = resolved
} }
func TestDailyValidPeriodCanUseExplicitDate(t *testing.T) { func TestDailyValidPeriodCanUseExplicitDate(t *testing.T) {
@@ -26,11 +29,23 @@ func TestDailyValidPeriodCanUseExplicitDate(t *testing.T) {
now := mustParse("2026-05-29T17:45:00-05:00") now := mustParse("2026-05-29T17:45:00-05:00")
date := mustParse("2026-05-31T12:00:00-05:00") date := mustParse("2026-05-31T12:00:00-05:00")
resolved, err := Resolve(DailyToday, ResolveRequest{Now: now, Location: location, Date: date}) resolved, err := Resolve(Daily, ResolveRequest{Now: now, Location: location, Date: date})
if err != nil { if err != nil {
t.Fatalf("Resolve() error = %v", err) t.Fatalf("Resolve() error = %v", err)
} }
assertPeriod(t, resolved.ValidPeriod, "2026-05-31T00:00:00-05:00", "2026-06-01T00:00:00-05:00") assertPeriod(t, resolved.ValidPeriod, "2026-05-31T00:00:00-05:00", "2026-06-01T00:00:00-05:00")
if resolved.Definition.PromptID != "weather.daily_generated_text" {
t.Fatalf("PromptID = %q, want weather.daily_generated_text", resolved.Definition.PromptID)
}
if resolved.Definition.GenerationMode != GenerationModeGeneratedTextTemplate {
t.Fatalf("GenerationMode = %q, want generated_text_template", resolved.Definition.GenerationMode)
}
if resolved.Definition.TemplateID != "daily" {
t.Fatalf("TemplateID = %q, want daily", resolved.Definition.TemplateID)
}
if resolved.Definition.GeneratedTextSchemaID != "daily" {
t.Fatalf("GeneratedTextSchemaID = %q, want daily", resolved.Definition.GeneratedTextSchemaID)
}
} }
func TestTodayValidPeriod(t *testing.T) { func TestTodayValidPeriod(t *testing.T) {
@@ -256,7 +271,7 @@ func TestEveningBatchIncludesTomorrow(t *testing.T) {
} }
} }
func TestBatchesDoNotIncludeHourly(t *testing.T) { func TestBatchesDoNotIncludeManualReports(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}
@@ -269,8 +284,8 @@ func TestBatchesDoNotIncludeHourly(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 == Hourly { if resolved.Definition.ID == Hourly || resolved.Definition.ID == Daily {
t.Fatalf("batch included %q, want hourly excluded", resolved.Definition.ID) t.Fatalf("batch included %q, want manual reports excluded", resolved.Definition.ID)
} }
} }
} }
@@ -280,7 +295,7 @@ func TestIDForCommandName(t *testing.T) {
name string name string
want ID want ID
}{ }{
{name: "daily", want: DailyToday}, {name: "daily", want: Daily},
{name: "today", want: Today}, {name: "today", want: Today},
{name: "tomorrow", want: Tomorrow}, {name: "tomorrow", want: Tomorrow},
{name: "hourly", want: Hourly}, {name: "hourly", want: Hourly},
@@ -323,8 +338,7 @@ func TestIDForConfigKey(t *testing.T) {
key string key string
want ID want ID
}{ }{
{key: "daily", want: DailyToday}, {key: "daily", want: Daily},
{key: "daily_today", want: DailyToday},
{key: "today", want: Today}, {key: "today", want: Today},
{key: "tomorrow", want: Tomorrow}, {key: "tomorrow", want: Tomorrow},
{key: "hourly", want: Hourly}, {key: "hourly", want: Hourly},
@@ -351,6 +365,9 @@ func TestIDForConfigKey(t *testing.T) {
if _, err := IDForConfigKey("daily_tomorrow"); err == nil || !strings.Contains(err.Error(), `report config key "daily_tomorrow" is not a known report`) { if _, err := IDForConfigKey("daily_tomorrow"); err == nil || !strings.Contains(err.Error(), `report config key "daily_tomorrow" is not a known report`) {
t.Fatalf("IDForConfigKey(daily_tomorrow) error = %v, want unknown key", err) t.Fatalf("IDForConfigKey(daily_tomorrow) error = %v, want unknown key", err)
} }
if _, err := IDForConfigKey("daily_today"); err == nil || !strings.Contains(err.Error(), `report config key "daily_today" is not a known report`) {
t.Fatalf("IDForConfigKey(daily_today) error = %v, want unknown key", err)
}
dailyID, err := IDForConfigKey("daily") dailyID, err := IDForConfigKey("daily")
if err != nil { if err != nil {
t.Fatalf("IDForConfigKey(daily) error = %v", err) t.Fatalf("IDForConfigKey(daily) error = %v", err)
@@ -418,7 +435,7 @@ func TestRegistryLookupErrorIsActionable(t *testing.T) {
func TestRegistryAllIncludesHourlyInStableOrder(t *testing.T) { func TestRegistryAllIncludesHourlyInStableOrder(t *testing.T) {
ids := resolvedDefinitionIDs(DefaultRegistry().All()) ids := resolvedDefinitionIDs(DefaultRegistry().All())
want := []string{"daily_today", "today", "tomorrow", "hourly", "three_day", "weekend", "storm"} want := []string{"daily", "today", "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)
} }
@@ -443,7 +460,7 @@ func TestRegistryDefinitionsDeclareGenerationMetadata(t *testing.T) {
if !definition.Generated { if !definition.Generated {
continue continue
} }
if definition.ID == Hourly || definition.ID == Today || definition.ID == Tomorrow { if definition.ID == Hourly || definition.ID == Daily || definition.ID == Today || definition.ID == Tomorrow {
wantTemplate := string(definition.ID) wantTemplate := string(definition.ID)
if definition.GenerationMode != GenerationModeGeneratedTextTemplate { if definition.GenerationMode != GenerationModeGeneratedTextTemplate {
t.Fatalf("%s GenerationMode = %q, want %q", definition.ID, definition.GenerationMode, GenerationModeGeneratedTextTemplate) t.Fatalf("%s GenerationMode = %q, want %q", definition.ID, definition.GenerationMode, GenerationModeGeneratedTextTemplate)
@@ -478,11 +495,11 @@ func TestRegistryDefinitionsDeclarePathAndCompatibilityPolicy(t *testing.T) {
comparisonStrategy ComparisonStrategy comparisonStrategy ComparisonStrategy
}{ }{
{ {
id: DailyToday, id: Daily,
artifactGroup: "daily", artifactGroup: "daily",
batchOutputName: "daily.md", batchOutputName: "daily.md",
generated: true, generated: true,
compatiblePriorIDs: []ID{DailyToday}, compatiblePriorIDs: []ID{Daily},
comparisonStrategy: CompareSameValidDate, comparisonStrategy: CompareSameValidDate,
}, },
{ {
@@ -572,7 +589,7 @@ func TestRegistryDefinitionsDeclareDefaultModules(t *testing.T) {
want []module.ID want []module.ID
}{ }{
{ {
id: DailyToday, id: Daily,
want: []module.ID{ want: []module.ID{
module.Metadata, module.Metadata,
module.CurrentConditions, module.CurrentConditions,
@@ -586,6 +603,7 @@ func TestRegistryDefinitionsDeclareDefaultModules(t *testing.T) {
module.SPCConvectiveDiscussion, module.SPCConvectiveDiscussion,
module.WeatherStory, module.WeatherStory,
module.OutdoorWindows, module.OutdoorWindows,
module.DailyPlanning,
module.HourlyForecast, module.HourlyForecast,
}, },
}, },
@@ -703,7 +721,7 @@ func TestRegistryDefinitionsDeclareDefaultModules(t *testing.T) {
func TestRegistryAppliesModuleOverridesWithoutChangingDefaults(t *testing.T) { func TestRegistryAppliesModuleOverridesWithoutChangingDefaults(t *testing.T) {
base := DefaultRegistry() base := DefaultRegistry()
overridden, err := base.WithModuleOverrides(map[ID][]module.ConfigItem{ overridden, err := base.WithModuleOverrides(map[ID][]module.ConfigItem{
DailyToday: { Daily: {
{ID: module.Metadata}, {ID: module.Metadata},
{ID: module.AlertDigest}, {ID: module.AlertDigest},
}, },
@@ -712,7 +730,7 @@ func TestRegistryAppliesModuleOverridesWithoutChangingDefaults(t *testing.T) {
t.Fatalf("WithModuleOverrides() error = %v", err) t.Fatalf("WithModuleOverrides() error = %v", err)
} }
definition, err := overridden.Lookup(DailyToday) definition, err := overridden.Lookup(Daily)
if err != nil { if err != nil {
t.Fatalf("Lookup(overridden) error = %v", err) t.Fatalf("Lookup(overridden) error = %v", err)
} }
@@ -720,7 +738,7 @@ func TestRegistryAppliesModuleOverridesWithoutChangingDefaults(t *testing.T) {
t.Fatalf("overridden ModuleIDs() = %#v, want metadata and alert digest", definition.ModuleIDs()) t.Fatalf("overridden ModuleIDs() = %#v, want metadata and alert digest", definition.ModuleIDs())
} }
defaultDefinition, err := base.Lookup(DailyToday) defaultDefinition, err := base.Lookup(Daily)
if err != nil { if err != nil {
t.Fatalf("Lookup(default) error = %v", err) t.Fatalf("Lookup(default) error = %v", err)
} }
@@ -743,18 +761,22 @@ func TestRegistryRejectsModuleOverrideForUnknownReport(t *testing.T) {
func TestResolvedMetadata(t *testing.T) { func TestResolvedMetadata(t *testing.T) {
location := mustLoadLocation(t) location := mustLoadLocation(t)
resolved, err := Resolve(DailyToday, ResolveRequest{Now: mustParse("2026-05-29T05:00:00-05:00"), Location: location}) resolved, err := Resolve(Daily, ResolveRequest{
Now: mustParse("2026-05-29T05:00:00-05:00"),
Location: location,
Date: mustParse("2026-05-29T12:00:00-05:00"),
})
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 != DailyToday { if metadata.ReportID != Daily {
t.Fatalf("ReportID = %q, want daily_today", metadata.ReportID) t.Fatalf("ReportID = %q, want daily", metadata.ReportID)
} }
if metadata.PromptID != "weather.daily_report" { if metadata.PromptID != "weather.daily_generated_text" {
t.Fatalf("PromptID = %q, want weather.daily_report", metadata.PromptID) t.Fatalf("PromptID = %q, want weather.daily_generated_text", metadata.PromptID)
} }
if !strings.Contains(metadata.RunID, "daily_today") { if !strings.Contains(metadata.RunID, "daily") {
t.Fatalf("RunID = %q, want report id", metadata.RunID) t.Fatalf("RunID = %q, want report id", metadata.RunID)
} }
} }

View File

@@ -12,7 +12,7 @@ type Registry struct {
func DefaultRegistry() Registry { func DefaultRegistry() Registry {
definitions := []Definition{ definitions := []Definition{
dailyTodayDefinition(), dailyDefinition(),
todayDefinition(), todayDefinition(),
tomorrowDefinition(), tomorrowDefinition(),
hourlyDefinition(), hourlyDefinition(),
@@ -75,7 +75,7 @@ func (r Registry) MustLookup(id ID) Definition {
} }
func (r Registry) All() []Definition { func (r Registry) All() []Definition {
ids := []ID{DailyToday, Today, Tomorrow, Hourly, ThreeDay, Weekend, Storm} ids := []ID{Daily, Today, Tomorrow, 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 {

View File

@@ -27,12 +27,12 @@ func TestPathsUseRunIDAndWorkspace(t *testing.T) {
} }
for _, want := range []string{ for _, want := range []string{
filepath.Join("snapshots", "daily", "2026-05-29", "20260529T100000.000000000Z_daily_today.modules.json"), filepath.Join("snapshots", "daily", "2026-05-29", "20260529T100000.000000000Z_daily.modules.json"),
filepath.Join("snapshots", "daily", "2026-05-29", "20260529T100000.000000000Z_daily_today.metadata.json"), filepath.Join("snapshots", "daily", "2026-05-29", "20260529T100000.000000000Z_daily.metadata.json"),
filepath.Join("data-packages", "daily", "2026-05-29", "20260529T100000.000000000Z_daily_today.data_package.yaml"), filepath.Join("data-packages", "daily", "2026-05-29", "20260529T100000.000000000Z_daily.data_package.yaml"),
filepath.Join("preflight", "daily", "2026-05-29", "20260529T100000.000000000Z_daily_today.render.json"), filepath.Join("preflight", "daily", "2026-05-29", "20260529T100000.000000000Z_daily.render.json"),
filepath.Join("notifications", "daily", "2026-05-29", "20260529T100000.000000000Z_daily_today.distributor.json"), filepath.Join("notifications", "daily", "2026-05-29", "20260529T100000.000000000Z_daily.distributor.json"),
filepath.Join("reports", "daily", "20260529T100000.000000000Z_daily_today.md"), filepath.Join("reports", "daily", "20260529T100000.000000000Z_daily.md"),
} { } {
if !strings.Contains(pathsString(paths), want) { if !strings.Contains(pathsString(paths), want) {
t.Fatalf("paths = %#v, want component %q", paths, want) t.Fatalf("paths = %#v, want component %q", paths, want)
@@ -49,6 +49,13 @@ func TestGeneratedTextArtifactPathsUseSnapshotTree(t *testing.T) {
validDate string validDate string
runID string runID string
}{ }{
{
name: "daily",
resolved: resolveDailyAt(t, "2026-05-29T05:00:00-05:00"),
group: "daily",
validDate: "2026-05-29",
runID: "20260529T100000.000000000Z_daily",
},
{ {
name: "hourly", name: "hourly",
resolved: resolveHourlyAt(t, "2026-05-29T05:00:00-05:00"), resolved: resolveHourlyAt(t, "2026-05-29T05:00:00-05:00"),
@@ -243,9 +250,12 @@ func TestSaveArtifactsAndMetadataRoundTrip(t *testing.T) {
if strings.Contains(string(data), "MetadataPath") || strings.Contains(string(data), "metadataPath") { if strings.Contains(string(data), "MetadataPath") || strings.Contains(string(data), "metadataPath") {
t.Fatalf("metadata JSON includes runtime-only MetadataPath:\n%s", string(data)) t.Fatalf("metadata JSON includes runtime-only MetadataPath:\n%s", string(data))
} }
for _, unexpected := range []string{"generatedTextSchemaId", "generatedTextRawPath", "generatedTextResultPath", "generatedTextPath", "renderContextPath"} { if decoded.GeneratedTextSchemaID != "daily" {
t.Fatalf("GeneratedTextSchemaID = %q, want daily", decoded.GeneratedTextSchemaID)
}
for _, unexpected := range []string{"generatedTextRawPath", "generatedTextResultPath", "generatedTextPath", "renderContextPath"} {
if strings.Contains(string(data), unexpected) { if strings.Contains(string(data), unexpected) {
t.Fatalf("metadata JSON includes generated-text field %q for Markdown report:\n%s", unexpected, string(data)) t.Fatalf("metadata JSON includes unsaved generated-text path field %q:\n%s", unexpected, string(data))
} }
} }
} }
@@ -547,9 +557,10 @@ func resolveDailyAt(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.DailyToday, report.ResolveRequest{ resolved, err := report.DefaultRegistry().Resolve(report.Daily, report.ResolveRequest{
Now: now, Now: now,
Location: location, Location: location,
Date: now,
}) })
if err != nil { if err != nil {
t.Fatalf("Resolve() error = %v", err) t.Fatalf("Resolve() error = %v", err)