Added a location block to the data package

This commit is contained in:
2026-05-29 19:57:08 -05:00
parent 42defcf4b9
commit 8a762bf34f
16 changed files with 152 additions and 9 deletions

View File

@@ -522,6 +522,7 @@ func BuildBriefing(req BriefingRequest, bundle *forecast.Bundle) (briefing.Packa
Bundle: bundle,
Units: req.Config.WeatherAPI.Units,
Timezone: req.Config.WeatherAPI.Timezone,
Location: briefingLocation(req.Config),
}, summary)
case report.ThreeDay, report.Weekend:
summaries, err := forecast.BuildPeriodDailySummaries(bundle, req.Resolved.ValidPeriod, location, dayparts)
@@ -534,6 +535,7 @@ func BuildBriefing(req BriefingRequest, bundle *forecast.Bundle) (briefing.Packa
Bundle: bundle,
Units: req.Config.WeatherAPI.Units,
Timezone: req.Config.WeatherAPI.Timezone,
Location: briefingLocation(req.Config),
}, summaries)
}
return briefing.BuildThreeDay(briefing.BuildContext{
@@ -541,6 +543,7 @@ func BuildBriefing(req BriefingRequest, bundle *forecast.Bundle) (briefing.Packa
Bundle: bundle,
Units: req.Config.WeatherAPI.Units,
Timezone: req.Config.WeatherAPI.Timezone,
Location: briefingLocation(req.Config),
}, summaries)
case report.Storm:
return briefing.BuildStorm(briefing.BuildContext{
@@ -548,12 +551,26 @@ func BuildBriefing(req BriefingRequest, bundle *forecast.Bundle) (briefing.Packa
Bundle: bundle,
Units: req.Config.WeatherAPI.Units,
Timezone: req.Config.WeatherAPI.Timezone,
Location: briefingLocation(req.Config),
})
default:
return briefing.Package{}, fmt.Errorf("briefing is not implemented for report %q", req.Resolved.Definition.ID)
}
}
func briefingLocation(cfg config.Config) *briefing.LocationContext {
location := briefing.LocationContext{
ID: cfg.Location.ID,
Name: cfg.Location.Name,
Region: cfg.Location.Region,
Timezone: cfg.WeatherAPI.Timezone,
}
if location.ID == "" && location.Name == "" && location.Region == "" && location.Timezone == "" {
return nil
}
return &location
}
func defaultStore(cfg config.Config) (*state.FilesystemStore, error) {
return state.NewFilesystemStore(cfg.Workspace)
}

View File

@@ -2,6 +2,7 @@ package app
import (
"context"
"encoding/json"
"errors"
"net/http"
"net/http/httptest"
@@ -195,6 +196,16 @@ func TestGenerateReportWritesReportAndPreflight(t *testing.T) {
if !strings.Contains(string(data), `"recentChanges"`) || !strings.Contains(string(data), `data_package.v1`) {
t.Fatalf("data package missing expected content:\n%s", string(data))
}
var savedDataPackage struct {
Briefing briefing.Package `json:"briefing"`
}
if err := json.Unmarshal(data, &savedDataPackage); err != nil {
t.Fatalf("decode data package: %v", err)
}
location := savedDataPackage.Briefing.Metadata.Location
if location == nil || location.ID != "home" || location.Name != "Brentwood" || location.Region != "St. Louis Metro" || location.Timezone != "America/Chicago" {
t.Fatalf("data package location = %#v, want configured prompt location", location)
}
if !strings.Contains(string(data), "Short-term AFD narrative for generated report.") || !strings.Contains(string(data), "Long-term AFD narrative for generated report.") {
t.Fatalf("data package missing AFD short/long-term discussion:\n%s", string(data))
}

View File

@@ -29,6 +29,12 @@ func TestDailyBriefingFromRepresentativeFixture(t *testing.T) {
Bundle: bundle,
Units: "us",
Timezone: "America/Chicago",
Location: &LocationContext{
ID: "home",
Name: "Brentwood",
Region: "St. Louis Metro",
Timezone: "America/Chicago",
},
}, summary)
if err != nil {
t.Fatalf("BuildDaily() error = %v", err)
@@ -46,6 +52,9 @@ func TestDailyBriefingFromRepresentativeFixture(t *testing.T) {
if pkg.Metadata.Units != "us" || pkg.Metadata.Timezone != "America/Chicago" {
t.Fatalf("metadata units/timezone = %q/%q", pkg.Metadata.Units, pkg.Metadata.Timezone)
}
if pkg.Metadata.Location == nil || pkg.Metadata.Location.ID != "home" || pkg.Metadata.Location.Name != "Brentwood" || pkg.Metadata.Location.Region != "St. Louis Metro" || pkg.Metadata.Location.Timezone != "America/Chicago" {
t.Fatalf("metadata location = %#v, want configured prompt location", pkg.Metadata.Location)
}
if len(pkg.Metadata.Sources) != 1 || pkg.Metadata.Sources[0].DataSHA256 != "abc123" {
t.Fatalf("Sources = %#v, want source hash", pkg.Metadata.Sources)
}

View File

@@ -31,6 +31,7 @@ type Metadata struct {
Units string `json:"units"`
Timezone string `json:"timezone"`
ValidPeriod timeutil.Period `json:"validPeriod"`
Location *LocationContext `json:"location,omitempty"`
SourceLocationID string `json:"sourceLocationId,omitempty"`
SourceLocation string `json:"sourceLocation,omitempty"`
Sources []SourceMetadata `json:"sources,omitempty"`
@@ -38,6 +39,13 @@ type Metadata struct {
Alerts *AlertStatus `json:"alerts,omitempty"`
}
type LocationContext struct {
ID string `json:"id,omitempty"`
Name string `json:"name,omitempty"`
Region string `json:"region,omitempty"`
Timezone string `json:"timezone,omitempty"`
}
type SourceMetadata struct {
Name string `json:"name"`
Endpoint string `json:"endpoint,omitempty"`
@@ -61,6 +69,7 @@ type BuildContext struct {
Bundle *forecast.Bundle
Units string
Timezone string
Location *LocationContext
}
func BuildMetadata(ctx BuildContext) Metadata {
@@ -76,6 +85,7 @@ func BuildMetadata(ctx BuildContext) Metadata {
Units: ctx.Units,
Timezone: ctx.Timezone,
ValidPeriod: metadata.ValidPeriod,
Location: copyLocation(ctx.Location),
SourceLocationID: sourceLocationID,
SourceLocation: sourceLocation,
Sources: sourceMetadata(ctx.Bundle),
@@ -84,6 +94,14 @@ func BuildMetadata(ctx BuildContext) Metadata {
}
}
func copyLocation(location *LocationContext) *LocationContext {
if location == nil {
return nil
}
copied := *location
return &copied
}
func Save(path string, pkg Package) error {
if err := fileutil.WriteJSONAtomic(path, pkg); err != nil {
return fmt.Errorf("save briefing package: %w", err)

View File

@@ -437,6 +437,7 @@ func TestRunGenerateDailyWritesMarkdownReport(t *testing.T) {
"generate", "daily",
"--config", configPath,
"--date", "2026-05-29",
"--tz", "UTC",
"--out", outPath,
}, &stdout, &stderr)
if err != nil {
@@ -463,6 +464,24 @@ func TestRunGenerateDailyWritesMarkdownReport(t *testing.T) {
if !strings.Contains(string(data), `data_package.v1`) || !strings.Contains(string(data), `"daily_today"`) {
t.Fatalf("data package output missing expected content:\n%s", string(data))
}
var decoded struct {
Briefing struct {
Metadata struct {
Location struct {
ID string `json:"id"`
Name string `json:"name"`
Region string `json:"region"`
Timezone string `json:"timezone"`
} `json:"location"`
} `json:"metadata"`
} `json:"briefing"`
}
if err := json.Unmarshal(data, &decoded); err != nil {
t.Fatalf("decode data package: %v", err)
}
if decoded.Briefing.Metadata.Location.ID != "home" || decoded.Briefing.Metadata.Location.Name != "Brentwood" || decoded.Briefing.Metadata.Location.Region != "St. Louis Metro" || decoded.Briefing.Metadata.Location.Timezone != "UTC" {
t.Fatalf("location = %#v, want configured location with overridden timezone", decoded.Briefing.Metadata.Location)
}
preflightMatches, err := filepath.Glob(filepath.Join(workspaceRoot, "preflight", "daily", "2026-05-29", "*.render.json"))
if err != nil {
t.Fatalf("glob preflight: %v", err)

View File

@@ -14,6 +14,7 @@ const (
type Config struct {
WeatherAPI WeatherAPIConfig `yaml:"weather_api"`
Location LocationConfig `yaml:"location"`
MissingSource MissingSourceConfig `yaml:"missing_source"`
Scriptorium ScriptoriumConfig `yaml:"scriptorium"`
Workspace WorkspaceConfig `yaml:"workspace"`
@@ -30,6 +31,12 @@ type WeatherAPIConfig struct {
Format string `yaml:"format"`
}
type LocationConfig struct {
ID string `yaml:"id"`
Name string `yaml:"name"`
Region string `yaml:"region"`
}
type MissingSourceConfig struct {
Default MissingSourcePolicy `yaml:"default"`
Sources map[string]MissingSourcePolicy `yaml:"sources"`

View File

@@ -23,6 +23,9 @@ func TestDefaults(t *testing.T) {
if cfg.WeatherAPI.Format != "json" {
t.Fatalf("Format = %q, want json", cfg.WeatherAPI.Format)
}
if cfg.Location.ID != "home" || cfg.Location.Name != "Brentwood" || cfg.Location.Region != "St. Louis Metro" {
t.Fatalf("Location = %#v, want home/Brentwood/St. Louis Metro", cfg.Location)
}
if cfg.MissingSource.Default != MissingSourceWarn {
t.Fatalf("MissingSource.Default = %q, want warn", cfg.MissingSource.Default)
}
@@ -43,6 +46,9 @@ func TestLoadExampleConfig(t *testing.T) {
if cfg.MissingSource.Sources["alerts"] != MissingSourceNone {
t.Fatalf("alerts policy = %q, want none", cfg.MissingSource.Sources["alerts"])
}
if cfg.Location.ID != "home" || cfg.Location.Name != "Brentwood" || cfg.Location.Region != "St. Louis Metro" {
t.Fatalf("Location = %#v, want example location", cfg.Location)
}
}
func TestLoadMinimalExampleConfig(t *testing.T) {
@@ -63,6 +69,9 @@ func TestLoadMinimalExampleConfig(t *testing.T) {
if cfg.Workspace.Root != "workspace" {
t.Fatalf("Workspace.Root = %q, want default workspace", cfg.Workspace.Root)
}
if cfg.Location.Name != "Brentwood" {
t.Fatalf("Location.Name = %q, want default Brentwood", cfg.Location.Name)
}
}
func TestExplicitMissingConfigReturnsError(t *testing.T) {

View File

@@ -13,6 +13,11 @@ func Defaults() Config {
Timezone: "America/Chicago",
Format: "json",
},
Location: LocationConfig{
ID: "home",
Name: "Brentwood",
Region: "St. Louis Metro",
},
MissingSource: MissingSourceConfig{
Default: MissingSourceWarn,
Sources: map[string]MissingSourcePolicy{},

View File

@@ -31,6 +31,9 @@ func TestBuildDailyDataPackage(t *testing.T) {
if pkg.Briefing.Daily == nil {
t.Fatal("Briefing.Daily = nil")
}
if pkg.Briefing.Metadata.Location == nil || pkg.Briefing.Metadata.Location.Name != "Brentwood" {
t.Fatalf("Briefing.Metadata.Location = %#v, want configured location", pkg.Briefing.Metadata.Location)
}
if pkg.RecentChanges.Items == nil || len(pkg.RecentChanges.Items) != 0 {
t.Fatalf("RecentChanges.Items = %#v, want empty slice", pkg.RecentChanges.Items)
}
@@ -155,6 +158,12 @@ func validBriefingPackage() briefing.Package {
GeneratedAt: generatedAt,
Units: "us",
Timezone: "America/Chicago",
Location: &briefing.LocationContext{
ID: "home",
Name: "Brentwood",
Region: "St. Louis Metro",
Timezone: "America/Chicago",
},
ValidPeriod: timeutil.Period{
Start: time.Date(2026, 5, 29, 5, 0, 0, 0, time.UTC),
End: time.Date(2026, 5, 30, 5, 0, 0, 0, time.UTC),

View File

@@ -122,6 +122,9 @@ func TestSaveArtifactsAndMetadataRoundTrip(t *testing.T) {
if decoded.RenderedReportPath != renderedReportPath {
t.Fatalf("RenderedReportPath = %q, want %q", decoded.RenderedReportPath, renderedReportPath)
}
if decoded.Location == nil || decoded.Location.Name != "Brentwood" || decoded.Location.Timezone != "America/Chicago" {
t.Fatalf("metadata location = %#v, want briefing location", decoded.Location)
}
if strings.Contains(string(data), "MetadataPath") || strings.Contains(string(data), "metadataPath") {
t.Fatalf("metadata JSON includes runtime-only MetadataPath:\n%s", string(data))
}
@@ -437,7 +440,13 @@ func stateBriefingPackage(resolved report.Resolved) briefing.Package {
GeneratedAt: resolved.GeneratedAt,
Units: "us",
Timezone: resolved.Timezone,
ValidPeriod: resolved.ValidPeriod,
Location: &briefing.LocationContext{
ID: "home",
Name: "Brentwood",
Region: "St. Louis Metro",
Timezone: resolved.Timezone,
},
ValidPeriod: resolved.ValidPeriod,
},
Daily: &briefing.Daily{ForecastSummaryDate: "2026-05-29"},
}

View File

@@ -21,6 +21,7 @@ type Metadata struct {
GeneratedAt time.Time `json:"generatedAt"`
Timezone string `json:"timezone"`
ValidPeriod timeutil.Period `json:"validPeriod"`
Location *briefing.LocationContext `json:"location,omitempty"`
SourceLocationID string `json:"sourceLocationId,omitempty"`
SourceLocation string `json:"sourceLocation,omitempty"`
Sources []briefing.SourceMetadata `json:"sources,omitempty"`
@@ -43,6 +44,7 @@ func BuildMetadata(resolved report.Resolved, briefingPackage briefing.Package, p
GeneratedAt: metadata.GeneratedAt,
Timezone: metadata.Timezone,
ValidPeriod: metadata.ValidPeriod,
Location: copyLocation(briefingPackage.Metadata.Location),
SourceLocationID: briefingPackage.Metadata.SourceLocationID,
SourceLocation: briefingPackage.Metadata.SourceLocation,
Sources: briefingPackage.Metadata.Sources,
@@ -53,3 +55,11 @@ func BuildMetadata(resolved report.Resolved, briefingPackage briefing.Package, p
RenderedReportPath: paths.RenderedReport,
}
}
func copyLocation(location *briefing.LocationContext) *briefing.LocationContext {
if location == nil {
return nil
}
copied := *location
return &copied
}