Retire unused module and forecast compatibility exports

This commit is contained in:
2026-08-13 04:15:42 +00:00
parent c3ebf06bd5
commit cd7b9aef2b
11 changed files with 36 additions and 75 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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