Add report registry and valid periods

This commit is contained in:
2026-05-29 17:17:50 +00:00
parent d494550b20
commit caf21dfedd
8 changed files with 763 additions and 6 deletions

View File

@@ -1,7 +1,8 @@
# Weatherreporter CLI
`weatherreporter` currently resolves configuration and command requests, then
returns a not-implemented error for report generation and scheduled runs.
`weatherreporter` currently resolves configuration, command requests, report
definitions, and valid periods, then returns a not-implemented error for report
generation and scheduled runs.
## Shortest Useful Command
@@ -9,8 +10,8 @@ returns a not-implemented error for report generation and scheduled runs.
weatherreporter generate daily --date 2026-05-29 --out ./daily.md
```
The command parses flags, loads configuration, resolves the request, and then
stops before weather data fetching or report rendering.
The command parses flags, loads configuration, resolves report identity and the
valid period, and then stops before weather data fetching or report rendering.
## Command Overview

View File

@@ -0,0 +1,63 @@
# Report Registry Internals
This document describes the implemented report identity and valid-period
boundary.
## Purpose
`internal/report` centralizes report IDs, prompt IDs, comparison strategies,
valid-period resolution, report metadata, and scheduled batch membership.
## Inputs and Outputs
Inputs:
- report ID or batch name
- generation time
- configured timezone
- optional Daily date override
- optional manual storm start and end times
Outputs:
- `report.Resolved` values with definition metadata and half-open valid periods
- `report.Metadata` values suitable for later persisted run metadata
## Boundaries
- This package defines report identity and time coverage only.
- It does not fetch weather data, build briefings, compare snapshots, write
state, or call `scriptorium`.
## Behavior
- Daily Today covers one configured local civil day.
- Daily Tomorrow covers the next configured local civil day.
- 3-Day Outlook covers generation time through local midnight after the second
following local civil day.
- Weekend Outlook covers Saturday 00:00 to Monday 00:00 Monday through
Thursday; Friday and Saturday cover the remaining weekend from Friday 18:00
or generation time, whichever is later.
- Manual Storm Report uses explicit start and end times.
- Morning batch resolves Daily Today and 3-Day Outlook, plus Weekend Outlook
except on Sunday.
- Evening batch resolves Daily Tomorrow.
## Failure Behavior
- Unknown report and batch names return actionable errors.
- Sunday Weekend Outlook resolution returns an error.
- Storm windows require start and end, with end after start.
## Tests
Inspect:
- `internal/report/period_test.go`
- `internal/app/app_test.go`
## Invariants
- Report selection goes through the registry.
- Valid periods are independent of rendered report text.
- Prompt IDs and comparison strategies are declared with report definitions.

View File

@@ -9,6 +9,8 @@ import (
"gitea.maximumdirect.net/eric/weatherreporter/internal/adapters/weatherapi"
"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/timeutil"
)
type ReportKind string
@@ -49,16 +51,81 @@ type FetchBundleRequest struct {
func Generate(ctx context.Context, req GenerateRequest) error {
_ = ctx
_ = req
if _, err := ResolveGenerate(req, time.Now()); err != nil {
return err
}
return fmt.Errorf("generate is not implemented")
}
func RunBatch(ctx context.Context, req BatchRequest) error {
_ = ctx
_ = req
if _, err := ResolveBatch(req, time.Now()); err != nil {
return err
}
return fmt.Errorf("run is not implemented")
}
func ResolveGenerate(req GenerateRequest, now time.Time) (report.Resolved, error) {
location, err := timeutil.LoadLocation(req.Config.WeatherAPI.Timezone)
if err != nil {
return report.Resolved{}, err
}
id, err := reportIDForCommand(req.Report)
if err != nil {
return report.Resolved{}, err
}
return report.DefaultRegistry().Resolve(id, report.ResolveRequest{
Now: now,
Location: location,
Date: req.Date,
StormStart: req.StormStart,
StormEnd: req.StormEnd,
})
}
func ResolveBatch(req BatchRequest, now time.Time) ([]report.Resolved, error) {
location, err := timeutil.LoadLocation(req.Config.WeatherAPI.Timezone)
if err != nil {
return nil, err
}
batch, err := reportBatchForCommand(req.Batch)
if err != nil {
return nil, err
}
return report.DefaultRegistry().BatchReports(batch, report.ResolveRequest{
Now: now,
Location: location,
})
}
func reportIDForCommand(kind ReportKind) (report.ID, error) {
switch kind {
case ReportDaily:
return report.DailyToday, nil
case ReportTomorrow:
return report.DailyTomorrow, nil
case ReportThreeDay:
return report.ThreeDay, nil
case ReportWeekend:
return report.Weekend, nil
case ReportStorm:
return report.Storm, nil
default:
return "", fmt.Errorf("unknown report command %q", kind)
}
}
func reportBatchForCommand(kind BatchKind) (report.Batch, error) {
switch kind {
case BatchMorning:
return report.Morning, nil
case BatchEvening:
return report.Evening, nil
default:
return "", fmt.Errorf("unknown batch command %q", kind)
}
}
func FetchBundle(ctx context.Context, req FetchBundleRequest) (*forecast.Bundle, error) {
client, err := weatherapi.New(req.Config)
if err != nil {

View File

@@ -8,8 +8,10 @@ import (
"path/filepath"
"strings"
"testing"
"time"
"gitea.maximumdirect.net/eric/weatherreporter/internal/config"
"gitea.maximumdirect.net/eric/weatherreporter/internal/report"
)
func TestFetchAndSaveBundle(t *testing.T) {
@@ -62,3 +64,77 @@ func TestFetchAndSaveBundleRequiresOutputPath(t *testing.T) {
t.Fatalf("error = %q, want output path context", err.Error())
}
}
func TestResolveGenerateMapsCommandToReportDefinition(t *testing.T) {
cfg := config.Defaults()
cfg.WeatherAPI.Timezone = "America/Chicago"
now := mustParse("2026-05-29T18:00:00-05:00")
resolved, err := ResolveGenerate(GenerateRequest{
Config: cfg,
Report: ReportTomorrow,
}, now)
if err != nil {
t.Fatalf("ResolveGenerate() error = %v", err)
}
if resolved.Definition.ID != report.DailyTomorrow {
t.Fatalf("ID = %q, want daily_tomorrow", resolved.Definition.ID)
}
if resolved.Definition.PromptID != "weather.daily_report" {
t.Fatalf("PromptID = %q, want weather.daily_report", resolved.Definition.PromptID)
}
if got := resolved.ValidPeriod.Start.Format("2006-01-02"); got != "2026-05-30" {
t.Fatalf("valid start date = %s, want 2026-05-30", got)
}
}
func TestResolveGenerateStorm(t *testing.T) {
cfg := config.Defaults()
cfg.WeatherAPI.Timezone = "America/Chicago"
now := mustParse("2026-05-29T12:00:00-05:00")
start := mustParse("2026-05-29T18:00:00-05:00")
end := mustParse("2026-05-30T06:00:00-05:00")
resolved, err := ResolveGenerate(GenerateRequest{
Config: cfg,
Report: ReportStorm,
StormStart: start,
StormEnd: end,
}, now)
if err != nil {
t.Fatalf("ResolveGenerate() error = %v", err)
}
if resolved.Definition.ID != report.Storm {
t.Fatalf("ID = %q, want storm", resolved.Definition.ID)
}
if !resolved.ValidPeriod.Start.Equal(start) || !resolved.ValidPeriod.End.Equal(end) {
t.Fatalf("period = %#v, want storm window", resolved.ValidPeriod)
}
}
func TestResolveBatchMorningSkipsWeekendOnSunday(t *testing.T) {
cfg := config.Defaults()
cfg.WeatherAPI.Timezone = "America/Chicago"
now := mustParse("2026-05-31T06:00:00-05:00")
resolved, err := ResolveBatch(BatchRequest{Config: cfg, Batch: BatchMorning}, now)
if err != nil {
t.Fatalf("ResolveBatch() error = %v", err)
}
if len(resolved) != 2 {
t.Fatalf("resolved length = %d, want 2", len(resolved))
}
for _, item := range resolved {
if item.Definition.ID == report.Weekend {
t.Fatal("morning batch included weekend on Sunday")
}
}
}
func mustParse(value string) time.Time {
parsed, err := time.Parse(time.RFC3339, value)
if err != nil {
panic(err)
}
return parsed
}

View File

@@ -0,0 +1,87 @@
// Package report defines report identities, registry metadata, and valid periods.
package report
import (
"fmt"
"time"
"gitea.maximumdirect.net/eric/weatherreporter/internal/timeutil"
)
type ID string
const (
DailyToday ID = "daily_today"
DailyTomorrow ID = "daily_tomorrow"
ThreeDay ID = "three_day"
Weekend ID = "weekend"
Storm ID = "storm"
)
type ComparisonStrategy string
const (
CompareSameValidDate ComparisonStrategy = "same_valid_date"
CompareWeekendWindow ComparisonStrategy = "same_weekend_window"
CompareExplicitWindow ComparisonStrategy = "explicit_event_window"
)
type Batch string
const (
Morning Batch = "morning"
Evening Batch = "evening"
)
type Definition struct {
ID ID
Name string
PromptID string
ComparisonStrategy ComparisonStrategy
DefaultOutputName string
Morning bool
Evening bool
resolve func(ResolveRequest) (timeutil.Period, error)
}
func (d Definition) ResolvePeriod(req ResolveRequest) (timeutil.Period, error) {
if d.resolve == nil {
return timeutil.Period{}, fmt.Errorf("report %q has no valid-period resolver", d.ID)
}
return d.resolve(req)
}
type ResolveRequest struct {
Now time.Time
Location *time.Location
Date time.Time
StormStart time.Time
StormEnd time.Time
}
type Resolved struct {
Definition Definition
GeneratedAt time.Time
Timezone string
ValidPeriod timeutil.Period
}
type Metadata struct {
RunID string `json:"runId"`
ReportID ID `json:"reportId"`
PromptID string `json:"promptId"`
GeneratedAt time.Time `json:"generatedAt"`
Timezone string `json:"timezone"`
ValidPeriod timeutil.Period `json:"validPeriod"`
}
func (r Resolved) Metadata() Metadata {
return Metadata{
RunID: r.GeneratedAt.UTC().Format("20060102T150405Z") + "_" + string(r.Definition.ID),
ReportID: r.Definition.ID,
PromptID: r.Definition.PromptID,
GeneratedAt: r.GeneratedAt,
Timezone: r.Timezone,
ValidPeriod: r.ValidPeriod,
}
}

144
internal/report/period.go Normal file
View File

@@ -0,0 +1,144 @@
package report
import (
"fmt"
"time"
"gitea.maximumdirect.net/eric/weatherreporter/internal/timeutil"
)
func Resolve(id ID, req ResolveRequest) (Resolved, error) {
return DefaultRegistry().Resolve(id, req)
}
func (r Registry) Resolve(id ID, req ResolveRequest) (Resolved, error) {
definition, err := r.Lookup(id)
if err != nil {
return Resolved{}, err
}
return r.resolveDefinition(definition, req)
}
func (r Registry) BatchReports(batch Batch, req ResolveRequest) ([]Resolved, error) {
if req.Location == nil {
req.Location = time.UTC
}
if req.Now.IsZero() {
req.Now = time.Now()
}
switch batch {
case Morning:
ids := []ID{DailyToday, ThreeDay}
if req.Now.In(req.Location).Weekday() != time.Sunday {
ids = append(ids, Weekend)
}
return r.resolveIDs(ids, req)
case Evening:
return r.resolveIDs([]ID{DailyTomorrow}, req)
default:
return nil, fmt.Errorf("unknown batch %q", batch)
}
}
func (r Registry) resolveIDs(ids []ID, req ResolveRequest) ([]Resolved, error) {
resolved := make([]Resolved, 0, len(ids))
for _, id := range ids {
item, err := r.Resolve(id, req)
if err != nil {
return nil, err
}
resolved = append(resolved, item)
}
return resolved, nil
}
func (r Registry) resolveDefinition(definition Definition, req ResolveRequest) (Resolved, error) {
if req.Location == nil {
req.Location = time.UTC
}
if req.Now.IsZero() {
req.Now = time.Now()
}
period, err := definition.ResolvePeriod(req)
if err != nil {
return Resolved{}, err
}
return Resolved{
Definition: definition,
GeneratedAt: req.Now,
Timezone: req.Location.String(),
ValidPeriod: period,
}, nil
}
func resolveDailyToday(req ResolveRequest) (timeutil.Period, error) {
if !req.Date.IsZero() {
return timeutil.CivilDay(req.Date, req.Location), nil
}
return timeutil.CivilDay(req.Now, req.Location), nil
}
func ParseStormPeriod(start string, end string, location *time.Location) (timeutil.Period, error) {
if location == nil {
location = time.UTC
}
startTime, err := timeutil.ParseStormTime(start, location)
if err != nil {
return timeutil.Period{}, err
}
endTime, err := timeutil.ParseStormTime(end, location)
if err != nil {
return timeutil.Period{}, err
}
period := timeutil.Period{Start: startTime, End: endTime}
if !period.IsValid() {
return timeutil.Period{}, fmt.Errorf("storm report requires end time after start time")
}
return period, nil
}
func resolveDailyTomorrow(req ResolveRequest) (timeutil.Period, error) {
return timeutil.CivilDay(req.Now.In(req.Location).AddDate(0, 0, 1), req.Location), nil
}
func resolveThreeDay(req ResolveRequest) (timeutil.Period, error) {
localNow := req.Now.In(req.Location)
endDate := localNow.AddDate(0, 0, 3)
end := time.Date(endDate.Year(), endDate.Month(), endDate.Day(), 0, 0, 0, 0, req.Location)
return timeutil.Period{Start: localNow, End: end}, nil
}
func resolveWeekend(req ResolveRequest) (timeutil.Period, error) {
localNow := req.Now.In(req.Location)
weekday := localNow.Weekday()
if weekday == time.Sunday {
return timeutil.Period{}, fmt.Errorf("weekend outlook is not scheduled on Sunday morning")
}
daysUntilSaturday := (int(time.Saturday) - int(weekday) + 7) % 7
saturday := localNow.AddDate(0, 0, daysUntilSaturday)
start := time.Date(saturday.Year(), saturday.Month(), saturday.Day(), 0, 0, 0, 0, req.Location)
if weekday == time.Friday || weekday == time.Saturday {
friday := start.AddDate(0, 0, -1)
fridayEvening := time.Date(friday.Year(), friday.Month(), friday.Day(), 18, 0, 0, 0, req.Location)
start = fridayEvening
if localNow.After(start) {
start = localNow
}
}
end := time.Date(saturday.Year(), saturday.Month(), saturday.Day(), 0, 0, 0, 0, req.Location).AddDate(0, 0, 2)
return timeutil.Period{Start: start, End: end}, nil
}
func resolveStorm(req ResolveRequest) (timeutil.Period, error) {
if req.StormStart.IsZero() {
return timeutil.Period{}, fmt.Errorf("storm report requires a start time")
}
if req.StormEnd.IsZero() {
return timeutil.Period{}, fmt.Errorf("storm report requires an end time")
}
if !req.StormEnd.After(req.StormStart) {
return timeutil.Period{}, fmt.Errorf("storm report requires end time after start time")
}
return timeutil.Period{Start: req.StormStart, End: req.StormEnd}, nil
}

View File

@@ -0,0 +1,231 @@
package report
import (
"strings"
"testing"
"time"
"gitea.maximumdirect.net/eric/weatherreporter/internal/timeutil"
)
func TestDailyValidPeriod(t *testing.T) {
location := mustLoadLocation(t)
now := mustParse("2026-05-29T17:45:00-05:00")
resolved, err := Resolve(DailyToday, ResolveRequest{Now: now, Location: location})
if err != nil {
t.Fatalf("Resolve() error = %v", err)
}
assertPeriod(t, resolved.ValidPeriod, "2026-05-29T00:00:00-05:00", "2026-05-30T00:00:00-05:00")
}
func TestDailyValidPeriodCanUseExplicitDate(t *testing.T) {
location := mustLoadLocation(t)
now := mustParse("2026-05-29T17:45:00-05:00")
date := mustParse("2026-05-31T12:00:00-05:00")
resolved, err := Resolve(DailyToday, ResolveRequest{Now: now, Location: location, Date: date})
if err != nil {
t.Fatalf("Resolve() error = %v", err)
}
assertPeriod(t, resolved.ValidPeriod, "2026-05-31T00:00:00-05:00", "2026-06-01T00:00:00-05:00")
}
func TestTomorrowValidPeriodFromEveningGeneration(t *testing.T) {
location := mustLoadLocation(t)
now := mustParse("2026-05-29T20:00:00-05:00")
resolved, err := Resolve(DailyTomorrow, ResolveRequest{Now: now, Location: location})
if err != nil {
t.Fatalf("Resolve() error = %v", err)
}
assertPeriod(t, resolved.ValidPeriod, "2026-05-30T00:00:00-05:00", "2026-05-31T00:00:00-05:00")
if resolved.Definition.PromptID != "weather.daily_report" {
t.Fatalf("PromptID = %q, want weather.daily_report", resolved.Definition.PromptID)
}
}
func TestThreeDayPeriodCalculation(t *testing.T) {
location := mustLoadLocation(t)
now := mustParse("2026-05-29T05:00:00-05:00")
resolved, err := Resolve(ThreeDay, ResolveRequest{Now: now, Location: location})
if err != nil {
t.Fatalf("Resolve() error = %v", err)
}
assertPeriod(t, resolved.ValidPeriod, "2026-05-29T05:00:00-05:00", "2026-06-01T00:00:00-05:00")
}
func TestWeekendPeriodCalculation(t *testing.T) {
location := mustLoadLocation(t)
tests := []struct {
name string
now string
start string
end string
}{
{name: "monday", now: "2026-05-25T05:00:00-05:00", start: "2026-05-30T00:00:00-05:00", end: "2026-06-01T00:00:00-05:00"},
{name: "friday before evening", now: "2026-05-29T05:00:00-05:00", start: "2026-05-29T18:00:00-05:00", end: "2026-06-01T00:00:00-05:00"},
{name: "friday after evening", now: "2026-05-29T19:30:00-05:00", start: "2026-05-29T19:30:00-05:00", end: "2026-06-01T00:00:00-05:00"},
{name: "saturday", now: "2026-05-30T08:00:00-05:00", start: "2026-05-30T08:00:00-05:00", end: "2026-06-01T00:00:00-05:00"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
resolved, err := Resolve(Weekend, ResolveRequest{Now: mustParse(tt.now), Location: location})
if err != nil {
t.Fatalf("Resolve() error = %v", err)
}
assertPeriod(t, resolved.ValidPeriod, tt.start, tt.end)
})
}
}
func TestWeekendSundayErrors(t *testing.T) {
location := mustLoadLocation(t)
_, err := Resolve(Weekend, ResolveRequest{Now: mustParse("2026-05-31T08:00:00-05:00"), Location: location})
if err == nil {
t.Fatal("Resolve() error = nil, want Sunday weekend error")
}
if !strings.Contains(err.Error(), "Sunday") {
t.Fatalf("error = %q, want Sunday context", err.Error())
}
}
func TestStormManualPeriodParsingAndValidation(t *testing.T) {
location := mustLoadLocation(t)
period, err := ParseStormPeriod("2026-05-29T18:00", "2026-05-30T06:00:00-05:00", location)
if err != nil {
t.Fatalf("ParseStormPeriod() error = %v", err)
}
assertPeriod(t, period, "2026-05-29T18:00:00-05:00", "2026-05-30T06:00:00-05:00")
_, err = ParseStormPeriod("2026-05-30T06:00", "2026-05-29T18:00", location)
if err == nil {
t.Fatal("ParseStormPeriod() error = nil, want invalid period error")
}
}
func TestStormResolve(t *testing.T) {
location := mustLoadLocation(t)
resolved, err := Resolve(Storm, ResolveRequest{
Now: mustParse("2026-05-29T12:00:00-05:00"),
Location: location,
StormStart: mustParse("2026-05-29T18:00:00-05:00"),
StormEnd: mustParse("2026-05-30T06:00:00-05:00"),
})
if err != nil {
t.Fatalf("Resolve() error = %v", err)
}
assertPeriod(t, resolved.ValidPeriod, "2026-05-29T18:00:00-05:00", "2026-05-30T06:00:00-05:00")
if resolved.Definition.ComparisonStrategy != CompareExplicitWindow {
t.Fatalf("ComparisonStrategy = %q, want explicit event window", resolved.Definition.ComparisonStrategy)
}
}
func TestMorningBatchSkipsWeekendOnSunday(t *testing.T) {
location := mustLoadLocation(t)
resolved, err := DefaultRegistry().BatchReports(Morning, ResolveRequest{
Now: mustParse("2026-05-31T06:00:00-05:00"),
Location: location,
})
if err != nil {
t.Fatalf("BatchReports() error = %v", err)
}
ids := resolvedIDs(resolved)
if strings.Join(ids, ",") != "daily_today,three_day" {
t.Fatalf("ids = %v, want daily_today and three_day", ids)
}
}
func TestEveningBatchIncludesTomorrow(t *testing.T) {
location := mustLoadLocation(t)
resolved, err := DefaultRegistry().BatchReports(Evening, ResolveRequest{
Now: mustParse("2026-05-29T18:00:00-05:00"),
Location: location,
})
if err != nil {
t.Fatalf("BatchReports() error = %v", err)
}
ids := resolvedIDs(resolved)
if strings.Join(ids, ",") != "daily_tomorrow" {
t.Fatalf("ids = %v, want daily_tomorrow", ids)
}
}
func TestRegistryLookupErrorIsActionable(t *testing.T) {
_, err := DefaultRegistry().Lookup(ID("unknown"))
if err == nil {
t.Fatal("Lookup() error = nil, want unknown report error")
}
if !strings.Contains(err.Error(), `unknown report "unknown"`) {
t.Fatalf("error = %q, want unknown report context", err.Error())
}
}
func TestRegistryDefinitionsHavePromptIDsAndComparisonStrategies(t *testing.T) {
for _, definition := range DefaultRegistry().All() {
if definition.PromptID == "" {
t.Fatalf("%s PromptID is empty", definition.ID)
}
if definition.ComparisonStrategy == "" {
t.Fatalf("%s ComparisonStrategy is empty", definition.ID)
}
}
}
func TestResolvedMetadata(t *testing.T) {
location := mustLoadLocation(t)
resolved, err := Resolve(DailyToday, ResolveRequest{Now: mustParse("2026-05-29T05:00:00-05:00"), Location: location})
if err != nil {
t.Fatalf("Resolve() error = %v", err)
}
metadata := resolved.Metadata()
if metadata.ReportID != DailyToday {
t.Fatalf("ReportID = %q, want daily_today", metadata.ReportID)
}
if metadata.PromptID != "weather.daily_report" {
t.Fatalf("PromptID = %q, want weather.daily_report", metadata.PromptID)
}
if !strings.Contains(metadata.RunID, "daily_today") {
t.Fatalf("RunID = %q, want report id", metadata.RunID)
}
}
func assertPeriod(t *testing.T, period timeutil.Period, wantStart string, wantEnd string) {
t.Helper()
if !period.IsValid() {
t.Fatalf("period = %#v, want valid", period)
}
if period.Start.Format(time.RFC3339) != wantStart {
t.Fatalf("Start = %s, want %s", period.Start.Format(time.RFC3339), wantStart)
}
if period.End.Format(time.RFC3339) != wantEnd {
t.Fatalf("End = %s, want %s", period.End.Format(time.RFC3339), wantEnd)
}
}
func resolvedIDs(resolved []Resolved) []string {
ids := make([]string, 0, len(resolved))
for _, item := range resolved {
ids = append(ids, string(item.Definition.ID))
}
return ids
}
func mustLoadLocation(t *testing.T) *time.Location {
t.Helper()
location, err := time.LoadLocation("America/Chicago")
if err != nil {
t.Fatalf("load location: %v", err)
}
return location
}
func mustParse(value string) time.Time {
parsed, err := time.Parse(time.RFC3339, value)
if err != nil {
panic(err)
}
return parsed
}

View File

@@ -0,0 +1,88 @@
package report
import "fmt"
type Registry struct {
definitions map[ID]Definition
}
func DefaultRegistry() Registry {
definitions := []Definition{
{
ID: DailyToday,
Name: "Daily Report",
PromptID: "weather.daily_report",
ComparisonStrategy: CompareSameValidDate,
DefaultOutputName: "daily.md",
Morning: true,
resolve: resolveDailyToday,
},
{
ID: DailyTomorrow,
Name: "Tomorrow Planning Brief",
PromptID: "weather.daily_report",
ComparisonStrategy: CompareSameValidDate,
DefaultOutputName: "tomorrow.md",
Evening: true,
resolve: resolveDailyTomorrow,
},
{
ID: ThreeDay,
Name: "3-Day Outlook",
PromptID: "weather.three_day_outlook",
ComparisonStrategy: CompareSameValidDate,
DefaultOutputName: "three_day.md",
Morning: true,
resolve: resolveThreeDay,
},
{
ID: Weekend,
Name: "Weekend Outlook",
PromptID: "weather.weekend_outlook",
ComparisonStrategy: CompareWeekendWindow,
DefaultOutputName: "weekend.md",
Morning: true,
resolve: resolveWeekend,
},
{
ID: Storm,
Name: "Storm Report",
PromptID: "weather.storm_report",
ComparisonStrategy: CompareExplicitWindow,
DefaultOutputName: "storm.md",
resolve: resolveStorm,
},
}
registry := Registry{definitions: map[ID]Definition{}}
for _, definition := range definitions {
registry.definitions[definition.ID] = definition
}
return registry
}
func (r Registry) Lookup(id ID) (Definition, error) {
definition, ok := r.definitions[id]
if !ok {
return Definition{}, fmt.Errorf("unknown report %q", id)
}
return definition, nil
}
func (r Registry) MustLookup(id ID) Definition {
definition, err := r.Lookup(id)
if err != nil {
panic(err)
}
return definition
}
func (r Registry) All() []Definition {
ids := []ID{DailyToday, DailyTomorrow, ThreeDay, Weekend, Storm}
out := make([]Definition, 0, len(ids))
for _, id := range ids {
if definition, ok := r.definitions[id]; ok {
out = append(out, definition)
}
}
return out
}