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

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`,
`three_day`, `weekend`, and `storm`. Canonical report IDs and accepted aliases
are also valid, including `daily_today`, `three_day_outlook`,
`weekend_outlook`, and `storm_report`. Hyphens and underscores are treated
equivalently in report keys.
are also valid, including `three_day_outlook`, `weekend_outlook`, and
`storm_report`. Hyphens and underscores are treated equivalently in report
keys. `daily_today` is not a supported report key.
`reports.today` applies only to the Today Report. `reports.daily` and
`reports.daily_today` apply only to the existing Daily Today report.
`reports.today` applies only to the Today Report. `reports.daily` applies only
to the dated Daily Report.
Each report entry supports:

View File

@@ -39,6 +39,10 @@ Outputs:
Every registered composition entry has a builder. Unknown or unimplemented
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_planning` and `hourly_forecast`; those outputs feed the Tomorrow
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
readiness, commute/school/workday concerns, and overnight change-watch fields.
It is compatible only with the `daily` report ID value. No active default
report composition includes it yet.
It is compatible only with the `daily` report ID value. The default Daily
Report composition includes it.
Hourly Report supports source and valid-period modules that operate over its
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
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
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.
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
day. No active default report composition includes it yet.
day. The default Daily Report composition includes it.
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
timestamps where applicable.
Today Report, Tomorrow Report, and Hourly Report module snapshots use the same
package schema and categories when converted into prompt input. The default
hourly module list places
Daily Report, Today Report, Tomorrow Report, and Hourly Report module snapshots
use the same package schema and categories when converted into prompt input.
The default hourly module list places
`precip_timing` under `derived_summaries`, alert and SPC outlooks under
`applicable_risk_products`, AFD/SPC discussion/weather story under
`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
package.
The default Today and Tomorrow module lists include civil-day summary stanzas,
planning stanzas, and `hourly_forecast` in the data package before structured
GeneratedText is requested from Scriptorium. Today uses `today_planning`;
Tomorrow uses `tomorrow_planning`.
The default Daily, Today, and Tomorrow module lists include civil-day summary
stanzas, planning stanzas, and `hourly_forecast` in the data package before
structured GeneratedText is requested from Scriptorium. Daily uses
`daily_planning`, Today uses `today_planning`, and Tomorrow uses
`tomorrow_planning`.
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.
The generate command names are `daily`, `today`, `tomorrow`, `hourly`,
`three-day`, `weekend`, and `storm`. Config keys also accept underscore and
legacy descriptive aliases such as `daily_today`, `three_day_outlook`,
`three-day`, `weekend`, and `storm`. Config keys also accept selected
underscore and descriptive aliases such as `three_day_outlook`,
`weekend_outlook`, and `storm_report`.
`daily` resolves to the existing Daily Today report ID `daily_today`.
`today` resolves to the independent Today report ID `today`. `reports.today`
is not an alias for `reports.daily` or `reports.daily_today`.
`daily` resolves to the dated Daily Report ID `daily`. `today` resolves to the
independent Today report ID `today`. `reports.today` is not an alias for
`reports.daily`, and `reports.daily_today` is not a supported config key.
Markdown report definitions use the `scriptorium_markdown` generation mode.
Their template and structured-text schema identifiers are empty. Today Report,
Tomorrow Report, and Hourly Report declare `generated_text_template`; the app
uses their template and schema identifiers to validate generated text and render
embedded Markdown templates.
Their template and structured-text schema identifiers are empty. Daily Report,
Today Report, Tomorrow Report, and Hourly Report declare
`generated_text_template`; the app uses their template and schema identifiers
to validate generated text and render embedded Markdown templates.
## Reports
| 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 |
| 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 |
@@ -59,8 +59,8 @@ All report definitions are eligible for generation.
## Valid Periods
- Daily Today covers the selected local civil day, or the current local civil
day when no date override is supplied.
- Daily Report covers the selected local civil day and requires an explicit
date.
- Today Report covers the selected local civil day, or the current local civil
day when no date override is supplied.
- 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
output path copying uses batch output names from report definitions. Report
module overrides can use short keys such as `today`, `tomorrow`, and `hourly`,
canonical report IDs such as `daily_today`, or descriptive names such as
`three_day_outlook`.
module overrides can use short keys such as `daily`, `today`, `tomorrow`, and
`hourly`, or descriptive names such as `three_day_outlook`.
## Batch Membership
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.
## 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:
```text
20260529T100000.123456789Z_daily_today
20260529T100000.123456789Z_daily
20260529T100000.123456789Z_today
```

View File

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

View File

@@ -105,6 +105,11 @@ func TestGenerateReportWritesReportAndPreflight(t *testing.T) {
ExitCode: 0,
OutputPath: "",
},
structuredRunResult: &scriptorium.StructuredRunResult{
Command: []string{"scriptorium", "run"},
Stderr: "wrote generated text",
ExitCode: 0,
},
runBody: "# Daily Report\n\nRain this morning.\n",
}
outputPath := filepath.Join(t.TempDir(), "daily.md")
@@ -128,25 +133,28 @@ func TestGenerateReportWritesReportAndPreflight(t *testing.T) {
if renderer.renderCalls != 1 {
t.Fatalf("render calls = %d, want 1", renderer.renderCalls)
}
if renderer.runCalls != 1 {
t.Fatalf("run calls = %d, want 1", renderer.runCalls)
if renderer.structuredRunCalls != 1 {
t.Fatalf("structured run calls = %d, want 1", renderer.structuredRunCalls)
}
if renderer.renderRequest.PromptID != "weather.daily_report" {
t.Fatalf("render PromptID = %q, want weather.daily_report", renderer.renderRequest.PromptID)
if renderer.runCalls != 0 {
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 {
t.Fatalf("render DataPackagePath = %q, want managed path %q", renderer.renderRequest.DataPackagePath, result.DataPackagePath)
}
if renderer.runRequest.DataPackagePath != result.DataPackagePath {
t.Fatalf("run DataPackagePath = %q, want managed path %q", renderer.runRequest.DataPackagePath, result.DataPackagePath)
if renderer.structuredRunRequest.DataPackagePath != result.DataPackagePath {
t.Fatalf("structured run DataPackagePath = %q, want managed path %q", renderer.structuredRunRequest.DataPackagePath, result.DataPackagePath)
}
if renderer.runRequest.OutputPath != result.ReportPath {
t.Fatalf("run OutputPath = %q, want managed report path %q", renderer.runRequest.OutputPath, result.ReportPath)
if renderer.structuredRunRequest.OutputPath != result.GeneratedTextRawPath {
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)
}
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)
if err != nil {
t.Fatalf("read module snapshot: %v", err)
@@ -252,7 +260,7 @@ func TestGenerateReportWritesReportAndPreflight(t *testing.T) {
if err != nil {
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))
}
}
@@ -754,8 +762,7 @@ func TestGenerateReportSavesFinalMetadataForMarkdownAndGeneratedTextReports(t *t
cfg.Workspace.Root = t.TempDir()
resolved, err := ResolveGenerate(GenerateRequest{
Config: cfg,
Report: ReportDaily,
Date: mustParse("2026-05-29T12:00:00-05:00"),
Report: ReportThreeDay,
}, mustParse("2026-05-29T05:00:00-05:00"))
if err != nil {
t.Fatalf("ResolveGenerate() error = %v", err)
@@ -764,7 +771,7 @@ func TestGenerateReportSavesFinalMetadataForMarkdownAndGeneratedTextReports(t *t
result, err := GenerateReport(context.Background(), ReportRequest{
Config: cfg,
Resolved: resolved,
Renderer: successfulRenderer("# Daily Report\n"),
Renderer: successfulRenderer("# 3-Day Outlook\n"),
})
if err != nil {
t.Fatalf("GenerateReport() error = %v", err)
@@ -1125,7 +1132,7 @@ func TestGenerateReportNotifiesManagedReportPath(t *testing.T) {
if req.PipelineID != "weatherreporter.daily" {
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)
}
if req.IdempotencyKey != req.BundleID+"."+result.Metadata.RunID {
@@ -1208,10 +1215,9 @@ func TestGenerateReportDoesNotNotifyAfterRenderOrRunFailure(t *testing.T) {
{
name: "Run",
renderer: &recordingRenderer{
renderResult: &scriptorium.RenderResult{ExitCode: 0},
runResult: &scriptorium.RunResult{ExitCode: 2, Stderr: "run failed"},
runErr: errors.New("run failed"),
runBody: "# Daily Report\n",
renderResult: &scriptorium.RenderResult{ExitCode: 0},
structuredRunResult: &scriptorium.StructuredRunResult{ExitCode: 2, Stderr: "run failed"},
structuredRunErr: errors.New("run failed"),
},
},
}
@@ -1350,12 +1356,11 @@ func TestGenerateReportReturnsRunErrorAfterPreflight(t *testing.T) {
}
renderer := &recordingRenderer{
renderResult: &scriptorium.RenderResult{ExitCode: 0},
runResult: &scriptorium.RunResult{
structuredRunResult: &scriptorium.StructuredRunResult{
Stderr: "validation failed",
ExitCode: 2,
},
runErr: errors.New("scriptorium run exited with code 2: validation failed"),
runBody: "# Daily Report\n",
structuredRunErr: errors.New("scriptorium run exited with code 2: validation failed"),
}
_, err = GenerateReport(context.Background(), ReportRequest{
@@ -1366,8 +1371,8 @@ func TestGenerateReportReturnsRunErrorAfterPreflight(t *testing.T) {
if err == nil {
t.Fatal("GenerateReport() error = nil, want run error")
}
if renderer.renderCalls != 1 || renderer.runCalls != 1 {
t.Fatalf("calls render=%d run=%d, want one of each", renderer.renderCalls, renderer.runCalls)
if renderer.renderCalls != 1 || renderer.structuredRunCalls != 1 || renderer.runCalls != 0 {
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)
if err != nil {
@@ -1380,8 +1385,11 @@ func TestGenerateReportReturnsRunErrorAfterPreflight(t *testing.T) {
if _, err := os.Stat(paths.Metadata); err != nil {
t.Fatalf("expected metadata for failed run %q: %v", paths.Metadata, err)
}
if _, err := os.Stat(paths.RenderedReport); err != nil {
t.Fatalf("expected report from validation exit %q: %v", paths.RenderedReport, err)
if _, err := os.Stat(paths.GeneratedTextRaw); err != nil {
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."}`
}
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 {
t.Helper()
cfg.Workspace.Root = t.TempDir()
@@ -2835,9 +2847,10 @@ func (s *recordingStore) SaveMetadata(ctx context.Context, metadata state.Metada
func successfulRenderer(body string) *recordingRenderer {
return &recordingRenderer{
renderResult: &scriptorium.RenderResult{ExitCode: 0},
runResult: &scriptorium.RunResult{ExitCode: 0},
runBody: body,
renderResult: &scriptorium.RenderResult{ExitCode: 0},
runResult: &scriptorium.RunResult{ExitCode: 0},
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" {
body = validTomorrowGeneratedTextJSON()
}
if req.PromptID == "weather.daily_generated_text" {
body = validDailyGeneratedTextJSON()
}
if err := os.WriteFile(req.OutputPath, []byte(body), 0o600); err != nil {
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) {
r.structuredRunCalls++
r.structuredRunRequest = req
if r.structuredRunBody != "" {
if err := os.WriteFile(req.OutputPath, []byte(r.structuredRunBody), 0o600); err != nil {
body := r.structuredRunBody
if body == "" {
body = validGeneratedTextJSONForPrompt(req.PromptID)
}
if body != "" {
if err := os.WriteFile(req.OutputPath, []byte(body), 0o600); err != nil {
return nil, err
}
}
@@ -2952,3 +2972,18 @@ func (r *recordingRenderer) StructuredRun(_ context.Context, req scriptorium.Str
}
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)
}
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)
}
if value.Location == nil || value.Location.Name != "Brentwood" {
@@ -395,7 +395,7 @@ func TestAreaForecastDiscussionModuleUsesHourlyDefaultSections(t *testing.T) {
func testModuleContext() ModuleContext {
generatedAt := mustParseModuleTime("2026-05-29T08:00:00-05:00")
definition := report.DefaultRegistry().MustLookup(report.DailyToday)
definition := report.DefaultRegistry().MustLookup(report.Daily)
resolved := report.Resolved{
Definition: definition,
GeneratedAt: generatedAt,

View File

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

View File

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

View File

@@ -77,7 +77,7 @@ func TestHourlyDefaultModuleOptions(t *testing.T) {
func TestModuleRegistryRejectsUnknownModule(t *testing.T) {
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"`) {
t.Fatalf("error = %v, want unknown module", err)
}
@@ -85,7 +85,7 @@ func TestModuleRegistryRejectsUnknownModule(t *testing.T) {
func TestModuleRegistryRejectsDuplicateModuleIDs(t *testing.T) {
registry := MustDefaultModuleRegistry()
err := registry.ValidateComposition(report.DailyToday, []module.ConfigItem{
err := registry.ValidateComposition(report.Daily, []module.ConfigItem{
{ID: module.Metadata},
{ID: module.Metadata},
})
@@ -106,8 +106,8 @@ func TestModuleRegistryRejectsDuplicateStanzaNames(t *testing.T) {
func TestModuleRegistryRejectsIncompatibleReports(t *testing.T) {
registry := MustDefaultModuleRegistry()
err := registry.ValidateComposition(report.DailyToday, []module.ConfigItem{{ID: module.TomorrowPlanning}})
if err == nil || !strings.Contains(err.Error(), `module "tomorrow_planning" is not compatible with report "daily_today"`) {
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"`) {
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 {
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) {
err := registry.ValidateComposition(id, []module.ConfigItem{{ID: module.TodayPlanning}})
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) {
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)
}
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) {
err := registry.ValidateComposition(id, []module.ConfigItem{{ID: module.DailyPlanning}})
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) {
registry := MustDefaultModuleRegistry()
err := registry.ValidateComposition(report.DailyToday, []module.ConfigItem{
err := registry.ValidateComposition(report.Daily, []module.ConfigItem{
{ID: module.Metadata, Options: module.CurrentConditionsOptions{}},
})
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) {
registry := MustDefaultModuleRegistry()
err := registry.ValidateComposition(report.DailyToday, []module.ConfigItem{
err := registry.ValidateComposition(report.Daily, []module.ConfigItem{
{ID: module.Metadata, Options: module.MetadataOptions{}},
{ID: module.CurrentConditions, Options: &module.CurrentConditionsOptions{}},
})

View File

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

View File

@@ -251,7 +251,7 @@ func TestBatchOutputIncludesNotificationDetails(t *testing.T) {
Failed: 1,
Reports: []app.BatchReportResult{
{
ReportID: "daily_today",
ReportID: "daily",
Status: "succeeded",
OutputPath: "/tmp/daily.md",
NotificationStatus: "accepted",
@@ -299,11 +299,11 @@ func TestBatchOutputDoesNotExposeSecretLikeNotificationErrors(t *testing.T) {
Failed: 1,
Reports: []app.BatchReportResult{
{
ReportID: "daily_today",
ReportID: "daily",
Status: "failed",
Error: "notify report daily_today: upload failed: [redacted]",
Error: "notify report daily: upload failed: [redacted]",
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 {
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))
}
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 {
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))
}
if !strings.Contains(string(data), "location:") ||
@@ -1299,6 +1299,21 @@ JSON
],
"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
printf 'wrote generated text\n' >&2
exit 0
@@ -1345,6 +1360,21 @@ if [ "$1" = "run" ]; then
"precipitation_timing": "A cold front is moving into the region.",
"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
printf 'wrote generated text\n' >&2
exit 0

View File

@@ -163,7 +163,7 @@ reports:
if err != nil {
t.Fatalf("ReportModuleOverrides() error = %v", err)
}
items := overrides[report.DailyToday]
items := overrides[report.Daily]
if len(items) != 6 {
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)
}
}
if _, ok := overrides[report.DailyToday]; ok {
t.Fatalf("daily override = %#v, want today override to stay distinct", overrides[report.DailyToday])
if _, ok := overrides[report.Daily]; ok {
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
}{
{
name: "DuplicateAlias",
name: "RetiredDailyReportID",
reports: map[string]ReportConfig{
"daily": {},
"daily_today": {},
},
wantErr: "duplicates report override",
wantErr: "reports.daily_today",
},
{
name: "TodayAndDailyAreDistinct",
@@ -425,7 +425,7 @@ reports:
deterministic_modules:
- tomorrow_planning
`,
wantErr: `not compatible with report "daily_today"`,
wantErr: `not compatible with report "daily"`,
},
{
name: "RemovedPlaceholderModule",
@@ -461,7 +461,7 @@ reports:
wantErr: "options are invalid",
},
{
name: "DuplicateReportAlias",
name: "RetiredDailyReportID",
yaml: `
reports:
daily:
@@ -471,7 +471,7 @@ reports:
deterministic_modules:
- current_conditions
`,
wantErr: "duplicates report override",
wantErr: "reports.daily_today",
},
{
name: "HourlyIncompatibleDailyModule",

View File

@@ -115,7 +115,7 @@ func BuildDerived(req BuildDerivedRequest) (DerivedFacts, error) {
switch req.Resolved.Definition.ID {
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)
if err != nil {
return DerivedFacts{}, err

View File

@@ -56,7 +56,7 @@ func TestBuildCollectedCopiesBundleFactsAndKeepsSourcesSeparate(t *testing.T) {
func TestBuildDerivedDailySlicesDaypartsAndAlerts(t *testing.T) {
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{
Resolved: resolved,
Timezone: location.String(),
@@ -253,7 +253,7 @@ func TestBuildDerivedSelectsSPCConvectiveOutlooksByValidPeriod(t *testing.T) {
}{
{
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"},
wantDiscussion: []string{"day1 early", "day1 late"},
},
@@ -322,7 +322,7 @@ func TestBuildDerivedUnsupportedReportReturnsActionableError(t *testing.T) {
func TestBuildDerivedSPCConvectiveOutlooksDistinguishesMissingAndCheckedEmpty(t *testing.T) {
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.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 {
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 {
t.Fatalf("resolve %s: %v", id, err)
}

View File

@@ -121,7 +121,7 @@ func TestCatalogLookupSupportsTodayDefinition(t *testing.T) {
func TestCatalogLookupSupportsDailyDefinitionAssets(t *testing.T) {
definition := report.Definition{
ID: report.ID("daily"),
ID: report.Daily,
GenerationMode: report.GenerationModeGeneratedTextTemplate,
GeneratedTextSchemaID: "daily",
TemplateID: "daily",
@@ -206,7 +206,7 @@ func TestCatalogValidationDispatchSupportsKnownSchemas(t *testing.T) {
}
dailyHandler, err := LookupDefinition(report.Definition{
ID: report.ID("daily"),
ID: report.Daily,
GenerationMode: report.GenerationModeGeneratedTextTemplate,
GeneratedTextSchemaID: "daily",
TemplateID: "daily",
@@ -281,7 +281,7 @@ func TestCatalogBuildRenderContextRejectsMismatchedGeneratedText(t *testing.T) {
}
dailyHandler, err := LookupDefinition(report.Definition{
ID: report.ID("daily"),
ID: report.Daily,
GenerationMode: report.GenerationModeGeneratedTextTemplate,
GeneratedTextSchemaID: "daily",
TemplateID: "daily",
@@ -303,7 +303,7 @@ func TestCatalogBuildRenderContextRejectsMismatchedGeneratedText(t *testing.T) {
func TestCatalogBuildRenderContextSupportsDaily(t *testing.T) {
handler, err := LookupDefinition(report.Definition{
ID: report.ID("daily"),
ID: report.Daily,
GenerationMode: report.GenerationModeGeneratedTextTemplate,
GeneratedTextSchemaID: "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" {
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)
}
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)
return briefing.Metadata{
RunID: "run-daily",
ReportID: report.ID("daily"),
ReportID: report.Daily,
PromptID: "weather.daily_generated_text",
GeneratedAt: generatedAt,
Units: "imperial",
@@ -763,7 +763,7 @@ func testDailySnapshot(t *testing.T) module.Snapshot {
StanzaName: string(module.Metadata),
Value: briefing.MetadataModule{
RunID: "run-daily",
ReportID: report.ID("daily"),
ReportID: report.Daily,
PromptID: "weather.daily_generated_text",
GeneratedAt: testDailyMetadata().GeneratedAt,
Units: "imperial",

View File

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

View File

@@ -19,11 +19,11 @@ func TestBuildDailyDataPackage(t *testing.T) {
if 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)
}
if pkg.Report.PromptID != "weather.daily_report" {
t.Fatalf("PromptID = %q, want weather.daily_report", pkg.Report.PromptID)
if pkg.Report.PromptID != "weather.daily_generated_text" {
t.Fatalf("PromptID = %q, want weather.daily_generated_text", pkg.Report.PromptID)
}
if pkg.Report.CurrentLocalDate != "2026-05-29" {
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) {
data := []byte(`
schema_version: weatherreporter.data_package.v2
run_id: 20260529T100000Z_daily_today
run_id: 20260529T100000Z_daily
report:
id: daily_today
prompt_id: weather.daily_report
id: daily
prompt_id: weather.daily_generated_text
generated_at: 2026-05-29T10:00:00Z
timezone: America/Chicago
current_local_date: "2026-05-29"
@@ -264,7 +264,7 @@ report:
end: 2026-05-30T05:00:00Z
briefing:
metadata:
run_id: 20260529T100000Z_daily_today
run_id: 20260529T100000Z_daily
raw_data:
alert_digest:
checked: true
@@ -283,10 +283,10 @@ func validBuildRequest(t *testing.T) BuildRequest {
generatedAt := time.Date(2026, 5, 29, 10, 0, 0, 0, time.UTC)
return BuildRequest{
Metadata: Metadata{
RunID: "20260529T100000Z_daily_today",
ReportID: report.DailyToday,
RunID: "20260529T100000Z_daily",
ReportID: report.Daily,
Variant: "today",
PromptID: "weather.daily_report",
PromptID: "weather.daily_generated_text",
GeneratedAt: generatedAt,
Timezone: "America/Chicago",
ValidPeriod: timeutil.Period{
@@ -295,7 +295,7 @@ func validBuildRequest(t *testing.T) BuildRequest {
},
},
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.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}},

View File

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

View File

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

View File

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

View File

@@ -14,11 +14,14 @@ func TestDailyValidPeriod(t *testing.T) {
location := mustLoadLocation(t)
now := mustParse("2026-05-29T17:45:00-05:00")
resolved, err := Resolve(DailyToday, ResolveRequest{Now: now, Location: location})
if err != nil {
t.Fatalf("Resolve() error = %v", err)
resolved, err := Resolve(Daily, ResolveRequest{Now: now, Location: location})
if err == nil {
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) {
@@ -26,11 +29,23 @@ func TestDailyValidPeriodCanUseExplicitDate(t *testing.T) {
now := mustParse("2026-05-29T17:45: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 {
t.Fatalf("Resolve() error = %v", err)
}
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) {
@@ -256,7 +271,7 @@ func TestEveningBatchIncludesTomorrow(t *testing.T) {
}
}
func TestBatchesDoNotIncludeHourly(t *testing.T) {
func TestBatchesDoNotIncludeManualReports(t *testing.T) {
location := mustLoadLocation(t)
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)
}
for _, resolved := range append(morning, evening...) {
if resolved.Definition.ID == Hourly {
t.Fatalf("batch included %q, want hourly excluded", resolved.Definition.ID)
if resolved.Definition.ID == Hourly || resolved.Definition.ID == Daily {
t.Fatalf("batch included %q, want manual reports excluded", resolved.Definition.ID)
}
}
}
@@ -280,7 +295,7 @@ func TestIDForCommandName(t *testing.T) {
name string
want ID
}{
{name: "daily", want: DailyToday},
{name: "daily", want: Daily},
{name: "today", want: Today},
{name: "tomorrow", want: Tomorrow},
{name: "hourly", want: Hourly},
@@ -323,8 +338,7 @@ func TestIDForConfigKey(t *testing.T) {
key string
want ID
}{
{key: "daily", want: DailyToday},
{key: "daily_today", want: DailyToday},
{key: "daily", want: Daily},
{key: "today", want: Today},
{key: "tomorrow", want: Tomorrow},
{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`) {
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")
if err != nil {
t.Fatalf("IDForConfigKey(daily) error = %v", err)
@@ -418,7 +435,7 @@ func TestRegistryLookupErrorIsActionable(t *testing.T) {
func TestRegistryAllIncludesHourlyInStableOrder(t *testing.T) {
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, ",") {
t.Fatalf("All() ids = %#v, want %#v", ids, want)
}
@@ -443,7 +460,7 @@ func TestRegistryDefinitionsDeclareGenerationMetadata(t *testing.T) {
if !definition.Generated {
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)
if 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
}{
{
id: DailyToday,
id: Daily,
artifactGroup: "daily",
batchOutputName: "daily.md",
generated: true,
compatiblePriorIDs: []ID{DailyToday},
compatiblePriorIDs: []ID{Daily},
comparisonStrategy: CompareSameValidDate,
},
{
@@ -572,7 +589,7 @@ func TestRegistryDefinitionsDeclareDefaultModules(t *testing.T) {
want []module.ID
}{
{
id: DailyToday,
id: Daily,
want: []module.ID{
module.Metadata,
module.CurrentConditions,
@@ -586,6 +603,7 @@ func TestRegistryDefinitionsDeclareDefaultModules(t *testing.T) {
module.SPCConvectiveDiscussion,
module.WeatherStory,
module.OutdoorWindows,
module.DailyPlanning,
module.HourlyForecast,
},
},
@@ -703,7 +721,7 @@ func TestRegistryDefinitionsDeclareDefaultModules(t *testing.T) {
func TestRegistryAppliesModuleOverridesWithoutChangingDefaults(t *testing.T) {
base := DefaultRegistry()
overridden, err := base.WithModuleOverrides(map[ID][]module.ConfigItem{
DailyToday: {
Daily: {
{ID: module.Metadata},
{ID: module.AlertDigest},
},
@@ -712,7 +730,7 @@ func TestRegistryAppliesModuleOverridesWithoutChangingDefaults(t *testing.T) {
t.Fatalf("WithModuleOverrides() error = %v", err)
}
definition, err := overridden.Lookup(DailyToday)
definition, err := overridden.Lookup(Daily)
if err != nil {
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())
}
defaultDefinition, err := base.Lookup(DailyToday)
defaultDefinition, err := base.Lookup(Daily)
if err != nil {
t.Fatalf("Lookup(default) error = %v", err)
}
@@ -743,18 +761,22 @@ func TestRegistryRejectsModuleOverrideForUnknownReport(t *testing.T) {
func TestResolvedMetadata(t *testing.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 {
t.Fatalf("Resolve() error = %v", err)
}
metadata := resolved.Metadata()
if metadata.ReportID != DailyToday {
t.Fatalf("ReportID = %q, want daily_today", metadata.ReportID)
if metadata.ReportID != Daily {
t.Fatalf("ReportID = %q, want daily", metadata.ReportID)
}
if metadata.PromptID != "weather.daily_report" {
t.Fatalf("PromptID = %q, want weather.daily_report", metadata.PromptID)
if metadata.PromptID != "weather.daily_generated_text" {
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)
}
}

View File

@@ -12,7 +12,7 @@ type Registry struct {
func DefaultRegistry() Registry {
definitions := []Definition{
dailyTodayDefinition(),
dailyDefinition(),
todayDefinition(),
tomorrowDefinition(),
hourlyDefinition(),
@@ -75,7 +75,7 @@ func (r Registry) MustLookup(id ID) 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))
for _, id := range ids {
if definition, ok := r.definitions[id]; ok {

View File

@@ -27,12 +27,12 @@ func TestPathsUseRunIDAndWorkspace(t *testing.T) {
}
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_today.metadata.json"),
filepath.Join("data-packages", "daily", "2026-05-29", "20260529T100000.000000000Z_daily_today.data_package.yaml"),
filepath.Join("preflight", "daily", "2026-05-29", "20260529T100000.000000000Z_daily_today.render.json"),
filepath.Join("notifications", "daily", "2026-05-29", "20260529T100000.000000000Z_daily_today.distributor.json"),
filepath.Join("reports", "daily", "20260529T100000.000000000Z_daily_today.md"),
filepath.Join("snapshots", "daily", "2026-05-29", "20260529T100000.000000000Z_daily.modules.json"),
filepath.Join("snapshots", "daily", "2026-05-29", "20260529T100000.000000000Z_daily.metadata.json"),
filepath.Join("data-packages", "daily", "2026-05-29", "20260529T100000.000000000Z_daily.data_package.yaml"),
filepath.Join("preflight", "daily", "2026-05-29", "20260529T100000.000000000Z_daily.render.json"),
filepath.Join("notifications", "daily", "2026-05-29", "20260529T100000.000000000Z_daily.distributor.json"),
filepath.Join("reports", "daily", "20260529T100000.000000000Z_daily.md"),
} {
if !strings.Contains(pathsString(paths), want) {
t.Fatalf("paths = %#v, want component %q", paths, want)
@@ -49,6 +49,13 @@ func TestGeneratedTextArtifactPathsUseSnapshotTree(t *testing.T) {
validDate 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",
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") {
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) {
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 {
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,
Location: location,
Date: now,
})
if err != nil {
t.Fatalf("Resolve() error = %v", err)