diff --git a/internal/state/filesystem.go b/internal/state/filesystem.go index 452c038..0353c21 100644 --- a/internal/state/filesystem.go +++ b/internal/state/filesystem.go @@ -91,18 +91,17 @@ func (s *FilesystemStore) Paths(resolved report.Resolved) (ArtifactPaths, error) return ArtifactPaths{}, fmt.Errorf("report %q has no artifact group", resolved.Definition.ID) } validDate := resolved.ValidPeriod.Start.Format("2006-01-02") - filenameBase := metadata.RunID return ArtifactPaths{ - 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.yaml"), - Preflight: s.join(s.preflightDir, group, validDate, filenameBase+".render.json"), - Notification: s.join(s.notificationsDir, group, validDate, filenameBase+".distributor.json"), - RenderedReport: s.join(s.reportsDir, group, filenameBase+".md"), - GeneratedTextRaw: s.join(s.snapshotsDir, group, validDate, filenameBase+".generated_text.raw.json"), - GeneratedTextResult: s.join(s.snapshotsDir, group, validDate, filenameBase+".generated_text.run.json"), - GeneratedText: s.join(s.snapshotsDir, group, validDate, filenameBase+".generated_text.json"), - RenderContext: s.join(s.snapshotsDir, group, validDate, filenameBase+".render_context.json"), + ModuleSnapshot: s.join(s.snapshotsDir, group, validDate, "modules."+metadata.RunID+".json"), + Metadata: s.join(s.snapshotsDir, group, validDate, "metadata."+metadata.RunID+".json"), + DataPackage: s.join(s.dataPackagesDir, group, validDate, "data_package."+metadata.RunID+".yaml"), + Preflight: s.join(s.preflightDir, group, validDate, "render."+metadata.RunID+".json"), + Notification: s.join(s.notificationsDir, group, validDate, "distributor."+metadata.RunID+".json"), + RenderedReport: s.join(s.reportsDir, group, validDate, "report."+metadata.RunID+".md"), + GeneratedTextRaw: s.join(s.snapshotsDir, group, validDate, "generated_text_raw."+metadata.RunID+".json"), + GeneratedTextResult: s.join(s.snapshotsDir, group, validDate, "generated_text_result."+metadata.RunID+".json"), + GeneratedText: s.join(s.snapshotsDir, group, validDate, "generated_text."+metadata.RunID+".json"), + RenderContext: s.join(s.snapshotsDir, group, validDate, "render_context."+metadata.RunID+".json"), }, nil } @@ -149,7 +148,7 @@ func (s *FilesystemStore) BatchDistributorNotificationPath(ref BatchDistributorN return "", err } localDate := ref.StartedAt.In(ref.Location).Format("2006-01-02") - return s.join(s.notificationsDir, "batches", ref.Batch, localDate, ref.BatchRunID+".distributor.json"), nil + return s.join(s.notificationsDir, "batches", ref.Batch, localDate, "distributor."+ref.BatchRunID+".json"), nil } func (s *FilesystemStore) SaveBatchDistributorNotification(_ context.Context, ref BatchDistributorNotificationRef, artifact BatchDistributorNotificationArtifact) (string, error) { @@ -272,7 +271,7 @@ func (s *FilesystemStore) FindPriorSnapshot(_ context.Context, resolved report.R return nil, fmt.Errorf("read snapshot metadata directory %q: %w", dir, err) } for _, entry := range entries { - if entry.IsDir() || !strings.HasSuffix(entry.Name(), ".metadata.json") { + if entry.IsDir() || !isMetadataFilename(entry.Name()) { continue } path := filepath.Join(dir, entry.Name()) @@ -323,7 +322,7 @@ func (s *FilesystemStore) ListReports(_ context.Context, limit int) ([]ReportRec if err != nil { return fmt.Errorf("inspect %q: %w", path, err) } - if entry.IsDir() || !strings.HasSuffix(entry.Name(), ".metadata.json") { + if entry.IsDir() || !isMetadataFilename(entry.Name()) { return nil } record, err := s.reportRecord(path) @@ -517,6 +516,10 @@ func validatePathSegment(name string, value string) error { return nil } +func isMetadataFilename(name string) bool { + return strings.HasPrefix(name, "metadata.") && strings.HasSuffix(name, ".json") && len(name) > len("metadata..json") +} + func readJSON(path string, target any) error { data, err := os.ReadFile(path) if err != nil { diff --git a/internal/state/filesystem_test.go b/internal/state/filesystem_test.go index 1dc706b..d764fbd 100644 --- a/internal/state/filesystem_test.go +++ b/internal/state/filesystem_test.go @@ -27,12 +27,12 @@ func TestPathsUseRunIDAndWorkspace(t *testing.T) { } for _, want := range []string{ - filepath.Join("snapshots", "daily", "2026-05-29", "20260529T100000.000000000Z_daily_2026-05-29.modules.json"), - filepath.Join("snapshots", "daily", "2026-05-29", "20260529T100000.000000000Z_daily_2026-05-29.metadata.json"), - filepath.Join("data-packages", "daily", "2026-05-29", "20260529T100000.000000000Z_daily_2026-05-29.data_package.yaml"), - filepath.Join("preflight", "daily", "2026-05-29", "20260529T100000.000000000Z_daily_2026-05-29.render.json"), - filepath.Join("notifications", "daily", "2026-05-29", "20260529T100000.000000000Z_daily_2026-05-29.distributor.json"), - filepath.Join("reports", "daily", "20260529T100000.000000000Z_daily_2026-05-29.md"), + filepath.Join("snapshots", "daily", "2026-05-29", "modules.20260529T100000.000000000Z_daily_2026-05-29.json"), + filepath.Join("snapshots", "daily", "2026-05-29", "metadata.20260529T100000.000000000Z_daily_2026-05-29.json"), + filepath.Join("data-packages", "daily", "2026-05-29", "data_package.20260529T100000.000000000Z_daily_2026-05-29.yaml"), + filepath.Join("preflight", "daily", "2026-05-29", "render.20260529T100000.000000000Z_daily_2026-05-29.json"), + filepath.Join("notifications", "daily", "2026-05-29", "distributor.20260529T100000.000000000Z_daily_2026-05-29.json"), + filepath.Join("reports", "daily", "2026-05-29", "report.20260529T100000.000000000Z_daily_2026-05-29.md"), } { if !strings.Contains(pathsString(paths), want) { t.Fatalf("paths = %#v, want component %q", paths, want) @@ -83,7 +83,7 @@ func TestBatchDistributorNotificationPathUsesWorkspaceBatchDateAndRunID(t *testi if err != nil { t.Fatalf("BatchDistributorNotificationPath() error = %v", err) } - want := filepath.Join("notifications", "batches", "evening", "2026-06-17", "20260618T033000.123456789Z_evening.distributor.json") + want := filepath.Join("notifications", "batches", "evening", "2026-06-17", "distributor.20260618T033000.123456789Z_evening.json") if !strings.Contains(path, want) { t.Fatalf("path = %q, want component %q", path, want) } @@ -118,13 +118,13 @@ func TestSaveBatchDistributorNotificationRoundTrip(t *testing.T) { { ReportID: report.Today, RunID: "20260617T120000.000000000Z_today", - SourcePath: "/workspace/reports/today/20260617T120000.000000000Z_today.md", + SourcePath: "/workspace/reports/today/2026-06-17/report.20260617T120000.000000000Z_today.md", BundlePaths: []string{"2026-06-17/today/report.md"}, }, { ReportID: report.Daily, RunID: "20260617T120000.000000000Z_daily_2026-06-19", - SourcePath: "/workspace/reports/daily/20260617T120000.000000000Z_daily_2026-06-19.md", + SourcePath: "/workspace/reports/daily/2026-06-19/report.20260617T120000.000000000Z_daily_2026-06-19.md", BundlePaths: []string{"2026-06-19/daily/report.md"}, }, }, @@ -148,7 +148,7 @@ func TestSaveBatchDistributorNotificationRoundTrip(t *testing.T) { if err != nil { t.Fatalf("SaveBatchDistributorNotification() error = %v", err) } - wantPath := filepath.Join("notifications", "batches", "morning", "2026-06-17", "20260617T120000.000000000Z_morning.distributor.json") + wantPath := filepath.Join("notifications", "batches", "morning", "2026-06-17", "distributor.20260617T120000.000000000Z_morning.json") if !strings.Contains(path, wantPath) { t.Fatalf("path = %q, want component %q", path, wantPath) } @@ -302,15 +302,23 @@ func TestGeneratedTextArtifactPathsUseSnapshotTree(t *testing.T) { t.Fatalf("Paths() error = %v", err) } wants := map[string]string{ - "DataPackage": filepath.Join("data-packages", tt.group, tt.validDate, tt.runID+".data_package.yaml"), - "RenderedReport": filepath.Join("reports", tt.group, tt.runID+".md"), - "GeneratedTextRaw": filepath.Join("snapshots", tt.group, tt.validDate, tt.runID+".generated_text.raw.json"), - "GeneratedTextResult": filepath.Join("snapshots", tt.group, tt.validDate, tt.runID+".generated_text.run.json"), - "GeneratedText": filepath.Join("snapshots", tt.group, tt.validDate, tt.runID+".generated_text.json"), - "RenderContext": filepath.Join("snapshots", tt.group, tt.validDate, tt.runID+".render_context.json"), + "ModuleSnapshot": filepath.Join("snapshots", tt.group, tt.validDate, "modules."+tt.runID+".json"), + "Metadata": filepath.Join("snapshots", tt.group, tt.validDate, "metadata."+tt.runID+".json"), + "DataPackage": filepath.Join("data-packages", tt.group, tt.validDate, "data_package."+tt.runID+".yaml"), + "Preflight": filepath.Join("preflight", tt.group, tt.validDate, "render."+tt.runID+".json"), + "Notification": filepath.Join("notifications", tt.group, tt.validDate, "distributor."+tt.runID+".json"), + "RenderedReport": filepath.Join("reports", tt.group, tt.validDate, "report."+tt.runID+".md"), + "GeneratedTextRaw": filepath.Join("snapshots", tt.group, tt.validDate, "generated_text_raw."+tt.runID+".json"), + "GeneratedTextResult": filepath.Join("snapshots", tt.group, tt.validDate, "generated_text_result."+tt.runID+".json"), + "GeneratedText": filepath.Join("snapshots", tt.group, tt.validDate, "generated_text."+tt.runID+".json"), + "RenderContext": filepath.Join("snapshots", tt.group, tt.validDate, "render_context."+tt.runID+".json"), } got := map[string]string{ + "ModuleSnapshot": paths.ModuleSnapshot, + "Metadata": paths.Metadata, "DataPackage": paths.DataPackage, + "Preflight": paths.Preflight, + "Notification": paths.Notification, "RenderedReport": paths.RenderedReport, "GeneratedTextRaw": paths.GeneratedTextRaw, "GeneratedTextResult": paths.GeneratedTextResult, @@ -610,7 +618,7 @@ func TestSaveMetadataUsesExplicitMetadataPath(t *testing.T) { t.Fatalf("Paths() error = %v", err) } otherDir := filepath.Join(t.TempDir(), "other-artifacts") - derivedMetadataPath := filepath.Join(otherDir, resolved.Metadata().RunID+".metadata.json") + derivedMetadataPath := filepath.Join(otherDir, "metadata."+resolved.Metadata().RunID+".json") metadata := BuildMetadataFromBriefingMetadata(resolved, briefingMetadata, ArtifactPaths{ ModuleSnapshot: paths.ModuleSnapshot,