226 lines
7.0 KiB
Go
226 lines
7.0 KiB
Go
package diagnostics
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"time"
|
|
|
|
"gitea.maximumdirect.net/eric/audita/internal/core/chunking"
|
|
"gitea.maximumdirect.net/eric/audita/internal/core/config"
|
|
"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
|
|
}
|
|
|
|
type RetentionDecisionInput struct {
|
|
RetentionMode string
|
|
RunSucceeded bool
|
|
HasSkippedCorrections bool
|
|
}
|
|
|
|
func ShouldRetainRunDirectory(input RetentionDecisionInput) bool {
|
|
// Failed runs are always retained.
|
|
if !input.RunSucceeded {
|
|
return true
|
|
}
|
|
|
|
switch input.RetentionMode {
|
|
case "always":
|
|
return true
|
|
case "never":
|
|
return true
|
|
case "auto":
|
|
return input.HasSkippedCorrections
|
|
default:
|
|
// Be conservative for unknown values.
|
|
return true
|
|
}
|
|
}
|
|
|
|
// InvocationMetadata captures non-secret invocation details for diagnostics.
|
|
type InvocationMetadata struct {
|
|
Operation string `json:"operation"`
|
|
TranscriptPath string `json:"transcript_path"`
|
|
GlossaryPath string `json:"glossary_path"`
|
|
OutputPath string `json:"output_path,omitempty"`
|
|
ReportJSONPath string `json:"report_json_path,omitempty"`
|
|
Modules []string `json:"modules"`
|
|
RunID string `json:"run_id"`
|
|
StartedAt time.Time `json:"started_at"`
|
|
}
|
|
|
|
// 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 a unique run directory identifier.
|
|
runID := fmt.Sprintf("run-%d", time.Now().UTC().UnixNano())
|
|
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
|
|
}
|
|
|
|
func (r *RunDirectory) runID() string {
|
|
return filepath.Base(r.path)
|
|
}
|
|
|
|
// WriteInvocationMetadata writes invocation metadata for this run.
|
|
func (r *RunDirectory) WriteInvocationMetadata(metadata InvocationMetadata) error {
|
|
if metadata.RunID == "" {
|
|
metadata.RunID = r.runID()
|
|
}
|
|
if metadata.StartedAt.IsZero() {
|
|
metadata.StartedAt = r.createdAt
|
|
}
|
|
|
|
path := filepath.Join(r.path, "invocation.json")
|
|
bytes, err := json.MarshalIndent(metadata, "", " ")
|
|
if err != nil {
|
|
return fmt.Errorf("failed to marshal invocation metadata: %w", err)
|
|
}
|
|
bytes = append(bytes, '\n')
|
|
if err := os.WriteFile(path, bytes, 0o644); err != nil {
|
|
return fmt.Errorf("failed to write invocation metadata: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// WriteEffectiveConfig writes redacted effective config metadata for this run.
|
|
func (r *RunDirectory) WriteEffectiveConfig(cfg config.Config) error {
|
|
path := filepath.Join(r.path, "effective-config.json")
|
|
redacted := cfg.Redacted()
|
|
bytes, err := json.MarshalIndent(redacted, "", " ")
|
|
if err != nil {
|
|
return fmt.Errorf("failed to marshal effective config: %w", err)
|
|
}
|
|
bytes = append(bytes, '\n')
|
|
if err := os.WriteFile(path, bytes, 0o644); err != nil {
|
|
return fmt.Errorf("failed to write effective config: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// 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)
|
|
}
|
|
|
|
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)
|
|
}
|
|
|
|
// WriteChunkingSummary writes the chunking summary artifact
|
|
func (r *RunDirectory) WriteChunkingSummary(summary *chunking.DetailedSummary) error {
|
|
summaryPath := filepath.Join(r.path, "chunking-summary.json")
|
|
bytes, err := json.MarshalIndent(summary, "", " ")
|
|
if err != nil {
|
|
return fmt.Errorf("failed to marshal chunking summary: %w", err)
|
|
}
|
|
bytes = append(bytes, '\n')
|
|
if err := os.WriteFile(summaryPath, bytes, 0o644); err != nil {
|
|
return fmt.Errorf("failed to write chunking summary: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (r *RunDirectory) ApplyRetention(input RetentionDecisionInput) error {
|
|
decision := input
|
|
if decision.RetentionMode == "" {
|
|
decision.RetentionMode = r.retention
|
|
}
|
|
|
|
if ShouldRetainRunDirectory(decision) {
|
|
return nil
|
|
}
|
|
return os.RemoveAll(r.path)
|
|
}
|