From 0e839915371344a17d927b8253ab305283c62f8a Mon Sep 17 00:00:00 2001 From: Eric Rakestraw Date: Mon, 11 May 2026 13:59:46 +0000 Subject: [PATCH] Add diagnostics references to reports --- internal/cli/run.go | 23 +++++++++-- internal/cli/run_test.go | 65 +++++++++++++++++++++++++++++++ internal/core/reporting/report.go | 13 +++++++ 3 files changed, 98 insertions(+), 3 deletions(-) diff --git a/internal/cli/run.go b/internal/cli/run.go index a8b549d..fee41a9 100644 --- a/internal/cli/run.go +++ b/internal/cli/run.go @@ -5,6 +5,7 @@ import ( "flag" "fmt" "io" + "path/filepath" "strings" "time" @@ -296,7 +297,7 @@ func runProcess(args []string, stdout, stderr io.Writer) int { if runErr != nil { errorPhase, errorMessage := extractErrorPhase(runErr) - report := buildProcessReport("failed", inv, startedAt, completedAt, errorMessage, errorPhase, nil, nil) + report := buildProcessReport("failed", inv, runDir, startedAt, completedAt, errorMessage, errorPhase, nil, nil) if strings.TrimSpace(inv.ReportJSONPath) != "" { if err := reporting.WriteProcessReport(inv.ReportJSONPath, report); err != nil { @@ -313,7 +314,7 @@ func runProcess(args []string, stdout, stderr io.Writer) int { return 1 } - report := buildProcessReport("success", inv, startedAt, completedAt, "", "", normSummary, chunkSummary) + report := buildProcessReport("success", inv, runDir, startedAt, completedAt, "", "", normSummary, chunkSummary) if strings.TrimSpace(inv.ReportJSONPath) != "" { if err := reporting.WriteProcessReport(inv.ReportJSONPath, report); err != nil { @@ -348,7 +349,7 @@ func extractErrorPhase(err error) (phase string, message string) { return "", msg } -func buildProcessReport(status string, inv processInvocation, startedAt, completedAt time.Time, errorMessage string, errorPhase string, normalizationSummary *normalization.NormalizationSummary, chunkingSummary *chunking.Summary) reporting.ProcessReport { +func buildProcessReport(status string, inv processInvocation, runDir *diagnostics.RunDirectory, startedAt, completedAt time.Time, errorMessage string, errorPhase string, normalizationSummary *normalization.NormalizationSummary, chunkingSummary *chunking.Summary) reporting.ProcessReport { report := reporting.ProcessReport{ Phase: "phase3-chunking", Status: status, @@ -361,6 +362,22 @@ func buildProcessReport(status string, inv processInvocation, startedAt, complet CompletedAt: &completedAt, ErrorPhase: errorPhase, } + if runDir != nil { + diagnosticsDir := runDir.Path() + report.Diagnostics = &reporting.DiagnosticsMetadata{ + DirectoryPath: diagnosticsDir, + SourceTranscriptPath: filepath.Join(diagnosticsDir, "source-transcript.json"), + ParsedSourceTranscriptPath: filepath.Join(diagnosticsDir, "source-transcript-parsed.json"), + NormalizedTranscriptPath: filepath.Join(diagnosticsDir, "normalized-transcript.json"), + NormalizationSummaryPath: filepath.Join(diagnosticsDir, "normalization-summary.json"), + ChunkingSummaryPath: filepath.Join(diagnosticsDir, "chunking-summary.json"), + InvocationMetadataPath: filepath.Join(diagnosticsDir, "invocation.json"), + RedactedEffectiveConfigPath: filepath.Join(diagnosticsDir, "effective-config.json"), + } + if status == "failed" { + report.Diagnostics.ErrorLogPath = filepath.Join(diagnosticsDir, "error.log") + } + } if errorMessage != "" { report.ErrorMessage = errorMessage } diff --git a/internal/cli/run_test.go b/internal/cli/run_test.go index 6d27613..05a2fce 100644 --- a/internal/cli/run_test.go +++ b/internal/cli/run_test.go @@ -276,6 +276,21 @@ func TestRunProcessReportJSONSuccessIncludesNormalizationSummary(t *testing.T) { if report.NormalizationMerges == nil || *report.NormalizationMerges != 1 { t.Fatalf("expected 1 merge, got %v", report.NormalizationMerges) } + if report.Diagnostics == nil { + t.Fatalf("expected diagnostics metadata in success report") + } + if report.Diagnostics.DirectoryPath == "" { + t.Fatalf("expected diagnostics directory path in success report") + } + if report.Diagnostics.SourceTranscriptPath == "" || + report.Diagnostics.ParsedSourceTranscriptPath == "" || + report.Diagnostics.NormalizedTranscriptPath == "" || + report.Diagnostics.NormalizationSummaryPath == "" || + report.Diagnostics.ChunkingSummaryPath == "" || + report.Diagnostics.InvocationMetadataPath == "" || + report.Diagnostics.RedactedEffectiveConfigPath == "" { + t.Fatalf("expected populated diagnostics artifact references in success report: %+v", report.Diagnostics) + } } func TestRunProcessReportJSONBestEffortOnFailure(t *testing.T) { @@ -283,6 +298,7 @@ func TestRunProcessReportJSONBestEffortOnFailure(t *testing.T) { var stderr bytes.Buffer reportPath := filepath.Join(t.TempDir(), "report.json") + workDir := t.TempDir() exitCode := Run([]string{ "process", fixturePath("malformed_transcript.json"), @@ -290,6 +306,10 @@ func TestRunProcessReportJSONBestEffortOnFailure(t *testing.T) { fixturePath("tiny_glossary.yaml"), "--report-json", reportPath, + "--work-dir", + workDir, + "--work-dir-retention", + "always", }, &stdout, &stderr) if exitCode == 0 { t.Fatalf("expected nonzero exit code") @@ -308,6 +328,15 @@ func TestRunProcessReportJSONBestEffortOnFailure(t *testing.T) { if !strings.Contains(report.ErrorMessage, "not valid JSON") { t.Fatalf("expected parse error message, got %q", report.ErrorMessage) } + if report.Diagnostics == nil { + t.Fatalf("expected diagnostics metadata in failed report") + } + if report.Diagnostics.DirectoryPath == "" || report.Diagnostics.ErrorLogPath == "" { + t.Fatalf("expected diagnostics directory and error log references in failed report: %+v", report.Diagnostics) + } + if _, err := os.Stat(report.Diagnostics.ErrorLogPath); err != nil { + t.Fatalf("expected error log file at reported path: %v", err) + } } func TestRunProcessReportJSONDoesNotLeakAPIKeys(t *testing.T) { @@ -338,6 +367,42 @@ func TestRunProcessReportJSONDoesNotLeakAPIKeys(t *testing.T) { } } +func TestRunProcessReportJSONAndRunDirReportShareDiagnosticsMetadata(t *testing.T) { + var stdout bytes.Buffer + var stderr bytes.Buffer + + workDir := t.TempDir() + reportPath := filepath.Join(t.TempDir(), "report.json") + + exitCode := Run([]string{ + "process", + fixturePath("tiny_transcript.json"), + "--glossary", + fixturePath("tiny_glossary.yaml"), + "--report-json", + reportPath, + "--work-dir", + workDir, + "--work-dir-retention", + "always", + }, &stdout, &stderr) + if exitCode != 0 { + t.Fatalf("expected exit code 0, got %d with stderr %q", exitCode, stderr.String()) + } + + externalReport := readProcessReport(t, reportPath) + runPath := onlyRunDir(t, workDir) + runDirReport := readProcessReport(t, filepath.Join(runPath, "report.json")) + + if externalReport.Diagnostics == nil || runDirReport.Diagnostics == nil { + t.Fatalf("expected diagnostics metadata in both reports") + } + if *externalReport.Diagnostics != *runDirReport.Diagnostics { + t.Fatalf("expected same diagnostics metadata in --report-json and run-dir report\nexternal=%+v\nrun-dir=%+v", + *externalReport.Diagnostics, *runDirReport.Diagnostics) + } +} + func TestRunProcessWritesNormalizationDiagnosticsArtifacts(t *testing.T) { var stdout bytes.Buffer var stderr bytes.Buffer diff --git a/internal/core/reporting/report.go b/internal/core/reporting/report.go index 83dfc6d..5e0a712 100644 --- a/internal/core/reporting/report.go +++ b/internal/core/reporting/report.go @@ -25,6 +25,19 @@ type ProcessReport struct { NormalizationIDReassignments *int `json:"normalization_id_reassignments,omitempty"` NormalizationSkipped NormalizationSkipped `json:"normalization_skipped,omitempty"` Chunking *ChunkingSummary `json:"chunking,omitempty"` + Diagnostics *DiagnosticsMetadata `json:"diagnostics,omitempty"` +} + +type DiagnosticsMetadata struct { + DirectoryPath string `json:"directory_path,omitempty"` + SourceTranscriptPath string `json:"source_transcript_path,omitempty"` + ParsedSourceTranscriptPath string `json:"parsed_source_transcript_path,omitempty"` + NormalizedTranscriptPath string `json:"normalized_transcript_path,omitempty"` + NormalizationSummaryPath string `json:"normalization_summary_path,omitempty"` + ChunkingSummaryPath string `json:"chunking_summary_path,omitempty"` + InvocationMetadataPath string `json:"invocation_metadata_path,omitempty"` + RedactedEffectiveConfigPath string `json:"redacted_effective_config_path,omitempty"` + ErrorLogPath string `json:"error_log_path,omitempty"` } type NormalizationSkipped struct {