Write prompt data packages as YAML

This commit is contained in:
2026-06-09 21:08:17 +00:00
parent 2483c2362d
commit 0b516d9762
11 changed files with 394 additions and 255 deletions

View File

@@ -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
<workspace.root>/
snapshots/<artifact_group>/<YYYY-MM-DD>/<run_id>.briefing.json
snapshots/<artifact_group>/<YYYY-MM-DD>/<run_id>.metadata.json
data-packages/<artifact_group>/<YYYY-MM-DD>/<run_id>.data_package.json
data-packages/<artifact_group>/<YYYY-MM-DD>/<run_id>.data_package.yaml
preflight/<artifact_group>/<YYYY-MM-DD>/<run_id>.render.json
notifications/<artifact_group>/<YYYY-MM-DD>/<run_id>.distributor.json
reports/<artifact_group>/<run_id>.md

View File

@@ -72,16 +72,16 @@ workspace/
data-packages/
daily/
YYYY-MM-DD/
<run_id>.data_package.json
<run_id>.data_package.yaml
three-day/
YYYY-MM-DD/
<run_id>.data_package.json
<run_id>.data_package.yaml
weekend/
YYYY-MM-DD/
<run_id>.data_package.json
<run_id>.data_package.yaml
storm/
YYYY-MM-DD/
<run_id>.data_package.json
<run_id>.data_package.yaml
preflight/
daily/
YYYY-MM-DD/

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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