diff --git a/docs/operations.md b/docs/operations.md index f72c6d8..03e5862 100644 --- a/docs/operations.md +++ b/docs/operations.md @@ -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 diff --git a/internal/app/app.go b/internal/app/app.go index 5477edc..ea2f266 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -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, diff --git a/internal/app/app_test.go b/internal/app/app_test.go index 1ef9aef..da2eb96 100644 --- a/internal/app/app_test.go +++ b/internal/app/app_test.go @@ -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}, diff --git a/internal/state/metadata.go b/internal/state/metadata.go index fdfb7d2..56c9f2b 100644 --- a/internal/state/metadata.go +++ b/internal/state/metadata.go @@ -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,