Files
stonks/internal/archive/archive.go
T
2026-09-05 09:55:24 +08:00

87 lines
2.5 KiB
Go

// 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
}