Update documentation for templated reports

This commit is contained in:
2026-06-14 08:22:09 -05:00
parent 9d6502460e
commit bb8de054dc
4 changed files with 274 additions and 1124 deletions

View File

@@ -1,342 +0,0 @@
# GeneratedText Report Rendering Roadmap
## Purpose
This roadmap defines planned work to move selected reports from full Markdown
LLM generation to deterministic Markdown rendering with structured LLM-filled
text slots. The first target report is the rolling next-hours report, named
`hourly`.
This feature is not implemented yet, so this document lives under
`docs/roadmap/`.
## Intent
The current module and prompt-input system has reached the point where many
report facts can be calculated deterministically. The next step is to let Go
own report structure while using the LLM for only the prose that benefits from
language synthesis.
The intended workflow is:
1. collect upstream facts once;
2. derive deterministic report facts;
3. build module outputs and the prompt data package;
4. ask Scriptorium for structured JSON using the schema associated with the
prompt ID on the Scriptorium backend;
5. validate and persist that structured response as `GeneratedText`;
6. build a curated render context;
7. render final Markdown from an embedded human-editable template.
This should improve consistency for headings, dates, ordering, units, recurring
sections, and report-specific format while preserving the LLM's role in
summarizing and explaining weather impacts.
## Terminology
- `CollectedFacts`: normalized upstream inputs from Weather API sources.
- `DerivedFacts`: deterministic Go-calculated facts sliced to a report period.
- `ModuleOutput`: prompt-facing deterministic module stanzas.
- `GeneratedText`: validated structured LLM output. It is not authoritative
weather data; it is prose or short text intended to fill named template
slots.
- `RenderContext`: the final deterministic object passed to a Markdown
template. It may expose selected values from CollectedFacts, DerivedFacts,
ModuleOutput, report metadata, and GeneratedText.
The report template should not receive the unbounded raw data package directly.
It should receive a curated render context so templates remain readable and do
not become a second application logic layer.
## Locked Decisions
- Use `GeneratedText` as the name for the structured LLM output layer.
- Continue using Scriptorium behind `internal/adapters/scriptorium`.
- Use Scriptorium structured JSON output. The Scriptorium backend associates
the prompt ID with the JSON schema; weatherreporter does not pass a schema
file at runtime.
- Keep Scriptorium retries inside Scriptorium.
- Also validate the returned JSON inside `weatherreporter` before rendering.
- Use embedded, separate, human-editable Markdown template files.
- Use embedded, separate, human-readable JSON schema files.
- Use Go standard library `text/template` for Markdown rendering unless a
concrete limitation appears.
- Let Scriptorium enforce the prompt-associated JSON Schema and perform
retries, then have weatherreporter validate by unmarshalling into a typed
GeneratedText struct and checking required fields. Do not add a Go JSON
Schema dependency in the first implementation.
- Do not generate schemas or templates from inline Go.
- Start with the rolling next-hours report only.
- Use `hourly` as the only public report ID for the rolling next-hours
report.
- Do not keep compatibility aliases from retired roadmap terminology.
- Do not add a generic workflow engine or plugin system.
## Hourly Report Target
The rolling next-hours report target identity is:
- report ID: `hourly`
- CLI command: `weatherreporter generate hourly`
- display name: `Hourly Report`
- prompt ID for structured text generation: `weather.hourly_generated_text`
- Markdown template ID: `hourly`
- GeneratedText schema ID: `hourly`
- artifact group: `hourly`
- batch output name: `hourly.md`
- default valid-period length: 6 hours
- valid period: `[generation_time, generation_time + hourlyReportHours)`
Use a package-owned constant for the valid-period length, initially 6 hours.
The duration should not be user-configurable in the first implementation.
The report remains explicit-generation only at first. Do not add it to morning
or evening batches until cadence and downstream behavior are proven.
## Embedded Asset Layout
Recommended asset package:
```text
internal/reporttemplate/
templates/
hourly.md.tmpl
schemas/
hourly.generated_text.schema.json
```
`internal/reporttemplate` should embed assets with `go:embed` and expose a
narrow API for:
- looking up a template by template ID;
- looking up a schema by schema ID;
- rendering Markdown from a typed render context.
Do not allow template or schema asset IDs to come from free-form user input in
the initial implementation. Report definitions should declare the IDs.
## Report Definition Fields
Extend report definitions only as needed for this generation mode. Required
fields:
- `GenerationMode`
- `TemplateID`
- `GeneratedTextSchemaID`
Recommended generation modes:
- `GenerationModeScriptoriumMarkdown`: existing full Markdown generation path.
- `GenerationModeGeneratedTextTemplate`: structured GeneratedText plus
deterministic template rendering.
Existing reports should stay on the current full Markdown path until migrated.
The `hourly` report should use the GeneratedText/template path from the start.
For generated-text reports, the existing `PromptID` field remains the
Scriptorium prompt ID and should be set to `weather.hourly_generated_text`.
## GeneratedText Contract
The hourly JSON schema should define only the text slots the template needs.
Keep the first schema small. Example shape:
```json
{
"summary": "Brief hourly overview.",
"timing": "Plain-language timing of notable changes.",
"impacts": "Practical impacts for the next few hours.",
"confidence": "Optional confidence or uncertainty note."
}
```
The exact schema should be locked during implementation based on the first
hourly template. Required fields should be genuinely required by the template.
Optional fields should be omitted from the rendered Markdown when empty.
GeneratedText should be persisted as structured JSON exactly as validated by
weatherreporter. Raw Scriptorium output should also be persisted for debugging
when practical.
## Template Contract
The hourly Markdown template should be a readable file that a human can edit
without digging into Go code. It should own:
- Markdown heading structure;
- section order;
- deterministic fact placement;
- conditional inclusion of optional GeneratedText slots;
- final report layout.
The template should not own:
- Weather API fetching;
- fact derivation;
- module composition;
- Scriptorium invocation;
- schema validation;
- path construction;
- distributor upload behavior.
The render context should prepare strings and simple values so template logic
stays shallow. Prefer small conditionals over complex template functions.
## Scriptorium Adapter Direction
`internal/adapters/scriptorium` should expose weatherreporter-owned request and
result types for structured JSON generation. The adapter should hide
Scriptorium argv details from app, report, facts, module, prompt-input, and
template packages.
Scriptorium uses the prompt ID to select the backend-associated structured
output schema. The adapter should not pass a schema path at runtime.
## State And Artifacts
Add managed artifacts for the new generation path. Recommended logical
artifacts:
- module snapshot JSON;
- data package YAML;
- raw GeneratedText JSON written by Scriptorium;
- structured Scriptorium command result for the GeneratedText run;
- validated GeneratedText JSON;
- render context JSON;
- rendered Markdown report;
- metadata linking all of the above.
The existing `preflight` artifact should remain as the rendered prompt/input
inspection artifact before structured generation. GeneratedText reports add
separate artifacts for raw GeneratedText, structured run result, validated
GeneratedText, and render context. Metadata should record the
`GeneratedTextSchemaID`; no per-run schema artifact is persisted.
Artifact paths should remain under the workspace and use the report artifact
group, valid start date, and RunID conventions already used by state.
## End-To-End Behavior
Reports using `GenerationModeGeneratedTextTemplate` should follow the same
fact collection, derivation, module snapshot, prompt package, state, and
notification boundaries as current reports. The difference is limited to the
last rendering segment: Scriptorium returns structured GeneratedText, Go
validates that structure, Go builds a curated render context, and an embedded
template renders the final Markdown.
The existing full-Markdown Scriptorium path should remain available for
reports that have not migrated.
## Target Architecture
GeneratedText rendering adds one new generation mode, not a replacement for the
whole report system. Reports that opt into this mode should continue to use
the existing report registry, fact pipeline, module snapshot, data package,
state store, and notification boundaries.
The target flow is:
1. `internal/report` declares whether a report uses the existing full-Markdown
Scriptorium path or the GeneratedText/template path.
2. The app builds CollectedFacts, DerivedFacts, module snapshots, and the
prompt data package using the same boundaries as existing reports.
3. `internal/adapters/scriptorium` asks Scriptorium for structured JSON using
the report's prompt ID. The Scriptorium backend owns the prompt-to-schema
association.
4. Weatherreporter validates the returned JSON as typed `GeneratedText`.
5. A report-specific render context is built from selected deterministic facts,
module outputs, metadata, and GeneratedText.
6. `internal/reporttemplate` renders the final Markdown from an embedded,
human-editable template.
7. The managed Markdown report remains the canonical output and the distributor
upload source.
This structure keeps each layer understandable:
- CollectedFacts and DerivedFacts remain weather-data contracts.
- ModuleOutput remains the prompt-facing deterministic facts contract.
- GeneratedText remains LLM-written prose slots, not weather truth.
- RenderContext remains a curated template input, not a second raw data package.
- Templates own presentation order and headings, not weather derivation logic.
## Editing Surface
The desired human editing surface is file-based and report-specific. For the
hourly report, the important files should be easy to find and inspect:
- one Markdown template file for the report layout;
- one JSON schema file describing the expected GeneratedText payload;
- one report definition declaring the prompt ID, template ID, schema ID, module
composition, valid period, artifact group, and output naming policy.
The template and schema should be embedded into the binary, but they should
remain ordinary files in the repository. A maintainer should be able to tweak
headings, reorder sections, or inspect the structured prose contract without
reading unrelated orchestration code.
## Boundary Guidelines
- Keep report identity, prompt IDs, generation mode, template IDs, schema IDs,
valid periods, module composition, and output naming in `internal/report`.
- Keep Scriptorium subprocess details in `internal/adapters/scriptorium`.
- Keep Markdown template lookup and rendering in `internal/reporttemplate`.
- Keep GeneratedText validation in a narrow package or component with typed
report-specific structs.
- Keep app orchestration responsible for sequencing and artifact persistence,
not for report-specific prose or template structure.
- Keep distributor upload behavior unchanged: upload the managed Markdown
report after it exists.
- Do not let templates fetch data, derive facts, perform source selection, or
construct workspace paths.
- Do not let Scriptorium-specific flags, schema paths, or subprocess details
leak into report definitions or domain packages.
- Do not add user-provided template or schema paths in the first
implementation.
## Target Acceptance Criteria
The feature is successful when:
- `weatherreporter generate hourly` produces a deterministic Markdown report
whose headings, dates, and section order come from an embedded template;
- Scriptorium fills only the report's structured GeneratedText slots;
- weatherreporter validates GeneratedText before rendering;
- the raw GeneratedText, validated GeneratedText, render context, rendered
Markdown, and metadata are inspectable after a run;
- existing daily, tomorrow, three-day, weekend, and storm reports keep their
current full-Markdown Scriptorium path until explicitly migrated;
- failures in structured generation, validation, or template rendering are
diagnosable from saved artifacts where practical;
- the managed Markdown report remains the source for optional copies and
distributor uploads;
- the implementation does not introduce a generic workflow engine, plugin
system, or broad report rewrite.
## Deferred Work
Do not include these in the first implementation:
- migrating daily, tomorrow, three-day, weekend, or storm to GeneratedText;
- user-configurable template or local schema source overrides;
- user-defined templates or schemas;
- a generic workflow engine;
- template hot reloading;
- dynamic module selection for templates;
- distributor-specific generated-text behavior;
- prompt/schema generation from Go structs;
- scheduled hourly batch membership;
- configurable hourly report duration.
## Open Questions
No questions block roadmap creation.
Recommended approach: keep JSON schema enforcement in Scriptorium for retry
behavior, and also perform weatherreporter-side typed validation before
rendering. Weatherreporter should validate the returned JSON by unmarshalling
into the report's GeneratedText type and checking required fields; it should
not add a Go JSON Schema dependency in the first implementation.
Viable alternative: trust Scriptorium validation and only unmarshal into typed
Go structs without explicit required-field checks. This is simpler, but it
weakens weatherreporter's rendering contract and may make empty or malformed
LLM output harder to diagnose.

View File

@@ -1,782 +0,0 @@
# GeneratedText Hourly Report Implementation Roadmap
## Purpose
This roadmap defines the concrete implementation sequence for
`docs/roadmap/generated-text.md`. It is written for an LLM coding agent that
will implement each stage in order.
This is a future-work roadmap. Until a stage is implemented, non-roadmap docs
must not describe `GeneratedText`, embedded report templates, or other
unimplemented GeneratedText behavior as available.
## Source Feature Roadmap
Use `docs/roadmap/generated-text.md` as the authoritative feature roadmap for
intent, terminology, target behavior, and policy choices. This document is the
step-by-step implementation plan. If target behavior changes, update
`generated-text.md` first, then update this plan.
Future implementation agents should read `generated-text.md` before starting
Stage 1. Treat that file as the conceptual contract for why the feature exists
and what final shape it should have; treat this file as the ordered work plan
for reaching that shape.
## Locked Implementation Decisions
- Implement the first GeneratedText path for the rolling next-hours report,
named `hourly`.
- Use `hourly` as the only planned public name for the rolling next-hours
report; do not keep compatibility aliases from retired roadmap terminology.
- Use report ID `hourly`, artifact group `hourly`, batch output name
`hourly.md`, and CLI command `weatherreporter generate hourly`.
- Keep the rolling report explicit-generation only; do not add scheduled batch
membership in this implementation.
- Keep the valid-period length as a package-owned constant, initially 6 hours.
- Add report generation modes:
- `scriptorium_markdown` for existing reports;
- `generated_text_template` for `hourly`.
- Keep existing reports on the current Scriptorium Markdown path.
- For generated-text reports, keep `Definition.PromptID` as the Scriptorium
prompt ID and set the hourly value to `weather.hourly_generated_text`.
- Add `TemplateID` and `GeneratedTextSchemaID` to report definitions.
- Use embedded assets under `internal/reporttemplate`.
- Use standard-library `text/template`.
- Let Scriptorium enforce its prompt-associated JSON Schema and retries;
weatherreporter validates by unmarshalling into a typed GeneratedText struct
and checking required fields. Do not add a Go JSON Schema dependency in this
implementation.
- Keep the existing preflight artifact for prompt/input inspection.
- Add managed artifacts for raw GeneratedText JSON, structured Scriptorium run
result, validated GeneratedText JSON, and render context JSON.
- Distributor notification continues to upload the managed Markdown report only.
- Preserve current CLI behavior, artifact paths, and generated output behavior
for daily, tomorrow, three-day, weekend, and storm reports.
## Stage 1: Roadmap Reconciliation And Hourly Identity
Goal: remove conflicting future instructions and establish `hourly` as the only
planned public name for the rolling next-hours report.
Files to inspect:
- `docs/roadmap/generated-text.md`
- the superseded roadmap file for the retired rolling-report name
- `docs/roadmap/future.md`
- `docs/roadmap/implementation.md`
- `internal/app/app.go`
- `internal/report/definition.go`
- `internal/report/registry.go`
Implementation:
- Replace the superseded roadmap file for the retired rolling-report name with
a short note pointing to `docs/roadmap/generated-text.md`, or delete it if
the repository policy at implementation time prefers removing obsolete
roadmap files.
- Ensure no roadmap instructs a future agent to implement retired public
identifiers for the rolling report.
- If partial code using retired identifiers already exists, rename it to
`hourly` rather than adding aliases:
- app report kind -> `ReportHourly`;
- report ID constant -> `report.Hourly`;
- CLI command -> `hourly`;
- artifact group -> `hourly`;
- prompt IDs and tests updated to hourly names.
- Do not add compatibility aliases for retired command or config names.
Acceptance criteria:
- Active roadmap docs use `hourly` for the rolling report.
- No active roadmap describes a retired identifier as the target public report
ID.
- Existing code compiles after any rename work.
Suggested validation:
```bash
rg -n 'near[_-]term|Near''Term' docs internal
go test ./internal/app ./internal/report ./internal/cli
```
This stage is small enough for one implementation prompt.
## Stage 2: Report Definition Generation Mode
Goal: add generation-mode metadata to report definitions without changing the
current report generation path.
Files to inspect:
- `internal/report/definition.go`
- `internal/report/registry.go`
- `internal/report/*_report.go`
- `internal/report/period_test.go`
- `internal/state/filesystem.go`
Implementation:
- Add a `GenerationMode` type in `internal/report`.
- Add constants:
- `GenerationModeScriptoriumMarkdown`
- `GenerationModeGeneratedTextTemplate`
- Add fields to `report.Definition`:
- `GenerationMode GenerationMode`
- `TemplateID string`
- `GeneratedTextSchemaID string`
- Default or explicitly set existing reports to
`GenerationModeScriptoriumMarkdown`.
- Update report definition tests so every generated report declares a
generation mode.
- Do not wire app behavior in this stage.
- Keep existing `PromptID` semantics: it is the Scriptorium prompt ID for the
report's generation mode.
Acceptance criteria:
- Existing report definitions retain current prompt IDs and behavior.
- Registry tests fail if a generated report omits generation mode.
- Generated-text-only fields are empty for existing Markdown reports.
Suggested validation:
```bash
go test ./internal/report ./internal/state
```
This stage is small enough for one implementation prompt.
## Stage 3: Hourly Report Definition, Modules, Facts, And CLI Shell
Goal: add the `hourly` report identity, rolling valid period, module
composition, facts derivation, config override aliases, and CLI command shell.
Files to inspect:
- `internal/report/definition.go`
- `internal/report/registry.go`
- new `internal/report/hourly_report.go`
- `internal/report/period_test.go`
- `internal/briefing/modules.go`
- `internal/facts/facts.go`
- `internal/facts/facts_test.go`
- `internal/config/reports.go`
- `internal/config/config_test.go`
- `internal/app/app.go`
- `internal/cli/root.go`
- `internal/cli/root_test.go`
Implementation:
- Add report ID `Hourly ID = "hourly"`.
- Add comparison strategy `CompareRollingWindow = "rolling_window"`.
- Add `hourlyReportHours` constant, initially `6`.
- Add `hourlyDefinition()`:
- `Name: "Hourly Report"`
- `PromptID: "weather.hourly_generated_text"`
- `GenerationMode: GenerationModeGeneratedTextTemplate`
- `TemplateID: "hourly"`
- `GeneratedTextSchemaID: "hourly"`
- `ComparisonStrategy: CompareRollingWindow`
- `ArtifactGroup: "hourly"`
- `BatchOutputName: "hourly.md"`
- `Generated: true`
- `CompatiblePriorIDs: []ID{Hourly}`
- no morning/evening batch membership
- Resolve valid period as `[generation_time, generation_time + 6h)` in the
effective timezone.
- Add default hourly modules in order:
1. `metadata`
2. `current_conditions`
3. `hourly_forecast`
4. `precip_timing`
5. `alert_digest`
6. `spc_convective_outlooks`
7. `area_forecast_discussion`
8. `spc_convective_discussion`
9. `weather_story`
- Configure hourly `area_forecast_discussion` options to include only:
- `key_messages`
- `short_term`
- Update module supported-report lists:
- include `report.Hourly` in all broadly compatible modules;
- include `report.Hourly` for `hourly_forecast`;
- keep daily/daypart modules incompatible.
- Add `facts.BuildDerived` support for hourly:
- valid-period hourly slice;
- precipitation timing from that slice;
- alert overlap filtering;
- SPC outlook/discussion overlap filtering;
- no daily summaries, daypart summaries, or storm summary.
- Add config report aliases:
- `hourly`
- reject or do not recognize retired rolling-report config keys.
- Add app report kind `ReportHourly = "hourly"` and map it to
`report.Hourly`.
- Add CLI parsing and help for:
```bash
weatherreporter generate hourly [--config PATH] [--units VALUE] [--tz NAME] [--out PATH]
```
- Do not add `--date`, `--start`, `--end`, or duration flags.
- It is acceptable for `generate hourly` to return an explicit
"generation mode not implemented" error until the app workflow stage, but
parser and resolution tests should pass.
Acceptance criteria:
- `report.DefaultRegistry().Lookup(report.Hourly)` succeeds.
- hourly valid period is exactly six hours and not civil-day truncated.
- hourly default module composition validates.
- daily-only modules reject hourly.
- facts derivation supports the hourly module list.
- CLI help includes `generate hourly`.
- Existing command behavior is unchanged.
Suggested validation:
```bash
go test ./internal/report ./internal/briefing ./internal/facts
go test ./internal/config ./internal/app ./internal/cli
go run ./cmd/weatherreporter --help
```
This stage is large but still suitable for one implementation prompt if kept
strictly to report identity/facts/CLI shell. If it becomes unwieldy, split it
after report/facts and implement CLI/config aliases in a follow-up prompt.
## Stage 4: Embedded Template And Schema Assets
Goal: create the embedded asset package and first hourly template/schema.
Files to create:
- `internal/reporttemplate/reporttemplate.go`
- `internal/reporttemplate/templates/hourly.md.tmpl`
- `internal/reporttemplate/schemas/hourly.generated_text.schema.json`
- `internal/reporttemplate/reporttemplate_test.go`
Implementation:
- Use `go:embed` for templates and schemas.
- Expose a narrow API:
- `Template(id string) (string, error)` or equivalent;
- `Schema(id string) ([]byte, error)` or equivalent;
- `Render(id string, data any) ([]byte, error)`.
- Use `text/template`.
- Register only `hourly` assets initially.
- Missing or unknown asset IDs must return actionable errors.
- Keep template functions minimal. Prefer prepared render-context strings over
complex template logic.
- Add an initial hourly JSON Schema with:
- required `summary`;
- required `timing`;
- required `impacts`;
- optional `confidence`;
- all fields strings;
- `additionalProperties: false`.
- Add an initial hourly Markdown template that is readable and deterministic.
It should include fixed headings, report valid period, selected deterministic
facts, and GeneratedText slots.
Acceptance criteria:
- schema asset can be looked up for tests, local validation, and out-of-band
Scriptorium registration;
- template asset can be looked up;
- template renders from a test render context;
- missing template/schema IDs fail clearly;
- no generated schema or template content is inline in Go.
Suggested validation:
```bash
go test ./internal/reporttemplate
```
This stage is small enough for one implementation prompt.
## Stage 5: GeneratedText Contract And Render Context
Goal: add typed GeneratedText validation and a typed hourly render context.
Files to create or inspect:
- new `internal/generatedtext` package, or another narrow package if the
implementation chooses a better local name
- `internal/reporttemplate`
- `internal/module`
- `internal/briefing`
- `internal/promptinput`
Implementation:
- Add typed hourly GeneratedText:
```go
type Hourly struct {
Summary string `json:"summary"`
Timing string `json:"timing"`
Impacts string `json:"impacts"`
Confidence string `json:"confidence,omitempty"`
}
```
- Add validation:
- malformed JSON fails;
- empty required fields fail after trimming whitespace;
- unknown fields fail by using `json.Decoder.DisallowUnknownFields`;
- validated output is normalized to stable JSON.
- Do not add a Go JSON Schema dependency in this implementation.
- Add typed hourly render context.
- Build render context from:
- report metadata and location;
- selected module outputs from the module snapshot;
- validated hourly GeneratedText.
- Use `module.StanzaValue` or equivalent typed decoding rather than ad hoc map
traversal where practical.
- Keep render context curated; do not pass the full raw data package to the
template.
Acceptance criteria:
- valid GeneratedText passes and round-trips to stable JSON;
- missing required fields fail;
- unknown fields fail;
- render context has enough deterministic fields for the hourly template;
- render context construction fails clearly when required module stanzas are
missing.
Suggested validation:
```bash
go test ./internal/generatedtext ./internal/reporttemplate ./internal/module
```
This stage is small enough for one implementation prompt.
## Stage 6: State Artifacts For GeneratedText Reports
Goal: add explicit managed paths and save/load helpers for generated-text
artifacts.
Files to inspect:
- `internal/state/filesystem.go`
- `internal/state/store.go`
- `internal/state/metadata.go`
- `internal/state/filesystem_test.go`
- `internal/fileutil`
Implementation:
- Extend `state.ArtifactPaths` with:
- `GeneratedTextRaw`
- `GeneratedTextResult`
- `GeneratedText`
- `RenderContext`
- Store all four under the existing snapshots tree:
- `snapshots/<artifact_group>/<YYYY-MM-DD>/<run_id>.generated_text.raw.json`
- `snapshots/<artifact_group>/<YYYY-MM-DD>/<run_id>.generated_text.run.json`
- `snapshots/<artifact_group>/<YYYY-MM-DD>/<run_id>.generated_text.json`
- `snapshots/<artifact_group>/<YYYY-MM-DD>/<run_id>.render_context.json`
- Do not add new workspace config directories.
- Extend `state.Metadata` with optional path fields for those artifacts.
Also record `GeneratedTextSchemaID` as metadata for auditability without
copying schema contents into every run.
- Add store methods as needed, for example:
- `SaveGeneratedTextRaw`
- `SaveGeneratedTextResult`
- `SaveGeneratedText`
- `SaveRenderContext`
- Use atomic writes through existing `fileutil` helpers.
- Keep existing metadata JSON shape for Markdown-path reports, with new fields
omitted when empty.
Acceptance criteria:
- path tests cover all new artifact paths;
- save helpers create parent directories and write atomically;
- metadata links generated-text paths for hourly reports;
- existing report metadata tests still pass.
Suggested validation:
```bash
go test ./internal/state ./internal/fileutil
```
This stage is small enough for one implementation prompt.
## Stage 7: Scriptorium GeneratedText Run Adapter
Goal: add a GeneratedText run helper behind the Scriptorium adapter while
preserving the existing `scriptorium run` command shape.
Files to inspect:
- `internal/adapters/scriptorium/runner.go`
- `internal/adapters/scriptorium/runner_test.go`
- `docs/integrations/scriptorium.md` after implementation
Implementation:
- Add weatherreporter-owned request/result types:
```go
type StructuredRunRequest struct {
PromptID string
DataPackagePath string
OutputPath string
}
```
- Add `StructuredRun(ctx, req)` to the runner.
- Validate prompt ID, data package path, and output path before
subprocess execution.
- Preserve current command result capture behavior:
- full argv;
- stdout;
- stderr;
- truncation flags;
- exit code;
- output path.
- Do not change current `Render` or `Run` methods.
- Scriptorium selects the structured output schema from the backend prompt
configuration associated with `PromptID`; do not pass a schema path at
runtime.
- Use the same `scriptorium run` command form as Markdown report generation.
The only material difference is that `--out` points to the raw GeneratedText
JSON artifact path rather than a Markdown report path.
- Use this argv shape:
```bash
scriptorium run \
--prompt <prompt_id> \
--input data_package=<path> \
--out <generated_text_raw_path>
```
- Do not add `--format json` or any schema flag. Structured output is selected
by Scriptorium's internal prompt configuration.
- If local Scriptorium documentation needs updating after implementation,
update `docs/integrations/scriptorium.md` to describe this contract.
Acceptance criteria:
- argv construction tests prove `scriptorium run` is invoked without
`--format` or schema flags and with `--out <generated_text_raw_path>`;
- nonzero exits return captured result plus error;
- missing required fields fail before subprocess execution;
- current Scriptorium render/run tests still pass;
- tokens/secrets are not added to argv or errors.
Suggested validation:
```bash
go test ./internal/adapters/scriptorium
```
This stage is small enough for one implementation prompt.
## Stage 8: GeneratedText App Workflow Success Path
Goal: wire the successful generated-text/template path into app orchestration
for hourly while preserving the existing path for all current reports.
Files to inspect:
- `internal/app/app.go`
- `internal/app/app_test.go`
- `internal/adapters/scriptorium`
- `internal/reporttemplate`
- `internal/generatedtext`
- `internal/state`
- `internal/promptinput`
Implementation:
- Extend the app `Renderer` interface, or introduce a second narrow interface,
so tests can fake structured generation.
- Branch report generation by `req.Resolved.Definition.GenerationMode`.
- For `GenerationModeScriptoriumMarkdown`, keep the existing flow unchanged.
- For the successful `GenerationModeGeneratedTextTemplate` path:
1. run existing preflight and save preflight artifact;
2. call Scriptorium structured generation with raw output path
`GeneratedTextRaw`;
3. save structured Scriptorium command result to `GeneratedTextResult`;
4. validate raw JSON into typed GeneratedText;
5. save normalized GeneratedText to `GeneratedText`;
6. build hourly render context from metadata, module snapshot, and
GeneratedText;
7. save render context JSON;
8. render Markdown via embedded template to managed report path;
9. save final metadata with all generated-text paths and the schema ID;
- Defer distributor notification and optional output copy regression coverage
to Stage 10.
- Keep failure-specific persistence tests minimal in this stage; Stage 9 owns
failure artifact behavior.
- Keep Recent Changes empty for hourly.
Acceptance criteria:
- hourly generation reaches Scriptorium structured generation, validates
GeneratedText, renders Markdown, and persists final metadata;
- daily/tomorrow/three-day/weekend/storm generation still uses existing
Markdown Scriptorium path;
- generated-text artifacts are linked from metadata for successful hourly runs;
- app tests cover the successful hourly workflow with fake structured
Scriptorium output.
Suggested validation:
```bash
go test ./internal/app ./internal/state ./internal/reporttemplate ./internal/generatedtext
```
This stage is small enough for one implementation prompt if failure behavior is
kept to the later stages.
## Stage 9: GeneratedText Failure Artifact Persistence
Goal: make generated-text failures inspectable without changing successful
behavior from Stage 8.
Files to inspect:
- `internal/app/app.go`
- `internal/app/app_test.go`
- `internal/adapters/scriptorium`
- `internal/generatedtext`
- `internal/reporttemplate`
- `internal/state`
Implementation:
- Add focused app tests for failures at each generated-text step:
- preflight failure;
- structured Scriptorium generation failure;
- GeneratedText validation failure;
- render context construction failure;
- Markdown template rendering failure.
- Ensure failures preserve available artifacts:
- failed preflight saves preflight output when possible;
- failed structured generation saves command result when available;
- validation failures preserve raw GeneratedText output when available;
- render-context failures preserve validated GeneratedText when already
available;
- template failures preserve GeneratedText and render context when already
built.
- Return actionable errors that include report ID, RunID, and operation context
without exposing data package contents or secrets.
- Do not run distributor notification after any generated-text failure.
- Do not create optional `--out` copies after any generated-text failure.
Acceptance criteria:
- every generated-text failure point has an app-level regression test;
- partial artifacts are persisted where available and linked from metadata when
metadata can be safely written;
- failed hourly runs do not notify distributor;
- existing Markdown-generation failure behavior is unchanged.
Suggested validation:
```bash
go test ./internal/app ./internal/state ./internal/generatedtext ./internal/reporttemplate
```
This stage is small enough for one implementation prompt.
## Stage 10: GeneratedText Output Copy And Notification Regression Coverage
Goal: preserve existing user-facing output behavior and distributor boundaries
for generated-text reports.
Files to inspect:
- `internal/app/app.go`
- `internal/app/app_test.go`
- `internal/adapters/distributor`
- `internal/state`
Implementation:
- Keep `--out` copy behavior unchanged after the managed Markdown report
exists.
- Keep `--out-dir` behavior unchanged for batch paths if hourly is ever run
through a batch helper during tests.
- Run distributor notification, if enabled, only after:
- structured generation succeeds;
- GeneratedText validates;
- Markdown template rendering succeeds;
- final metadata is saved.
- Use the managed Markdown report path as the distributor source.
- Never use optional `--out` or `--out-dir` copies as distributor source files.
- Keep distributor request construction template-driven; do not add
hourly-specific path branching.
- Keep Recent Changes empty for hourly in this implementation.
Acceptance criteria:
- `--out` copies the managed generated-text Markdown report after success;
- distributor notification receives the managed Markdown report path;
- distributor notification is not called on any failure from Stage 9;
- enabled distributor failure makes hourly generation fail with the same
semantics as existing generated reports;
- existing daily/tomorrow/three-day/weekend/storm distributor behavior is
unchanged.
Suggested validation:
```bash
go test ./internal/app ./internal/adapters/distributor ./internal/state
```
This stage is small enough for one implementation prompt.
## Stage 11: End-To-End CLI, Config, And Prompt Package Coverage
Goal: prove the public hourly workflow and configured module overrides work
through the CLI/app boundary.
Files to inspect:
- `internal/cli/root.go`
- `internal/cli/root_test.go`
- `internal/config/reports.go`
- `internal/config/config_test.go`
- `internal/app/app_test.go`
- `examples/config.yml`
Implementation:
- Add CLI command tests for `generate hourly`.
- Reject the retired rolling-report command name.
- Reject hourly `--date`, `--start`, `--end`, and duration-like flags.
- Add config override tests for `reports.hourly.deterministic_modules`.
- Ensure retired rolling-report config keys are rejected.
- Add app/CLI workflow tests using fake Scriptorium structured output.
- Update examples only if they enumerate report module overrides.
Acceptance criteria:
- `weatherreporter --help` lists `generate hourly`;
- the retired rolling-report command name is not accepted;
- hourly config overrides validate compatible modules;
- incompatible daily/daypart modules fail clearly for hourly;
- examples load successfully.
Suggested validation:
```bash
go test ./internal/cli ./internal/config ./internal/app
go run ./cmd/weatherreporter --help
```
This stage is small enough for one implementation prompt.
## Stage 12: Implemented Documentation
Goal: document implemented behavior after the code exists.
Files to inspect and update:
- `docs/cli.md`
- `docs/operations.md`
- `docs/config.md`
- `docs/internal/app-orchestration.md`
- `docs/internal/report-registry.md`
- `docs/internal/briefing.md`
- `docs/internal/prompt-input.md`
- `docs/internal/scriptorium-adapter.md`
- `docs/internal/state.md`
- new `docs/internal/reporttemplate.md`
- `docs/integrations/scriptorium.md`
- `docs/roadmap/future.md`
- the superseded roadmap file for the retired rolling-report name
- `examples/config.yml`, only if examples changed
Documentation requirements:
- Non-roadmap docs describe only implemented hourly and GeneratedText behavior.
- `docs/cli.md` documents `generate hourly`, not the retired command name.
- `docs/config.md` documents `reports.hourly` only if override support is
implemented.
- Internal docs distinguish:
- CollectedFacts;
- DerivedFacts;
- ModuleOutput;
- GeneratedText;
- RenderContext.
- Scriptorium integration docs include the structured-output command actually
used by the adapter.
- State docs list generated-text artifacts and metadata links.
- the superseded roadmap file for the retired rolling-report name is deleted
or marked superseded.
- Deferred migrations for other report types stay under roadmap docs only.
Acceptance criteria:
- No non-roadmap docs describe unimplemented generated-text migrations.
- No active docs describe retired rolling-report identifiers as the target
public report.
- Maintained examples remain valid.
Suggested validation:
```bash
go test ./internal/config
rg -n 'near[_-]term|Near''Term' docs internal examples
git diff --check
```
This stage is small enough for one implementation prompt.
## Stage 13: Final Validation
Goal: run full validation and perform targeted manual checks.
Commands:
```bash
go test ./...
go run ./cmd/weatherreporter --help
git diff --check
```
Manual checks:
- `weatherreporter --help` lists `generate hourly`.
- `weatherreporter --help` does not list the retired rolling-report command.
- generated hourly Markdown has deterministic headings and section order.
- GeneratedText JSON is persisted separately from deterministic facts.
- render context JSON is persisted and readable.
- embedded hourly template is readable and easy to edit.
- embedded hourly schema is readable and easy to edit.
- existing Markdown-generation reports still run through the previous
Scriptorium `run --out <markdown>` path.
- distributor notification, when enabled, uploads the managed Markdown report.
This stage is small enough for one implementation prompt.
## Deferred Work
Do not include these in the first implementation:
- migrating daily, tomorrow, three-day, weekend, or storm to GeneratedText;
- user-configurable template or local schema source overrides;
- user-defined templates or schemas;
- a generic workflow engine;
- template hot reloading;
- dynamic module selection for templates;
- prompt/schema generation from Go structs;
- scheduled hourly batch membership;
- configurable hourly report duration;
- rolling-window Recent Changes for hourly;
- distributor-specific generated-text behavior.
## Open Questions
None block implementation.

232
docs/templates.md Normal file
View File

@@ -0,0 +1,232 @@
# Report Templates
## Purpose
This guide describes the implemented Markdown report template surface for
`weatherreporter`. It is for maintainers editing embedded report templates,
especially the hourly report template.
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.
## 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 strings can be guarded with `{{ with .Field }}...{{ end }}`.
- Slices can be rendered with `{{ range .Items }}...{{ else }}...{{ end }}`.
## Hourly Template Variables
These are the complete variables currently available to
`internal/reporttemplate/templates/hourly.md.tmpl`.
### Report Metadata
| 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. |
### 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.Timing` | string | Required prose about timing of notable weather changes or hazards. |
| `.GeneratedText.Impacts` | string | Required prose about practical near-term impacts. |
| `.GeneratedText.Confidence` | string | Optional confidence or uncertainty note. Empty when omitted by the LLM. |
Example:
```gotemplate
## Summary
{{ .GeneratedText.Summary }}
{{ with .GeneratedText.Confidence }}
## Confidence
{{ . }}
{{ end }}
```
### Current Conditions
| Variable | Type | Description |
| --- | --- | --- |
| `.CurrentConditions` | string | Deterministic one-line current conditions summary. May include condition text, temperature, apparent temperature, humidity, and wind. |
Example value:
```text
Partly cloudy; 74 F; feels like 76 F; humidity 71%; wind S 8 mph.
```
### Hourly Forecast
| 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. |
Example:
```gotemplate
## Hourly Forecast
{{ range .HourlyForecast }}
- {{ .Time }}: {{ .Summary }}{{ with .Temperature }}; {{ . }}{{ end }}{{ with .Precipitation }}; {{ . }}{{ end }}{{ with .Wind }}; {{ . }}{{ end }}
{{ else }}
- No hourly forecast rows available.
{{ 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
| Variable | Type | Description |
| --- | --- | --- |
| `.Alerts` | []string | Alert labels for active alerts overlapping the report period. Empty when there are no relevant alert overlaps. |
Example:
```gotemplate
## Alerts
{{ range .Alerts }}
- {{ . }}
{{ else }}
- No active alert overlaps for this report period.
{{ end }}
```
### SPC Outlooks
| Variable | Type | Description |
| --- | --- | --- |
| `.SPCOutlooks` | []string | SPC convective outlook labels overlapping the report period. Empty when there are no overlapping outlooks. |
Example value:
```text
Slight Risk from 2026-05-29 at 7:00 AM to 2026-05-29 at 3:00 PM
```
### Forecast 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. |
Example:
```gotemplate
## Forecast Discussion
{{ range .ForecastDiscussion.KeyMessages }}
- {{ . }}
{{ end }}{{ with .ForecastDiscussion.ShortTerm }}
{{ . }}
{{ end }}
```
### SPC 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 }}
```
### Weather Story
| Variable | Type | Description |
| --- | --- | --- |
| `.WeatherStory` | string | Weather story title and description, or `No weather story available.` |
## Common Patterns
Use `with` for optional strings:
```gotemplate
{{ with .GeneratedText.Confidence }}
## Confidence
{{ . }}
{{ 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 }}
```
## 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 currently exercise the hourly template through
`internal/generatedtext/render_context_test.go`.

View File

@@ -0,0 +1,42 @@
You are writing structured text slots for a short-term 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 valid period in `report.valid_period`, typically the
next several hours for the configured location.
Write for a general local audience. Be clear, practical, and concise.
Return these fields:
- `summary`: 1-2 sentences summarizing the main weather story for the valid
period.
- `timing`: 1-3 sentences explaining when the most important changes or hazards
are expected during the valid period.
- `impacts`: 1-3 sentences explaining practical impacts for people planning
travel, outdoor activity, errands, commuting, or similar near-term decisions.
- `confidence`: optional. Include only if uncertainty, timing spread, or
conflicting signals materially affect how the reader should interpret the
forecast.
Guidance:
- Prefer location-applicable risk products, active alerts, and overlapping SPC
products for hazard wording.
- Use the hourly forecast and precipitation timing modules for timing details.
- Use current conditions only for immediate context; do not let them override
the forecast.
- Use AFD key messages and short-term discussion for context, but keep regional
or broad discussion tied back to the configured location and valid period.
- Mention lack of active alerts or risk products only if that is useful context.
- Do not repeat deterministic tables or lists verbatim.
- Do not include Markdown headings, bullets, or code fences.
- Do not include fields outside the schema.
- If a field cannot be supported by the data, keep it brief and conservative.
Return JSON only.