Persist module snapshots for generated reports

This commit is contained in:
2026-06-09 20:58:35 +00:00
parent 40639309b1
commit 2483c2362d
11 changed files with 236 additions and 82 deletions

View File

@@ -17,6 +17,7 @@ import (
"gitea.maximumdirect.net/eric/weatherreporter/internal/facts"
"gitea.maximumdirect.net/eric/weatherreporter/internal/fileutil"
"gitea.maximumdirect.net/eric/weatherreporter/internal/forecast"
"gitea.maximumdirect.net/eric/weatherreporter/internal/module"
"gitea.maximumdirect.net/eric/weatherreporter/internal/promptinput"
"gitea.maximumdirect.net/eric/weatherreporter/internal/report"
"gitea.maximumdirect.net/eric/weatherreporter/internal/state"
@@ -73,6 +74,11 @@ type BriefingRequest struct {
OutputPath string
}
type ModuleSnapshotRequest struct {
Config config.Config
Resolved report.Resolved
}
type ReportRequest struct {
Config config.Config
Resolved report.Resolved
@@ -88,21 +94,23 @@ type BriefingResult struct {
}
type ReportResult struct {
Briefing briefing.Package
BriefingPath string
DataPackage promptinput.Package
DataPackagePath string
PreflightPath string
ReportPath string
OutputPath string
NotificationPath string
Metadata state.Metadata
MetadataPath string
PriorSnapshot *state.PriorSnapshot
RecentChanges []changes.Change
RenderResult *scriptorium.RenderResult
RunResult *scriptorium.RunResult
Notification *NotificationResult
Briefing briefing.Package
ModuleSnapshot module.Snapshot
BriefingPath string
ModuleSnapshotPath string
DataPackage promptinput.Package
DataPackagePath string
PreflightPath string
ReportPath string
OutputPath string
NotificationPath string
Metadata state.Metadata
MetadataPath string
PriorSnapshot *state.PriorSnapshot
RecentChanges []changes.Change
RenderResult *scriptorium.RenderResult
RunResult *scriptorium.RunResult
Notification *NotificationResult
}
type BatchResult struct {
@@ -493,11 +501,22 @@ func GenerateReport(ctx context.Context, req ReportRequest) (*ReportResult, erro
if err != nil {
return nil, err
}
moduleSnapshot, err := BuildModuleSnapshot(ModuleSnapshotRequest{
Config: req.Config,
Resolved: req.Resolved,
}, bundle)
if err != nil {
return nil, err
}
briefingPath, err := store.SaveBriefing(ctx, req.Resolved, briefingPackage)
if err != nil {
return nil, err
}
moduleSnapshotPath, err := store.SaveModuleSnapshot(ctx, req.Resolved, moduleSnapshot)
if err != nil {
return nil, err
}
recentChanges, err := recentChanges(ctx, store, priorSnapshot, briefingPackage, req.Config.RecentChange)
if err != nil {
@@ -537,6 +556,7 @@ func GenerateReport(ctx context.Context, req ReportRequest) (*ReportResult, erro
}
metadata := state.BuildMetadata(req.Resolved, briefingPackage, state.ArtifactPaths{
Briefing: briefingPath,
ModuleSnapshot: moduleSnapshotPath,
Metadata: paths.Metadata,
DataPackage: dataPackagePath,
Preflight: preflightPath,
@@ -590,21 +610,23 @@ func GenerateReport(ctx context.Context, req ReportRequest) (*ReportResult, erro
}
return &ReportResult{
Briefing: briefingPackage,
BriefingPath: briefingPath,
DataPackage: dataPackage,
DataPackagePath: dataPackagePath,
PreflightPath: preflightPath,
ReportPath: reportPath,
OutputPath: outputPath,
NotificationPath: notificationPath,
Metadata: metadata,
MetadataPath: metadataPath,
PriorSnapshot: priorSnapshot,
RecentChanges: recentChanges,
RenderResult: renderResult,
RunResult: runResult,
Notification: notification,
Briefing: briefingPackage,
ModuleSnapshot: moduleSnapshot,
BriefingPath: briefingPath,
ModuleSnapshotPath: moduleSnapshotPath,
DataPackage: dataPackage,
DataPackagePath: dataPackagePath,
PreflightPath: preflightPath,
ReportPath: reportPath,
OutputPath: outputPath,
NotificationPath: notificationPath,
Metadata: metadata,
MetadataPath: metadataPath,
PriorSnapshot: priorSnapshot,
RecentChanges: recentChanges,
RenderResult: renderResult,
RunResult: runResult,
Notification: notification,
}, nil
}
@@ -807,20 +829,7 @@ func distributorUploadFiles(sourcePath string, bundlePaths []string) []distribut
func BuildBriefing(req BriefingRequest, bundle *weatherdata.Bundle) (briefing.Package, error) {
collected := facts.BuildCollected(bundle)
dayparts := make([]forecast.DaypartDefinition, 0, len(req.Config.Dayparts))
for _, daypart := range req.Config.Dayparts {
dayparts = append(dayparts, forecast.DaypartDefinition{
Name: daypart.Name,
Start: daypart.Start,
End: daypart.End,
})
}
derived, err := facts.BuildDerived(facts.BuildDerivedRequest{
Resolved: req.Resolved,
Timezone: req.Config.WeatherAPI.Timezone,
Dayparts: dayparts,
Collected: collected,
})
derived, err := buildDerivedFacts(req.Config, req.Resolved, collected)
if err != nil {
return briefing.Package{}, err
}
@@ -846,6 +855,62 @@ func BuildBriefing(req BriefingRequest, bundle *weatherdata.Bundle) (briefing.Pa
}
}
func BuildModuleSnapshot(req ModuleSnapshotRequest, bundle *weatherdata.Bundle) (module.Snapshot, error) {
collected := facts.BuildCollected(bundle)
derived, err := buildDerivedFacts(req.Config, req.Resolved, collected)
if err != nil {
return module.Snapshot{}, err
}
registry, err := briefing.DefaultModuleRegistry()
if err != nil {
return module.Snapshot{}, err
}
moduleContext := briefing.ModuleContext{
Resolved: req.Resolved,
Collected: collected,
Derived: derived,
Units: req.Config.WeatherAPI.Units,
Timezone: req.Config.WeatherAPI.Timezone,
Location: briefingLocation(req.Config),
}
var outputs []module.Output
for _, item := range req.Resolved.Definition.Modules {
definition, err := registry.Lookup(item.ID)
if err != nil {
return module.Snapshot{}, err
}
if definition.Builder == nil {
continue
}
output, err := registry.BuildModule(moduleContext, item)
if err != nil {
return module.Snapshot{}, err
}
if output == nil {
continue
}
outputs = append(outputs, *output)
}
return module.NewSnapshot(outputs)
}
func buildDerivedFacts(cfg config.Config, resolved report.Resolved, collected facts.CollectedFacts) (facts.DerivedFacts, error) {
dayparts := make([]forecast.DaypartDefinition, 0, len(cfg.Dayparts))
for _, daypart := range cfg.Dayparts {
dayparts = append(dayparts, forecast.DaypartDefinition{
Name: daypart.Name,
Start: daypart.Start,
End: daypart.End,
})
}
return facts.BuildDerived(facts.BuildDerivedRequest{
Resolved: resolved,
Timezone: cfg.WeatherAPI.Timezone,
Dayparts: dayparts,
Collected: collected,
})
}
func briefingLocation(cfg config.Config) *briefing.LocationContext {
location := briefing.LocationContext{
ID: cfg.Location.ID,

View File

@@ -194,7 +194,14 @@ 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)
}
assertPathsExist(t, result.BriefingPath, 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)
if err != nil {
t.Fatalf("read module snapshot: %v", err)
}
if !strings.Contains(string(snapshotData), module.SnapshotSchemaVersion) || !strings.Contains(string(snapshotData), `"metadata"`) || !strings.Contains(string(snapshotData), `"derived_daily_summary"`) {
t.Fatalf("module snapshot missing expected stanzas:\n%s", string(snapshotData))
}
data, err := os.ReadFile(result.DataPackagePath)
if err != nil {
t.Fatalf("read data package: %v", err)
@@ -238,7 +245,7 @@ func TestGenerateReportWritesReportAndPreflight(t *testing.T) {
if result.Metadata.RunID != resolved.Metadata().RunID {
t.Fatalf("metadata RunID = %q, want %q", result.Metadata.RunID, resolved.Metadata().RunID)
}
if result.Metadata.BriefingPath != result.BriefingPath || result.Metadata.DataPackagePath != result.DataPackagePath {
if result.Metadata.BriefingPath != result.BriefingPath || result.Metadata.ModuleSnapshotPath != result.ModuleSnapshotPath || result.Metadata.DataPackagePath != result.DataPackagePath {
t.Fatalf("metadata does not link artifact paths: %#v", result.Metadata)
}
if result.Metadata.RenderedReportPath != result.ReportPath {
@@ -647,6 +654,7 @@ func TestGenerateReportIncludesRecentChangesFromPriorSnapshot(t *testing.T) {
}
_, err = store.SaveMetadata(context.Background(), state.BuildMetadata(priorResolved, priorBriefing, state.ArtifactPaths{
Briefing: priorBriefingPath,
ModuleSnapshot: priorPaths.ModuleSnapshot,
Metadata: priorPaths.Metadata,
DataPackage: priorPaths.DataPackage,
Preflight: priorPaths.Preflight,
@@ -762,6 +770,7 @@ func TestTomorrowReportCanCompareAgainstPriorDailySnapshot(t *testing.T) {
}
_, err = store.SaveMetadata(context.Background(), state.BuildMetadata(priorResolved, priorBriefing, state.ArtifactPaths{
Briefing: priorBriefingPath,
ModuleSnapshot: priorPaths.ModuleSnapshot,
Metadata: priorPaths.Metadata,
DataPackage: priorPaths.DataPackage,
Preflight: priorPaths.Preflight,
@@ -829,6 +838,7 @@ func TestGenerateThreeDayReportWritesReportAndRecentChanges(t *testing.T) {
}
_, err = store.SaveMetadata(context.Background(), state.BuildMetadata(priorResolved, priorBriefing, state.ArtifactPaths{
Briefing: priorBriefingPath,
ModuleSnapshot: priorPaths.ModuleSnapshot,
Metadata: priorPaths.Metadata,
DataPackage: priorPaths.DataPackage,
Preflight: priorPaths.Preflight,
@@ -905,6 +915,7 @@ func TestGenerateWeekendReportWritesReportAndRecentChanges(t *testing.T) {
}
_, err = store.SaveMetadata(context.Background(), state.BuildMetadata(priorResolved, priorBriefing, state.ArtifactPaths{
Briefing: priorBriefingPath,
ModuleSnapshot: priorPaths.ModuleSnapshot,
Metadata: priorPaths.Metadata,
DataPackage: priorPaths.DataPackage,
Preflight: priorPaths.Preflight,
@@ -1045,15 +1056,15 @@ func TestInspectGeneratedReportArtifacts(t *testing.T) {
if err != nil {
t.Fatalf("InspectMetadata() error = %v", err)
}
if metadata.BriefingPath != result.BriefingPath || metadata.DataPackagePath != result.DataPackagePath {
if metadata.BriefingPath != result.BriefingPath || metadata.ModuleSnapshotPath != result.ModuleSnapshotPath || metadata.DataPackagePath != result.DataPackagePath {
t.Fatalf("metadata paths = %#v, want generated artifact paths", metadata)
}
briefingPackage, err := InspectBriefing(context.Background(), InspectRunRequest{Config: cfg, RunID: result.Metadata.RunID})
moduleSnapshot, err := InspectModules(context.Background(), InspectRunRequest{Config: cfg, RunID: result.Metadata.RunID})
if err != nil {
t.Fatalf("InspectBriefing() error = %v", err)
t.Fatalf("InspectModules() error = %v", err)
}
if briefingPackage.Metadata.RunID != result.Metadata.RunID {
t.Fatalf("briefing RunID = %q, want %q", briefingPackage.Metadata.RunID, result.Metadata.RunID)
if moduleSnapshot.SchemaVersion != module.SnapshotSchemaVersion || len(moduleSnapshot.Outputs) == 0 {
t.Fatalf("module snapshot = %#v, want persisted outputs", moduleSnapshot)
}
dataPackage, err := InspectDataPackage(context.Background(), InspectRunRequest{Config: cfg, RunID: result.Metadata.RunID})
if err != nil {

View File

@@ -6,6 +6,7 @@ import (
"gitea.maximumdirect.net/eric/weatherreporter/internal/briefing"
"gitea.maximumdirect.net/eric/weatherreporter/internal/config"
"gitea.maximumdirect.net/eric/weatherreporter/internal/module"
"gitea.maximumdirect.net/eric/weatherreporter/internal/promptinput"
"gitea.maximumdirect.net/eric/weatherreporter/internal/report"
"gitea.maximumdirect.net/eric/weatherreporter/internal/state"
@@ -44,12 +45,12 @@ func InspectMetadata(ctx context.Context, req InspectRunRequest) (state.Metadata
return inspection.metadata, err
}
func InspectBriefing(ctx context.Context, req InspectRunRequest) (briefing.Package, error) {
func InspectModules(ctx context.Context, req InspectRunRequest) (module.Snapshot, error) {
inspection, err := inspectRun(ctx, req)
if err != nil {
return briefing.Package{}, err
return module.Snapshot{}, err
}
return inspection.store.LoadBriefing(ctx, inspection.metadata.BriefingPath)
return inspection.store.LoadModuleSnapshot(ctx, inspection.metadata.ModuleSnapshotPath)
}
func InspectDataPackage(ctx context.Context, req InspectRunRequest) (promptinput.Package, error) {