Add a new narrative forecast module and remove the unimplemented daily forecast stub

This commit is contained in:
2026-06-10 08:24:50 -05:00
parent 7b760a0823
commit c3da3af2f4
19 changed files with 255 additions and 44 deletions

View File

@@ -129,8 +129,8 @@ name the variable only; they should not contain the token value.
- `default`: missing-source behavior for optional sources. One of `error`, `warn`, or `none`. Default: `warn`.
- `sources`: optional map of source-specific overrides, using the same policy values.
Hourly forecast data is required for generated reports. Optional sources and
stub source slots use the missing-source policy.
Hourly forecast data is required for generated reports. Optional sources use
the missing-source policy.
### `scriptorium`
@@ -196,6 +196,7 @@ reports:
deterministic_modules:
- metadata
- current_conditions
- narrative_forecast
- id: area_forecast_discussion
options:
sections:
@@ -209,8 +210,8 @@ combinations, duplicate stanza names, and invalid options fail config loading.
includes all available AFD sections.
The module registry accepts all module IDs documented in
[Module Contract Internals](internal/module.md). Modules without builders are
valid in composition but do not emit YAML stanzas.
[Module Contract Internals](internal/module.md). Unknown or unimplemented
module IDs fail validation instead of being skipped.
## Secrets

View File

@@ -89,10 +89,6 @@ source-specific `missing_source.sources` policy:
- `discussion` for `/discussion`
- `weather_story` for `/weatherstories/latest`
The adapter also creates a missing stub source record for `daily` because that
source slot exists in the internal bundle but is not fetched from the Weather
API.
Policy behavior:
- `error`: fail the fetch for that source

View File

@@ -27,15 +27,14 @@ Outputs:
- `ModuleDefinition` values with module ID, stanza name, option type,
supported reports, fact requirements, missing-data behavior, and builder
- `module.Output` values for source-oriented stanzas:
`metadata`, `current_conditions`, `alert_digest`,
`metadata`, `current_conditions`, `narrative_forecast`, `alert_digest`,
`area_forecast_discussion`, and `weather_story`
- `module.Output` values for derived stanzas:
`derived_daily_summary`, `derived_daypart_summaries`, `precip_timing`,
`outdoor_windows`, and `tomorrow_planning`
The registry also contains accepted composition entries for modules that do not
emit stanzas until a builder exists. App orchestration skips those entries when
constructing snapshots.
Every registered composition entry has a builder. Unknown or unimplemented
module IDs fail validation instead of being skipped.
## Boundaries

View File

@@ -31,6 +31,7 @@ The registry recognizes these IDs:
- `metadata`
- `current_conditions`
- `narrative_forecast`
- `derived_daily_summary`
- `derived_daypart_summaries`
- `precip_timing`

View File

@@ -39,6 +39,7 @@ report:
briefing:
metadata: {}
current_conditions: {}
narrative_forecast: {}
recent_changes:
items: []
```

View File

@@ -22,7 +22,6 @@ Outputs:
- `weatherdata.Bundle` with observation, current conditions, hourly forecast,
narrative forecast, active alerts, discussion, latest weather story, source
records, and source warnings
- stub source record for the daily forecast source slot
- optional saved bundle JSON through app fetch helpers
## Boundaries
@@ -70,7 +69,7 @@ data is required and cannot be skipped.
- HTTP errors, response read failures, and envelope decode failures include
endpoint context.
- Missing hourly data or hourly forecasts with no periods fail bundle fetch.
- Optional and stub sources follow missing-source policy.
- Optional sources follow missing-source policy.
- Explicit `data: null` from `/alerts/active` produces an empty, non-missing
alert run.

View File

@@ -72,6 +72,7 @@ reports:
deterministic_modules:
- metadata
- current_conditions
- narrative_forecast
- derived_daily_summary
- derived_daypart_summaries
- precip_timing

View File

@@ -112,9 +112,6 @@ func (c *Client) FetchBundle(ctx context.Context) (*weatherdata.Bundle, error) {
if err := builder.fetchWeatherStory(ctx); err != nil {
return nil, err
}
if err := builder.addStub("daily", "daily forecast data is not available from the weather API yet"); err != nil {
return nil, err
}
return builder.bundle, nil
}
@@ -267,15 +264,6 @@ func (b *bundleBuilder) fetchWeatherStory(ctx context.Context) error {
return nil
}
func (b *bundleBuilder) addStub(sourceName string, message string) error {
source := weatherdata.Source{
Name: sourceName,
FetchedAt: b.fetchedAt,
Missing: true,
}
return b.applyMissingPolicy(&source, "missing_source", message)
}
func (b *bundleBuilder) handleMissing(source *weatherdata.Source, message string, required bool) error {
source.Missing = true
if required {

View File

@@ -55,11 +55,11 @@ func TestFetchBundleFromFixtures(t *testing.T) {
if bundle.WeatherStory.UpdatedAt == nil {
t.Fatalf("WeatherStory.UpdatedAt = nil, want update timestamp")
}
if len(bundle.Sources) != 8 {
t.Fatalf("Sources length = %d, want 8", len(bundle.Sources))
if len(bundle.Sources) != 7 {
t.Fatalf("Sources length = %d, want 7", len(bundle.Sources))
}
if len(bundle.Warnings) != 1 {
t.Fatalf("Warnings length = %d, want daily warning", len(bundle.Warnings))
if len(bundle.Warnings) != 0 {
t.Fatalf("Warnings length = %d, want no warnings", len(bundle.Warnings))
}
if !containsPath(requested, "/forecast/hourly") || containsPath(requested, "/forecast/hourly/today") {
t.Fatalf("requested paths = %v, want full hourly endpoint only", requested)
@@ -197,7 +197,7 @@ func TestMissingSourcePolicyWarnNoneError(t *testing.T) {
wantWarns int
wantSource bool
}{
{name: "warn", policy: config.MissingSourceWarn, wantWarns: 2, wantSource: true},
{name: "warn", policy: config.MissingSourceWarn, wantWarns: 1, wantSource: true},
{name: "none", policy: config.MissingSourceNone, wantWarns: 0, wantSource: true},
{name: "error", policy: config.MissingSourceError, wantErr: true},
}

View File

@@ -161,9 +161,19 @@ func TestGenerateReportWritesReportAndPreflight(t *testing.T) {
if !strings.Contains(string(data), "schema_version: weatherreporter.data_package.v2") ||
!strings.Contains(string(data), "recent_changes:") ||
!strings.Contains(string(data), "current_conditions:") ||
!strings.Contains(string(data), "narrative_forecast:") ||
!strings.Contains(string(data), "area_forecast_discussion:") {
t.Fatalf("data package missing expected content:\n%s", string(data))
}
if strings.Contains(string(data), "source_warnings:") {
t.Fatalf("data package has source warnings, want none for complete fetched sources:\n%s", string(data))
}
currentIndex := strings.Index(string(data), " current_conditions:")
narrativeIndex := strings.Index(string(data), " narrative_forecast:")
summaryIndex := strings.Index(string(data), " derived_daily_summary:")
if currentIndex < 0 || narrativeIndex < 0 || summaryIndex < 0 || !(currentIndex < narrativeIndex && narrativeIndex < summaryIndex) {
t.Fatalf("data package stanza order is wrong, want current_conditions then narrative_forecast then derived_daily_summary:\n%s", string(data))
}
savedDataPackage, err := promptinput.LoadYAML(data)
if err != nil {
t.Fatalf("decode data package: %v", err)
@@ -178,6 +188,10 @@ func TestGenerateReportWritesReportAndPreflight(t *testing.T) {
if !ok || current["condition_text"] != "Clear" {
t.Fatalf("data package current conditions = %#v, want current conditions", savedDataPackage.Briefing.Values["current_conditions"])
}
narrative, ok := savedDataPackage.Briefing.Values["narrative_forecast"].(map[string]any)
if !ok || narrative["product"] != "narrative" || !strings.Contains(string(data), "Morning storms, then partly sunny.") {
t.Fatalf("data package narrative forecast = %#v, want narrative forecast", savedDataPackage.Briefing.Values["narrative_forecast"])
}
story, ok := savedDataPackage.Briefing.Values["weather_story"].(map[string]any)
if !ok || story["title"] != "Several Chances for Rain Through Monday" {
t.Fatalf("data package weather story = %#v, want weather story title", savedDataPackage.Briefing.Values["weather_story"])
@@ -954,8 +968,11 @@ func TestInspectGeneratedReportArtifacts(t *testing.T) {
if err != nil {
t.Fatalf("InspectSources() error = %v", err)
}
if len(sources.Sources) == 0 || len(sources.Warnings) == 0 {
t.Fatalf("sources = %#v, want provenance and warnings", sources)
if len(sources.Sources) == 0 {
t.Fatalf("sources = %#v, want provenance", sources)
}
if len(sources.Warnings) != 0 {
t.Fatalf("sources warnings = %#v, want none for complete fetched sources", sources.Warnings)
}
}

View File

@@ -24,6 +24,7 @@ func TestBaseModulesBuildAvailableSourceOutputs(t *testing.T) {
}{
{id: module.Metadata, stanza: "metadata"},
{id: module.CurrentConditions, stanza: "current_conditions"},
{id: module.NarrativeForecast, stanza: "narrative_forecast"},
{id: module.AlertDigest, stanza: "alert_digest"},
{id: module.AreaForecastDiscussion, stanza: "area_forecast_discussion"},
{id: module.WeatherStory, stanza: "weather_story"},
@@ -44,6 +45,51 @@ func TestBaseModulesBuildAvailableSourceOutputs(t *testing.T) {
}
}
func TestNarrativeForecastModuleUsesValidPeriodNarrativePeriods(t *testing.T) {
registry := MustDefaultModuleRegistry()
ctx := testModuleContext()
output, err := registry.BuildModule(ctx, module.ConfigItem{ID: module.NarrativeForecast})
if err != nil {
t.Fatalf("BuildModule() error = %v", err)
}
value := moduleValue[NarrativeForecastModule](t, output)
if value.Product != "narrative" || value.SourceLocationID != "test-grid" || len(value.Periods) != 1 {
t.Fatalf("NarrativeForecast = %#v, want narrative metadata and one valid-period period", value)
}
period := value.Periods[0]
if period.Name != "Today" || period.TextDescription != "Morning storms, then partly sunny." {
t.Fatalf("NarrativeForecast period = %#v, want Today narrative", period)
}
if period.IsDay == nil || !*period.IsDay || period.TemperatureF == nil || *period.TemperatureF != 81 || period.ProbabilityOfPrecipitationPercent == nil || *period.ProbabilityOfPrecipitationPercent != 60 {
t.Fatalf("NarrativeForecast period = %#v, want day, temperature, and precip values", period)
}
data, err := json.Marshal(output.Value)
if err != nil {
t.Fatalf("Marshal narrative forecast: %v", err)
}
jsonText := string(data)
for _, field := range []string{"source_location_id", "text_description", "temperature_f", "wind_speed_mph", "probability_of_precipitation_percent"} {
if !strings.Contains(jsonText, field) {
t.Fatalf("narrative json = %s, want field %s", jsonText, field)
}
}
if strings.Contains(jsonText, "Tomorrow night") {
t.Fatalf("narrative json = %s, want only valid-period narrative periods", jsonText)
}
}
func TestNarrativeForecastModuleRejectsUnsupportedReports(t *testing.T) {
registry := MustDefaultModuleRegistry()
ctx := testModuleContext()
ctx.Resolved.Definition = report.DefaultRegistry().MustLookup(report.Weekend)
_, err := registry.BuildModule(ctx, module.ConfigItem{ID: module.NarrativeForecast})
if err == nil || !strings.Contains(err.Error(), `module "narrative_forecast" is not compatible with report "weekend"`) {
t.Fatalf("BuildModule() error = %v, want incompatible report", err)
}
}
func TestMetadataModuleUsesPromptSafeSourceWarningSummary(t *testing.T) {
registry := MustDefaultModuleRegistry()
ctx := testModuleContext()
@@ -130,10 +176,12 @@ func TestBaseModulesOmitMissingOptionalOutputs(t *testing.T) {
registry := MustDefaultModuleRegistry()
ctx := testModuleContext()
ctx.Collected.Current = nil
ctx.Collected.Narrative = nil
ctx.Derived.ValidPeriodNarrativePeriods = nil
ctx.Collected.Discussion = nil
ctx.Collected.WeatherStory = nil
for _, id := range []module.ID{module.CurrentConditions, module.AreaForecastDiscussion, module.WeatherStory} {
for _, id := range []module.ID{module.CurrentConditions, module.NarrativeForecast, module.AreaForecastDiscussion, module.WeatherStory} {
output, err := registry.BuildModule(ctx, module.ConfigItem{ID: id})
if err != nil {
t.Fatalf("BuildModule(%s) error = %v", id, err)
@@ -212,6 +260,9 @@ func testModuleContext() ModuleContext {
humidity := 71.0
windMph := 8.0
windDirection := 190.0
narrativeTempF := 81.0
narrativePop := 60.0
narrativeWind := 12.0
updatedAt := mustParseModuleTime("2026-05-29T07:30:00-05:00")
return ModuleContext{
Resolved: resolved,
@@ -225,6 +276,25 @@ func testModuleContext() ModuleContext {
WindSpeedMph: &windMph,
WindDirectionDegrees: &windDirection,
},
Narrative: &weatherdata.ForecastRun{
LocationID: "test-grid",
LocationName: "Testville",
IssuedAt: mustParseModuleTime("2026-05-29T10:30:00-05:00"),
UpdatedAt: &updatedAt,
Product: "narrative",
Periods: []weatherdata.ForecastPeriod{
{
Name: "Today",
StartTime: mustParseModuleTime("2026-05-29T06:00:00-05:00"),
EndTime: mustParseModuleTime("2026-05-29T18:00:00-05:00"),
IsDay: &isDay,
TextDescription: "Morning storms, then partly sunny.",
TemperatureF: floatPtr(narrativeTempF),
WindSpeedMph: &narrativeWind,
ProbabilityOfPrecipitationPercent: &narrativePop,
},
},
},
Alerts: &weatherdata.AlertRun{Alerts: []json.RawMessage{
json.RawMessage(`{"event":"Flood Watch","headline":"Flooding possible","severity":"Moderate"}`),
}},
@@ -257,6 +327,18 @@ func testModuleContext() ModuleContext {
}},
},
Derived: facts.DerivedFacts{
ValidPeriodNarrativePeriods: []weatherdata.ForecastPeriod{
{
Name: "Today",
StartTime: mustParseModuleTime("2026-05-29T06:00:00-05:00"),
EndTime: mustParseModuleTime("2026-05-29T18:00:00-05:00"),
IsDay: &isDay,
TextDescription: "Morning storms, then partly sunny.",
TemperatureF: floatPtr(narrativeTempF),
WindSpeedMph: &narrativeWind,
ProbabilityOfPrecipitationPercent: &narrativePop,
},
},
AlertOverlaps: []forecast.AlertOverlap{{
Event: "Flood Watch",
Headline: "Flooding possible",

View File

@@ -194,6 +194,15 @@ func derivedModuleContext(id report.ID) ModuleContext {
derivedHour("2026-05-29T12:00:00-05:00", "Thunderstorms with gusty wind", 80, 96, floatPtr(101), 42),
derivedHour("2026-05-29T13:00:00-05:00", "Heavy rain", 70, 82, nil, 30),
}
narrative := []weatherdata.ForecastPeriod{
{
Name: "Today",
StartTime: mustParseModuleTime("2026-05-29T06:00:00-05:00"),
EndTime: mustParseModuleTime("2026-05-29T18:00:00-05:00"),
TextDescription: "Morning storms, then partly sunny.",
TemperatureF: floatPtr(81),
},
}
summary.Dayparts[2].AlertOverlaps = []forecast.AlertOverlap{{Event: "Severe Thunderstorm Watch"}}
return ModuleContext{
Resolved: report.Resolved{
@@ -202,11 +211,19 @@ func derivedModuleContext(id report.ID) ModuleContext {
Timezone: "America/Chicago",
ValidPeriod: summary.Period,
},
Collected: facts.CollectedFacts{
Narrative: &weatherdata.ForecastRun{
IssuedAt: mustParseModuleTime("2026-05-29T10:30:00-05:00"),
Product: "narrative",
Periods: append([]weatherdata.ForecastPeriod(nil), narrative...),
},
},
Derived: facts.DerivedFacts{
ValidPeriodHourlyPeriods: hours,
DailySummaries: []forecast.DailySummary{summary},
DaypartSummaries: append([]forecast.DaypartSummary(nil), summary.Dayparts...),
PrecipTiming: forecast.BuildPrecipTiming(hours),
ValidPeriodHourlyPeriods: hours,
ValidPeriodNarrativePeriods: narrative,
DailySummaries: []forecast.DailySummary{summary},
DaypartSummaries: append([]forecast.DaypartSummary(nil), summary.Dayparts...),
PrecipTiming: forecast.BuildPrecipTiming(hours),
},
Units: "us",
Timezone: "America/Chicago",

View File

@@ -152,6 +152,8 @@ func collectedFactAvailable(requirement module.FactRequirement, ctx ModuleContex
switch requirement {
case module.CollectedCurrentConditions:
return ctx.Collected.Current != nil
case module.CollectedNarrativeForecast:
return ctx.Collected.Narrative != nil
case module.CollectedAlerts:
return ctx.Collected.Alerts != nil
case module.CollectedDiscussion:
@@ -262,6 +264,16 @@ func defaultModuleDefinitions() []ModuleDefinition {
MissingData: module.MissingDataOmit,
Builder: buildCurrentConditionsModule,
},
{
ID: module.NarrativeForecast,
StanzaName: "narrative_forecast",
DefaultOptions: module.NarrativeForecastOptions{},
RequiredCollected: []module.FactRequirement{module.CollectedNarrativeForecast},
RequiredDerived: []module.FactRequirement{module.RequiresDerivedNarrativePeriods},
SupportedReports: []report.ID{report.DailyToday, report.DailyTomorrow},
MissingData: module.MissingDataOmit,
Builder: buildNarrativeForecastModule,
},
{
ID: module.DerivedDailySummary,
StanzaName: "derived_daily_summary",

View File

@@ -0,0 +1,91 @@
package briefing
import (
"time"
"gitea.maximumdirect.net/eric/weatherreporter/internal/module"
"gitea.maximumdirect.net/eric/weatherreporter/internal/weatherdata"
)
type NarrativeForecastModule struct {
Product string `json:"product,omitempty"`
IssuedAt time.Time `json:"issued_at,omitempty"`
UpdatedAt *time.Time `json:"updated_at,omitempty"`
SourceLocation string `json:"source_location,omitempty"`
SourceLocationID string `json:"source_location_id,omitempty"`
Periods []NarrativeForecastPeriod `json:"periods,omitempty"`
}
type NarrativeForecastPeriod struct {
Name string `json:"name,omitempty"`
StartTime time.Time `json:"start_time"`
EndTime time.Time `json:"end_time"`
IsDay *bool `json:"is_day,omitempty"`
TextDescription string `json:"text_description,omitempty"`
TemperatureC *float64 `json:"temperature_c,omitempty"`
TemperatureF *float64 `json:"temperature_f,omitempty"`
TemperatureCMin *float64 `json:"temperature_c_min,omitempty"`
TemperatureFMin *float64 `json:"temperature_f_min,omitempty"`
TemperatureCMax *float64 `json:"temperature_c_max,omitempty"`
TemperatureFMax *float64 `json:"temperature_f_max,omitempty"`
WindSpeedKmh *float64 `json:"wind_speed_kmh,omitempty"`
WindSpeedMph *float64 `json:"wind_speed_mph,omitempty"`
WindGustKmh *float64 `json:"wind_gust_kmh,omitempty"`
WindGustMph *float64 `json:"wind_gust_mph,omitempty"`
WindDirectionDegrees *float64 `json:"wind_direction_degrees,omitempty"`
ProbabilityOfPrecipitationPercent *float64 `json:"probability_of_precipitation_percent,omitempty"`
}
func buildNarrativeForecastModule(ctx ModuleContext, _ any) (*module.Output, error) {
narrative := ctx.Collected.Narrative
if narrative == nil || len(ctx.Derived.ValidPeriodNarrativePeriods) == 0 {
return nil, nil
}
value := NarrativeForecastModule{
Product: narrative.Product,
IssuedAt: narrative.IssuedAt,
UpdatedAt: copyTime(narrative.UpdatedAt),
SourceLocation: narrative.LocationName,
SourceLocationID: narrative.LocationID,
Periods: narrativeForecastPeriods(ctx.Derived.ValidPeriodNarrativePeriods),
}
if value.isEmpty() {
return nil, nil
}
return &module.Output{ID: module.NarrativeForecast, StanzaName: "narrative_forecast", Value: value}, nil
}
func narrativeForecastPeriods(periods []weatherdata.ForecastPeriod) []NarrativeForecastPeriod {
out := make([]NarrativeForecastPeriod, 0, len(periods))
for _, period := range periods {
out = append(out, NarrativeForecastPeriod{
Name: period.Name,
StartTime: period.StartTime,
EndTime: period.EndTime,
IsDay: copyBool(period.IsDay),
TextDescription: period.TextDescription,
TemperatureC: copyFloat(period.TemperatureC),
TemperatureF: copyFloat(period.TemperatureF),
TemperatureCMin: copyFloat(period.TemperatureCMin),
TemperatureFMin: copyFloat(period.TemperatureFMin),
TemperatureCMax: copyFloat(period.TemperatureCMax),
TemperatureFMax: copyFloat(period.TemperatureFMax),
WindSpeedKmh: copyFloat(period.WindSpeedKmh),
WindSpeedMph: copyFloat(period.WindSpeedMph),
WindGustKmh: copyFloat(period.WindGustKmh),
WindGustMph: copyFloat(period.WindGustMph),
WindDirectionDegrees: copyFloat(period.WindDirectionDegrees),
ProbabilityOfPrecipitationPercent: copyFloat(period.ProbabilityOfPrecipitationPercent),
})
}
return out
}
func (v NarrativeForecastModule) isEmpty() bool {
return v.Product == "" &&
v.IssuedAt.IsZero() &&
v.UpdatedAt == nil &&
v.SourceLocation == "" &&
v.SourceLocationID == "" &&
len(v.Periods) == 0
}

View File

@@ -736,8 +736,8 @@ func TestRunInspectGeneratedArtifacts(t *testing.T) {
sourcesOutput = stdout.String()
}
}
if !strings.Contains(sourcesOutput, `"warnings"`) {
t.Fatalf("inspect sources output missing warnings:\n%s", sourcesOutput)
if !strings.Contains(sourcesOutput, `"name": "narrative"`) || strings.Contains(sourcesOutput, `"name": "daily"`) {
t.Fatalf("inspect sources output missing narrative source or has unexpected daily source:\n%s", sourcesOutput)
}
}

View File

@@ -17,7 +17,7 @@ func TestBuildCollectedCopiesBundleFactsAndKeepsSourcesSeparate(t *testing.T) {
Current: &weatherdata.Current{ConditionText: "Clear"},
Hourly: &weatherdata.ForecastRun{Product: "hourly"},
Sources: []weatherdata.Source{{Name: "hourly"}},
Warnings: []weatherdata.SourceWarning{{Source: "daily", Code: "missing_source"}},
Warnings: []weatherdata.SourceWarning{{Source: "discussion", Code: "missing_source"}},
}
collected := BuildCollected(bundle)
@@ -27,13 +27,13 @@ func TestBuildCollectedCopiesBundleFactsAndKeepsSourcesSeparate(t *testing.T) {
if len(collected.SourceProvenance) != 1 || collected.SourceProvenance[0].Name != "hourly" {
t.Fatalf("SourceProvenance = %#v, want hourly source", collected.SourceProvenance)
}
if len(collected.SourceWarnings) != 1 || collected.SourceWarnings[0].Source != "daily" {
t.Fatalf("SourceWarnings = %#v, want daily warning", collected.SourceWarnings)
if len(collected.SourceWarnings) != 1 || collected.SourceWarnings[0].Source != "discussion" {
t.Fatalf("SourceWarnings = %#v, want discussion warning", collected.SourceWarnings)
}
bundle.Sources[0].Name = "changed"
bundle.Warnings[0].Source = "changed"
if collected.SourceProvenance[0].Name != "hourly" || collected.SourceWarnings[0].Source != "daily" {
if collected.SourceProvenance[0].Name != "hourly" || collected.SourceWarnings[0].Source != "discussion" {
t.Fatalf("collected source slices changed after bundle mutation: %#v %#v", collected.SourceProvenance, collected.SourceWarnings)
}
}

View File

@@ -13,6 +13,7 @@ type ID string
const (
Metadata ID = "metadata"
CurrentConditions ID = "current_conditions"
NarrativeForecast ID = "narrative_forecast"
DerivedDailySummary ID = "derived_daily_summary"
DerivedDaypartSummaries ID = "derived_daypart_summaries"
PrecipTiming ID = "precip_timing"
@@ -104,6 +105,7 @@ type FactRequirement string
const (
CollectedCurrentConditions FactRequirement = "collected.current_conditions"
CollectedNarrativeForecast FactRequirement = "collected.narrative_forecast"
CollectedAlerts FactRequirement = "collected.alerts"
CollectedDiscussion FactRequirement = "collected.discussion"
CollectedWeatherStory FactRequirement = "collected.weather_story"
@@ -127,6 +129,7 @@ const (
type MetadataOptions struct{}
type CurrentConditionsOptions struct{}
type NarrativeForecastOptions struct{}
type DerivedDailySummaryOptions struct{}
type DerivedDaypartSummariesOptions struct{}
type PrecipTimingOptions struct{}

View File

@@ -41,6 +41,7 @@ func dailyTodayModules() []module.ConfigItem {
return moduleItems(
module.Metadata,
module.CurrentConditions,
module.NarrativeForecast,
module.DerivedDailySummary,
module.DerivedDaypartSummaries,
module.PrecipTiming,

View File

@@ -262,6 +262,7 @@ func TestRegistryDefinitionsDeclareDefaultModules(t *testing.T) {
want: []module.ID{
module.Metadata,
module.CurrentConditions,
module.NarrativeForecast,
module.DerivedDailySummary,
module.DerivedDaypartSummaries,
module.PrecipTiming,
@@ -276,6 +277,7 @@ func TestRegistryDefinitionsDeclareDefaultModules(t *testing.T) {
want: []module.ID{
module.Metadata,
module.CurrentConditions,
module.NarrativeForecast,
module.DerivedDailySummary,
module.DerivedDaypartSummaries,
module.PrecipTiming,