From cd7b9aef2bd09b6959a77adf5f06503b6314ab6f Mon Sep 17 00:00:00 2001 From: Eric Rakestraw Date: Thu, 13 Aug 2026 04:15:42 +0000 Subject: [PATCH] Retire unused module and forecast compatibility exports --- docs/internal/forecast-derivation.md | 2 +- docs/internal/module.md | 6 +-- internal/briefing/base_modules_test.go | 2 +- .../derived_daypart_summaries_module.go | 2 +- internal/briefing/derived_modules_test.go | 6 +-- internal/briefing/hourly_forecast_module.go | 4 +- internal/forecast/derive.go | 4 +- internal/forecast/derive_test.go | 20 ++------- internal/forecast/thresholds.go | 20 --------- internal/module/module.go | 41 ++++++++----------- internal/module/module_test.go | 4 +- 11 files changed, 36 insertions(+), 75 deletions(-) delete mode 100644 internal/forecast/thresholds.go diff --git a/docs/internal/forecast-derivation.md b/docs/internal/forecast-derivation.md index 90ebf82..3320212 100644 --- a/docs/internal/forecast-derivation.md +++ b/docs/internal/forecast-derivation.md @@ -65,7 +65,7 @@ implementation rules. ## Verification and invariants Focused tests cover local civil days, clipped periods, daypart resolution, -summary metrics, precipitation windows, threshold helpers, and alert overlap: +summary metrics, precipitation-window threshold behavior, and alert overlap: ```sh go test ./internal/forecast ./internal/timeutil diff --git a/docs/internal/module.md b/docs/internal/module.md index 98cf868..3d4c67b 100644 --- a/docs/internal/module.md +++ b/docs/internal/module.md @@ -1,6 +1,6 @@ # Module Contract Internals -`internal/module` defines the stable envelope between report composition, +`internal/module` defines the envelope between report composition, module builders, in-memory snapshots, templates, and prompt packages. It does not define a report, execute a builder, or choose prompt-export policy; those responsibilities belong to [report registry](report-registry.md) and @@ -13,8 +13,8 @@ Each `Output` has a module ID, stanza name, rich `Value`, and runtime-only otherwise the rich value. This permits custom prompt exports without shrinking the template value. -`NewSnapshot` builds the ordered `weatherreporter.modules.v1` snapshot and -validates it. Its JSON representation contains IDs, stanza names, and rich values only; +`NewSnapshot` builds and validates the ordered in-memory snapshot. Its JSON +representation carries a package-owned schema marker, IDs, stanza names, and rich values only; `PromptValue` is deliberately excluded. `StanzaValue` decodes a named rich stanza into a caller-supplied type, reporting a missing stanza separately from a decoding error. diff --git a/internal/briefing/base_modules_test.go b/internal/briefing/base_modules_test.go index a1e2fc2..c575851 100644 --- a/internal/briefing/base_modules_test.go +++ b/internal/briefing/base_modules_test.go @@ -137,7 +137,7 @@ func TestHourlyForecastPrecipMentionThreshold(t *testing.T) { {StartTime: mustParseModuleTime("2026-05-29T09:00:00-05:00"), ProbabilityOfPrecipitationPercent: floatPtr(20)}, {StartTime: mustParseModuleTime("2026-05-29T10:00:00-05:00")}, } - value := hourlyForecastPeriodsWithPrecipMentionThreshold(periods, "America/Chicago", DefaultHourlyForecastPrecipMentionProbabilityThreshold) + value := hourlyForecastPeriodsWithPrecipMentionThreshold(periods, "America/Chicago", 20) if len(value) != 3 { t.Fatalf("periods length = %d, want 3", len(value)) } diff --git a/internal/briefing/derived_daypart_summaries_module.go b/internal/briefing/derived_daypart_summaries_module.go index e7f6503..56b9af4 100644 --- a/internal/briefing/derived_daypart_summaries_module.go +++ b/internal/briefing/derived_daypart_summaries_module.go @@ -164,7 +164,7 @@ func derivedDaypartSummaryValue(daypart forecast.DaypartSummary, timezone string value.MaxPopPercent = roundedInt(&daypart.MaxPrecipitationProbability.Value) value.MaxPopTime = clockLabel(daypart.MaxPrecipitationProbability.Time, timezone) value.MaxPopTimeLabel = hourMinuteLabel(daypart.MaxPrecipitationProbability.Time, timezone) - value.MentionPrecipitation = mentionHourlyForecastPrecipitation(&daypart.MaxPrecipitationProbability.Value, DefaultHourlyForecastPrecipMentionProbabilityThreshold) + value.MentionPrecipitation = mentionHourlyForecastPrecipitation(&daypart.MaxPrecipitationProbability.Value, hourlyForecastPrecipMentionProbabilityThreshold) } if daypart.PeakWindGust != nil { value.MaxWindGustMph = roundedInt(&daypart.PeakWindGust.Value) diff --git a/internal/briefing/derived_modules_test.go b/internal/briefing/derived_modules_test.go index fba3fe1..a497954 100644 --- a/internal/briefing/derived_modules_test.go +++ b/internal/briefing/derived_modules_test.go @@ -147,7 +147,7 @@ func TestPrecipTimingModuleHandlesRainyAndDryForecasts(t *testing.T) { t.Fatalf("BuildModule(rainy) error = %v", err) } rainy := moduleValue[PrecipTimingModule](t, output) - if rainy.MaxPopPercent == nil || *rainy.MaxPopPercent != 80 || rainy.MaxPopTime != "12 PM" || rainy.ProbabilityThreshold != forecast.DefaultPrecipWindowProbabilityThreshold || !rainy.ThunderMentioned { + if rainy.MaxPopPercent == nil || *rainy.MaxPopPercent != 80 || rainy.MaxPopTime != "12 PM" || rainy.ProbabilityThreshold != 40 || !rainy.ThunderMentioned { t.Fatalf("rainy precip timing = %#v, want peak, threshold, and thunder", rainy) } if len(rainy.PrecipitationWindows) != 2 { @@ -266,7 +266,7 @@ func TestPrecipTimingModuleBuildsExpectationPhrases(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { value := precipTimingValue(forecast.PrecipTiming{ - ProbabilityThreshold: forecast.DefaultPrecipWindowProbabilityThreshold, + ProbabilityThreshold: 40, PrecipitationWindows: []forecast.PrecipitationWindow{ { Start: now, @@ -274,7 +274,7 @@ func TestPrecipTimingModuleBuildsExpectationPhrases(t *testing.T) { Value: tt.maxPop, Time: now, }, - ProbabilityThreshold: forecast.DefaultPrecipWindowProbabilityThreshold, + ProbabilityThreshold: 40, TextDescriptions: tt.descriptions, }, }, diff --git a/internal/briefing/hourly_forecast_module.go b/internal/briefing/hourly_forecast_module.go index d43688b..62a6536 100644 --- a/internal/briefing/hourly_forecast_module.go +++ b/internal/briefing/hourly_forecast_module.go @@ -9,7 +9,7 @@ import ( "gitea.maximumdirect.net/eric/weatherreporter/internal/weatherdata" ) -const DefaultHourlyForecastPrecipMentionProbabilityThreshold = 20 +const hourlyForecastPrecipMentionProbabilityThreshold = 20 type HourlyForecastModule struct { Product string `json:"product,omitempty"` @@ -184,7 +184,7 @@ func hourlyForecastPromptPeriods(periods []HourlyForecastPeriod) []HourlyForecas } func hourlyForecastPeriods(periods []weatherdata.ForecastPeriod, timezone string) []HourlyForecastPeriod { - return hourlyForecastPeriodsWithPrecipMentionThreshold(periods, timezone, DefaultHourlyForecastPrecipMentionProbabilityThreshold) + return hourlyForecastPeriodsWithPrecipMentionThreshold(periods, timezone, hourlyForecastPrecipMentionProbabilityThreshold) } func hourlyForecastPeriodsWithPrecipMentionThreshold(periods []weatherdata.ForecastPeriod, timezone string, threshold float64) []HourlyForecastPeriod { diff --git a/internal/forecast/derive.go b/internal/forecast/derive.go index 9be4a23..f57ca42 100644 --- a/internal/forecast/derive.go +++ b/internal/forecast/derive.go @@ -47,7 +47,7 @@ type TimedValue struct { Time time.Time `json:"time"` } -const DefaultPrecipWindowProbabilityThreshold = 40 +const precipWindowProbabilityThreshold = 40 type Indicators struct { Snow bool `json:"snow,omitempty"` @@ -86,7 +86,7 @@ type PrecipitationWindow struct { } func BuildPrecipTiming(periods []weatherdata.ForecastPeriod) PrecipTiming { - return buildPrecipTimingWithThreshold(periods, DefaultPrecipWindowProbabilityThreshold) + return buildPrecipTimingWithThreshold(periods, precipWindowProbabilityThreshold) } func buildPrecipTimingWithThreshold(periods []weatherdata.ForecastPeriod, threshold float64) PrecipTiming { diff --git a/internal/forecast/derive_test.go b/internal/forecast/derive_test.go index 88519f4..492a53e 100644 --- a/internal/forecast/derive_test.go +++ b/internal/forecast/derive_test.go @@ -484,8 +484,8 @@ func TestBuildPrecipTimingBuildsThresholdWindows(t *testing.T) { if timing.MaxPrecipitationProbability == nil || timing.MaxPrecipitationProbability.Value != 80 { t.Fatalf("MaxPrecipitationProbability = %#v, want 80", timing.MaxPrecipitationProbability) } - if timing.ProbabilityThreshold != DefaultPrecipWindowProbabilityThreshold { - t.Fatalf("ProbabilityThreshold = %v, want default threshold", timing.ProbabilityThreshold) + if timing.ProbabilityThreshold != 40 { + t.Fatalf("ProbabilityThreshold = %v, want 40", timing.ProbabilityThreshold) } if len(timing.PrecipitationWindows) != 2 { t.Fatalf("PrecipitationWindows length = %d, want 2: %#v", len(timing.PrecipitationWindows), timing.PrecipitationWindows) @@ -567,26 +567,14 @@ func TestBuildPrecipTimingHandlesDryForecast(t *testing.T) { if timing.FirstPrecipitation != nil || timing.LastPrecipitation != nil || len(timing.PrecipitationWindows) != 0 || timing.ThunderMentioned { t.Fatalf("dry timing = %#v, want no precip timing and no thunder", timing) } - if timing.ProbabilityThreshold != DefaultPrecipWindowProbabilityThreshold { - t.Fatalf("ProbabilityThreshold = %v, want default threshold", timing.ProbabilityThreshold) + if timing.ProbabilityThreshold != 40 { + t.Fatalf("ProbabilityThreshold = %v, want 40", timing.ProbabilityThreshold) } if timing.MaxPrecipitationProbability == nil || timing.MaxPrecipitationProbability.Value != 0 { t.Fatalf("dry max precip = %#v, want checked zero chance", timing.MaxPrecipitationProbability) } } -func TestThresholdHelpers(t *testing.T) { - if !DifferenceAtLeast(50, 56, 5) { - t.Fatal("DifferenceAtLeast = false, want true") - } - if !CrossesAtOrAbove(29, 32, 32) { - t.Fatal("CrossesAtOrAbove = false, want true") - } - if !CrossesBelow(35, 31, 32) { - t.Fatal("CrossesBelow = false, want true") - } -} - func testBundle(location *time.Location) *weatherdata.Bundle { return &weatherdata.Bundle{ Hourly: &weatherdata.ForecastRun{Periods: []weatherdata.ForecastPeriod{ diff --git a/internal/forecast/thresholds.go b/internal/forecast/thresholds.go deleted file mode 100644 index e087929..0000000 --- a/internal/forecast/thresholds.go +++ /dev/null @@ -1,20 +0,0 @@ -package forecast - -func DifferenceAtLeast(previous float64, current float64, threshold float64) bool { - return abs(current-previous) >= threshold -} - -func CrossesAtOrAbove(previous float64, current float64, threshold float64) bool { - return previous < threshold && current >= threshold -} - -func CrossesBelow(previous float64, current float64, threshold float64) bool { - return previous >= threshold && current < threshold -} - -func abs(value float64) float64 { - if value < 0 { - return -value - } - return value -} diff --git a/internal/module/module.go b/internal/module/module.go index 3337ee7..9629846 100644 --- a/internal/module/module.go +++ b/internal/module/module.go @@ -6,7 +6,7 @@ import ( "fmt" ) -const SnapshotSchemaVersion = "weatherreporter.modules.v1" +const snapshotSchemaVersion = "weatherreporter.modules.v1" type ID string @@ -55,16 +55,16 @@ func (o Output) DataPackageValue() any { func NewSnapshot(outputs []Output) (Snapshot, error) { snapshot := Snapshot{ - SchemaVersion: SnapshotSchemaVersion, + SchemaVersion: snapshotSchemaVersion, Outputs: append([]Output(nil), outputs...), } - if err := snapshot.Validate(); err != nil { + if err := snapshot.validate(); err != nil { return Snapshot{}, err } return snapshot, nil } -func (s Snapshot) Validate() error { +func (s Snapshot) validate() error { if s.SchemaVersion == "" { return fmt.Errorf("schemaVersion is required") } @@ -89,29 +89,22 @@ func (s Snapshot) Validate() error { return nil } -func (s Snapshot) LookupStanza(name string) (Output, bool) { - for _, output := range s.Outputs { - if output.StanzaName == name { - return output, true - } - } - return Output{}, false -} - func StanzaValue[T any](s Snapshot, name string) (T, bool, error) { var zero T - output, ok := s.LookupStanza(name) - if !ok { - return zero, false, nil + for _, output := range s.Outputs { + if output.StanzaName != name { + continue + } + data, err := json.Marshal(output.Value) + if err != nil { + return zero, true, fmt.Errorf("marshal stanza %q: %w", name, err) + } + if err := json.Unmarshal(data, &zero); err != nil { + return zero, true, fmt.Errorf("decode stanza %q: %w", name, err) + } + return zero, true, nil } - data, err := json.Marshal(output.Value) - if err != nil { - return zero, true, fmt.Errorf("marshal stanza %q: %w", name, err) - } - if err := json.Unmarshal(data, &zero); err != nil { - return zero, true, fmt.Errorf("decode stanza %q: %w", name, err) - } - return zero, true, nil + return zero, false, nil } type MissingDataBehavior string diff --git a/internal/module/module_test.go b/internal/module/module_test.go index 6923a6b..8424a5c 100644 --- a/internal/module/module_test.go +++ b/internal/module/module_test.go @@ -24,8 +24,8 @@ func TestSnapshotPreservesOutputOrderAndJSON(t *testing.T) { if err != nil { t.Fatalf("NewSnapshot() error = %v", err) } - if snapshot.SchemaVersion != SnapshotSchemaVersion { - t.Fatalf("SchemaVersion = %q, want %q", snapshot.SchemaVersion, SnapshotSchemaVersion) + if snapshot.SchemaVersion != "weatherreporter.modules.v1" { + t.Fatalf("SchemaVersion = %q, want weatherreporter.modules.v1", snapshot.SchemaVersion) } if snapshot.Outputs[0].ID != Metadata || snapshot.Outputs[1].ID != AlertDigest { t.Fatalf("Outputs order = %#v, want input order", snapshot.Outputs)