Refresh SPC outlook definition sources
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user