Add Daily recent changes comparison

This commit is contained in:
2026-05-29 17:53:03 +00:00
parent b17a3591e0
commit 53a4abd508
12 changed files with 602 additions and 12 deletions

View File

@@ -11,6 +11,7 @@ import (
"gitea.maximumdirect.net/eric/weatherreporter/internal/adapters/scriptorium"
"gitea.maximumdirect.net/eric/weatherreporter/internal/adapters/weatherapi"
"gitea.maximumdirect.net/eric/weatherreporter/internal/briefing"
"gitea.maximumdirect.net/eric/weatherreporter/internal/changes"
"gitea.maximumdirect.net/eric/weatherreporter/internal/config"
"gitea.maximumdirect.net/eric/weatherreporter/internal/forecast"
"gitea.maximumdirect.net/eric/weatherreporter/internal/promptinput"
@@ -85,6 +86,7 @@ type DailyReportResult struct {
Metadata state.Metadata
MetadataPath string
PriorSnapshot *state.PriorSnapshot
RecentChanges []changes.Change
RenderResult *scriptorium.RenderResult
RunResult *scriptorium.RunResult
}
@@ -267,7 +269,11 @@ func GenerateDailyReport(ctx context.Context, req DailyReportRequest) (*DailyRep
return nil, err
}
dataPackage, err := promptinput.Build(briefingPackage)
recentChanges, err := dailyRecentChanges(ctx, store, priorSnapshot, briefingPackage, req.Config.RecentChange)
if err != nil {
return nil, err
}
dataPackage, err := promptinput.BuildWithRecentChanges(briefingPackage, recentChanges)
if err != nil {
return nil, err
}
@@ -352,6 +358,7 @@ func GenerateDailyReport(ctx context.Context, req DailyReportRequest) (*DailyRep
Metadata: metadata,
MetadataPath: metadataPath,
PriorSnapshot: priorSnapshot,
RecentChanges: recentChanges,
RenderResult: renderResult,
RunResult: runResult,
}, nil
@@ -386,6 +393,22 @@ func defaultStore(cfg config.Config) (*state.FilesystemStore, error) {
return state.NewFilesystemStore(cfg.Workspace)
}
func dailyRecentChanges(ctx context.Context, store state.Store, priorSnapshot *state.PriorSnapshot, current briefing.Package, cfg config.RecentChangeConfig) ([]changes.Change, error) {
if priorSnapshot == nil {
return nil, nil
}
previous, err := store.LoadBriefing(ctx, priorSnapshot.BriefingPath)
if err != nil {
return nil, err
}
return changes.CompareDaily(previous, current, changes.Thresholds{
TemperatureDegrees: cfg.TemperatureDegrees,
PrecipProbabilityPoints: cfg.PrecipProbabilityPoints,
WindGustMilesPerHour: cfg.WindGustMilesPerHour,
PrecipTimingShiftMinutes: cfg.PrecipTimingShiftMinutes,
})
}
func copyFileAtomic(source string, target string) error {
data, err := os.ReadFile(source)
if err != nil {

View File

@@ -12,7 +12,9 @@ import (
"time"
"gitea.maximumdirect.net/eric/weatherreporter/internal/adapters/scriptorium"
"gitea.maximumdirect.net/eric/weatherreporter/internal/briefing"
"gitea.maximumdirect.net/eric/weatherreporter/internal/config"
"gitea.maximumdirect.net/eric/weatherreporter/internal/forecast"
"gitea.maximumdirect.net/eric/weatherreporter/internal/report"
"gitea.maximumdirect.net/eric/weatherreporter/internal/state"
)
@@ -222,6 +224,9 @@ func TestGenerateDailyReportWritesReportAndPreflight(t *testing.T) {
if result.Metadata.RenderedReportPath != result.ReportPath {
t.Fatalf("metadata rendered report path = %q, want %q", result.Metadata.RenderedReportPath, result.ReportPath)
}
if len(result.RecentChanges) != 0 {
t.Fatalf("RecentChanges = %#v, want none without prior snapshot", result.RecentChanges)
}
report, err := os.ReadFile(outputPath)
if err != nil {
t.Fatalf("read report output: %v", err)
@@ -337,6 +342,78 @@ func TestGenerateDailyReportReturnsRunErrorAfterPreflight(t *testing.T) {
}
}
func TestGenerateDailyReportIncludesRecentChangesFromPriorSnapshot(t *testing.T) {
server := dailyBundleServer(t)
cfg := config.Defaults()
cfg.WeatherAPI.BaseURL = server.URL + "/"
cfg.WeatherAPI.Timezone = "America/Chicago"
cfg.Workspace.Root = t.TempDir()
store, err := state.NewFilesystemStore(cfg.Workspace)
if err != nil {
t.Fatalf("NewFilesystemStore() error = %v", err)
}
priorResolved, err := ResolveGenerate(GenerateRequest{
Config: cfg,
Report: ReportDaily,
Date: mustParse("2026-05-29T12:00:00-05:00"),
}, mustParse("2026-05-29T04:00:00-05:00"))
if err != nil {
t.Fatalf("ResolveGenerate(prior) error = %v", err)
}
priorBriefing := priorDailyBriefing(priorResolved)
priorBriefingPath, err := store.SaveBriefing(context.Background(), priorResolved, priorBriefing)
if err != nil {
t.Fatalf("SaveBriefing() error = %v", err)
}
priorPaths, err := store.Paths(priorResolved)
if err != nil {
t.Fatalf("Paths() error = %v", err)
}
_, err = store.SaveMetadata(context.Background(), state.BuildMetadata(priorResolved, priorBriefing, state.ArtifactPaths{
Briefing: priorBriefingPath,
DataPackage: priorPaths.DataPackage,
Preflight: priorPaths.Preflight,
RenderedReport: priorPaths.RenderedReport,
}))
if err != nil {
t.Fatalf("SaveMetadata() error = %v", err)
}
currentResolved, err := ResolveGenerate(GenerateRequest{
Config: cfg,
Report: ReportDaily,
Date: mustParse("2026-05-29T12:00:00-05:00"),
}, mustParse("2026-05-29T05:00:00-05:00"))
if err != nil {
t.Fatalf("ResolveGenerate(current) error = %v", err)
}
renderer := &recordingRenderer{
renderResult: &scriptorium.RenderResult{ExitCode: 0},
runResult: &scriptorium.RunResult{ExitCode: 0},
runBody: "# Daily Report\n",
}
result, err := GenerateDailyReport(context.Background(), DailyReportRequest{
Config: cfg,
Resolved: currentResolved,
Renderer: renderer,
Store: store,
})
if err != nil {
t.Fatalf("GenerateDailyReport() error = %v", err)
}
if len(result.RecentChanges) == 0 {
t.Fatal("RecentChanges length = 0, want changes from prior snapshot")
}
data, err := os.ReadFile(result.DataPackagePath)
if err != nil {
t.Fatalf("read data package: %v", err)
}
if !strings.Contains(string(data), "alert_added") || !strings.Contains(string(data), "temperature_shift") {
t.Fatalf("data package missing recent changes:\n%s", string(data))
}
}
func TestResolveGenerateMapsCommandToReportDefinition(t *testing.T) {
cfg := config.Defaults()
cfg.WeatherAPI.Timezone = "America/Chicago"
@@ -435,6 +512,37 @@ func mustParse(value string) time.Time {
return parsed
}
func priorDailyBriefing(resolved report.Resolved) briefing.Package {
low := 50.0
high := 58.0
precip := 10.0
return briefing.Package{
Metadata: briefing.Metadata{
SchemaVersion: briefing.SchemaVersion,
RunID: resolved.Metadata().RunID,
ReportID: resolved.Definition.ID,
Variant: "today",
PromptID: resolved.Definition.PromptID,
GeneratedAt: resolved.GeneratedAt,
Units: "us",
Timezone: resolved.Timezone,
ValidPeriod: resolved.ValidPeriod,
},
Daily: &briefing.Daily{
BottomLine: briefing.BottomLine{
Temperature: forecast.Range{Min: &low, Max: &high},
MaxPrecipProbability: &forecast.TimedValue{
Value: precip,
Time: resolved.ValidPeriod.Start.Add(6 * time.Hour),
},
},
Dayparts: []forecast.DaypartSummary{
{Name: "morning"},
},
},
}
}
type recordingRenderer struct {
renderCalls int
runCalls int