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

@@ -13,6 +13,7 @@ import (
"gitea.maximumdirect.net/eric/weatherreporter/internal/briefing"
"gitea.maximumdirect.net/eric/weatherreporter/internal/config"
"gitea.maximumdirect.net/eric/weatherreporter/internal/fileutil"
"gitea.maximumdirect.net/eric/weatherreporter/internal/module"
"gitea.maximumdirect.net/eric/weatherreporter/internal/promptinput"
"gitea.maximumdirect.net/eric/weatherreporter/internal/report"
)
@@ -28,6 +29,7 @@ type FilesystemStore struct {
type ArtifactPaths struct {
Briefing string `json:"briefing"`
ModuleSnapshot string `json:"moduleSnapshot"`
Metadata string `json:"metadata"`
DataPackage string `json:"dataPackage"`
Preflight string `json:"preflight"`
@@ -91,6 +93,7 @@ func (s *FilesystemStore) Paths(resolved report.Resolved) (ArtifactPaths, error)
filenameBase := metadata.RunID
return ArtifactPaths{
Briefing: s.join(s.snapshotsDir, group, validDate, filenameBase+".briefing.json"),
ModuleSnapshot: s.join(s.snapshotsDir, group, validDate, filenameBase+".modules.json"),
Metadata: s.join(s.snapshotsDir, group, validDate, filenameBase+".metadata.json"),
DataPackage: s.join(s.dataPackagesDir, group, validDate, filenameBase+".data_package.json"),
Preflight: s.join(s.preflightDir, group, validDate, filenameBase+".render.json"),
@@ -110,6 +113,20 @@ func (s *FilesystemStore) SaveBriefing(_ context.Context, resolved report.Resolv
return paths.Briefing, nil
}
func (s *FilesystemStore) SaveModuleSnapshot(_ context.Context, resolved report.Resolved, snapshot module.Snapshot) (string, error) {
paths, err := s.Paths(resolved)
if err != nil {
return "", err
}
if err := snapshot.Validate(); err != nil {
return "", err
}
if err := fileutil.WriteJSONAtomic(paths.ModuleSnapshot, snapshot); err != nil {
return "", err
}
return paths.ModuleSnapshot, nil
}
func (s *FilesystemStore) SaveDataPackage(_ context.Context, resolved report.Resolved, pkg promptinput.Package) (string, error) {
paths, err := s.Paths(resolved)
if err != nil {
@@ -167,6 +184,9 @@ func (s *FilesystemStore) SaveMetadata(_ context.Context, metadata Metadata) (st
if metadata.BriefingPath == "" {
return "", fmt.Errorf("metadata briefing path is required")
}
if metadata.ModuleSnapshotPath == "" {
return "", fmt.Errorf("metadata module snapshot path is required")
}
if metadata.DataPackagePath == "" {
return "", fmt.Errorf("metadata data package path is required")
}
@@ -308,6 +328,20 @@ func (s *FilesystemStore) LoadDataPackage(_ context.Context, path string) (promp
return pkg, nil
}
func (s *FilesystemStore) LoadModuleSnapshot(_ context.Context, path string) (module.Snapshot, error) {
if path == "" {
return module.Snapshot{}, fmt.Errorf("module snapshot path is required")
}
var snapshot module.Snapshot
if err := readJSON(path, &snapshot); err != nil {
return module.Snapshot{}, err
}
if err := snapshot.Validate(); err != nil {
return module.Snapshot{}, err
}
return snapshot, nil
}
func (s *FilesystemStore) reportRecord(path string) (ReportRecord, error) {
var metadata Metadata
if err := readJSON(path, &metadata); err != nil {

View File

@@ -11,6 +11,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/timeutil"
@@ -27,6 +28,7 @@ func TestPathsUseRunIDAndWorkspace(t *testing.T) {
for _, want := range []string{
filepath.Join("snapshots", "daily", "2026-05-29", "20260529T100000.000000000Z_daily_today.briefing.json"),
filepath.Join("snapshots", "daily", "2026-05-29", "20260529T100000.000000000Z_daily_today.modules.json"),
filepath.Join("snapshots", "daily", "2026-05-29", "20260529T100000.000000000Z_daily_today.metadata.json"),
filepath.Join("data-packages", "daily", "2026-05-29", "20260529T100000.000000000Z_daily_today.data_package.json"),
filepath.Join("preflight", "daily", "2026-05-29", "20260529T100000.000000000Z_daily_today.render.json"),
@@ -56,6 +58,14 @@ func TestSaveArtifactsAndMetadataRoundTrip(t *testing.T) {
if err != nil {
t.Fatalf("SaveDataPackage() error = %v", err)
}
snapshot, err := module.NewSnapshot([]module.Output{{ID: module.Metadata, StanzaName: "metadata", Value: map[string]string{"run_id": resolved.Metadata().RunID}}})
if err != nil {
t.Fatalf("NewSnapshot() error = %v", err)
}
moduleSnapshotPath, err := store.SaveModuleSnapshot(context.Background(), resolved, snapshot)
if err != nil {
t.Fatalf("SaveModuleSnapshot() error = %v", err)
}
preflightPath, err := store.SavePreflight(context.Background(), resolved, PreflightArtifact{Stdout: `{"ok":true}`})
if err != nil {
t.Fatalf("SavePreflight() error = %v", err)
@@ -112,6 +122,7 @@ func TestSaveArtifactsAndMetadataRoundTrip(t *testing.T) {
}
metadata := BuildMetadata(resolved, briefingPackage, ArtifactPaths{
Briefing: briefingPath,
ModuleSnapshot: moduleSnapshotPath,
Metadata: paths.Metadata,
DataPackage: dataPackagePath,
Preflight: preflightPath,
@@ -122,7 +133,7 @@ func TestSaveArtifactsAndMetadataRoundTrip(t *testing.T) {
t.Fatalf("SaveMetadata() error = %v", err)
}
for _, path := range []string{briefingPath, dataPackagePath, preflightPath, notificationPath, renderedReportPath, metadataPath} {
for _, path := range []string{briefingPath, moduleSnapshotPath, dataPackagePath, preflightPath, notificationPath, renderedReportPath, metadataPath} {
if _, err := os.Stat(path); err != nil {
t.Fatalf("expected artifact %q: %v", path, err)
}
@@ -134,6 +145,13 @@ func TestSaveArtifactsAndMetadataRoundTrip(t *testing.T) {
if loadedBriefing.Metadata.RunID != resolved.Metadata().RunID {
t.Fatalf("loaded briefing RunID = %q, want %q", loadedBriefing.Metadata.RunID, resolved.Metadata().RunID)
}
loadedSnapshot, err := store.LoadModuleSnapshot(context.Background(), moduleSnapshotPath)
if err != nil {
t.Fatalf("LoadModuleSnapshot() error = %v", err)
}
if loadedSnapshot.SchemaVersion != module.SnapshotSchemaVersion || len(loadedSnapshot.Outputs) != 1 {
t.Fatalf("loaded module snapshot = %#v, want one metadata output", loadedSnapshot)
}
var decoded Metadata
data, err := os.ReadFile(metadataPath)
if err != nil {
@@ -145,7 +163,7 @@ func TestSaveArtifactsAndMetadataRoundTrip(t *testing.T) {
if decoded.RunID != resolved.Metadata().RunID {
t.Fatalf("RunID = %q, want %q", decoded.RunID, resolved.Metadata().RunID)
}
if decoded.BriefingPath != briefingPath || decoded.DataPackagePath != dataPackagePath || decoded.PreflightPath != preflightPath {
if decoded.BriefingPath != briefingPath || decoded.ModuleSnapshotPath != moduleSnapshotPath || decoded.DataPackagePath != dataPackagePath || decoded.PreflightPath != preflightPath {
t.Fatalf("metadata paths = %#v, want saved artifact paths", decoded)
}
if decoded.RenderedReportPath != renderedReportPath {
@@ -173,6 +191,7 @@ func TestSaveMetadataUsesExplicitMetadataPath(t *testing.T) {
metadata := BuildMetadata(resolved, briefingPackage, ArtifactPaths{
Briefing: otherBriefingPath,
ModuleSnapshot: paths.ModuleSnapshot,
Metadata: paths.Metadata,
DataPackage: paths.DataPackage,
Preflight: paths.Preflight,
@@ -208,6 +227,7 @@ func TestFindPriorSnapshot(t *testing.T) {
}
_, err = store.SaveMetadata(context.Background(), BuildMetadata(first, briefingPackage, ArtifactPaths{
Briefing: briefingPath,
ModuleSnapshot: paths.ModuleSnapshot,
Metadata: paths.Metadata,
DataPackage: paths.DataPackage,
Preflight: paths.Preflight,
@@ -247,6 +267,7 @@ func TestFindPriorSnapshotUsesValidDate(t *testing.T) {
}
_, err = store.SaveMetadata(context.Background(), BuildMetadata(previousDate, briefingPackage, ArtifactPaths{
Briefing: briefingPath,
ModuleSnapshot: paths.ModuleSnapshot,
Metadata: paths.Metadata,
DataPackage: paths.DataPackage,
Preflight: paths.Preflight,
@@ -280,6 +301,7 @@ func TestFindPriorSnapshotSupportsThreeDay(t *testing.T) {
}
_, err = store.SaveMetadata(context.Background(), BuildMetadata(first, briefingPackage, ArtifactPaths{
Briefing: briefingPath,
ModuleSnapshot: paths.ModuleSnapshot,
Metadata: paths.Metadata,
DataPackage: paths.DataPackage,
Preflight: paths.Preflight,
@@ -316,6 +338,7 @@ func TestFindPriorSnapshotSupportsWeekend(t *testing.T) {
}
_, err = store.SaveMetadata(context.Background(), BuildMetadata(first, briefingPackage, ArtifactPaths{
Briefing: briefingPath,
ModuleSnapshot: paths.ModuleSnapshot,
Metadata: paths.Metadata,
DataPackage: paths.DataPackage,
Preflight: paths.Preflight,
@@ -352,6 +375,7 @@ func TestFindPriorSnapshotSupportsNarrowedWeekendPeriod(t *testing.T) {
}
_, err = store.SaveMetadata(context.Background(), BuildMetadata(first, briefingPackage, ArtifactPaths{
Briefing: briefingPath,
ModuleSnapshot: paths.ModuleSnapshot,
Metadata: paths.Metadata,
DataPackage: paths.DataPackage,
Preflight: paths.Preflight,
@@ -521,6 +545,7 @@ func pathsString(paths ArtifactPaths) string {
return strings.Join([]string{
paths.Briefing,
paths.Metadata,
paths.ModuleSnapshot,
paths.DataPackage,
paths.Preflight,
paths.Notification,

View File

@@ -27,6 +27,7 @@ type Metadata struct {
Sources []briefing.SourceMetadata `json:"sources,omitempty"`
SourceWarnings []weatherdata.SourceWarning `json:"sourceWarnings,omitempty"`
BriefingPath string `json:"briefingPath"`
ModuleSnapshotPath string `json:"moduleSnapshotPath"`
DataPackagePath string `json:"dataPackagePath"`
PreflightPath string `json:"preflightPath"`
NotificationPath string `json:"notificationPath,omitempty"`
@@ -51,6 +52,7 @@ func BuildMetadata(resolved report.Resolved, briefingPackage briefing.Package, p
Sources: briefingPackage.Metadata.Sources,
SourceWarnings: briefingPackage.Metadata.SourceWarnings,
BriefingPath: paths.Briefing,
ModuleSnapshotPath: paths.ModuleSnapshot,
DataPackagePath: paths.DataPackage,
PreflightPath: paths.Preflight,
RenderedReportPath: paths.RenderedReport,

View File

@@ -7,6 +7,7 @@ import (
"time"
"gitea.maximumdirect.net/eric/weatherreporter/internal/briefing"
"gitea.maximumdirect.net/eric/weatherreporter/internal/module"
"gitea.maximumdirect.net/eric/weatherreporter/internal/promptinput"
"gitea.maximumdirect.net/eric/weatherreporter/internal/report"
)
@@ -14,6 +15,7 @@ import (
type Store interface {
Paths(report.Resolved) (ArtifactPaths, error)
SaveBriefing(context.Context, report.Resolved, briefing.Package) (string, error)
SaveModuleSnapshot(context.Context, report.Resolved, module.Snapshot) (string, error)
SaveDataPackage(context.Context, report.Resolved, promptinput.Package) (string, error)
SavePreflight(context.Context, report.Resolved, PreflightArtifact) (string, error)
SaveDistributorNotification(context.Context, report.Resolved, DistributorNotificationArtifact) (string, error)
@@ -21,6 +23,7 @@ type Store interface {
SaveMetadata(context.Context, Metadata) (string, error)
FindPriorSnapshot(context.Context, report.Resolved) (*PriorSnapshot, error)
LoadBriefing(context.Context, string) (briefing.Package, error)
LoadModuleSnapshot(context.Context, string) (module.Snapshot, error)
}
type PriorSnapshot struct {