# Report Templates ## Purpose This guide describes the implemented Markdown report template surface for `weatherreporter`. It is for maintainers editing embedded report templates, especially generated-text-template reports. Templates are Go `text/template` files. The current implemented templates are: - `internal/reporttemplate/templates/daily.md.tmpl` - `internal/reporttemplate/templates/today.md.tmpl` - `internal/reporttemplate/templates/tomorrow.md.tmpl` - `internal/reporttemplate/templates/hourly.md.tmpl` Templates are rendered from structured contexts such as `DailyRenderContext`, `TodayRenderContext`, `TomorrowRenderContext`, and `HourlyRenderContext`. Weather data collection, derivation, module execution, generated text validation, and artifact paths are handled before template rendering. ## Editing Rules - Use Go `text/template` syntax. - Keep templates focused on Markdown layout, headings, ordering, and simple conditional display. - Do not put weather derivation, source selection, or path construction logic in templates. - Missing template keys are errors. A misspelled variable will fail rendering. - No custom template functions are currently registered. - Optional module stanzas are pointers and should be guarded with `{{ with .Modules.WeatherStory }}...{{ end }}`. - Slices can be rendered with `{{ range .Items }}...{{ else }}...{{ end }}`. ## Hourly Context The hourly template receives five top-level values: | Variable | Type | Description | | --- | --- | --- | | `.Report` | HourlyReportContext | Display metadata and friendly labels for the rendered report. | | `.GeneratedText` | Hourly | Structured text returned by Scriptorium. | | `.Modules` | HourlyTemplateModules | Preferred deterministic template surface, keyed by module purpose. | | `.Collected` | facts.CollectedFacts | Normalized upstream facts for advanced template use. | | `.Derived` | facts.DerivedFacts | Shared derived facts for advanced template use. | Prefer `.Modules` for normal template edits. `.Collected` and `.Derived` are available when a template needs lower-level facts, but templates should still avoid nontrivial derivation. ## Report | Variable | Type | Description | | --- | --- | --- | | `.Report.Title` | string | Display title. Currently `Hourly Report`. | | `.Report.LocationName` | string | Prompt/report location label, such as `Brentwood, MO`. | | `.Report.GeneratedAt` | time.Time | Canonical generation timestamp. | | `.Report.GeneratedAtLabel` | string | Friendly local generation time label. | | `.Report.ValidPeriod` | timeutil.Period | Canonical valid period. | | `.Report.ValidPeriodLabel` | string | Friendly local valid period label, such as `2026-05-29 at 8:30 AM to 2026-05-29 at 2:30 PM`. | | `.Report.Timezone` | string | Effective report timezone. | ## GeneratedText These fields are written by Scriptorium as structured JSON, validated by weatherreporter, and then inserted into the render context. | Variable | Type | Description | | --- | --- | --- | | `.GeneratedText.Summary` | string | Required short prose summary. | | `.GeneratedText.ForecastDiscussion` | string | Required prose for the Forecast Discussion section. | | `.GeneratedText.PrecipitationTiming` | string | Optional prose rendered after deterministic precipitation windows. | | `.GeneratedText.Confidence` | string | Optional confidence or uncertainty note. Empty when omitted by the LLM; not rendered by the current hourly template. | Example: ```gotemplate {{ .GeneratedText.Summary }} ## Forecast Discussion {{ .GeneratedText.ForecastDiscussion }} ``` ## Tomorrow Context The Tomorrow template receives five top-level values: | Variable | Type | Description | | --- | --- | --- | | `.Report` | TomorrowReportContext | Display metadata and friendly labels for the rendered report. | | `.GeneratedText` | Tomorrow | Structured text returned by Scriptorium. | | `.Modules` | TomorrowTemplateModules | Preferred deterministic template surface, keyed by module purpose. | | `.Collected` | facts.CollectedFacts | Normalized upstream facts for advanced template use. | | `.Derived` | facts.DerivedFacts | Shared derived facts for advanced template use. | Tomorrow report metadata includes `.Report.Title`, `.Report.ForecastDate`, `.Report.ForecastDateLabel`, `.Report.ForecastDayName`, `.Report.GeneratedAt`, `.Report.GeneratedAtLabel`, `.Report.ValidPeriod`, and `.Report.Timezone`. Tomorrow generated text uses the same `.GeneratedText.Summary`, `.GeneratedText.PrecipitationTiming`, and `.GeneratedText.Confidence` fields as Hourly. `.GeneratedText.ForecastDiscussion` is a slice of paragraphs and should be rendered with `range`. Tomorrow modules include the Hourly module fields plus: | Variable | Type | Description | | --- | --- | --- | | `.Modules.DerivedDailySummary` | *briefing.DerivedDailySummaryModule | Daily summary facts for the forecast date. | | `.Modules.DerivedDaypartSummaries` | *map[string]briefing.DerivedDaypartSummaryModule | Raw daypart summary map, when direct keyed access is needed. | | `.Modules.Dayparts` | []generatedtext.TomorrowDaypartContext | Ordered daypart summaries for deterministic template rendering. | | `.Modules.TomorrowPlanning` | *briefing.TomorrowPlanningModule | Planning facts for the next local civil day. | Prefer `.Modules.Dayparts` over ranging through `.Modules.DerivedDaypartSummaries`; it follows configured daypart order and falls back to sorted keys for any unmatched entries. ## Daily Context The Daily template receives the same five top-level values as Tomorrow, using `DailyReportContext`, `Daily`, and `DailyTemplateModules`. Daily report metadata includes `.Report.Title`, `.Report.ForecastDate`, `.Report.ForecastDateLabel`, `.Report.ForecastDayName`, `.Report.GeneratedAt`, `.Report.GeneratedAtLabel`, `.Report.ValidPeriod`, and `.Report.Timezone`. Daily generated text uses `.GeneratedText.Summary`, `.GeneratedText.ForecastDiscussion`, `.GeneratedText.PrecipitationTiming`, and `.GeneratedText.Confidence`. Forecast discussion is a slice of paragraphs and should be rendered with `range`. Daily uses template ID `daily`, generated-text schema ID `daily`, and prompt source `internal/reporttemplate/prompts/daily.generated_text.md`. Daily modules include the Hourly module fields plus: | Variable | Type | Description | | --- | --- | --- | | `.Modules.DerivedDailySummary` | *briefing.DerivedDailySummaryModule | Daily summary facts for the forecast date. | | `.Modules.DerivedDaypartSummaries` | *map[string]briefing.DerivedDaypartSummaryModule | Raw daypart summary map, when direct keyed access is needed. | | `.Modules.Dayparts` | []generatedtext.DailyDaypartContext | Ordered daypart summaries for deterministic template rendering. | | `.Modules.DailyPlanning` | *briefing.DailyPlanningModule | Planning facts for the selected local civil day. | Prefer `.Modules.Dayparts` over ranging through `.Modules.DerivedDaypartSummaries`; it follows configured daypart order and falls back to sorted keys for any unmatched entries. ## Today Context The Today template receives the same five top-level values as Tomorrow, using `TodayReportContext`, `Today`, and `TodayTemplateModules`. Today report metadata includes `.Report.Title`, `.Report.ForecastDate`, `.Report.ForecastDateLabel`, `.Report.ForecastDayName`, `.Report.GeneratedAt`, `.Report.GeneratedAtLabel`, `.Report.ValidPeriod`, and `.Report.Timezone`. Today generated text uses `.GeneratedText.Summary`, `.GeneratedText.ForecastDiscussion`, `.GeneratedText.PrecipitationTiming`, and `.GeneratedText.Confidence`. Forecast discussion is a slice of paragraphs and should be rendered with `range`. Today uses template ID `today`, generated-text schema ID `today`, and prompt source `internal/reporttemplate/prompts/today.generated_text.md`. Today modules include the Hourly module fields plus: | Variable | Type | Description | | --- | --- | --- | | `.Modules.DerivedDailySummary` | *briefing.DerivedDailySummaryModule | Daily summary facts for the forecast date. | | `.Modules.DerivedDaypartSummaries` | *map[string]briefing.DerivedDaypartSummaryModule | Raw daypart summary map, when direct keyed access is needed. | | `.Modules.Dayparts` | []generatedtext.TodayDaypartContext | Ordered daypart summaries for deterministic template rendering. | | `.Modules.TodayPlanning` | *briefing.TodayPlanningModule | Planning facts for the current local civil day. | Prefer `.Modules.Dayparts` over ranging through `.Modules.DerivedDaypartSummaries`; it follows configured daypart order and falls back to sorted keys for any unmatched entries. ## Modules `.Modules` exposes typed outputs from the same module pipeline used for the prompt data package. Module fields are pointers because missing-data policy may omit a stanza. | Variable | Type | Description | | --- | --- | --- | | `.Modules.Metadata` | *briefing.MetadataModule | Report metadata module output, when present. | | `.Modules.CurrentConditions` | *briefing.CurrentConditionsModule | Current conditions from `/conditions/current`. | | `.Modules.HourlyForecast` | *briefing.HourlyForecastModule | Hourly forecast periods overlapping the report valid period. | | `.Modules.PrecipTiming` | *briefing.PrecipTimingModule | Derived precipitation timing facts and threshold windows. | | `.Modules.AlertDigest` | *briefing.AlertDigestModule | Active alert status and relevant alert overlaps. | | `.Modules.SPCConvectiveOutlooks` | *briefing.SPCConvectiveOutlooksModule | SPC outlooks that overlap the report valid period. | | `.Modules.AreaForecastDiscussion` | *briefing.AreaForecastDiscussionModule | AFD key messages and configured discussion sections. | | `.Modules.SPCConvectiveDiscussion` | *briefing.SPCConvectiveDiscussionModule | SPC discussions retained for qualifying overlapping categorical risk days. | | `.Modules.WeatherStory` | *briefing.WeatherStoryModule | Latest NWS weather story, when available. | ### Current Conditions Common fields: | Variable | Type | Description | | --- | --- | --- | | `.Modules.CurrentConditions.ConditionText` | string | Current condition text. | | `.Modules.CurrentConditions.ConditionTextLower` | string | Lower-case current condition text for inline sentences. | | `.Modules.CurrentConditions.TemperatureF` | *int | Rounded current temperature. | | `.Modules.CurrentConditions.ApparentTemperatureF` | *int | Rounded apparent temperature. | | `.Modules.CurrentConditions.RelativeHumidityPercent` | *int | Rounded relative humidity. | | `.Modules.CurrentConditions.WindDirection` | string | 16-point compass wind direction. | | `.Modules.CurrentConditions.WindDirectionText` | string | Lower-case full wind direction text, such as `northwest`. | | `.Modules.CurrentConditions.WindSpeedMph` | *int | Rounded wind speed. | Example: ```gotemplate {{ with .Modules.CurrentConditions }} {{ .ConditionText }}{{ with .TemperatureF }}; {{ . }} F{{ end }}{{ with .WindDirection }}; wind {{ . }}{{ end }}{{ with .WindSpeedMph }} {{ . }} mph{{ end }} {{ else }} No current conditions available. {{ end }} ``` ### Hourly Forecast Common period fields: | Variable | Type | Description | | --- | --- | --- | | `.Modules.HourlyForecast.Periods` | []briefing.HourlyForecastPeriod | Ordered periods for the hourly report valid period. | | `.Modules.HourlyForecast.Periods[].HourLabel` | string | Friendly hour label such as `4:00 PM`. | | `.Modules.HourlyForecast.Periods[].PeriodBegins` | string | Friendly local period start label. | | `.Modules.HourlyForecast.Periods[].PeriodEnds` | string | Friendly local period end label. | | `.Modules.HourlyForecast.Periods[].Name` | string | Source period name. | | `.Modules.HourlyForecast.Periods[].TextDescription` | string | Hourly forecast text. | | `.Modules.HourlyForecast.Periods[].TextDescriptionLower` | string | Lower-case hourly forecast text for inline sentences. | | `.Modules.HourlyForecast.Periods[].TemperatureF` | *float64 | Forecast temperature. | | `.Modules.HourlyForecast.Periods[].ProbabilityOfPrecipitationPercent` | *float64 | Forecast precipitation probability. | | `.Modules.HourlyForecast.Periods[].MentionPrecipitation` | bool | True when precipitation probability meets the hourly mention threshold. | | `.Modules.HourlyForecast.Periods[].WindDirection` | string | 16-point compass wind direction. | | `.Modules.HourlyForecast.Periods[].WindSpeedMph` | *float64 | Wind speed. | | `.Modules.HourlyForecast.Periods[].WindGustMph` | *float64 | Wind gust. | Example: ```gotemplate {{ with .Modules.HourlyForecast }}{{ range .Periods }} - **{{ .HourLabel }}:**{{ with .TemperatureF }} {{ . }}°F{{ end }} and {{ .TextDescriptionLower }}.{{ if .MentionPrecipitation }}{{ with .ProbabilityOfPrecipitationPercent }} Probability of precipitation is {{ . }}%.{{ end }}{{ end }} {{ else }} - No hourly forecast rows available. {{ end }}{{ end }} ``` ### Precipitation Timing Common fields: | Variable | Type | Description | | --- | --- | --- | | `.Modules.PrecipTiming.MaxPopPercent` | *int | Highest hourly precipitation probability in the valid period. | | `.Modules.PrecipTiming.MaxPopTime` | string | Friendly local time for the highest hourly precipitation probability. | | `.Modules.PrecipTiming.ProbabilityThreshold` | float64 | Threshold used to define precipitation windows. | | `.Modules.PrecipTiming.PrecipitationWindows` | []briefing.PrecipitationWindowModule | One or more threshold precipitation windows. | | `.Modules.PrecipTiming.PrecipitationWindows[].PeriodBegins` | string | Friendly local window start. | | `.Modules.PrecipTiming.PrecipitationWindows[].PeriodBeginsHourLabel` | string | Friendly window start hour, such as `4:00 PM`. | | `.Modules.PrecipTiming.PrecipitationWindows[].PeriodEnds` | string | Friendly local window end; omitted for open windows. | | `.Modules.PrecipTiming.PrecipitationWindows[].PeriodEndsHourLabel` | string | Friendly window end hour; omitted for open windows. | | `.Modules.PrecipTiming.PrecipitationWindows[].MaxPopPercent` | *int | Highest precipitation probability inside the window. | | `.Modules.PrecipTiming.PrecipitationWindows[].MaxPopTime` | string | Friendly local time for the window maximum. | | `.Modules.PrecipTiming.PrecipitationWindows[].MaxPopHourLabel` | string | Friendly hour label for the window maximum. | | `.Modules.PrecipTiming.ThunderMentioned` | bool | Whether thunder is mentioned in the forecast text. | ### Alert Digest | Variable | Type | Description | | --- | --- | --- | | `.Modules.AlertDigest.Checked` | bool | Whether alert data was checked successfully. | | `.Modules.AlertDigest.ActiveCount` | int | Active alert count from the source. | | `.Modules.AlertDigest.RelevantCount` | int | Alert count overlapping the report period. | | `.Modules.AlertDigest.Missing` | bool | True when alert data is unavailable. | | `.Modules.AlertDigest.Relevant` | []briefing.AlertSummary | Relevant alert summaries. | | `.Modules.AlertDigest.Relevant[].Event` | string | Alert event name. | | `.Modules.AlertDigest.Relevant[].Headline` | string | Alert headline. | | `.Modules.AlertDigest.Relevant[].Severity` | string | Alert severity. | ### SPC Outlooks And Discussion | Variable | Type | Description | | --- | --- | --- | | `.Modules.SPCConvectiveOutlooks.Checked` | bool | Whether SPC outlook data was checked successfully. | | `.Modules.SPCConvectiveOutlooks.AsOf` | string | Friendly source as-of time. | | `.Modules.SPCConvectiveOutlooks.IssuedAt` | string | Friendly source issue time. | | `.Modules.SPCConvectiveOutlooks.Outlooks` | []briefing.SPCConvectiveOutlookRecord | Overlapping outlook records. | | `.Modules.SPCConvectiveOutlooks.Outlooks[].Day` | int | SPC day number. | | `.Modules.SPCConvectiveOutlooks.Outlooks[].OutlookType` | string | Outlook type, such as `categorical`. | | `.Modules.SPCConvectiveOutlooks.Outlooks[].Label` | string | Short outlook label. | | `.Modules.SPCConvectiveOutlooks.Outlooks[].LabelText` | string | Human-readable outlook label. | | `.Modules.SPCConvectiveOutlooks.Outlooks[].PeriodBegins` | string | Friendly outlook period start. | | `.Modules.SPCConvectiveOutlooks.Outlooks[].PeriodEnds` | string | Friendly outlook period end. | | `.Modules.SPCConvectiveOutlooks.Outlooks[].ImageURL` | string | Source image URL. | | `.Modules.SPCConvectiveDiscussion.IncludedBecause` | string | Criterion used to include discussions. | | `.Modules.SPCConvectiveDiscussion.Discussions` | []briefing.SPCConvectiveDiscussionRecord | Retained discussion records. | | `.Modules.SPCConvectiveDiscussion.Discussions[].Headline` | string | Discussion headline. | | `.Modules.SPCConvectiveDiscussion.Discussions[].Summary` | string | Discussion summary. | | `.Modules.SPCConvectiveDiscussion.Discussions[].Discussion` | string | Full discussion text. | ### Area Forecast Discussion | Variable | Type | Description | | --- | --- | --- | | `.Modules.AreaForecastDiscussion.Product` | string | Source product identifier. | | `.Modules.AreaForecastDiscussion.KeyMessages` | []string | AFD key messages. | | `.Modules.AreaForecastDiscussion.ShortTerm` | string | AFD short-term section text. | | `.Modules.AreaForecastDiscussion.LongTerm` | string | AFD long-term section text. | ### Weather Story | Variable | Type | Description | | --- | --- | --- | | `.Modules.WeatherStory.Available` | bool | True when a story is available. | | `.Modules.WeatherStory.OfficeID` | string | Source office ID. | | `.Modules.WeatherStory.PeriodBegins` | string | Friendly story period start. | | `.Modules.WeatherStory.PeriodEnds` | string | Friendly story period end. | | `.Modules.WeatherStory.UpdatedAt` | *time.Time | Canonical update timestamp. | | `.Modules.WeatherStory.Title` | string | Story title. | | `.Modules.WeatherStory.Description` | string | Story description. | | `.Modules.WeatherStory.AltText` | string | Story image alt text. | | `.Modules.WeatherStory.Priority` | bool | Source priority flag. | | `.Modules.WeatherStory.Order` | int | Source order. | | `.Modules.WeatherStory.DownloadURL` | string | Source download URL. | ## Collected And Derived Facts The template also receives the full `facts.CollectedFacts` and `facts.DerivedFacts` structs: - `.Collected` contains normalized source data and provenance from upstream Weather API fetches. - `.Derived` contains shared slices and calculations used across modules, such as valid-period hourly periods, precipitation timing, alert overlaps, and SPC filtering inputs. These values are intentionally lower-level than `.Modules`. Use them when a template needs a specific field that is not exposed by a module, but keep calculation-heavy changes in Go. ## Validation After editing a template, run: ```bash go test ./internal/reporttemplate ./internal/generatedtext ./internal/app ``` For a full check, run: ```bash go test ./... go run ./cmd/weatherreporter --help git diff --check ``` Template render tests exercise the Daily, Today, Tomorrow, and Hourly templates through `internal/generatedtext/render_context_test.go` and `internal/reporttemplate/reporttemplate_test.go`.