Refactor the template variable framework

This commit is contained in:
2026-06-14 08:57:53 -05:00
parent bb8de054dc
commit 28b8391d53
8 changed files with 512 additions and 552 deletions

View File

@@ -10,38 +10,52 @@ Templates are Go `text/template` files. The current implemented template is:
- `internal/reporttemplate/templates/hourly.md.tmpl`
The hourly template is rendered from a curated `HourlyRenderContext`, not from
the raw prompt data package. Weather data selection, derivation, module
execution, LLM generation, and artifact paths are handled before template
rendering.
The hourly template is rendered from a structured `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.
- 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 strings can be guarded with `{{ with .Field }}...{{ end }}`.
- Optional module stanzas are pointers and should be guarded with
`{{ with .Modules.WeatherStory }}...{{ end }}`.
- Slices can be rendered with `{{ range .Items }}...{{ else }}...{{ end }}`.
## Hourly Template Variables
## Hourly Context
These are the complete variables currently available to
`internal/reporttemplate/templates/hourly.md.tmpl`.
### Report Metadata
The hourly template receives five top-level values:
| Variable | Type | Description |
| --- | --- | --- |
| `.ReportTitle` | string | Display title for the report. Currently `Hourly Report`. |
| `.LocationName` | string | Prompt/report location label, such as `Brentwood, MO`. Falls back to source location if configured location metadata is unavailable. |
| `.ValidPeriod` | string | Friendly local valid period label, such as `2026-05-29 at 8:30 AM to 2026-05-29 at 2:30 PM`. |
| `.GeneratedAt` | string | Friendly local generation time label. |
| `.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. |
### GeneratedText
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.
@@ -67,150 +81,163 @@ Example:
{{ end }}
```
### Current Conditions
## 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 |
| --- | --- | --- |
| `.CurrentConditions` | string | Deterministic one-line current conditions summary. May include condition text, temperature, apparent temperature, humidity, and wind. |
| `.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. |
Example value:
### Current Conditions
```text
Partly cloudy; 74 F; feels like 76 F; humidity 71%; wind S 8 mph.
Common fields:
| Variable | Type | Description |
| --- | --- | --- |
| `.Modules.CurrentConditions.ConditionText` | string | Current condition text. |
| `.Modules.CurrentConditions.TemperatureF` | *float64 | Current temperature. |
| `.Modules.CurrentConditions.ApparentTemperatureF` | *float64 | Apparent temperature. |
| `.Modules.CurrentConditions.RelativeHumidityPercent` | *float64 | Relative humidity. |
| `.Modules.CurrentConditions.WindDirection` | string | 16-point compass wind direction. |
| `.Modules.CurrentConditions.WindSpeedMph` | *float64 | 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 |
| --- | --- | --- |
| `.HourlyForecast` | []HourlyForecastRow | Ordered rows for the hourly report valid period. |
| `.HourlyForecast[].Time` | string | Friendly local period start time, or the source period name if no start label is available. |
| `.HourlyForecast[].Summary` | string | Hourly text description, falling back to the period name. |
| `.HourlyForecast[].Temperature` | string | Rounded temperature label such as `75 F`, or empty. |
| `.HourlyForecast[].Precipitation` | string | Rounded precipitation probability label such as `70% precipitation`, or empty. |
| `.HourlyForecast[].Wind` | string | Wind label such as `wind S 10 mph, gusts 18 mph`, or empty. |
| `.Modules.HourlyForecast.Periods` | []briefing.HourlyForecastPeriod | Ordered periods for the hourly report valid period. |
| `.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[].TemperatureF` | *float64 | Forecast temperature. |
| `.Modules.HourlyForecast.Periods[].ProbabilityOfPrecipitationPercent` | *float64 | Forecast precipitation probability. |
| `.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
## Hourly Forecast
{{ range .HourlyForecast }}
- {{ .Time }}: {{ .Summary }}{{ with .Temperature }}; {{ . }}{{ end }}{{ with .Precipitation }}; {{ . }}{{ end }}{{ with .Wind }}; {{ . }}{{ end }}
{{ with .Modules.HourlyForecast }}{{ range .Periods }}
- {{ .PeriodBegins }}: {{ .TextDescription }}{{ with .TemperatureF }}; {{ . }} F{{ end }}{{ with .ProbabilityOfPrecipitationPercent }}; {{ . }}% precipitation{{ end }}
{{ else }}
- No hourly forecast rows available.
{{ end }}
{{ end }}{{ end }}
```
### Precipitation Timing
| Variable | Type | Description |
| --- | --- | --- |
| `.PrecipitationTiming` | string | Deterministic precipitation timing summary. May include peak probability, precipitation windows, and thunder mention. |
Example value:
```text
Peak precipitation probability 70% at 10 AM; 2026-05-29 at 10:00 AM to 2026-05-29 at 12:00 PM (max 70% at 10 AM); Thunder is mentioned in the forecast.
```
### Alerts
Common fields:
| Variable | Type | Description |
| --- | --- | --- |
| `.Alerts` | []string | Alert labels for active alerts overlapping the report period. Empty when there are no relevant alert overlaps. |
| `.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[].PeriodEnds` | string | Friendly local window end; 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.ThunderMentioned` | bool | Whether thunder is mentioned in the forecast text. |
Example:
```gotemplate
## Alerts
{{ range .Alerts }}
- {{ . }}
{{ else }}
- No active alert overlaps for this report period.
{{ end }}
```
### SPC Outlooks
### Alert Digest
| Variable | Type | Description |
| --- | --- | --- |
| `.SPCOutlooks` | []string | SPC convective outlook labels overlapping the report period. Empty when there are no overlapping outlooks. |
| `.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. |
Example value:
```text
Slight Risk from 2026-05-29 at 7:00 AM to 2026-05-29 at 3:00 PM
```
### Forecast Discussion
### SPC Outlooks And Discussion
| Variable | Type | Description |
| --- | --- | --- |
| `.ForecastDiscussion.KeyMessages` | []string | AFD key messages included for the hourly report. |
| `.ForecastDiscussion.ShortTerm` | string | AFD short-term discussion text, or empty if unavailable. |
| `.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. |
Example:
```gotemplate
## Forecast Discussion
{{ range .ForecastDiscussion.KeyMessages }}
- {{ . }}
{{ end }}{{ with .ForecastDiscussion.ShortTerm }}
{{ . }}
{{ end }}
```
### SPC Discussion
### Area Forecast Discussion
| Variable | Type | Description |
| --- | --- | --- |
| `.SPCDiscussions` | []string | SPC discussion labels retained for overlapping qualifying outlook periods. Empty when there is no relevant SPC discussion. |
Example:
```gotemplate
## SPC Discussion
{{ range .SPCDiscussions }}
- {{ . }}
{{ else }}
- No overlapping SPC discussion.
{{ end }}
```
| `.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 |
| --- | --- | --- |
| `.WeatherStory` | string | Weather story title and description, or `No weather story available.` |
| `.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. |
## Common Patterns
## Collected And Derived Facts
Use `with` for optional strings:
The template also receives the full `facts.CollectedFacts` and
`facts.DerivedFacts` structs:
```gotemplate
{{ with .GeneratedText.Confidence }}
## Confidence
- `.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.
{{ . }}
{{ end }}
```
Use `range` with `else` for optional lists:
```gotemplate
{{ range .SPCOutlooks }}
- {{ . }}
{{ else }}
- No overlapping SPC outlooks.
{{ end }}
```
Keep punctuation outside optional blocks when possible:
```gotemplate
- {{ .Time }}: {{ .Summary }}{{ with .Temperature }}; {{ . }}{{ end }}
```
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
@@ -229,4 +256,5 @@ git diff --check
```
Template render tests currently exercise the hourly template through
`internal/generatedtext/render_context_test.go`.
`internal/generatedtext/render_context_test.go` and
`internal/reporttemplate/reporttemplate_test.go`.