// Package archive optionally saves a local copy of each run's fetched data // and analysis, for debugging prompt/parsing issues without reproducing a // live run. package archive import ( "encoding/json" "fmt" "os" "path/filepath" "strings" "time" "github.com/ytshih/stonks/internal/config" "github.com/ytshih/stonks/internal/fetch" "github.com/ytshih/stonks/internal/report" ) const defaultDir = "~/.local/state/stonks/runs" // Run holds everything one pipeline run produced, for archiving. type Run struct { Result fetch.Result Prompt string // final prompt sent to the analyzer, including any advisor section RawResponse string // analyzer's raw, unparsed response Report report.Report } // Save writes Run's artifacts to a new timestamped subdirectory under // cfg.Dir (or defaultDir if cfg.Dir is empty) and returns that directory's // path. Callers should treat a non-nil error as non-fatal — archiving is a // debugging aid, not part of the report pipeline. func Save(cfg config.ArchiveConfig, run Run, now time.Time) (string, error) { base := cfg.Dir if base == "" { base = defaultDir } base, err := expandHome(base) if err != nil { return "", fmt.Errorf("archive: %w", err) } dir := filepath.Join(base, now.UTC().Format("20060102T150405Z")) if err := os.MkdirAll(dir, 0o755); err != nil { return "", fmt.Errorf("archive: create %q: %w", dir, err) } fetchedJSON, err := json.MarshalIndent(run.Result, "", " ") if err != nil { return "", fmt.Errorf("archive: marshal fetched data: %w", err) } recsJSON, err := json.MarshalIndent(run.Report.Recommendations, "", " ") if err != nil { return "", fmt.Errorf("archive: marshal recommendations: %w", err) } files := map[string][]byte{ "fetched.json": fetchedJSON, "prompt.txt": []byte(run.Prompt), "analysis_raw.md": []byte(run.RawResponse), "report.md": []byte(run.Report.Text), "recommendations.json": recsJSON, } for name, data := range files { if err := os.WriteFile(filepath.Join(dir, name), data, 0o644); err != nil { return dir, fmt.Errorf("archive: write %q: %w", name, err) } } return dir, nil } // expandHome replaces a leading "~" with the current user's home directory. func expandHome(path string) (string, error) { if path != "~" && !strings.HasPrefix(path, "~/") { return path, nil } home, err := os.UserHomeDir() if err != nil { return "", fmt.Errorf("resolve home directory: %w", err) } if path == "~" { return home, nil } return filepath.Join(home, path[2:]), nil }