2026-05-29 18:28:25 +00:00
package briefing
import (
"fmt"
"gitea.maximumdirect.net/eric/weatherreporter/internal/forecast"
"gitea.maximumdirect.net/eric/weatherreporter/internal/report"
"gitea.maximumdirect.net/eric/weatherreporter/internal/timeutil"
)
type Storm struct {
TimingWindow timeutil . Period ` json:"timingWindow" `
EventHeadlines [ ] string ` json:"eventHeadlines,omitempty" `
Hazards [ ] string ` json:"hazards,omitempty" `
MostLikelyScenario [ ] string ` json:"mostLikelyScenario,omitempty" `
ReasonableWorstCase [ ] string ` json:"reasonableWorstCase,omitempty" `
ConfidenceInputs [ ] string ` json:"confidenceInputs,omitempty" `
WhatToWatchNext [ ] string ` json:"whatToWatchNext,omitempty" `
RelevantAlerts [ ] forecast . AlertOverlap ` json:"relevantAlerts,omitempty" `
HourlyPeriods [ ] forecast . ForecastPeriod ` json:"hourlyPeriods,omitempty" `
DailyPeriods [ ] forecast . ForecastPeriod ` json:"dailyPeriods,omitempty" `
NarrativePeriods [ ] forecast . ForecastPeriod ` json:"narrativePeriods,omitempty" `
WindowSummary forecast . DaypartSummary ` json:"windowSummary" `
Discussion DiscussionContext ` json:"discussion,omitempty" `
WeatherStory * WeatherStoryContext ` json:"weatherStory,omitempty" `
}
func BuildStorm ( ctx BuildContext ) ( Package , error ) {
if ctx . Resolved . Definition . ID != report . Storm {
return Package { } , fmt . Errorf ( "storm briefing requires a storm report definition" )
}
if ctx . Bundle == nil {
return Package { } , fmt . Errorf ( "forecast bundle is required" )
}
period := ctx . Resolved . ValidPeriod
hourly := forecast . SelectHourlyPeriods ( ctx . Bundle . Hourly , period )
narrative := forecast . SelectNarrativePeriods ( ctx . Bundle , period )
daily := forecast . SelectHourlyPeriods ( ctx . Bundle . Daily , period )
alerts := forecast . AlertOverlaps ( ctx . Bundle . Alerts , period )
summary := forecast . SummarizeDaypart ( "storm window" , period , hourly )
summary . AlertOverlaps = alerts
storm := & Storm {
TimingWindow : period ,
EventHeadlines : stormHeadlines ( alerts ) ,
Hazards : stormHazards ( alerts , summary ) ,
MostLikelyScenario : mostLikelyStormScenario ( hourly , narrative , summary ) ,
ReasonableWorstCase : reasonableWorstCase ( alerts , summary ) ,
ConfidenceInputs : stormConfidenceInputs ( ctx . Bundle ) ,
WhatToWatchNext : stormWatchItems ( alerts , summary , ctx . Bundle ) ,
RelevantAlerts : alerts ,
HourlyPeriods : hourly ,
DailyPeriods : daily ,
NarrativePeriods : narrative ,
WindowSummary : summary ,
Discussion : buildDiscussion ( ctx . Bundle . Discussion ) ,
WeatherStory : buildWeatherStory ( ctx . Bundle ) ,
}
2026-05-29 20:04:27 -05:00
pkg := buildPackage ( ctx )
pkg . Storm = storm
2026-05-29 19:34:23 -05:00
setRelevantAlertCount ( & pkg . Metadata , len ( alerts ) )
return pkg , nil
2026-05-29 18:28:25 +00:00
}
func stormHeadlines ( alerts [ ] forecast . AlertOverlap ) [ ] string {
var headlines [ ] string
for _ , alert := range alerts {
if alert . Headline != "" {
headlines = appendUnique ( headlines , alert . Headline )
continue
}
if alert . Event != "" {
headlines = appendUnique ( headlines , alert . Event )
}
}
if len ( headlines ) == 0 {
return [ ] string { "No active alert headline overlaps the selected storm window." }
}
return headlines
}
func stormHazards ( alerts [ ] forecast . AlertOverlap , summary forecast . DaypartSummary ) [ ] string {
hazards := map [ string ] struct { } { }
for _ , alert := range alerts {
if alert . Event != "" {
hazards [ alert . Event ] = struct { } { }
}
}
for _ , hazard := range hazardsForIndicators ( summary . Indicators ) {
hazards [ hazard ] = struct { } { }
}
if summary . MaxPrecipitationProbability != nil && summary . MaxPrecipitationProbability . Value >= 50 {
hazards [ "precipitation" ] = struct { } { }
}
if summary . PeakWindGust != nil && summary . PeakWindGust . Value >= 30 {
hazards [ "wind" ] = struct { } { }
}
out := sortedSet ( hazards )
if len ( out ) == 0 {
return [ ] string { "No storm-specific hazard signal stands out in the selected source data." }
}
return out
}
func mostLikelyStormScenario ( hourly [ ] forecast . ForecastPeriod , narrative [ ] forecast . ForecastPeriod , summary forecast . DaypartSummary ) [ ] string {
var items [ ] string
if summary . DominantCondition != "" {
items = append ( items , "Dominant hourly condition: " + summary . DominantCondition + "." )
}
if summary . MaxPrecipitationProbability != nil {
items = append ( items , fmt . Sprintf ( "Peak precipitation chance is near %.0f%% around %s." , summary . MaxPrecipitationProbability . Value , summary . MaxPrecipitationProbability . Time . Format ( "15:04" ) ) )
}
if summary . PeakWindGust != nil {
items = append ( items , fmt . Sprintf ( "Peak wind gust is near %.0f mph around %s." , summary . PeakWindGust . Value , summary . PeakWindGust . Time . Format ( "15:04" ) ) )
}
for _ , period := range narrative {
if period . TextDescription != "" {
items = append ( items , "Narrative guidance: " + period . TextDescription )
break
}
}
if len ( items ) == 0 && len ( hourly ) > 0 {
items = append ( items , "Hourly forecast periods are available, but no focused storm signal is prominent." )
}
if len ( items ) == 0 {
items = append ( items , "No active storm signal is evident from the selected forecast window." )
}
return items
}
func reasonableWorstCase ( alerts [ ] forecast . AlertOverlap , summary forecast . DaypartSummary ) [ ] string {
var items [ ] string
for _ , alert := range alerts {
label := alert . Event
if label == "" {
label = alert . Headline
}
if label != "" {
items = appendUnique ( items , "Alert scenario to consider: " + label + "." )
}
}
if summary . Indicators . Wind {
items = appendUnique ( items , "Wind impacts could be higher where stronger gusts occur." )
}
if summary . Indicators . Snow || summary . Indicators . Ice {
items = appendUnique ( items , "Wintry precipitation could create travel impacts if it overlaps the event window." )
}
if len ( items ) == 0 {
items = append ( items , "No clear reasonable worst-case signal is represented in the selected data." )
}
return items
}
func stormConfidenceInputs ( bundle * forecast . Bundle ) [ ] string {
var items [ ] string
if bundle == nil {
return [ ] string { "No source bundle was available for confidence context." }
}
if bundle . Discussion != nil {
items = appendUnique ( items , bundle . Discussion . KeyMessages ... )
2026-05-29 23:29:04 -05:00
if bundle . Discussion . ShortTerm != nil && bundle . Discussion . ShortTerm . Text != "" {
2026-05-29 18:28:25 +00:00
items = appendUnique ( items , "Short-term discussion is available for confidence context." )
}
}
if bundle . WeatherStory != nil && len ( bundle . WeatherStory . Raw ) > 0 {
items = appendUnique ( items , "Weather story source is available." )
}
for _ , warning := range bundle . Warnings {
if warning . Code != "" {
items = appendUnique ( items , "Source warning: " + warning . Code + "." )
}
}
if len ( items ) == 0 {
items = append ( items , "No explicit confidence or uncertainty signal was available from the selected source context." )
}
return items
}
func stormWatchItems ( alerts [ ] forecast . AlertOverlap , summary forecast . DaypartSummary , bundle * forecast . Bundle ) [ ] string {
var items [ ] string
if len ( alerts ) > 0 {
items = append ( items , "Watch for alert extensions, cancellations, or upgrades." )
}
if summary . MaxPrecipitationProbability != nil {
items = append ( items , "Watch precipitation timing and probability trends." )
}
if summary . PeakWindGust != nil {
items = append ( items , "Watch wind gust trends." )
}
if bundle != nil && bundle . Discussion != nil {
items = append ( items , "Watch the next forecast discussion update for confidence changes." )
}
if len ( items ) == 0 {
items = append ( items , "Watch for new alerts or stronger wording if the weather pattern changes." )
}
return appendUnique ( nil , items ... )
}