diff --git a/docs/internal/briefing.md b/docs/internal/briefing.md index e044307..e070957 100644 --- a/docs/internal/briefing.md +++ b/docs/internal/briefing.md @@ -10,7 +10,10 @@ snapshot, construct YAML, invoke Promptkit, or render a report. Every `ModuleDefinition` declares an ID, stanza name, default option value, required collected and derived facts, supported report IDs, missing-data -behavior, duplicate policy, builder, and optional prompt exporter. +behavior, duplicate policy, builder, and optional prompt exporter. The +briefing-owned fact-requirement vocabulary supplies each prerequisite's stable +identity, category, and availability predicate; registry construction rejects +unknown requirements and requirements listed under the wrong category. `BuildModule` first verifies the requested module, report compatibility, and option shape. It then applies the declared missing-data behavior: diff --git a/internal/briefing/fact_requirements.go b/internal/briefing/fact_requirements.go index 3626f9a..f27d594 100644 --- a/internal/briefing/fact_requirements.go +++ b/internal/briefing/fact_requirements.go @@ -8,8 +8,9 @@ const ( ) type factRequirement struct { - identity string - category factRequirementCategory + identity string + category factRequirementCategory + available func(ModuleContext) bool } func (r factRequirement) String() string { @@ -17,25 +18,55 @@ func (r factRequirement) String() string { } var ( - currentConditionsRequirement = factRequirement{identity: "collected.current_conditions", category: collectedFactRequirement} - narrativeForecastRequirement = factRequirement{identity: "collected.narrative_forecast", category: collectedFactRequirement} - hourlyForecastRequirement = factRequirement{identity: "collected.hourly_forecast", category: collectedFactRequirement} - alertsRequirement = factRequirement{identity: "collected.alerts", category: collectedFactRequirement} - discussionRequirement = factRequirement{identity: "collected.discussion", category: collectedFactRequirement} - weatherStoryRequirement = factRequirement{identity: "collected.weather_story", category: collectedFactRequirement} - spcOutlooksRequirement = factRequirement{identity: "collected.spc_convective_outlooks", category: collectedFactRequirement} - sourceMetadataRequirement = factRequirement{identity: "collected.source_metadata", category: collectedFactRequirement} + currentConditionsRequirement = &factRequirement{identity: "collected.current_conditions", category: collectedFactRequirement, available: func(ctx ModuleContext) bool { + return ctx.Collected.Current != nil + }} + narrativeForecastRequirement = &factRequirement{identity: "collected.narrative_forecast", category: collectedFactRequirement, available: func(ctx ModuleContext) bool { + return ctx.Collected.Narrative != nil + }} + hourlyForecastRequirement = &factRequirement{identity: "collected.hourly_forecast", category: collectedFactRequirement, available: func(ctx ModuleContext) bool { + return ctx.Collected.Hourly != nil + }} + alertsRequirement = &factRequirement{identity: "collected.alerts", category: collectedFactRequirement, available: func(ctx ModuleContext) bool { + return ctx.Collected.Alerts != nil + }} + discussionRequirement = &factRequirement{identity: "collected.discussion", category: collectedFactRequirement, available: func(ctx ModuleContext) bool { + return ctx.Collected.Discussion != nil + }} + weatherStoryRequirement = &factRequirement{identity: "collected.weather_story", category: collectedFactRequirement, available: func(ctx ModuleContext) bool { + return ctx.Collected.WeatherStory != nil + }} + spcOutlooksRequirement = &factRequirement{identity: "collected.spc_convective_outlooks", category: collectedFactRequirement, available: func(ctx ModuleContext) bool { + return ctx.Collected.SPCConvectiveOutlooks != nil + }} + sourceMetadataRequirement = &factRequirement{identity: "collected.source_metadata", category: collectedFactRequirement, available: func(ctx ModuleContext) bool { + return len(ctx.Collected.SourceProvenance) > 0 || len(ctx.Collected.SourceWarnings) > 0 + }} - hourlyPeriodsRequirement = factRequirement{identity: "derived.hourly_periods", category: derivedFactRequirement} - narrativePeriodsRequirement = factRequirement{identity: "derived.narrative_periods", category: derivedFactRequirement} - alertOverlapsRequirement = factRequirement{identity: "derived.alert_overlaps", category: derivedFactRequirement} - dailySummariesRequirement = factRequirement{identity: "derived.daily_summaries", category: derivedFactRequirement} - daypartSummariesRequirement = factRequirement{identity: "derived.daypart_summaries", category: derivedFactRequirement} - precipTimingRequirement = factRequirement{identity: "derived.precip_timing", category: derivedFactRequirement} - spcDerivedOutlooksRequirement = factRequirement{identity: "derived.spc_convective_outlooks", category: derivedFactRequirement} + hourlyPeriodsRequirement = &factRequirement{identity: "derived.hourly_periods", category: derivedFactRequirement, available: func(ctx ModuleContext) bool { + return len(ctx.Derived.ValidPeriodHourlyPeriods) > 0 + }} + narrativePeriodsRequirement = &factRequirement{identity: "derived.narrative_periods", category: derivedFactRequirement, available: func(ctx ModuleContext) bool { + return len(ctx.Derived.ValidPeriodNarrativePeriods) > 0 + }} + alertOverlapsRequirement = &factRequirement{identity: "derived.alert_overlaps", category: derivedFactRequirement, available: func(ModuleContext) bool { + return true + }} + dailySummariesRequirement = &factRequirement{identity: "derived.daily_summaries", category: derivedFactRequirement, available: func(ctx ModuleContext) bool { + return len(ctx.Derived.DailySummaries) > 0 + }} + daypartSummariesRequirement = &factRequirement{identity: "derived.daypart_summaries", category: derivedFactRequirement, available: func(ctx ModuleContext) bool { + return len(ctx.Derived.DaypartSummaries) > 0 + }} + precipTimingRequirement = &factRequirement{identity: "derived.precip_timing", category: derivedFactRequirement, available: func(ModuleContext) bool { + return true + }} + spcDerivedOutlooksRequirement = &factRequirement{identity: "derived.spc_convective_outlooks", category: derivedFactRequirement, available: func(ctx ModuleContext) bool { + return ctx.Derived.SPCConvectiveOutlooks != nil + }} ) -var factRequirementVocabulary = []factRequirement{ +var factRequirementVocabulary = []*factRequirement{ currentConditionsRequirement, narrativeForecastRequirement, hourlyForecastRequirement, diff --git a/internal/briefing/modules.go b/internal/briefing/modules.go index 26a4ebe..8ad5805 100644 --- a/internal/briefing/modules.go +++ b/internal/briefing/modules.go @@ -28,8 +28,8 @@ type ModuleDefinition struct { ID module.ID StanzaName string DefaultOptions any - RequiredCollected []factRequirement - RequiredDerived []factRequirement + RequiredCollected []*factRequirement + RequiredDerived []*factRequirement SupportedReports []report.ID MissingData module.MissingDataBehavior AllowDuplicate bool @@ -76,6 +76,9 @@ func NewModuleRegistry(definitions []ModuleDefinition) (ModuleRegistry, error) { if definition.MissingData == module.MissingDataWarn { return ModuleRegistry{}, fmt.Errorf("module %q uses unsupported missing data behavior %q", definition.ID, definition.MissingData) } + if err := validateFactRequirements(definition); err != nil { + return ModuleRegistry{}, err + } 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) } @@ -153,60 +156,48 @@ func (r ModuleRegistry) BuildModule(ctx ModuleContext, item module.ConfigItem) ( func missingRequirements(definition ModuleDefinition, ctx ModuleContext) []string { var missing []string for _, requirement := range definition.RequiredCollected { - if !collectedFactAvailable(requirement, ctx) { + if !requirement.available(ctx) { missing = append(missing, requirement.String()) } } for _, requirement := range definition.RequiredDerived { - if !derivedFactAvailable(requirement, ctx) { + if !requirement.available(ctx) { missing = append(missing, requirement.String()) } } return missing } -func collectedFactAvailable(requirement factRequirement, ctx ModuleContext) bool { - switch requirement { - case currentConditionsRequirement: - return ctx.Collected.Current != nil - case narrativeForecastRequirement: - return ctx.Collected.Narrative != nil - case hourlyForecastRequirement: - return ctx.Collected.Hourly != nil - case alertsRequirement: - return ctx.Collected.Alerts != nil - case discussionRequirement: - return ctx.Collected.Discussion != nil - case weatherStoryRequirement: - return ctx.Collected.WeatherStory != nil - case spcOutlooksRequirement: - return ctx.Collected.SPCConvectiveOutlooks != nil - case sourceMetadataRequirement: - return len(ctx.Collected.SourceProvenance) > 0 || len(ctx.Collected.SourceWarnings) > 0 - default: - return false +func validateFactRequirements(definition ModuleDefinition) error { + if err := validateFactRequirementCategory(definition.ID, definition.RequiredCollected, collectedFactRequirement); err != nil { + return err } + return validateFactRequirementCategory(definition.ID, definition.RequiredDerived, derivedFactRequirement) } -func derivedFactAvailable(requirement factRequirement, ctx ModuleContext) bool { - switch requirement { - case hourlyPeriodsRequirement: - return len(ctx.Derived.ValidPeriodHourlyPeriods) > 0 - case narrativePeriodsRequirement: - return len(ctx.Derived.ValidPeriodNarrativePeriods) > 0 - case alertOverlapsRequirement: - return true - case dailySummariesRequirement: - return len(ctx.Derived.DailySummaries) > 0 - case daypartSummariesRequirement: - return len(ctx.Derived.DaypartSummaries) > 0 - case precipTimingRequirement: - return true - case spcDerivedOutlooksRequirement: - return ctx.Derived.SPCConvectiveOutlooks != nil - default: - return false +func validateFactRequirementCategory(moduleID module.ID, requirements []*factRequirement, want factRequirementCategory) error { + for _, requirement := range requirements { + if requirement == nil { + return fmt.Errorf("module %q uses unknown %s fact requirement %q", moduleID, want, "") + } + descriptor, ok := lookupFactRequirement(requirement.identity) + if !ok || descriptor != requirement { + return fmt.Errorf("module %q uses unknown %s fact requirement %q", moduleID, want, requirement.identity) + } + if descriptor.category != want { + return fmt.Errorf("module %q lists %s fact requirement %q as %s", moduleID, descriptor.category, descriptor.identity, want) + } } + return nil +} + +func lookupFactRequirement(identity string) (*factRequirement, bool) { + for _, requirement := range factRequirementVocabulary { + if requirement.identity == identity { + return requirement, true + } + } + return nil, false } func (r ModuleRegistry) ValidateComposition(reportID report.ID, items []module.ConfigItem) error { @@ -291,7 +282,7 @@ func defaultModuleDefinitions() []ModuleDefinition { ID: module.Metadata, StanzaName: "metadata", DefaultOptions: module.MetadataOptions{}, - RequiredCollected: []factRequirement{sourceMetadataRequirement}, + RequiredCollected: []*factRequirement{sourceMetadataRequirement}, SupportedReports: allReports, MissingData: module.MissingDataEmpty, Builder: buildMetadataModule, @@ -300,7 +291,7 @@ func defaultModuleDefinitions() []ModuleDefinition { ID: module.CurrentConditions, StanzaName: "current_conditions", DefaultOptions: module.CurrentConditionsOptions{}, - RequiredCollected: []factRequirement{currentConditionsRequirement}, + RequiredCollected: []*factRequirement{currentConditionsRequirement}, SupportedReports: allReports, MissingData: module.MissingDataOmit, Builder: buildCurrentConditionsModule, @@ -310,8 +301,8 @@ func defaultModuleDefinitions() []ModuleDefinition { ID: module.NarrativeForecast, StanzaName: "narrative_forecast", DefaultOptions: module.NarrativeForecastOptions{}, - RequiredCollected: []factRequirement{narrativeForecastRequirement}, - RequiredDerived: []factRequirement{narrativePeriodsRequirement}, + RequiredCollected: []*factRequirement{narrativeForecastRequirement}, + RequiredDerived: []*factRequirement{narrativePeriodsRequirement}, SupportedReports: []report.ID{report.Daily, report.Today, report.Tomorrow}, MissingData: module.MissingDataOmit, Builder: buildNarrativeForecastModule, @@ -320,8 +311,8 @@ func defaultModuleDefinitions() []ModuleDefinition { ID: module.HourlyForecast, StanzaName: "hourly_forecast", DefaultOptions: module.HourlyForecastOptions{}, - RequiredCollected: []factRequirement{hourlyForecastRequirement}, - RequiredDerived: []factRequirement{hourlyPeriodsRequirement}, + RequiredCollected: []*factRequirement{hourlyForecastRequirement}, + RequiredDerived: []*factRequirement{hourlyPeriodsRequirement}, SupportedReports: []report.ID{report.Daily, report.Today, report.Tomorrow, report.Hourly}, MissingData: module.MissingDataOmit, Builder: buildHourlyForecastModule, @@ -331,7 +322,7 @@ func defaultModuleDefinitions() []ModuleDefinition { ID: module.DerivedDailySummary, StanzaName: "derived_daily_summary", DefaultOptions: module.DerivedDailySummaryOptions{}, - RequiredDerived: []factRequirement{dailySummariesRequirement, precipTimingRequirement}, + RequiredDerived: []*factRequirement{dailySummariesRequirement, precipTimingRequirement}, SupportedReports: []report.ID{report.Daily, report.Today, report.Tomorrow}, MissingData: module.MissingDataError, Builder: buildDerivedDailySummaryModule, @@ -340,7 +331,7 @@ func defaultModuleDefinitions() []ModuleDefinition { ID: module.DerivedDaypartSummaries, StanzaName: "derived_daypart_summaries", DefaultOptions: module.DerivedDaypartSummariesOptions{}, - RequiredDerived: []factRequirement{daypartSummariesRequirement}, + RequiredDerived: []*factRequirement{daypartSummariesRequirement}, SupportedReports: daypartReports, MissingData: module.MissingDataError, Builder: buildDerivedDaypartSummariesModule, @@ -350,7 +341,7 @@ func defaultModuleDefinitions() []ModuleDefinition { ID: module.PrecipTiming, StanzaName: "precip_timing", DefaultOptions: module.PrecipTimingOptions{}, - RequiredDerived: []factRequirement{precipTimingRequirement}, + RequiredDerived: []*factRequirement{precipTimingRequirement}, SupportedReports: allReports, MissingData: module.MissingDataEmpty, Builder: buildPrecipTimingModule, @@ -359,8 +350,8 @@ func defaultModuleDefinitions() []ModuleDefinition { ID: module.AlertDigest, StanzaName: "alert_digest", DefaultOptions: module.AlertDigestOptions{}, - RequiredCollected: []factRequirement{alertsRequirement}, - RequiredDerived: []factRequirement{alertOverlapsRequirement}, + RequiredCollected: []*factRequirement{alertsRequirement}, + RequiredDerived: []*factRequirement{alertOverlapsRequirement}, SupportedReports: allReports, MissingData: module.MissingDataEmpty, Builder: buildAlertDigestModule, @@ -369,8 +360,8 @@ func defaultModuleDefinitions() []ModuleDefinition { ID: module.SPCConvectiveOutlooks, StanzaName: string(module.SPCConvectiveOutlooks), DefaultOptions: module.SPCConvectiveOutlooksOptions{}, - RequiredCollected: []factRequirement{spcOutlooksRequirement}, - RequiredDerived: []factRequirement{spcDerivedOutlooksRequirement}, + RequiredCollected: []*factRequirement{spcOutlooksRequirement}, + RequiredDerived: []*factRequirement{spcDerivedOutlooksRequirement}, SupportedReports: allReports, MissingData: module.MissingDataEmpty, Builder: buildSPCConvectiveOutlooksModule, @@ -379,7 +370,7 @@ func defaultModuleDefinitions() []ModuleDefinition { ID: module.AreaForecastDiscussion, StanzaName: "area_forecast_discussion", DefaultOptions: module.AreaForecastDiscussionOptions{}, - RequiredCollected: []factRequirement{discussionRequirement}, + RequiredCollected: []*factRequirement{discussionRequirement}, SupportedReports: allReports, MissingData: module.MissingDataOmit, Builder: buildAreaForecastDiscussionModule, @@ -388,8 +379,8 @@ func defaultModuleDefinitions() []ModuleDefinition { ID: module.SPCConvectiveDiscussion, StanzaName: string(module.SPCConvectiveDiscussion), DefaultOptions: module.SPCConvectiveDiscussionOptions{}, - RequiredCollected: []factRequirement{spcOutlooksRequirement}, - RequiredDerived: []factRequirement{spcDerivedOutlooksRequirement}, + RequiredCollected: []*factRequirement{spcOutlooksRequirement}, + RequiredDerived: []*factRequirement{spcDerivedOutlooksRequirement}, SupportedReports: allReports, MissingData: module.MissingDataOmit, Builder: buildSPCConvectiveDiscussionModule, @@ -398,7 +389,7 @@ func defaultModuleDefinitions() []ModuleDefinition { ID: module.WeatherStory, StanzaName: "weather_story", DefaultOptions: module.WeatherStoryOptions{}, - RequiredCollected: []factRequirement{weatherStoryRequirement}, + RequiredCollected: []*factRequirement{weatherStoryRequirement}, SupportedReports: allReports, MissingData: module.MissingDataOmit, Builder: buildWeatherStoryModule, @@ -407,7 +398,7 @@ func defaultModuleDefinitions() []ModuleDefinition { ID: module.OutdoorWindows, StanzaName: "outdoor_windows", DefaultOptions: module.OutdoorWindowsOptions{}, - RequiredDerived: []factRequirement{daypartSummariesRequirement}, + RequiredDerived: []*factRequirement{daypartSummariesRequirement}, SupportedReports: daypartReports, MissingData: module.MissingDataEmpty, Builder: buildOutdoorWindowsModule, @@ -416,7 +407,7 @@ func defaultModuleDefinitions() []ModuleDefinition { ID: module.TodayPlanning, StanzaName: "today_planning", DefaultOptions: module.TodayPlanningOptions{}, - RequiredDerived: []factRequirement{dailySummariesRequirement}, + RequiredDerived: []*factRequirement{dailySummariesRequirement}, SupportedReports: []report.ID{report.Today}, MissingData: module.MissingDataEmpty, Builder: buildTodayPlanningModule, @@ -425,7 +416,7 @@ func defaultModuleDefinitions() []ModuleDefinition { ID: module.TomorrowPlanning, StanzaName: "tomorrow_planning", DefaultOptions: module.TomorrowPlanningOptions{}, - RequiredDerived: []factRequirement{dailySummariesRequirement}, + RequiredDerived: []*factRequirement{dailySummariesRequirement}, SupportedReports: []report.ID{report.Tomorrow}, MissingData: module.MissingDataEmpty, Builder: buildTomorrowPlanningModule, @@ -434,7 +425,7 @@ func defaultModuleDefinitions() []ModuleDefinition { ID: module.DailyPlanning, StanzaName: "daily_planning", DefaultOptions: module.DailyPlanningOptions{}, - RequiredDerived: []factRequirement{dailySummariesRequirement}, + RequiredDerived: []*factRequirement{dailySummariesRequirement}, SupportedReports: []report.ID{report.Daily}, MissingData: module.MissingDataEmpty, Builder: buildDailyPlanningModule, diff --git a/internal/briefing/modules_test.go b/internal/briefing/modules_test.go index 90fedfd..7cc8839 100644 --- a/internal/briefing/modules_test.go +++ b/internal/briefing/modules_test.go @@ -31,7 +31,7 @@ func TestDefaultModuleRegistryValidatesReportDefaults(t *testing.T) { } func TestFactRequirementVocabularyMatchesModuleDefinitions(t *testing.T) { - vocabulary := make(map[string]factRequirement, len(factRequirementVocabulary)) + vocabulary := make(map[string]*factRequirement, len(factRequirementVocabulary)) for _, requirement := range factRequirementVocabulary { if requirement.identity == "" { t.Fatal("fact requirement identity is empty") @@ -42,10 +42,13 @@ func TestFactRequirementVocabularyMatchesModuleDefinitions(t *testing.T) { if _, exists := vocabulary[requirement.identity]; exists { t.Fatalf("duplicate fact requirement %q", requirement.identity) } + if requirement.available == nil { + t.Fatalf("fact requirement %q has no availability predicate", requirement.identity) + } vocabulary[requirement.identity] = requirement } - used := map[factRequirement]struct{}{} + used := map[*factRequirement]struct{}{} for _, definition := range defaultModuleDefinitions() { for _, requirement := range definition.RequiredCollected { assertFactRequirementCategory(t, vocabulary, used, definition.ID, requirement, collectedFactRequirement) @@ -61,7 +64,7 @@ func TestFactRequirementVocabularyMatchesModuleDefinitions(t *testing.T) { } } -func assertFactRequirementCategory(t *testing.T, vocabulary map[string]factRequirement, used map[factRequirement]struct{}, moduleID module.ID, requirement factRequirement, want factRequirementCategory) { +func assertFactRequirementCategory(t *testing.T, vocabulary map[string]*factRequirement, used map[*factRequirement]struct{}, moduleID module.ID, requirement *factRequirement, want factRequirementCategory) { t.Helper() descriptor, ok := vocabulary[requirement.identity] if !ok { @@ -480,6 +483,52 @@ func TestModuleRegistryRejectsDefinitionsWithoutBuilders(t *testing.T) { } } +func TestModuleRegistryRejectsInvalidFactRequirements(t *testing.T) { + tests := []struct { + name string + configure func(*ModuleDefinition) + wantErr string + }{ + { + name: "Unknown", + configure: func(definition *ModuleDefinition) { + definition.RequiredCollected = []*factRequirement{{identity: "collected.unknown", category: collectedFactRequirement}} + }, + wantErr: `module "metadata" uses unknown collected fact requirement "collected.unknown"`, + }, + { + name: "DerivedListedAsCollected", + configure: func(definition *ModuleDefinition) { + definition.RequiredCollected = []*factRequirement{dailySummariesRequirement} + }, + wantErr: `module "metadata" lists derived fact requirement "derived.daily_summaries" as collected`, + }, + { + name: "CollectedListedAsDerived", + configure: func(definition *ModuleDefinition) { + definition.RequiredDerived = []*factRequirement{currentConditionsRequirement} + }, + wantErr: `module "metadata" lists collected fact requirement "collected.current_conditions" as derived`, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + definition := ModuleDefinition{ + ID: module.Metadata, + StanzaName: "metadata", + DefaultOptions: module.MetadataOptions{}, + Builder: noopModuleBuilder, + } + tt.configure(&definition) + _, err := NewModuleRegistry([]ModuleDefinition{definition}) + if err == nil || err.Error() != tt.wantErr { + t.Fatalf("error = %v, want %q", err, tt.wantErr) + } + }) + } +} + func TestModuleRegistryRejectsUnsupportedMissingDataWarn(t *testing.T) { _, err := NewModuleRegistry([]ModuleDefinition{ {ID: module.Metadata, StanzaName: "metadata", DefaultOptions: module.MetadataOptions{}, MissingData: module.MissingDataWarn, Builder: noopModuleBuilder}, @@ -513,25 +562,25 @@ func TestModuleRegistryAcceptsTypedOptions(t *testing.T) { func TestSPCConvectiveOutlookCollectedRequirementAvailability(t *testing.T) { ctx := ModuleContext{} - if collectedFactAvailable(spcOutlooksRequirement, ctx) { - t.Fatal("collectedFactAvailable() = true, want false without source") + if spcOutlooksRequirement.available(ctx) { + t.Fatal("SPC outlook requirement is available, want false without source") } ctx.Collected = facts.CollectedFacts{SPCConvectiveOutlooks: &weatherdata.ConvectiveOutlookRun{}} - if !collectedFactAvailable(spcOutlooksRequirement, ctx) { - t.Fatal("collectedFactAvailable() = false, want true with checked source") + if !spcOutlooksRequirement.available(ctx) { + t.Fatal("SPC outlook requirement is unavailable, want true with checked source") } } func TestSPCConvectiveOutlookDerivedRequirementAvailability(t *testing.T) { ctx := ModuleContext{} - if derivedFactAvailable(spcDerivedOutlooksRequirement, ctx) { - t.Fatal("derivedFactAvailable() = true, want false without derived outlooks") + if spcDerivedOutlooksRequirement.available(ctx) { + t.Fatal("derived SPC outlook requirement is available, want false without derived outlooks") } ctx.Derived = facts.DerivedFacts{SPCConvectiveOutlooks: []weatherdata.ConvectiveOutlook{}} - if !derivedFactAvailable(spcDerivedOutlooksRequirement, ctx) { - t.Fatal("derivedFactAvailable() = false, want true for checked empty derived outlooks") + if !spcDerivedOutlooksRequirement.available(ctx) { + t.Fatal("derived SPC outlook requirement is unavailable, want true for checked empty derived outlooks") } }