Add Today planning module scaffold

This commit is contained in:
2026-06-15 14:37:08 +00:00
parent 7b4c73d1e6
commit 4e704e4f51
11 changed files with 261 additions and 8 deletions

View File

@@ -33,7 +33,7 @@ Outputs:
`weather_story` `weather_story`
- `module.Output` values for derived stanzas: - `module.Output` values for derived stanzas:
`derived_daily_summary`, `derived_daypart_summaries`, `precip_timing`, `derived_daily_summary`, `derived_daypart_summaries`, `precip_timing`,
`outdoor_windows`, and `tomorrow_planning` `outdoor_windows`, `today_planning`, and `tomorrow_planning`
Every registered composition entry has a builder. Unknown or unimplemented Every registered composition entry has a builder. Unknown or unimplemented
module IDs fail validation instead of being skipped. module IDs fail validation instead of being skipped.
@@ -42,13 +42,17 @@ Tomorrow Report supports the Daily-style civil-day modules plus
`tomorrow_planning` and `hourly_forecast`; those outputs feed the Tomorrow `tomorrow_planning` and `hourly_forecast`; those outputs feed the Tomorrow
GeneratedText prompt package and embedded Markdown template. GeneratedText prompt package and embedded Markdown template.
`today_planning` is a Today-specific deterministic planning stanza with
morning readiness, commute/school/workday concerns, outdoor planning, and
late-day change-watch fields. It is compatible with `report.Today` only.
Hourly Report supports source and valid-period modules that operate over its Hourly Report supports source and valid-period modules that operate over its
rolling six-hour period: `metadata`, `current_conditions`, `hourly_forecast`, rolling six-hour period: `metadata`, `current_conditions`, `hourly_forecast`,
`precip_timing`, `alert_digest`, `spc_convective_outlooks`, `precip_timing`, `alert_digest`, `spc_convective_outlooks`,
`area_forecast_discussion`, `spc_convective_discussion`, and `weather_story`. `area_forecast_discussion`, `spc_convective_discussion`, and `weather_story`.
It does not support daily/daypart-only modules such as It does not support daily/daypart-only modules such as
`derived_daily_summary`, `derived_daypart_summaries`, `outdoor_windows`, or `derived_daily_summary`, `derived_daypart_summaries`, `outdoor_windows`,
`tomorrow_planning`. `today_planning`, or `tomorrow_planning`.
Prompt-facing module values use local, human-readable date and time labels Prompt-facing module values use local, human-readable date and time labels
where the LLM is expected to reason about report content. Canonical timestamps where the LLM is expected to reason about report content. Canonical timestamps

View File

@@ -42,6 +42,7 @@ The registry recognizes these IDs:
- `spc_convective_discussion` - `spc_convective_discussion`
- `weather_story` - `weather_story`
- `outdoor_windows` - `outdoor_windows`
- `today_planning`
- `tomorrow_planning` - `tomorrow_planning`
Every registered module has a builder. Report composition entries that refer to Every registered module has a builder. Report composition entries that refer to

View File

@@ -337,6 +337,80 @@ func TestOutdoorWindowsAndTomorrowPlanningModulesPreserveDailyContent(t *testing
} }
} }
func TestTodayPlanningModulePackagesPlanningFields(t *testing.T) {
registry := MustDefaultModuleRegistry()
ctx := todayModuleContext()
output, err := registry.BuildModule(ctx, module.ConfigItem{ID: module.TodayPlanning})
if err != nil {
t.Fatalf("BuildModule(today planning) error = %v", err)
}
if output.ID != module.TodayPlanning || output.StanzaName != "today_planning" {
t.Fatalf("output = %#v, want today planning stanza", output)
}
planning := moduleValue[TodayPlanningModule](t, output)
if len(planning.MorningReadiness) == 0 ||
len(planning.CommuteSchoolWorkdayConcerns) == 0 ||
len(planning.OutdoorPlanning) == 0 ||
len(planning.LateDayChangeWatch) == 0 {
t.Fatalf("today planning = %#v, want populated planning fields", planning)
}
if !containsString(planning.MorningReadiness, "Morning precipitation chance peaks near 60%.") {
t.Fatalf("MorningReadiness = %#v, want precipitation readiness note", planning.MorningReadiness)
}
if !containsString(planning.OutdoorPlanning, "Best outdoor window: Overnight (cold risk).") ||
!containsString(planning.OutdoorPlanning, "Toughest outdoor window: Afternoon (high precipitation chance, gusty wind, alert overlap, heat risk).") {
t.Fatalf("OutdoorPlanning = %#v, want deterministic best and toughest windows", planning.OutdoorPlanning)
}
if !containsString(planning.LateDayChangeWatch, "Afternoon precipitation timing may shift; current peak is near 80%.") {
t.Fatalf("LateDayChangeWatch = %#v, want late-day change note", planning.LateDayChangeWatch)
}
data, err := json.Marshal(output.Value)
if err != nil {
t.Fatalf("marshal today planning: %v", err)
}
jsonText := string(data)
for _, field := range []string{"morning_readiness", "commute_school_workday_concerns", "outdoor_planning", "late_day_change_watch"} {
if !strings.Contains(jsonText, field) {
t.Fatalf("today planning json = %s, want field %s", jsonText, field)
}
}
if strings.Contains(jsonText, "morningReadiness") || strings.Contains(jsonText, "lateDayChangeWatch") {
t.Fatalf("today planning json = %s, want snake_case fields", jsonText)
}
}
func TestTodayPlanningModuleRejectsUnsupportedReports(t *testing.T) {
registry := MustDefaultModuleRegistry()
for _, id := range []report.ID{report.Tomorrow, report.DailyToday} {
t.Run(string(id), func(t *testing.T) {
ctx := derivedModuleContext(id)
_, err := registry.BuildModule(ctx, module.ConfigItem{ID: module.TodayPlanning})
if err == nil || !strings.Contains(err.Error(), `module "today_planning" is not compatible with report`) {
t.Fatalf("BuildModule(%s) error = %v, want incompatible report", id, err)
}
})
}
}
func TestTodayPlanningModuleHandlesMissingDailySummary(t *testing.T) {
registry := MustDefaultModuleRegistry()
ctx := todayModuleContext()
ctx.Derived.DailySummaries = nil
output, err := registry.BuildModule(ctx, module.ConfigItem{ID: module.TodayPlanning})
if err != nil {
t.Fatalf("BuildModule(today planning) error = %v", err)
}
planning := moduleValue[TodayPlanningModule](t, output)
if len(planning.MorningReadiness) != 0 ||
len(planning.CommuteSchoolWorkdayConcerns) != 0 ||
len(planning.OutdoorPlanning) != 0 ||
len(planning.LateDayChangeWatch) != 0 {
t.Fatalf("today planning = %#v, want empty output without daily summary", planning)
}
}
func TestDerivedModulesHandleMissingData(t *testing.T) { func TestDerivedModulesHandleMissingData(t *testing.T) {
registry := MustDefaultModuleRegistry() registry := MustDefaultModuleRegistry()
ctx := derivedModuleContext(report.DailyToday) ctx := derivedModuleContext(report.DailyToday)
@@ -430,6 +504,16 @@ func derivedModuleContext(id report.ID) ModuleContext {
} }
} }
func todayModuleContext() ModuleContext {
ctx := derivedModuleContext(report.DailyToday)
ctx.Resolved.Definition = report.Definition{
ID: report.Today,
Name: "Today Report",
PromptID: "weather.today_generated_text",
}
return ctx
}
func derivedDaypart(name string, start string, end string, text string, temperature float64, apparent *float64, precip float64, gust float64) forecast.DaypartSummary { func derivedDaypart(name string, start string, end string, text string, temperature float64, apparent *float64, precip float64, gust float64) forecast.DaypartSummary {
hour := derivedHour(start, text, precip, temperature, apparent, gust) hour := derivedHour(start, text, precip, temperature, apparent, gust)
return forecast.SummarizeDaypart(name, timeutil.Period{ return forecast.SummarizeDaypart(name, timeutil.Period{

View File

@@ -249,8 +249,8 @@ func (d ModuleDefinition) ValidateOptions(options any) error {
} }
func defaultModuleDefinitions() []ModuleDefinition { func defaultModuleDefinitions() []ModuleDefinition {
allReports := []report.ID{report.DailyToday, report.Tomorrow, report.Hourly, report.ThreeDay, report.Weekend, report.Storm} allReports := []report.ID{report.DailyToday, report.Today, report.Tomorrow, report.Hourly, report.ThreeDay, report.Weekend, report.Storm}
daypartReports := []report.ID{report.DailyToday, report.Tomorrow, report.ThreeDay, report.Weekend} daypartReports := []report.ID{report.DailyToday, report.Today, report.Tomorrow, report.ThreeDay, report.Weekend}
return []ModuleDefinition{ return []ModuleDefinition{
{ {
ID: module.Metadata, ID: module.Metadata,
@@ -276,7 +276,7 @@ func defaultModuleDefinitions() []ModuleDefinition {
DefaultOptions: module.NarrativeForecastOptions{}, DefaultOptions: module.NarrativeForecastOptions{},
RequiredCollected: []module.FactRequirement{module.CollectedNarrativeForecast}, RequiredCollected: []module.FactRequirement{module.CollectedNarrativeForecast},
RequiredDerived: []module.FactRequirement{module.RequiresDerivedNarrativePeriods}, RequiredDerived: []module.FactRequirement{module.RequiresDerivedNarrativePeriods},
SupportedReports: []report.ID{report.DailyToday, report.Tomorrow}, SupportedReports: []report.ID{report.DailyToday, report.Today, report.Tomorrow},
MissingData: module.MissingDataOmit, MissingData: module.MissingDataOmit,
Builder: buildNarrativeForecastModule, Builder: buildNarrativeForecastModule,
}, },
@@ -286,7 +286,7 @@ func defaultModuleDefinitions() []ModuleDefinition {
DefaultOptions: module.HourlyForecastOptions{}, DefaultOptions: module.HourlyForecastOptions{},
RequiredCollected: []module.FactRequirement{module.CollectedHourlyForecast}, RequiredCollected: []module.FactRequirement{module.CollectedHourlyForecast},
RequiredDerived: []module.FactRequirement{module.RequiresDerivedHourlyPeriods}, RequiredDerived: []module.FactRequirement{module.RequiresDerivedHourlyPeriods},
SupportedReports: []report.ID{report.DailyToday, report.Tomorrow, report.Hourly}, SupportedReports: []report.ID{report.DailyToday, report.Today, report.Tomorrow, report.Hourly},
MissingData: module.MissingDataOmit, MissingData: module.MissingDataOmit,
Builder: buildHourlyForecastModule, Builder: buildHourlyForecastModule,
}, },
@@ -295,7 +295,7 @@ func defaultModuleDefinitions() []ModuleDefinition {
StanzaName: "derived_daily_summary", StanzaName: "derived_daily_summary",
DefaultOptions: module.DerivedDailySummaryOptions{}, DefaultOptions: module.DerivedDailySummaryOptions{},
RequiredDerived: []module.FactRequirement{module.RequiresDerivedDailySummaries, module.RequiresDerivedPrecipTiming}, RequiredDerived: []module.FactRequirement{module.RequiresDerivedDailySummaries, module.RequiresDerivedPrecipTiming},
SupportedReports: []report.ID{report.DailyToday, report.Tomorrow}, SupportedReports: []report.ID{report.DailyToday, report.Today, report.Tomorrow},
MissingData: module.MissingDataError, MissingData: module.MissingDataError,
Builder: buildDerivedDailySummaryModule, Builder: buildDerivedDailySummaryModule,
}, },
@@ -374,6 +374,15 @@ func defaultModuleDefinitions() []ModuleDefinition {
MissingData: module.MissingDataEmpty, MissingData: module.MissingDataEmpty,
Builder: buildOutdoorWindowsModule, Builder: buildOutdoorWindowsModule,
}, },
{
ID: module.TodayPlanning,
StanzaName: "today_planning",
DefaultOptions: module.TodayPlanningOptions{},
RequiredDerived: []module.FactRequirement{module.RequiresDerivedDailySummaries},
SupportedReports: []report.ID{report.Today},
MissingData: module.MissingDataEmpty,
Builder: buildTodayPlanningModule,
},
{ {
ID: module.TomorrowPlanning, ID: module.TomorrowPlanning,
StanzaName: "tomorrow_planning", StanzaName: "tomorrow_planning",

View File

@@ -112,6 +112,43 @@ func TestModuleRegistryRejectsIncompatibleReports(t *testing.T) {
} }
} }
func TestModuleRegistryValidatesTodayPlanningSupport(t *testing.T) {
registry := MustDefaultModuleRegistry()
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} {
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`) {
t.Fatalf("ValidateComposition(%s) error = %v, want incompatible report", id, err)
}
})
}
}
func TestModuleRegistrySupportsTodayEligibleModules(t *testing.T) {
registry := MustDefaultModuleRegistry()
err := registry.ValidateComposition(report.Today, []module.ConfigItem{
{ID: module.Metadata},
{ID: module.CurrentConditions},
{ID: module.NarrativeForecast},
{ID: module.HourlyForecast},
{ID: module.DerivedDailySummary},
{ID: module.DerivedDaypartSummaries},
{ID: module.PrecipTiming},
{ID: module.AlertDigest},
{ID: module.SPCConvectiveOutlooks},
{ID: module.AreaForecastDiscussion},
{ID: module.SPCConvectiveDiscussion},
{ID: module.WeatherStory},
{ID: module.OutdoorWindows},
})
if err != nil {
t.Fatalf("ValidateComposition(today eligible modules) error = %v", err)
}
}
func TestModuleRegistryRejectsHourlyIncompatibleModules(t *testing.T) { func TestModuleRegistryRejectsHourlyIncompatibleModules(t *testing.T) {
registry := MustDefaultModuleRegistry() registry := MustDefaultModuleRegistry()
for _, id := range []module.ID{ for _, id := range []module.ID{
@@ -119,6 +156,7 @@ func TestModuleRegistryRejectsHourlyIncompatibleModules(t *testing.T) {
module.DerivedDailySummary, module.DerivedDailySummary,
module.DerivedDaypartSummaries, module.DerivedDaypartSummaries,
module.OutdoorWindows, module.OutdoorWindows,
module.TodayPlanning,
module.TomorrowPlanning, module.TomorrowPlanning,
} { } {
t.Run(string(id), func(t *testing.T) { t.Run(string(id), func(t *testing.T) {

View File

@@ -28,6 +28,13 @@ type TomorrowPlanning struct {
OvernightChangeWatch []string OvernightChangeWatch []string
} }
type TodayPlanning struct {
MorningReadiness []string
CommuteSchoolWorkdayConcerns []string
OutdoorPlanning []string
LateDayChangeWatch []string
}
func buildOutdoorWindows(dayparts []forecast.DaypartSummary) OutdoorWindows { func buildOutdoorWindows(dayparts []forecast.DaypartSummary) OutdoorWindows {
var best *OutdoorWindow var best *OutdoorWindow
var worst *OutdoorWindow var worst *OutdoorWindow
@@ -48,6 +55,49 @@ func buildOutdoorWindows(dayparts []forecast.DaypartSummary) OutdoorWindows {
return OutdoorWindows{Best: best, Worst: worst} return OutdoorWindows{Best: best, Worst: worst}
} }
func buildTodayPlanning(summary *forecast.DailySummary) *TodayPlanning {
planning := &TodayPlanning{}
morning := daypartNamed(summary.Dayparts, "morning")
if morning != nil {
planning.MorningReadiness = append(planning.MorningReadiness, readinessNotes(*morning)...)
}
if len(planning.MorningReadiness) == 0 {
planning.MorningReadiness = append(planning.MorningReadiness, "Morning weather looks routine based on the available hourly forecast.")
}
for _, daypart := range summary.Dayparts {
if daypart.Name == "overnight" || daypart.Name == "evening" {
continue
}
planning.CommuteSchoolWorkdayConcerns = appendUnique(planning.CommuteSchoolWorkdayConcerns, concernNotes(daypart)...)
}
for _, alert := range summary.AlertOverlaps {
if alert.Event != "" {
planning.CommuteSchoolWorkdayConcerns = appendUnique(planning.CommuteSchoolWorkdayConcerns, "Active alert to plan around: "+alert.Event+".")
}
}
if len(planning.CommuteSchoolWorkdayConcerns) == 0 {
planning.CommuteSchoolWorkdayConcerns = append(planning.CommuteSchoolWorkdayConcerns, "No major commute, school, or workday weather concerns stand out in the available forecast.")
}
planning.OutdoorPlanning = append(planning.OutdoorPlanning, outdoorPlanningNotes(summary.Dayparts)...)
if len(planning.OutdoorPlanning) == 0 {
planning.OutdoorPlanning = append(planning.OutdoorPlanning, "No standout outdoor weather constraints are evident in the available forecast.")
}
for _, name := range []string{"afternoon", "evening"} {
daypart := daypartNamed(summary.Dayparts, name)
if daypart != nil {
planning.LateDayChangeWatch = appendUnique(planning.LateDayChangeWatch, lateDayWatchNotes(*daypart)...)
}
}
if len(planning.LateDayChangeWatch) == 0 {
planning.LateDayChangeWatch = append(planning.LateDayChangeWatch, "Watch for forecast timing or intensity adjustments later today.")
}
return planning
}
func buildTomorrowPlanning(summary *forecast.DailySummary) *TomorrowPlanning { func buildTomorrowPlanning(summary *forecast.DailySummary) *TomorrowPlanning {
planning := &TomorrowPlanning{} planning := &TomorrowPlanning{}
morning := daypartNamed(summary.Dayparts, "morning") morning := daypartNamed(summary.Dayparts, "morning")
@@ -84,6 +134,18 @@ func buildTomorrowPlanning(summary *forecast.DailySummary) *TomorrowPlanning {
return planning return planning
} }
func outdoorPlanningNotes(dayparts []forecast.DaypartSummary) []string {
windows := buildOutdoorWindows(dayparts)
var notes []string
if windows.Best != nil {
notes = append(notes, fmt.Sprintf("Best outdoor window: %s (%s).", titleWord(windows.Best.Daypart), strings.Join(windows.Best.Reasons, ", ")))
}
if windows.Worst != nil && (windows.Best == nil || windows.Worst.Daypart != windows.Best.Daypart) {
notes = append(notes, fmt.Sprintf("Toughest outdoor window: %s (%s).", titleWord(windows.Worst.Daypart), strings.Join(windows.Worst.Reasons, ", ")))
}
return appendUnique(nil, notes...)
}
func readinessNotes(daypart forecast.DaypartSummary) []string { func readinessNotes(daypart forecast.DaypartSummary) []string {
notes := []string{} notes := []string{}
if daypart.MaxPrecipitationProbability != nil && daypart.MaxPrecipitationProbability.Value >= 50 { if daypart.MaxPrecipitationProbability != nil && daypart.MaxPrecipitationProbability.Value >= 50 {
@@ -104,6 +166,27 @@ func readinessNotes(daypart forecast.DaypartSummary) []string {
return appendUnique(nil, notes...) return appendUnique(nil, notes...)
} }
func lateDayWatchNotes(daypart forecast.DaypartSummary) []string {
notes := []string{}
prefix := titleWord(daypart.Name)
if prefix == "" {
prefix = "Late-day"
}
if daypart.MaxPrecipitationProbability != nil && daypart.MaxPrecipitationProbability.Value >= 30 {
notes = append(notes, fmt.Sprintf("%s precipitation timing may shift; current peak is near %.0f%%.", prefix, daypart.MaxPrecipitationProbability.Value))
}
if daypart.PeakWindGust != nil && daypart.PeakWindGust.Value >= 30 {
notes = append(notes, fmt.Sprintf("%s gusts may reach %.0f mph.", prefix, daypart.PeakWindGust.Value))
}
if daypart.Indicators.Snow || daypart.Indicators.Ice {
notes = append(notes, prefix+" wintry weather could affect late-day travel.")
}
if len(daypart.AlertOverlaps) > 0 {
notes = append(notes, prefix+" alert timing could affect late-day plans.")
}
return appendUnique(nil, notes...)
}
func concernNotes(daypart forecast.DaypartSummary) []string { func concernNotes(daypart forecast.DaypartSummary) []string {
notes := []string{} notes := []string{}
prefix := titleWord(daypart.Name) prefix := titleWord(daypart.Name)

View File

@@ -0,0 +1,26 @@
package briefing
import "gitea.maximumdirect.net/eric/weatherreporter/internal/module"
type TodayPlanningModule struct {
MorningReadiness []string `json:"morning_readiness,omitempty"`
CommuteSchoolWorkdayConcerns []string `json:"commute_school_workday_concerns,omitempty"`
OutdoorPlanning []string `json:"outdoor_planning,omitempty"`
LateDayChangeWatch []string `json:"late_day_change_watch,omitempty"`
}
func buildTodayPlanningModule(ctx ModuleContext, _ any) (*module.Output, error) {
summary := ctx.Derived.FirstDailySummary()
if summary == nil {
return &module.Output{ID: module.TodayPlanning, StanzaName: "today_planning", Value: TodayPlanningModule{}}, nil
}
planning := buildTodayPlanning(summary)
value := TodayPlanningModule{}
if planning != nil {
value.MorningReadiness = append([]string(nil), planning.MorningReadiness...)
value.CommuteSchoolWorkdayConcerns = append([]string(nil), planning.CommuteSchoolWorkdayConcerns...)
value.OutdoorPlanning = append([]string(nil), planning.OutdoorPlanning...)
value.LateDayChangeWatch = append([]string(nil), planning.LateDayChangeWatch...)
}
return &module.Output{ID: module.TodayPlanning, StanzaName: "today_planning", Value: value}, nil
}

View File

@@ -24,6 +24,7 @@ const (
SPCConvectiveOutlooks ID = "spc_convective_outlooks" SPCConvectiveOutlooks ID = "spc_convective_outlooks"
SPCConvectiveDiscussion ID = "spc_convective_discussion" SPCConvectiveDiscussion ID = "spc_convective_discussion"
OutdoorWindows ID = "outdoor_windows" OutdoorWindows ID = "outdoor_windows"
TodayPlanning ID = "today_planning"
TomorrowPlanning ID = "tomorrow_planning" TomorrowPlanning ID = "tomorrow_planning"
) )
@@ -148,4 +149,5 @@ type WeatherStoryOptions struct{}
type SPCConvectiveOutlooksOptions struct{} type SPCConvectiveOutlooksOptions struct{}
type SPCConvectiveDiscussionOptions struct{} type SPCConvectiveDiscussionOptions struct{}
type OutdoorWindowsOptions struct{} type OutdoorWindowsOptions struct{}
type TodayPlanningOptions struct{}
type TomorrowPlanningOptions struct{} type TomorrowPlanningOptions struct{}

View File

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

View File

@@ -107,6 +107,7 @@ func TestBuildUsesNamedSnapshotStanzas(t *testing.T) {
req.Modules = snapshotWithOutputs(t, req.Modules = snapshotWithOutputs(t,
module.Output{ID: module.Metadata, StanzaName: "metadata", Value: map[string]string{"run_id": req.Metadata.RunID}}, module.Output{ID: module.Metadata, StanzaName: "metadata", Value: map[string]string{"run_id": req.Metadata.RunID}},
module.Output{ID: module.DerivedDaypartSummaries, StanzaName: "derived_daypart_summaries", Value: map[string]any{"days": []string{"2026-05-29"}}}, module.Output{ID: module.DerivedDaypartSummaries, StanzaName: "derived_daypart_summaries", Value: map[string]any{"days": []string{"2026-05-29"}}},
module.Output{ID: module.TodayPlanning, StanzaName: "today_planning", Value: map[string]any{"morning_readiness": []string{"routine"}}},
) )
pkg, err := Build(req) pkg, err := Build(req)
@@ -120,6 +121,9 @@ func TestBuildUsesNamedSnapshotStanzas(t *testing.T) {
if _, ok := pkg.Briefing.Values["derived_daypart_summaries"]; !ok { if _, ok := pkg.Briefing.Values["derived_daypart_summaries"]; !ok {
t.Fatal("Briefing.Values[derived_daypart_summaries] missing") t.Fatal("Briefing.Values[derived_daypart_summaries] missing")
} }
if _, ok := pkg.Briefing.Values["today_planning"]; !ok {
t.Fatal("Briefing.Values[today_planning] missing")
}
} }
func TestMarshalYAMLIsDeterministicAndGroupsNamedStanzas(t *testing.T) { func TestMarshalYAMLIsDeterministicAndGroupsNamedStanzas(t *testing.T) {

View File

@@ -13,6 +13,7 @@ type ID string
const ( const (
DailyToday ID = "daily_today" DailyToday ID = "daily_today"
Today ID = "today"
Tomorrow ID = "tomorrow" Tomorrow ID = "tomorrow"
Hourly ID = "hourly" Hourly ID = "hourly"
ThreeDay ID = "three_day" ThreeDay ID = "three_day"