Add generated text state artifacts
This commit is contained in:
@@ -27,12 +27,16 @@ type FilesystemStore struct {
|
||||
}
|
||||
|
||||
type ArtifactPaths struct {
|
||||
ModuleSnapshot string `json:"moduleSnapshot"`
|
||||
Metadata string `json:"metadata"`
|
||||
DataPackage string `json:"dataPackage"`
|
||||
Preflight string `json:"preflight"`
|
||||
Notification string `json:"notification,omitempty"`
|
||||
RenderedReport string `json:"renderedReport,omitempty"`
|
||||
ModuleSnapshot string `json:"moduleSnapshot"`
|
||||
Metadata string `json:"metadata"`
|
||||
DataPackage string `json:"dataPackage"`
|
||||
Preflight string `json:"preflight"`
|
||||
Notification string `json:"notification,omitempty"`
|
||||
RenderedReport string `json:"renderedReport,omitempty"`
|
||||
GeneratedTextRaw string `json:"generatedTextRaw,omitempty"`
|
||||
GeneratedTextResult string `json:"generatedTextResult,omitempty"`
|
||||
GeneratedText string `json:"generatedText,omitempty"`
|
||||
RenderContext string `json:"renderContext,omitempty"`
|
||||
}
|
||||
|
||||
type ReportRecord struct {
|
||||
@@ -89,12 +93,16 @@ func (s *FilesystemStore) Paths(resolved report.Resolved) (ArtifactPaths, error)
|
||||
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"),
|
||||
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"),
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -148,6 +156,50 @@ func (s *FilesystemStore) SaveDistributorNotification(_ context.Context, resolve
|
||||
return paths.Notification, nil
|
||||
}
|
||||
|
||||
func (s *FilesystemStore) SaveGeneratedTextRaw(_ context.Context, resolved report.Resolved, data []byte) (string, error) {
|
||||
paths, err := s.Paths(resolved)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if err := fileutil.WriteFileAtomic(paths.GeneratedTextRaw, data); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return paths.GeneratedTextRaw, nil
|
||||
}
|
||||
|
||||
func (s *FilesystemStore) SaveGeneratedTextResult(_ context.Context, resolved report.Resolved, value any) (string, error) {
|
||||
paths, err := s.Paths(resolved)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if err := fileutil.WriteJSONAtomic(paths.GeneratedTextResult, value); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return paths.GeneratedTextResult, nil
|
||||
}
|
||||
|
||||
func (s *FilesystemStore) SaveGeneratedText(_ context.Context, resolved report.Resolved, data []byte) (string, error) {
|
||||
paths, err := s.Paths(resolved)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if err := fileutil.WriteFileAtomic(paths.GeneratedText, data); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return paths.GeneratedText, nil
|
||||
}
|
||||
|
||||
func (s *FilesystemStore) SaveRenderContext(_ context.Context, resolved report.Resolved, value any) (string, error) {
|
||||
paths, err := s.Paths(resolved)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if err := fileutil.WriteJSONAtomic(paths.RenderContext, value); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return paths.RenderContext, nil
|
||||
}
|
||||
|
||||
func (s *FilesystemStore) PrepareRenderedReport(_ context.Context, resolved report.Resolved) (string, error) {
|
||||
paths, err := s.Paths(resolved)
|
||||
if err != nil {
|
||||
@@ -325,6 +377,37 @@ func (s *FilesystemStore) LoadModuleSnapshot(_ context.Context, path string) (mo
|
||||
return snapshot, nil
|
||||
}
|
||||
|
||||
func (s *FilesystemStore) LoadGeneratedText(_ context.Context, path string) ([]byte, error) {
|
||||
if path == "" {
|
||||
return nil, fmt.Errorf("generated text path is required")
|
||||
}
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("read %q: %w", path, err)
|
||||
}
|
||||
return data, nil
|
||||
}
|
||||
|
||||
func (s *FilesystemStore) LoadGeneratedTextResult(_ context.Context, path string, target any) error {
|
||||
if path == "" {
|
||||
return fmt.Errorf("generated text result path is required")
|
||||
}
|
||||
if target == nil {
|
||||
return fmt.Errorf("generated text result target is required")
|
||||
}
|
||||
return readJSON(path, target)
|
||||
}
|
||||
|
||||
func (s *FilesystemStore) LoadRenderContext(_ context.Context, path string, target any) error {
|
||||
if path == "" {
|
||||
return fmt.Errorf("render context path is required")
|
||||
}
|
||||
if target == nil {
|
||||
return fmt.Errorf("render context target is required")
|
||||
}
|
||||
return readJSON(path, target)
|
||||
}
|
||||
|
||||
func (s *FilesystemStore) reportRecord(path string) (ReportRecord, error) {
|
||||
var metadata Metadata
|
||||
if err := readJSON(path, &metadata); err != nil {
|
||||
|
||||
@@ -40,6 +40,35 @@ func TestPathsUseRunIDAndWorkspace(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestGeneratedTextArtifactPathsUseSnapshotTree(t *testing.T) {
|
||||
store := newTestStore(t)
|
||||
resolved := resolveHourlyAt(t, "2026-05-29T05:00:00-05:00")
|
||||
|
||||
paths, err := store.Paths(resolved)
|
||||
if err != nil {
|
||||
t.Fatalf("Paths() error = %v", err)
|
||||
}
|
||||
|
||||
runID := "20260529T100000.000000000Z_hourly"
|
||||
wants := map[string]string{
|
||||
"GeneratedTextRaw": filepath.Join("snapshots", "hourly", "2026-05-29", runID+".generated_text.raw.json"),
|
||||
"GeneratedTextResult": filepath.Join("snapshots", "hourly", "2026-05-29", runID+".generated_text.run.json"),
|
||||
"GeneratedText": filepath.Join("snapshots", "hourly", "2026-05-29", runID+".generated_text.json"),
|
||||
"RenderContext": filepath.Join("snapshots", "hourly", "2026-05-29", runID+".render_context.json"),
|
||||
}
|
||||
got := map[string]string{
|
||||
"GeneratedTextRaw": paths.GeneratedTextRaw,
|
||||
"GeneratedTextResult": paths.GeneratedTextResult,
|
||||
"GeneratedText": paths.GeneratedText,
|
||||
"RenderContext": paths.RenderContext,
|
||||
}
|
||||
for name, want := range wants {
|
||||
if !strings.Contains(got[name], want) {
|
||||
t.Fatalf("%s path = %q, want component %q", name, got[name], want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSaveArtifactsAndMetadataRoundTrip(t *testing.T) {
|
||||
store := newTestStore(t)
|
||||
resolved := resolveDailyAt(t, "2026-05-29T05:00:00-05:00")
|
||||
@@ -180,6 +209,117 @@ func TestSaveArtifactsAndMetadataRoundTrip(t *testing.T) {
|
||||
if strings.Contains(string(data), "MetadataPath") || strings.Contains(string(data), "metadataPath") {
|
||||
t.Fatalf("metadata JSON includes runtime-only MetadataPath:\n%s", string(data))
|
||||
}
|
||||
for _, unexpected := range []string{"generatedTextSchemaId", "generatedTextRawPath", "generatedTextResultPath", "generatedTextPath", "renderContextPath"} {
|
||||
if strings.Contains(string(data), unexpected) {
|
||||
t.Fatalf("metadata JSON includes generated-text field %q for Markdown report:\n%s", unexpected, string(data))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSaveGeneratedTextArtifactsAndMetadataRoundTrip(t *testing.T) {
|
||||
store := newTestStore(t)
|
||||
resolved := resolveHourlyAt(t, "2026-05-29T05:00:00-05:00")
|
||||
briefingMetadata := stateBriefingMetadata(resolved)
|
||||
|
||||
rawPath, err := store.SaveGeneratedTextRaw(context.Background(), resolved, []byte(`{"summary":"raw"}`))
|
||||
if err != nil {
|
||||
t.Fatalf("SaveGeneratedTextRaw() error = %v", err)
|
||||
}
|
||||
resultPath, err := store.SaveGeneratedTextResult(context.Background(), resolved, map[string]any{
|
||||
"command": []string{"scriptorium", "run"},
|
||||
"status": "succeeded",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("SaveGeneratedTextResult() error = %v", err)
|
||||
}
|
||||
generatedPath, err := store.SaveGeneratedText(context.Background(), resolved, []byte(`{"summary":"Storm chances increase.","timing":"Late morning.","impacts":"Brief downpours."}`))
|
||||
if err != nil {
|
||||
t.Fatalf("SaveGeneratedText() error = %v", err)
|
||||
}
|
||||
contextPath, err := store.SaveRenderContext(context.Background(), resolved, struct {
|
||||
ReportTitle string `json:"reportTitle"`
|
||||
Location string `json:"location"`
|
||||
}{
|
||||
ReportTitle: "Hourly Report",
|
||||
Location: "Brentwood",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("SaveRenderContext() error = %v", err)
|
||||
}
|
||||
|
||||
for _, path := range []string{rawPath, resultPath, generatedPath, contextPath} {
|
||||
if _, err := os.Stat(path); err != nil {
|
||||
t.Fatalf("expected generated-text artifact %q: %v", path, err)
|
||||
}
|
||||
}
|
||||
rawData, err := store.LoadGeneratedText(context.Background(), rawPath)
|
||||
if err != nil {
|
||||
t.Fatalf("LoadGeneratedText() raw error = %v", err)
|
||||
}
|
||||
if string(rawData) != `{"summary":"raw"}` {
|
||||
t.Fatalf("raw data = %q, want saved bytes", rawData)
|
||||
}
|
||||
generatedData, err := store.LoadGeneratedText(context.Background(), generatedPath)
|
||||
if err != nil {
|
||||
t.Fatalf("LoadGeneratedText() generated error = %v", err)
|
||||
}
|
||||
if !strings.Contains(string(generatedData), `"timing":"Late morning."`) {
|
||||
t.Fatalf("generated text = %q, want saved normalized JSON", generatedData)
|
||||
}
|
||||
var runResult struct {
|
||||
Command []string `json:"command"`
|
||||
Status string `json:"status"`
|
||||
}
|
||||
if err := store.LoadGeneratedTextResult(context.Background(), resultPath, &runResult); err != nil {
|
||||
t.Fatalf("LoadGeneratedTextResult() error = %v", err)
|
||||
}
|
||||
if runResult.Status != "succeeded" || strings.Join(runResult.Command, " ") != "scriptorium run" {
|
||||
t.Fatalf("run result = %#v, want saved result", runResult)
|
||||
}
|
||||
var renderContext struct {
|
||||
ReportTitle string `json:"reportTitle"`
|
||||
Location string `json:"location"`
|
||||
}
|
||||
if err := store.LoadRenderContext(context.Background(), contextPath, &renderContext); err != nil {
|
||||
t.Fatalf("LoadRenderContext() error = %v", err)
|
||||
}
|
||||
if renderContext.ReportTitle != "Hourly Report" || renderContext.Location != "Brentwood" {
|
||||
t.Fatalf("render context = %#v, want saved context", renderContext)
|
||||
}
|
||||
|
||||
paths, err := store.Paths(resolved)
|
||||
if err != nil {
|
||||
t.Fatalf("Paths() error = %v", err)
|
||||
}
|
||||
metadata := BuildMetadataFromBriefingMetadata(resolved, briefingMetadata, ArtifactPaths{
|
||||
ModuleSnapshot: paths.ModuleSnapshot,
|
||||
Metadata: paths.Metadata,
|
||||
DataPackage: paths.DataPackage,
|
||||
Preflight: paths.Preflight,
|
||||
RenderedReport: paths.RenderedReport,
|
||||
GeneratedTextRaw: rawPath,
|
||||
GeneratedTextResult: resultPath,
|
||||
GeneratedText: generatedPath,
|
||||
RenderContext: contextPath,
|
||||
})
|
||||
metadataPath, err := store.SaveMetadata(context.Background(), metadata)
|
||||
if err != nil {
|
||||
t.Fatalf("SaveMetadata() error = %v", err)
|
||||
}
|
||||
var decoded Metadata
|
||||
metadataData, err := os.ReadFile(metadataPath)
|
||||
if err != nil {
|
||||
t.Fatalf("read metadata: %v", err)
|
||||
}
|
||||
if err := json.Unmarshal(metadataData, &decoded); err != nil {
|
||||
t.Fatalf("decode metadata: %v", err)
|
||||
}
|
||||
if decoded.GeneratedTextSchemaID != "hourly" {
|
||||
t.Fatalf("GeneratedTextSchemaID = %q, want hourly", decoded.GeneratedTextSchemaID)
|
||||
}
|
||||
if decoded.GeneratedTextRawPath != rawPath || decoded.GeneratedTextResultPath != resultPath || decoded.GeneratedTextPath != generatedPath || decoded.RenderContextPath != contextPath {
|
||||
t.Fatalf("metadata generated-text paths = %#v, want saved artifact paths", decoded)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSaveMetadataUsesExplicitMetadataPath(t *testing.T) {
|
||||
@@ -471,5 +611,9 @@ func pathsString(paths ArtifactPaths) string {
|
||||
paths.Preflight,
|
||||
paths.Notification,
|
||||
paths.RenderedReport,
|
||||
paths.GeneratedTextRaw,
|
||||
paths.GeneratedTextResult,
|
||||
paths.GeneratedText,
|
||||
paths.RenderContext,
|
||||
}, "\n")
|
||||
}
|
||||
|
||||
@@ -12,30 +12,35 @@ import (
|
||||
const MetadataSchemaVersion = "weatherreporter.metadata.v1"
|
||||
|
||||
type Metadata struct {
|
||||
SchemaVersion string `json:"schemaVersion"`
|
||||
RunID string `json:"runId"`
|
||||
MetadataPath string `json:"-"`
|
||||
ReportID report.ID `json:"reportId"`
|
||||
Variant string `json:"variant,omitempty"`
|
||||
PromptID string `json:"promptId"`
|
||||
GeneratedAt time.Time `json:"generatedAt"`
|
||||
Timezone string `json:"timezone"`
|
||||
ValidPeriod timeutil.Period `json:"validPeriod"`
|
||||
Location *briefing.LocationContext `json:"location,omitempty"`
|
||||
SourceLocationID string `json:"sourceLocationId,omitempty"`
|
||||
SourceLocation string `json:"sourceLocation,omitempty"`
|
||||
Sources []briefing.SourceMetadata `json:"sources,omitempty"`
|
||||
SourceWarnings []weatherdata.SourceWarning `json:"sourceWarnings,omitempty"`
|
||||
ModuleSnapshotPath string `json:"moduleSnapshotPath"`
|
||||
DataPackagePath string `json:"dataPackagePath"`
|
||||
PreflightPath string `json:"preflightPath"`
|
||||
NotificationPath string `json:"notificationPath,omitempty"`
|
||||
RenderedReportPath string `json:"renderedReportPath,omitempty"`
|
||||
SchemaVersion string `json:"schemaVersion"`
|
||||
RunID string `json:"runId"`
|
||||
MetadataPath string `json:"-"`
|
||||
ReportID report.ID `json:"reportId"`
|
||||
Variant string `json:"variant,omitempty"`
|
||||
PromptID string `json:"promptId"`
|
||||
GeneratedAt time.Time `json:"generatedAt"`
|
||||
Timezone string `json:"timezone"`
|
||||
ValidPeriod timeutil.Period `json:"validPeriod"`
|
||||
Location *briefing.LocationContext `json:"location,omitempty"`
|
||||
SourceLocationID string `json:"sourceLocationId,omitempty"`
|
||||
SourceLocation string `json:"sourceLocation,omitempty"`
|
||||
Sources []briefing.SourceMetadata `json:"sources,omitempty"`
|
||||
SourceWarnings []weatherdata.SourceWarning `json:"sourceWarnings,omitempty"`
|
||||
ModuleSnapshotPath string `json:"moduleSnapshotPath"`
|
||||
DataPackagePath string `json:"dataPackagePath"`
|
||||
PreflightPath string `json:"preflightPath"`
|
||||
NotificationPath string `json:"notificationPath,omitempty"`
|
||||
RenderedReportPath string `json:"renderedReportPath,omitempty"`
|
||||
GeneratedTextSchemaID string `json:"generatedTextSchemaId,omitempty"`
|
||||
GeneratedTextRawPath string `json:"generatedTextRawPath,omitempty"`
|
||||
GeneratedTextResultPath string `json:"generatedTextResultPath,omitempty"`
|
||||
GeneratedTextPath string `json:"generatedTextPath,omitempty"`
|
||||
RenderContextPath string `json:"renderContextPath,omitempty"`
|
||||
}
|
||||
|
||||
func BuildMetadataFromBriefingMetadata(resolved report.Resolved, briefingMetadata briefing.Metadata, paths ArtifactPaths) Metadata {
|
||||
metadata := resolved.Metadata()
|
||||
return Metadata{
|
||||
out := Metadata{
|
||||
SchemaVersion: MetadataSchemaVersion,
|
||||
RunID: metadata.RunID,
|
||||
MetadataPath: paths.Metadata,
|
||||
@@ -55,6 +60,14 @@ func BuildMetadataFromBriefingMetadata(resolved report.Resolved, briefingMetadat
|
||||
PreflightPath: paths.Preflight,
|
||||
RenderedReportPath: paths.RenderedReport,
|
||||
}
|
||||
if resolved.Definition.GenerationMode == report.GenerationModeGeneratedTextTemplate {
|
||||
out.GeneratedTextSchemaID = resolved.Definition.GeneratedTextSchemaID
|
||||
out.GeneratedTextRawPath = paths.GeneratedTextRaw
|
||||
out.GeneratedTextResultPath = paths.GeneratedTextResult
|
||||
out.GeneratedTextPath = paths.GeneratedText
|
||||
out.RenderContextPath = paths.RenderContext
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func copyLocation(location *briefing.LocationContext) *briefing.LocationContext {
|
||||
|
||||
@@ -17,10 +17,17 @@ type Store interface {
|
||||
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)
|
||||
SaveGeneratedTextRaw(context.Context, report.Resolved, []byte) (string, error)
|
||||
SaveGeneratedTextResult(context.Context, report.Resolved, any) (string, error)
|
||||
SaveGeneratedText(context.Context, report.Resolved, []byte) (string, error)
|
||||
SaveRenderContext(context.Context, report.Resolved, any) (string, error)
|
||||
PrepareRenderedReport(context.Context, report.Resolved) (string, error)
|
||||
SaveMetadata(context.Context, Metadata) (string, error)
|
||||
FindPriorSnapshot(context.Context, report.Resolved) (*PriorSnapshot, error)
|
||||
LoadModuleSnapshot(context.Context, string) (module.Snapshot, error)
|
||||
LoadGeneratedText(context.Context, string) ([]byte, error)
|
||||
LoadGeneratedTextResult(context.Context, string, any) error
|
||||
LoadRenderContext(context.Context, string, any) error
|
||||
}
|
||||
|
||||
type PriorSnapshot struct {
|
||||
|
||||
Reference in New Issue
Block a user