Add daily generated text assets

This commit is contained in:
2026-06-15 16:23:13 +00:00
parent 4eece7cc8a
commit 3d452a120a
11 changed files with 528 additions and 20 deletions

View File

@@ -9,20 +9,21 @@ This document describes structured generated-text handling in
generated-text-template reports and builds curated render contexts for
templates. It also owns the generated-text catalog that connects report
definitions to validators, render-context builders, schema assets, and template
assets. The implemented contracts are Today Report, Tomorrow Report, and Hourly
Report.
assets. The implemented contracts are Daily validation and asset lookup, Today
Report, Tomorrow Report, and Hourly Report.
## Inputs And Outputs
Inputs:
- raw GeneratedText JSON for Today, Tomorrow Report, or Hourly Report
- raw GeneratedText JSON for Daily, Today, Tomorrow Report, or Hourly Report
- report metadata from `internal/briefing`
- a module snapshot from `internal/module`
- validated generated text
Outputs:
- typed `Daily` generated text
- typed `Today` generated text
- typed `Tomorrow` generated text
- typed `Hourly` generated text
@@ -33,6 +34,20 @@ Outputs:
- generated-text catalog handlers for report definitions that use
`generated_text_template`
The Daily generated text JSON accepts the same public fields and validation
rules as Tomorrow. Its catalog entry is selected through schema ID `daily` and
template ID `daily`; render-context construction is not implemented until the
Daily report context exists.
```json
{
"summary": "string",
"forecast_discussion": ["string"],
"precipitation_timing": "string",
"confidence": "string"
}
```
The hourly generated text JSON accepts:
```json
@@ -96,8 +111,8 @@ JSON when blank.
- Malformed generated-text JSON fails with decode context.
- Unknown generated-text JSON fields fail during decoding.
- Empty required fields fail after trimming whitespace.
- Today and Tomorrow forecast discussion fails when no nonblank paragraphs
remain.
- Daily, Today, and Tomorrow forecast discussion fails when no nonblank
paragraphs remain.
- Missing optional render-context stanzas become nil module pointers.
- Invalid render metadata, including missing timezone, missing generated time,
or invalid valid period, fails before template rendering.
@@ -109,6 +124,7 @@ JSON when blank.
Inspect:
- `internal/generatedtext/hourly_test.go`
- `internal/generatedtext/daily_test.go`
- `internal/generatedtext/today_test.go`
- `internal/generatedtext/tomorrow_test.go`
- `internal/generatedtext/catalog_test.go`

View File

@@ -6,8 +6,8 @@ in `internal/reporttemplate`.
## Purpose
`internal/reporttemplate` owns repository-native report templates and companion
GeneratedText JSON schemas. The implemented template contracts are Today
Report, Tomorrow Report, and Hourly Report.
GeneratedText JSON schemas. The implemented template assets are Daily, Today,
Tomorrow, and Hourly.
The package embeds assets from:
@@ -31,13 +31,13 @@ Outputs:
- GeneratedText schema bytes for prompt/schema configuration
- rendered Markdown bytes for app orchestration to persist
The implemented template IDs are `today`, `tomorrow`, and `hourly`. The
implemented schema IDs are also `today`, `tomorrow`, and `hourly`, backed by
matching `*.generated_text.schema.json` files.
The implemented template IDs are `daily`, `today`, `tomorrow`, and `hourly`.
The implemented schema IDs are also `daily`, `today`, `tomorrow`, and
`hourly`, backed by matching `*.generated_text.schema.json` files.
The Today generated-text prompt source is
`internal/reporttemplate/prompts/today.generated_text.md`, selected by prompt
ID `weather.today_generated_text`.
Generated-text prompt sources are maintained under
`internal/reporttemplate/prompts/`, including Daily's
`daily.generated_text.md` source for prompt ID `weather.daily_generated_text`.
## Boundaries
@@ -67,6 +67,10 @@ forecast rows, daily/daypart summaries, planning facts, and a multi-paragraph
forecast discussion generated-text slot. The ordered daypart slice is built in
Go so templates do not range over maps.
The Daily template asset uses the same Markdown structure and field surface as
Tomorrow's template. Its typed render context in `internal/generatedtext` is
not implemented yet.
Templates use `text/template` with `missingkey=error`, so missing context fields
fail rendering instead of producing incomplete Markdown.
@@ -78,10 +82,11 @@ to write for each generated-text prompt. Hourly requires:
- `summary`
- `forecast_discussion`
Today and Tomorrow require `summary` and a nonempty `forecast_discussion`
array. All generated-text schemas allow optional `precipitation_timing` and
`confidence`, and reject additional properties. Weather truth remains in module
outputs; GeneratedText is limited to prose slots consumed by the template.
Daily, Today, and Tomorrow require `summary` and a nonempty
`forecast_discussion` array. All generated-text schemas allow optional
`precipitation_timing` and `confidence`, and reject additional properties.
Weather truth remains in module outputs; GeneratedText is limited to prose
slots consumed by the template.
## Failure Behavior

View File

@@ -12,10 +12,12 @@ import (
const (
schemaIDHourly = "hourly"
schemaIDDaily = "daily"
schemaIDToday = "today"
schemaIDTomorrow = "tomorrow"
templateIDHourly = "hourly"
templateIDDaily = "daily"
templateIDToday = "today"
templateIDTomorrow = "tomorrow"
)
@@ -46,6 +48,11 @@ var catalog = []catalogEntry{
validate: validateHourly,
renderContextBuilder: buildHourlyContext,
},
{
schemaID: schemaIDDaily,
templateID: templateIDDaily,
validate: validateDaily,
},
{
schemaID: schemaIDToday,
templateID: templateIDToday,
@@ -142,6 +149,10 @@ func validateHourly(data []byte) (any, []byte, error) {
return ValidateHourly(data)
}
func validateDaily(data []byte) (any, []byte, error) {
return ValidateDaily(data)
}
func validateToday(data []byte) (any, []byte, error) {
return ValidateToday(data)
}

View File

@@ -119,6 +119,32 @@ func TestCatalogLookupSupportsTodayDefinition(t *testing.T) {
}
}
func TestCatalogLookupSupportsDailyDefinitionAssets(t *testing.T) {
definition := report.Definition{
ID: report.ID("daily"),
GenerationMode: report.GenerationModeGeneratedTextTemplate,
GeneratedTextSchemaID: "daily",
TemplateID: "daily",
}
handler, err := LookupDefinition(definition)
if err != nil {
t.Fatalf("LookupDefinition(daily) error = %v", err)
}
if handler.SchemaID() != "daily" || handler.TemplateID() != "daily" {
t.Fatalf("handler IDs = %q/%q, want daily/daily", handler.SchemaID(), handler.TemplateID())
}
if schema, err := handler.Schema(); err != nil {
t.Fatalf("Schema() error = %v", err)
} else if len(schema) == 0 {
t.Fatal("Schema() returned empty asset")
}
if template, err := handler.Template(); err != nil {
t.Fatalf("Template() error = %v", err)
} else if !strings.Contains(template, "# {{ .Report.Title }}") {
t.Fatalf("Template() = %q, want Daily template source", template)
}
}
func TestCatalogValidationDispatchSupportsKnownSchemas(t *testing.T) {
hourlyHandler, err := LookupDefinition(report.DefaultRegistry().MustLookup(report.Hourly))
if err != nil {
@@ -178,6 +204,29 @@ func TestCatalogValidationDispatchSupportsKnownSchemas(t *testing.T) {
if !strings.Contains(string(normalized), `"forecast_discussion":["A front will keep rain chances elevated."]`) {
t.Fatalf("today normalized text = %s, want trimmed discussion paragraph", normalized)
}
dailyHandler, err := LookupDefinition(report.Definition{
ID: report.ID("daily"),
GenerationMode: report.GenerationModeGeneratedTextTemplate,
GeneratedTextSchemaID: "daily",
TemplateID: "daily",
})
if err != nil {
t.Fatalf("LookupDefinition(daily) error = %v", err)
}
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. ", ""]
}`))
if err != nil {
t.Fatalf("Validate(daily) error = %v", err)
}
if _, ok := daily.(Daily); !ok {
t.Fatalf("daily generated text type = %T, want generatedtext.Daily", daily)
}
if !strings.Contains(string(normalized), `"forecast_discussion":["A front will keep rain chances in the forecast."]`) {
t.Fatalf("daily normalized text = %s, want trimmed discussion paragraph", normalized)
}
}
func TestCatalogBuildRenderContextRejectsMismatchedGeneratedText(t *testing.T) {
@@ -231,3 +280,25 @@ func TestCatalogBuildRenderContextRejectsMismatchedGeneratedText(t *testing.T) {
t.Fatalf("BuildRenderContext(today) error = %v, want today generated text requirement", err)
}
}
func TestCatalogBuildRenderContextRejectsDailyUntilContextExists(t *testing.T) {
handler, err := LookupDefinition(report.Definition{
ID: report.ID("daily"),
GenerationMode: report.GenerationModeGeneratedTextTemplate,
GeneratedTextSchemaID: "daily",
TemplateID: "daily",
})
if err != nil {
t.Fatalf("LookupDefinition(daily) error = %v", err)
}
_, err = handler.BuildRenderContext(testTomorrowMetadata(), testTomorrowSnapshot(t), testCollected(), testTomorrowDerived(), Daily{
Summary: "Showers are possible during the selected day.",
ForecastDiscussion: []string{"A front will keep rain chances in the forecast."},
})
if err == nil {
t.Fatal("BuildRenderContext(daily) error = nil, want missing builder")
}
if !strings.Contains(err.Error(), `render-context builder is not registered for template "daily"`) {
t.Fatalf("BuildRenderContext(daily) error = %v, want missing daily builder", err)
}
}

View File

@@ -0,0 +1,37 @@
package generatedtext
import (
"fmt"
"strings"
)
type Daily struct {
Summary string `json:"summary"`
ForecastDiscussion []string `json:"forecast_discussion"`
PrecipitationTiming string `json:"precipitation_timing,omitempty"`
Confidence string `json:"confidence,omitempty"`
}
func ValidateDaily(data []byte) (Daily, []byte, error) {
value, err := decodeGeneratedText[Daily](data, "daily")
if err != nil {
return Daily{}, nil, err
}
value.Summary = strings.TrimSpace(value.Summary)
value.PrecipitationTiming = strings.TrimSpace(value.PrecipitationTiming)
value.Confidence = strings.TrimSpace(value.Confidence)
value.ForecastDiscussion = trimNonEmpty(value.ForecastDiscussion)
if value.Summary == "" {
return Daily{}, nil, fmt.Errorf("daily generated text summary is required")
}
if len(value.ForecastDiscussion) == 0 {
return Daily{}, nil, fmt.Errorf("daily generated text forecast discussion is required")
}
normalized, err := normalizeGeneratedText(value, "daily")
if err != nil {
return Daily{}, nil, err
}
return value, normalized, nil
}

View File

@@ -0,0 +1,111 @@
package generatedtext
import (
"strings"
"testing"
)
func TestValidateDailyNormalizesJSON(t *testing.T) {
value, normalized, err := ValidateDaily([]byte(`{
"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 "
}`))
if err != nil {
t.Fatalf("ValidateDaily() error = %v", err)
}
if value.Summary != "Showers are possible during the selected day." {
t.Fatalf("Summary = %q, want trimmed summary", value.Summary)
}
if strings.Join(value.ForecastDiscussion, "|") != "A front will keep rain chances in the forecast.|Temperatures stay seasonable by afternoon." {
t.Fatalf("ForecastDiscussion = %#v, want trimmed non-empty paragraphs", value.ForecastDiscussion)
}
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"}`
if string(normalized) != want {
t.Fatalf("normalized = %s, want %s", normalized, want)
}
}
func TestValidateDailyOmitsEmptyOptionalFields(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": " "
}`))
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."]}`
if string(normalized) != want {
t.Fatalf("normalized = %s, want %s", normalized, want)
}
}
func TestValidateDailyRejectsInvalidInput(t *testing.T) {
tests := []struct {
name string
in string
want string
}{
{
name: "malformed",
in: `{`,
want: "decode daily generated text",
},
{
name: "unknown field",
in: `{"summary":"Showers are possible during the selected day.","forecast_discussion":["A front will keep rain chances in the forecast."],"extra":"value"}`,
want: `unknown field "extra"`,
},
{
name: "missing summary",
in: `{"forecast_discussion":["A front will keep rain chances in the forecast."]}`,
want: "summary is required",
},
{
name: "blank summary",
in: `{"summary":" ","forecast_discussion":["A front will keep rain chances in the forecast."]}`,
want: "summary is required",
},
{
name: "missing forecast discussion",
in: `{"summary":"Showers are possible during the selected day."}`,
want: "forecast discussion is required",
},
{
name: "blank forecast discussion",
in: `{"summary":"Showers are possible during the selected day.","forecast_discussion":[" ",""]}`,
want: "forecast discussion is required",
},
{
name: "forecast discussion wrong type",
in: `{"summary":"Showers are possible during the selected day.","forecast_discussion":"A front will keep rain chances in the forecast."}`,
want: "cannot unmarshal string",
},
{
name: "multiple values",
in: `{"summary":"Showers are possible during the selected day.","forecast_discussion":["A front will keep rain chances in the forecast."]} {}`,
want: "multiple JSON values",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
_, _, err := ValidateDaily([]byte(test.in))
if err == nil {
t.Fatal("ValidateDaily() error = nil, want error")
}
if !strings.Contains(err.Error(), test.want) {
t.Fatalf("ValidateDaily() error = %v, want %q", err, test.want)
}
})
}
}

View File

@@ -0,0 +1,38 @@
TASK: You are writing structured prose slots for a dated daily weather report.
The calling application will render the final Markdown report. Your job is not to write the full report. Return only a JSON object matching the configured schema.
Use only the supplied `data_package`. Do not invent weather details, times, hazards, probabilities, or impacts that are not supported by the data.
The report focuses on the selected local civil day in `report.valid_period` for the configured location.
Return these fields:
- `summary`: required. 1-2 sentences summarizing the main weather story for the selected day.
- `forecast_discussion`: required. 1 or more short paragraphs explaining the setup, timing, trend, or uncertainty most relevant to the selected day.
- `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.
Return JSON only.
# summary
Lead with the most practical local outcome for the selected day. If an active warning is relevant during the report period, lead with the hazard.
Mention the expected temperature character, precipitation risk, wind, visibility, heat, cold, or other hazards only when supported by the data package.
# forecast_discussion
Use deterministic module facts and narrative products to explain the most useful details for the selected day.
Useful context may include:
- timing of condition changes by daypart or hour
- boundaries, forcing, moisture, instability, or storm mode when supported
- active alerts or SPC outlooks that apply to the location
- planning concerns surfaced by `daily_planning`
- confidence or uncertainty
# precipitation_timing
Use 1-2 sentences to add practical precipitation context only when the data package contains deterministic precipitation windows. Include expected timing, type, intensity, duration, and uncertainty only when those details are supported.

View File

@@ -12,12 +12,14 @@ import (
var assets embed.FS
var templates = map[string]string{
"daily": "templates/daily.md.tmpl",
"hourly": "templates/hourly.md.tmpl",
"today": "templates/today.md.tmpl",
"tomorrow": "templates/tomorrow.md.tmpl",
}
var schemas = map[string]string{
"daily": "schemas/daily.generated_text.schema.json",
"hourly": "schemas/hourly.generated_text.schema.json",
"today": "schemas/today.generated_text.schema.json",
"tomorrow": "schemas/tomorrow.generated_text.schema.json",

View File

@@ -2,6 +2,7 @@ package reporttemplate
import (
"encoding/json"
"os"
"strings"
"testing"
)
@@ -30,6 +31,18 @@ func TestTomorrowTemplateLookup(t *testing.T) {
}
}
func TestDailyTemplateLookup(t *testing.T) {
source, err := Template("daily")
if err != nil {
t.Fatalf("Template() error = %v", err)
}
for _, want := range []string{"# {{ .Report.Title }}", "**Forecast date:**", "## Daypart Forecast", "## Precipitation Timing", "## Forecast Discussion"} {
if !strings.Contains(source, want) {
t.Fatalf("template missing %q:\n%s", want, source)
}
}
}
func TestTodayTemplateLookup(t *testing.T) {
source, err := Template("today")
if err != nil {
@@ -84,6 +97,37 @@ func TestTomorrowSchemaLookup(t *testing.T) {
}
}
func TestDailySchemaLookup(t *testing.T) {
data, err := Schema("daily")
if err != nil {
t.Fatalf("Schema() error = %v", err)
}
schema := assertSchema(t, data, "summary,forecast_discussion")
property, ok := schema.Properties["forecast_discussion"].(map[string]any)
if !ok {
t.Fatal("schema property forecast_discussion missing or invalid")
}
if property["type"] != "array" {
t.Fatalf("forecast_discussion type = %v, want array", property["type"])
}
if property["minItems"] != float64(1) {
t.Fatalf("forecast_discussion minItems = %v, want 1", property["minItems"])
}
items, ok := property["items"].(map[string]any)
if !ok || items["type"] != "string" {
t.Fatalf("forecast_discussion items = %#v, want string items", property["items"])
}
for _, field := range []string{"summary", "precipitation_timing", "confidence"} {
property, ok := schema.Properties[field].(map[string]any)
if !ok {
t.Fatalf("schema property %q missing or invalid", field)
}
if property["type"] != "string" {
t.Fatalf("schema property %q type = %v, want string", field, property["type"])
}
}
}
func TestTodaySchemaLookup(t *testing.T) {
data, err := Schema("today")
if err != nil {
@@ -351,6 +395,85 @@ func TestRenderTomorrow(t *testing.T) {
})
}
func TestRenderDaily(t *testing.T) {
rendered, err := Render("daily", testDailyRenderContext{
Report: testDailyReportContext{
Title: "Monday's Weather",
ForecastDateLabel: "Monday, June 15, 2026",
GeneratedAtLabel: "Sunday, June 14, 2026 at 9:14 AM",
},
GeneratedText: testDailyGeneratedText{
Summary: "The selected day starts dry before showers return later in the day.",
ForecastDiscussion: []string{
"Clouds increase after sunrise.",
"Rain chances peak during the afternoon.",
},
PrecipitationTiming: "A few showers may linger into early evening.",
},
Modules: testDailyModules{
Dayparts: []testDailyDaypart{
{
Key: "morning",
Summary: testDaypartSummary{
DisplayName: "Morning",
TemperatureTrend: "rising",
TemperatureStartPhraseF: "upper 50s",
TemperatureEndPhraseF: "upper 60s",
DominantConditionDisplay: "Sunny",
DominantConditionLower: "sunny",
},
},
{
Key: "afternoon",
Summary: testDaypartSummary{
DisplayName: "Afternoon",
TemperatureTrend: "steady",
TemperatureSteadyPhraseF: "upper 70s",
DominantConditionDisplay: "Showers",
DominantConditionLower: "showers",
MaxPopPercent: intPtr(70),
MaxPopTimeLabel: "3:00 PM",
MentionPrecipitation: true,
},
},
},
PrecipTiming: &testPrecipTiming{
PrecipitationWindows: []testPrecipWindow{
{PeriodBeginsHourLabel: "3:00 PM", PeriodEndsHourLabel: "6:00 PM", MaxPopPercent: intPtr(70), MaxPopHourLabel: "3:00 PM"},
},
},
},
})
if err != nil {
t.Fatalf("Render() error = %v", err)
}
text := string(rendered)
for _, want := range []string{
"# Monday's Weather",
"**Forecast date:** Monday, June 15, 2026",
"**Updated:** Sunday, June 14, 2026 at 9:14 AM",
"The selected day starts dry before showers return later in the day.",
"- **Morning:** Sunny, with temperatures rising from the upper 50s to the upper 60s.",
"- **Afternoon:** Showers, with temperatures in the upper 70s. Chance of precipitation is 70%.",
"- **3:00 PM** to **6:00 PM**: Precipitation is expected during this period. The peak precipitation chance is 70% at 3:00 PM.",
"A few showers may linger into early evening.",
"Clouds increase after sunrise.",
"Rain chances peak during the afternoon.",
} {
if !strings.Contains(text, want) {
t.Fatalf("rendered template missing %q:\n%s", want, text)
}
}
assertOrderedText(t, text, []string{
"# Monday's Weather",
"## Daypart Forecast",
"- **Morning:**",
"- **Afternoon:**",
"## Precipitation Timing",
"## Forecast Discussion",
})
}
func TestRenderToday(t *testing.T) {
rendered, err := Render("today", testTodayRenderContext{
Report: testTodayReportContext{
@@ -532,17 +655,29 @@ func TestRenderHourlyOmitsConditionalSectionsForClearWeather(t *testing.T) {
}
func TestUnknownAssetsReturnActionableErrors(t *testing.T) {
if _, err := Template("daily"); err == nil || !strings.Contains(err.Error(), `unknown report template "daily"`) {
if _, err := Template("missing"); err == nil || !strings.Contains(err.Error(), `unknown report template "missing"`) {
t.Fatalf("Template() error = %v, want unknown template", err)
}
if _, err := Schema("daily"); err == nil || !strings.Contains(err.Error(), `unknown generated text schema "daily"`) {
if _, err := Schema("missing"); err == nil || !strings.Contains(err.Error(), `unknown generated text schema "missing"`) {
t.Fatalf("Schema() error = %v, want unknown schema", err)
}
if _, err := Render("daily", testRenderContext{}); err == nil || !strings.Contains(err.Error(), `unknown report template "daily"`) {
if _, err := Render("missing", testRenderContext{}); err == nil || !strings.Contains(err.Error(), `unknown report template "missing"`) {
t.Fatalf("Render() error = %v, want unknown template", err)
}
}
func TestDailyPromptAssetExists(t *testing.T) {
data, err := os.ReadFile("prompts/daily.generated_text.md")
if err != nil {
t.Fatalf("read Daily prompt asset: %v", err)
}
for _, want := range []string{"TASK:", "`summary`", "`forecast_discussion`", "`daily_planning`"} {
if !strings.Contains(string(data), want) {
t.Fatalf("Daily prompt asset missing %q:\n%s", want, string(data))
}
}
}
func TestRenderFailsForMissingContextFields(t *testing.T) {
_, err := Render("hourly", map[string]any{"Report": map[string]any{"Title": "Hourly Report"}})
if err == nil {
@@ -571,6 +706,12 @@ type testTodayRenderContext struct {
Modules testTodayModules
}
type testDailyRenderContext struct {
Report testDailyReportContext
GeneratedText testDailyGeneratedText
Modules testDailyModules
}
type testReportContext struct {
Title string
LocationName string
@@ -597,6 +738,12 @@ type testTodayReportContext struct {
GeneratedAtLabel string
}
type testDailyReportContext struct {
Title string
ForecastDateLabel string
GeneratedAtLabel string
}
type testTomorrowGeneratedText struct {
Summary string
ForecastDiscussion []string
@@ -604,6 +751,13 @@ type testTomorrowGeneratedText struct {
Confidence string
}
type testDailyGeneratedText struct {
Summary string
ForecastDiscussion []string
PrecipitationTiming string
Confidence string
}
type testModules struct {
CurrentConditions *testCurrentConditions
HourlyForecast *testHourlyForecast
@@ -627,6 +781,11 @@ type testTodayModules struct {
TodayPlanning *testTodayPlanning
}
type testDailyModules struct {
Dayparts []testDailyDaypart
PrecipTiming *testPrecipTiming
}
type testTodayPlanning struct {
MorningReadiness []string
CommuteSchoolWorkdayConcerns []string
@@ -639,6 +798,11 @@ type testTomorrowDaypart struct {
Summary testDaypartSummary
}
type testDailyDaypart struct {
Key string
Summary testDaypartSummary
}
type testDaypartSummary struct {
DisplayName string
TempRangeF string

View File

@@ -0,0 +1,29 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "weatherreporter.daily.generated_text.schema.json",
"title": "Daily GeneratedText",
"type": "object",
"additionalProperties": false,
"required": [
"summary",
"forecast_discussion"
],
"properties": {
"summary": {
"type": "string"
},
"forecast_discussion": {
"type": "array",
"items": {
"type": "string"
},
"minItems": 1
},
"precipitation_timing": {
"type": "string"
},
"confidence": {
"type": "string"
}
}
}

View File

@@ -0,0 +1,24 @@
# {{ .Report.Title }}
**Forecast date:** {{ .Report.ForecastDateLabel }}
**Updated:** {{ .Report.GeneratedAtLabel }}
{{ .GeneratedText.Summary }}
## Daypart Forecast
{{ with .Modules.Dayparts }}{{ range . }}{{ $daypart := . }}- **{{ if .Summary.DisplayName }}{{ .Summary.DisplayName }}{{ else }}{{ .Key }}{{ end }}:** {{ with .Summary.DominantConditionDisplay }}{{ . }}{{ else }}{{ with .Summary.DominantCondition }}{{ . }}{{ else }}Forecast details are limited{{ end }}{{ end }}{{ if eq .Summary.TemperatureTrend "rising" }}{{ with .Summary.TemperatureStartPhraseF }}, with temperatures rising from the {{ . }}{{ with $daypart.Summary.TemperatureEndPhraseF }} to the {{ . }}{{ end }}{{ end }}{{ else if eq .Summary.TemperatureTrend "falling" }}{{ with .Summary.TemperatureStartPhraseF }}, with temperatures falling from the {{ . }}{{ with $daypart.Summary.TemperatureEndPhraseF }} to the {{ . }}{{ end }}{{ end }}{{ else if eq .Summary.TemperatureTrend "peaking" }}{{ with .Summary.TemperaturePeakPhraseF }}, with temperatures peaking in the {{ . }}{{ end }}{{ else }}{{ with .Summary.TemperatureSteadyPhraseF }}, with temperatures in the {{ . }}{{ else }}{{ with .Summary.TemperaturePhraseF }}, with temperatures in the {{ . }}{{ end }}{{ end }}{{ end }}.{{ if .Summary.MentionPrecipitation }}{{ with .Summary.MaxPopPercent }} Chance of precipitation is {{ . }}%.{{ end }}{{ end }}
{{ end }}{{ else }}- No daypart forecast details are available.
{{ end }}
{{ with .Modules.PrecipTiming }}{{ with .PrecipitationWindows }}
## Precipitation Timing
{{ range . }}{{ $window := . }}
- **{{ if .PeriodBeginsHourLabel }}{{ .PeriodBeginsHourLabel }}{{ else }}{{ .PeriodBegins }}{{ end }}**{{ with .PeriodEndsHourLabel }} to **{{ . }}**{{ else }}{{ with .PeriodEnds }} to **{{ . }}**{{ end }}{{ end }}: Precipitation is expected during this period.{{ with .MaxPopPercent }} The peak precipitation chance is {{ . }}%{{ with $window.MaxPopHourLabel }} at {{ . }}{{ else }}{{ with $window.MaxPopTime }} at {{ . }}{{ end }}{{ end }}.{{ end }}
{{ end }}{{ with $.GeneratedText.PrecipitationTiming }}
{{ . }}
{{ end }}
{{ end }}{{ end }}
## Forecast Discussion
{{ range .GeneratedText.ForecastDiscussion }}
{{ . }}
{{ end }}