Validate render identity and daypart fallbacks

This commit is contained in:
2026-08-13 02:11:25 +00:00
parent 360c665a3e
commit 2bd921f247
12 changed files with 253 additions and 184 deletions

View File

@@ -2,6 +2,7 @@ package generatedtext
import (
"fmt"
"reflect"
"sort"
"time"
@@ -9,6 +10,7 @@ import (
"gitea.maximumdirect.net/eric/weatherreporter/internal/facts"
"gitea.maximumdirect.net/eric/weatherreporter/internal/forecast"
"gitea.maximumdirect.net/eric/weatherreporter/internal/module"
"gitea.maximumdirect.net/eric/weatherreporter/internal/report"
"gitea.maximumdirect.net/eric/weatherreporter/internal/timeutil"
)
@@ -16,8 +18,6 @@ type HourlyRenderContext struct {
Report HourlyReportContext
GeneratedText Hourly
Modules HourlyTemplateModules
Collected facts.CollectedFacts
Derived facts.DerivedFacts
}
type HourlyReportContext struct {
@@ -31,7 +31,6 @@ type HourlyReportContext struct {
}
type HourlyTemplateModules struct {
Metadata *briefing.MetadataModule
CurrentConditions *briefing.CurrentConditionsModule
HourlyForecast *briefing.HourlyForecastModule
PrecipTiming *briefing.PrecipTimingModule
@@ -46,24 +45,18 @@ type TomorrowRenderContext struct {
Report TomorrowReportContext
GeneratedText Tomorrow
Modules TomorrowTemplateModules
Collected facts.CollectedFacts
Derived facts.DerivedFacts
}
type DailyRenderContext struct {
Report DailyReportContext
GeneratedText Daily
Modules DailyTemplateModules
Collected facts.CollectedFacts
Derived facts.DerivedFacts
}
type TodayRenderContext struct {
Report TodayReportContext
GeneratedText Today
Modules TodayTemplateModules
Collected facts.CollectedFacts
Derived facts.DerivedFacts
}
type TodayReportContext struct {
@@ -78,12 +71,12 @@ type TodayReportContext struct {
}
type TodayTemplateModules struct {
Metadata *briefing.MetadataModule
CurrentConditions *briefing.CurrentConditionsModule
HourlyForecast *briefing.HourlyForecastModule
DerivedDailySummary *briefing.DerivedDailySummaryModule
DerivedDaypartSummaries *map[string]briefing.DerivedDaypartSummaryModule
Dayparts []TodayDaypartContext
HasDaypartDetails bool
PrecipTiming *briefing.PrecipTimingModule
AlertDigest *briefing.AlertDigestModule
SPCConvectiveOutlooks *briefing.SPCConvectiveOutlooksModule
@@ -110,7 +103,6 @@ type TomorrowReportContext struct {
}
type TomorrowTemplateModules struct {
Metadata *briefing.MetadataModule
CurrentConditions *briefing.CurrentConditionsModule
HourlyForecast *briefing.HourlyForecastModule
DerivedDailySummary *briefing.DerivedDailySummaryModule
@@ -142,7 +134,6 @@ type DailyReportContext struct {
}
type DailyTemplateModules struct {
Metadata *briefing.MetadataModule
CurrentConditions *briefing.CurrentConditionsModule
HourlyForecast *briefing.HourlyForecastModule
DerivedDailySummary *briefing.DerivedDailySummaryModule
@@ -175,7 +166,6 @@ type dayStyleReportContext struct {
}
type dayStyleTemplateModules struct {
Metadata *briefing.MetadataModule
CurrentConditions *briefing.CurrentConditionsModule
HourlyForecast *briefing.HourlyForecastModule
DerivedDailySummary *briefing.DerivedDailySummaryModule
@@ -188,7 +178,10 @@ type dayStyleTemplateModules struct {
WeatherStory *briefing.WeatherStoryModule
}
func BuildHourlyRenderContext(identity briefing.PreparedIdentity, snapshot module.Snapshot, generated Hourly, collected facts.CollectedFacts, derived facts.DerivedFacts) (HourlyRenderContext, error) {
func BuildHourlyRenderContext(identity briefing.PreparedIdentity, snapshot module.Snapshot, generated Hourly) (HourlyRenderContext, error) {
if err := validateRenderIdentity(report.Hourly, identity, snapshot); err != nil {
return HourlyRenderContext{}, err
}
location, err := timeutil.LoadLocation(identity.Timezone)
if err != nil {
return HourlyRenderContext{}, fmt.Errorf("build hourly render context: %w", err)
@@ -215,12 +208,13 @@ func BuildHourlyRenderContext(identity briefing.PreparedIdentity, snapshot modul
},
GeneratedText: generated,
Modules: modules,
Collected: collected,
Derived: derived,
}, nil
}
func BuildDailyRenderContext(identity briefing.PreparedIdentity, snapshot module.Snapshot, generated Daily, collected facts.CollectedFacts, derived facts.DerivedFacts) (DailyRenderContext, error) {
func BuildDailyRenderContext(identity briefing.PreparedIdentity, snapshot module.Snapshot, generated Daily, derived facts.DerivedFacts) (DailyRenderContext, error) {
if err := validateRenderIdentity(report.Daily, identity, snapshot); err != nil {
return DailyRenderContext{}, err
}
reportContext, err := buildDayStyleReportContext(identity, "daily", func(dayName string) string {
return dayName + "'s Weather"
})
@@ -235,12 +229,13 @@ func BuildDailyRenderContext(identity briefing.PreparedIdentity, snapshot module
Report: reportContext.dailyReportContext(),
GeneratedText: generated,
Modules: modules,
Collected: collected,
Derived: derived,
}, nil
}
func BuildTodayRenderContext(identity briefing.PreparedIdentity, snapshot module.Snapshot, generated Today, collected facts.CollectedFacts, derived facts.DerivedFacts) (TodayRenderContext, error) {
func BuildTodayRenderContext(identity briefing.PreparedIdentity, snapshot module.Snapshot, generated Today, derived facts.DerivedFacts) (TodayRenderContext, error) {
if err := validateRenderIdentity(report.Today, identity, snapshot); err != nil {
return TodayRenderContext{}, err
}
reportContext, err := buildDayStyleReportContext(identity, "today", func(string) string {
return "Today's Weather"
})
@@ -255,12 +250,13 @@ func BuildTodayRenderContext(identity briefing.PreparedIdentity, snapshot module
Report: reportContext.todayReportContext(),
GeneratedText: generated,
Modules: modules,
Collected: collected,
Derived: derived,
}, nil
}
func BuildTomorrowRenderContext(identity briefing.PreparedIdentity, snapshot module.Snapshot, generated Tomorrow, collected facts.CollectedFacts, derived facts.DerivedFacts) (TomorrowRenderContext, error) {
func BuildTomorrowRenderContext(identity briefing.PreparedIdentity, snapshot module.Snapshot, generated Tomorrow, derived facts.DerivedFacts) (TomorrowRenderContext, error) {
if err := validateRenderIdentity(report.Tomorrow, identity, snapshot); err != nil {
return TomorrowRenderContext{}, err
}
reportContext, err := buildDayStyleReportContext(identity, "tomorrow", func(dayName string) string {
return dayName + "'s Weather"
})
@@ -275,11 +271,36 @@ func BuildTomorrowRenderContext(identity briefing.PreparedIdentity, snapshot mod
Report: reportContext.tomorrowReportContext(),
GeneratedText: generated,
Modules: modules,
Collected: collected,
Derived: derived,
}, nil
}
func validateRenderIdentity(expected report.ID, identity briefing.PreparedIdentity, snapshot module.Snapshot) error {
if identity.ReportID != expected {
return fmt.Errorf("render identity report %q does not match expected report %q", identity.ReportID, expected)
}
metadata, found, err := module.StanzaValue[briefing.MetadataModule](snapshot, string(module.Metadata))
if err != nil {
return fmt.Errorf("read metadata module for report %q: %w", expected, err)
}
if !found || metadataMatchesIdentity(metadata, identity) {
return nil
}
return fmt.Errorf("metadata module does not match render identity for report %q", expected)
}
func metadataMatchesIdentity(metadata briefing.MetadataModule, identity briefing.PreparedIdentity) bool {
if metadata.RunID != identity.RunID || metadata.ReportID != identity.ReportID || metadata.Variant != identity.Variant || metadata.PromptID != identity.PromptID || !metadata.GeneratedAt.Equal(identity.GeneratedAt) || metadata.Units != identity.Units || metadata.Timezone != identity.Timezone || metadata.ValidPeriod != identity.ValidPeriod || !reflect.DeepEqual(metadata.Location, identity.Location) || len(metadata.SourceWarnings) != len(identity.SourceWarnings) {
return false
}
for index, warning := range identity.SourceWarnings {
summary := metadata.SourceWarnings[index]
if summary.Source != warning.Source || summary.Code != warning.Code || summary.Severity != warning.Severity || summary.Message != warning.Message || summary.CompletenessImpact != warning.CompletenessImpact {
return false
}
}
return true
}
func buildDayStyleReportContext(identity briefing.PreparedIdentity, name string, title func(string) string) (dayStyleReportContext, error) {
location, err := timeutil.LoadLocation(identity.Timezone)
if err != nil {
@@ -359,7 +380,6 @@ func dailyTemplateModules(snapshot module.Snapshot, derived facts.DerivedFacts)
return DailyTemplateModules{}, err
}
return DailyTemplateModules{
Metadata: common.Metadata,
CurrentConditions: common.CurrentConditions,
HourlyForecast: common.HourlyForecast,
DerivedDailySummary: common.DerivedDailySummary,
@@ -385,13 +405,14 @@ func todayTemplateModules(snapshot module.Snapshot, derived facts.DerivedFacts)
if err != nil {
return TodayTemplateModules{}, err
}
dayparts := orderedTodayDayparts(common.DerivedDaypartSummaries, derived.DaypartSummaries)
return TodayTemplateModules{
Metadata: common.Metadata,
CurrentConditions: common.CurrentConditions,
HourlyForecast: common.HourlyForecast,
DerivedDailySummary: common.DerivedDailySummary,
DerivedDaypartSummaries: common.DerivedDaypartSummaries,
Dayparts: orderedTodayDayparts(common.DerivedDaypartSummaries, derived.DaypartSummaries),
Dayparts: dayparts,
HasDaypartDetails: hasTodayDaypartDetails(dayparts),
PrecipTiming: common.PrecipTiming,
AlertDigest: common.AlertDigest,
SPCConvectiveOutlooks: common.SPCConvectiveOutlooks,
@@ -404,10 +425,6 @@ func todayTemplateModules(snapshot module.Snapshot, derived facts.DerivedFacts)
func hourlyTemplateModules(snapshot module.Snapshot) (HourlyTemplateModules, error) {
lookup := newModuleSnapshotLookup(snapshot)
metadata, err := lookup.metadata()
if err != nil {
return HourlyTemplateModules{}, err
}
current, err := lookup.currentConditions()
if err != nil {
return HourlyTemplateModules{}, err
@@ -441,7 +458,6 @@ func hourlyTemplateModules(snapshot module.Snapshot) (HourlyTemplateModules, err
return HourlyTemplateModules{}, err
}
return HourlyTemplateModules{
Metadata: metadata,
CurrentConditions: current,
HourlyForecast: hourly,
PrecipTiming: precip,
@@ -463,7 +479,6 @@ func tomorrowTemplateModules(snapshot module.Snapshot, derived facts.DerivedFact
return TomorrowTemplateModules{}, err
}
return TomorrowTemplateModules{
Metadata: common.Metadata,
CurrentConditions: common.CurrentConditions,
HourlyForecast: common.HourlyForecast,
DerivedDailySummary: common.DerivedDailySummary,
@@ -481,10 +496,6 @@ func tomorrowTemplateModules(snapshot module.Snapshot, derived facts.DerivedFact
func dayStyleTemplateModuleSnapshot(snapshot module.Snapshot) (dayStyleTemplateModules, moduleSnapshotLookup, error) {
lookup := newModuleSnapshotLookup(snapshot)
metadata, err := lookup.metadata()
if err != nil {
return dayStyleTemplateModules{}, moduleSnapshotLookup{}, err
}
current, err := lookup.currentConditions()
if err != nil {
return dayStyleTemplateModules{}, moduleSnapshotLookup{}, err
@@ -526,7 +537,6 @@ func dayStyleTemplateModuleSnapshot(snapshot module.Snapshot) (dayStyleTemplateM
return dayStyleTemplateModules{}, moduleSnapshotLookup{}, err
}
return dayStyleTemplateModules{
Metadata: metadata,
CurrentConditions: current,
HourlyForecast: hourly,
DerivedDailySummary: daily,
@@ -556,10 +566,6 @@ func newModuleSnapshotLookup(snapshot module.Snapshot) moduleSnapshotLookup {
}
}
func (lookup moduleSnapshotLookup) metadata() (*briefing.MetadataModule, error) {
return optionalStanza[briefing.MetadataModule](lookup, module.Metadata)
}
func (lookup moduleSnapshotLookup) currentConditions() (*briefing.CurrentConditionsModule, error) {
return optionalStanza[briefing.CurrentConditionsModule](lookup, module.CurrentConditions)
}
@@ -638,6 +644,15 @@ func orderedTodayDayparts(dayparts *map[string]briefing.DerivedDaypartSummaryMod
return out
}
func hasTodayDaypartDetails(dayparts []TodayDaypartContext) bool {
for _, daypart := range dayparts {
if daypart.Summary.DominantConditionDisplay != "" || daypart.Summary.DominantCondition != "" {
return true
}
}
return false
}
func orderedDailyDayparts(dayparts *map[string]briefing.DerivedDaypartSummaryModule, ordered []forecast.DaypartSummary) []DailyDaypartContext {
orderedRows := orderedDaypartRows(dayparts, ordered)
out := make([]DailyDaypartContext, 0, len(orderedRows))