diff --git a/docs/internal/state.md b/docs/internal/state.md index 1870788..11747e9 100644 --- a/docs/internal/state.md +++ b/docs/internal/state.md @@ -55,13 +55,13 @@ Workspace subdirectories must be relative paths that stay under ## Managed Layout Paths are derived from the resolved report definition's artifact group, the -valid-period start date for JSON artifacts, and the RunID. +valid-period start date for dated artifacts, and the RunID. ```text / snapshots///.briefing.json snapshots///.metadata.json - data-packages///.data_package.json + data-packages///.data_package.yaml preflight///.render.json notifications///.distributor.json reports//.md diff --git a/docs/operations.md b/docs/operations.md index 8022451..f72c6d8 100644 --- a/docs/operations.md +++ b/docs/operations.md @@ -72,16 +72,16 @@ workspace/ data-packages/ daily/ YYYY-MM-DD/ - .data_package.json + .data_package.yaml three-day/ YYYY-MM-DD/ - .data_package.json + .data_package.yaml weekend/ YYYY-MM-DD/ - .data_package.json + .data_package.yaml storm/ YYYY-MM-DD/ - .data_package.json + .data_package.yaml preflight/ daily/ YYYY-MM-DD/ diff --git a/internal/adapters/scriptorium/runner_test.go b/internal/adapters/scriptorium/runner_test.go index 0791e72..e9fb0ad 100644 --- a/internal/adapters/scriptorium/runner_test.go +++ b/internal/adapters/scriptorium/runner_test.go @@ -20,7 +20,7 @@ func TestRenderConstructsCommand(t *testing.T) { result, err := runner.Render(context.Background(), RenderRequest{ PromptID: "weather.daily_report", - DataPackagePath: "/tmp/data_package.json", + DataPackagePath: "/tmp/data_package.yaml", }) if err != nil { t.Fatalf("Render() error = %v", err) @@ -31,7 +31,7 @@ func TestRenderConstructsCommand(t *testing.T) { "--config", "/etc/scriptorium.yml", "--profile", "weather", "--prompt", "weather.daily_report", - "--input", "data_package=/tmp/data_package.json", + "--input", "data_package=/tmp/data_package.yaml", "--format", "json", } if commands.name != "/usr/local/bin/scriptorium" { @@ -57,7 +57,7 @@ func TestRenderReturnsResultForNonzeroExit(t *testing.T) { result, err := runner.Render(context.Background(), RenderRequest{ PromptID: "weather.daily_report", - DataPackagePath: "/tmp/data_package.json", + DataPackagePath: "/tmp/data_package.yaml", }) if err == nil { t.Fatal("Render() error = nil, want nonzero exit error") @@ -85,7 +85,7 @@ func TestRunConstructsCommand(t *testing.T) { result, err := runner.Run(context.Background(), RunRequest{ PromptID: "weather.daily_report", - DataPackagePath: "/tmp/data_package.json", + DataPackagePath: "/tmp/data_package.yaml", OutputPath: "/tmp/daily.md", }) if err != nil { @@ -97,7 +97,7 @@ func TestRunConstructsCommand(t *testing.T) { "--config", "/etc/scriptorium.yml", "--profile", "weather", "--prompt", "weather.daily_report", - "--input", "data_package=/tmp/data_package.json", + "--input", "data_package=/tmp/data_package.yaml", "--out", "/tmp/daily.md", } if commands.name != "/usr/local/bin/scriptorium" { @@ -130,7 +130,7 @@ func TestRunReturnsResultForValidationExit(t *testing.T) { result, err := runner.Run(context.Background(), RunRequest{ PromptID: "weather.daily_report", - DataPackagePath: "/tmp/data_package.json", + DataPackagePath: "/tmp/data_package.yaml", OutputPath: "/tmp/daily.md", }) if err == nil { diff --git a/internal/app/app.go b/internal/app/app.go index ccf744d..5477edc 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -522,7 +522,19 @@ func GenerateReport(ctx context.Context, req ReportRequest) (*ReportResult, erro if err != nil { return nil, err } - dataPackage, err := promptinput.BuildWithRecentChanges(briefingPackage, recentChanges) + metadata := state.BuildMetadata(req.Resolved, briefingPackage, state.ArtifactPaths{ + Briefing: briefingPath, + ModuleSnapshot: moduleSnapshotPath, + Metadata: paths.Metadata, + DataPackage: paths.DataPackage, + Preflight: paths.Preflight, + RenderedReport: paths.RenderedReport, + }) + dataPackage, err := promptinput.Build(promptinput.BuildRequest{ + Metadata: promptMetadata(metadata), + Modules: moduleSnapshot, + RecentChanges: recentChanges, + }) if err != nil { return nil, err } @@ -530,6 +542,7 @@ func GenerateReport(ctx context.Context, req ReportRequest) (*ReportResult, erro if err != nil { return nil, err } + metadata.DataPackagePath = dataPackagePath renderer := req.Renderer if renderer == nil { @@ -554,14 +567,7 @@ func GenerateReport(ctx context.Context, req ReportRequest) (*ReportResult, erro return nil, err } } - metadata := state.BuildMetadata(req.Resolved, briefingPackage, state.ArtifactPaths{ - Briefing: briefingPath, - ModuleSnapshot: moduleSnapshotPath, - Metadata: paths.Metadata, - DataPackage: dataPackagePath, - Preflight: preflightPath, - RenderedReport: paths.RenderedReport, - }) + metadata.PreflightPath = preflightPath metadataPath, metadataErr := store.SaveMetadata(ctx, metadata) if metadataErr != nil { return nil, metadataErr @@ -894,6 +900,19 @@ func BuildModuleSnapshot(req ModuleSnapshotRequest, bundle *weatherdata.Bundle) return module.NewSnapshot(outputs) } +func promptMetadata(metadata state.Metadata) promptinput.Metadata { + return promptinput.Metadata{ + RunID: metadata.RunID, + ReportID: metadata.ReportID, + Variant: metadata.Variant, + PromptID: metadata.PromptID, + GeneratedAt: metadata.GeneratedAt, + Timezone: metadata.Timezone, + ValidPeriod: metadata.ValidPeriod, + SourceWarnings: metadata.SourceWarnings, + } +} + func buildDerivedFacts(cfg config.Config, resolved report.Resolved, collected facts.CollectedFacts) (facts.DerivedFacts, error) { dayparts := make([]forecast.DaypartDefinition, 0, len(cfg.Dayparts)) for _, daypart := range cfg.Dayparts { diff --git a/internal/app/app_test.go b/internal/app/app_test.go index 90cd1ba..1ef9aef 100644 --- a/internal/app/app_test.go +++ b/internal/app/app_test.go @@ -17,6 +17,7 @@ import ( "gitea.maximumdirect.net/eric/weatherreporter/internal/config" "gitea.maximumdirect.net/eric/weatherreporter/internal/forecast" "gitea.maximumdirect.net/eric/weatherreporter/internal/module" + "gitea.maximumdirect.net/eric/weatherreporter/internal/promptinput" "gitea.maximumdirect.net/eric/weatherreporter/internal/report" "gitea.maximumdirect.net/eric/weatherreporter/internal/state" ) @@ -206,31 +207,32 @@ func TestGenerateReportWritesReportAndPreflight(t *testing.T) { if err != nil { t.Fatalf("read data package: %v", err) } - if !strings.Contains(string(data), `"recentChanges"`) || !strings.Contains(string(data), `data_package.v1`) { + if !strings.HasSuffix(result.DataPackagePath, ".data_package.yaml") { + t.Fatalf("DataPackagePath = %q, want YAML data package path", result.DataPackagePath) + } + if !strings.Contains(string(data), "schema_version: weatherreporter.data_package.v2") || + !strings.Contains(string(data), "recent_changes:") || + !strings.Contains(string(data), "current_conditions:") || + !strings.Contains(string(data), "area_forecast_discussion:") { t.Fatalf("data package missing expected content:\n%s", string(data)) } - var savedDataPackage struct { - Report struct { - CurrentLocalDate string `json:"currentLocalDate"` - } `json:"report"` - Briefing briefing.Package `json:"briefing"` - } - if err := json.Unmarshal(data, &savedDataPackage); err != nil { + savedDataPackage, err := promptinput.LoadYAML(data) + if err != nil { t.Fatalf("decode data package: %v", err) } if savedDataPackage.Report.CurrentLocalDate != "2026-05-29" { t.Fatalf("data package currentLocalDate = %q, want 2026-05-29", savedDataPackage.Report.CurrentLocalDate) } - 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 _, ok := savedDataPackage.Briefing.Values["metadata"]; !ok { + t.Fatal("data package metadata stanza missing") } - current := savedDataPackage.Briefing.CurrentConditions - if current == nil || current.ConditionText != "Clear" || current.TemperatureF == nil || *current.TemperatureF != 75 { - t.Fatalf("data package current conditions = %#v, want current conditions", current) + current, ok := savedDataPackage.Briefing.Values["current_conditions"].(map[string]any) + if !ok || current["condition_text"] != "Clear" { + t.Fatalf("data package current conditions = %#v, want current conditions", savedDataPackage.Briefing.Values["current_conditions"]) } - if savedDataPackage.Briefing.Daily == nil || savedDataPackage.Briefing.Daily.WeatherStory == nil || savedDataPackage.Briefing.Daily.WeatherStory.Title != "Several Chances for Rain Through Monday" { - t.Fatalf("data package weather story = %#v, want weather story title", savedDataPackage.Briefing.Daily) + story, ok := savedDataPackage.Briefing.Values["weather_story"].(map[string]any) + if !ok || story["title"] != "Several Chances for Rain Through Monday" { + t.Fatalf("data package weather story = %#v, want weather story title", savedDataPackage.Briefing.Values["weather_story"]) } 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)) @@ -1012,7 +1014,7 @@ func TestGenerateStormReportWritesReport(t *testing.T) { if err != nil { t.Fatalf("read data package: %v", err) } - if !strings.Contains(string(data), `"storm"`) || !strings.Contains(string(data), `"weather.storm_report"`) { + if !strings.Contains(string(data), "id: storm") || !strings.Contains(string(data), "prompt_id: weather.storm_report") || !strings.Contains(string(data), "precip_timing:") { t.Fatalf("data package missing storm content:\n%s", string(data)) } } diff --git a/internal/cli/root_test.go b/internal/cli/root_test.go index cd5c145..ab2f0f8 100644 --- a/internal/cli/root_test.go +++ b/internal/cli/root_test.go @@ -90,12 +90,12 @@ func TestRunGenerateStormWritesMarkdownReport(t *testing.T) { if !strings.Contains(string(report), "# Daily Report") { t.Fatalf("report output missing markdown:\n%s", string(report)) } - dataPackagePath := oneArtifact(t, workspaceRoot, "data-packages", "storm", "2026-05-29", "*.data_package.json") + dataPackagePath := oneArtifact(t, workspaceRoot, "data-packages", "storm", "2026-05-29", "*.data_package.yaml") data, err := os.ReadFile(dataPackagePath) if err != nil { t.Fatalf("read managed data package: %v", err) } - if !strings.Contains(string(data), `"storm"`) || !strings.Contains(string(data), `"weather.storm_report"`) { + if !strings.Contains(string(data), "id: storm") || !strings.Contains(string(data), "prompt_id: weather.storm_report") { t.Fatalf("data package output missing storm content:\n%s", string(data)) } } @@ -126,12 +126,12 @@ func TestRunGenerateTomorrowWritesMarkdownReport(t *testing.T) { if !strings.Contains(string(report), "# Daily Report") { t.Fatalf("report output missing markdown:\n%s", string(report)) } - dataPackagePath := oneArtifact(t, workspaceRoot, "data-packages", "daily", "2026-05-30", "*.data_package.json") + dataPackagePath := oneArtifact(t, workspaceRoot, "data-packages", "daily", "2026-05-30", "*.data_package.yaml") data, err := os.ReadFile(dataPackagePath) if err != nil { t.Fatalf("read managed data package: %v", err) } - if !strings.Contains(string(data), `"daily_tomorrow"`) || !strings.Contains(string(data), `"planning"`) { + if !strings.Contains(string(data), "id: daily_tomorrow") || !strings.Contains(string(data), "tomorrow_planning:") { t.Fatalf("data package output missing tomorrow content:\n%s", string(data)) } reportMatches, err := filepath.Glob(filepath.Join(workspaceRoot, "reports", "daily", "*.md")) @@ -164,7 +164,7 @@ func TestRunEveningGeneratesTomorrowReport(t *testing.T) { if err != nil { t.Fatalf("Run() error = %v", err) } - dataPackageMatches, err := filepath.Glob(filepath.Join(workspaceRoot, "data-packages", "daily", "2026-05-30", "*.data_package.json")) + dataPackageMatches, err := filepath.Glob(filepath.Join(workspaceRoot, "data-packages", "daily", "2026-05-30", "*.data_package.yaml")) if err != nil { t.Fatalf("glob data package: %v", err) } @@ -210,7 +210,7 @@ func TestRunGenerateThreeDayWritesMarkdownReport(t *testing.T) { if !strings.Contains(string(report), "# Daily Report") { t.Fatalf("report output missing markdown:\n%s", string(report)) } - dataPackageMatches, err := filepath.Glob(filepath.Join(workspaceRoot, "data-packages", "three-day", "2026-05-29", "*.data_package.json")) + dataPackageMatches, err := filepath.Glob(filepath.Join(workspaceRoot, "data-packages", "three-day", "2026-05-29", "*.data_package.yaml")) if err != nil { t.Fatalf("glob data package: %v", err) } @@ -221,7 +221,7 @@ func TestRunGenerateThreeDayWritesMarkdownReport(t *testing.T) { if err != nil { t.Fatalf("read managed data package: %v", err) } - if !strings.Contains(string(data), `"three_day"`) || !strings.Contains(string(data), `"threeDay"`) { + if !strings.Contains(string(data), "id: three_day") || !strings.Contains(string(data), "derived_daypart_summaries:") { t.Fatalf("data package output missing 3-day content:\n%s", string(data)) } } @@ -256,7 +256,7 @@ func TestRunGenerateWeekendWritesMarkdownReport(t *testing.T) { if !strings.Contains(string(report), "# Daily Report") { t.Fatalf("report output missing markdown:\n%s", string(report)) } - dataPackageMatches, err := filepath.Glob(filepath.Join(workspaceRoot, "data-packages", "weekend", "2026-05-29", "*.data_package.json")) + dataPackageMatches, err := filepath.Glob(filepath.Join(workspaceRoot, "data-packages", "weekend", "2026-05-29", "*.data_package.yaml")) if err != nil { t.Fatalf("glob data package: %v", err) } @@ -267,7 +267,7 @@ func TestRunGenerateWeekendWritesMarkdownReport(t *testing.T) { if err != nil { t.Fatalf("read managed data package: %v", err) } - if !strings.Contains(string(data), `"weekend"`) || !strings.Contains(string(data), `"planning"`) { + if !strings.Contains(string(data), "id: weekend") || !strings.Contains(string(data), "derived_daypart_summaries:") { t.Fatalf("data package output missing weekend content:\n%s", string(data)) } } @@ -293,7 +293,7 @@ func TestRunMorningIncludesWeekendExceptSunday(t *testing.T) { if err != nil { t.Fatalf("Run() error = %v", err) } - weekendPackages, err := filepath.Glob(filepath.Join(workspaceRoot, "data-packages", "weekend", "2026-05-29", "*.data_package.json")) + weekendPackages, err := filepath.Glob(filepath.Join(workspaceRoot, "data-packages", "weekend", "2026-05-29", "*.data_package.yaml")) if err != nil { t.Fatalf("glob weekend packages: %v", err) } @@ -337,11 +337,11 @@ func TestRunMorningReportsPartialFailureAndContinues(t *testing.T) { if !strings.Contains(stderr.String(), "status=failed") || !strings.Contains(stderr.String(), "status=succeeded") { t.Fatalf("stderr missing structured report logs:\n%s", stderr.String()) } - dailyPackages, err := filepath.Glob(filepath.Join(workspaceRoot, "data-packages", "daily", "2026-05-29", "*.data_package.json")) + dailyPackages, err := filepath.Glob(filepath.Join(workspaceRoot, "data-packages", "daily", "2026-05-29", "*.data_package.yaml")) if err != nil { t.Fatalf("glob daily packages: %v", err) } - weekendPackages, err := filepath.Glob(filepath.Join(workspaceRoot, "data-packages", "weekend", "2026-05-29", "*.data_package.json")) + weekendPackages, err := filepath.Glob(filepath.Join(workspaceRoot, "data-packages", "weekend", "2026-05-29", "*.data_package.yaml")) if err != nil { t.Fatalf("glob weekend packages: %v", err) } @@ -593,11 +593,11 @@ func TestRunMorningGeneratesDailyAndThreeDayOnSunday(t *testing.T) { if err != nil { t.Fatalf("Run() error = %v", err) } - dailyPackages, err := filepath.Glob(filepath.Join(workspaceRoot, "data-packages", "daily", "2026-05-31", "*.data_package.json")) + dailyPackages, err := filepath.Glob(filepath.Join(workspaceRoot, "data-packages", "daily", "2026-05-31", "*.data_package.yaml")) if err != nil { t.Fatalf("glob daily packages: %v", err) } - threeDayPackages, err := filepath.Glob(filepath.Join(workspaceRoot, "data-packages", "three-day", "2026-05-31", "*.data_package.json")) + threeDayPackages, err := filepath.Glob(filepath.Join(workspaceRoot, "data-packages", "three-day", "2026-05-31", "*.data_package.yaml")) if err != nil { t.Fatalf("glob 3-day packages: %v", err) } @@ -638,7 +638,7 @@ func TestRunGenerateDailyWritesMarkdownReport(t *testing.T) { if !strings.Contains(string(report), "# Daily Report") { t.Fatalf("report output missing markdown:\n%s", string(report)) } - dataPackageMatches, err := filepath.Glob(filepath.Join(workspaceRoot, "data-packages", "daily", "2026-05-29", "*.data_package.json")) + dataPackageMatches, err := filepath.Glob(filepath.Join(workspaceRoot, "data-packages", "daily", "2026-05-29", "*.data_package.yaml")) if err != nil { t.Fatalf("glob data package: %v", err) } @@ -649,26 +649,15 @@ func TestRunGenerateDailyWritesMarkdownReport(t *testing.T) { if err != nil { t.Fatalf("read managed data package: %v", err) } - if !strings.Contains(string(data), `data_package.v1`) || !strings.Contains(string(data), `"daily_today"`) { + if !strings.Contains(string(data), "schema_version: weatherreporter.data_package.v2") || !strings.Contains(string(data), "id: 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) + if !strings.Contains(string(data), "location:") || + !strings.Contains(string(data), "id: home") || + !strings.Contains(string(data), "name: Brentwood") || + !strings.Contains(string(data), "region: St. Louis Metro") || + !strings.Contains(string(data), "timezone: UTC") { + t.Fatalf("data package missing configured location with overridden timezone:\n%s", string(data)) } preflightMatches, err := filepath.Glob(filepath.Join(workspaceRoot, "preflight", "daily", "2026-05-29", "*.render.json")) if err != nil { @@ -715,14 +704,14 @@ func TestRunInspectGeneratedArtifacts(t *testing.T) { if err != nil { t.Fatalf("Run(generate) error = %v", err) } - dataPackageMatches, err := filepath.Glob(filepath.Join(workspaceRoot, "data-packages", "daily", "2026-05-29", "*.data_package.json")) + dataPackageMatches, err := filepath.Glob(filepath.Join(workspaceRoot, "data-packages", "daily", "2026-05-29", "*.data_package.yaml")) if err != nil { t.Fatalf("glob data package: %v", err) } if len(dataPackageMatches) != 1 { t.Fatalf("data package files = %#v, want one", dataPackageMatches) } - runID := strings.TrimSuffix(filepath.Base(dataPackageMatches[0]), ".data_package.json") + runID := strings.TrimSuffix(filepath.Base(dataPackageMatches[0]), ".data_package.yaml") stdout.Reset() err = runner.Run(context.Background(), []string{"inspect", "reports", "--config", configPath, "--limit", "1"}, &stdout, &stderr) diff --git a/internal/promptinput/package.go b/internal/promptinput/package.go index 8927d83..84eae62 100644 --- a/internal/promptinput/package.go +++ b/internal/promptinput/package.go @@ -1,72 +1,93 @@ -// Package promptinput builds prompt data packages from briefing packages. +// Package promptinput builds prompt data packages from module snapshots. package promptinput import ( + "bytes" + "encoding/json" "fmt" "time" - "gitea.maximumdirect.net/eric/weatherreporter/internal/briefing" "gitea.maximumdirect.net/eric/weatherreporter/internal/changes" "gitea.maximumdirect.net/eric/weatherreporter/internal/fileutil" + "gitea.maximumdirect.net/eric/weatherreporter/internal/module" "gitea.maximumdirect.net/eric/weatherreporter/internal/report" "gitea.maximumdirect.net/eric/weatherreporter/internal/timeutil" "gitea.maximumdirect.net/eric/weatherreporter/internal/weatherdata" + "gopkg.in/yaml.v3" ) -const SchemaVersion = "weatherreporter.data_package.v1" +const SchemaVersion = "weatherreporter.data_package.v2" + +type BuildRequest struct { + Metadata Metadata + Modules module.Snapshot + RecentChanges []changes.Change +} + +type Metadata struct { + RunID string + ReportID report.ID + Variant string + PromptID string + GeneratedAt time.Time + Timezone string + ValidPeriod timeutil.Period + SourceWarnings []weatherdata.SourceWarning +} type Package struct { - SchemaVersion string `json:"schemaVersion"` - RunID string `json:"runId"` - Report Report `json:"report"` - Briefing briefing.Package `json:"briefing"` - RecentChanges RecentChanges `json:"recentChanges"` - SourceWarnings []weatherdata.SourceWarning `json:"sourceWarnings,omitempty"` + SchemaVersion string `json:"schemaVersion" yaml:"schema_version"` + RunID string `json:"runId" yaml:"run_id"` + Report Report `json:"report" yaml:"report"` + Briefing BriefingStanzas `json:"briefing" yaml:"briefing"` + RecentChanges RecentChanges `json:"recentChanges" yaml:"recent_changes"` + SourceWarnings []weatherdata.SourceWarning `json:"sourceWarnings,omitempty" yaml:"source_warnings,omitempty"` } type Report struct { - ID report.ID `json:"id"` - Variant string `json:"variant,omitempty"` - PromptID string `json:"promptId"` - GeneratedAt time.Time `json:"generatedAt"` - Timezone string `json:"timezone"` - CurrentLocalDate string `json:"currentLocalDate"` - ValidPeriod timeutil.Period `json:"validPeriod"` + ID report.ID `json:"id" yaml:"id"` + Variant string `json:"variant,omitempty" yaml:"variant,omitempty"` + PromptID string `json:"promptId" yaml:"prompt_id"` + GeneratedAt time.Time `json:"generatedAt" yaml:"generated_at"` + Timezone string `json:"timezone" yaml:"timezone"` + CurrentLocalDate string `json:"currentLocalDate" yaml:"current_local_date"` + ValidPeriod timeutil.Period `json:"validPeriod" yaml:"valid_period"` +} + +type BriefingStanzas struct { + Order []string `json:"-" yaml:"-"` + Values map[string]any `json:"-" yaml:"-"` } type RecentChanges struct { - Items []changes.Change `json:"items"` + Items []changes.Change `json:"items" yaml:"items"` } -func Build(briefingPackage briefing.Package) (Package, error) { - return BuildWithRecentChanges(briefingPackage, nil) -} - -func BuildWithRecentChanges(briefingPackage briefing.Package, recentChanges []changes.Change) (Package, error) { - localDate, err := currentLocalDate(briefingPackage.Metadata.GeneratedAt, briefingPackage.Metadata.Timezone) +func Build(req BuildRequest) (Package, error) { + localDate, err := currentLocalDate(req.Metadata.GeneratedAt, req.Metadata.Timezone) if err != nil { return Package{}, err } - items := make([]changes.Change, len(recentChanges)) - copy(items, recentChanges) + items := make([]changes.Change, len(req.RecentChanges)) + copy(items, req.RecentChanges) if items == nil { items = []changes.Change{} } pkg := Package{ SchemaVersion: SchemaVersion, - RunID: briefingPackage.Metadata.RunID, + RunID: req.Metadata.RunID, Report: Report{ - ID: briefingPackage.Metadata.ReportID, - Variant: briefingPackage.Metadata.Variant, - PromptID: briefingPackage.Metadata.PromptID, - GeneratedAt: briefingPackage.Metadata.GeneratedAt, - Timezone: briefingPackage.Metadata.Timezone, + ID: req.Metadata.ReportID, + Variant: req.Metadata.Variant, + PromptID: req.Metadata.PromptID, + GeneratedAt: req.Metadata.GeneratedAt, + Timezone: req.Metadata.Timezone, CurrentLocalDate: localDate, - ValidPeriod: briefingPackage.Metadata.ValidPeriod, + ValidPeriod: req.Metadata.ValidPeriod, }, - Briefing: briefingPackage, + Briefing: stanzasFromSnapshot(req.Modules), RecentChanges: RecentChanges{Items: items}, - SourceWarnings: briefingPackage.Metadata.SourceWarnings, + SourceWarnings: append([]weatherdata.SourceWarning(nil), req.Metadata.SourceWarnings...), } if err := Validate(pkg); err != nil { return Package{}, err @@ -74,6 +95,16 @@ func BuildWithRecentChanges(briefingPackage briefing.Package, recentChanges []ch return pkg, nil } +func stanzasFromSnapshot(snapshot module.Snapshot) BriefingStanzas { + values := map[string]any{} + order := make([]string, 0, len(snapshot.Outputs)) + for _, output := range snapshot.Outputs { + order = append(order, output.StanzaName) + values[output.StanzaName] = output.Value + } + return BriefingStanzas{Order: order, Values: values} +} + func currentLocalDate(generatedAt time.Time, timezone string) (string, error) { location, err := timeutil.LoadLocation(timezone) if err != nil { @@ -86,6 +117,9 @@ func Validate(pkg Package) error { if pkg.SchemaVersion == "" { return fmt.Errorf("schemaVersion is required") } + if pkg.SchemaVersion != SchemaVersion { + return fmt.Errorf("schemaVersion must be %s", SchemaVersion) + } if pkg.RunID == "" { return fmt.Errorf("runId is required") } @@ -107,33 +141,134 @@ func Validate(pkg Package) error { if !pkg.Report.ValidPeriod.IsValid() { return fmt.Errorf("report.validPeriod must be valid") } - if pkg.Briefing.Metadata.RunID == "" { - return fmt.Errorf("briefing.metadata.runId is required") + if len(pkg.Briefing.Order) == 0 { + return fmt.Errorf("briefing stanzas are required") } - if pkg.Briefing.Metadata.RunID != pkg.RunID { - return fmt.Errorf("briefing.metadata.runId must match runId") - } - if pkg.Briefing.Metadata.SchemaVersion == "" { - return fmt.Errorf("briefing.metadata.schemaVersion is required") - } - if pkg.Briefing.Metadata.PromptID != pkg.Report.PromptID { - return fmt.Errorf("briefing.metadata.promptId must match report.promptId") - } - if pkg.Briefing.Metadata.ReportID != pkg.Report.ID { - return fmt.Errorf("briefing.metadata.reportId must match report.id") - } - if pkg.Briefing.Daily == nil && pkg.Briefing.ThreeDay == nil && pkg.Briefing.Weekend == nil && pkg.Briefing.Storm == nil { - return fmt.Errorf("briefing report content is required") + for _, name := range pkg.Briefing.Order { + if name == "" { + return fmt.Errorf("briefing stanza name is required") + } + if _, ok := pkg.Briefing.Values[name]; !ok { + return fmt.Errorf("briefing stanza %q is missing", name) + } } return nil } func Save(path string, pkg Package) error { - if err := Validate(pkg); err != nil { + data, err := MarshalYAML(pkg) + if err != nil { return err } - if err := fileutil.WriteJSONAtomic(path, pkg); err != nil { + if err := fileutil.WriteFileAtomic(path, data); err != nil { return fmt.Errorf("save data package: %w", err) } return nil } + +func MarshalYAML(pkg Package) ([]byte, error) { + if err := Validate(pkg); err != nil { + return nil, err + } + data, err := yaml.Marshal(pkg) + if err != nil { + return nil, fmt.Errorf("marshal data package: %w", err) + } + return data, nil +} + +func LoadYAML(data []byte) (Package, error) { + var pkg Package + decoder := yaml.NewDecoder(bytes.NewReader(data)) + if err := decoder.Decode(&pkg); err != nil { + return Package{}, fmt.Errorf("decode data package: %w", err) + } + if err := Validate(pkg); err != nil { + return Package{}, err + } + return pkg, nil +} + +func (b BriefingStanzas) MarshalYAML() (any, error) { + node := &yaml.Node{Kind: yaml.MappingNode} + for _, name := range b.Order { + value, ok := b.Values[name] + if !ok { + continue + } + keyNode := &yaml.Node{Kind: yaml.ScalarNode, Value: name} + valueNode, err := yamlNode(value) + if err != nil { + return nil, fmt.Errorf("marshal briefing stanza %q: %w", name, err) + } + node.Content = append(node.Content, keyNode, valueNode) + } + return node, nil +} + +func (b *BriefingStanzas) UnmarshalYAML(value *yaml.Node) error { + if value.Kind != yaml.MappingNode { + return fmt.Errorf("briefing must be a mapping") + } + values := map[string]any{} + order := make([]string, 0, len(value.Content)/2) + for i := 0; i < len(value.Content); i += 2 { + name := value.Content[i].Value + var stanza any + if err := value.Content[i+1].Decode(&stanza); err != nil { + return err + } + order = append(order, name) + values[name] = stanza + } + b.Order = order + b.Values = values + return nil +} + +func (b BriefingStanzas) MarshalJSON() ([]byte, error) { + out := map[string]any{} + for _, name := range b.Order { + if value, ok := b.Values[name]; ok { + out[name] = value + } + } + return json.Marshal(out) +} + +func (b *BriefingStanzas) UnmarshalJSON(data []byte) error { + var values map[string]any + if err := json.Unmarshal(data, &values); err != nil { + return err + } + order := make([]string, 0, len(values)) + for name := range values { + order = append(order, name) + } + b.Order = order + b.Values = values + return nil +} + +func yamlNode(value any) (*yaml.Node, error) { + data, err := json.Marshal(value) + if err != nil { + return nil, err + } + var normalized any + if err := json.Unmarshal(data, &normalized); err != nil { + return nil, err + } + data, err = yaml.Marshal(normalized) + if err != nil { + return nil, err + } + var node yaml.Node + if err := yaml.Unmarshal(data, &node); err != nil { + return nil, err + } + if len(node.Content) == 0 { + return &yaml.Node{Kind: yaml.MappingNode}, nil + } + return node.Content[0], nil +} diff --git a/internal/promptinput/package_test.go b/internal/promptinput/package_test.go index 5d82c63..aaebb6f 100644 --- a/internal/promptinput/package_test.go +++ b/internal/promptinput/package_test.go @@ -1,20 +1,17 @@ package promptinput import ( - "encoding/json" "strings" "testing" "time" - "gitea.maximumdirect.net/eric/weatherreporter/internal/briefing" + "gitea.maximumdirect.net/eric/weatherreporter/internal/module" "gitea.maximumdirect.net/eric/weatherreporter/internal/report" "gitea.maximumdirect.net/eric/weatherreporter/internal/timeutil" ) func TestBuildDailyDataPackage(t *testing.T) { - briefingPackage := validBriefingPackage() - - pkg, err := Build(briefingPackage) + pkg, err := Build(validBuildRequest(t)) if err != nil { t.Fatalf("Build() error = %v", err) } @@ -23,7 +20,7 @@ func TestBuildDailyDataPackage(t *testing.T) { t.Fatalf("SchemaVersion = %q, want %q", pkg.SchemaVersion, SchemaVersion) } if pkg.RunID != "20260529T100000Z_daily_today" { - t.Fatalf("RunID = %q, want briefing run id", pkg.RunID) + t.Fatalf("RunID = %q, want metadata run id", pkg.RunID) } if pkg.Report.PromptID != "weather.daily_report" { t.Fatalf("PromptID = %q, want weather.daily_report", pkg.Report.PromptID) @@ -31,14 +28,11 @@ func TestBuildDailyDataPackage(t *testing.T) { if pkg.Report.CurrentLocalDate != "2026-05-29" { t.Fatalf("CurrentLocalDate = %q, want 2026-05-29", pkg.Report.CurrentLocalDate) } - if pkg.Briefing.Daily == nil { - t.Fatal("Briefing.Daily = nil") + if pkg.Briefing.Order[0] != "metadata" || pkg.Briefing.Order[1] != "current_conditions" || pkg.Briefing.Order[2] != "derived_daily_summary" { + t.Fatalf("Briefing.Order = %#v, want snapshot stanza order", pkg.Briefing.Order) } - 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.Briefing.CurrentConditions == nil || pkg.Briefing.CurrentConditions.ConditionText != "Partly cloudy" { - t.Fatalf("Briefing.CurrentConditions = %#v, want current conditions", pkg.Briefing.CurrentConditions) + if got := pkg.Briefing.Values["current_conditions"].(map[string]string)["condition_text"]; got != "Partly cloudy" { + t.Fatalf("current_conditions.condition_text = %q, want Partly cloudy", got) } if pkg.RecentChanges.Items == nil || len(pkg.RecentChanges.Items) != 0 { t.Fatalf("RecentChanges.Items = %#v, want empty slice", pkg.RecentChanges.Items) @@ -46,11 +40,11 @@ func TestBuildDailyDataPackage(t *testing.T) { } func TestBuildCurrentLocalDateUsesReportTimezone(t *testing.T) { - briefingPackage := validBriefingPackage() - briefingPackage.Metadata.GeneratedAt = time.Date(2026, 5, 30, 2, 30, 0, 0, time.UTC) - briefingPackage.Metadata.Timezone = "America/Chicago" + req := validBuildRequest(t) + req.Metadata.GeneratedAt = time.Date(2026, 5, 30, 2, 30, 0, 0, time.UTC) + req.Metadata.Timezone = "America/Chicago" - pkg, err := Build(briefingPackage) + pkg, err := Build(req) if err != nil { t.Fatalf("Build() error = %v", err) } @@ -61,10 +55,10 @@ func TestBuildCurrentLocalDateUsesReportTimezone(t *testing.T) { } func TestBuildRejectsInvalidReportTimezone(t *testing.T) { - briefingPackage := validBriefingPackage() - briefingPackage.Metadata.Timezone = "Not/AZone" + req := validBuildRequest(t) + req.Metadata.Timezone = "Not/AZone" - _, err := Build(briefingPackage) + _, err := Build(req) if err == nil { t.Fatal("Build() error = nil, want invalid timezone error") } @@ -74,7 +68,7 @@ func TestBuildRejectsInvalidReportTimezone(t *testing.T) { } func TestValidateRequiresFields(t *testing.T) { - pkg, err := Build(validBriefingPackage()) + pkg, err := Build(validBuildRequest(t)) if err != nil { t.Fatalf("Build() error = %v", err) } @@ -90,7 +84,7 @@ func TestValidateRequiresFields(t *testing.T) { } func TestValidateRequiresCurrentLocalDate(t *testing.T) { - pkg, err := Build(validBriefingPackage()) + pkg, err := Build(validBuildRequest(t)) if err != nil { t.Fatalf("Build() error = %v", err) } @@ -105,17 +99,17 @@ func TestValidateRequiresCurrentLocalDate(t *testing.T) { } } -func TestBuildThreeDayDataPackage(t *testing.T) { - briefingPackage := validBriefingPackage() - briefingPackage.Metadata.RunID = "20260529T100000Z_three_day" - briefingPackage.Metadata.ReportID = report.ThreeDay - briefingPackage.Metadata.PromptID = "weather.three_day_outlook" - briefingPackage.Daily = nil - briefingPackage.ThreeDay = &briefing.ThreeDay{ - Days: []briefing.OutlookDay{{Date: "2026-05-29"}}, - } +func TestBuildUsesNamedSnapshotStanzas(t *testing.T) { + req := validBuildRequest(t) + req.Metadata.RunID = "20260529T100000Z_three_day" + req.Metadata.ReportID = report.ThreeDay + req.Metadata.PromptID = "weather.three_day_outlook" + req.Modules = snapshotWithOutputs(t, + module.Output{ID: module.Metadata, StanzaName: "metadata", Value: map[string]string{"run_id": req.Metadata.RunID}}, + module.Output{ID: module.ForecastDelta, StanzaName: "three_day", Value: map[string]any{"days": []string{"2026-05-29"}}}, + ) - pkg, err := Build(briefingPackage) + pkg, err := Build(req) if err != nil { t.Fatalf("Build() error = %v", err) } @@ -123,107 +117,88 @@ func TestBuildThreeDayDataPackage(t *testing.T) { if pkg.Report.ID != report.ThreeDay { t.Fatalf("Report.ID = %q, want three_day", pkg.Report.ID) } - if pkg.Briefing.ThreeDay == nil { - t.Fatal("Briefing.ThreeDay = nil") + if _, ok := pkg.Briefing.Values["three_day"]; !ok { + t.Fatal("Briefing.Values[three_day] missing") } } -func TestBuildWeekendDataPackage(t *testing.T) { - briefingPackage := validBriefingPackage() - briefingPackage.Metadata.RunID = "20260529T100000Z_weekend" - briefingPackage.Metadata.ReportID = report.Weekend - briefingPackage.Metadata.PromptID = "weather.weekend_outlook" - briefingPackage.Daily = nil - briefingPackage.Weekend = &briefing.Weekend{ - Days: []briefing.OutlookDay{{Date: "2026-05-30"}}, - } - - pkg, err := Build(briefingPackage) +func TestMarshalYAMLIsDeterministicAndUsesNamedStanzas(t *testing.T) { + pkg, err := Build(validBuildRequest(t)) if err != nil { t.Fatalf("Build() error = %v", err) } - if pkg.Report.ID != report.Weekend { - t.Fatalf("Report.ID = %q, want weekend", pkg.Report.ID) - } - if pkg.Briefing.Weekend == nil { - t.Fatal("Briefing.Weekend = nil") - } -} - -func TestBuildStormDataPackage(t *testing.T) { - briefingPackage := validBriefingPackage() - briefingPackage.Metadata.RunID = "20260529T100000Z_storm" - briefingPackage.Metadata.ReportID = report.Storm - briefingPackage.Metadata.PromptID = "weather.storm_report" - briefingPackage.Daily = nil - briefingPackage.Storm = &briefing.Storm{ - TimingWindow: briefingPackage.Metadata.ValidPeriod, - Hazards: []string{"Thunderstorms"}, - } - - pkg, err := Build(briefingPackage) - if err != nil { - t.Fatalf("Build() error = %v", err) - } - - if pkg.Report.ID != report.Storm { - t.Fatalf("Report.ID = %q, want storm", pkg.Report.ID) - } - if pkg.Report.PromptID != "weather.storm_report" { - t.Fatalf("PromptID = %q, want weather.storm_report", pkg.Report.PromptID) - } - if pkg.Briefing.Storm == nil { - t.Fatal("Briefing.Storm = nil") - } -} - -func TestMarshalDeterministic(t *testing.T) { - pkg, err := Build(validBriefingPackage()) - if err != nil { - t.Fatalf("Build() error = %v", err) - } - - first, err := json.MarshalIndent(pkg, "", " ") + first, err := MarshalYAML(pkg) if err != nil { t.Fatalf("first marshal: %v", err) } - second, err := json.MarshalIndent(pkg, "", " ") + second, err := MarshalYAML(pkg) if err != nil { t.Fatalf("second marshal: %v", err) } if string(first) != string(second) { - t.Fatalf("JSON output changed between marshals:\n%s\n---\n%s", string(first), string(second)) + t.Fatalf("YAML output changed between marshals:\n%s\n---\n%s", string(first), string(second)) + } + if !strings.Contains(string(first), "schema_version: weatherreporter.data_package.v2") || + !strings.Contains(string(first), "briefing:\n") || + !strings.Contains(string(first), " current_conditions:\n") || + !strings.Contains(string(first), " condition_text: Partly cloudy") { + t.Fatalf("YAML output missing expected named stanzas:\n%s", string(first)) } } -func validBriefingPackage() briefing.Package { +func TestLoadYAMLRoundTrip(t *testing.T) { + pkg, err := Build(validBuildRequest(t)) + if err != nil { + t.Fatalf("Build() error = %v", err) + } + data, err := MarshalYAML(pkg) + if err != nil { + t.Fatalf("MarshalYAML() error = %v", err) + } + + loaded, err := LoadYAML(data) + if err != nil { + t.Fatalf("LoadYAML() error = %v", err) + } + + if loaded.SchemaVersion != SchemaVersion || loaded.RunID != pkg.RunID { + t.Fatalf("loaded package = %#v, want schema and run id", loaded) + } + if loaded.Briefing.Order[1] != "current_conditions" { + t.Fatalf("loaded briefing order = %#v, want current_conditions second", loaded.Briefing.Order) + } +} + +func validBuildRequest(t *testing.T) BuildRequest { + t.Helper() generatedAt := time.Date(2026, 5, 29, 10, 0, 0, 0, time.UTC) - return briefing.Package{ - Metadata: briefing.Metadata{ - SchemaVersion: briefing.SchemaVersion, - RunID: "20260529T100000Z_daily_today", - ReportID: report.DailyToday, - PromptID: "weather.daily_report", - GeneratedAt: generatedAt, - Units: "us", - Timezone: "America/Chicago", - Location: &briefing.LocationContext{ - ID: "home", - Name: "Brentwood", - Region: "St. Louis Metro", - Timezone: "America/Chicago", - }, + return BuildRequest{ + Metadata: Metadata{ + RunID: "20260529T100000Z_daily_today", + ReportID: report.DailyToday, + Variant: "today", + PromptID: "weather.daily_report", + GeneratedAt: generatedAt, + 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), }, }, - CurrentConditions: &briefing.CurrentConditionsContext{ - ConditionText: "Partly cloudy", - }, - Daily: &briefing.Daily{ - ForecastSummaryDate: "2026-05-29", - }, + Modules: snapshotWithOutputs(t, + module.Output{ID: module.Metadata, StanzaName: "metadata", Value: map[string]string{"run_id": "20260529T100000Z_daily_today"}}, + module.Output{ID: module.CurrentConditions, StanzaName: "current_conditions", Value: map[string]string{"condition_text": "Partly cloudy"}}, + module.Output{ID: module.DerivedDailySummary, StanzaName: "derived_daily_summary", Value: map[string]string{"date": "2026-05-29"}}, + ), } } + +func snapshotWithOutputs(t *testing.T, outputs ...module.Output) module.Snapshot { + t.Helper() + snapshot, err := module.NewSnapshot(outputs) + if err != nil { + t.Fatalf("NewSnapshot() error = %v", err) + } + return snapshot +} diff --git a/internal/state/filesystem.go b/internal/state/filesystem.go index e4a8be4..fc1be21 100644 --- a/internal/state/filesystem.go +++ b/internal/state/filesystem.go @@ -95,7 +95,7 @@ func (s *FilesystemStore) Paths(resolved report.Resolved) (ArtifactPaths, error) Briefing: s.join(s.snapshotsDir, group, validDate, filenameBase+".briefing.json"), ModuleSnapshot: s.join(s.snapshotsDir, group, validDate, filenameBase+".modules.json"), Metadata: s.join(s.snapshotsDir, group, validDate, filenameBase+".metadata.json"), - DataPackage: s.join(s.dataPackagesDir, group, validDate, filenameBase+".data_package.json"), + DataPackage: s.join(s.dataPackagesDir, group, validDate, filenameBase+".data_package.yaml"), Preflight: s.join(s.preflightDir, group, validDate, filenameBase+".render.json"), Notification: s.join(s.notificationsDir, group, validDate, filenameBase+".distributor.json"), RenderedReport: s.join(s.reportsDir, group, filenameBase+".md"), @@ -132,10 +132,7 @@ func (s *FilesystemStore) SaveDataPackage(_ context.Context, resolved report.Res if err != nil { return "", err } - if err := promptinput.Validate(pkg); err != nil { - return "", err - } - if err := fileutil.WriteJSONAtomic(paths.DataPackage, pkg); err != nil { + if err := promptinput.Save(paths.DataPackage, pkg); err != nil { return "", err } return paths.DataPackage, nil @@ -321,8 +318,12 @@ func (s *FilesystemStore) LoadDataPackage(_ context.Context, path string) (promp if path == "" { return promptinput.Package{}, fmt.Errorf("data package path is required") } - var pkg promptinput.Package - if err := readJSON(path, &pkg); err != nil { + data, err := os.ReadFile(path) + if err != nil { + return promptinput.Package{}, fmt.Errorf("read %q: %w", path, err) + } + pkg, err := promptinput.LoadYAML(data) + if err != nil { return promptinput.Package{}, err } return pkg, nil diff --git a/internal/state/filesystem_test.go b/internal/state/filesystem_test.go index 249aeaa..1d9a82a 100644 --- a/internal/state/filesystem_test.go +++ b/internal/state/filesystem_test.go @@ -30,7 +30,7 @@ func TestPathsUseRunIDAndWorkspace(t *testing.T) { filepath.Join("snapshots", "daily", "2026-05-29", "20260529T100000.000000000Z_daily_today.briefing.json"), filepath.Join("snapshots", "daily", "2026-05-29", "20260529T100000.000000000Z_daily_today.modules.json"), filepath.Join("snapshots", "daily", "2026-05-29", "20260529T100000.000000000Z_daily_today.metadata.json"), - filepath.Join("data-packages", "daily", "2026-05-29", "20260529T100000.000000000Z_daily_today.data_package.json"), + filepath.Join("data-packages", "daily", "2026-05-29", "20260529T100000.000000000Z_daily_today.data_package.yaml"), filepath.Join("preflight", "daily", "2026-05-29", "20260529T100000.000000000Z_daily_today.render.json"), filepath.Join("notifications", "daily", "2026-05-29", "20260529T100000.000000000Z_daily_today.distributor.json"), filepath.Join("reports", "daily", "20260529T100000.000000000Z_daily_today.md"), @@ -45,7 +45,22 @@ func TestSaveArtifactsAndMetadataRoundTrip(t *testing.T) { store := newTestStore(t) resolved := resolveDailyAt(t, "2026-05-29T05:00:00-05:00") briefingPackage := stateBriefingPackage(resolved) - dataPackage, err := promptinput.Build(briefingPackage) + snapshot, err := module.NewSnapshot([]module.Output{{ID: module.Metadata, StanzaName: "metadata", Value: map[string]string{"run_id": resolved.Metadata().RunID}}}) + if err != nil { + t.Fatalf("NewSnapshot() error = %v", err) + } + dataPackage, err := promptinput.Build(promptinput.BuildRequest{ + Metadata: promptinput.Metadata{ + RunID: resolved.Metadata().RunID, + ReportID: resolved.Definition.ID, + Variant: briefingPackage.Metadata.Variant, + PromptID: resolved.Definition.PromptID, + GeneratedAt: resolved.GeneratedAt, + Timezone: resolved.Timezone, + ValidPeriod: resolved.ValidPeriod, + }, + Modules: snapshot, + }) if err != nil { t.Fatalf("Build() error = %v", err) } @@ -58,10 +73,6 @@ func TestSaveArtifactsAndMetadataRoundTrip(t *testing.T) { if err != nil { t.Fatalf("SaveDataPackage() error = %v", err) } - snapshot, err := module.NewSnapshot([]module.Output{{ID: module.Metadata, StanzaName: "metadata", Value: map[string]string{"run_id": resolved.Metadata().RunID}}}) - if err != nil { - t.Fatalf("NewSnapshot() error = %v", err) - } moduleSnapshotPath, err := store.SaveModuleSnapshot(context.Background(), resolved, snapshot) if err != nil { t.Fatalf("SaveModuleSnapshot() error = %v", err) @@ -152,6 +163,13 @@ func TestSaveArtifactsAndMetadataRoundTrip(t *testing.T) { if loadedSnapshot.SchemaVersion != module.SnapshotSchemaVersion || len(loadedSnapshot.Outputs) != 1 { t.Fatalf("loaded module snapshot = %#v, want one metadata output", loadedSnapshot) } + loadedDataPackage, err := store.LoadDataPackage(context.Background(), dataPackagePath) + if err != nil { + t.Fatalf("LoadDataPackage() error = %v", err) + } + if loadedDataPackage.SchemaVersion != promptinput.SchemaVersion || loadedDataPackage.Briefing.Order[0] != "metadata" { + t.Fatalf("loaded data package = %#v, want YAML package with metadata stanza", loadedDataPackage) + } var decoded Metadata data, err := os.ReadFile(metadataPath) if err != nil { diff --git a/internal/weatherdata/bundle.go b/internal/weatherdata/bundle.go index 1352ff0..3dcbd09 100644 --- a/internal/weatherdata/bundle.go +++ b/internal/weatherdata/bundle.go @@ -33,12 +33,12 @@ type Source struct { } type SourceWarning struct { - Source string `json:"source"` - Code string `json:"code"` - Severity string `json:"severity"` - Message string `json:"message"` - Endpoint string `json:"endpoint,omitempty"` - CompletenessImpact string `json:"completenessImpact,omitempty"` + Source string `json:"source" yaml:"source"` + Code string `json:"code" yaml:"code"` + Severity string `json:"severity" yaml:"severity"` + Message string `json:"message" yaml:"message"` + Endpoint string `json:"endpoint,omitempty" yaml:"endpoint,omitempty"` + CompletenessImpact string `json:"completenessImpact,omitempty" yaml:"completeness_impact,omitempty"` } type Observation struct {