Wire hourly generated text rendering
This commit is contained in:
15
docs/cli.md
15
docs/cli.md
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
`
|
||||
|
||||
|
||||
Reference in New Issue
Block a user