Run report generation through module snapshots
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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},
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user