2026-09-24 17:34:03 +00:00
|
|
|
|
package reporting
|
|
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
|
"fmt"
|
|
|
|
|
|
"sort"
|
|
|
|
|
|
"strings"
|
|
|
|
|
|
|
2026-10-11 01:40:38 +00:00
|
|
|
|
"gitea.maximumdirect.net/eric/audita/v3/internal/core/redaction"
|
2026-09-24 17:34:03 +00:00
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
// 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>")
|
|
|
|
|
|
}
|