Files
audita/internal/core/reporting/markdown.go
Eric Rakestraw 2d41748006
Some checks failed
ci/woodpecker/tag/release Pipeline failed
Use the Go v3 module path for Audita
2026-10-11 01:40:38 +00:00

181 lines
5.7 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package reporting
import (
"fmt"
"sort"
"strings"
"gitea.maximumdirect.net/eric/audita/v3/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", "<br>")
}