// Package module defines stable module contracts for prompt-facing stanzas. package module import ( "encoding/json" "fmt" ) const SnapshotSchemaVersion = "weatherreporter.modules.v1" type ID string const ( Metadata ID = "metadata" CurrentConditions ID = "current_conditions" NarrativeForecast ID = "narrative_forecast" HourlyForecast ID = "hourly_forecast" DerivedDailySummary ID = "derived_daily_summary" DerivedDaypartSummaries ID = "derived_daypart_summaries" PrecipTiming ID = "precip_timing" AlertDigest ID = "alert_digest" AreaForecastDiscussion ID = "area_forecast_discussion" WeatherStory ID = "weather_story" SPCConvectiveOutlooks ID = "spc_convective_outlooks" SPCConvectiveDiscussion ID = "spc_convective_discussion" OutdoorWindows ID = "outdoor_windows" TodayPlanning ID = "today_planning" TomorrowPlanning ID = "tomorrow_planning" DailyPlanning ID = "daily_planning" ) type ConfigItem struct { ID ID `json:"id"` Options any `json:"options,omitempty"` } type Output struct { ID ID `json:"id"` StanzaName string `json:"stanzaName"` Value any `json:"value"` PromptValue any `json:"-" yaml:"-"` } type Snapshot struct { SchemaVersion string `json:"schemaVersion"` Outputs []Output `json:"outputs"` } func (o Output) DataPackageValue() any { if o.PromptValue != nil { return o.PromptValue } return o.Value } func NewSnapshot(outputs []Output) (Snapshot, error) { snapshot := Snapshot{ SchemaVersion: SnapshotSchemaVersion, Outputs: append([]Output(nil), outputs...), } if err := snapshot.Validate(); err != nil { return Snapshot{}, err } return snapshot, nil } func (s Snapshot) Validate() error { if s.SchemaVersion == "" { return fmt.Errorf("schemaVersion is required") } seenModules := map[ID]struct{}{} seenStanzas := map[string]struct{}{} for i, output := range s.Outputs { if output.ID == "" { return fmt.Errorf("outputs[%d].id is required", i) } if output.StanzaName == "" { return fmt.Errorf("outputs[%d].stanzaName is required", i) } if _, ok := seenModules[output.ID]; ok { return fmt.Errorf("duplicate module output %q", output.ID) } seenModules[output.ID] = struct{}{} if _, ok := seenStanzas[output.StanzaName]; ok { return fmt.Errorf("duplicate stanza name %q", output.StanzaName) } seenStanzas[output.StanzaName] = struct{}{} } return nil } func (s Snapshot) LookupStanza(name string) (Output, bool) { for _, output := range s.Outputs { if output.StanzaName == name { return output, true } } return Output{}, false } func StanzaValue[T any](s Snapshot, name string) (T, bool, error) { var zero T output, ok := s.LookupStanza(name) if !ok { return zero, false, nil } data, err := json.Marshal(output.Value) if err != nil { return zero, true, fmt.Errorf("marshal stanza %q: %w", name, err) } if err := json.Unmarshal(data, &zero); err != nil { return zero, true, fmt.Errorf("decode stanza %q: %w", name, err) } return zero, true, nil } type FactRequirement string const ( CollectedCurrentConditions FactRequirement = "collected.current_conditions" CollectedNarrativeForecast FactRequirement = "collected.narrative_forecast" CollectedHourlyForecast FactRequirement = "collected.hourly_forecast" CollectedAlerts FactRequirement = "collected.alerts" CollectedDiscussion FactRequirement = "collected.discussion" CollectedWeatherStory FactRequirement = "collected.weather_story" CollectedSPCConvectiveOutlooks FactRequirement = "collected.spc_convective_outlooks" CollectedSourceMetadata FactRequirement = "collected.source_metadata" RequiresDerivedHourlyPeriods FactRequirement = "derived.hourly_periods" RequiresDerivedNarrativePeriods FactRequirement = "derived.narrative_periods" RequiresDerivedAlertOverlaps FactRequirement = "derived.alert_overlaps" RequiresDerivedDailySummaries FactRequirement = "derived.daily_summaries" RequiresDerivedDaypartSummaries FactRequirement = "derived.daypart_summaries" RequiresDerivedPrecipTiming FactRequirement = "derived.precip_timing" RequiresDerivedSPCConvectiveOutlooks FactRequirement = "derived.spc_convective_outlooks" ) type MissingDataBehavior string const ( MissingDataOmit MissingDataBehavior = "omit" MissingDataEmpty MissingDataBehavior = "empty" MissingDataError MissingDataBehavior = "error" MissingDataWarn MissingDataBehavior = "warn" ) type MetadataOptions struct{} type CurrentConditionsOptions struct{} type NarrativeForecastOptions struct{} type HourlyForecastOptions struct{} type DerivedDailySummaryOptions struct{} type DerivedDaypartSummariesOptions struct{} type PrecipTimingOptions struct{} type AlertDigestOptions struct{} type AreaForecastDiscussionOptions struct { Sections []string `json:"sections,omitempty" yaml:"sections,omitempty"` } type WeatherStoryOptions struct{} type SPCConvectiveOutlooksOptions struct{} type SPCConvectiveDiscussionOptions struct{} type OutdoorWindowsOptions struct{} type TodayPlanningOptions struct{} type TomorrowPlanningOptions struct{} type DailyPlanningOptions struct{}