Cleanup after implementation of the module architecture and remove completed roadmap files
This commit is contained in:
@@ -827,13 +827,6 @@ func BuildModuleSnapshotFromFacts(req ModuleSnapshotRequest, reportFacts ReportF
|
||||
}
|
||||
var outputs []module.Output
|
||||
for _, item := range req.Resolved.Definition.Modules {
|
||||
definition, err := registry.Lookup(item.ID)
|
||||
if err != nil {
|
||||
return module.Snapshot{}, err
|
||||
}
|
||||
if definition.Builder == nil {
|
||||
continue
|
||||
}
|
||||
output, err := registry.BuildModule(moduleContext, item)
|
||||
if err != nil {
|
||||
return module.Snapshot{}, err
|
||||
|
||||
@@ -3,6 +3,7 @@ package briefing
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
"gitea.maximumdirect.net/eric/weatherreporter/internal/facts"
|
||||
"gitea.maximumdirect.net/eric/weatherreporter/internal/module"
|
||||
@@ -61,6 +62,12 @@ func NewModuleRegistry(definitions []ModuleDefinition) (ModuleRegistry, error) {
|
||||
if _, ok := registry.definitions[definition.ID]; ok {
|
||||
return ModuleRegistry{}, fmt.Errorf("duplicate module definition %q", definition.ID)
|
||||
}
|
||||
if definition.Builder == nil {
|
||||
return ModuleRegistry{}, fmt.Errorf("module %q has no builder", definition.ID)
|
||||
}
|
||||
if definition.MissingData == module.MissingDataWarn {
|
||||
return ModuleRegistry{}, fmt.Errorf("module %q uses unsupported missing data behavior %q", definition.ID, definition.MissingData)
|
||||
}
|
||||
if existingID, ok := seenStanzas[definition.StanzaName]; ok {
|
||||
return ModuleRegistry{}, fmt.Errorf("duplicate stanza name %q for modules %q and %q", definition.StanzaName, existingID, definition.ID)
|
||||
}
|
||||
@@ -92,6 +99,20 @@ func (r ModuleRegistry) BuildModule(ctx ModuleContext, item module.ConfigItem) (
|
||||
if definition.Builder == nil {
|
||||
return nil, fmt.Errorf("module %q has no builder", item.ID)
|
||||
}
|
||||
missing := missingRequirements(definition, ctx)
|
||||
if len(missing) > 0 {
|
||||
switch definition.MissingData {
|
||||
case module.MissingDataOmit:
|
||||
return nil, nil
|
||||
case module.MissingDataError:
|
||||
return nil, fmt.Errorf("module %q missing required facts: %s", item.ID, strings.Join(missing, ", "))
|
||||
case module.MissingDataEmpty:
|
||||
case module.MissingDataWarn:
|
||||
return nil, fmt.Errorf("module %q uses unsupported missing data behavior %q", item.ID, definition.MissingData)
|
||||
default:
|
||||
return nil, fmt.Errorf("module %q has unknown missing data behavior %q", item.ID, definition.MissingData)
|
||||
}
|
||||
}
|
||||
options := item.Options
|
||||
if options == nil {
|
||||
options = definition.DefaultOptions
|
||||
@@ -112,6 +133,57 @@ func (r ModuleRegistry) BuildModule(ctx ModuleContext, item module.ConfigItem) (
|
||||
return output, nil
|
||||
}
|
||||
|
||||
func missingRequirements(definition ModuleDefinition, ctx ModuleContext) []string {
|
||||
var missing []string
|
||||
for _, requirement := range definition.RequiredCollected {
|
||||
if !collectedFactAvailable(requirement, ctx) {
|
||||
missing = append(missing, string(requirement))
|
||||
}
|
||||
}
|
||||
for _, requirement := range definition.RequiredDerived {
|
||||
if !derivedFactAvailable(requirement, ctx) {
|
||||
missing = append(missing, string(requirement))
|
||||
}
|
||||
}
|
||||
return missing
|
||||
}
|
||||
|
||||
func collectedFactAvailable(requirement module.FactRequirement, ctx ModuleContext) bool {
|
||||
switch requirement {
|
||||
case module.CollectedCurrentConditions:
|
||||
return ctx.Collected.Current != nil
|
||||
case module.CollectedAlerts:
|
||||
return ctx.Collected.Alerts != nil
|
||||
case module.CollectedDiscussion:
|
||||
return ctx.Collected.Discussion != nil
|
||||
case module.CollectedWeatherStory:
|
||||
return ctx.Collected.WeatherStory != nil
|
||||
case module.CollectedSourceMetadata:
|
||||
return len(ctx.Collected.SourceProvenance) > 0 || len(ctx.Collected.SourceWarnings) > 0
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func derivedFactAvailable(requirement module.FactRequirement, ctx ModuleContext) bool {
|
||||
switch requirement {
|
||||
case module.RequiresDerivedHourlyPeriods:
|
||||
return len(ctx.Derived.ValidPeriodHourlyPeriods) > 0
|
||||
case module.RequiresDerivedNarrativePeriods:
|
||||
return len(ctx.Derived.ValidPeriodNarrativePeriods) > 0
|
||||
case module.RequiresDerivedAlertOverlaps:
|
||||
return true
|
||||
case module.RequiresDerivedDailySummaries:
|
||||
return len(ctx.Derived.DailySummaries) > 0
|
||||
case module.RequiresDerivedDaypartSummaries:
|
||||
return len(ctx.Derived.DaypartSummaries) > 0
|
||||
case module.RequiresDerivedPrecipTiming:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func (r ModuleRegistry) ValidateComposition(reportID report.ID, items []module.ConfigItem) error {
|
||||
seenModules := map[module.ID]struct{}{}
|
||||
seenStanzas := map[string]module.ID{}
|
||||
@@ -208,14 +280,6 @@ func defaultModuleDefinitions() []ModuleDefinition {
|
||||
MissingData: module.MissingDataError,
|
||||
Builder: buildDerivedDaypartSummariesModule,
|
||||
},
|
||||
{
|
||||
ID: module.HourlyTable,
|
||||
StanzaName: "hourly_table",
|
||||
DefaultOptions: module.HourlyTableOptions{},
|
||||
RequiredDerived: []module.FactRequirement{module.RequiresDerivedHourlyPeriods},
|
||||
SupportedReports: []report.ID{report.Storm},
|
||||
MissingData: module.MissingDataError,
|
||||
},
|
||||
{
|
||||
ID: module.PrecipTiming,
|
||||
StanzaName: "precip_timing",
|
||||
@@ -253,13 +317,6 @@ func defaultModuleDefinitions() []ModuleDefinition {
|
||||
MissingData: module.MissingDataOmit,
|
||||
Builder: buildWeatherStoryModule,
|
||||
},
|
||||
{
|
||||
ID: module.ForecastDelta,
|
||||
StanzaName: "forecast_delta",
|
||||
DefaultOptions: module.ForecastDeltaOptions{},
|
||||
SupportedReports: []report.ID{report.DailyToday, report.DailyTomorrow, report.ThreeDay},
|
||||
MissingData: module.MissingDataEmpty,
|
||||
},
|
||||
{
|
||||
ID: module.OutdoorWindows,
|
||||
StanzaName: "outdoor_windows",
|
||||
@@ -278,21 +335,5 @@ func defaultModuleDefinitions() []ModuleDefinition {
|
||||
MissingData: module.MissingDataEmpty,
|
||||
Builder: buildTomorrowPlanningModule,
|
||||
},
|
||||
{
|
||||
ID: module.WeekendPlanning,
|
||||
StanzaName: "weekend_planning",
|
||||
DefaultOptions: module.WeekendPlanningOptions{},
|
||||
RequiredDerived: []module.FactRequirement{module.RequiresDerivedDailySummaries},
|
||||
SupportedReports: []report.ID{report.Weekend},
|
||||
MissingData: module.MissingDataEmpty,
|
||||
},
|
||||
{
|
||||
ID: module.StormWindowSummary,
|
||||
StanzaName: "storm_window_summary",
|
||||
DefaultOptions: module.StormWindowSummaryOptions{},
|
||||
RequiredDerived: []module.FactRequirement{module.RequiresDerivedStormWindowSummary},
|
||||
SupportedReports: []report.ID{report.Storm},
|
||||
MissingData: module.MissingDataError,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,41 @@ func TestDefaultModuleRegistryValidatesReportDefaults(t *testing.T) {
|
||||
if err := registry.ValidateComposition(definition.ID, definition.Modules); err != nil {
|
||||
t.Fatalf("ValidateComposition(%s) error = %v", definition.ID, err)
|
||||
}
|
||||
for _, item := range definition.Modules {
|
||||
moduleDefinition, err := registry.Lookup(item.ID)
|
||||
if err != nil {
|
||||
t.Fatalf("Lookup(%s) error = %v", item.ID, err)
|
||||
}
|
||||
if moduleDefinition.Builder == nil {
|
||||
t.Fatalf("report %s module %s has no builder", definition.ID, item.ID)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestDefaultReportModulesBuildSnapshots(t *testing.T) {
|
||||
registry := MustDefaultModuleRegistry()
|
||||
for _, definition := range report.DefaultRegistry().All() {
|
||||
t.Run(string(definition.ID), func(t *testing.T) {
|
||||
ctx := derivedModuleContext(definition.ID)
|
||||
var outputs []module.Output
|
||||
for _, item := range definition.Modules {
|
||||
output, err := registry.BuildModule(ctx, item)
|
||||
if err != nil {
|
||||
t.Fatalf("BuildModule(%s) error = %v", item.ID, err)
|
||||
}
|
||||
if output != nil {
|
||||
outputs = append(outputs, *output)
|
||||
}
|
||||
}
|
||||
snapshot, err := module.NewSnapshot(outputs)
|
||||
if err != nil {
|
||||
t.Fatalf("NewSnapshot() error = %v", err)
|
||||
}
|
||||
if len(snapshot.Outputs) == 0 {
|
||||
t.Fatal("snapshot outputs = 0, want default report modules")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,8 +73,8 @@ func TestModuleRegistryRejectsDuplicateModuleIDs(t *testing.T) {
|
||||
|
||||
func TestModuleRegistryRejectsDuplicateStanzaNames(t *testing.T) {
|
||||
_, err := NewModuleRegistry([]ModuleDefinition{
|
||||
{ID: module.Metadata, StanzaName: "metadata", DefaultOptions: module.MetadataOptions{}},
|
||||
{ID: module.CurrentConditions, StanzaName: "metadata", DefaultOptions: module.CurrentConditionsOptions{}},
|
||||
{ID: module.Metadata, StanzaName: "metadata", DefaultOptions: module.MetadataOptions{}, Builder: noopModuleBuilder},
|
||||
{ID: module.CurrentConditions, StanzaName: "metadata", DefaultOptions: module.CurrentConditionsOptions{}, Builder: noopModuleBuilder},
|
||||
})
|
||||
if err == nil || !strings.Contains(err.Error(), `duplicate stanza name "metadata"`) {
|
||||
t.Fatalf("error = %v, want duplicate stanza name", err)
|
||||
@@ -48,12 +83,30 @@ func TestModuleRegistryRejectsDuplicateStanzaNames(t *testing.T) {
|
||||
|
||||
func TestModuleRegistryRejectsIncompatibleReports(t *testing.T) {
|
||||
registry := MustDefaultModuleRegistry()
|
||||
err := registry.ValidateComposition(report.DailyToday, []module.ConfigItem{{ID: module.StormWindowSummary}})
|
||||
if err == nil || !strings.Contains(err.Error(), `module "storm_window_summary" is not compatible with report "daily_today"`) {
|
||||
err := registry.ValidateComposition(report.DailyToday, []module.ConfigItem{{ID: module.TomorrowPlanning}})
|
||||
if err == nil || !strings.Contains(err.Error(), `module "tomorrow_planning" is not compatible with report "daily_today"`) {
|
||||
t.Fatalf("error = %v, want incompatible report", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestModuleRegistryRejectsDefinitionsWithoutBuilders(t *testing.T) {
|
||||
_, err := NewModuleRegistry([]ModuleDefinition{
|
||||
{ID: module.Metadata, StanzaName: "metadata", DefaultOptions: module.MetadataOptions{}},
|
||||
})
|
||||
if err == nil || !strings.Contains(err.Error(), `module "metadata" has no builder`) {
|
||||
t.Fatalf("error = %v, want missing builder", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestModuleRegistryRejectsUnsupportedMissingDataWarn(t *testing.T) {
|
||||
_, err := NewModuleRegistry([]ModuleDefinition{
|
||||
{ID: module.Metadata, StanzaName: "metadata", DefaultOptions: module.MetadataOptions{}, MissingData: module.MissingDataWarn, Builder: noopModuleBuilder},
|
||||
})
|
||||
if err == nil || !strings.Contains(err.Error(), `unsupported missing data behavior`) {
|
||||
t.Fatalf("error = %v, want unsupported missing-data behavior", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestModuleRegistryRejectsInvalidOptionShapes(t *testing.T) {
|
||||
registry := MustDefaultModuleRegistry()
|
||||
err := registry.ValidateComposition(report.DailyToday, []module.ConfigItem{
|
||||
@@ -74,3 +127,7 @@ func TestModuleRegistryAcceptsTypedOptions(t *testing.T) {
|
||||
t.Fatalf("ValidateComposition() error = %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func noopModuleBuilder(ModuleContext, any) (*module.Output, error) {
|
||||
return &module.Output{ID: module.Metadata, StanzaName: "metadata", Value: struct{}{}}, nil
|
||||
}
|
||||
|
||||
@@ -196,10 +196,20 @@ reports:
|
||||
reports:
|
||||
daily:
|
||||
deterministic_modules:
|
||||
- storm_window_summary
|
||||
- tomorrow_planning
|
||||
`,
|
||||
wantErr: `not compatible with report "daily_today"`,
|
||||
},
|
||||
{
|
||||
name: "RemovedPlaceholderModule",
|
||||
yaml: `
|
||||
reports:
|
||||
daily:
|
||||
deterministic_modules:
|
||||
- forecast_delta
|
||||
`,
|
||||
wantErr: `unknown module "forecast_delta"`,
|
||||
},
|
||||
{
|
||||
name: "InvalidOptions",
|
||||
yaml: `
|
||||
|
||||
@@ -15,16 +15,12 @@ const (
|
||||
CurrentConditions ID = "current_conditions"
|
||||
DerivedDailySummary ID = "derived_daily_summary"
|
||||
DerivedDaypartSummaries ID = "derived_daypart_summaries"
|
||||
HourlyTable ID = "hourly_table"
|
||||
PrecipTiming ID = "precip_timing"
|
||||
AlertDigest ID = "alert_digest"
|
||||
AreaForecastDiscussion ID = "area_forecast_discussion"
|
||||
WeatherStory ID = "weather_story"
|
||||
ForecastDelta ID = "forecast_delta"
|
||||
OutdoorWindows ID = "outdoor_windows"
|
||||
TomorrowPlanning ID = "tomorrow_planning"
|
||||
WeekendPlanning ID = "weekend_planning"
|
||||
StormWindowSummary ID = "storm_window_summary"
|
||||
)
|
||||
|
||||
type ConfigItem struct {
|
||||
@@ -107,18 +103,17 @@ func StanzaValue[T any](s Snapshot, name string) (T, bool, error) {
|
||||
type FactRequirement string
|
||||
|
||||
const (
|
||||
CollectedCurrentConditions FactRequirement = "collected.current_conditions"
|
||||
CollectedAlerts FactRequirement = "collected.alerts"
|
||||
CollectedDiscussion FactRequirement = "collected.discussion"
|
||||
CollectedWeatherStory FactRequirement = "collected.weather_story"
|
||||
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"
|
||||
RequiresDerivedStormWindowSummary FactRequirement = "derived.storm_window_summary"
|
||||
CollectedCurrentConditions FactRequirement = "collected.current_conditions"
|
||||
CollectedAlerts FactRequirement = "collected.alerts"
|
||||
CollectedDiscussion FactRequirement = "collected.discussion"
|
||||
CollectedWeatherStory FactRequirement = "collected.weather_story"
|
||||
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"
|
||||
)
|
||||
|
||||
type MissingDataBehavior string
|
||||
@@ -134,15 +129,11 @@ type MetadataOptions struct{}
|
||||
type CurrentConditionsOptions struct{}
|
||||
type DerivedDailySummaryOptions struct{}
|
||||
type DerivedDaypartSummariesOptions struct{}
|
||||
type HourlyTableOptions struct{}
|
||||
type PrecipTimingOptions struct{}
|
||||
type AlertDigestOptions struct{}
|
||||
type AreaForecastDiscussionOptions struct {
|
||||
Sections []string `json:"sections,omitempty" yaml:"sections,omitempty"`
|
||||
}
|
||||
type WeatherStoryOptions struct{}
|
||||
type ForecastDeltaOptions struct{}
|
||||
type OutdoorWindowsOptions struct{}
|
||||
type TomorrowPlanningOptions struct{}
|
||||
type WeekendPlanningOptions struct{}
|
||||
type StormWindowSummaryOptions struct{}
|
||||
|
||||
@@ -106,7 +106,7 @@ func TestBuildUsesNamedSnapshotStanzas(t *testing.T) {
|
||||
req.Metadata.PromptID = "weather.three_day_outlook"
|
||||
req.Modules = snapshotWithOutputs(t,
|
||||
module.Output{ID: module.Metadata, StanzaName: "metadata", Value: map[string]string{"run_id": req.Metadata.RunID}},
|
||||
module.Output{ID: module.ForecastDelta, StanzaName: "three_day", Value: map[string]any{"days": []string{"2026-05-29"}}},
|
||||
module.Output{ID: module.DerivedDaypartSummaries, StanzaName: "three_day", Value: map[string]any{"days": []string{"2026-05-29"}}},
|
||||
)
|
||||
|
||||
pkg, err := Build(req)
|
||||
|
||||
@@ -266,7 +266,6 @@ func TestRegistryDefinitionsDeclareDefaultModules(t *testing.T) {
|
||||
module.DerivedDaypartSummaries,
|
||||
module.PrecipTiming,
|
||||
module.AlertDigest,
|
||||
module.ForecastDelta,
|
||||
module.AreaForecastDiscussion,
|
||||
module.WeatherStory,
|
||||
module.OutdoorWindows,
|
||||
@@ -281,7 +280,6 @@ func TestRegistryDefinitionsDeclareDefaultModules(t *testing.T) {
|
||||
module.DerivedDaypartSummaries,
|
||||
module.PrecipTiming,
|
||||
module.AlertDigest,
|
||||
module.ForecastDelta,
|
||||
module.AreaForecastDiscussion,
|
||||
module.WeatherStory,
|
||||
module.OutdoorWindows,
|
||||
@@ -296,7 +294,6 @@ func TestRegistryDefinitionsDeclareDefaultModules(t *testing.T) {
|
||||
module.DerivedDaypartSummaries,
|
||||
module.PrecipTiming,
|
||||
module.AlertDigest,
|
||||
module.ForecastDelta,
|
||||
module.AreaForecastDiscussion,
|
||||
module.WeatherStory,
|
||||
module.OutdoorWindows,
|
||||
@@ -313,7 +310,6 @@ func TestRegistryDefinitionsDeclareDefaultModules(t *testing.T) {
|
||||
module.AreaForecastDiscussion,
|
||||
module.WeatherStory,
|
||||
module.OutdoorWindows,
|
||||
module.WeekendPlanning,
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -321,12 +317,10 @@ func TestRegistryDefinitionsDeclareDefaultModules(t *testing.T) {
|
||||
want: []module.ID{
|
||||
module.Metadata,
|
||||
module.CurrentConditions,
|
||||
module.HourlyTable,
|
||||
module.PrecipTiming,
|
||||
module.AlertDigest,
|
||||
module.AreaForecastDiscussion,
|
||||
module.WeatherStory,
|
||||
module.StormWindowSummary,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -109,7 +109,6 @@ func dailyTodayModules() []module.ConfigItem {
|
||||
module.DerivedDaypartSummaries,
|
||||
module.PrecipTiming,
|
||||
module.AlertDigest,
|
||||
module.ForecastDelta,
|
||||
module.AreaForecastDiscussion,
|
||||
module.WeatherStory,
|
||||
module.OutdoorWindows,
|
||||
@@ -129,7 +128,6 @@ func threeDayModules() []module.ConfigItem {
|
||||
module.DerivedDaypartSummaries,
|
||||
module.PrecipTiming,
|
||||
module.AlertDigest,
|
||||
module.ForecastDelta,
|
||||
module.AreaForecastDiscussion,
|
||||
module.WeatherStory,
|
||||
module.OutdoorWindows,
|
||||
@@ -146,7 +144,6 @@ func weekendModules() []module.ConfigItem {
|
||||
module.AreaForecastDiscussion,
|
||||
module.WeatherStory,
|
||||
module.OutdoorWindows,
|
||||
module.WeekendPlanning,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -154,12 +151,10 @@ func stormModules() []module.ConfigItem {
|
||||
return moduleItems(
|
||||
module.Metadata,
|
||||
module.CurrentConditions,
|
||||
module.HourlyTable,
|
||||
module.PrecipTiming,
|
||||
module.AlertDigest,
|
||||
module.AreaForecastDiscussion,
|
||||
module.WeatherStory,
|
||||
module.StormWindowSummary,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user