Centralize report path policy

This commit is contained in:
2026-05-29 20:28:06 +00:00
parent 6915bf1ba2
commit 7dc2ac9253
7 changed files with 140 additions and 64 deletions

View File

@@ -6,7 +6,6 @@ import (
"fmt"
"os"
"path/filepath"
"strings"
"time"
"gitea.maximumdirect.net/eric/weatherreporter/internal/adapters/scriptorium"
@@ -157,7 +156,7 @@ func Generate(ctx context.Context, req GenerateRequest) error {
if err != nil {
return err
}
if isGeneratedReport(resolved.Definition.ID) {
if resolved.Definition.Generated {
_, err := GenerateReport(ctx, ReportRequest{
Config: req.Config,
Resolved: resolved,
@@ -200,7 +199,7 @@ func RunBatchDetailed(ctx context.Context, req BatchRequest) (*BatchResult, erro
startedAt := now
result := &BatchResult{Batch: req.Batch, StartedAt: startedAt}
for _, resolved := range resolvedReports {
if !isGeneratedReport(resolved.Definition.ID) {
if !resolved.Definition.Generated {
return nil, fmt.Errorf("run is not implemented")
}
}
@@ -257,19 +256,10 @@ func batchReportResult(resolved report.Resolved) BatchReportResult {
}
func batchOutputPath(outputDir string, definition report.Definition) string {
if outputDir == "" || definition.DefaultOutputName == "" {
if outputDir == "" || definition.BatchOutputName == "" {
return ""
}
name := strings.ReplaceAll(definition.DefaultOutputName, "_", "-")
return filepath.Join(outputDir, name)
}
func isGeneratedReport(id report.ID) bool {
return isDailyReport(id) || id == report.ThreeDay || id == report.Weekend || id == report.Storm
}
func isDailyReport(id report.ID) bool {
return id == report.DailyToday || id == report.DailyTomorrow
return filepath.Join(outputDir, definition.BatchOutputName)
}
func ResolveGenerate(req GenerateRequest, now time.Time) (report.Resolved, error) {

View File

@@ -39,6 +39,10 @@ type Definition struct {
PromptID string
ComparisonStrategy ComparisonStrategy
DefaultOutputName string
ArtifactGroup string
BatchOutputName string
Generated bool
CompatiblePriorIDs []ID
Morning bool
Evening bool
resolve func(ResolveRequest) (timeutil.Period, error)
@@ -51,6 +55,15 @@ func (d Definition) ResolvePeriod(req ResolveRequest) (timeutil.Period, error) {
return d.resolve(req)
}
func (d Definition) CompatibleWithPrior(id ID) bool {
for _, compatibleID := range d.CompatiblePriorIDs {
if id == compatibleID {
return true
}
}
return false
}
type ResolveRequest struct {
Now time.Time
Location *time.Location

View File

@@ -1,6 +1,7 @@
package report
import (
"reflect"
"strings"
"testing"
"time"
@@ -177,6 +178,79 @@ func TestRegistryDefinitionsHavePromptIDsAndComparisonStrategies(t *testing.T) {
}
}
func TestRegistryDefinitionsDeclarePathAndCompatibilityPolicy(t *testing.T) {
tests := []struct {
id ID
artifactGroup string
batchOutputName string
generated bool
compatiblePriorIDs []ID
}{
{
id: DailyToday,
artifactGroup: "daily",
batchOutputName: "daily.md",
generated: true,
compatiblePriorIDs: []ID{DailyToday, DailyTomorrow},
},
{
id: DailyTomorrow,
artifactGroup: "daily",
batchOutputName: "tomorrow.md",
generated: true,
compatiblePriorIDs: []ID{DailyToday, DailyTomorrow},
},
{
id: ThreeDay,
artifactGroup: "three-day",
batchOutputName: "three-day.md",
generated: true,
compatiblePriorIDs: []ID{ThreeDay},
},
{
id: Weekend,
artifactGroup: "weekend",
batchOutputName: "weekend.md",
generated: true,
compatiblePriorIDs: []ID{Weekend},
},
{
id: Storm,
artifactGroup: "storm",
batchOutputName: "storm.md",
generated: true,
compatiblePriorIDs: []ID{Storm},
},
}
registry := DefaultRegistry()
for _, tt := range tests {
t.Run(string(tt.id), func(t *testing.T) {
definition, err := registry.Lookup(tt.id)
if err != nil {
t.Fatalf("Lookup() error = %v", err)
}
if definition.ArtifactGroup != tt.artifactGroup {
t.Fatalf("ArtifactGroup = %q, want %q", definition.ArtifactGroup, tt.artifactGroup)
}
if definition.BatchOutputName != tt.batchOutputName {
t.Fatalf("BatchOutputName = %q, want %q", definition.BatchOutputName, tt.batchOutputName)
}
if definition.Generated != tt.generated {
t.Fatalf("Generated = %t, want %t", definition.Generated, tt.generated)
}
if !reflect.DeepEqual(definition.CompatiblePriorIDs, tt.compatiblePriorIDs) {
t.Fatalf("CompatiblePriorIDs = %#v, want %#v", definition.CompatiblePriorIDs, tt.compatiblePriorIDs)
}
for _, id := range tt.compatiblePriorIDs {
if !definition.CompatibleWithPrior(id) {
t.Fatalf("CompatibleWithPrior(%q) = false, want true", id)
}
}
})
}
}
func TestResolvedMetadata(t *testing.T) {
location := mustLoadLocation(t)
resolved, err := Resolve(DailyToday, ResolveRequest{Now: mustParse("2026-05-29T05:00:00-05:00"), Location: location})

View File

@@ -14,6 +14,10 @@ func DefaultRegistry() Registry {
PromptID: "weather.daily_report",
ComparisonStrategy: CompareSameValidDate,
DefaultOutputName: "daily.md",
ArtifactGroup: "daily",
BatchOutputName: "daily.md",
Generated: true,
CompatiblePriorIDs: []ID{DailyToday, DailyTomorrow},
Morning: true,
resolve: resolveDailyToday,
},
@@ -23,6 +27,10 @@ func DefaultRegistry() Registry {
PromptID: "weather.daily_report",
ComparisonStrategy: CompareSameValidDate,
DefaultOutputName: "tomorrow.md",
ArtifactGroup: "daily",
BatchOutputName: "tomorrow.md",
Generated: true,
CompatiblePriorIDs: []ID{DailyToday, DailyTomorrow},
Evening: true,
resolve: resolveDailyTomorrow,
},
@@ -32,6 +40,10 @@ func DefaultRegistry() Registry {
PromptID: "weather.three_day_outlook",
ComparisonStrategy: CompareSameValidDate,
DefaultOutputName: "three_day.md",
ArtifactGroup: "three-day",
BatchOutputName: "three-day.md",
Generated: true,
CompatiblePriorIDs: []ID{ThreeDay},
Morning: true,
resolve: resolveThreeDay,
},
@@ -41,6 +53,10 @@ func DefaultRegistry() Registry {
PromptID: "weather.weekend_outlook",
ComparisonStrategy: CompareWeekendWindow,
DefaultOutputName: "weekend.md",
ArtifactGroup: "weekend",
BatchOutputName: "weekend.md",
Generated: true,
CompatiblePriorIDs: []ID{Weekend},
Morning: true,
resolve: resolveWeekend,
},
@@ -50,6 +66,10 @@ func DefaultRegistry() Registry {
PromptID: "weather.storm_report",
ComparisonStrategy: CompareExplicitWindow,
DefaultOutputName: "storm.md",
ArtifactGroup: "storm",
BatchOutputName: "storm.md",
Generated: true,
CompatiblePriorIDs: []ID{Storm},
resolve: resolveStorm,
},
}

View File

@@ -79,9 +79,9 @@ func (s *FilesystemStore) Paths(resolved report.Resolved) (ArtifactPaths, error)
if metadata.RunID == "" {
return ArtifactPaths{}, fmt.Errorf("run id is required")
}
group, err := reportGroup(resolved.Definition.ID)
if err != nil {
return ArtifactPaths{}, err
group := resolved.Definition.ArtifactGroup
if group == "" {
return ArtifactPaths{}, fmt.Errorf("report %q has no artifact group", resolved.Definition.ID)
}
validDate := resolved.ValidPeriod.Start.Format("2006-01-02")
filenameBase := metadata.RunID
@@ -175,9 +175,9 @@ func (s *FilesystemStore) FindPriorSnapshot(_ context.Context, resolved report.R
if resolved.Definition.ComparisonStrategy != report.CompareSameValidDate && resolved.Definition.ComparisonStrategy != report.CompareWeekendWindow {
return nil, nil
}
group, err := reportGroup(resolved.Definition.ID)
if err != nil {
return nil, err
group := resolved.Definition.ArtifactGroup
if group == "" {
return nil, fmt.Errorf("report %q has no artifact group", resolved.Definition.ID)
}
dirs, err := s.metadataDirectories(resolved, group)
if err != nil {
@@ -205,7 +205,7 @@ func (s *FilesystemStore) FindPriorSnapshot(_ context.Context, resolved report.R
if metadata.RunID == resolved.Metadata().RunID {
continue
}
if !compatiblePriorReport(group, metadata.ReportID, resolved.Definition.ID) {
if !resolved.Definition.CompatibleWithPrior(metadata.ReportID) {
continue
}
if !comparablePeriod(metadata, resolved) {
@@ -343,19 +343,6 @@ func (s *FilesystemStore) metadataDirectories(resolved report.Resolved, group st
return dirs, nil
}
func compatiblePriorReport(group string, prior report.ID, current report.ID) bool {
switch group {
case "daily":
return prior == report.DailyToday || prior == report.DailyTomorrow
case "three-day":
return prior == report.ThreeDay && current == report.ThreeDay
case "weekend":
return prior == report.Weekend && current == report.Weekend
default:
return false
}
}
func (s *FilesystemStore) join(parts ...string) string {
all := append([]string{s.root}, parts...)
return filepath.Join(all...)
@@ -375,21 +362,6 @@ func validateRelativeDir(name string, value string) error {
return nil
}
func reportGroup(id report.ID) (string, error) {
switch id {
case report.DailyToday, report.DailyTomorrow:
return "daily", nil
case report.ThreeDay:
return "three-day", nil
case report.Weekend:
return "weekend", nil
case report.Storm:
return "storm", nil
default:
return "", fmt.Errorf("unknown report %q", id)
}
}
func writeJSONAtomic(path string, value any) error {
data, err := json.MarshalIndent(value, "", " ")
if err != nil {