Retire unused module and forecast compatibility exports
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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{
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user