diff --git a/docs/integrations/promptkit.md b/docs/integrations/promptkit.md index e413899..13c820f 100644 --- a/docs/integrations/promptkit.md +++ b/docs/integrations/promptkit.md @@ -3,7 +3,7 @@ Weatherreporter uses Promptkit for all generated-text reports. The four logical prompts are `weather.daily_generated_text`, `weather.today_generated_text`, `weather.tomorrow_generated_text`, and `weather.hourly_generated_text`, each at version -`1.0.0`. Their prompt assets and generated-text JSON Schemas are embedded by +`1.0.1`. Their prompt assets and generated-text JSON Schemas are embedded by `internal/promptassets`. Before collection, Weatherreporter inspects the exact prompt version, requires one required @@ -17,6 +17,10 @@ JSON that Weatherreporter validates before rendering its own Markdown template. execution receipts are project-owned, safe provenance records. Content-rich diagnostics are opt-in through `--llm-debug-dir`; see [operations](../operations.md) for retention and permissions. +The generated-text schemas require `summary`, `forecast_discussion`, and +`precipitation_timing`, and reject additional properties. Prompts return an empty string for +`precipitation_timing` when the deterministic package contains no precipitation windows. + Prompt/profile configuration is owned by the [configuration reference](../config.md). Adapter construction and mapping are documented in the [Promptkit adapter internals](../internal/promptkit-adapter.md). Durable metadata compatibility is described in [state internals](../internal/state.md). diff --git a/docs/internal/generatedtext.md b/docs/internal/generatedtext.md index cef876f..6df70b8 100644 --- a/docs/internal/generatedtext.md +++ b/docs/internal/generatedtext.md @@ -17,8 +17,9 @@ value and canonical normalized JSON, loads its canonical schema through Daily, Today, and Tomorrow use a day-style value with required trimmed summary and one or more nonblank discussion paragraphs. Hourly requires trimmed summary -and a single trimmed discussion string. Each form permits optional trimmed -precipitation-timing and confidence prose. Typed decoding rejects unknown JSON +and a single trimmed discussion string. Every form also requires the +`precipitation_timing` field; an empty string means there is no supported timing +prose to render. Typed decoding rejects missing required fields and unknown JSON fields; no general-purpose JSON Schema engine is used at runtime. ## Render contexts diff --git a/docs/internal/report-registry.md b/docs/internal/report-registry.md index 1e8505e..bae9540 100644 --- a/docs/internal/report-registry.md +++ b/docs/internal/report-registry.md @@ -18,10 +18,10 @@ period and run metadata for one invocation. | Report ID | Prompt version | Period policy | Comparison | Registry batch flag | Output copy | | --- | --- | --- | --- | --- | --- | -| `daily` | `1.0.0` | Explicit local civil day | Same valid date | Dynamic Daily inclusion is app-owned | `daily.md` | -| `today` | `1.0.0` | Selected or current local civil day | Same valid date | Morning | `today.md` | -| `tomorrow` | `1.0.0` | Next local civil day | Same valid date | Evening | `tomorrow.md` | -| `hourly` | `1.0.0` | Rolling six-hour interval | Rolling window | — | `hourly.md` | +| `daily` | `1.0.1` | Explicit local civil day | Same valid date | Dynamic Daily inclusion is app-owned | `daily.md` | +| `today` | `1.0.1` | Selected or current local civil day | Same valid date | Morning | `today.md` | +| `tomorrow` | `1.0.1` | Next local civil day | Same valid date | Evening | `tomorrow.md` | +| `hourly` | `1.0.1` | Rolling six-hour interval | Rolling window | — | `hourly.md` | Each report pairs its ID and prompt version with matching template and schema IDs. Exact template fields and schema assets belong to [report templates](../templates.md) diff --git a/docs/templates.md b/docs/templates.md index 3d8c50d..48b53aa 100644 --- a/docs/templates.md +++ b/docs/templates.md @@ -128,8 +128,7 @@ It is not a source for deterministic weather facts. | --- | --- | --- | --- | | `.GeneratedText.Summary` | `string` | `string` | Required. | | `.GeneratedText.ForecastDiscussion` | `string` | `[]string` | Required; range over the day-style paragraph slice. | -| `.GeneratedText.PrecipitationTiming` | `string` | `string` | Optional prose used by the precipitation partial when deterministic windows exist. | -| `.GeneratedText.Confidence` | `string` | `string` | Optional validated prose; the current templates do not render it. | +| `.GeneratedText.PrecipitationTiming` | `string` | `string` | Required field; an empty string represents no supported prose. The precipitation partial uses nonempty prose only when deterministic windows exist. | The JSON schema rejects unknown properties and defines the required fields, but the schema body and validation behavior are documented in [Generated Text diff --git a/internal/adapters/promptkit/adapter_test.go b/internal/adapters/promptkit/adapter_test.go index 521ef0f..b84634c 100644 --- a/internal/adapters/promptkit/adapter_test.go +++ b/internal/adapters/promptkit/adapter_test.go @@ -68,11 +68,11 @@ func (client *fakeClient) request() promptkit.GenerateRequest { func TestInspectPromptAndProfile(t *testing.T) { adapter := newTestAdapter(t, &fakeClient{}) - inspection, err := adapter.InspectPrompt(context.Background(), "weather.daily_generated_text", "1.0.0") + inspection, err := adapter.InspectPrompt(context.Background(), "weather.daily_generated_text", "1.0.1") if err != nil { t.Fatalf("InspectPrompt() error = %v", err) } - if inspection.PromptID != "weather.daily_generated_text" || inspection.PromptVersion != "1.0.0" || inspection.DefaultProfileID != "gemini-flash-latest" { + if inspection.PromptID != "weather.daily_generated_text" || inspection.PromptVersion != "1.0.1" || inspection.DefaultProfileID != "gemini-flash-latest" { t.Fatalf("inspection = %#v", inspection) } if len(inspection.Inputs) != 1 || inspection.Inputs[0].Name != "data_package" || !inspection.Inputs[0].Required || inspection.Inputs[0].ContentType != "application/yaml" { @@ -369,7 +369,7 @@ func testProfileDirectory(t *testing.T, profile string) string { func testExecuteRequest() promptexec.ExecuteRequest { return promptexec.ExecuteRequest{ PromptID: "weather.daily_generated_text", - PromptVersion: "1.0.0", + PromptVersion: "1.0.1", ProfileID: "test-profile", DataPackage: []byte("report:\n id: daily\nbriefing: {}\n"), DataPackagePath: "data-packages/daily/data_package.yaml", @@ -378,7 +378,7 @@ func testExecuteRequest() promptexec.ExecuteRequest { func validResponse() *promptkit.GenerateResponse { return &promptkit.GenerateResponse{ - Content: `{"summary":"A quiet day is expected.","forecast_discussion":["High pressure keeps conditions settled."],"confidence":"High."}`, + Content: `{"summary":"A quiet day is expected.","forecast_discussion":["High pressure keeps conditions settled."],"precipitation_timing":""}`, Usage: promptkit.TokenUsage{PromptTokens: 12, CompletionTokens: 8, TotalTokens: 20}, } } diff --git a/internal/app/prompt_artifact_paths_test.go b/internal/app/prompt_artifact_paths_test.go index 3c63208..52ad9ca 100644 --- a/internal/app/prompt_artifact_paths_test.go +++ b/internal/app/prompt_artifact_paths_test.go @@ -119,7 +119,7 @@ func (e artifactPathExecutor) Execute(_ context.Context, req promptexec.ExecuteR PromptHash: "prompt-hash", RenderedPromptHash: "rendered-hash", ProfileID: req.ProfileID, BackendID: "test", ModelName: "test-model", GeneratedHash: "generated-hash", StartedAt: now, EndedAt: now, DataPackagePath: req.DataPackagePath, - RawOutput: []byte(`{"summary":"Showers are possible during the selected day.","forecast_discussion":["A front will keep rain chances in the forecast."],"precipitation_timing":"Rain is most likely during the afternoon.","confidence":"Medium"}`), + RawOutput: []byte(`{"summary":"Showers are possible during the selected day.","forecast_discussion":["A front will keep rain chances in the forecast."],"precipitation_timing":"Rain is most likely during the afternoon."}`), Validation: promptexec.NewValidation(validation, "json_schema", "daily.generated_text.schema.json", nil), }, nil } diff --git a/internal/app/single_report_workflow_test.go b/internal/app/single_report_workflow_test.go index c9d58e0..9737216 100644 --- a/internal/app/single_report_workflow_test.go +++ b/internal/app/single_report_workflow_test.go @@ -643,7 +643,7 @@ func workflowTime(value string) time.Time { } func validHourlyWorkflowJSON() string { - return `{"summary":"Storm chances increase through late morning.","forecast_discussion":"A front will keep the region unsettled.","precipitation_timing":"A cold front is moving into the region.","confidence":"Medium"}` + return `{"summary":"Storm chances increase through late morning.","forecast_discussion":"A front will keep the region unsettled.","precipitation_timing":"A cold front is moving into the region."}` } func validTomorrowWorkflowJSON() string { @@ -655,5 +655,5 @@ func validTodayWorkflowJSON() string { } func validDailyWorkflowJSON() string { - return `{"summary":"Showers are possible during the selected day.","forecast_discussion":["A front will keep rain chances in the forecast.","Temperatures stay seasonable by afternoon."],"precipitation_timing":"Rain is most likely during the afternoon.","confidence":"Medium"}` + return `{"summary":"Showers are possible during the selected day.","forecast_discussion":["A front will keep rain chances in the forecast.","Temperatures stay seasonable by afternoon."],"precipitation_timing":"Rain is most likely during the afternoon."}` } diff --git a/internal/generatedtext/catalog_test.go b/internal/generatedtext/catalog_test.go index d73b70b..61d0078 100644 --- a/internal/generatedtext/catalog_test.go +++ b/internal/generatedtext/catalog_test.go @@ -147,7 +147,8 @@ func TestCatalogValidationDispatchSupportsKnownSchemas(t *testing.T) { } hourly, normalized, err := hourlyHandler.Validate([]byte(`{ "summary": " Storm chances increase. ", - "forecast_discussion": " A front will keep the region unsettled. " + "forecast_discussion": " A front will keep the region unsettled. ", + "precipitation_timing": "" }`)) if err != nil { t.Fatalf("Validate(hourly) error = %v", err) @@ -165,7 +166,8 @@ func TestCatalogValidationDispatchSupportsKnownSchemas(t *testing.T) { } tomorrow, normalized, err := tomorrowHandler.Validate([]byte(`{ "summary": " Storms become more likely tomorrow. ", - "forecast_discussion": [" A front will keep showers in the forecast. ", ""] + "forecast_discussion": [" A front will keep showers in the forecast. ", ""], + "precipitation_timing": "" }`)) if err != nil { t.Fatalf("Validate(tomorrow) error = %v", err) @@ -187,7 +189,8 @@ func TestCatalogValidationDispatchSupportsKnownSchemas(t *testing.T) { } today, normalized, err := todayHandler.Validate([]byte(`{ "summary": " Showers are likely today. ", - "forecast_discussion": [" A front will keep rain chances elevated. ", ""] + "forecast_discussion": [" A front will keep rain chances elevated. ", ""], + "precipitation_timing": "" }`)) if err != nil { t.Fatalf("Validate(today) error = %v", err) @@ -209,7 +212,8 @@ func TestCatalogValidationDispatchSupportsKnownSchemas(t *testing.T) { } daily, normalized, err := dailyHandler.Validate([]byte(`{ "summary": " Showers are possible during the selected day. ", - "forecast_discussion": [" A front will keep rain chances in the forecast. ", ""] + "forecast_discussion": [" A front will keep rain chances in the forecast. ", ""], + "precipitation_timing": "" }`)) if err != nil { t.Fatalf("Validate(daily) error = %v", err) diff --git a/internal/generatedtext/daily.go b/internal/generatedtext/daily.go index f1323e4..7635fba 100644 --- a/internal/generatedtext/daily.go +++ b/internal/generatedtext/daily.go @@ -3,8 +3,7 @@ package generatedtext type Daily struct { Summary string `json:"summary"` ForecastDiscussion []string `json:"forecast_discussion"` - PrecipitationTiming string `json:"precipitation_timing,omitempty"` - Confidence string `json:"confidence,omitempty"` + PrecipitationTiming string `json:"precipitation_timing"` } func ValidateDaily(data []byte) (Daily, []byte, error) { @@ -16,7 +15,6 @@ func (d *Daily) dayStyleFields() dayStyleFields { Summary: d.Summary, ForecastDiscussion: d.ForecastDiscussion, PrecipitationTiming: d.PrecipitationTiming, - Confidence: d.Confidence, } } @@ -24,5 +22,4 @@ func (d *Daily) setDayStyleFields(fields dayStyleFields) { d.Summary = fields.Summary d.ForecastDiscussion = fields.ForecastDiscussion d.PrecipitationTiming = fields.PrecipitationTiming - d.Confidence = fields.Confidence } diff --git a/internal/generatedtext/daily_test.go b/internal/generatedtext/daily_test.go index d7194ce..3bacb23 100644 --- a/internal/generatedtext/daily_test.go +++ b/internal/generatedtext/daily_test.go @@ -13,8 +13,7 @@ func TestValidateDailyNormalizesJSON(t *testing.T) { "", " Temperatures stay seasonable by afternoon. " ], - "precipitation_timing": " Rain is most likely during the afternoon. ", - "confidence": " Medium " + "precipitation_timing": " Rain is most likely during the afternoon. " }`)) if err != nil { t.Fatalf("ValidateDaily() error = %v", err) @@ -28,23 +27,22 @@ func TestValidateDailyNormalizesJSON(t *testing.T) { if value.PrecipitationTiming != "Rain is most likely during the afternoon." { t.Fatalf("PrecipitationTiming = %q, want trimmed precipitation timing", value.PrecipitationTiming) } - want := `{"summary":"Showers are possible during the selected day.","forecast_discussion":["A front will keep rain chances in the forecast.","Temperatures stay seasonable by afternoon."],"precipitation_timing":"Rain is most likely during the afternoon.","confidence":"Medium"}` + want := `{"summary":"Showers are possible during the selected day.","forecast_discussion":["A front will keep rain chances in the forecast.","Temperatures stay seasonable by afternoon."],"precipitation_timing":"Rain is most likely during the afternoon."}` if string(normalized) != want { t.Fatalf("normalized = %s, want %s", normalized, want) } } -func TestValidateDailyOmitsEmptyOptionalFields(t *testing.T) { +func TestValidateDailyPreservesRequiredEmptyPrecipitationTiming(t *testing.T) { _, normalized, err := ValidateDaily([]byte(`{ "summary": "Showers are possible during the selected day.", "forecast_discussion": ["A front will keep rain chances in the forecast."], - "precipitation_timing": " ", - "confidence": " " + "precipitation_timing": " " }`)) if err != nil { t.Fatalf("ValidateDaily() error = %v", err) } - want := `{"summary":"Showers are possible during the selected day.","forecast_discussion":["A front will keep rain chances in the forecast."]}` + want := `{"summary":"Showers are possible during the selected day.","forecast_discussion":["A front will keep rain chances in the forecast."],"precipitation_timing":""}` if string(normalized) != want { t.Fatalf("normalized = %s, want %s", normalized, want) } diff --git a/internal/generatedtext/day_style.go b/internal/generatedtext/day_style.go index 48076e6..cd6fd2f 100644 --- a/internal/generatedtext/day_style.go +++ b/internal/generatedtext/day_style.go @@ -9,7 +9,6 @@ type dayStyleFields struct { Summary string ForecastDiscussion []string PrecipitationTiming string - Confidence string } type dayStyleGeneratedText interface { @@ -31,7 +30,6 @@ func validateDayStyleGeneratedText[T any, PT interface { fields := pointer.dayStyleFields() fields.Summary = strings.TrimSpace(fields.Summary) fields.PrecipitationTiming = strings.TrimSpace(fields.PrecipitationTiming) - fields.Confidence = strings.TrimSpace(fields.Confidence) fields.ForecastDiscussion = trimNonEmpty(fields.ForecastDiscussion) if fields.Summary == "" { var zero T @@ -41,6 +39,10 @@ func validateDayStyleGeneratedText[T any, PT interface { var zero T return zero, nil, fmt.Errorf("%s generated text forecast discussion is required", name) } + if err := requireGeneratedTextStringField(data, name, "precipitation_timing"); err != nil { + var zero T + return zero, nil, err + } pointer.setDayStyleFields(fields) normalized, err := normalizeGeneratedText(value, name) diff --git a/internal/generatedtext/day_style_test.go b/internal/generatedtext/day_style_test.go index b337594..6ec073f 100644 --- a/internal/generatedtext/day_style_test.go +++ b/internal/generatedtext/day_style_test.go @@ -60,8 +60,7 @@ func TestValidateDayStyleGeneratedTextSharedBehavior(t *testing.T) { "", " Second paragraph. " ], - "precipitation_timing": " Afternoon. ", - "confidence": " Medium " + "precipitation_timing": " Afternoon. " }`)) if err != nil { t.Fatalf("validate() error = %v", err) @@ -76,31 +75,35 @@ func TestValidateDayStyleGeneratedTextSharedBehavior(t *testing.T) { if fields.PrecipitationTiming != "Afternoon." { t.Fatalf("PrecipitationTiming = %q, want trimmed precipitation timing", fields.PrecipitationTiming) } - if fields.Confidence != "Medium" { - t.Fatalf("Confidence = %q, want trimmed confidence", fields.Confidence) - } - want := `{"summary":"Shared summary.","forecast_discussion":["First paragraph.","Second paragraph."],"precipitation_timing":"Afternoon.","confidence":"Medium"}` + want := `{"summary":"Shared summary.","forecast_discussion":["First paragraph.","Second paragraph."],"precipitation_timing":"Afternoon."}` if string(normalized) != want { t.Fatalf("normalized = %s, want %s", normalized, want) } }) - t.Run("omits empty optional fields", func(t *testing.T) { + t.Run("preserves required empty precipitation timing", func(t *testing.T) { _, normalized, err := report.validate([]byte(`{ "summary": "Shared summary.", "forecast_discussion": ["First paragraph."], - "precipitation_timing": " ", - "confidence": " " + "precipitation_timing": " " }`)) if err != nil { t.Fatalf("validate() error = %v", err) } - want := `{"summary":"Shared summary.","forecast_discussion":["First paragraph."]}` + want := `{"summary":"Shared summary.","forecast_discussion":["First paragraph."],"precipitation_timing":""}` if string(normalized) != want { t.Fatalf("normalized = %s, want %s", normalized, want) } }) + t.Run("requires precipitation timing field", func(t *testing.T) { + _, _, err := report.validate([]byte(`{"summary":"Shared summary.","forecast_discussion":["First paragraph."]}`)) + want := fmt.Sprintf("%s generated text precipitation timing is required", report.name) + if err == nil || err.Error() != want { + t.Fatalf("validate() error = %v, want %q", err, want) + } + }) + t.Run("rejects unknown fields", func(t *testing.T) { _, _, err := report.validate([]byte(`{"summary":"Shared summary.","forecast_discussion":["First paragraph."],"extra":"value"}`)) if err == nil { @@ -110,6 +113,13 @@ func TestValidateDayStyleGeneratedTextSharedBehavior(t *testing.T) { t.Fatalf("validate() error = %v, want unknown field error", err) } }) + + t.Run("rejects retired confidence field", func(t *testing.T) { + _, _, err := report.validate([]byte(`{"summary":"Shared summary.","forecast_discussion":["First paragraph."],"precipitation_timing":"","confidence":"Medium"}`)) + if err == nil || !strings.Contains(err.Error(), `unknown field "confidence"`) { + t.Fatalf("validate() error = %v, want retired confidence field rejection", err) + } + }) }) } } diff --git a/internal/generatedtext/hourly.go b/internal/generatedtext/hourly.go index 9e431ae..ae8eeed 100644 --- a/internal/generatedtext/hourly.go +++ b/internal/generatedtext/hourly.go @@ -9,8 +9,7 @@ import ( type Hourly struct { Summary string `json:"summary"` ForecastDiscussion string `json:"forecast_discussion"` - PrecipitationTiming string `json:"precipitation_timing,omitempty"` - Confidence string `json:"confidence,omitempty"` + PrecipitationTiming string `json:"precipitation_timing"` } func ValidateHourly(data []byte) (Hourly, []byte, error) { @@ -22,13 +21,15 @@ func ValidateHourly(data []byte) (Hourly, []byte, error) { value.Summary = strings.TrimSpace(value.Summary) value.ForecastDiscussion = strings.TrimSpace(value.ForecastDiscussion) value.PrecipitationTiming = strings.TrimSpace(value.PrecipitationTiming) - value.Confidence = strings.TrimSpace(value.Confidence) if value.Summary == "" { return Hourly{}, nil, fmt.Errorf("hourly generated text summary is required") } if value.ForecastDiscussion == "" { return Hourly{}, nil, fmt.Errorf("hourly generated text forecast discussion is required") } + if err := requireGeneratedTextStringField(data, "hourly", "precipitation_timing"); err != nil { + return Hourly{}, nil, err + } normalized, err := normalizeGeneratedText(value, "hourly") if err != nil { diff --git a/internal/generatedtext/hourly_test.go b/internal/generatedtext/hourly_test.go index d98a336..4260b16 100644 --- a/internal/generatedtext/hourly_test.go +++ b/internal/generatedtext/hourly_test.go @@ -9,8 +9,7 @@ func TestValidateHourlyNormalizesJSON(t *testing.T) { value, normalized, err := ValidateHourly([]byte(`{ "summary": " Storm chances increase. ", "forecast_discussion": " A front will keep the region unsettled. ", - "precipitation_timing": " Showers are most likely early this afternoon. ", - "confidence": " Medium " + "precipitation_timing": " Showers are most likely early this afternoon. " }`)) if err != nil { t.Fatalf("ValidateHourly() error = %v", err) @@ -24,23 +23,22 @@ func TestValidateHourlyNormalizesJSON(t *testing.T) { if value.PrecipitationTiming != "Showers are most likely early this afternoon." { t.Fatalf("PrecipitationTiming = %q, want trimmed precipitation timing", value.PrecipitationTiming) } - want := `{"summary":"Storm chances increase.","forecast_discussion":"A front will keep the region unsettled.","precipitation_timing":"Showers are most likely early this afternoon.","confidence":"Medium"}` + want := `{"summary":"Storm chances increase.","forecast_discussion":"A front will keep the region unsettled.","precipitation_timing":"Showers are most likely early this afternoon."}` if string(normalized) != want { t.Fatalf("normalized = %s, want %s", normalized, want) } } -func TestValidateHourlyOmitsEmptyConfidence(t *testing.T) { +func TestValidateHourlyPreservesRequiredEmptyPrecipitationTiming(t *testing.T) { _, normalized, err := ValidateHourly([]byte(`{ "summary": "Storm chances increase.", "forecast_discussion": "A front will keep the region unsettled.", - "precipitation_timing": " ", - "confidence": " " + "precipitation_timing": " " }`)) if err != nil { t.Fatalf("ValidateHourly() error = %v", err) } - want := `{"summary":"Storm chances increase.","forecast_discussion":"A front will keep the region unsettled."}` + want := `{"summary":"Storm chances increase.","forecast_discussion":"A front will keep the region unsettled.","precipitation_timing":""}` if string(normalized) != want { t.Fatalf("normalized = %s, want %s", normalized, want) } @@ -72,6 +70,21 @@ func TestValidateHourlyRejectsInvalidInput(t *testing.T) { in: `{"summary":"Storm chances increase.","forecast_discussion":" "}`, want: "forecast discussion is required", }, + { + name: "missing precipitation timing", + in: `{"summary":"Storm chances increase.","forecast_discussion":"A front will keep the region unsettled."}`, + want: "precipitation timing is required", + }, + { + name: "null precipitation timing", + in: `{"summary":"Storm chances increase.","forecast_discussion":"A front will keep the region unsettled.","precipitation_timing":null}`, + want: "precipitation timing must be a string", + }, + { + name: "retired confidence field rejected", + in: `{"summary":"Storm chances increase.","forecast_discussion":"A front will keep the region unsettled.","precipitation_timing":"","confidence":"Medium"}`, + want: `unknown field "confidence"`, + }, { name: "old timing field rejected", in: `{"summary":"Storm chances increase.","forecast_discussion":"A front will keep the region unsettled.","timing":"Late morning."}`, diff --git a/internal/generatedtext/json.go b/internal/generatedtext/json.go index 027a9b4..7d3897d 100644 --- a/internal/generatedtext/json.go +++ b/internal/generatedtext/json.go @@ -5,6 +5,7 @@ import ( "encoding/json" "fmt" "io" + "strings" ) func decodeGeneratedText[T any](data []byte, name string) (T, error) { @@ -24,6 +25,21 @@ func decodeGeneratedText[T any](data []byte, name string) (T, error) { return value, fmt.Errorf("decode %s generated text: multiple JSON values", name) } +func requireGeneratedTextStringField(data []byte, name, field string) error { + var fields map[string]json.RawMessage + if err := json.Unmarshal(data, &fields); err != nil { + return fmt.Errorf("decode %s generated text: %w", name, err) + } + raw, ok := fields[field] + if !ok { + return fmt.Errorf("%s generated text %s is required", name, strings.ReplaceAll(field, "_", " ")) + } + if bytes.Equal(bytes.TrimSpace(raw), []byte("null")) { + return fmt.Errorf("%s generated text %s must be a string", name, strings.ReplaceAll(field, "_", " ")) + } + return nil +} + func normalizeGeneratedText[T any](value T, name string) ([]byte, error) { normalized, err := json.Marshal(value) if err != nil { diff --git a/internal/generatedtext/render_context_test.go b/internal/generatedtext/render_context_test.go index 65b5e10..f856c7b 100644 --- a/internal/generatedtext/render_context_test.go +++ b/internal/generatedtext/render_context_test.go @@ -21,7 +21,6 @@ func TestBuildHourlyRenderContext(t *testing.T) { Summary: "Storm chances increase through late morning.", ForecastDiscussion: "A front will keep the region unsettled.", PrecipitationTiming: "A cold front is moving into the region.", - Confidence: "Medium confidence in timing.", } collected := testCollected() derived := testDerived() diff --git a/internal/generatedtext/today.go b/internal/generatedtext/today.go index b400fa7..98ff6cd 100644 --- a/internal/generatedtext/today.go +++ b/internal/generatedtext/today.go @@ -3,8 +3,7 @@ package generatedtext type Today struct { Summary string `json:"summary"` ForecastDiscussion []string `json:"forecast_discussion"` - PrecipitationTiming string `json:"precipitation_timing,omitempty"` - Confidence string `json:"confidence,omitempty"` + PrecipitationTiming string `json:"precipitation_timing"` } func ValidateToday(data []byte) (Today, []byte, error) { @@ -16,7 +15,6 @@ func (t *Today) dayStyleFields() dayStyleFields { Summary: t.Summary, ForecastDiscussion: t.ForecastDiscussion, PrecipitationTiming: t.PrecipitationTiming, - Confidence: t.Confidence, } } @@ -24,5 +22,4 @@ func (t *Today) setDayStyleFields(fields dayStyleFields) { t.Summary = fields.Summary t.ForecastDiscussion = fields.ForecastDiscussion t.PrecipitationTiming = fields.PrecipitationTiming - t.Confidence = fields.Confidence } diff --git a/internal/generatedtext/today_test.go b/internal/generatedtext/today_test.go index b2d3701..cff56cb 100644 --- a/internal/generatedtext/today_test.go +++ b/internal/generatedtext/today_test.go @@ -13,8 +13,7 @@ func TestValidateTodayNormalizesJSON(t *testing.T) { "", " Temperatures stay mild through the afternoon. " ], - "precipitation_timing": " Rain is most likely during the afternoon. ", - "confidence": " Medium " + "precipitation_timing": " Rain is most likely during the afternoon. " }`)) if err != nil { t.Fatalf("ValidateToday() error = %v", err) @@ -28,23 +27,22 @@ func TestValidateTodayNormalizesJSON(t *testing.T) { if value.PrecipitationTiming != "Rain is most likely during the afternoon." { t.Fatalf("PrecipitationTiming = %q, want trimmed precipitation timing", value.PrecipitationTiming) } - want := `{"summary":"Showers are likely today.","forecast_discussion":["A front will keep rain chances elevated.","Temperatures stay mild through the afternoon."],"precipitation_timing":"Rain is most likely during the afternoon.","confidence":"Medium"}` + want := `{"summary":"Showers are likely today.","forecast_discussion":["A front will keep rain chances elevated.","Temperatures stay mild through the afternoon."],"precipitation_timing":"Rain is most likely during the afternoon."}` if string(normalized) != want { t.Fatalf("normalized = %s, want %s", normalized, want) } } -func TestValidateTodayOmitsEmptyOptionalFields(t *testing.T) { +func TestValidateTodayPreservesRequiredEmptyPrecipitationTiming(t *testing.T) { _, normalized, err := ValidateToday([]byte(`{ "summary": "Showers are likely today.", "forecast_discussion": ["A front will keep rain chances elevated."], - "precipitation_timing": " ", - "confidence": " " + "precipitation_timing": " " }`)) if err != nil { t.Fatalf("ValidateToday() error = %v", err) } - want := `{"summary":"Showers are likely today.","forecast_discussion":["A front will keep rain chances elevated."]}` + want := `{"summary":"Showers are likely today.","forecast_discussion":["A front will keep rain chances elevated."],"precipitation_timing":""}` if string(normalized) != want { t.Fatalf("normalized = %s, want %s", normalized, want) } diff --git a/internal/generatedtext/tomorrow.go b/internal/generatedtext/tomorrow.go index 8854acc..26022ba 100644 --- a/internal/generatedtext/tomorrow.go +++ b/internal/generatedtext/tomorrow.go @@ -3,8 +3,7 @@ package generatedtext type Tomorrow struct { Summary string `json:"summary"` ForecastDiscussion []string `json:"forecast_discussion"` - PrecipitationTiming string `json:"precipitation_timing,omitempty"` - Confidence string `json:"confidence,omitempty"` + PrecipitationTiming string `json:"precipitation_timing"` } func ValidateTomorrow(data []byte) (Tomorrow, []byte, error) { @@ -16,7 +15,6 @@ func (t *Tomorrow) dayStyleFields() dayStyleFields { Summary: t.Summary, ForecastDiscussion: t.ForecastDiscussion, PrecipitationTiming: t.PrecipitationTiming, - Confidence: t.Confidence, } } @@ -24,5 +22,4 @@ func (t *Tomorrow) setDayStyleFields(fields dayStyleFields) { t.Summary = fields.Summary t.ForecastDiscussion = fields.ForecastDiscussion t.PrecipitationTiming = fields.PrecipitationTiming - t.Confidence = fields.Confidence } diff --git a/internal/generatedtext/tomorrow_test.go b/internal/generatedtext/tomorrow_test.go index ce54354..f8aed4c 100644 --- a/internal/generatedtext/tomorrow_test.go +++ b/internal/generatedtext/tomorrow_test.go @@ -13,8 +13,7 @@ func TestValidateTomorrowNormalizesJSON(t *testing.T) { "", " Temperatures stay seasonable by afternoon. " ], - "precipitation_timing": " Rain is most likely before sunrise. ", - "confidence": " Medium " + "precipitation_timing": " Rain is most likely before sunrise. " }`)) if err != nil { t.Fatalf("ValidateTomorrow() error = %v", err) @@ -28,23 +27,22 @@ func TestValidateTomorrowNormalizesJSON(t *testing.T) { if value.PrecipitationTiming != "Rain is most likely before sunrise." { t.Fatalf("PrecipitationTiming = %q, want trimmed precipitation timing", value.PrecipitationTiming) } - want := `{"summary":"Storms become more likely tomorrow.","forecast_discussion":["A front will keep showers in the forecast.","Temperatures stay seasonable by afternoon."],"precipitation_timing":"Rain is most likely before sunrise.","confidence":"Medium"}` + want := `{"summary":"Storms become more likely tomorrow.","forecast_discussion":["A front will keep showers in the forecast.","Temperatures stay seasonable by afternoon."],"precipitation_timing":"Rain is most likely before sunrise."}` if string(normalized) != want { t.Fatalf("normalized = %s, want %s", normalized, want) } } -func TestValidateTomorrowOmitsEmptyOptionalFields(t *testing.T) { +func TestValidateTomorrowPreservesRequiredEmptyPrecipitationTiming(t *testing.T) { _, normalized, err := ValidateTomorrow([]byte(`{ "summary": "Storms become more likely tomorrow.", "forecast_discussion": ["A front will keep showers in the forecast."], - "precipitation_timing": " ", - "confidence": " " + "precipitation_timing": " " }`)) if err != nil { t.Fatalf("ValidateTomorrow() error = %v", err) } - want := `{"summary":"Storms become more likely tomorrow.","forecast_discussion":["A front will keep showers in the forecast."]}` + want := `{"summary":"Storms become more likely tomorrow.","forecast_discussion":["A front will keep showers in the forecast."],"precipitation_timing":""}` if string(normalized) != want { t.Fatalf("normalized = %s, want %s", normalized, want) } diff --git a/internal/promptassets/assets/prompts/daily/daily_generated_text.user.md b/internal/promptassets/assets/prompts/daily/daily_generated_text.user.md index c481216..0936d82 100644 --- a/internal/promptassets/assets/prompts/daily/daily_generated_text.user.md +++ b/internal/promptassets/assets/prompts/daily/daily_generated_text.user.md @@ -8,8 +8,7 @@ Return these fields: - `summary`: required. One or two sentences summarizing the main weather story for the valid period. - `forecast_discussion`: required. Three paragraphs explaining the broader setup, trend, or forecast reasoning most relevant to the valid period. -- `precipitation_timing`: optional. Include only when the deterministic `precip_timing` module contains precipitation windows. -- `confidence`: optional. Include only if uncertainty, timing spread, or conflicting signals materially affect how the reader should interpret the forecast. +- `precipitation_timing`: required. When the deterministic `precip_timing` module contains precipitation windows, provide the supported timing prose described below. Otherwise, return an empty string (`""`). Return JSON only. @@ -27,7 +26,7 @@ In most cases, include three paragraphs: a two-to-four sentence relevant local o # Precipitation timing -Include this only if precipitation is forecast. Use one to four sentences to give practical context about a supported frontal, convective, or stratiform setup; expected type, intensity, and duration; and uncertainty in onset or duration. +When precipitation windows are present, use one to four sentences to give practical context about a supported frontal, convective, or stratiform setup; expected type, intensity, and duration; and uncertainty in onset or duration. When no precipitation windows are present, return an empty string (`""`) for `precipitation_timing`. # Narrative source selection diff --git a/internal/promptassets/assets/prompts/daily/daily_generated_text.yml b/internal/promptassets/assets/prompts/daily/daily_generated_text.yml index 7959bde..80481a8 100644 --- a/internal/promptassets/assets/prompts/daily/daily_generated_text.yml +++ b/internal/promptassets/assets/prompts/daily/daily_generated_text.yml @@ -1,5 +1,5 @@ id: weather.daily_generated_text -version: "1.0.0" +version: "1.0.1" default_profile: gemini-flash-latest description: Daily weather report analysis prompt. inputs: diff --git a/internal/promptassets/assets/prompts/hourly/hourly_generated_text.user.md b/internal/promptassets/assets/prompts/hourly/hourly_generated_text.user.md index 5bcac60..f441c1e 100644 --- a/internal/promptassets/assets/prompts/hourly/hourly_generated_text.user.md +++ b/internal/promptassets/assets/prompts/hourly/hourly_generated_text.user.md @@ -8,8 +8,7 @@ Return these fields: - `summary`: required. One or two sentences summarizing the main weather story for the valid period. - `forecast_discussion`: required. Two or three sentences explaining the broader setup, trend, or forecast reasoning most relevant to the valid period. -- `precipitation_timing`: optional. Include only when the deterministic `precip_timing` module contains precipitation windows. -- `confidence`: optional. Include only if uncertainty, timing spread, or conflicting signals materially affect how the reader should interpret the forecast. +- `precipitation_timing`: required. When the deterministic `precip_timing` module contains precipitation windows, provide the supported timing prose described below. Otherwise, return an empty string (`""`). Return JSON only. @@ -25,4 +24,4 @@ Use narrative products to explain the “why” behind the local forecast when u # Precipitation timing -Include this only if precipitation is forecast. Use one to four sentences to give practical context about a supported frontal, convective, or stratiform setup; expected type, intensity, and duration; and uncertainty in onset or duration. +When precipitation windows are present, use one to four sentences to give practical context about a supported frontal, convective, or stratiform setup; expected type, intensity, and duration; and uncertainty in onset or duration. When no precipitation windows are present, return an empty string (`""`) for `precipitation_timing`. diff --git a/internal/promptassets/assets/prompts/hourly/hourly_generated_text.yml b/internal/promptassets/assets/prompts/hourly/hourly_generated_text.yml index 21b4806..966f55e 100644 --- a/internal/promptassets/assets/prompts/hourly/hourly_generated_text.yml +++ b/internal/promptassets/assets/prompts/hourly/hourly_generated_text.yml @@ -1,5 +1,5 @@ id: weather.hourly_generated_text -version: "1.0.0" +version: "1.0.1" default_profile: gemini-flash-latest description: Hourly weather report analysis prompt. inputs: diff --git a/internal/promptassets/assets/prompts/today/today_generated_text.user.md b/internal/promptassets/assets/prompts/today/today_generated_text.user.md index 5270f55..fe853f2 100644 --- a/internal/promptassets/assets/prompts/today/today_generated_text.user.md +++ b/internal/promptassets/assets/prompts/today/today_generated_text.user.md @@ -8,8 +8,7 @@ Return these fields: - `summary`: required. One or two sentences summarizing the main weather story for the valid period. - `forecast_discussion`: required. Three paragraphs explaining the broader setup, trend, or forecast reasoning most relevant to the valid period. -- `precipitation_timing`: optional. Include only when the deterministic `precip_timing` module contains precipitation windows. -- `confidence`: optional. Include only if uncertainty, timing spread, or conflicting signals materially affect how the reader should interpret the forecast. +- `precipitation_timing`: required. When the deterministic `precip_timing` module contains precipitation windows, provide the supported timing prose described below. Otherwise, return an empty string (`""`). Return JSON only. @@ -27,4 +26,4 @@ In most cases, include three paragraphs: a two-to-four sentence relevant local o # Precipitation timing -Include this only if precipitation is forecast. Use one to four sentences to give practical context about a supported frontal, convective, or stratiform setup; expected type, intensity, and duration; and uncertainty in onset or duration. +When precipitation windows are present, use one to four sentences to give practical context about a supported frontal, convective, or stratiform setup; expected type, intensity, and duration; and uncertainty in onset or duration. When no precipitation windows are present, return an empty string (`""`) for `precipitation_timing`. diff --git a/internal/promptassets/assets/prompts/today/today_generated_text.yml b/internal/promptassets/assets/prompts/today/today_generated_text.yml index 31bc92f..0784318 100644 --- a/internal/promptassets/assets/prompts/today/today_generated_text.yml +++ b/internal/promptassets/assets/prompts/today/today_generated_text.yml @@ -1,5 +1,5 @@ id: weather.today_generated_text -version: "1.0.0" +version: "1.0.1" default_profile: gemini-flash-latest description: Today's weather report analysis prompt. inputs: diff --git a/internal/promptassets/assets/prompts/tomorrow/tomorrow_generated_text.user.md b/internal/promptassets/assets/prompts/tomorrow/tomorrow_generated_text.user.md index cf3e236..69b4eac 100644 --- a/internal/promptassets/assets/prompts/tomorrow/tomorrow_generated_text.user.md +++ b/internal/promptassets/assets/prompts/tomorrow/tomorrow_generated_text.user.md @@ -8,8 +8,7 @@ Return these fields: - `summary`: required. One or two sentences summarizing the main weather story for the valid period. - `forecast_discussion`: required. Three paragraphs explaining the broader setup, trend, or forecast reasoning most relevant to the valid period. -- `precipitation_timing`: optional. Include only when the deterministic `precip_timing` module contains precipitation windows. -- `confidence`: optional. Include only if uncertainty, timing spread, or conflicting signals materially affect how the reader should interpret the forecast. +- `precipitation_timing`: required. When the deterministic `precip_timing` module contains precipitation windows, provide the supported timing prose described below. Otherwise, return an empty string (`""`). Return JSON only. @@ -27,4 +26,4 @@ In most cases, include three paragraphs: a two-to-four sentence relevant local o # Precipitation timing -Use one or two sentences to give practical context about a supported frontal, convective, or stratiform setup; expected type, intensity, and duration; and uncertainty in onset or duration. +When precipitation windows are present, use one or two sentences to give practical context about a supported frontal, convective, or stratiform setup; expected type, intensity, and duration; and uncertainty in onset or duration. When no precipitation windows are present, return an empty string (`""`) for `precipitation_timing`. diff --git a/internal/promptassets/assets/prompts/tomorrow/tomorrow_generated_text.yml b/internal/promptassets/assets/prompts/tomorrow/tomorrow_generated_text.yml index 07563ed..d65a569 100644 --- a/internal/promptassets/assets/prompts/tomorrow/tomorrow_generated_text.yml +++ b/internal/promptassets/assets/prompts/tomorrow/tomorrow_generated_text.yml @@ -1,5 +1,5 @@ id: weather.tomorrow_generated_text -version: "1.0.0" +version: "1.0.1" default_profile: gemini-flash-latest description: Tomorrow's weather report analysis prompt. inputs: diff --git a/internal/promptassets/assets/schemas/daily.generated_text.schema.json b/internal/promptassets/assets/schemas/daily.generated_text.schema.json index 5091fc0..21b1de0 100644 --- a/internal/promptassets/assets/schemas/daily.generated_text.schema.json +++ b/internal/promptassets/assets/schemas/daily.generated_text.schema.json @@ -4,11 +4,10 @@ "title": "Daily GeneratedText", "type": "object", "additionalProperties": false, - "required": ["summary", "forecast_discussion"], + "required": ["summary", "forecast_discussion", "precipitation_timing"], "properties": { "summary": {"type": "string"}, "forecast_discussion": {"type": "array", "items": {"type": "string"}, "minItems": 1}, - "precipitation_timing": {"type": "string"}, - "confidence": {"type": "string"} + "precipitation_timing": {"type": "string"} } } diff --git a/internal/promptassets/assets/schemas/hourly.generated_text.schema.json b/internal/promptassets/assets/schemas/hourly.generated_text.schema.json index 6b49afb..45c362f 100644 --- a/internal/promptassets/assets/schemas/hourly.generated_text.schema.json +++ b/internal/promptassets/assets/schemas/hourly.generated_text.schema.json @@ -4,11 +4,10 @@ "title": "Hourly GeneratedText", "type": "object", "additionalProperties": false, - "required": ["summary", "forecast_discussion"], + "required": ["summary", "forecast_discussion", "precipitation_timing"], "properties": { "summary": {"type": "string"}, "forecast_discussion": {"type": "string"}, - "precipitation_timing": {"type": "string"}, - "confidence": {"type": "string"} + "precipitation_timing": {"type": "string"} } } diff --git a/internal/promptassets/assets/schemas/today.generated_text.schema.json b/internal/promptassets/assets/schemas/today.generated_text.schema.json index 5ba5da7..1fad04b 100644 --- a/internal/promptassets/assets/schemas/today.generated_text.schema.json +++ b/internal/promptassets/assets/schemas/today.generated_text.schema.json @@ -4,11 +4,10 @@ "title": "Today GeneratedText", "type": "object", "additionalProperties": false, - "required": ["summary", "forecast_discussion"], + "required": ["summary", "forecast_discussion", "precipitation_timing"], "properties": { "summary": {"type": "string"}, "forecast_discussion": {"type": "array", "items": {"type": "string"}, "minItems": 1}, - "precipitation_timing": {"type": "string"}, - "confidence": {"type": "string"} + "precipitation_timing": {"type": "string"} } } diff --git a/internal/promptassets/assets/schemas/tomorrow.generated_text.schema.json b/internal/promptassets/assets/schemas/tomorrow.generated_text.schema.json index c7f121e..bff0368 100644 --- a/internal/promptassets/assets/schemas/tomorrow.generated_text.schema.json +++ b/internal/promptassets/assets/schemas/tomorrow.generated_text.schema.json @@ -4,11 +4,10 @@ "title": "Tomorrow GeneratedText", "type": "object", "additionalProperties": false, - "required": ["summary", "forecast_discussion"], + "required": ["summary", "forecast_discussion", "precipitation_timing"], "properties": { "summary": {"type": "string"}, "forecast_discussion": {"type": "array", "items": {"type": "string"}, "minItems": 1}, - "precipitation_timing": {"type": "string"}, - "confidence": {"type": "string"} + "precipitation_timing": {"type": "string"} } } diff --git a/internal/promptassets/promptassets_test.go b/internal/promptassets/promptassets_test.go index 8f1991b..625bc12 100644 --- a/internal/promptassets/promptassets_test.go +++ b/internal/promptassets/promptassets_test.go @@ -67,8 +67,8 @@ func TestPromptAssetsDeclareTheFourGeneratedTextPrompts(t *testing.T) { if err := yaml.Unmarshal(data, &definition); err != nil { t.Fatalf("decode prompt definition: %v", err) } - if definition.ID != tc.id || definition.Version != "1.0.0" || definition.DefaultProfile != "gemini-flash-latest" { - t.Fatalf("definition = %#v, want %s version 1.0.0 and gemini-flash-latest", definition, tc.id) + if definition.ID != tc.id || definition.Version != "1.0.1" || definition.DefaultProfile != "gemini-flash-latest" { + t.Fatalf("definition = %#v, want %s version 1.0.1 and gemini-flash-latest", definition, tc.id) } if len(definition.Inputs) != 1 || definition.Inputs[0].Name != "data_package" || !definition.Inputs[0].Required || definition.Inputs[0].ContentType != "application/yaml" { t.Fatalf("inputs = %#v, want one required YAML data_package", definition.Inputs) @@ -101,11 +101,11 @@ func TestSchemasAreCanonicalAndIndependent(t *testing.T) { if err := json.Unmarshal(data, &schema); err != nil { t.Fatalf("decode schema: %v", err) } - if schema.Type != "object" || schema.AdditionalProperties || strings.Join(schema.Required, ",") != "summary,forecast_discussion" { + if schema.Type != "object" || schema.AdditionalProperties || strings.Join(schema.Required, ",") != "summary,forecast_discussion,precipitation_timing" { t.Fatalf("schema = %#v, want strict generated-text object", schema) } - if _, ok := schema.Properties["confidence"]; !ok { - t.Fatalf("schema properties = %#v, want confidence", schema.Properties) + if _, ok := schema.Properties["confidence"]; ok { + t.Fatalf("schema properties = %#v, do not want retired confidence field", schema.Properties) } if id == "daily" && (schema.ID != "weatherreporter.daily.generated_text.schema.json" || schema.Title != "Daily GeneratedText") { t.Fatalf("daily schema identity = %q/%q, want corrected Daily identity", schema.ID, schema.Title) @@ -129,11 +129,11 @@ func TestPromptkitInspectsEmbeddedPromptsOffline(t *testing.T) { } for _, id := range []string{"weather.daily_generated_text", "weather.today_generated_text", "weather.tomorrow_generated_text", "weather.hourly_generated_text"} { t.Run(id, func(t *testing.T) { - inspection, err := engine.InspectPrompt(context.Background(), id, "1.0.0") + inspection, err := engine.InspectPrompt(context.Background(), id, "1.0.1") if err != nil { t.Fatalf("InspectPrompt() error = %v", err) } - if inspection.PromptID != id || inspection.PromptVersion != "1.0.0" || inspection.DefaultProfileID != "gemini-flash-latest" { + if inspection.PromptID != id || inspection.PromptVersion != "1.0.1" || inspection.DefaultProfileID != "gemini-flash-latest" { t.Fatalf("inspection = %#v", inspection) } }) diff --git a/internal/report/daily_report.go b/internal/report/daily_report.go index 93284c7..d77afb8 100644 --- a/internal/report/daily_report.go +++ b/internal/report/daily_report.go @@ -12,7 +12,7 @@ func dailyDefinition() Definition { ID: Daily, Name: "Daily Report", PromptID: "weather.daily_generated_text", - PromptVersion: "1.0.0", + PromptVersion: "1.0.1", TemplateID: "daily", GeneratedTextSchemaID: "daily", ComparisonStrategy: CompareSameValidDate, diff --git a/internal/report/hourly_report.go b/internal/report/hourly_report.go index 6860bdf..c99bfa9 100644 --- a/internal/report/hourly_report.go +++ b/internal/report/hourly_report.go @@ -14,7 +14,7 @@ func hourlyDefinition() Definition { ID: Hourly, Name: "Hourly Report", PromptID: "weather.hourly_generated_text", - PromptVersion: "1.0.0", + PromptVersion: "1.0.1", TemplateID: "hourly", GeneratedTextSchemaID: "hourly", ComparisonStrategy: CompareRollingWindow, diff --git a/internal/report/period_test.go b/internal/report/period_test.go index 818d087..8f92fa8 100644 --- a/internal/report/period_test.go +++ b/internal/report/period_test.go @@ -51,8 +51,8 @@ func TestRegistryContainsOnlyPromptBackedReports(t *testing.T) { } for _, definition := range definitions { - if definition.PromptVersion != "1.0.0" { - t.Fatalf("%s PromptVersion = %q, want 1.0.0", definition.ID, definition.PromptVersion) + if definition.PromptVersion != "1.0.1" { + t.Fatalf("%s PromptVersion = %q, want 1.0.1", definition.ID, definition.PromptVersion) } if definition.TemplateID == "" || definition.GeneratedTextSchemaID == "" { t.Fatalf("%s template/schema = %q/%q, want both set", definition.ID, definition.TemplateID, definition.GeneratedTextSchemaID) diff --git a/internal/report/today_report.go b/internal/report/today_report.go index 89414db..5a70277 100644 --- a/internal/report/today_report.go +++ b/internal/report/today_report.go @@ -10,7 +10,7 @@ func todayDefinition() Definition { ID: Today, Name: "Today Report", PromptID: "weather.today_generated_text", - PromptVersion: "1.0.0", + PromptVersion: "1.0.1", TemplateID: "today", GeneratedTextSchemaID: "today", ComparisonStrategy: CompareSameValidDate, diff --git a/internal/report/tomorrow_report.go b/internal/report/tomorrow_report.go index d77a36d..4e2352c 100644 --- a/internal/report/tomorrow_report.go +++ b/internal/report/tomorrow_report.go @@ -10,7 +10,7 @@ func tomorrowDefinition() Definition { ID: Tomorrow, Name: "Tomorrow Report", PromptID: "weather.tomorrow_generated_text", - PromptVersion: "1.0.0", + PromptVersion: "1.0.1", TemplateID: "tomorrow", GeneratedTextSchemaID: "tomorrow", ComparisonStrategy: CompareSameValidDate, diff --git a/internal/reporttemplate/reporttemplate_test.go b/internal/reporttemplate/reporttemplate_test.go index ecf4e59..3023703 100644 --- a/internal/reporttemplate/reporttemplate_test.go +++ b/internal/reporttemplate/reporttemplate_test.go @@ -64,7 +64,7 @@ func TestSchemaLookup(t *testing.T) { if err != nil { t.Fatalf("Schema() error = %v", err) } - assertStringSchema(t, data, "summary,forecast_discussion", []string{"summary", "forecast_discussion", "precipitation_timing", "confidence"}) + assertStringSchema(t, data, "summary,forecast_discussion,precipitation_timing", []string{"summary", "forecast_discussion", "precipitation_timing"}) } func TestTomorrowSchemaLookup(t *testing.T) { @@ -72,7 +72,7 @@ func TestTomorrowSchemaLookup(t *testing.T) { if err != nil { t.Fatalf("Schema() error = %v", err) } - schema := assertSchema(t, data, "summary,forecast_discussion") + schema := assertSchema(t, data, "summary,forecast_discussion,precipitation_timing") property, ok := schema.Properties["forecast_discussion"].(map[string]any) if !ok { t.Fatal("schema property forecast_discussion missing or invalid") @@ -87,7 +87,7 @@ func TestTomorrowSchemaLookup(t *testing.T) { if !ok || items["type"] != "string" { t.Fatalf("forecast_discussion items = %#v, want string items", property["items"]) } - for _, field := range []string{"summary", "precipitation_timing", "confidence"} { + for _, field := range []string{"summary", "precipitation_timing"} { property, ok := schema.Properties[field].(map[string]any) if !ok { t.Fatalf("schema property %q missing or invalid", field) @@ -103,7 +103,7 @@ func TestDailySchemaLookup(t *testing.T) { if err != nil { t.Fatalf("Schema() error = %v", err) } - schema := assertSchema(t, data, "summary,forecast_discussion") + schema := assertSchema(t, data, "summary,forecast_discussion,precipitation_timing") property, ok := schema.Properties["forecast_discussion"].(map[string]any) if !ok { t.Fatal("schema property forecast_discussion missing or invalid") @@ -118,7 +118,7 @@ func TestDailySchemaLookup(t *testing.T) { if !ok || items["type"] != "string" { t.Fatalf("forecast_discussion items = %#v, want string items", property["items"]) } - for _, field := range []string{"summary", "precipitation_timing", "confidence"} { + for _, field := range []string{"summary", "precipitation_timing"} { property, ok := schema.Properties[field].(map[string]any) if !ok { t.Fatalf("schema property %q missing or invalid", field) @@ -134,7 +134,7 @@ func TestTodaySchemaLookup(t *testing.T) { if err != nil { t.Fatalf("Schema() error = %v", err) } - schema := assertSchema(t, data, "summary,forecast_discussion") + schema := assertSchema(t, data, "summary,forecast_discussion,precipitation_timing") property, ok := schema.Properties["forecast_discussion"].(map[string]any) if !ok { t.Fatal("schema property forecast_discussion missing or invalid") @@ -149,7 +149,7 @@ func TestTodaySchemaLookup(t *testing.T) { if !ok || items["type"] != "string" { t.Fatalf("forecast_discussion items = %#v, want string items", property["items"]) } - for _, field := range []string{"summary", "precipitation_timing", "confidence"} { + for _, field := range []string{"summary", "precipitation_timing"} { property, ok := schema.Properties[field].(map[string]any) if !ok { t.Fatalf("schema property %q missing or invalid", field) @@ -214,7 +214,6 @@ func TestRenderHourly(t *testing.T) { Summary: "Storm chances increase through late morning.", ForecastDiscussion: "A front will keep the region unsettled.", PrecipitationTiming: "A cold front is moving into the region.", - Confidence: "Medium confidence in timing.", }, Modules: testModules{ CurrentConditions: &testCurrentConditions{ @@ -282,7 +281,7 @@ func TestRenderHourly(t *testing.T) { t.Fatalf("rendered template missing %q:\n%s", want, text) } } - if strings.Contains(text, "19%") || strings.Contains(text, "wind S") || strings.Contains(text, "## Confidence") { + if strings.Contains(text, "19%") || strings.Contains(text, "wind S") { t.Fatalf("rendered template included omitted details:\n%s", text) } for _, unwanted := range []string{"Avoid low-water crossings.", "Slight risk for severe thunderstorms"} { @@ -955,7 +954,6 @@ type testGeneratedText struct { Summary string ForecastDiscussion string PrecipitationTiming string - Confidence string } type testTomorrowReportContext struct { @@ -980,14 +978,12 @@ type testTomorrowGeneratedText struct { Summary string ForecastDiscussion []string PrecipitationTiming string - Confidence string } type testDailyGeneratedText struct { Summary string ForecastDiscussion []string PrecipitationTiming string - Confidence string } type testModules struct { diff --git a/internal/state/prompt_artifacts_test.go b/internal/state/prompt_artifacts_test.go index 4ed6895..b132d05 100644 --- a/internal/state/prompt_artifacts_test.go +++ b/internal/state/prompt_artifacts_test.go @@ -172,9 +172,9 @@ func validPreparationArtifact() PromptPreparationArtifact { return PromptPreparationArtifact{ SchemaVersion: PromptPreparationSchemaVersion, Status: PromptPreparationSucceeded, ReportID: report.Daily, RunID: "weatherreporter-run", PromptID: "weather.daily_generated_text", - PromptVersion: "1.0.0", DataPackagePath: "/workspace/data.yaml", + PromptVersion: "1.0.1", DataPackagePath: "/workspace/data.yaml", Preparation: &promptexec.Preparation{ - PromptID: "weather.daily_generated_text", PromptVersion: "1.0.0", + PromptID: "weather.daily_generated_text", PromptVersion: "1.0.1", DataPackagePath: "/workspace/data.yaml", }, StartedAt: started, EndedAt: started.Add(time.Second), Duration: time.Second, @@ -186,9 +186,9 @@ func validExecutionArtifact() PromptExecutionArtifact { validation := promptexec.NewValidation(promptexec.ValidationPassed, "json_schema", "daily.generated_text.schema.json", nil) return PromptExecutionArtifact{ SchemaVersion: PromptExecutionSchemaVersion, Status: PromptExecutionSucceeded, - ReportID: report.Daily, RunID: "weatherreporter-run", PromptID: "weather.daily_generated_text", PromptVersion: "1.0.0", + ReportID: report.Daily, RunID: "weatherreporter-run", PromptID: "weather.daily_generated_text", PromptVersion: "1.0.1", Provenance: &PromptExecutionProvenance{ - RunID: "provider-run", PromptID: "weather.daily_generated_text", PromptVersion: "1.0.0", + RunID: "provider-run", PromptID: "weather.daily_generated_text", PromptVersion: "1.0.1", PromptHash: "prompt-hash", RenderedPromptHash: "rendered-hash", ProfileID: "profile", BackendID: "backend", ModelName: "model", DataPackagePath: "/workspace/data.yaml", StartedAt: started, EndedAt: started.Add(time.Second), Duration: time.Second, @@ -201,7 +201,7 @@ func validFailedExecutionArtifact() PromptExecutionArtifact { started := time.Date(2026, 5, 29, 15, 0, 0, 0, time.UTC) return PromptExecutionArtifact{ SchemaVersion: PromptExecutionSchemaVersion, Status: PromptExecutionFailed, - ReportID: report.Daily, RunID: "weatherreporter-run", PromptID: "weather.daily_generated_text", PromptVersion: "1.0.0", + ReportID: report.Daily, RunID: "weatherreporter-run", PromptID: "weather.daily_generated_text", PromptVersion: "1.0.1", StartedAt: started, EndedAt: started, Error: &PromptArtifactError{Category: promptexec.Generation, Message: "provider unavailable"}, } }