87 lines
2.5 KiB
Go
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"
|
|
|
|
"gitea.konchin.com/service/stonks/internal/config"
|
|
"gitea.konchin.com/service/stonks/internal/fetch"
|
|
"gitea.konchin.com/service/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
|
|
}
|