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
|
Implemented Markdown `generate` commands write a JSON module snapshot, YAML
|
||||||
data package, preflight artifact, managed Markdown report, and metadata under
|
data package, preflight artifact, managed Markdown report, and metadata under
|
||||||
the configured workspace. `--out` writes an extra Markdown copy for the
|
the configured workspace. `--out` writes an extra Markdown copy for the
|
||||||
operator; distributor notification uses the managed report path, not the extra
|
operator on those Markdown-path commands; distributor notification uses the
|
||||||
copy. `generate hourly` is parsed and resolved, covers the next six hours in
|
managed report path, not the extra copy. `generate hourly` covers the next six
|
||||||
the effective report timezone, and does not accept date or event window flags;
|
hours in the effective report timezone, does not accept date or event window
|
||||||
its rendering workflow is not implemented yet. `generate storm` requires
|
flags, writes managed generated-text artifacts, validates the structured text,
|
||||||
explicit event-window bounds with `--start` and `--end`.
|
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
|
`run morning` generates Daily Today and the 3-Day Outlook, plus Weekend Outlook
|
||||||
except on Sunday. `run evening` generates the Tomorrow Planning Brief. Batch
|
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`.
|
- `--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`.
|
- `--units VALUE`: override configured Weather API units for `generate` and `run`.
|
||||||
- `--tz NAME`: override configured Weather API timezone 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`.
|
- `--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.
|
- `--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`.
|
- `--start TIME`: required start time for `generate storm`.
|
||||||
@@ -83,6 +85,7 @@ are no distributor-specific CLI flags.
|
|||||||
|
|
||||||
```sh
|
```sh
|
||||||
weatherreporter generate tomorrow --out ./tomorrow.md
|
weatherreporter generate tomorrow --out ./tomorrow.md
|
||||||
|
weatherreporter generate hourly
|
||||||
weatherreporter generate three-day --out ./three-day.md
|
weatherreporter generate three-day --out ./three-day.md
|
||||||
weatherreporter generate weekend --out ./weekend.md
|
weatherreporter generate weekend --out ./weekend.md
|
||||||
weatherreporter generate storm --start 2026-05-29T18:00 --end 2026-05-30T06:00 --out ./storm.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,
|
- generated report results with JSON module snapshot, YAML data package,
|
||||||
preflight, report, metadata, prior snapshot, Recent Changes, Scriptorium
|
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
|
- batch summaries with per-report status, artifact paths, error text, and
|
||||||
notification outcome when attempted
|
notification outcome when attempted
|
||||||
- saved Weather API bundle JSON for fetch workflows
|
- saved Weather API bundle JSON for fetch workflows
|
||||||
@@ -63,7 +64,7 @@ defaults.
|
|||||||
|
|
||||||
## Generation Workflow
|
## Generation Workflow
|
||||||
|
|
||||||
Single-report generation follows this order:
|
Single-report generation shares this setup:
|
||||||
|
|
||||||
1. Resolve the command report to a `report.Resolved` value.
|
1. Resolve the command report to a `report.Resolved` value.
|
||||||
2. Create or use a filesystem store.
|
2. Create or use a filesystem store.
|
||||||
@@ -76,13 +77,26 @@ Single-report generation follows this order:
|
|||||||
9. Run Scriptorium render preflight.
|
9. Run Scriptorium render preflight.
|
||||||
10. Save preflight JSON when a render result is available.
|
10. Save preflight JSON when a render result is available.
|
||||||
11. Save metadata for inspection.
|
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.
|
For `scriptorium_markdown` reports, generation then:
|
||||||
14. Save metadata with the managed report path.
|
|
||||||
15. If distributor notification is enabled, notify using the managed report
|
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.
|
path as the source file.
|
||||||
16. Save a distributor notification debug artifact and update metadata with its
|
16. Saves a distributor notification debug artifact and updates metadata with
|
||||||
path.
|
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
|
If render preflight returns both a result and an error, preflight JSON and
|
||||||
metadata are persisted before the error is returned. If Scriptorium report
|
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
|
- Generated reports use the same app request and result types regardless of
|
||||||
report ID.
|
report ID.
|
||||||
- Render preflight precedes Scriptorium report generation.
|
- 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.
|
- Recent Changes are computed from structured module snapshots.
|
||||||
- Metadata links artifacts produced for a run.
|
- Metadata links artifacts produced for a run.
|
||||||
- Distributor notification maps the managed Markdown report path to configured
|
- Distributor notification maps the managed Markdown report path to configured
|
||||||
|
|||||||
@@ -26,10 +26,11 @@ Each report definition declares:
|
|||||||
- morning or evening batch membership
|
- morning or evening batch membership
|
||||||
- default ordered module composition
|
- default ordered module composition
|
||||||
|
|
||||||
Current Markdown report definitions use the `scriptorium_markdown` generation
|
Markdown report definitions use the `scriptorium_markdown` generation mode.
|
||||||
mode. Their template and structured-text schema identifiers are empty. Hourly
|
Their template and structured-text schema identifiers are empty. Hourly Report
|
||||||
Report declares `generated_text_template` with template ID `hourly` and schema
|
declares `generated_text_template` with template ID `hourly` and schema ID
|
||||||
ID `hourly`; the app rendering workflow for that mode is not implemented yet.
|
`hourly`; the app uses those identifiers to validate generated text and render
|
||||||
|
the embedded hourly template.
|
||||||
|
|
||||||
## Reports
|
## Reports
|
||||||
|
|
||||||
|
|||||||
@@ -11,24 +11,27 @@ Generation commands:
|
|||||||
```text
|
```text
|
||||||
weatherreporter generate daily --date 2026-05-29
|
weatherreporter generate daily --date 2026-05-29
|
||||||
weatherreporter generate tomorrow
|
weatherreporter generate tomorrow
|
||||||
|
weatherreporter generate hourly
|
||||||
weatherreporter generate three-day
|
weatherreporter generate three-day
|
||||||
weatherreporter generate weekend
|
weatherreporter generate weekend
|
||||||
weatherreporter generate storm --start 2026-05-29T18:00 --end 2026-05-30T06:00
|
weatherreporter generate storm --start 2026-05-29T18:00 --end 2026-05-30T06:00
|
||||||
```
|
```
|
||||||
|
|
||||||
Each implemented Markdown generation command resolves a report period, fetches
|
Markdown-path generation commands resolve a report period, fetch a Weather API
|
||||||
a Weather API bundle, builds a JSON module snapshot, builds a YAML prompt input
|
bundle, build a JSON module snapshot, build a YAML prompt input data package,
|
||||||
data package, runs `scriptorium render`, runs `scriptorium run`, and writes
|
run `scriptorium render`, run `scriptorium run`, and write managed artifacts
|
||||||
managed artifacts under the configured workspace.
|
under the configured workspace. When distributor notification is enabled,
|
||||||
When distributor notification is enabled, weatherreporter uploads the managed
|
weatherreporter uploads the managed Markdown report after `scriptorium run`
|
||||||
Markdown report after `scriptorium run` succeeds and final metadata is saved.
|
succeeds and final metadata is saved. `--out PATH` writes an extra Markdown
|
||||||
`--out PATH` writes an extra Markdown copy for the current generated report; it
|
copy for Markdown-path generated reports; it is not used as the distributor
|
||||||
is not used as the distributor upload source.
|
upload source.
|
||||||
|
|
||||||
`generate hourly` is an explicit command shell. It covers the six-hour rolling
|
`generate hourly` covers the six-hour rolling period from generation time in
|
||||||
period from generation time in the effective report timezone and is not part of
|
the effective report timezone and is not part of scheduled morning or evening
|
||||||
scheduled morning or evening batches, but its rendering workflow is not
|
batches. It builds the same module snapshot and data package, runs
|
||||||
implemented yet.
|
`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:
|
Batch commands:
|
||||||
|
|
||||||
@@ -71,6 +74,10 @@ workspace/
|
|||||||
YYYY-MM-DD/
|
YYYY-MM-DD/
|
||||||
<run_id>.modules.json
|
<run_id>.modules.json
|
||||||
<run_id>.metadata.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/
|
storm/
|
||||||
YYYY-MM-DD/
|
YYYY-MM-DD/
|
||||||
<run_id>.modules.json
|
<run_id>.modules.json
|
||||||
@@ -156,6 +163,7 @@ Each generated report writes metadata that links:
|
|||||||
- prompt input data package path
|
- prompt input data package path
|
||||||
- preflight output path
|
- preflight output path
|
||||||
- managed Markdown report 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
|
- distributor notification debug artifact path, when notification is attempted
|
||||||
|
|
||||||
Batch summaries include report status, error text when applicable, notification
|
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
|
the valid-period start date, artifact group, and RunID. Distributor owns
|
||||||
destination merge, retention, and derived snapshot behavior such as `latest`.
|
destination merge, retention, and derived snapshot behavior such as `latest`.
|
||||||
|
|
||||||
Notification happens after final metadata save. Weather API, module snapshot,
|
Notification happens after final metadata save for Markdown-path generation.
|
||||||
data-package, render preflight, Scriptorium run, and metadata-save failures do
|
Weather API, module snapshot, data-package, render preflight, Scriptorium run,
|
||||||
not trigger notification. A notification failure fails that report.
|
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
|
In a batch, other reports continue, the failed report includes notification
|
||||||
fields in the JSON summary, and the batch returns nonzero.
|
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/facts"
|
||||||
"gitea.maximumdirect.net/eric/weatherreporter/internal/fileutil"
|
"gitea.maximumdirect.net/eric/weatherreporter/internal/fileutil"
|
||||||
"gitea.maximumdirect.net/eric/weatherreporter/internal/forecast"
|
"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/module"
|
||||||
"gitea.maximumdirect.net/eric/weatherreporter/internal/promptinput"
|
"gitea.maximumdirect.net/eric/weatherreporter/internal/promptinput"
|
||||||
"gitea.maximumdirect.net/eric/weatherreporter/internal/report"
|
"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/state"
|
||||||
"gitea.maximumdirect.net/eric/weatherreporter/internal/timeutil"
|
"gitea.maximumdirect.net/eric/weatherreporter/internal/timeutil"
|
||||||
"gitea.maximumdirect.net/eric/weatherreporter/internal/weatherdata"
|
"gitea.maximumdirect.net/eric/weatherreporter/internal/weatherdata"
|
||||||
@@ -89,21 +91,26 @@ type ReportRequest struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type ReportResult struct {
|
type ReportResult struct {
|
||||||
ModuleSnapshot module.Snapshot
|
ModuleSnapshot module.Snapshot
|
||||||
ModuleSnapshotPath string
|
ModuleSnapshotPath string
|
||||||
DataPackage promptinput.Package
|
DataPackage promptinput.Package
|
||||||
DataPackagePath string
|
DataPackagePath string
|
||||||
PreflightPath string
|
PreflightPath string
|
||||||
ReportPath string
|
ReportPath string
|
||||||
OutputPath string
|
OutputPath string
|
||||||
NotificationPath string
|
NotificationPath string
|
||||||
Metadata state.Metadata
|
Metadata state.Metadata
|
||||||
MetadataPath string
|
MetadataPath string
|
||||||
PriorSnapshot *state.PriorSnapshot
|
PriorSnapshot *state.PriorSnapshot
|
||||||
RecentChanges []changes.Change
|
RecentChanges []changes.Change
|
||||||
RenderResult *scriptorium.RenderResult
|
RenderResult *scriptorium.RenderResult
|
||||||
RunResult *scriptorium.RunResult
|
RunResult *scriptorium.RunResult
|
||||||
Notification *NotificationResult
|
StructuredRunResult *scriptorium.StructuredRunResult
|
||||||
|
GeneratedTextRawPath string
|
||||||
|
GeneratedTextResultPath string
|
||||||
|
GeneratedTextPath string
|
||||||
|
RenderContextPath string
|
||||||
|
Notification *NotificationResult
|
||||||
}
|
}
|
||||||
|
|
||||||
type BatchResult struct {
|
type BatchResult struct {
|
||||||
@@ -151,6 +158,7 @@ func (e BatchError) Error() string {
|
|||||||
type Renderer interface {
|
type Renderer interface {
|
||||||
Render(context.Context, scriptorium.RenderRequest) (*scriptorium.RenderResult, error)
|
Render(context.Context, scriptorium.RenderRequest) (*scriptorium.RenderResult, error)
|
||||||
Run(context.Context, scriptorium.RunRequest) (*scriptorium.RunResult, error)
|
Run(context.Context, scriptorium.RunRequest) (*scriptorium.RunResult, error)
|
||||||
|
StructuredRun(context.Context, scriptorium.StructuredRunRequest) (*scriptorium.StructuredRunResult, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type Notifier interface {
|
type Notifier interface {
|
||||||
@@ -438,9 +446,6 @@ func FetchAndSaveBundle(ctx context.Context, req FetchBundleRequest) (*weatherda
|
|||||||
}
|
}
|
||||||
|
|
||||||
func GenerateReport(ctx context.Context, req ReportRequest) (*ReportResult, error) {
|
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
|
store := req.Store
|
||||||
if store == nil {
|
if store == nil {
|
||||||
defaultStore, err := defaultStore(req.Config)
|
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))
|
briefingMetadata := briefing.BuildMetadata(briefingBuildContext(req.Config, req.Resolved, reportFacts.Collected))
|
||||||
metadata := state.BuildMetadataFromBriefingMetadata(req.Resolved, briefingMetadata, state.ArtifactPaths{
|
metadata := state.BuildMetadataFromBriefingMetadata(req.Resolved, briefingMetadata, state.ArtifactPaths{
|
||||||
ModuleSnapshot: moduleSnapshotPath,
|
ModuleSnapshot: moduleSnapshotPath,
|
||||||
Metadata: paths.Metadata,
|
Metadata: paths.Metadata,
|
||||||
DataPackage: paths.DataPackage,
|
DataPackage: paths.DataPackage,
|
||||||
Preflight: paths.Preflight,
|
Preflight: paths.Preflight,
|
||||||
RenderedReport: paths.RenderedReport,
|
RenderedReport: paths.RenderedReport,
|
||||||
|
GeneratedTextRaw: paths.GeneratedTextRaw,
|
||||||
|
GeneratedTextResult: paths.GeneratedTextResult,
|
||||||
|
GeneratedText: paths.GeneratedText,
|
||||||
|
RenderContext: paths.RenderContext,
|
||||||
})
|
})
|
||||||
dataPackage, err := promptinput.Build(promptinput.BuildRequest{
|
dataPackage, err := promptinput.Build(promptinput.BuildRequest{
|
||||||
Metadata: promptMetadata(metadata),
|
Metadata: promptMetadata(metadata),
|
||||||
@@ -540,6 +549,28 @@ func GenerateReport(ctx context.Context, req ReportRequest) (*ReportResult, erro
|
|||||||
return nil, renderErr
|
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)
|
reportPath, err := store.PrepareRenderedReport(ctx, req.Resolved)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -598,6 +629,107 @@ func GenerateReport(ctx context.Context, req ReportRequest) (*ReportResult, erro
|
|||||||
}, nil
|
}, 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) {
|
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)
|
notifier, enabled := reportNotifier(cfg, notifier)
|
||||||
if !enabled {
|
if !enabled {
|
||||||
@@ -941,3 +1073,21 @@ func preflightArtifact(result *scriptorium.RenderResult) state.PreflightArtifact
|
|||||||
ExitCode: result.ExitCode,
|
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 := config.Defaults()
|
||||||
|
cfg.WeatherAPI.BaseURL = server.URL + "/"
|
||||||
|
cfg.WeatherAPI.Timezone = "America/Chicago"
|
||||||
cfg.Workspace.Root = t.TempDir()
|
cfg.Workspace.Root = t.TempDir()
|
||||||
|
cfg.Location.ID = "home"
|
||||||
|
cfg.Location.Name = "Brentwood"
|
||||||
|
cfg.Location.Region = "MO"
|
||||||
resolved, err := ResolveGenerate(GenerateRequest{
|
resolved, err := ResolveGenerate(GenerateRequest{
|
||||||
Config: cfg,
|
Config: cfg,
|
||||||
Report: ReportHourly,
|
Report: ReportHourly,
|
||||||
@@ -362,14 +368,136 @@ func TestGenerateHourlyReportModeNotImplemented(t *testing.T) {
|
|||||||
if resolved.Definition.GenerationMode != report.GenerationModeGeneratedTextTemplate {
|
if resolved.Definition.GenerationMode != report.GenerationModeGeneratedTextTemplate {
|
||||||
t.Fatalf("GenerationMode = %q, want generated text template", resolved.Definition.GenerationMode)
|
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,
|
Config: cfg,
|
||||||
Resolved: resolved,
|
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"`) {
|
if err != nil {
|
||||||
t.Fatalf("GenerateReport() error = %v, want generation mode not implemented", err)
|
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 {
|
type recordingRenderer struct {
|
||||||
renderCalls int
|
renderCalls int
|
||||||
runCalls int
|
runCalls int
|
||||||
renderRequest scriptorium.RenderRequest
|
structuredRunCalls int
|
||||||
runRequest scriptorium.RunRequest
|
renderRequest scriptorium.RenderRequest
|
||||||
renderResult *scriptorium.RenderResult
|
runRequest scriptorium.RunRequest
|
||||||
runResult *scriptorium.RunResult
|
structuredRunRequest scriptorium.StructuredRunRequest
|
||||||
err error
|
renderResult *scriptorium.RenderResult
|
||||||
runErr error
|
runResult *scriptorium.RunResult
|
||||||
runBody string
|
structuredRunResult *scriptorium.StructuredRunResult
|
||||||
|
err error
|
||||||
|
runErr error
|
||||||
|
structuredRunErr error
|
||||||
|
runBody string
|
||||||
|
structuredRunBody string
|
||||||
}
|
}
|
||||||
|
|
||||||
type recordingStore struct {
|
type recordingStore struct {
|
||||||
@@ -1746,6 +1879,26 @@ func (s *recordingStore) SavePreflight(ctx context.Context, resolved report.Reso
|
|||||||
return s.Store.SavePreflight(ctx, resolved, artifact)
|
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) {
|
func (s *recordingStore) PrepareRenderedReport(ctx context.Context, resolved report.Resolved) (string, error) {
|
||||||
s.calls = append(s.calls, "prepare_report")
|
s.calls = append(s.calls, "prepare_report")
|
||||||
return s.Store.PrepareRenderedReport(ctx, resolved)
|
return s.Store.PrepareRenderedReport(ctx, resolved)
|
||||||
@@ -1765,10 +1918,11 @@ func successfulRenderer(body string) *recordingRenderer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type selectiveRenderer struct {
|
type selectiveRenderer struct {
|
||||||
renderCalls int
|
renderCalls int
|
||||||
runCalls int
|
runCalls int
|
||||||
failRenderPrompt string
|
structuredRunCalls int
|
||||||
runBody string
|
failRenderPrompt string
|
||||||
|
runBody string
|
||||||
}
|
}
|
||||||
|
|
||||||
type recordingNotifier struct {
|
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
|
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) {
|
func (r *recordingRenderer) Render(_ context.Context, req scriptorium.RenderRequest) (*scriptorium.RenderResult, error) {
|
||||||
r.renderCalls++
|
r.renderCalls++
|
||||||
r.renderRequest = req
|
r.renderRequest = req
|
||||||
@@ -1845,3 +2004,17 @@ func (r *recordingRenderer) Run(_ context.Context, req scriptorium.RunRequest) (
|
|||||||
}
|
}
|
||||||
return r.runResult, r.runErr
|
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.
|
--config PATH Load configuration from PATH instead of /usr/local/etc/weatherreporter/config.yml.
|
||||||
--units VALUE Override weather API units.
|
--units VALUE Override weather API units.
|
||||||
--tz NAME Override weather API timezone.
|
--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.
|
--out-dir PATH Write extra Markdown report copies for run commands.
|
||||||
`
|
`
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user