package diagnostics import ( "encoding/json" "fmt" "os" "path/filepath" "time" "gitea.maximumdirect.net/eric/audita/internal/core/normalization" "gitea.maximumdirect.net/eric/audita/internal/core/reporting" "gitea.maximumdirect.net/eric/audita/internal/core/schema" ) // RunDirectory represents a per-run diagnostics directory type RunDirectory struct { path string retention string createdAt time.Time sourceSaved bool } // NewRunDirectory creates a new run directory under the configured work dir func NewRunDirectory(workDir, retention string) (*RunDirectory, error) { if workDir == "" { workDir = ".audita-runs" } // Create work directory if it doesn't exist if err := os.MkdirAll(workDir, 0o755); err != nil { return nil, fmt.Errorf("failed to create work directory %q: %w", workDir, err) } // Create unique run directory with timestamp timestamp := time.Now().UTC().Format("2006-01-02T15-04-05Z") runID := fmt.Sprintf("run-%s", timestamp) runPath := filepath.Join(workDir, runID) if err := os.Mkdir(runPath, 0o755); err != nil { return nil, fmt.Errorf("failed to create run directory %q: %w", runPath, err) } return &RunDirectory{ path: runPath, retention: retention, createdAt: time.Now().UTC(), }, nil } // Path returns the run directory path func (r *RunDirectory) Path() string { return r.path } // WriteSourceTranscript writes the source transcript artifact func (r *RunDirectory) WriteSourceTranscript(transcript *schema.SourceTranscript, raw []byte) error { // Write raw source for reference sourcePath := filepath.Join(r.path, "source-transcript.json") if err := os.WriteFile(sourcePath, raw, 0o644); err != nil { return fmt.Errorf("failed to write source transcript: %w", err) } // Write parsed source for debugging parsedPath := filepath.Join(r.path, "source-transcript-parsed.json") parsedBytes, err := json.MarshalIndent(transcript, "", " ") if err != nil { return fmt.Errorf("failed to marshal parsed source transcript: %w", err) } parsedBytes = append(parsedBytes, '\n') if err := os.WriteFile(parsedPath, parsedBytes, 0o644); err != nil { return fmt.Errorf("failed to write parsed source transcript: %w", err) } r.sourceSaved = true return nil } // WriteNormalizedTranscript writes the normalized transcript artifact func (r *RunDirectory) WriteNormalizedTranscript(transcript *schema.Transcript) error { normalizedPath := filepath.Join(r.path, "normalized-transcript.json") bytes, err := schema.TranscriptToJSON(transcript) if err != nil { return fmt.Errorf("failed to serialize normalized transcript: %w", err) } if err := os.WriteFile(normalizedPath, bytes, 0o644); err != nil { return fmt.Errorf("failed to write normalized transcript: %w", err) } return nil } // WriteNormalizationSummary writes the normalization summary artifact func (r *RunDirectory) WriteNormalizationSummary(summary *normalization.NormalizationSummary) error { summaryPath := filepath.Join(r.path, "normalization-summary.json") bytes, err := json.MarshalIndent(summary, "", " ") if err != nil { return fmt.Errorf("failed to marshal normalization summary: %w", err) } bytes = append(bytes, '\n') if err := os.WriteFile(summaryPath, bytes, 0o644); err != nil { return fmt.Errorf("failed to write normalization summary: %w", err) } return nil } // WriteReport writes the authoritative report artifact func (r *RunDirectory) WriteReport(report reporting.ProcessReport) error { reportPath := filepath.Join(r.path, "report.json") bytes, err := json.MarshalIndent(report, "", " ") if err != nil { return fmt.Errorf("failed to marshal report: %w", err) } bytes = append(bytes, '\n') if err := os.WriteFile(reportPath, bytes, 0o644); err != nil { return fmt.Errorf("failed to write report: %w", err) } return nil } // WriteErrorLog writes an error log on failure func (r *RunDirectory) WriteErrorLog(errorMessage string) error { errorPath := filepath.Join(r.path, "error.log") return os.WriteFile(errorPath, []byte(errorMessage+"\n"), 0o644) } // ApplyRetention applies the retention policy // TODO: In later phases, implement full retention logic including: // - auto mode should keep runs with skipped corrections // - Add proper cleanup of old runs // - Consider disk space limits func (r *RunDirectory) ApplyRetention() error { // For Phase 2, implement minimal retention: // - always: keep everything (do nothing) // - never: remove successful runs // - auto: remove successful runs (same as never for now) // - failed runs are always kept (handled by caller) if r.retention == "never" || r.retention == "auto" { // Remove successful runs if r.sourceSaved { // Simple heuristic: if we saved source, it was successful return os.RemoveAll(r.path) } } // always: keep everything, or failed runs: keep everything return nil }