Centralize atomic artifact writes

This commit is contained in:
2026-05-29 20:33:35 +00:00
parent 7dc2ac9253
commit 1355605e70
13 changed files with 207 additions and 181 deletions

View File

@@ -48,8 +48,8 @@ CLI contract used by this project.
## State Or Manifest Behavior ## State Or Manifest Behavior
`SaveRenderResult` can write render results atomically. The app and state store None directly. The adapter returns render and run results; app orchestration and
own managed preflight paths and metadata links. state own managed preflight paths and metadata links.
## Skip And Resume Behavior ## Skip And Resume Behavior

View File

@@ -15,7 +15,7 @@ Inputs:
- resolved report definition and valid period - resolved report definition and valid period
- briefing package - briefing package
- prompt input data package - prompt input data package
- Scriptorium render result - preflight artifact
- rendered report path preparation request - rendered report path preparation request
- RunID for inspection lookups - RunID for inspection lookups
@@ -35,7 +35,8 @@ Outputs:
- This package owns managed workspace layout, path validation, filesystem - This package owns managed workspace layout, path validation, filesystem
writes, and metadata reads. writes, and metadata reads.
- It does not fetch weather data, derive forecasts, build prompt input content, - It does not fetch weather data, derive forecasts, build prompt input content,
compare briefing contents, invoke Scriptorium, or parse CLI flags. compare briefing contents, invoke Scriptorium, import adapter result types, or
parse CLI flags.
## Config Fields Used ## Config Fields Used

View File

@@ -12,6 +12,7 @@ Developers and LLM coding agents should use it with
- `internal/cli`: command parsing, flag handling, help text, and JSON output. - `internal/cli`: command parsing, flag handling, help text, and JSON output.
- `internal/config`: configuration structs, defaults, loading, overrides, and - `internal/config`: configuration structs, defaults, loading, overrides, and
validation. validation.
- `internal/fileutil`: shared atomic filesystem write and copy helpers.
- `internal/adapters/weatherapi`: Weather API HTTP adapter. - `internal/adapters/weatherapi`: Weather API HTTP adapter.
- `internal/adapters/scriptorium`: Scriptorium subprocess adapter. - `internal/adapters/scriptorium`: Scriptorium subprocess adapter.
- `internal/forecast`: normalized bundle types and deterministic forecast - `internal/forecast`: normalized bundle types and deterministic forecast

View File

@@ -3,12 +3,9 @@ package scriptorium
import ( import (
"context" "context"
"encoding/json"
"fmt" "fmt"
"io" "io"
"os"
"os/exec" "os/exec"
"path/filepath"
"time" "time"
) )
@@ -207,37 +204,6 @@ func (r Runner) runArgs(req RunRequest) []string {
return args return args
} }
func SaveRenderResult(path string, result *RenderResult) error {
if result == nil {
return fmt.Errorf("render result is required")
}
data, err := json.MarshalIndent(result, "", " ")
if err != nil {
return fmt.Errorf("marshal render result: %w", err)
}
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
return fmt.Errorf("create preflight directory %q: %w", filepath.Dir(path), err)
}
tmp, err := os.CreateTemp(filepath.Dir(path), "."+filepath.Base(path)+".*.tmp")
if err != nil {
return fmt.Errorf("create temporary preflight file: %w", err)
}
tmpName := tmp.Name()
defer os.Remove(tmpName)
if _, err := tmp.Write(data); err != nil {
tmp.Close()
return fmt.Errorf("write temporary preflight file: %w", err)
}
if err := tmp.Close(); err != nil {
return fmt.Errorf("close temporary preflight file: %w", err)
}
if err := os.Rename(tmpName, path); err != nil {
return fmt.Errorf("save preflight %q: %w", path, err)
}
return nil
}
type limitedBuffer struct { type limitedBuffer struct {
data []byte data []byte
limit int limit int

View File

@@ -11,14 +11,13 @@ import (
"io" "io"
"net/http" "net/http"
"net/url" "net/url"
"os"
"path" "path"
"path/filepath"
"strconv" "strconv"
"strings" "strings"
"time" "time"
"gitea.maximumdirect.net/eric/weatherreporter/internal/config" "gitea.maximumdirect.net/eric/weatherreporter/internal/config"
"gitea.maximumdirect.net/eric/weatherreporter/internal/fileutil"
"gitea.maximumdirect.net/eric/weatherreporter/internal/forecast" "gitea.maximumdirect.net/eric/weatherreporter/internal/forecast"
) )
@@ -398,29 +397,8 @@ func sourceHash(raw json.RawMessage) (string, error) {
} }
func SaveBundle(path string, bundle *forecast.Bundle) error { func SaveBundle(path string, bundle *forecast.Bundle) error {
data, err := json.MarshalIndent(bundle, "", " ") if err := fileutil.WriteJSONAtomic(path, bundle); err != nil {
if err != nil { return fmt.Errorf("save bundle: %w", err)
return fmt.Errorf("marshal forecast bundle: %w", err)
}
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
return fmt.Errorf("create bundle directory %q: %w", filepath.Dir(path), err)
}
tmp, err := os.CreateTemp(filepath.Dir(path), "."+filepath.Base(path)+".*.tmp")
if err != nil {
return fmt.Errorf("create temporary bundle file: %w", err)
}
tmpName := tmp.Name()
defer os.Remove(tmpName)
if _, err := tmp.Write(data); err != nil {
tmp.Close()
return fmt.Errorf("write temporary bundle file: %w", err)
}
if err := tmp.Close(); err != nil {
return fmt.Errorf("close temporary bundle file: %w", err)
}
if err := os.Rename(tmpName, path); err != nil {
return fmt.Errorf("save bundle %q: %w", path, err)
} }
return nil return nil
} }

View File

@@ -4,7 +4,6 @@ package app
import ( import (
"context" "context"
"fmt" "fmt"
"os"
"path/filepath" "path/filepath"
"time" "time"
@@ -13,6 +12,7 @@ import (
"gitea.maximumdirect.net/eric/weatherreporter/internal/briefing" "gitea.maximumdirect.net/eric/weatherreporter/internal/briefing"
"gitea.maximumdirect.net/eric/weatherreporter/internal/changes" "gitea.maximumdirect.net/eric/weatherreporter/internal/changes"
"gitea.maximumdirect.net/eric/weatherreporter/internal/config" "gitea.maximumdirect.net/eric/weatherreporter/internal/config"
"gitea.maximumdirect.net/eric/weatherreporter/internal/fileutil"
"gitea.maximumdirect.net/eric/weatherreporter/internal/forecast" "gitea.maximumdirect.net/eric/weatherreporter/internal/forecast"
"gitea.maximumdirect.net/eric/weatherreporter/internal/promptinput" "gitea.maximumdirect.net/eric/weatherreporter/internal/promptinput"
"gitea.maximumdirect.net/eric/weatherreporter/internal/report" "gitea.maximumdirect.net/eric/weatherreporter/internal/report"
@@ -450,7 +450,7 @@ func GenerateReport(ctx context.Context, req ReportRequest) (*ReportResult, erro
preflightPath := paths.Preflight preflightPath := paths.Preflight
if renderResult != nil { if renderResult != nil {
var err error var err error
preflightPath, err = store.SavePreflight(ctx, req.Resolved, renderResult) preflightPath, err = store.SavePreflight(ctx, req.Resolved, preflightArtifact(renderResult))
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -480,7 +480,7 @@ func GenerateReport(ctx context.Context, req ReportRequest) (*ReportResult, erro
OutputPath: reportPath, OutputPath: reportPath,
}) })
if runErr == nil && req.OutputPath != "" && req.OutputPath != reportPath { if runErr == nil && req.OutputPath != "" && req.OutputPath != reportPath {
if err := copyFileAtomic(reportPath, req.OutputPath); err != nil { if err := fileutil.CopyFileAtomic(reportPath, req.OutputPath); err != nil {
return nil, err return nil, err
} }
} }
@@ -608,29 +608,16 @@ func recentChanges(ctx context.Context, store state.Store, priorSnapshot *state.
} }
} }
func copyFileAtomic(source string, target string) error { func preflightArtifact(result *scriptorium.RenderResult) state.PreflightArtifact {
data, err := os.ReadFile(source) if result == nil {
if err != nil { return state.PreflightArtifact{}
return fmt.Errorf("read rendered report %q: %w", source, err)
} }
if err := os.MkdirAll(filepath.Dir(target), 0o755); err != nil { return state.PreflightArtifact{
return fmt.Errorf("create report output directory %q: %w", filepath.Dir(target), err) Command: append([]string(nil), result.Command...),
Stdout: result.Stdout,
Stderr: result.Stderr,
StdoutTruncated: result.StdoutTruncated,
StderrTruncated: result.StderrTruncated,
ExitCode: result.ExitCode,
} }
tmp, err := os.CreateTemp(filepath.Dir(target), "."+filepath.Base(target)+".*.tmp")
if err != nil {
return fmt.Errorf("create temporary report output file: %w", err)
}
tmpName := tmp.Name()
defer os.Remove(tmpName)
if _, err := tmp.Write(data); err != nil {
tmp.Close()
return fmt.Errorf("write temporary report output file: %w", err)
}
if err := tmp.Close(); err != nil {
return fmt.Errorf("close temporary report output file: %w", err)
}
if err := os.Rename(tmpName, target); err != nil {
return fmt.Errorf("save report output %q: %w", target, err)
}
return nil
} }

View File

@@ -2,12 +2,10 @@
package briefing package briefing
import ( import (
"encoding/json"
"fmt" "fmt"
"os"
"path/filepath"
"time" "time"
"gitea.maximumdirect.net/eric/weatherreporter/internal/fileutil"
"gitea.maximumdirect.net/eric/weatherreporter/internal/forecast" "gitea.maximumdirect.net/eric/weatherreporter/internal/forecast"
"gitea.maximumdirect.net/eric/weatherreporter/internal/report" "gitea.maximumdirect.net/eric/weatherreporter/internal/report"
"gitea.maximumdirect.net/eric/weatherreporter/internal/timeutil" "gitea.maximumdirect.net/eric/weatherreporter/internal/timeutil"
@@ -78,29 +76,8 @@ func BuildMetadata(ctx BuildContext) Metadata {
} }
func Save(path string, pkg Package) error { func Save(path string, pkg Package) error {
data, err := json.MarshalIndent(pkg, "", " ") if err := fileutil.WriteJSONAtomic(path, pkg); err != nil {
if err != nil { return fmt.Errorf("save briefing package: %w", err)
return fmt.Errorf("marshal briefing package: %w", err)
}
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
return fmt.Errorf("create briefing directory %q: %w", filepath.Dir(path), err)
}
tmp, err := os.CreateTemp(filepath.Dir(path), "."+filepath.Base(path)+".*.tmp")
if err != nil {
return fmt.Errorf("create temporary briefing file: %w", err)
}
tmpName := tmp.Name()
defer os.Remove(tmpName)
if _, err := tmp.Write(data); err != nil {
tmp.Close()
return fmt.Errorf("write temporary briefing file: %w", err)
}
if err := tmp.Close(); err != nil {
return fmt.Errorf("close temporary briefing file: %w", err)
}
if err := os.Rename(tmpName, path); err != nil {
return fmt.Errorf("save briefing %q: %w", path, err)
} }
return nil return nil
} }

View File

@@ -0,0 +1,48 @@
// Package fileutil provides narrow filesystem helpers for durable artifacts.
package fileutil
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
)
func WriteFileAtomic(path string, data []byte) error {
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
return fmt.Errorf("create directory %q: %w", filepath.Dir(path), err)
}
tmp, err := os.CreateTemp(filepath.Dir(path), "."+filepath.Base(path)+".*.tmp")
if err != nil {
return fmt.Errorf("create temporary file for %q: %w", path, err)
}
tmpName := tmp.Name()
defer os.Remove(tmpName)
if _, err := tmp.Write(data); err != nil {
tmp.Close()
return fmt.Errorf("write temporary file for %q: %w", path, err)
}
if err := tmp.Close(); err != nil {
return fmt.Errorf("close temporary file for %q: %w", path, err)
}
if err := os.Rename(tmpName, path); err != nil {
return fmt.Errorf("save %q: %w", path, err)
}
return nil
}
func WriteJSONAtomic(path string, value any) error {
data, err := json.MarshalIndent(value, "", " ")
if err != nil {
return fmt.Errorf("marshal %q: %w", path, err)
}
return WriteFileAtomic(path, data)
}
func CopyFileAtomic(source string, target string) error {
data, err := os.ReadFile(source)
if err != nil {
return fmt.Errorf("read %q: %w", source, err)
}
return WriteFileAtomic(target, data)
}

View File

@@ -0,0 +1,103 @@
package fileutil
import (
"os"
"path/filepath"
"strings"
"testing"
)
func TestWriteFileAtomicCreatesParentDirectory(t *testing.T) {
path := filepath.Join(t.TempDir(), "nested", "artifact.txt")
if err := WriteFileAtomic(path, []byte("artifact")); err != nil {
t.Fatalf("WriteFileAtomic() error = %v", err)
}
data, err := os.ReadFile(path)
if err != nil {
t.Fatalf("ReadFile() error = %v", err)
}
if string(data) != "artifact" {
t.Fatalf("data = %q, want artifact", data)
}
}
func TestWriteFileAtomicOverwritesTarget(t *testing.T) {
path := filepath.Join(t.TempDir(), "artifact.txt")
if err := WriteFileAtomic(path, []byte("old")); err != nil {
t.Fatalf("WriteFileAtomic() initial error = %v", err)
}
if err := WriteFileAtomic(path, []byte("new")); err != nil {
t.Fatalf("WriteFileAtomic() overwrite error = %v", err)
}
data, err := os.ReadFile(path)
if err != nil {
t.Fatalf("ReadFile() error = %v", err)
}
if string(data) != "new" {
t.Fatalf("data = %q, want new", data)
}
}
func TestWriteFileAtomicCleansTemporaryFileAfterRenameError(t *testing.T) {
dir := t.TempDir()
target := filepath.Join(dir, "target")
if err := os.Mkdir(target, 0o755); err != nil {
t.Fatalf("Mkdir() error = %v", err)
}
err := WriteFileAtomic(target, []byte("data"))
if err == nil {
t.Fatal("WriteFileAtomic() error = nil, want rename error")
}
if !strings.Contains(err.Error(), "save") {
t.Fatalf("error = %q, want save context", err.Error())
}
matches, err := filepath.Glob(filepath.Join(dir, ".target.*.tmp"))
if err != nil {
t.Fatalf("Glob() error = %v", err)
}
if len(matches) != 0 {
t.Fatalf("temporary files = %v, want none", matches)
}
}
func TestWriteJSONAtomic(t *testing.T) {
path := filepath.Join(t.TempDir(), "artifact.json")
if err := WriteJSONAtomic(path, map[string]string{"status": "ok"}); err != nil {
t.Fatalf("WriteJSONAtomic() error = %v", err)
}
data, err := os.ReadFile(path)
if err != nil {
t.Fatalf("ReadFile() error = %v", err)
}
if string(data) != "{\n \"status\": \"ok\"\n}" {
t.Fatalf("json = %q, want indented object", data)
}
}
func TestCopyFileAtomic(t *testing.T) {
dir := t.TempDir()
source := filepath.Join(dir, "source.txt")
target := filepath.Join(dir, "nested", "target.txt")
if err := os.WriteFile(source, []byte("copied"), 0o600); err != nil {
t.Fatalf("WriteFile() error = %v", err)
}
if err := CopyFileAtomic(source, target); err != nil {
t.Fatalf("CopyFileAtomic() error = %v", err)
}
data, err := os.ReadFile(target)
if err != nil {
t.Fatalf("ReadFile() error = %v", err)
}
if string(data) != "copied" {
t.Fatalf("data = %q, want copied", data)
}
}

View File

@@ -2,14 +2,12 @@
package promptinput package promptinput
import ( import (
"encoding/json"
"fmt" "fmt"
"os"
"path/filepath"
"time" "time"
"gitea.maximumdirect.net/eric/weatherreporter/internal/briefing" "gitea.maximumdirect.net/eric/weatherreporter/internal/briefing"
"gitea.maximumdirect.net/eric/weatherreporter/internal/changes" "gitea.maximumdirect.net/eric/weatherreporter/internal/changes"
"gitea.maximumdirect.net/eric/weatherreporter/internal/fileutil"
"gitea.maximumdirect.net/eric/weatherreporter/internal/forecast" "gitea.maximumdirect.net/eric/weatherreporter/internal/forecast"
"gitea.maximumdirect.net/eric/weatherreporter/internal/report" "gitea.maximumdirect.net/eric/weatherreporter/internal/report"
"gitea.maximumdirect.net/eric/weatherreporter/internal/timeutil" "gitea.maximumdirect.net/eric/weatherreporter/internal/timeutil"
@@ -117,29 +115,8 @@ func Save(path string, pkg Package) error {
if err := Validate(pkg); err != nil { if err := Validate(pkg); err != nil {
return err return err
} }
data, err := json.MarshalIndent(pkg, "", " ") if err := fileutil.WriteJSONAtomic(path, pkg); err != nil {
if err != nil { return fmt.Errorf("save data package: %w", err)
return fmt.Errorf("marshal data package: %w", err)
}
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
return fmt.Errorf("create data package directory %q: %w", filepath.Dir(path), err)
}
tmp, err := os.CreateTemp(filepath.Dir(path), "."+filepath.Base(path)+".*.tmp")
if err != nil {
return fmt.Errorf("create temporary data package file: %w", err)
}
tmpName := tmp.Name()
defer os.Remove(tmpName)
if _, err := tmp.Write(data); err != nil {
tmp.Close()
return fmt.Errorf("write temporary data package file: %w", err)
}
if err := tmp.Close(); err != nil {
return fmt.Errorf("close temporary data package file: %w", err)
}
if err := os.Rename(tmpName, path); err != nil {
return fmt.Errorf("save data package %q: %w", path, err)
} }
return nil return nil
} }

View File

@@ -10,9 +10,9 @@ import (
"strings" "strings"
"time" "time"
"gitea.maximumdirect.net/eric/weatherreporter/internal/adapters/scriptorium"
"gitea.maximumdirect.net/eric/weatherreporter/internal/briefing" "gitea.maximumdirect.net/eric/weatherreporter/internal/briefing"
"gitea.maximumdirect.net/eric/weatherreporter/internal/config" "gitea.maximumdirect.net/eric/weatherreporter/internal/config"
"gitea.maximumdirect.net/eric/weatherreporter/internal/fileutil"
"gitea.maximumdirect.net/eric/weatherreporter/internal/promptinput" "gitea.maximumdirect.net/eric/weatherreporter/internal/promptinput"
"gitea.maximumdirect.net/eric/weatherreporter/internal/report" "gitea.maximumdirect.net/eric/weatherreporter/internal/report"
) )
@@ -99,7 +99,7 @@ func (s *FilesystemStore) SaveBriefing(_ context.Context, resolved report.Resolv
if err != nil { if err != nil {
return "", err return "", err
} }
if err := writeJSONAtomic(paths.Briefing, pkg); err != nil { if err := fileutil.WriteJSONAtomic(paths.Briefing, pkg); err != nil {
return "", err return "", err
} }
return paths.Briefing, nil return paths.Briefing, nil
@@ -113,21 +113,18 @@ func (s *FilesystemStore) SaveDataPackage(_ context.Context, resolved report.Res
if err := promptinput.Validate(pkg); err != nil { if err := promptinput.Validate(pkg); err != nil {
return "", err return "", err
} }
if err := writeJSONAtomic(paths.DataPackage, pkg); err != nil { if err := fileutil.WriteJSONAtomic(paths.DataPackage, pkg); err != nil {
return "", err return "", err
} }
return paths.DataPackage, nil return paths.DataPackage, nil
} }
func (s *FilesystemStore) SavePreflight(_ context.Context, resolved report.Resolved, result *scriptorium.RenderResult) (string, error) { func (s *FilesystemStore) SavePreflight(_ context.Context, resolved report.Resolved, artifact PreflightArtifact) (string, error) {
if result == nil {
return "", fmt.Errorf("render result is required")
}
paths, err := s.Paths(resolved) paths, err := s.Paths(resolved)
if err != nil { if err != nil {
return "", err return "", err
} }
if err := writeJSONAtomic(paths.Preflight, result); err != nil { if err := fileutil.WriteJSONAtomic(paths.Preflight, artifact); err != nil {
return "", err return "", err
} }
return paths.Preflight, nil return paths.Preflight, nil
@@ -161,7 +158,7 @@ func (s *FilesystemStore) SaveMetadata(_ context.Context, metadata Metadata) (st
if path == "" { if path == "" {
return "", fmt.Errorf("metadata path cannot be resolved") return "", fmt.Errorf("metadata path cannot be resolved")
} }
if err := writeJSONAtomic(path, metadata); err != nil { if err := fileutil.WriteJSONAtomic(path, metadata); err != nil {
return "", err return "", err
} }
return path, nil return path, nil
@@ -362,33 +359,6 @@ func validateRelativeDir(name string, value string) error {
return nil return nil
} }
func writeJSONAtomic(path string, value any) error {
data, err := json.MarshalIndent(value, "", " ")
if err != nil {
return fmt.Errorf("marshal %q: %w", path, err)
}
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
return fmt.Errorf("create directory %q: %w", filepath.Dir(path), err)
}
tmp, err := os.CreateTemp(filepath.Dir(path), "."+filepath.Base(path)+".*.tmp")
if err != nil {
return fmt.Errorf("create temporary file for %q: %w", path, err)
}
tmpName := tmp.Name()
defer os.Remove(tmpName)
if _, err := tmp.Write(data); err != nil {
tmp.Close()
return fmt.Errorf("write temporary file for %q: %w", path, err)
}
if err := tmp.Close(); err != nil {
return fmt.Errorf("close temporary file for %q: %w", path, err)
}
if err := os.Rename(tmpName, path); err != nil {
return fmt.Errorf("save %q: %w", path, err)
}
return nil
}
func readJSON(path string, target any) error { func readJSON(path string, target any) error {
data, err := os.ReadFile(path) data, err := os.ReadFile(path)
if err != nil { if err != nil {

View File

@@ -9,7 +9,6 @@ import (
"testing" "testing"
"time" "time"
"gitea.maximumdirect.net/eric/weatherreporter/internal/adapters/scriptorium"
"gitea.maximumdirect.net/eric/weatherreporter/internal/briefing" "gitea.maximumdirect.net/eric/weatherreporter/internal/briefing"
"gitea.maximumdirect.net/eric/weatherreporter/internal/config" "gitea.maximumdirect.net/eric/weatherreporter/internal/config"
"gitea.maximumdirect.net/eric/weatherreporter/internal/promptinput" "gitea.maximumdirect.net/eric/weatherreporter/internal/promptinput"
@@ -56,7 +55,7 @@ func TestSaveArtifactsAndMetadataRoundTrip(t *testing.T) {
if err != nil { if err != nil {
t.Fatalf("SaveDataPackage() error = %v", err) t.Fatalf("SaveDataPackage() error = %v", err)
} }
preflightPath, err := store.SavePreflight(context.Background(), resolved, &scriptorium.RenderResult{Stdout: `{"ok":true}`}) preflightPath, err := store.SavePreflight(context.Background(), resolved, PreflightArtifact{Stdout: `{"ok":true}`})
if err != nil { if err != nil {
t.Fatalf("SavePreflight() error = %v", err) t.Fatalf("SavePreflight() error = %v", err)
} }
@@ -67,6 +66,17 @@ func TestSaveArtifactsAndMetadataRoundTrip(t *testing.T) {
if err := os.WriteFile(renderedReportPath, []byte("# Daily Report\n"), 0o600); err != nil { if err := os.WriteFile(renderedReportPath, []byte("# Daily Report\n"), 0o600); err != nil {
t.Fatalf("write rendered report: %v", err) t.Fatalf("write rendered report: %v", err)
} }
var preflight PreflightArtifact
preflightData, err := os.ReadFile(preflightPath)
if err != nil {
t.Fatalf("read preflight: %v", err)
}
if err := json.Unmarshal(preflightData, &preflight); err != nil {
t.Fatalf("decode preflight: %v", err)
}
if preflight.Stdout != `{"ok":true}` {
t.Fatalf("preflight stdout = %q, want render stdout", preflight.Stdout)
}
paths, err := store.Paths(resolved) paths, err := store.Paths(resolved)
if err != nil { if err != nil {
t.Fatalf("Paths() error = %v", err) t.Fatalf("Paths() error = %v", err)

View File

@@ -4,7 +4,6 @@ package state
import ( import (
"context" "context"
"gitea.maximumdirect.net/eric/weatherreporter/internal/adapters/scriptorium"
"gitea.maximumdirect.net/eric/weatherreporter/internal/briefing" "gitea.maximumdirect.net/eric/weatherreporter/internal/briefing"
"gitea.maximumdirect.net/eric/weatherreporter/internal/promptinput" "gitea.maximumdirect.net/eric/weatherreporter/internal/promptinput"
"gitea.maximumdirect.net/eric/weatherreporter/internal/report" "gitea.maximumdirect.net/eric/weatherreporter/internal/report"
@@ -14,7 +13,7 @@ type Store interface {
Paths(report.Resolved) (ArtifactPaths, error) Paths(report.Resolved) (ArtifactPaths, error)
SaveBriefing(context.Context, report.Resolved, briefing.Package) (string, error) SaveBriefing(context.Context, report.Resolved, briefing.Package) (string, error)
SaveDataPackage(context.Context, report.Resolved, promptinput.Package) (string, error) SaveDataPackage(context.Context, report.Resolved, promptinput.Package) (string, error)
SavePreflight(context.Context, report.Resolved, *scriptorium.RenderResult) (string, error) SavePreflight(context.Context, report.Resolved, PreflightArtifact) (string, error)
PrepareRenderedReport(context.Context, report.Resolved) (string, error) PrepareRenderedReport(context.Context, report.Resolved) (string, error)
SaveMetadata(context.Context, Metadata) (string, error) SaveMetadata(context.Context, Metadata) (string, error)
FindPriorSnapshot(context.Context, report.Resolved) (*PriorSnapshot, error) FindPriorSnapshot(context.Context, report.Resolved) (*PriorSnapshot, error)
@@ -26,3 +25,12 @@ type PriorSnapshot struct {
Metadata Metadata Metadata Metadata
BriefingPath string BriefingPath string
} }
type PreflightArtifact struct {
Command []string `json:"command"`
Stdout string `json:"stdout"`
Stderr string `json:"stderr"`
StdoutTruncated bool `json:"stdoutTruncated,omitempty"`
StderrTruncated bool `json:"stderrTruncated,omitempty"`
ExitCode int `json:"exitCode"`
}