Add Today report to morning batch
This commit is contained in:
@@ -134,7 +134,7 @@ failure context.
|
|||||||
|
|
||||||
## Batch Workflow
|
## Batch Workflow
|
||||||
|
|
||||||
`run morning` resolves Daily Today, 3-Day Outlook, and Weekend Outlook except
|
`run morning` resolves Today Report, 3-Day Outlook, and Weekend Outlook except
|
||||||
on Sunday. `run evening` resolves Tomorrow Report. Batch output copy names come
|
on Sunday. `run evening` resolves Tomorrow Report. Batch output copy names come
|
||||||
from report definitions. Batch generation continues independent reports after a
|
from report definitions. Batch generation continues independent reports after a
|
||||||
failure, records each result, writes compact status lines to stderr, emits a
|
failure, records each result, writes compact status lines to stderr, emits a
|
||||||
|
|||||||
@@ -28,22 +28,23 @@ Each report definition declares:
|
|||||||
- default ordered module composition
|
- default ordered module composition
|
||||||
|
|
||||||
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`, `tomorrow`, `hourly`, `three-day`,
|
The generate command names are `daily`, `today`, `tomorrow`, `hourly`,
|
||||||
`weekend`, and `storm`. Config keys also accept underscore and legacy
|
`three-day`, `weekend`, and `storm`. Config keys also accept underscore and
|
||||||
descriptive aliases such as `daily_today`, `three_day_outlook`,
|
legacy descriptive aliases such as `daily_today`, `three_day_outlook`,
|
||||||
`weekend_outlook`, and `storm_report`.
|
`weekend_outlook`, and `storm_report`.
|
||||||
|
|
||||||
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. Tomorrow
|
Their template and structured-text schema identifiers are empty. Today Report,
|
||||||
Report and Hourly Report declare `generated_text_template`; the app uses their
|
Tomorrow Report, and Hourly Report declare `generated_text_template`; the app
|
||||||
template and schema identifiers to validate generated text and render embedded
|
uses their template and schema identifiers to validate generated text and render
|
||||||
Markdown templates.
|
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 Today | `daily_today` | `weather.daily_report` | `scriptorium_markdown` | `daily` | `daily.md` | Daily Today |
|
||||||
|
| 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 |
|
||||||
| 3-Day Outlook | `three_day` | `weather.three_day_outlook` | `scriptorium_markdown` | `three-day` | `three-day.md` | 3-Day Outlook |
|
| 3-Day Outlook | `three_day` | `weather.three_day_outlook` | `scriptorium_markdown` | `three-day` | `three-day.md` | 3-Day Outlook |
|
||||||
@@ -56,6 +57,8 @@ All report definitions are eligible for generation.
|
|||||||
|
|
||||||
- Daily Today covers the selected local civil day, or the current local civil
|
- Daily Today 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.
|
||||||
|
- 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.
|
- Tomorrow Report covers the next local civil day from generation time.
|
||||||
- Hourly Report covers the half-open six-hour period from generation time in
|
- Hourly Report covers the half-open six-hour period from generation time in
|
||||||
the effective report timezone. The duration is an internal report constant,
|
the effective report timezone. The duration is an internal report constant,
|
||||||
@@ -84,15 +87,15 @@ 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 `tomorrow` and `hourly`, canonical
|
module overrides can use short keys such as `today`, `tomorrow`, and `hourly`,
|
||||||
report IDs such as `daily_today`, or accepted aliases such as
|
canonical report IDs such as `daily_today`, or accepted aliases such as
|
||||||
`three_day_outlook`.
|
`three_day_outlook`.
|
||||||
|
|
||||||
## Batch Membership
|
## Batch Membership
|
||||||
|
|
||||||
Morning batches include Daily Today, 3-Day Outlook, and Weekend Outlook except
|
Morning batches include Today Report, 3-Day Outlook, and Weekend Outlook
|
||||||
on Sunday. Evening batches include Tomorrow Report. Hourly Report is not part
|
except on Sunday. Evening batches include Tomorrow Report. Daily Today and
|
||||||
of a scheduled batch.
|
Hourly Report are not part of a scheduled batch.
|
||||||
|
|
||||||
## State And App Usage
|
## State And App Usage
|
||||||
|
|
||||||
|
|||||||
@@ -93,6 +93,8 @@ current report definition.
|
|||||||
|
|
||||||
- Daily Today compares with prior Daily Today snapshots for the same valid
|
- Daily Today compares with prior Daily Today snapshots for the same valid
|
||||||
local date.
|
local date.
|
||||||
|
- Today Report compares with prior Today Report snapshots for the same valid
|
||||||
|
local date.
|
||||||
- Tomorrow Report compares with prior Tomorrow Report snapshots for the same
|
- Tomorrow Report compares with prior Tomorrow Report snapshots for the same
|
||||||
valid local date.
|
valid local date.
|
||||||
- 3-Day Outlook compares with prior 3-Day snapshots for the same valid local
|
- 3-Day Outlook compares with prior 3-Day snapshots for the same valid local
|
||||||
|
|||||||
@@ -1106,7 +1106,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.Tomorrow:
|
case report.DailyToday, 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)
|
||||||
|
|||||||
@@ -1338,6 +1338,93 @@ func TestGenerateReportIncludesRecentChangesFromPriorSnapshot(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestGenerateTodayReportUsesTodayIdentityAndRecentChanges(t *testing.T) {
|
||||||
|
server := dailyBundleServer(t)
|
||||||
|
cfg := config.Defaults()
|
||||||
|
cfg.WeatherAPI.BaseURL = server.URL + "/"
|
||||||
|
cfg.WeatherAPI.Timezone = "America/Chicago"
|
||||||
|
cfg.Workspace.Root = t.TempDir()
|
||||||
|
store, err := state.NewFilesystemStore(cfg.Workspace)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("NewFilesystemStore() error = %v", err)
|
||||||
|
}
|
||||||
|
priorResolved, err := ResolveGenerate(GenerateRequest{
|
||||||
|
Config: cfg,
|
||||||
|
Report: ReportKind(report.CommandNameToday),
|
||||||
|
Date: mustParse("2026-05-29T12:00:00-05:00"),
|
||||||
|
}, mustParse("2026-05-29T04:00:00-05:00"))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("ResolveGenerate(prior) error = %v", err)
|
||||||
|
}
|
||||||
|
savePriorRun(t, store, priorResolved, priorDailyModuleSnapshot(t, priorResolved))
|
||||||
|
|
||||||
|
currentResolved, err := ResolveGenerate(GenerateRequest{
|
||||||
|
Config: cfg,
|
||||||
|
Report: ReportKind(report.CommandNameToday),
|
||||||
|
Date: mustParse("2026-05-29T12:00:00-05:00"),
|
||||||
|
}, mustParse("2026-05-29T05:00:00-05:00"))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("ResolveGenerate(current) error = %v", err)
|
||||||
|
}
|
||||||
|
renderer := &recordingRenderer{
|
||||||
|
renderResult: &scriptorium.RenderResult{ExitCode: 0},
|
||||||
|
structuredRunResult: &scriptorium.StructuredRunResult{ExitCode: 0},
|
||||||
|
structuredRunBody: validTodayGeneratedTextJSON(),
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := GenerateReport(context.Background(), ReportRequest{
|
||||||
|
Config: cfg,
|
||||||
|
Resolved: currentResolved,
|
||||||
|
Renderer: renderer,
|
||||||
|
Store: store,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("GenerateReport() error = %v", err)
|
||||||
|
}
|
||||||
|
if renderer.runCalls != 0 || renderer.structuredRunCalls != 1 {
|
||||||
|
t.Fatalf("renderer calls run=%d structured=%d, want generated-text flow", renderer.runCalls, renderer.structuredRunCalls)
|
||||||
|
}
|
||||||
|
if result.Metadata.ReportID != report.Today || result.Metadata.Variant != "today" || result.Metadata.GeneratedTextSchemaID != "today" {
|
||||||
|
t.Fatalf("metadata = %#v, want Today generated-text metadata", result.Metadata)
|
||||||
|
}
|
||||||
|
if !strings.Contains(result.Metadata.RunID, "_today") {
|
||||||
|
t.Fatalf("RunID = %q, want Today report ID suffix", result.Metadata.RunID)
|
||||||
|
}
|
||||||
|
if !strings.Contains(result.DataPackagePath, filepath.Join("data-packages", "today", "2026-05-29")) {
|
||||||
|
t.Fatalf("DataPackagePath = %q, want Today artifact group", result.DataPackagePath)
|
||||||
|
}
|
||||||
|
if !strings.Contains(result.ReportPath, filepath.Join("reports", "today")) {
|
||||||
|
t.Fatalf("ReportPath = %q, want Today report group", result.ReportPath)
|
||||||
|
}
|
||||||
|
if _, ok := result.ModuleSnapshot.LookupStanza("today_planning"); !ok {
|
||||||
|
t.Fatal("today_planning stanza missing")
|
||||||
|
}
|
||||||
|
if _, ok := result.ModuleSnapshot.LookupStanza("tomorrow_planning"); ok {
|
||||||
|
t.Fatal("tomorrow_planning stanza present, want Today-specific planning")
|
||||||
|
}
|
||||||
|
if result.PriorSnapshot == nil || len(result.RecentChanges) == 0 {
|
||||||
|
t.Fatalf("prior=%#v recentChanges=%#v, want Today daily comparison changes", result.PriorSnapshot, result.RecentChanges)
|
||||||
|
}
|
||||||
|
data, err := os.ReadFile(result.DataPackagePath)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("read data package: %v", err)
|
||||||
|
}
|
||||||
|
for _, want := range []string{"id: today", "prompt_id: weather.today_generated_text", "today_planning:", "recent_changes:"} {
|
||||||
|
if !strings.Contains(string(data), want) {
|
||||||
|
t.Fatalf("data package missing %q:\n%s", want, string(data))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
reportData, err := os.ReadFile(result.ReportPath)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("read report: %v", err)
|
||||||
|
}
|
||||||
|
for _, want := range []string{"# Today's Weather", "Today starts with showers before improving."} {
|
||||||
|
if !strings.Contains(string(reportData), want) {
|
||||||
|
t.Fatalf("today report missing %q:\n%s", want, string(reportData))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestGenerateTomorrowReportUsesTomorrowBriefingDate(t *testing.T) {
|
func TestGenerateTomorrowReportUsesTomorrowBriefingDate(t *testing.T) {
|
||||||
server := dailyBundleServer(t)
|
server := dailyBundleServer(t)
|
||||||
cfg := config.Defaults()
|
cfg := config.Defaults()
|
||||||
@@ -2072,8 +2159,8 @@ func TestRunBatchContinuesAfterReportFailure(t *testing.T) {
|
|||||||
if result.Total != 3 || result.Succeeded != 2 || result.Failed != 1 {
|
if result.Total != 3 || result.Succeeded != 2 || result.Failed != 1 {
|
||||||
t.Fatalf("summary total/succeeded/failed = %d/%d/%d, want 3/2/1", result.Total, result.Succeeded, result.Failed)
|
t.Fatalf("summary total/succeeded/failed = %d/%d/%d, want 3/2/1", result.Total, result.Succeeded, result.Failed)
|
||||||
}
|
}
|
||||||
if renderer.runCalls != 2 {
|
if renderer.runCalls != 1 || renderer.structuredRunCalls != 1 {
|
||||||
t.Fatalf("run calls = %d, want successful reports to continue", renderer.runCalls)
|
t.Fatalf("renderer calls run=%d structured=%d, want successful reports to continue", renderer.runCalls, renderer.structuredRunCalls)
|
||||||
}
|
}
|
||||||
var failedThreeDay bool
|
var failedThreeDay bool
|
||||||
for _, item := range result.Reports {
|
for _, item := range result.Reports {
|
||||||
@@ -2316,6 +2403,10 @@ func validTomorrowGeneratedTextJSON() string {
|
|||||||
return `{"summary":"Tomorrow 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":"Tomorrow 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 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 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()
|
||||||
@@ -2671,6 +2762,9 @@ func (r *selectiveRenderer) Run(_ context.Context, req scriptorium.RunRequest) (
|
|||||||
func (r *selectiveRenderer) StructuredRun(_ context.Context, req scriptorium.StructuredRunRequest) (*scriptorium.StructuredRunResult, error) {
|
func (r *selectiveRenderer) StructuredRun(_ context.Context, req scriptorium.StructuredRunRequest) (*scriptorium.StructuredRunResult, error) {
|
||||||
r.structuredRunCalls++
|
r.structuredRunCalls++
|
||||||
body := validHourlyGeneratedTextJSON()
|
body := validHourlyGeneratedTextJSON()
|
||||||
|
if req.PromptID == "weather.today_generated_text" {
|
||||||
|
body = validTodayGeneratedTextJSON()
|
||||||
|
}
|
||||||
if req.PromptID == "weather.tomorrow_generated_text" {
|
if req.PromptID == "weather.tomorrow_generated_text" {
|
||||||
body = validTomorrowGeneratedTextJSON()
|
body = validTomorrowGeneratedTextJSON()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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:
|
case report.DailyToday, report.Today:
|
||||||
return "today"
|
return "today"
|
||||||
case report.Tomorrow:
|
case report.Tomorrow:
|
||||||
return "tomorrow"
|
return "tomorrow"
|
||||||
|
|||||||
@@ -196,7 +196,15 @@ func TestRunMorningIncludesWeekendExceptSunday(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Run() error = %v", err)
|
t.Fatalf("Run() error = %v", err)
|
||||||
}
|
}
|
||||||
|
_ = oneArtifact(t, workspaceRoot, "data-packages", "today", "2026-05-29", "*.data_package.yaml")
|
||||||
_ = oneArtifact(t, workspaceRoot, "data-packages", "weekend", "2026-05-29", "*.data_package.yaml")
|
_ = oneArtifact(t, workspaceRoot, "data-packages", "weekend", "2026-05-29", "*.data_package.yaml")
|
||||||
|
dailyPackages, err := filepath.Glob(filepath.Join(workspaceRoot, "data-packages", "daily", "2026-05-29", "*.data_package.yaml"))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("glob daily packages: %v", err)
|
||||||
|
}
|
||||||
|
if len(dailyPackages) != 0 {
|
||||||
|
t.Fatalf("daily packages = %#v, want morning batch to use Today only", dailyPackages)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestRunMorningReportsPartialFailureAndContinues(t *testing.T) {
|
func TestRunMorningReportsPartialFailureAndContinues(t *testing.T) {
|
||||||
@@ -228,7 +236,7 @@ func TestRunMorningReportsPartialFailureAndContinues(t *testing.T) {
|
|||||||
if !strings.Contains(output.stderr, "status=failed") || !strings.Contains(output.stderr, "status=succeeded") {
|
if !strings.Contains(output.stderr, "status=failed") || !strings.Contains(output.stderr, "status=succeeded") {
|
||||||
t.Fatalf("stderr missing structured report logs:\n%s", output.stderr)
|
t.Fatalf("stderr missing structured report logs:\n%s", output.stderr)
|
||||||
}
|
}
|
||||||
_ = oneArtifact(t, workspaceRoot, "data-packages", "daily", "2026-05-29", "*.data_package.yaml")
|
_ = oneArtifact(t, workspaceRoot, "data-packages", "today", "2026-05-29", "*.data_package.yaml")
|
||||||
_ = oneArtifact(t, workspaceRoot, "data-packages", "weekend", "2026-05-29", "*.data_package.yaml")
|
_ = oneArtifact(t, workspaceRoot, "data-packages", "weekend", "2026-05-29", "*.data_package.yaml")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -448,7 +456,7 @@ func TestRunEveningReportsNotificationFailureWithoutToken(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestRunMorningGeneratesDailyAndThreeDayOnSunday(t *testing.T) {
|
func TestRunMorningGeneratesTodayAndThreeDayOnSunday(t *testing.T) {
|
||||||
server := dailyServer(t)
|
server := dailyServer(t)
|
||||||
tempDir := t.TempDir()
|
tempDir := t.TempDir()
|
||||||
scriptoriumPath := writeFakeScriptorium(t, tempDir)
|
scriptoriumPath := writeFakeScriptorium(t, tempDir)
|
||||||
@@ -469,16 +477,20 @@ func TestRunMorningGeneratesDailyAndThreeDayOnSunday(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Run() error = %v", err)
|
t.Fatalf("Run() error = %v", err)
|
||||||
}
|
}
|
||||||
dailyPackages, err := filepath.Glob(filepath.Join(workspaceRoot, "data-packages", "daily", "2026-05-31", "*.data_package.yaml"))
|
todayPackages, err := filepath.Glob(filepath.Join(workspaceRoot, "data-packages", "today", "2026-05-31", "*.data_package.yaml"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("glob daily packages: %v", err)
|
t.Fatalf("glob today packages: %v", err)
|
||||||
}
|
}
|
||||||
threeDayPackages, err := filepath.Glob(filepath.Join(workspaceRoot, "data-packages", "three-day", "2026-05-31", "*.data_package.yaml"))
|
threeDayPackages, err := filepath.Glob(filepath.Join(workspaceRoot, "data-packages", "three-day", "2026-05-31", "*.data_package.yaml"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("glob 3-day packages: %v", err)
|
t.Fatalf("glob 3-day packages: %v", err)
|
||||||
}
|
}
|
||||||
if len(dailyPackages) != 1 || len(threeDayPackages) != 1 {
|
dailyPackages, err := filepath.Glob(filepath.Join(workspaceRoot, "data-packages", "daily", "2026-05-31", "*.data_package.yaml"))
|
||||||
t.Fatalf("daily packages = %#v, 3-day packages = %#v; want one each", dailyPackages, threeDayPackages)
|
if err != nil {
|
||||||
|
t.Fatalf("glob daily packages: %v", err)
|
||||||
|
}
|
||||||
|
if len(todayPackages) != 1 || len(threeDayPackages) != 1 || len(dailyPackages) != 0 {
|
||||||
|
t.Fatalf("today packages = %#v, 3-day packages = %#v, daily packages = %#v; want Today and 3-day only", todayPackages, threeDayPackages, dailyPackages)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1107,6 +1119,20 @@ if [ "$1" = "run" ]; then
|
|||||||
fi
|
fi
|
||||||
shift
|
shift
|
||||||
done
|
done
|
||||||
|
if [ "$prompt" = "weather.today_generated_text" ]; then
|
||||||
|
cat > "$out" <<'JSON'
|
||||||
|
{
|
||||||
|
"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."
|
||||||
|
}
|
||||||
|
JSON
|
||||||
|
printf 'wrote generated text\n' >&2
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
if [ "$prompt" = "weather.tomorrow_generated_text" ]; then
|
if [ "$prompt" = "weather.tomorrow_generated_text" ]; then
|
||||||
cat > "$out" <<'JSON'
|
cat > "$out" <<'JSON'
|
||||||
{
|
{
|
||||||
@@ -1163,6 +1189,20 @@ 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.today_generated_text" ]; then
|
||||||
|
cat > "$out" <<'JSON'
|
||||||
|
{
|
||||||
|
"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."
|
||||||
|
}
|
||||||
JSON
|
JSON
|
||||||
printf 'wrote generated text\n' >&2
|
printf 'wrote generated text\n' >&2
|
||||||
exit 0
|
exit 0
|
||||||
@@ -1216,13 +1256,30 @@ if [ "$1" = "render" ]; then
|
|||||||
fi
|
fi
|
||||||
if [ "$1" = "run" ]; then
|
if [ "$1" = "run" ]; then
|
||||||
out=""
|
out=""
|
||||||
|
prompt=""
|
||||||
while [ "$#" -gt 0 ]; do
|
while [ "$#" -gt 0 ]; do
|
||||||
if [ "$1" = "--out" ]; then
|
if [ "$1" = "--out" ]; then
|
||||||
shift
|
shift
|
||||||
out="$1"
|
out="$1"
|
||||||
|
elif [ "$1" = "--prompt" ]; then
|
||||||
|
shift
|
||||||
|
prompt="$1"
|
||||||
fi
|
fi
|
||||||
shift
|
shift
|
||||||
done
|
done
|
||||||
|
if [ "$prompt" = "weather.today_generated_text" ]; then
|
||||||
|
cat > "$out" <<'JSON'
|
||||||
|
{
|
||||||
|
"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."
|
||||||
|
}
|
||||||
|
JSON
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
printf '# Batch Report\n\nGenerated by fake scriptorium.\n' > "$out"
|
printf '# Batch Report\n\nGenerated by fake scriptorium.\n' > "$out"
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
|
|||||||
@@ -184,6 +184,47 @@ reports:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestLoadTodayReportModuleOverrides(t *testing.T) {
|
||||||
|
path := writeConfig(t, `
|
||||||
|
reports:
|
||||||
|
today:
|
||||||
|
deterministic_modules:
|
||||||
|
- metadata
|
||||||
|
- current_conditions
|
||||||
|
- derived_daily_summary
|
||||||
|
- derived_daypart_summaries
|
||||||
|
- today_planning
|
||||||
|
`)
|
||||||
|
|
||||||
|
cfg, err := LoadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("LoadFile() error = %v", err)
|
||||||
|
}
|
||||||
|
overrides, err := cfg.ReportModuleOverrides()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("ReportModuleOverrides() error = %v", err)
|
||||||
|
}
|
||||||
|
items := overrides[report.Today]
|
||||||
|
if len(items) != 5 {
|
||||||
|
t.Fatalf("today override length = %d, want 5", len(items))
|
||||||
|
}
|
||||||
|
want := []module.ID{
|
||||||
|
module.Metadata,
|
||||||
|
module.CurrentConditions,
|
||||||
|
module.DerivedDailySummary,
|
||||||
|
module.DerivedDaypartSummaries,
|
||||||
|
module.TodayPlanning,
|
||||||
|
}
|
||||||
|
for i, id := range want {
|
||||||
|
if items[i].ID != id {
|
||||||
|
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])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestLoadHourlyReportModuleOverrides(t *testing.T) {
|
func TestLoadHourlyReportModuleOverrides(t *testing.T) {
|
||||||
path := writeConfig(t, `
|
path := writeConfig(t, `
|
||||||
reports:
|
reports:
|
||||||
@@ -286,6 +327,13 @@ func TestValidateReportModuleAliasesDirectly(t *testing.T) {
|
|||||||
},
|
},
|
||||||
wantErr: "duplicates report override",
|
wantErr: "duplicates report override",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "TodayAndDailyAreDistinct",
|
||||||
|
reports: map[string]ReportConfig{
|
||||||
|
"daily": {},
|
||||||
|
"today": {},
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "UnknownReport",
|
name: "UnknownReport",
|
||||||
reports: map[string]ReportConfig{
|
reports: map[string]ReportConfig{
|
||||||
@@ -299,6 +347,12 @@ func TestValidateReportModuleAliasesDirectly(t *testing.T) {
|
|||||||
cfg := Defaults()
|
cfg := Defaults()
|
||||||
cfg.Reports = tt.reports
|
cfg.Reports = tt.reports
|
||||||
err := Validate(cfg)
|
err := Validate(cfg)
|
||||||
|
if tt.wantErr == "" {
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Validate() error = %v", err)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
if err == nil {
|
if err == nil {
|
||||||
t.Fatal("Validate() error = nil, want report key error")
|
t.Fatal("Validate() error = nil, want report key error")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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.Tomorrow:
|
case report.DailyToday, 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
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
|
|
||||||
const (
|
const (
|
||||||
CommandNameDaily = "daily"
|
CommandNameDaily = "daily"
|
||||||
|
CommandNameToday = "today"
|
||||||
CommandNameTomorrow = "tomorrow"
|
CommandNameTomorrow = "tomorrow"
|
||||||
CommandNameHourly = "hourly"
|
CommandNameHourly = "hourly"
|
||||||
CommandNameThreeDay = "three-day"
|
CommandNameThreeDay = "three-day"
|
||||||
@@ -21,6 +22,8 @@ func IDForCommandName(name string) (ID, error) {
|
|||||||
switch name {
|
switch name {
|
||||||
case CommandNameDaily:
|
case CommandNameDaily:
|
||||||
return DailyToday, nil
|
return DailyToday, nil
|
||||||
|
case CommandNameToday:
|
||||||
|
return Today, nil
|
||||||
case CommandNameTomorrow:
|
case CommandNameTomorrow:
|
||||||
return Tomorrow, nil
|
return Tomorrow, nil
|
||||||
case CommandNameHourly:
|
case CommandNameHourly:
|
||||||
@@ -39,6 +42,7 @@ func IDForCommandName(name string) (ID, error) {
|
|||||||
func CommandNames() []string {
|
func CommandNames() []string {
|
||||||
return []string{
|
return []string{
|
||||||
CommandNameDaily,
|
CommandNameDaily,
|
||||||
|
CommandNameToday,
|
||||||
CommandNameTomorrow,
|
CommandNameTomorrow,
|
||||||
CommandNameHourly,
|
CommandNameHourly,
|
||||||
CommandNameThreeDay,
|
CommandNameThreeDay,
|
||||||
@@ -52,6 +56,8 @@ func IDForConfigKey(key string) (ID, error) {
|
|||||||
switch normalized {
|
switch normalized {
|
||||||
case "daily", "daily_today":
|
case "daily", "daily_today":
|
||||||
return DailyToday, nil
|
return DailyToday, nil
|
||||||
|
case "today":
|
||||||
|
return Today, nil
|
||||||
case "tomorrow":
|
case "tomorrow":
|
||||||
return Tomorrow, nil
|
return Tomorrow, nil
|
||||||
case "hourly":
|
case "hourly":
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ func (r Registry) BatchReports(batch Batch, req ResolveRequest) ([]Resolved, err
|
|||||||
}
|
}
|
||||||
switch batch {
|
switch batch {
|
||||||
case Morning:
|
case Morning:
|
||||||
ids := []ID{DailyToday, ThreeDay}
|
ids := []ID{Today, ThreeDay}
|
||||||
if req.Now.In(req.Location).Weekday() != time.Sunday {
|
if req.Now.In(req.Location).Weekday() != time.Sunday {
|
||||||
ids = append(ids, Weekend)
|
ids = append(ids, Weekend)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,6 +33,41 @@ func TestDailyValidPeriodCanUseExplicitDate(t *testing.T) {
|
|||||||
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")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestTodayValidPeriod(t *testing.T) {
|
||||||
|
location := mustLoadLocation(t)
|
||||||
|
now := mustParse("2026-05-29T17:45:00-05:00")
|
||||||
|
|
||||||
|
resolved, err := Resolve(Today, ResolveRequest{Now: now, Location: location})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Resolve() error = %v", err)
|
||||||
|
}
|
||||||
|
assertPeriod(t, resolved.ValidPeriod, "2026-05-29T00:00:00-05:00", "2026-05-30T00:00:00-05:00")
|
||||||
|
if resolved.Definition.PromptID != "weather.today_generated_text" {
|
||||||
|
t.Fatalf("PromptID = %q, want weather.today_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 != "today" {
|
||||||
|
t.Fatalf("TemplateID = %q, want today", resolved.Definition.TemplateID)
|
||||||
|
}
|
||||||
|
if resolved.Definition.GeneratedTextSchemaID != "today" {
|
||||||
|
t.Fatalf("GeneratedTextSchemaID = %q, want today", resolved.Definition.GeneratedTextSchemaID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTodayValidPeriodCanUseExplicitDate(t *testing.T) {
|
||||||
|
location := mustLoadLocation(t)
|
||||||
|
now := mustParse("2026-05-29T17:45:00-05:00")
|
||||||
|
date := mustParse("2026-05-31T12:00:00-05:00")
|
||||||
|
|
||||||
|
resolved, err := Resolve(Today, 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")
|
||||||
|
}
|
||||||
|
|
||||||
func TestTomorrowValidPeriodFromEveningGeneration(t *testing.T) {
|
func TestTomorrowValidPeriodFromEveningGeneration(t *testing.T) {
|
||||||
location := mustLoadLocation(t)
|
location := mustLoadLocation(t)
|
||||||
now := mustParse("2026-05-29T20:00:00-05:00")
|
now := mustParse("2026-05-29T20:00:00-05:00")
|
||||||
@@ -201,8 +236,8 @@ func TestMorningBatchSkipsWeekendOnSunday(t *testing.T) {
|
|||||||
t.Fatalf("BatchReports() error = %v", err)
|
t.Fatalf("BatchReports() error = %v", err)
|
||||||
}
|
}
|
||||||
ids := resolvedIDs(resolved)
|
ids := resolvedIDs(resolved)
|
||||||
if strings.Join(ids, ",") != "daily_today,three_day" {
|
if strings.Join(ids, ",") != "today,three_day" {
|
||||||
t.Fatalf("ids = %v, want daily_today and three_day", ids)
|
t.Fatalf("ids = %v, want today and three_day", ids)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -246,6 +281,7 @@ func TestIDForCommandName(t *testing.T) {
|
|||||||
want ID
|
want ID
|
||||||
}{
|
}{
|
||||||
{name: "daily", want: DailyToday},
|
{name: "daily", want: DailyToday},
|
||||||
|
{name: "today", want: Today},
|
||||||
{name: "tomorrow", want: Tomorrow},
|
{name: "tomorrow", want: Tomorrow},
|
||||||
{name: "hourly", want: Hourly},
|
{name: "hourly", want: Hourly},
|
||||||
{name: "three-day", want: ThreeDay},
|
{name: "three-day", want: ThreeDay},
|
||||||
@@ -263,9 +299,20 @@ func TestIDForCommandName(t *testing.T) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
if names := strings.Join(CommandNames(), ","); names != "daily,tomorrow,hourly,three-day,weekend,storm" {
|
if names := strings.Join(CommandNames(), ","); names != "daily,today,tomorrow,hourly,three-day,weekend,storm" {
|
||||||
t.Fatalf("CommandNames() = %s, want stable command names", names)
|
t.Fatalf("CommandNames() = %s, want stable command names", names)
|
||||||
}
|
}
|
||||||
|
dailyID, err := IDForCommandName("daily")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("IDForCommandName(daily) error = %v", err)
|
||||||
|
}
|
||||||
|
todayID, err := IDForCommandName("today")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("IDForCommandName(today) error = %v", err)
|
||||||
|
}
|
||||||
|
if dailyID == todayID {
|
||||||
|
t.Fatalf("daily and today both resolve to %q, want distinct report IDs", dailyID)
|
||||||
|
}
|
||||||
if _, err := IDForCommandName("near-term"); err == nil || !strings.Contains(err.Error(), `unknown report command "near-term"`) {
|
if _, err := IDForCommandName("near-term"); err == nil || !strings.Contains(err.Error(), `unknown report command "near-term"`) {
|
||||||
t.Fatalf("IDForCommandName(near-term) error = %v, want unknown command", err)
|
t.Fatalf("IDForCommandName(near-term) error = %v, want unknown command", err)
|
||||||
}
|
}
|
||||||
@@ -278,6 +325,7 @@ func TestIDForConfigKey(t *testing.T) {
|
|||||||
}{
|
}{
|
||||||
{key: "daily", want: DailyToday},
|
{key: "daily", want: DailyToday},
|
||||||
{key: "daily_today", want: DailyToday},
|
{key: "daily_today", want: DailyToday},
|
||||||
|
{key: "today", want: Today},
|
||||||
{key: "tomorrow", want: Tomorrow},
|
{key: "tomorrow", want: Tomorrow},
|
||||||
{key: "hourly", want: Hourly},
|
{key: "hourly", want: Hourly},
|
||||||
{key: "three_day", want: ThreeDay},
|
{key: "three_day", want: ThreeDay},
|
||||||
@@ -303,6 +351,17 @@ 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)
|
||||||
}
|
}
|
||||||
|
dailyID, err := IDForConfigKey("daily")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("IDForConfigKey(daily) error = %v", err)
|
||||||
|
}
|
||||||
|
todayID, err := IDForConfigKey("today")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("IDForConfigKey(today) error = %v", err)
|
||||||
|
}
|
||||||
|
if dailyID == todayID {
|
||||||
|
t.Fatalf("daily and today config keys both resolve to %q, want distinct report IDs", dailyID)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestBatchForCommandName(t *testing.T) {
|
func TestBatchForCommandName(t *testing.T) {
|
||||||
@@ -342,7 +401,7 @@ func TestMorningBatchReportOrder(t *testing.T) {
|
|||||||
t.Fatalf("BatchReports() error = %v", err)
|
t.Fatalf("BatchReports() error = %v", err)
|
||||||
}
|
}
|
||||||
ids := resolvedIDs(resolved)
|
ids := resolvedIDs(resolved)
|
||||||
if strings.Join(ids, ",") != "daily_today,three_day,weekend" {
|
if strings.Join(ids, ",") != "today,three_day,weekend" {
|
||||||
t.Fatalf("ids = %v, want morning report order", ids)
|
t.Fatalf("ids = %v, want morning report order", ids)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -359,7 +418,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", "tomorrow", "hourly", "three_day", "weekend", "storm"}
|
want := []string{"daily_today", "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)
|
||||||
}
|
}
|
||||||
@@ -384,7 +443,7 @@ func TestRegistryDefinitionsDeclareGenerationMetadata(t *testing.T) {
|
|||||||
if !definition.Generated {
|
if !definition.Generated {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if definition.ID == Hourly || definition.ID == Tomorrow {
|
if definition.ID == Hourly || 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)
|
||||||
@@ -426,6 +485,14 @@ func TestRegistryDefinitionsDeclarePathAndCompatibilityPolicy(t *testing.T) {
|
|||||||
compatiblePriorIDs: []ID{DailyToday},
|
compatiblePriorIDs: []ID{DailyToday},
|
||||||
comparisonStrategy: CompareSameValidDate,
|
comparisonStrategy: CompareSameValidDate,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: Today,
|
||||||
|
artifactGroup: "today",
|
||||||
|
batchOutputName: "today.md",
|
||||||
|
generated: true,
|
||||||
|
compatiblePriorIDs: []ID{Today},
|
||||||
|
comparisonStrategy: CompareSameValidDate,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: Tomorrow,
|
id: Tomorrow,
|
||||||
artifactGroup: "tomorrow",
|
artifactGroup: "tomorrow",
|
||||||
@@ -522,6 +589,25 @@ func TestRegistryDefinitionsDeclareDefaultModules(t *testing.T) {
|
|||||||
module.HourlyForecast,
|
module.HourlyForecast,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: Today,
|
||||||
|
want: []module.ID{
|
||||||
|
module.Metadata,
|
||||||
|
module.CurrentConditions,
|
||||||
|
module.NarrativeForecast,
|
||||||
|
module.DerivedDailySummary,
|
||||||
|
module.DerivedDaypartSummaries,
|
||||||
|
module.PrecipTiming,
|
||||||
|
module.AlertDigest,
|
||||||
|
module.SPCConvectiveOutlooks,
|
||||||
|
module.AreaForecastDiscussion,
|
||||||
|
module.SPCConvectiveDiscussion,
|
||||||
|
module.WeatherStory,
|
||||||
|
module.OutdoorWindows,
|
||||||
|
module.HourlyForecast,
|
||||||
|
module.TodayPlanning,
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: Tomorrow,
|
id: Tomorrow,
|
||||||
want: []module.ID{
|
want: []module.ID{
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ type Registry struct {
|
|||||||
func DefaultRegistry() Registry {
|
func DefaultRegistry() Registry {
|
||||||
definitions := []Definition{
|
definitions := []Definition{
|
||||||
dailyTodayDefinition(),
|
dailyTodayDefinition(),
|
||||||
|
todayDefinition(),
|
||||||
tomorrowDefinition(),
|
tomorrowDefinition(),
|
||||||
hourlyDefinition(),
|
hourlyDefinition(),
|
||||||
threeDayDefinition(),
|
threeDayDefinition(),
|
||||||
@@ -74,7 +75,7 @@ func (r Registry) MustLookup(id ID) Definition {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (r Registry) All() []Definition {
|
func (r Registry) All() []Definition {
|
||||||
ids := []ID{DailyToday, Tomorrow, Hourly, ThreeDay, Weekend, Storm}
|
ids := []ID{DailyToday, 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 {
|
||||||
|
|||||||
51
internal/report/today_report.go
Normal file
51
internal/report/today_report.go
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
package report
|
||||||
|
|
||||||
|
import (
|
||||||
|
"gitea.maximumdirect.net/eric/weatherreporter/internal/module"
|
||||||
|
"gitea.maximumdirect.net/eric/weatherreporter/internal/timeutil"
|
||||||
|
)
|
||||||
|
|
||||||
|
func todayDefinition() Definition {
|
||||||
|
return Definition{
|
||||||
|
ID: Today,
|
||||||
|
Name: "Today Report",
|
||||||
|
PromptID: "weather.today_generated_text",
|
||||||
|
GenerationMode: GenerationModeGeneratedTextTemplate,
|
||||||
|
TemplateID: "today",
|
||||||
|
GeneratedTextSchemaID: "today",
|
||||||
|
ComparisonStrategy: CompareSameValidDate,
|
||||||
|
ArtifactGroup: "today",
|
||||||
|
BatchOutputName: "today.md",
|
||||||
|
Generated: true,
|
||||||
|
CompatiblePriorIDs: []ID{Today},
|
||||||
|
Modules: todayModules(),
|
||||||
|
Morning: true,
|
||||||
|
resolve: resolveToday,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func todayModules() []module.ConfigItem {
|
||||||
|
return moduleItems(
|
||||||
|
module.Metadata,
|
||||||
|
module.CurrentConditions,
|
||||||
|
module.NarrativeForecast,
|
||||||
|
module.DerivedDailySummary,
|
||||||
|
module.DerivedDaypartSummaries,
|
||||||
|
module.PrecipTiming,
|
||||||
|
module.AlertDigest,
|
||||||
|
module.SPCConvectiveOutlooks,
|
||||||
|
module.AreaForecastDiscussion,
|
||||||
|
module.SPCConvectiveDiscussion,
|
||||||
|
module.WeatherStory,
|
||||||
|
module.OutdoorWindows,
|
||||||
|
module.HourlyForecast,
|
||||||
|
module.TodayPlanning,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func resolveToday(req ResolveRequest) (timeutil.Period, error) {
|
||||||
|
if !req.Date.IsZero() {
|
||||||
|
return timeutil.CivilDay(req.Date, req.Location), nil
|
||||||
|
}
|
||||||
|
return timeutil.CivilDay(req.Now, req.Location), nil
|
||||||
|
}
|
||||||
@@ -63,6 +63,13 @@ func TestGeneratedTextArtifactPathsUseSnapshotTree(t *testing.T) {
|
|||||||
validDate: "2026-05-30",
|
validDate: "2026-05-30",
|
||||||
runID: "20260529T230000.000000000Z_tomorrow",
|
runID: "20260529T230000.000000000Z_tomorrow",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "today",
|
||||||
|
resolved: resolveTodayAt(t, "2026-05-29T05:00:00-05:00"),
|
||||||
|
group: "today",
|
||||||
|
validDate: "2026-05-29",
|
||||||
|
runID: "20260529T100000.000000000Z_today",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
@@ -403,6 +410,24 @@ func TestFindPriorSnapshot(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestFindPriorSnapshotSupportsToday(t *testing.T) {
|
||||||
|
store := newTestStore(t)
|
||||||
|
first := resolveTodayAt(t, "2026-05-29T05:00:00-05:00")
|
||||||
|
second := resolveTodayAt(t, "2026-05-29T08:00:00-05:00")
|
||||||
|
savePriorMetadata(t, store, first, stateBriefingMetadata(first))
|
||||||
|
|
||||||
|
prior, err := store.FindPriorSnapshot(context.Background(), second)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("FindPriorSnapshot() error = %v", err)
|
||||||
|
}
|
||||||
|
if prior == nil {
|
||||||
|
t.Fatal("FindPriorSnapshot() = nil, want prior Today snapshot")
|
||||||
|
}
|
||||||
|
if prior.Metadata.RunID != first.Metadata().RunID {
|
||||||
|
t.Fatalf("RunID = %q, want %q", prior.Metadata.RunID, first.Metadata().RunID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestFindPriorSnapshotUsesValidDate(t *testing.T) {
|
func TestFindPriorSnapshotUsesValidDate(t *testing.T) {
|
||||||
store := newTestStore(t)
|
store := newTestStore(t)
|
||||||
previousDate := resolveDailyAt(t, "2026-05-28T05:00:00-05:00")
|
previousDate := resolveDailyAt(t, "2026-05-28T05:00:00-05:00")
|
||||||
@@ -532,6 +557,26 @@ func resolveDailyAt(t *testing.T, value string) report.Resolved {
|
|||||||
return resolved
|
return resolved
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func resolveTodayAt(t *testing.T, value string) report.Resolved {
|
||||||
|
t.Helper()
|
||||||
|
location, err := timeutil.LoadLocation("America/Chicago")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("LoadLocation() error = %v", err)
|
||||||
|
}
|
||||||
|
now, err := time.Parse(time.RFC3339, value)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("parse time: %v", err)
|
||||||
|
}
|
||||||
|
resolved, err := report.DefaultRegistry().Resolve(report.Today, report.ResolveRequest{
|
||||||
|
Now: now,
|
||||||
|
Location: location,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Resolve() error = %v", err)
|
||||||
|
}
|
||||||
|
return resolved
|
||||||
|
}
|
||||||
|
|
||||||
func resolveThreeDayAt(t *testing.T, value string) report.Resolved {
|
func resolveThreeDayAt(t *testing.T, value string) report.Resolved {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
location, err := timeutil.LoadLocation("America/Chicago")
|
location, err := timeutil.LoadLocation("America/Chicago")
|
||||||
|
|||||||
Reference in New Issue
Block a user