Add roadmap and implementation plan to move towards hybrid deterministic/llm generation of reports

This commit is contained in:
2026-06-13 23:20:04 -05:00
parent d425132ae7
commit b3f7c9c1f2
2 changed files with 997 additions and 374 deletions

View File

@@ -0,0 +1,343 @@
# 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, now named
`hourly` rather than `near_term`.
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.
- Rename the planned `near_term` report to `hourly`.
- Do not keep both `near_term` and `hourly` public report IDs.
- Do not add a generic workflow engine or plugin system.
## Hourly Report Target
The planned `near_term` report should become the `hourly` report.
Target report identity:
- 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 near-term 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.

File diff suppressed because it is too large Load Diff