package reporting import ( "fmt" "sort" "strings" "gitea.maximumdirect.net/eric/audita/internal/core/redaction" ) // CompletionLine is the brief successful CLI summary. It uses the same counts // as both YAML documents and remains independent of file-output selection. func CompletionLine(snapshot Snapshot) string { return fmt.Sprintf("Completed: %d corrections applied; %d work units with errors; %d recoveries.", count(snapshot.Summary.CorrectionsApplied), count(snapshot.Summary.WorkUnitsWithErrors), count(snapshot.Summary.Recoveries)) } func count(value *int) int { if value == nil { return 0 } return *value } // Markdown renders a fixed-layout human report from one processing snapshot. func Markdown(snapshot Snapshot, secrets ...string) []byte { r := redaction.New(secrets) safe := func(value string) string { return escapeMarkdown(r.String(value)) } var b strings.Builder fmt.Fprintf(&b, "# Audita report: %s\n\n", safe(snapshot.RunID)) fmt.Fprintf(&b, "Status: %s\n\n", safe(snapshot.Status)) fmt.Fprintf(&b, "Corrections applied: %d \n", count(snapshot.Summary.CorrectionsApplied)) fmt.Fprintf(&b, "Work units: %s total; %s completed; %s interrupted; %s not started \n", showCount(snapshot.Summary.WorkUnitsTotal), showCount(snapshot.Summary.WorkUnitsCompleted), showCount(snapshot.Summary.WorkUnitsInterrupted), showCount(snapshot.Summary.WorkUnitsNotStarted)) fmt.Fprintf(&b, "Errors: %s work units; %s run errors \n", showCount(snapshot.Summary.WorkUnitsWithErrors), showCount(snapshot.Summary.RunErrors)) fmt.Fprintf(&b, "Recoveries: %s of %s model operations \n", showCount(snapshot.Summary.Recoveries), showCount(snapshot.Summary.LLMOperations)) fmt.Fprintf(&b, "Proposal responses: %s; with filtering: %s\n\n", showCount(snapshot.Summary.ProposalResponses), showCount(snapshot.Summary.ProposalResponsesWithFiltering)) fmt.Fprintf(&b, "Transcript input: %s \n", safe(snapshot.TranscriptPath)) fmt.Fprintf(&b, "Glossary input: %s \n", safe(snapshot.GlossaryPath)) output := snapshot.OutputPath if output == "" { output = "stdout" } fmt.Fprintf(&b, "Transcript output: %s\n\n", safe(output)) fmt.Fprintln(&b, "## Work-unit errors") fmt.Fprintln(&b) writeMatrix(&b, snapshot, safe, false) fmt.Fprintln(&b) fmt.Fprintln(&b, "## Recoveries") fmt.Fprintln(&b) writeMatrix(&b, snapshot, safe, true) fmt.Fprintln(&b) fmt.Fprintln(&b, "Legend: `—` = not started; `n*` = interrupted with n recorded errors or recoveries. Completed error cells are 0 or 1.") fmt.Fprintln(&b) writeReasons(&b, "Error reasons", errorReasons(snapshot.Errors), safe) writeReasons(&b, "Recovery reasons", recoveryReasons(snapshot.Recoveries), safe) fmt.Fprintln(&b, "A recovery can have multiple reasons; reason counts need not sum to the recovery total.") fmt.Fprintln(&b) fmt.Fprintln(&b, "Details: `errors.yml` and `recoveries.yml`.") return []byte(b.String()) } func showCount(value *int) string { if value == nil { return "unknown" } return fmt.Sprint(*value) } func writeMatrix(b *strings.Builder, snapshot Snapshot, safe func(string) string, recovery bool) { if len(snapshot.StageNames) == 0 || len(snapshot.WorkUnits) == 0 { fmt.Fprintln(b, "No work units.") return } columns := snapshot.StageNames fmt.Fprint(b, "| Chunk |") for _, name := range columns { fmt.Fprintf(b, " %s |", safe(name)) } fmt.Fprint(b, "\n| --- |") for range columns { fmt.Fprint(b, " ---: |") } fmt.Fprintln(b) byChunk := make(map[int]map[int]WorkUnit) chunkMeta := make(map[int]WorkUnit) keys := make([]int, 0) for _, unit := range snapshot.WorkUnits { if byChunk[unit.ChunkIndex] == nil { byChunk[unit.ChunkIndex] = make(map[int]WorkUnit) keys = append(keys, unit.ChunkIndex) } byChunk[unit.ChunkIndex][unit.StageIndex] = unit chunkMeta[unit.ChunkIndex] = unit } sort.Ints(keys) for _, chunk := range keys { meta := chunkMeta[chunk] fmt.Fprintf(b, "| Chunk %d (segments %d–%d) |", chunk+1, meta.StartSegmentID, meta.EndSegmentID) for stage := range columns { unit, exists := byChunk[chunk][stage] cell := "—" if exists && unit.State != "not_started" { n := unit.ErrorCount if recovery { n = unit.RecoveryCount } cell = fmt.Sprint(n) if unit.State == "interrupted" { cell += "*" } } fmt.Fprintf(b, " %s |", cell) } fmt.Fprintln(b) } } func writeReasons(b *strings.Builder, title string, counts map[string]int, safe func(string) string) { fmt.Fprintf(b, "### %s\n\n", title) if len(counts) == 0 { fmt.Fprintln(b, "None.") fmt.Fprintln(b) return } keys := make([]string, 0, len(counts)) for key := range counts { keys = append(keys, key) } sort.Strings(keys) for _, key := range keys { fmt.Fprintf(b, "- `%s`: %d\n", safe(key), counts[key]) } fmt.Fprintln(b) } func errorReasons(records []ErrorRecord) map[string]int { counts := make(map[string]int) for _, record := range records { seen := make(map[string]bool) if record.Scope == "run" { seen[record.Reason] = true } for _, failure := range record.Failures { seen[failure.Reason] = true } for reason := range seen { if reason != "" { counts[reason]++ } } } return counts } func recoveryReasons(records []RecoveryRecord) map[string]int { counts := make(map[string]int) for _, record := range records { seen := make(map[string]bool) for _, reason := range record.Reasons { seen[reason] = true } for reason := range seen { counts[reason]++ } } return counts } func escapeMarkdown(value string) string { value = strings.ReplaceAll(value, "\\", "\\\\") value = strings.ReplaceAll(value, "|", "\\|") value = strings.ReplaceAll(value, "`", "\\`") value = strings.ReplaceAll(value, "\r\n", "\n") value = strings.ReplaceAll(value, "\r", "\n") return strings.ReplaceAll(value, "\n", "
") }