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
```
Each command resolves a report period, fetches a Weather API bundle, builds a
briefing, builds a prompt input data package, runs `scriptorium render`, runs
`scriptorium run`, and writes managed artifacts under the configured workspace.
Each command resolves a report period, fetches a Weather API bundle, builds
module snapshots, 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

View File

@@ -79,6 +79,11 @@ type ModuleSnapshotRequest struct {
Resolved report.Resolved
}
type ReportFacts struct {
Collected facts.CollectedFacts
Derived facts.DerivedFacts
}
type ReportRequest struct {
Config config.Config
Resolved report.Resolved
@@ -494,26 +499,33 @@ func GenerateReport(ctx context.Context, req ReportRequest) (*ReportResult, erro
if err != nil {
return nil, err
}
briefingPackage, err := BuildBriefing(BriefingRequest{
reportFacts, err := BuildReportFacts(ModuleSnapshotRequest{
Config: req.Config,
Resolved: req.Resolved,
}, bundle)
if err != nil {
return nil, err
}
moduleSnapshot, err := BuildModuleSnapshot(ModuleSnapshotRequest{
moduleSnapshot, err := BuildModuleSnapshotFromFacts(ModuleSnapshotRequest{
Config: req.Config,
Resolved: req.Resolved,
}, bundle)
}, reportFacts)
if err != nil {
return nil, err
}
briefingPath, err := store.SaveBriefing(ctx, req.Resolved, briefingPackage)
moduleSnapshotPath, err := store.SaveModuleSnapshot(ctx, req.Resolved, moduleSnapshot)
if err != nil {
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 {
return nil, err
}
@@ -522,7 +534,8 @@ func GenerateReport(ctx context.Context, req ReportRequest) (*ReportResult, erro
if err != nil {
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,
ModuleSnapshot: moduleSnapshotPath,
Metadata: paths.Metadata,
@@ -834,38 +847,56 @@ func distributorUploadFiles(sourcePath string, bundlePaths []string) []distribut
}
func BuildBriefing(req BriefingRequest, bundle *weatherdata.Bundle) (briefing.Package, error) {
collected := facts.BuildCollected(bundle)
derived, err := buildDerivedFacts(req.Config, req.Resolved, collected)
reportFacts, err := BuildReportFacts(ModuleSnapshotRequest{
Config: req.Config,
Resolved: req.Resolved,
}, bundle)
if err != nil {
return briefing.Package{}, err
}
buildContext := briefing.BuildContext{
Resolved: req.Resolved,
Bundle: collected.Bundle(),
Units: req.Config.WeatherAPI.Units,
Timezone: req.Config.WeatherAPI.Timezone,
Location: briefingLocation(req.Config),
}
return BuildBriefingFromFacts(req, reportFacts)
}
func BuildBriefingFromFacts(req BriefingRequest, reportFacts ReportFacts) (briefing.Package, error) {
buildContext := briefingBuildContext(req.Config, req.Resolved, reportFacts.Collected)
switch req.Resolved.Definition.ID {
case report.DailyToday, report.DailyTomorrow:
return briefing.BuildDaily(buildContext, derived.FirstDailySummary())
return briefing.BuildDaily(buildContext, reportFacts.Derived.FirstDailySummary())
case report.ThreeDay, 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:
return briefing.BuildStorm(buildContext, derived)
return briefing.BuildStorm(buildContext, reportFacts.Derived)
default:
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) {
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)
derived, err := buildDerivedFacts(req.Config, req.Resolved, collected)
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()
if err != nil {
@@ -873,8 +904,8 @@ func BuildModuleSnapshot(req ModuleSnapshotRequest, bundle *weatherdata.Bundle)
}
moduleContext := briefing.ModuleContext{
Resolved: req.Resolved,
Collected: collected,
Derived: derived,
Collected: reportFacts.Collected,
Derived: reportFacts.Derived,
Units: req.Config.WeatherAPI.Units,
Timezone: req.Config.WeatherAPI.Timezone,
Location: briefingLocation(req.Config),
@@ -900,6 +931,16 @@ func BuildModuleSnapshot(req ModuleSnapshotRequest, bundle *weatherdata.Bundle)
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 {
return promptinput.Metadata{
RunID: metadata.RunID,

View File

@@ -166,12 +166,18 @@ func TestGenerateReportWritesReportAndPreflight(t *testing.T) {
runBody: "# Daily Report\n\nRain this morning.\n",
}
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{
Config: cfg,
Resolved: resolved,
OutputPath: outputPath,
Renderer: renderer,
Store: store,
})
if err != nil {
t.Fatalf("GenerateReport() error = %v", err)
@@ -195,6 +201,9 @@ func TestGenerateReportWritesReportAndPreflight(t *testing.T) {
if 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)
snapshotData, err := os.ReadFile(result.ModuleSnapshotPath)
if err != nil {
@@ -1536,6 +1545,41 @@ type recordingRenderer struct {
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 {
return &recordingRenderer{
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 {
return BuildMetadataFromBriefingMetadata(resolved, briefingPackage.Metadata, paths)
}
func BuildMetadataFromBriefingMetadata(resolved report.Resolved, briefingMetadata briefing.Metadata, paths ArtifactPaths) Metadata {
metadata := resolved.Metadata()
return Metadata{
SchemaVersion: MetadataSchemaVersion,
RunID: metadata.RunID,
MetadataPath: paths.Metadata,
ReportID: metadata.ReportID,
Variant: briefingPackage.Metadata.Variant,
Variant: briefingMetadata.Variant,
PromptID: metadata.PromptID,
GeneratedAt: metadata.GeneratedAt,
Timezone: metadata.Timezone,
ValidPeriod: metadata.ValidPeriod,
Location: copyLocation(briefingPackage.Metadata.Location),
SourceLocationID: briefingPackage.Metadata.SourceLocationID,
SourceLocation: briefingPackage.Metadata.SourceLocation,
Sources: briefingPackage.Metadata.Sources,
SourceWarnings: briefingPackage.Metadata.SourceWarnings,
Location: copyLocation(briefingMetadata.Location),
SourceLocationID: briefingMetadata.SourceLocationID,
SourceLocation: briefingMetadata.SourceLocation,
Sources: briefingMetadata.Sources,
SourceWarnings: briefingMetadata.SourceWarnings,
BriefingPath: paths.Briefing,
ModuleSnapshotPath: paths.ModuleSnapshot,
DataPackagePath: paths.DataPackage,