Wire hourly generated text rendering

This commit is contained in:
2026-06-14 05:01:26 +00:00
parent 2a4ce64d6f
commit 422430613c
7 changed files with 428 additions and 75 deletions

View File

@@ -38,11 +38,13 @@ weatherreporter inspect sources [--config PATH] RUN_ID
Implemented Markdown `generate` commands write a JSON module snapshot, YAML
data package, preflight artifact, managed Markdown report, and metadata under
the configured workspace. `--out` writes an extra Markdown copy for the
operator; distributor notification uses the managed report path, not the extra
copy. `generate hourly` is parsed and resolved, covers the next six hours in
the effective report timezone, and does not accept date or event window flags;
its rendering workflow is not implemented yet. `generate storm` requires
explicit event-window bounds with `--start` and `--end`.
operator on those Markdown-path commands; distributor notification uses the
managed report path, not the extra copy. `generate hourly` covers the next six
hours in the effective report timezone, does not accept date or event window
flags, writes managed generated-text artifacts, validates the structured text,
and renders the managed Markdown report from the embedded hourly template.
`generate storm` requires explicit event-window bounds with `--start` and
`--end`.
`run morning` generates Daily Today and the 3-Day Outlook, plus Weekend Outlook
except on Sunday. `run evening` generates the Tomorrow Planning Brief. Batch
@@ -66,7 +68,7 @@ They do not fetch weather data or invoke `scriptorium`.
- `--config PATH`: load configuration from `PATH` instead of `/usr/local/etc/weatherreporter/config.yml`.
- `--units VALUE`: override configured Weather API units for `generate` and `run`.
- `--tz NAME`: override configured Weather API timezone for `generate` and `run`.
- `--out PATH`: write an extra Markdown report copy for `generate` commands.
- `--out PATH`: write an extra Markdown report copy where supported by the `generate` command.
- `--out-dir PATH`: write extra Markdown report copies for `run morning` and `run evening`.
- `--date YYYY-MM-DD`: optional date for `generate daily`; defaults to the current local date in the configured timezone.
- `--start TIME`: required start time for `generate storm`.
@@ -83,6 +85,7 @@ are no distributor-specific CLI flags.
```sh
weatherreporter generate tomorrow --out ./tomorrow.md
weatherreporter generate hourly
weatherreporter generate three-day --out ./three-day.md
weatherreporter generate weekend --out ./weekend.md
weatherreporter generate storm --start 2026-05-29T18:00 --end 2026-05-30T06:00 --out ./storm.md

View File

@@ -28,7 +28,8 @@ Outputs:
- generated report results with JSON module snapshot, YAML data package,
preflight, report, metadata, prior snapshot, Recent Changes, Scriptorium
result details, and notification result when attempted
result details, generated-text artifact paths when applicable, and
notification result when attempted
- batch summaries with per-report status, artifact paths, error text, and
notification outcome when attempted
- saved Weather API bundle JSON for fetch workflows
@@ -63,7 +64,7 @@ defaults.
## Generation Workflow
Single-report generation follows this order:
Single-report generation shares this setup:
1. Resolve the command report to a `report.Resolved` value.
2. Create or use a filesystem store.
@@ -76,13 +77,26 @@ Single-report generation follows this order:
9. Run Scriptorium render preflight.
10. Save preflight JSON when a render result is available.
11. Save metadata for inspection.
12. Run Scriptorium report generation to the managed report path.
13. Copy the managed report to the requested `--out` path when provided.
14. Save metadata with the managed report path.
15. If distributor notification is enabled, notify using the managed report
For `scriptorium_markdown` reports, generation then:
12. Runs Scriptorium report generation to the managed report path.
13. Copies the managed report to the requested `--out` path when provided.
14. Saves metadata with the managed report path.
15. If distributor notification is enabled, notifies using the managed report
path as the source file.
16. Save a distributor notification debug artifact and update metadata with its
path.
16. Saves a distributor notification debug artifact and updates metadata with
its path.
For `generated_text_template` reports, generation then:
12. Runs structured Scriptorium generation to the raw generated-text JSON path.
13. Saves the structured Scriptorium run result.
14. Validates and saves normalized generated text.
15. Builds and saves a typed render context.
16. Renders Markdown from the embedded template to the managed report path.
17. Saves final metadata with generated-text paths, render context path, schema
ID, and managed report path.
If render preflight returns both a result and an error, preflight JSON and
metadata are persisted before the error is returned. If Scriptorium report
@@ -142,6 +156,8 @@ Inspect:
- Generated reports use the same app request and result types regardless of
report ID.
- Render preflight precedes Scriptorium report generation.
- Generated-text reports render Markdown from a curated render context, not from
a raw data package.
- Recent Changes are computed from structured module snapshots.
- Metadata links artifacts produced for a run.
- Distributor notification maps the managed Markdown report path to configured

View File

@@ -26,10 +26,11 @@ Each report definition declares:
- morning or evening batch membership
- default ordered module composition
Current Markdown report definitions use the `scriptorium_markdown` generation
mode. Their template and structured-text schema identifiers are empty. Hourly
Report declares `generated_text_template` with template ID `hourly` and schema
ID `hourly`; the app rendering workflow for that mode is not implemented yet.
Markdown report definitions use the `scriptorium_markdown` generation mode.
Their template and structured-text schema identifiers are empty. Hourly Report
declares `generated_text_template` with template ID `hourly` and schema ID
`hourly`; the app uses those identifiers to validate generated text and render
the embedded hourly template.
## Reports

View File

@@ -11,24 +11,27 @@ Generation commands:
```text
weatherreporter generate daily --date 2026-05-29
weatherreporter generate tomorrow
weatherreporter generate hourly
weatherreporter generate three-day
weatherreporter generate weekend
weatherreporter generate storm --start 2026-05-29T18:00 --end 2026-05-30T06:00
```
Each implemented Markdown generation command resolves a report period, fetches
a Weather API bundle, builds a JSON module snapshot, builds a YAML prompt input
data package, runs `scriptorium render`, runs `scriptorium run`, and writes
managed artifacts under the configured workspace.
When distributor notification is enabled, weatherreporter uploads the managed
Markdown report after `scriptorium run` succeeds and final metadata is saved.
`--out PATH` writes an extra Markdown copy for the current generated report; it
is not used as the distributor upload source.
Markdown-path generation commands resolve a report period, fetch a Weather API
bundle, build a JSON module snapshot, build a YAML prompt input data package,
run `scriptorium render`, run `scriptorium run`, and write managed artifacts
under the configured workspace. When distributor notification is enabled,
weatherreporter uploads the managed Markdown report after `scriptorium run`
succeeds and final metadata is saved. `--out PATH` writes an extra Markdown
copy for Markdown-path generated reports; it is not used as the distributor
upload source.
`generate hourly` is an explicit command shell. It covers the six-hour rolling
period from generation time in the effective report timezone and is not part of
scheduled morning or evening batches, but its rendering workflow is not
implemented yet.
`generate hourly` covers the six-hour rolling period from generation time in
the effective report timezone and is not part of scheduled morning or evening
batches. It builds the same module snapshot and data package, runs
`scriptorium render` as preflight, runs structured `scriptorium run` to raw
GeneratedText JSON, validates the structured text, saves a render context, and
renders the managed Markdown report from the embedded hourly template.
Batch commands:
@@ -71,6 +74,10 @@ workspace/
YYYY-MM-DD/
<run_id>.modules.json
<run_id>.metadata.json
<run_id>.generated_text.raw.json
<run_id>.generated_text.run.json
<run_id>.generated_text.json
<run_id>.render_context.json
storm/
YYYY-MM-DD/
<run_id>.modules.json
@@ -156,6 +163,7 @@ Each generated report writes metadata that links:
- prompt input data package path
- preflight output path
- managed Markdown report path
- generated text schema ID and generated-text artifact paths for Hourly Report
- distributor notification debug artifact path, when notification is attempted
Batch summaries include report status, error text when applicable, notification
@@ -185,9 +193,11 @@ report generation has a distinct retry identity. The default bundle path uses
the valid-period start date, artifact group, and RunID. Distributor owns
destination merge, retention, and derived snapshot behavior such as `latest`.
Notification happens after final metadata save. Weather API, module snapshot,
data-package, render preflight, Scriptorium run, and metadata-save failures do
not trigger notification. A notification failure fails that report.
Notification happens after final metadata save for Markdown-path generation.
Weather API, module snapshot, data-package, render preflight, Scriptorium run,
and metadata-save failures do not trigger notification. Hourly generated-text
reports write the managed Markdown report and generated-text artifacts but do
not notify distributor yet. A notification failure fails that report.
In a batch, other reports continue, the failed report includes notification
fields in the JSON summary, and the batch returns nonzero.

View File

@@ -17,9 +17,11 @@ import (
"gitea.maximumdirect.net/eric/weatherreporter/internal/facts"
"gitea.maximumdirect.net/eric/weatherreporter/internal/fileutil"
"gitea.maximumdirect.net/eric/weatherreporter/internal/forecast"
"gitea.maximumdirect.net/eric/weatherreporter/internal/generatedtext"
"gitea.maximumdirect.net/eric/weatherreporter/internal/module"
"gitea.maximumdirect.net/eric/weatherreporter/internal/promptinput"
"gitea.maximumdirect.net/eric/weatherreporter/internal/report"
"gitea.maximumdirect.net/eric/weatherreporter/internal/reporttemplate"
"gitea.maximumdirect.net/eric/weatherreporter/internal/state"
"gitea.maximumdirect.net/eric/weatherreporter/internal/timeutil"
"gitea.maximumdirect.net/eric/weatherreporter/internal/weatherdata"
@@ -89,21 +91,26 @@ type ReportRequest struct {
}
type ReportResult struct {
ModuleSnapshot module.Snapshot
ModuleSnapshotPath string
DataPackage promptinput.Package
DataPackagePath string
PreflightPath string
ReportPath string
OutputPath string
NotificationPath string
Metadata state.Metadata
MetadataPath string
PriorSnapshot *state.PriorSnapshot
RecentChanges []changes.Change
RenderResult *scriptorium.RenderResult
RunResult *scriptorium.RunResult
Notification *NotificationResult
ModuleSnapshot module.Snapshot
ModuleSnapshotPath string
DataPackage promptinput.Package
DataPackagePath string
PreflightPath string
ReportPath string
OutputPath string
NotificationPath string
Metadata state.Metadata
MetadataPath string
PriorSnapshot *state.PriorSnapshot
RecentChanges []changes.Change
RenderResult *scriptorium.RenderResult
RunResult *scriptorium.RunResult
StructuredRunResult *scriptorium.StructuredRunResult
GeneratedTextRawPath string
GeneratedTextResultPath string
GeneratedTextPath string
RenderContextPath string
Notification *NotificationResult
}
type BatchResult struct {
@@ -151,6 +158,7 @@ func (e BatchError) Error() string {
type Renderer interface {
Render(context.Context, scriptorium.RenderRequest) (*scriptorium.RenderResult, error)
Run(context.Context, scriptorium.RunRequest) (*scriptorium.RunResult, error)
StructuredRun(context.Context, scriptorium.StructuredRunRequest) (*scriptorium.StructuredRunResult, error)
}
type Notifier interface {
@@ -438,9 +446,6 @@ func FetchAndSaveBundle(ctx context.Context, req FetchBundleRequest) (*weatherda
}
func GenerateReport(ctx context.Context, req ReportRequest) (*ReportResult, error) {
if req.Resolved.Definition.GenerationMode != report.GenerationModeScriptoriumMarkdown {
return nil, fmt.Errorf("generation mode %q is not implemented for report %q", req.Resolved.Definition.GenerationMode, req.Resolved.Definition.ID)
}
store := req.Store
if store == nil {
defaultStore, err := defaultStore(req.Config)
@@ -488,11 +493,15 @@ func GenerateReport(ctx context.Context, req ReportRequest) (*ReportResult, erro
}
briefingMetadata := briefing.BuildMetadata(briefingBuildContext(req.Config, req.Resolved, reportFacts.Collected))
metadata := state.BuildMetadataFromBriefingMetadata(req.Resolved, briefingMetadata, state.ArtifactPaths{
ModuleSnapshot: moduleSnapshotPath,
Metadata: paths.Metadata,
DataPackage: paths.DataPackage,
Preflight: paths.Preflight,
RenderedReport: paths.RenderedReport,
ModuleSnapshot: moduleSnapshotPath,
Metadata: paths.Metadata,
DataPackage: paths.DataPackage,
Preflight: paths.Preflight,
RenderedReport: paths.RenderedReport,
GeneratedTextRaw: paths.GeneratedTextRaw,
GeneratedTextResult: paths.GeneratedTextResult,
GeneratedText: paths.GeneratedText,
RenderContext: paths.RenderContext,
})
dataPackage, err := promptinput.Build(promptinput.BuildRequest{
Metadata: promptMetadata(metadata),
@@ -540,6 +549,28 @@ func GenerateReport(ctx context.Context, req ReportRequest) (*ReportResult, erro
return nil, renderErr
}
if req.Resolved.Definition.GenerationMode == report.GenerationModeGeneratedTextTemplate {
return generateTextTemplateReport(ctx, generatedReportRequest{
ReportRequest: req,
store: store,
paths: paths,
moduleSnapshot: moduleSnapshot,
moduleSnapshotPath: moduleSnapshotPath,
dataPackage: dataPackage,
dataPackagePath: dataPackagePath,
briefingMetadata: briefingMetadata,
metadata: metadata,
metadataPath: metadataPath,
preflightPath: preflightPath,
recentChanges: recentChanges,
renderResult: renderResult,
renderer: renderer,
})
}
if req.Resolved.Definition.GenerationMode != report.GenerationModeScriptoriumMarkdown {
return nil, fmt.Errorf("generation mode %q is not supported for report %q", req.Resolved.Definition.GenerationMode, req.Resolved.Definition.ID)
}
reportPath, err := store.PrepareRenderedReport(ctx, req.Resolved)
if err != nil {
return nil, err
@@ -598,6 +629,107 @@ func GenerateReport(ctx context.Context, req ReportRequest) (*ReportResult, erro
}, nil
}
type generatedReportRequest struct {
ReportRequest
store state.Store
paths state.ArtifactPaths
moduleSnapshot module.Snapshot
moduleSnapshotPath string
dataPackage promptinput.Package
dataPackagePath string
briefingMetadata briefing.Metadata
metadata state.Metadata
metadataPath string
preflightPath string
recentChanges []changes.Change
renderResult *scriptorium.RenderResult
renderer Renderer
}
func generateTextTemplateReport(ctx context.Context, req generatedReportRequest) (*ReportResult, error) {
structuredResult, runErr := req.renderer.StructuredRun(ctx, scriptorium.StructuredRunRequest{
PromptID: req.Resolved.Definition.PromptID,
DataPackagePath: req.dataPackagePath,
OutputPath: req.paths.GeneratedTextRaw,
})
generatedTextResultPath := req.paths.GeneratedTextResult
if structuredResult != nil {
var err error
generatedTextResultPath, err = req.store.SaveGeneratedTextResult(ctx, req.Resolved, structuredResult)
if err != nil {
return nil, err
}
req.metadata.GeneratedTextResultPath = generatedTextResultPath
req.metadataPath, err = req.store.SaveMetadata(ctx, req.metadata)
if err != nil {
return nil, err
}
}
if runErr != nil {
return nil, runErr
}
rawGeneratedText, err := req.store.LoadGeneratedText(ctx, req.paths.GeneratedTextRaw)
if err != nil {
return nil, err
}
hourlyText, normalizedGeneratedText, err := validateGeneratedText(req.Resolved.Definition, rawGeneratedText)
if err != nil {
return nil, err
}
generatedTextPath, err := req.store.SaveGeneratedText(ctx, req.Resolved, normalizedGeneratedText)
if err != nil {
return nil, err
}
req.metadata.GeneratedTextPath = generatedTextPath
renderContext, err := buildRenderContext(req.Resolved.Definition, req.briefingMetadata, req.moduleSnapshot, hourlyText)
if err != nil {
return nil, err
}
renderContextPath, err := req.store.SaveRenderContext(ctx, req.Resolved, renderContext)
if err != nil {
return nil, err
}
req.metadata.RenderContextPath = renderContextPath
rendered, err := reporttemplate.Render(req.Resolved.Definition.TemplateID, renderContext)
if err != nil {
return nil, err
}
reportPath, err := req.store.PrepareRenderedReport(ctx, req.Resolved)
if err != nil {
return nil, err
}
if err := fileutil.WriteFileAtomic(reportPath, rendered); err != nil {
return nil, err
}
req.metadata.RenderedReportPath = reportPath
metadataPath, err := req.store.SaveMetadata(ctx, req.metadata)
if err != nil {
return nil, err
}
return &ReportResult{
ModuleSnapshot: req.moduleSnapshot,
ModuleSnapshotPath: req.moduleSnapshotPath,
DataPackage: req.dataPackage,
DataPackagePath: req.dataPackagePath,
PreflightPath: req.preflightPath,
ReportPath: reportPath,
OutputPath: reportPath,
Metadata: req.metadata,
MetadataPath: metadataPath,
RecentChanges: req.recentChanges,
RenderResult: req.renderResult,
StructuredRunResult: structuredResult,
GeneratedTextRawPath: req.paths.GeneratedTextRaw,
GeneratedTextResultPath: generatedTextResultPath,
GeneratedTextPath: generatedTextPath,
RenderContextPath: renderContextPath,
}, nil
}
func notifyReport(ctx context.Context, cfg config.Config, resolved report.Resolved, reportPath string, metadata state.Metadata, notifier Notifier, store state.Store) (*NotificationResult, string, error) {
notifier, enabled := reportNotifier(cfg, notifier)
if !enabled {
@@ -941,3 +1073,21 @@ func preflightArtifact(result *scriptorium.RenderResult) state.PreflightArtifact
ExitCode: result.ExitCode,
}
}
func validateGeneratedText(definition report.Definition, data []byte) (generatedtext.Hourly, []byte, error) {
switch definition.GeneratedTextSchemaID {
case "hourly":
return generatedtext.ValidateHourly(data)
default:
return generatedtext.Hourly{}, nil, fmt.Errorf("generated text schema %q is not supported for report %q", definition.GeneratedTextSchemaID, definition.ID)
}
}
func buildRenderContext(definition report.Definition, metadata briefing.Metadata, snapshot module.Snapshot, hourly generatedtext.Hourly) (any, error) {
switch definition.TemplateID {
case "hourly":
return generatedtext.BuildHourlyRenderContext(metadata, snapshot, hourly)
default:
return nil, fmt.Errorf("report template %q is not supported for report %q", definition.TemplateID, definition.ID)
}
}

View File

@@ -349,9 +349,15 @@ func TestGenerateReportOmitsSPCConvectiveDiscussionBelowThreshold(t *testing.T)
}
}
func TestGenerateHourlyReportModeNotImplemented(t *testing.T) {
func TestGenerateHourlyReportUsesGeneratedTextTemplateWorkflow(t *testing.T) {
server := hourlyBundleServer(t)
cfg := config.Defaults()
cfg.WeatherAPI.BaseURL = server.URL + "/"
cfg.WeatherAPI.Timezone = "America/Chicago"
cfg.Workspace.Root = t.TempDir()
cfg.Location.ID = "home"
cfg.Location.Name = "Brentwood"
cfg.Location.Region = "MO"
resolved, err := ResolveGenerate(GenerateRequest{
Config: cfg,
Report: ReportHourly,
@@ -362,14 +368,136 @@ func TestGenerateHourlyReportModeNotImplemented(t *testing.T) {
if resolved.Definition.GenerationMode != report.GenerationModeGeneratedTextTemplate {
t.Fatalf("GenerationMode = %q, want generated text template", resolved.Definition.GenerationMode)
}
filesystemStore, err := state.NewFilesystemStore(cfg.Workspace)
if err != nil {
t.Fatalf("NewFilesystemStore() error = %v", err)
}
store := &recordingStore{Store: filesystemStore}
renderer := &recordingRenderer{
renderResult: &scriptorium.RenderResult{
Command: []string{"scriptorium", "render"},
Stdout: `{"prepared":true}`,
ExitCode: 0,
},
structuredRunResult: &scriptorium.StructuredRunResult{
Command: []string{"scriptorium", "run"},
Stderr: "wrote generated text",
ExitCode: 0,
},
structuredRunBody: `{
"summary": " Storm chances increase through late morning. ",
"timing": "The main window is 10 AM to noon.",
"impacts": "Brief downpours may slow travel.",
"confidence": "Medium"
}`,
}
_, err = GenerateReport(context.Background(), ReportRequest{
result, err := GenerateReport(context.Background(), ReportRequest{
Config: cfg,
Resolved: resolved,
Renderer: successfulRenderer("# Hourly Report\n"),
Renderer: renderer,
Store: store,
})
if err == nil || !strings.Contains(err.Error(), `generation mode "generated_text_template" is not implemented for report "hourly"`) {
t.Fatalf("GenerateReport() error = %v, want generation mode not implemented", err)
if err != nil {
t.Fatalf("GenerateReport() error = %v", err)
}
if renderer.renderCalls != 1 {
t.Fatalf("render calls = %d, want 1", renderer.renderCalls)
}
if renderer.structuredRunCalls != 1 {
t.Fatalf("structured run calls = %d, want 1", renderer.structuredRunCalls)
}
if renderer.runCalls != 0 {
t.Fatalf("markdown run calls = %d, want none", renderer.runCalls)
}
if renderer.structuredRunRequest.OutputPath != result.GeneratedTextRawPath {
t.Fatalf("structured run OutputPath = %q, want %q", renderer.structuredRunRequest.OutputPath, result.GeneratedTextRawPath)
}
if renderer.structuredRunRequest.DataPackagePath != result.DataPackagePath {
t.Fatalf("structured run DataPackagePath = %q, want %q", renderer.structuredRunRequest.DataPackagePath, result.DataPackagePath)
}
if got, want := strings.Join(store.calls, ","), "module_snapshot,data_package,preflight,metadata,generated_text_result,metadata,generated_text,render_context,prepare_report,metadata"; got != want {
t.Fatalf("store calls = %v, want %s", store.calls, want)
}
assertPathsExist(t,
result.ModuleSnapshotPath,
result.DataPackagePath,
result.PreflightPath,
result.GeneratedTextRawPath,
result.GeneratedTextResultPath,
result.GeneratedTextPath,
result.RenderContextPath,
result.ReportPath,
result.MetadataPath,
)
raw, err := os.ReadFile(result.GeneratedTextRawPath)
if err != nil {
t.Fatalf("read raw generated text: %v", err)
}
if !strings.Contains(string(raw), `"summary": " Storm chances increase through late morning. "`) {
t.Fatalf("raw generated text was not preserved:\n%s", string(raw))
}
normalized, err := os.ReadFile(result.GeneratedTextPath)
if err != nil {
t.Fatalf("read validated generated text: %v", err)
}
if string(normalized) != `{"summary":"Storm chances increase through late morning.","timing":"The main window is 10 AM to noon.","impacts":"Brief downpours may slow travel.","confidence":"Medium"}` {
t.Fatalf("validated generated text = %s, want normalized JSON", normalized)
}
renderContext, err := os.ReadFile(result.RenderContextPath)
if err != nil {
t.Fatalf("read render context: %v", err)
}
if !strings.Contains(string(renderContext), `"ReportTitle": "Hourly Report"`) || !strings.Contains(string(renderContext), `"LocationName": "Brentwood, MO"`) {
t.Fatalf("render context missing deterministic fields:\n%s", string(renderContext))
}
reportData, err := os.ReadFile(result.ReportPath)
if err != nil {
t.Fatalf("read report: %v", err)
}
reportText := string(reportData)
for _, want := range []string{
"# Hourly Report",
"Storm chances increase through late morning.",
"The main window is 10 AM to noon.",
"Flood Watch: Flooding possible (Moderate)",
"Short-term AFD narrative for hourly report.",
"Hourly Storm Chances - Scattered showers and thunderstorms are possible.",
} {
if !strings.Contains(reportText, want) {
t.Fatalf("rendered hourly report missing %q:\n%s", want, reportText)
}
}
if result.OutputPath != result.ReportPath {
t.Fatalf("OutputPath = %q, want managed report path %q", result.OutputPath, result.ReportPath)
}
if result.StructuredRunResult == nil || result.StructuredRunResult.OutputPath != result.GeneratedTextRawPath {
t.Fatalf("StructuredRunResult = %#v, want captured structured run result", result.StructuredRunResult)
}
if result.RunResult != nil {
t.Fatalf("RunResult = %#v, want nil for generated-text template workflow", result.RunResult)
}
if len(result.RecentChanges) != 0 {
t.Fatalf("RecentChanges = %#v, want none for hourly report", result.RecentChanges)
}
if result.Metadata.GeneratedTextSchemaID != "hourly" ||
result.Metadata.GeneratedTextRawPath != result.GeneratedTextRawPath ||
result.Metadata.GeneratedTextResultPath != result.GeneratedTextResultPath ||
result.Metadata.GeneratedTextPath != result.GeneratedTextPath ||
result.Metadata.RenderContextPath != result.RenderContextPath ||
result.Metadata.RenderedReportPath != result.ReportPath {
t.Fatalf("metadata generated-text links = %#v, want saved artifact links", result.Metadata)
}
metadataData, err := os.ReadFile(result.MetadataPath)
if err != nil {
t.Fatalf("read metadata: %v", err)
}
if !strings.Contains(string(metadataData), `"generatedTextSchemaId": "hourly"`) ||
!strings.Contains(string(metadataData), result.GeneratedTextRawPath) ||
!strings.Contains(string(metadataData), result.RenderContextPath) {
t.Fatalf("metadata JSON missing generated-text links:\n%s", string(metadataData))
}
}
@@ -1715,15 +1843,20 @@ func appBriefingMetadata(resolved report.Resolved) briefing.Metadata {
}
type recordingRenderer struct {
renderCalls int
runCalls int
renderRequest scriptorium.RenderRequest
runRequest scriptorium.RunRequest
renderResult *scriptorium.RenderResult
runResult *scriptorium.RunResult
err error
runErr error
runBody string
renderCalls int
runCalls int
structuredRunCalls int
renderRequest scriptorium.RenderRequest
runRequest scriptorium.RunRequest
structuredRunRequest scriptorium.StructuredRunRequest
renderResult *scriptorium.RenderResult
runResult *scriptorium.RunResult
structuredRunResult *scriptorium.StructuredRunResult
err error
runErr error
structuredRunErr error
runBody string
structuredRunBody string
}
type recordingStore struct {
@@ -1746,6 +1879,26 @@ func (s *recordingStore) SavePreflight(ctx context.Context, resolved report.Reso
return s.Store.SavePreflight(ctx, resolved, artifact)
}
func (s *recordingStore) SaveGeneratedTextRaw(ctx context.Context, resolved report.Resolved, data []byte) (string, error) {
s.calls = append(s.calls, "generated_text_raw")
return s.Store.SaveGeneratedTextRaw(ctx, resolved, data)
}
func (s *recordingStore) SaveGeneratedTextResult(ctx context.Context, resolved report.Resolved, value any) (string, error) {
s.calls = append(s.calls, "generated_text_result")
return s.Store.SaveGeneratedTextResult(ctx, resolved, value)
}
func (s *recordingStore) SaveGeneratedText(ctx context.Context, resolved report.Resolved, data []byte) (string, error) {
s.calls = append(s.calls, "generated_text")
return s.Store.SaveGeneratedText(ctx, resolved, data)
}
func (s *recordingStore) SaveRenderContext(ctx context.Context, resolved report.Resolved, value any) (string, error) {
s.calls = append(s.calls, "render_context")
return s.Store.SaveRenderContext(ctx, resolved, value)
}
func (s *recordingStore) PrepareRenderedReport(ctx context.Context, resolved report.Resolved) (string, error) {
s.calls = append(s.calls, "prepare_report")
return s.Store.PrepareRenderedReport(ctx, resolved)
@@ -1765,10 +1918,11 @@ func successfulRenderer(body string) *recordingRenderer {
}
type selectiveRenderer struct {
renderCalls int
runCalls int
failRenderPrompt string
runBody string
renderCalls int
runCalls int
structuredRunCalls int
failRenderPrompt string
runBody string
}
type recordingNotifier struct {
@@ -1826,6 +1980,11 @@ func (r *selectiveRenderer) Run(_ context.Context, req scriptorium.RunRequest) (
return &scriptorium.RunResult{ExitCode: 0, OutputPath: req.OutputPath}, nil
}
func (r *selectiveRenderer) StructuredRun(_ context.Context, req scriptorium.StructuredRunRequest) (*scriptorium.StructuredRunResult, error) {
r.structuredRunCalls++
return &scriptorium.StructuredRunResult{ExitCode: 0, OutputPath: req.OutputPath}, nil
}
func (r *recordingRenderer) Render(_ context.Context, req scriptorium.RenderRequest) (*scriptorium.RenderResult, error) {
r.renderCalls++
r.renderRequest = req
@@ -1845,3 +2004,17 @@ func (r *recordingRenderer) Run(_ context.Context, req scriptorium.RunRequest) (
}
return r.runResult, r.runErr
}
func (r *recordingRenderer) StructuredRun(_ context.Context, req scriptorium.StructuredRunRequest) (*scriptorium.StructuredRunResult, error) {
r.structuredRunCalls++
r.structuredRunRequest = req
if r.structuredRunBody != "" {
if err := os.WriteFile(req.OutputPath, []byte(r.structuredRunBody), 0o600); err != nil {
return nil, err
}
}
if r.structuredRunResult != nil {
r.structuredRunResult.OutputPath = req.OutputPath
}
return r.structuredRunResult, r.structuredRunErr
}

View File

@@ -37,7 +37,7 @@ Options:
--config PATH Load configuration from PATH instead of /usr/local/etc/weatherreporter/config.yml.
--units VALUE Override weather API units.
--tz NAME Override weather API timezone.
--out PATH Write an extra Markdown report copy for generate commands.
--out PATH Write an extra Markdown report copy where supported by the generate command.
--out-dir PATH Write extra Markdown report copies for run commands.
`