Add Phase 2 normalization diagnostics
This commit is contained in:
145
internal/core/diagnostics/run_dir.go
Normal file
145
internal/core/diagnostics/run_dir.go
Normal file
@@ -0,0 +1,145 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user