From 27849813db6ec9d992226efe0b11a7a939add58a Mon Sep 17 00:00:00 2001 From: Eric Rakestraw Date: Thu, 13 Aug 2026 01:12:47 +0000 Subject: [PATCH] Refresh SPC outlook definition sources --- docs/internal/briefing.md | 9 +++++ .../spc_convective_outlook_definitions.json | 31 ++++++++++++--- .../spc_convective_outlook_definitions.go | 24 ++++++++++-- .../spc_convective_outlooks_module_test.go | 38 ++++++++++++++++--- 4 files changed, 88 insertions(+), 14 deletions(-) diff --git a/docs/internal/briefing.md b/docs/internal/briefing.md index c5b21dc..0c31de6 100644 --- a/docs/internal/briefing.md +++ b/docs/internal/briefing.md @@ -52,6 +52,15 @@ Planning applies the same identity when recognizing morning, afternoon, evening, and overnight windows; display labels remain separate and preserve configured text with rune-safe first-letter capitalization. +The embedded SPC background-definition asset records its authoritative sources, +source update dates, and maintainer review schedule. Its categorical +`official_description` values transcribe the [SPC convective-outlook risk +table](https://www.spc.noaa.gov/about/outlooks/); its Conditional Intensity +Group entries follow the [SPC conditional-intensity +reference](https://www.spc.noaa.gov/exper/conditional-intensity-information). +`plain_language` values are Weatherreporter summaries. Weatherreporter +maintainers review the asset annually and whenever either source changes. + `area_forecast_discussion` accepts an optional typed section filter. Accepted typed option pointers are normalized to the declared value type before builder execution. Planning modules are report-specific: `daily_planning` supports Daily, diff --git a/internal/briefing/assets/spc_convective_outlook_definitions.json b/internal/briefing/assets/spc_convective_outlook_definitions.json index fee3b7a..bffa8cd 100644 --- a/internal/briefing/assets/spc_convective_outlook_definitions.json +++ b/internal/briefing/assets/spc_convective_outlook_definitions.json @@ -1,32 +1,50 @@ { + "provenance": { + "sources": [ + { + "url": "https://www.spc.noaa.gov/about/outlooks/", + "updated_on": "2026-03-03", + "applies_to": "categorical outlook descriptions" + }, + { + "url": "https://www.spc.noaa.gov/exper/conditional-intensity-information", + "updated_on": "2026-02-04", + "applies_to": "conditional intensity group descriptions" + } + ], + "reviewed_on": "2026-08-13", + "review_owner": "Weatherreporter maintainers", + "review_schedule": "Review annually and whenever SPC updates either referenced page." + }, + "definitions": { "categorical:TSTM": { "plain_language": "General or non-severe thunderstorms.", - "official_description": "No severe thunderstorms expected.", + "official_description": "Encloses a 10% or higher probability of thunderstorms.", "relative_level": "0 of 5" }, "categorical:MRGL": { "plain_language": "Isolated severe storms possible.", - "official_description": "Isolated severe storms may occur within the risk area, but they are expected to be limited in duration, coverage, and intensity.", + "official_description": "Includes severe storms of either limited organization and longevity or very low coverage.", "relative_level": "1 of 5" }, "categorical:SLGT": { "plain_language": "Scattered severe storms possible.", - "official_description": "Isolated intense storms are possible within the risk area, but severe weather is generally expected to be short-lived and/or not widespread.", + "official_description": "Implies organized severe thunderstorms are expected, but usually in low coverage with varying levels of intensity.", "relative_level": "2 of 5" }, "categorical:ENH": { "plain_language": "Numerous severe storms possible.", - "official_description": "Numerous severe storms are possible within the risk area, some of which may be intense.", + "official_description": "Depicts a greater concentration of organized severe thunderstorms with varying levels of intensity.", "relative_level": "3 of 5" }, "categorical:MDT": { "plain_language": "Widespread severe storms likely.", - "official_description": "Widespread severe storms are likely within the risk area. Storms may be long-lived, widespread, and intense. This risk is usually reserved for days with several supercells producing intense tornadoes and/or very large hail, or an intense squall line with widespread damaging winds.", + "official_description": "Indicates potential for widespread severe weather with several tornadoes and/or numerous severe thunderstorms, some of which may be intense.", "relative_level": "4 of 5" }, "categorical:HIGH": { "plain_language": "Major severe outbreak expected.", - "official_description": "A major severe weather outbreak is expected, with long-lived, very widespread, and particularly intense severe storms. This risk is reserved for when high confidence exists in widespread coverage of severe weather with embedded instances of extreme severity (i.e., violent tornadoes or very damaging convective wind events).", + "official_description": "Suggests a severe weather outbreak is expected from either numerous intense to violent long-track tornadoes or a long-lived derecho system with hurricane-force wind gusts producing widespread damage.", "relative_level": "5 of 5" }, "tornado:CIG1": { @@ -69,4 +87,5 @@ "official_description": "Intensity Level 2: Reasonable Max hail size greater than 3.75 inches. Note that this product describes the reasonable maximum intensity of a hazard if that hazard occurs. It does not by itself indicate the probability that the hazard will occur.", "relative_level": "2 of 2" } + } } diff --git a/internal/briefing/spc_convective_outlook_definitions.go b/internal/briefing/spc_convective_outlook_definitions.go index b4b0b01..50914ee 100644 --- a/internal/briefing/spc_convective_outlook_definitions.go +++ b/internal/briefing/spc_convective_outlook_definitions.go @@ -17,11 +17,29 @@ func mustLoadSPCOutlookBackgroundDefinitions() map[string]SPCOutlookBackgroundDe if err != nil { panic(fmt.Sprintf("read embedded SPC outlook definitions: %v", err)) } - var definitions map[string]SPCOutlookBackgroundDefinition - if err := json.Unmarshal(data, &definitions); err != nil { + var asset spcOutlookBackgroundDefinitionAsset + if err := json.Unmarshal(data, &asset); err != nil { panic(fmt.Sprintf("decode embedded SPC outlook definitions: %v", err)) } - return definitions + return asset.Definitions +} + +type spcOutlookBackgroundDefinitionAsset struct { + Provenance spcOutlookDefinitionProvenance `json:"provenance"` + Definitions map[string]SPCOutlookBackgroundDefinition `json:"definitions"` +} + +type spcOutlookDefinitionProvenance struct { + Sources []spcOutlookDefinitionSource `json:"sources"` + ReviewedOn string `json:"reviewed_on"` + ReviewOwner string `json:"review_owner"` + ReviewSchedule string `json:"review_schedule"` +} + +type spcOutlookDefinitionSource struct { + URL string `json:"url"` + UpdatedOn string `json:"updated_on"` + AppliesTo string `json:"applies_to"` } func spcOutlookBackgroundDefinition(outlookType string, label string) *SPCOutlookBackgroundDefinition { diff --git a/internal/briefing/spc_convective_outlooks_module_test.go b/internal/briefing/spc_convective_outlooks_module_test.go index daabe17..41e5c66 100644 --- a/internal/briefing/spc_convective_outlooks_module_test.go +++ b/internal/briefing/spc_convective_outlooks_module_test.go @@ -65,11 +65,9 @@ func TestSPCConvectiveOutlooksModuleBuildsPromptSafeRiskProduct(t *testing.T) { if got.Day != 1 || got.OutlookType != "categorical" || got.Label != "SLGT" || got.LabelText != "Slight Risk" { t.Fatalf("outlook = %#v, want categorical slight risk fields", got) } - if got.BackgroundDefinition == nil || - got.BackgroundDefinition.PlainLanguage != "Scattered severe storms possible." || - got.BackgroundDefinition.OfficialDescription != "Isolated intense storms are possible within the risk area, but severe weather is generally expected to be short-lived and/or not widespread." || - got.BackgroundDefinition.RelativeLevel != "2 of 5" { - t.Fatalf("background definition = %#v, want Slight Risk helper", got.BackgroundDefinition) + wantBackground := spcOutlookBackgroundDefinition("categorical", "SLGT") + if got.BackgroundDefinition == nil || wantBackground == nil || *got.BackgroundDefinition != *wantBackground { + t.Fatalf("background definition = %#v, want matching Slight Risk helper %#v", got.BackgroundDefinition, wantBackground) } if got.PeriodBegins != "2026-05-29 at 11:00 AM" || got.PeriodEnds != "2026-05-30 at 7:00 AM" || got.IssuedAt != "2026-05-29 at 8:45 AM" { t.Fatalf("outlook times = %#v, want friendly local labels", got) @@ -193,6 +191,36 @@ func TestSPCOutlookBackgroundDefinitionsAssetHasUsableEntries(t *testing.T) { } } +func TestSPCOutlookBackgroundDefinitionAssetRecordsProvenance(t *testing.T) { + data, err := spcConvectiveOutlookDefinitionAssets.ReadFile("assets/spc_convective_outlook_definitions.json") + if err != nil { + t.Fatalf("ReadFile() error = %v", err) + } + var asset spcOutlookBackgroundDefinitionAsset + if err := json.Unmarshal(data, &asset); err != nil { + t.Fatalf("Unmarshal() error = %v", err) + } + if asset.Provenance.ReviewedOn == "" || asset.Provenance.ReviewOwner == "" || asset.Provenance.ReviewSchedule == "" { + t.Fatalf("asset provenance = %#v, want review metadata", asset.Provenance) + } + wantSources := map[string]string{ + "https://www.spc.noaa.gov/about/outlooks/": "categorical outlook descriptions", + "https://www.spc.noaa.gov/exper/conditional-intensity-information": "conditional intensity group descriptions", + } + if len(asset.Provenance.Sources) != len(wantSources) { + t.Fatalf("asset provenance sources = %#v, want %d authoritative sources", asset.Provenance.Sources, len(wantSources)) + } + for _, source := range asset.Provenance.Sources { + if source.UpdatedOn == "" || wantSources[source.URL] != source.AppliesTo { + t.Fatalf("asset provenance source = %#v, want authoritative source metadata", source) + } + delete(wantSources, source.URL) + } + if len(wantSources) != 0 { + t.Fatalf("asset provenance missing sources = %#v", wantSources) + } +} + func TestSPCRiskDigestDefaultPolicyConstants(t *testing.T) { if defaultSPCRiskDigestOutlookType != "categorical" { t.Fatalf("defaultSPCRiskDigestOutlookType = %q, want categorical", defaultSPCRiskDigestOutlookType)