Run report generation through module snapshots

This commit is contained in:
2026-06-09 21:12:13 +00:00
parent 0b516d9762
commit 479d144592
4 changed files with 121 additions and 31 deletions

View File

@@ -16,9 +16,10 @@ 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 command resolves a report period, fetches a Weather API bundle, builds a Each command resolves a report period, fetches a Weather API bundle, builds
briefing, builds a prompt input data package, runs `scriptorium render`, runs module snapshots, builds a YAML prompt input data package, runs
`scriptorium run`, and writes managed artifacts under the configured workspace. `scriptorium render`, runs `scriptorium run`, and writes managed artifacts
under the configured workspace.
When distributor notification is enabled, weatherreporter uploads the managed When distributor notification is enabled, weatherreporter uploads the managed
Markdown report after `scriptorium run` succeeds and final metadata is saved. Markdown report after `scriptorium run` succeeds and final metadata is saved.
`--out PATH` writes an extra Markdown copy for the current generated report; it `--out PATH` writes an extra Markdown copy for the current generated report; it

View File

@@ -79,6 +79,11 @@ type ModuleSnapshotRequest struct {
Resolved report.Resolved Resolved report.Resolved
} }
type ReportFacts struct {
Collected facts.CollectedFacts
Derived facts.DerivedFacts
}
type ReportRequest struct { type ReportRequest struct {
Config config.Config Config config.Config
Resolved report.Resolved Resolved report.Resolved
@@ -494,26 +499,33 @@ func GenerateReport(ctx context.Context, req ReportRequest) (*ReportResult, erro
if err != nil { if err != nil {
return nil, err return nil, err
} }
briefingPackage, err := BuildBriefing(BriefingRequest{ reportFacts, err := BuildReportFacts(ModuleSnapshotRequest{
Config: req.Config, Config: req.Config,
Resolved: req.Resolved, Resolved: req.Resolved,
}, bundle) }, bundle)
if err != nil { if err != nil {
return nil, err return nil, err
} }
moduleSnapshot, err := BuildModuleSnapshot(ModuleSnapshotRequest{ moduleSnapshot, err := BuildModuleSnapshotFromFacts(ModuleSnapshotRequest{
Config: req.Config, Config: req.Config,
Resolved: req.Resolved, Resolved: req.Resolved,
}, bundle) }, reportFacts)
if err != nil { if err != nil {
return nil, err return nil, err
} }
briefingPath, err := store.SaveBriefing(ctx, req.Resolved, briefingPackage) moduleSnapshotPath, err := store.SaveModuleSnapshot(ctx, req.Resolved, moduleSnapshot)
if err != nil { if err != nil {
return nil, err return nil, err
} }
moduleSnapshotPath, err := store.SaveModuleSnapshot(ctx, req.Resolved, moduleSnapshot) briefingPackage, err := BuildBriefingFromFacts(BriefingRequest{
Config: req.Config,
Resolved: req.Resolved,
}, reportFacts)
if err != nil {
return nil, err
}
briefingPath, err := store.SaveBriefing(ctx, req.Resolved, briefingPackage)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -522,7 +534,8 @@ func GenerateReport(ctx context.Context, req ReportRequest) (*ReportResult, erro
if err != nil { if err != nil {
return nil, err return nil, err
} }
metadata := state.BuildMetadata(req.Resolved, briefingPackage, state.ArtifactPaths{ briefingMetadata := briefing.BuildMetadata(briefingBuildContext(req.Config, req.Resolved, reportFacts.Collected))
metadata := state.BuildMetadataFromBriefingMetadata(req.Resolved, briefingMetadata, state.ArtifactPaths{
Briefing: briefingPath, Briefing: briefingPath,
ModuleSnapshot: moduleSnapshotPath, ModuleSnapshot: moduleSnapshotPath,
Metadata: paths.Metadata, Metadata: paths.Metadata,
@@ -834,38 +847,56 @@ func distributorUploadFiles(sourcePath string, bundlePaths []string) []distribut
} }
func BuildBriefing(req BriefingRequest, bundle *weatherdata.Bundle) (briefing.Package, error) { func BuildBriefing(req BriefingRequest, bundle *weatherdata.Bundle) (briefing.Package, error) {
collected := facts.BuildCollected(bundle) reportFacts, err := BuildReportFacts(ModuleSnapshotRequest{
derived, err := buildDerivedFacts(req.Config, req.Resolved, collected) Config: req.Config,
Resolved: req.Resolved,
}, bundle)
if err != nil { if err != nil {
return briefing.Package{}, err return briefing.Package{}, err
} }
buildContext := briefing.BuildContext{ return BuildBriefingFromFacts(req, reportFacts)
Resolved: req.Resolved, }
Bundle: collected.Bundle(),
Units: req.Config.WeatherAPI.Units, func BuildBriefingFromFacts(req BriefingRequest, reportFacts ReportFacts) (briefing.Package, error) {
Timezone: req.Config.WeatherAPI.Timezone, buildContext := briefingBuildContext(req.Config, req.Resolved, reportFacts.Collected)
Location: briefingLocation(req.Config),
}
switch req.Resolved.Definition.ID { switch req.Resolved.Definition.ID {
case report.DailyToday, report.DailyTomorrow: case report.DailyToday, report.DailyTomorrow:
return briefing.BuildDaily(buildContext, derived.FirstDailySummary()) return briefing.BuildDaily(buildContext, reportFacts.Derived.FirstDailySummary())
case report.ThreeDay, report.Weekend: case report.ThreeDay, report.Weekend:
if req.Resolved.Definition.ID == report.Weekend { if req.Resolved.Definition.ID == report.Weekend {
return briefing.BuildWeekend(buildContext, derived.DailySummaries) return briefing.BuildWeekend(buildContext, reportFacts.Derived.DailySummaries)
} }
return briefing.BuildThreeDay(buildContext, derived.DailySummaries) return briefing.BuildThreeDay(buildContext, reportFacts.Derived.DailySummaries)
case report.Storm: case report.Storm:
return briefing.BuildStorm(buildContext, derived) return briefing.BuildStorm(buildContext, reportFacts.Derived)
default: default:
return briefing.Package{}, fmt.Errorf("briefing is not implemented for report %q", req.Resolved.Definition.ID) return briefing.Package{}, fmt.Errorf("briefing is not implemented for report %q", req.Resolved.Definition.ID)
} }
} }
func BuildModuleSnapshot(req ModuleSnapshotRequest, bundle *weatherdata.Bundle) (module.Snapshot, error) { func BuildModuleSnapshot(req ModuleSnapshotRequest, bundle *weatherdata.Bundle) (module.Snapshot, error) {
reportFacts, err := BuildReportFacts(req, bundle)
if err != nil {
return module.Snapshot{}, err
}
return BuildModuleSnapshotFromFacts(req, reportFacts)
}
func BuildReportFacts(req ModuleSnapshotRequest, bundle *weatherdata.Bundle) (ReportFacts, error) {
collected := facts.BuildCollected(bundle) collected := facts.BuildCollected(bundle)
derived, err := buildDerivedFacts(req.Config, req.Resolved, collected) derived, err := buildDerivedFacts(req.Config, req.Resolved, collected)
if err != nil { if err != nil {
return module.Snapshot{}, err return ReportFacts{}, err
}
return ReportFacts{
Collected: collected,
Derived: derived,
}, nil
}
func BuildModuleSnapshotFromFacts(req ModuleSnapshotRequest, reportFacts ReportFacts) (module.Snapshot, error) {
if !req.Resolved.ValidPeriod.IsValid() {
return module.Snapshot{}, fmt.Errorf("resolved valid period is required")
} }
registry, err := briefing.DefaultModuleRegistry() registry, err := briefing.DefaultModuleRegistry()
if err != nil { if err != nil {
@@ -873,8 +904,8 @@ func BuildModuleSnapshot(req ModuleSnapshotRequest, bundle *weatherdata.Bundle)
} }
moduleContext := briefing.ModuleContext{ moduleContext := briefing.ModuleContext{
Resolved: req.Resolved, Resolved: req.Resolved,
Collected: collected, Collected: reportFacts.Collected,
Derived: derived, Derived: reportFacts.Derived,
Units: req.Config.WeatherAPI.Units, Units: req.Config.WeatherAPI.Units,
Timezone: req.Config.WeatherAPI.Timezone, Timezone: req.Config.WeatherAPI.Timezone,
Location: briefingLocation(req.Config), Location: briefingLocation(req.Config),
@@ -900,6 +931,16 @@ func BuildModuleSnapshot(req ModuleSnapshotRequest, bundle *weatherdata.Bundle)
return module.NewSnapshot(outputs) return module.NewSnapshot(outputs)
} }
func briefingBuildContext(cfg config.Config, resolved report.Resolved, collected facts.CollectedFacts) briefing.BuildContext {
return briefing.BuildContext{
Resolved: resolved,
Bundle: collected.Bundle(),
Units: cfg.WeatherAPI.Units,
Timezone: cfg.WeatherAPI.Timezone,
Location: briefingLocation(cfg),
}
}
func promptMetadata(metadata state.Metadata) promptinput.Metadata { func promptMetadata(metadata state.Metadata) promptinput.Metadata {
return promptinput.Metadata{ return promptinput.Metadata{
RunID: metadata.RunID, RunID: metadata.RunID,

View File

@@ -166,12 +166,18 @@ func TestGenerateReportWritesReportAndPreflight(t *testing.T) {
runBody: "# Daily Report\n\nRain this morning.\n", runBody: "# Daily Report\n\nRain this morning.\n",
} }
outputPath := filepath.Join(t.TempDir(), "daily.md") outputPath := filepath.Join(t.TempDir(), "daily.md")
filesystemStore, err := state.NewFilesystemStore(cfg.Workspace)
if err != nil {
t.Fatalf("NewFilesystemStore() error = %v", err)
}
store := &recordingStore{Store: filesystemStore}
result, err := GenerateReport(context.Background(), ReportRequest{ result, err := GenerateReport(context.Background(), ReportRequest{
Config: cfg, Config: cfg,
Resolved: resolved, Resolved: resolved,
OutputPath: outputPath, OutputPath: outputPath,
Renderer: renderer, Renderer: renderer,
Store: store,
}) })
if err != nil { if err != nil {
t.Fatalf("GenerateReport() error = %v", err) t.Fatalf("GenerateReport() error = %v", err)
@@ -195,6 +201,9 @@ func TestGenerateReportWritesReportAndPreflight(t *testing.T) {
if renderer.runRequest.OutputPath != result.ReportPath { if renderer.runRequest.OutputPath != result.ReportPath {
t.Fatalf("run OutputPath = %q, want managed report path %q", renderer.runRequest.OutputPath, result.ReportPath) t.Fatalf("run OutputPath = %q, want managed report path %q", renderer.runRequest.OutputPath, result.ReportPath)
} }
if got, want := strings.Join(store.calls, ","), "module_snapshot,briefing,data_package,preflight,metadata,prepare_report,metadata"; !strings.HasPrefix(got, want) {
t.Fatalf("store calls = %v, want prefix %s", store.calls, want)
}
assertPathsExist(t, result.BriefingPath, result.ModuleSnapshotPath, result.DataPackagePath, result.PreflightPath, result.ReportPath, result.MetadataPath, outputPath) assertPathsExist(t, result.BriefingPath, result.ModuleSnapshotPath, result.DataPackagePath, result.PreflightPath, result.ReportPath, result.MetadataPath, outputPath)
snapshotData, err := os.ReadFile(result.ModuleSnapshotPath) snapshotData, err := os.ReadFile(result.ModuleSnapshotPath)
if err != nil { if err != nil {
@@ -1536,6 +1545,41 @@ type recordingRenderer struct {
runBody string runBody string
} }
type recordingStore struct {
state.Store
calls []string
}
func (s *recordingStore) SaveBriefing(ctx context.Context, resolved report.Resolved, pkg briefing.Package) (string, error) {
s.calls = append(s.calls, "briefing")
return s.Store.SaveBriefing(ctx, resolved, pkg)
}
func (s *recordingStore) SaveModuleSnapshot(ctx context.Context, resolved report.Resolved, snapshot module.Snapshot) (string, error) {
s.calls = append(s.calls, "module_snapshot")
return s.Store.SaveModuleSnapshot(ctx, resolved, snapshot)
}
func (s *recordingStore) SaveDataPackage(ctx context.Context, resolved report.Resolved, pkg promptinput.Package) (string, error) {
s.calls = append(s.calls, "data_package")
return s.Store.SaveDataPackage(ctx, resolved, pkg)
}
func (s *recordingStore) SavePreflight(ctx context.Context, resolved report.Resolved, artifact state.PreflightArtifact) (string, error) {
s.calls = append(s.calls, "preflight")
return s.Store.SavePreflight(ctx, resolved, artifact)
}
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)
}
func (s *recordingStore) SaveMetadata(ctx context.Context, metadata state.Metadata) (string, error) {
s.calls = append(s.calls, "metadata")
return s.Store.SaveMetadata(ctx, metadata)
}
func successfulRenderer(body string) *recordingRenderer { func successfulRenderer(body string) *recordingRenderer {
return &recordingRenderer{ return &recordingRenderer{
renderResult: &scriptorium.RenderResult{ExitCode: 0}, renderResult: &scriptorium.RenderResult{ExitCode: 0},

View File

@@ -35,22 +35,26 @@ type Metadata struct {
} }
func BuildMetadata(resolved report.Resolved, briefingPackage briefing.Package, paths ArtifactPaths) Metadata { func BuildMetadata(resolved report.Resolved, briefingPackage briefing.Package, paths ArtifactPaths) Metadata {
return BuildMetadataFromBriefingMetadata(resolved, briefingPackage.Metadata, paths)
}
func BuildMetadataFromBriefingMetadata(resolved report.Resolved, briefingMetadata briefing.Metadata, paths ArtifactPaths) Metadata {
metadata := resolved.Metadata() metadata := resolved.Metadata()
return Metadata{ return Metadata{
SchemaVersion: MetadataSchemaVersion, SchemaVersion: MetadataSchemaVersion,
RunID: metadata.RunID, RunID: metadata.RunID,
MetadataPath: paths.Metadata, MetadataPath: paths.Metadata,
ReportID: metadata.ReportID, ReportID: metadata.ReportID,
Variant: briefingPackage.Metadata.Variant, Variant: briefingMetadata.Variant,
PromptID: metadata.PromptID, PromptID: metadata.PromptID,
GeneratedAt: metadata.GeneratedAt, GeneratedAt: metadata.GeneratedAt,
Timezone: metadata.Timezone, Timezone: metadata.Timezone,
ValidPeriod: metadata.ValidPeriod, ValidPeriod: metadata.ValidPeriod,
Location: copyLocation(briefingPackage.Metadata.Location), Location: copyLocation(briefingMetadata.Location),
SourceLocationID: briefingPackage.Metadata.SourceLocationID, SourceLocationID: briefingMetadata.SourceLocationID,
SourceLocation: briefingPackage.Metadata.SourceLocation, SourceLocation: briefingMetadata.SourceLocation,
Sources: briefingPackage.Metadata.Sources, Sources: briefingMetadata.Sources,
SourceWarnings: briefingPackage.Metadata.SourceWarnings, SourceWarnings: briefingMetadata.SourceWarnings,
BriefingPath: paths.Briefing, BriefingPath: paths.Briefing,
ModuleSnapshotPath: paths.ModuleSnapshot, ModuleSnapshotPath: paths.ModuleSnapshot,
DataPackagePath: paths.DataPackage, DataPackagePath: paths.DataPackage,