Refresh SPC outlook definition sources

This commit is contained in:
2026-08-13 01:12:47 +00:00
parent 41b86109e3
commit 27849813db
4 changed files with 88 additions and 14 deletions

View File

@@ -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,

View File

@@ -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"
}
}
}

View File

@@ -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 {

View File

@@ -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)