textmachine/backend/cmd/tmctl/render.go

490 lines
23 KiB
Go
Raw 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 main
import (
"encoding/json"
"fmt"
"io"
"sort"
"strings"
"unicode/utf8"
"textmachine/backend/internal/pipeline"
"textmachine/backend/internal/store"
)
// render.go: the human/JSON output renderers of the commands, extracted from main.go into
// pure functions over the pipeline/store structs (package #4). Every output byte is a
// FROZEN contract (stable enums in --json — D12; the human tables are an audit
// surface D20.4): fmt.Printf was replaced with fmt.Fprintf(w, …) with
// w=os.Stdout — byte-for-byte; every deliberate change is listed in
// PROGRESS (one in this package: report carries ch/chunk/model_requested/err —
// the post-mortem of a failed call used to be an "empty string", the pain of a smoke run).
// The ledger callbacks preserve the EXACT order "print → read SpentUSD → print"
// of the original code: hoisting the read above the render would change the partial output on error.
// renderTranslate prints the per-chunk translation report and returns the
// CompletedWithFlags sentinel when chunks were flagged (exit 2).
func renderTranslate(w io.Writer, res *pipeline.BookResult, ledger func() (committed, reserved float64, err error)) error {
for _, ch := range res.Chunks {
fmt.Fprintf(w, "=== CHAPTER %d CHUNK %d — %s%s ===\n", ch.Chapter, ch.ChunkIdx, ch.Disposition, flagSuffix(ch.FlagReason))
switch {
case ch.Disposition == pipeline.DispOK:
fmt.Fprintln(w, ch.FinalText)
case ch.FinalText != "":
// Cosmetic sanitizer strip (D35.4a): the leak was removed and the remainder exported,
// but the chunk stays flagged for a human to verify the auto-clean — not lost to an
// empty placeholder (ch5/ch20 chapter openers used to drop whole for a leading «###»).
fmt.Fprintf(w, "[FLAG %s — leak cleaned, exported cleaned, verify] ↓\n", ch.FlagReason)
fmt.Fprintln(w, ch.FinalText)
default:
fmt.Fprintf(w, "[FLAG %s] chunk not translated — draft/edit unusable, flagged for a human\n", ch.FlagReason)
}
for _, st := range ch.Stages {
how := "call"
switch {
case st.Disposition == pipeline.DispSkipped:
how = "skipped"
case st.FromResume:
how = "resume"
}
fmt.Fprintf(w, " %-8s %-22s %-8s %-8s%s $%.6f (cum $%.6f) in=%d (cached=%d) out=%d+%d att=%d %dms finish=%s\n",
st.Stage, st.Model, how, st.Disposition, flagSuffix(st.FlagReason),
st.CostUSD, st.CumCostUSD,
st.Usage.PromptTokens, st.Usage.CachedTokens,
st.Usage.CompletionTokens, st.Usage.ReasoningTokens, st.Attempts, st.LatencyMS, st.FinishReason)
}
fmt.Fprintln(w)
}
fmt.Fprintf(w, "TOTAL (this run): $%.6f — chunks %d, flags %d\n", res.TotalUSD, len(res.Chunks), res.Flagged)
committed, reserved, err := ledger()
if err != nil {
return err
}
fmt.Fprintf(w, "Book ledger: committed=$%.6f reserved=$%.6f\n", committed, reserved)
// Exit code 2 is a typed sentinel, not an infra error: the report above is
// already on stdout; main() maps this to a non-zero exit for the operator.
if res.Flagged > 0 {
return &pipeline.CompletedWithFlags{Flagged: res.Flagged, Total: len(res.Chunks)}
}
return nil
}
// renderSignatureStop prints the operator-facing bank-mining signature-stop banner (R1-FL-A): the run
// paused before the edit wave because the miner proposed N new terms for owner sign. It surfaces the term
// count + the signature-map path + the resume path (promote a term into the mined-delta file OR decline it
// in the mined-rejects file, then re-run). Distinct from a flag/crash — main() maps the sentinel to exit 3.
func renderSignatureStop(w io.Writer, s *pipeline.WaveSignatureStop) {
fmt.Fprintf(w, "=== BANK-MINING: STOP FOR SIGNATURE (%d new term(s)) ===\n", s.Terms)
fmt.Fprintf(w, "The run STOPPED before the edit wave: the miner proposed %d new term(s) for the owner to sign.\n", s.Terms)
fmt.Fprintf(w, "Signature map: %s\n", s.SignaturePath)
fmt.Fprintln(w, "Next: for EACH term either promote it into the mined-delta file (approved + dst), OR decline it in")
fmt.Fprintln(w, "the mined-rejects file (book.yaml: mined_rejects), then re-run `tmctl translate`. The stop clears")
fmt.Fprintln(w, "once every proposed term is promoted or rejected (then the delta is empty → auto-continue to the edit wave).")
}
// flagSuffix renders a non-empty flag reason as "(reason)".
func flagSuffix(r pipeline.FlagReason) string {
if r == "" {
return ""
}
return "(" + string(r) + ")"
}
// renderReport prints the request_log table, the flag/memory/style sections and
// the ledger line. Deliberate package-#4 change: the ch/chunk columns, the model with
// a fallback to the REQUESTED one (a failed call has an empty model_actual — the row was
// anonymous) and the degraded/err tail — the DB has stored these columns since Milestone 2, the printer
// dropped them, and the post-mortem needed digging into sqlite3.
//
// All three store reads are CALLBACKS, like ledger: the historical report read and
// printed interleaved, and a read failure mid-audit left the already-printed part on stdout;
// hoisting the reads above the render would silently change that partial
// output to empty (a self-review finding — the same principle documented
// below for the ledger line).
func renderReport(w io.Writer,
fetchRows func() ([]store.RequestLogView, error),
fetchFlags func() ([]store.ChunkStatus, error),
fetchStates func() ([]store.RetrievalState, error),
ledger func() (committed, reserved float64, err error)) error {
rows, err := fetchRows()
if err != nil {
return err
}
fmt.Fprintf(w, "%-20s %-4s %-5s %-8s %-12s %-22s %8s %8s %8s %8s %8s %10s %8s %-8s %-5s %-3s %s\n",
"ts", "ch", "chunk", "stage", "role", "model", "prompt", "cached", "cwrite", "compl", "reason", "cost_usd", "ms", "finish", "tmhit", "ok", "err")
for _, row := range rows {
model := row.ModelActual
if model == "" && row.ModelRequested != "" {
model = row.ModelRequested + "(req)"
}
fmt.Fprintf(w, "%-20s %-4d %-5d %-8s %-12s %-22s %8d %8d %8d %8d %8d %10.6f %8d %-8s %-5d %-3d %s\n",
row.TS, row.Chapter, row.ChunkIdx, row.Stage, row.Role, model, row.PromptTokens, row.CachedTokens,
row.CacheCreationTokens, row.CompletionTokens, row.ReasoningTokens, row.CostUSD,
row.LatencyMS, row.FinishReason, row.TMHit, row.OK, errTail(row.Degraded, row.Err))
}
// Estimated-spend legend (pack-13 point-9, research/21 §1.10): rows whose cost_usd is a reservation
// ESTIMATE (a billed decode failure, or a paid 2xx with zero usage), not a provider-reported cost.
// Surfacing the share keeps an estimate from looking like a real zero-token call; est_tokens is the
// display-only fertility output estimate. Prints only when there is at least one such row.
var estRows, estTokens int
var estUSD float64
for _, row := range rows {
if row.Estimated == 1 {
estRows++
estUSD += row.CostUSD
estTokens += row.EstTokens
}
}
if estRows > 0 {
fmt.Fprintf(w, "estimated-cost rows: %d ($%.6f settled at the reservation estimate, not provider-reported; ~%d est. output tokens via fertility, display-only)\n", estRows, estUSD, estTokens)
}
// Flag section (Milestone 2): every chunk×stage whose disposition ≠ ok — the
// "flag for the editor" the plan requires (02-mvp Phase-1 acceptance allows N).
flags, err := fetchFlags()
if err != nil {
return err
}
headerPrinted := false
for _, f := range flags {
if f.Disposition == "ok" {
continue
}
if !headerPrinted {
fmt.Fprintf(w, "\n=== FLAGS (disposition ≠ ok) ===\n")
fmt.Fprintf(w, "%-4s %-6s %-8s %-9s %-16s %5s %10s %s\n",
"ch", "chunk", "stage", "disp", "reason", "att", "cost_usd", "detail")
headerPrinted = true
}
fmt.Fprintf(w, "%-4d %-6d %-8s %-9s %-16s %5d %10.6f %s\n",
f.Chapter, f.ChunkIdx, f.Stage, f.Disposition, f.FlagReason, f.Attempts, f.CostUSD, f.Detail)
}
// Memory section (step 4): the per-chunk retrieval-state, surfaced so silent glossary
// degradation is LOUD. The aggregate line always prints when a glossary is in use;
// each chunk with a post-check miss is listed (the flagger-mode signal — the model
// ignored an approved term, or we injected the wrong dst) with the offending src→dst.
states, err := fetchStates()
if err != nil {
return err
}
if len(states) > 0 {
var injected, sticky, ambiguous, spoiler, evicted, misses, style int
for _, rs := range states {
injected += rs.NExactHits
sticky += rs.NSticky
ambiguous += rs.NAmbiguousFlagged
spoiler += rs.NSpoilerBlocked
evicted += rs.NEvicted
misses += rs.NPostcheckMiss
style += rs.NStyleFlags
}
fmt.Fprintf(w, "\n=== MEMORY (retrieval-state) ===\n")
fmt.Fprintf(w, "injections(exact)=%d sticky=%d ambiguous=%d spoiler-blocked=%d evicted=%d post-check-misses=%d\n",
injected, sticky, ambiguous, spoiler, evicted, misses)
printed := false
for _, rs := range states {
if rs.NPostcheckMiss == 0 {
continue
}
if !printed {
fmt.Fprintf(w, "%-4s %-6s %6s %s\n", "ch", "chunk", "misses", "detail (src→dst not found in output)")
printed = true
}
fmt.Fprintf(w, "%-4d %-6d %6d %s\n", rs.Chapter, rs.ChunkIdx, rs.NPostcheckMiss, rs.PostcheckDetail)
}
// Cheap style/number flaggers (observability, not gates): total + per-chunk detail.
fmt.Fprintf(w, "\n=== STYLE GATES (observability: dialogue-dashes, ё, translit-interjections, 万/億 magnitudes, reflow-regression) — total %d ===\n", style)
stylePrinted := false
for _, rs := range states {
if rs.NStyleFlags == 0 {
continue
}
if !stylePrinted {
fmt.Fprintf(w, "%-4s %-6s %6s %s\n", "ch", "chunk", "flags", "detail")
stylePrinted = true
}
fmt.Fprintf(w, "%-4d %-6d %6d %s\n", rs.Chapter, rs.ChunkIdx, rs.NStyleFlags, rs.StyleDetail)
}
}
committed, reserved, err := ledger()
if err != nil {
return err
}
fmt.Fprintf(w, "\nBook ledger: committed=$%.6f reserved=$%.6f\n", committed, reserved)
return nil
}
// errTail collapses the degraded/err columns into one bounded table tail (empty
// when the call was clean). Truncation is on a rune boundary: err carries raw chunks
// of provider bodies (CJK/Cyrillic), a byte slice would print broken UTF-8
// (a self-review finding).
func errTail(degraded, errText string) string {
s := degraded
if errText != "" {
if s != "" {
s += " "
}
s += errText
}
if len(s) > 120 {
cut := 120
for cut > 0 && !utf8.RuneStart(s[cut]) {
cut--
}
s = s[:cut] + "…"
}
return s
}
// renderQuality prints the deterministic per-run quality-report (D39 layer 5): the claim-1 structural
// KPI (mean sentences per narrative paragraph) plus the aggregated deterministic signals
// (dialogue-dash, glossary consistency, number drift, CJK-leak / echo rates, trust-gated). Pure
// observability — a summary section, never an exit code. Kept SEPARATE from renderReport so that
// function's frozen callback contract (render_test.go) is untouched.
func renderQuality(w io.Writer, q *pipeline.QualityReport) error {
fmt.Fprintf(w, "\n=== QUALITY (per-run, deterministic signals — observability, not a gate) ===\n")
fmt.Fprintf(w, "total units=%d · reached final=%d · with export text=%d\n", q.TotalUnits, q.ProcessedUnits, q.TextUnits)
// Claim-1: choppy paragraphs. ≈1.0 sentences/paragraph = choppy (the owner's complaint); higher = merged prose.
fmt.Fprintf(w, "STRUCTURE (claim-1 «choppy paragraphs»): sentences/narrative-paragraph=%.2f (sentences=%d / narrative-paragraphs=%d)\n",
q.MeanSentPerNarrPara, q.NarrativeSentences, q.NarrativeParagraphs)
fmt.Fprintf(w, "SIGNALS: dialogue-dash=%d · glossary-misses=%d · number-drift=%d · trust-gated(seed)=%d · degenerate-loops=%d\n",
q.DialogueDashFlags, q.GlossaryMisses, q.NumberDriftFlags, q.TrustGated, q.DegenerateLoopRuns)
// Echo split (D39.18): draft = the translator echoed (incl. dropped c-lite members) / edit = the editor
// echoed in what was delivered. Cosmetic-strip is per unit (the sanitizer runs on the final stage).
fmt.Fprintf(w, "STRIPS/ECHO: cosmetic-strip units=%d (%.1f%%, markdown+CJK) · echo draft=%d (%.1f%%) · echo edit=%d (%.1f%%)\n",
q.CosmeticStripUnits, 100*q.CosmeticStripRate, q.EchoDraftChunks, 100*q.EchoDraftRate, q.EchoEditUnits, 100*q.EchoEditRate)
// Addressable-defect RESIDUAL (pack-16): how many defects a repair loop could actually attack in what
// this run shipped — the $0 measurement that decides whether enabling the paid loop is worth it. The
// line is printed only when something fired, so a clean book's report is unchanged.
// Money-side label provenance: hop COUNT (the per-unit boolean cannot count them) and the per-model
// spend split. Printed only when there is something to say, so a clean report is unchanged.
if q.EscalationHops > 0 || len(q.SpendByModel) > 0 || len(q.ContentLabels) > 0 {
parts := make([]string, 0, len(q.SpendByModel))
for _, m := range sortedFloatKeys(q.SpendByModel) {
parts = append(parts, fmt.Sprintf("%s=$%.6f", m, q.SpendByModel[m]))
}
line := fmt.Sprintf("ROUTING/MONEY: escalation hops=%d · spend by model: %s", q.EscalationHops, strings.Join(parts, " · "))
if len(q.ContentLabels) > 0 {
line += fmt.Sprintf(" · labels=%s · routing=%s", strings.Join(q.ContentLabels, ","), strings.Join(q.Routing, " · "))
}
fmt.Fprintln(w, line)
}
if q.RepairCandidates > 0 {
parts := make([]string, 0, len(q.RepairCandidatesByClass))
for _, cls := range sortedKeys(q.RepairCandidatesByClass) {
parts = append(parts, fmt.Sprintf("%s=%d", cls, q.RepairCandidatesByClass[cls]))
}
fmt.Fprintf(w, "REPAIR RESIDUAL: addressable defects=%d (%s) — deterministic candidates in the shipped text\n",
q.RepairCandidates, strings.Join(parts, " · "))
}
if q.RepairCalls > 0 {
fmt.Fprintf(w, "REPAIR LOOP: calls=%d · applied=%d · declined=%d (our flag was wrong) · rejected=%d (guard/re-gate refused)\n",
q.RepairCalls, q.RepairApplied, q.RepairDeclined, q.RepairRejected)
}
return nil
}
// sortedKeys renders a map's keys in a stable order (a report must not depend on map iteration).
// sortedFloatKeys is sortedKeys for a money map — one helper per value type keeps the render
// deterministic without reaching for generics (least mechanism; both maps are tiny).
func sortedFloatKeys(m map[string]float64) []string {
out := make([]string, 0, len(m))
for k := range m {
out = append(out, k)
}
sort.Strings(out)
return out
}
func sortedKeys(m map[string]int) []string {
out := make([]string, 0, len(m))
for k := range m {
out = append(out, k)
}
sort.Strings(out)
return out
}
// renderExport prints the read-only export projection (D39 layer 6 / D39.2-open №1): every chunk's
// FINAL export text EXACTLY as `tmctl translate` ships it (export-normalised), for the polygon
// extractor / reader-samples to read the same bytes the backend does instead of the raw checkpoint.
// The DEFAULT is stable JSON (the machine surface the extractor consumes); --plaintext emits the human
// concatenation (a per-chunk header banner + the text, flagged chunks marked). Pure $0 read — never an
// exit-2 sentinel (unlike translate/status): export is an audit projection, not a run verdict.
func renderExport(w io.Writer, exp *pipeline.BookExport, asPlaintext bool) error {
if asPlaintext {
// Manifest/drift summary first (F3/F4): a partial or drifted book is EXPLICIT, not silently
// exported as complete.
fmt.Fprintf(w, "# export %s — total units=%d, exported=%d, pending=%d",
exp.BookID, exp.TotalUnits, exp.TotalUnits-exp.PendingUnits, exp.PendingUnits)
if exp.GhostRows > 0 {
fmt.Fprintf(w, ", ghost-rows-dropped=%d", exp.GhostRows)
}
if exp.ConfigDrift {
fmt.Fprintf(w, " ⚠ CONFIG-DRIFT (the current config renders a different snapshot — a gate/stage may have changed since the run; %.12s)", exp.CurrentSnapshot)
}
fmt.Fprintln(w)
for _, ce := range exp.Chunks {
switch {
case ce.Disposition == "pending":
fmt.Fprintf(w, "=== CHAPTER %d CHUNK %d — pending (not yet translated) ===\n", ce.Chapter, ce.ChunkIdx)
case ce.Disposition == string(pipeline.DispOK):
fmt.Fprintf(w, "=== CHAPTER %d CHUNK %d ===\n", ce.Chapter, ce.ChunkIdx)
case ce.FinalText != "":
// Cosmetic sanitizer strip (D35.4a): auto-cleaned remainder, flagged for a human.
fmt.Fprintf(w, "=== CHAPTER %d CHUNK %d — %s%s (leak cleaned, verify) ===\n",
ce.Chapter, ce.ChunkIdx, ce.Disposition, flagParen(ce.FlagReason))
default:
// Substantive flag / upstream skip: no export text (D2 — contaminated output never ships).
fmt.Fprintf(w, "=== CHAPTER %d CHUNK %d — %s%s (not translated, flagged for a human) ===\n",
ce.Chapter, ce.ChunkIdx, ce.Disposition, flagParen(ce.FlagReason))
}
fmt.Fprintln(w, ce.FinalText)
}
return nil
}
enc := json.NewEncoder(w)
enc.SetIndent("", " ")
return enc.Encode(exp)
}
// flagParen renders a non-empty flag reason as " (reason)" for the plaintext export banner.
func flagParen(r string) string {
if r == "" {
return ""
}
return " (" + r + ")"
}
// renderStatusJSON emits the StatusReport as indented JSON (stable enums — the
// ratified CI/IDE contract, D12/D15.3) and maps flagged>0 to the exit-2 sentinel.
func renderStatusJSON(w io.Writer, rep *pipeline.StatusReport) error {
enc := json.NewEncoder(w)
enc.SetIndent("", " ")
if err := enc.Encode(rep); err != nil {
return err
}
// Symmetric with the human mode (minor 1d): flagged>0 is exit 2 (completed-with-flags), not
// exit 0. The full JSON is already on stdout; the sentinel only sets the shell disposition
// (its message goes to stderr in main), so a CI/IDE consumer both parses the projection AND
// sees "attention needed" in the exit code instead of silently reading 0.
if rep.Flagged > 0 {
return &pipeline.CompletedWithFlags{Flagged: rep.Flagged, Total: rep.TotalUnits}
}
return nil
}
// renderStatusHuman prints the operator dashboard (unit counts, per-chapter
// passports, money, secondary ETA) and maps flagged>0 to the exit-2 sentinel.
// cfgPath goes into the "what to do" hints (redrive/translate command lines).
func renderStatusHuman(w io.Writer, rep *pipeline.StatusReport, cfgPath string) error {
snap := rep.Snapshot
if snap == "" {
snap = "—"
} else if len(snap) > 12 {
snap = snap[:12]
}
drift := ""
if rep.SnapshotDrift {
drift += " ⚠ SNAPSHOT-DRIFT (several snapshots in chunk_status — the config changed; --resnapshot)"
}
if rep.ConfigDrift {
drift += " ⚠ CONFIG-DRIFT (the current config renders a different snapshot — an edit since the run; translate will require --resnapshot = re-paying for the book)"
}
fmt.Fprintf(w, "=== STATUS: %s (snapshot %s)%s ===\n", rep.BookID, snap, drift)
fmt.Fprintf(w, "Progress: %d/%d units (%.1f%%) — done=%d in_progress=%d flagged=%d pending=%d\n",
rep.Done, rep.TotalUnits, rep.PercentDone, rep.Done, rep.InProgress, rep.Flagged, rep.Pending)
fmt.Fprintf(w, "Escalations: %d · post-check misses (confirmed): %d · style-flags (observability): %d\n",
rep.Escalations, rep.PostcheckMisses, rep.StyleFlags)
ceil := ""
if rep.BookCeilingUSD > 0 {
ceil = fmt.Sprintf(" (ceiling $%.2f — %.1f%%)", rep.BookCeilingUSD, rep.CeilingPct)
}
fmt.Fprintf(w, "Money: committed=$%.6f reserved=$%.6f%s · book forecast ~$%.6f\n",
rep.CommittedUSD, rep.ReservedUSD, ceil, rep.ProjectedBookUSD)
if rep.ETASeconds > 0 {
fmt.Fprintf(w, "ETA: ~%.0fs (mean throughput of fresh calls, NOT EWMA — D12-deviation; secondary)\n", rep.ETASeconds)
}
// Content-label provenance (B6): printed only for a labelled book, so an ordinary run's output is
// unchanged. It answers "which endpoint received this book" — the routing decision, not a hash input.
if len(rep.ContentLabels) > 0 {
fmt.Fprintf(w, "CONTENT LABELS: %s · routing: %s\n", strings.Join(rep.ContentLabels, ", "), strings.Join(rep.Routing, " · "))
for _, p := range rep.ContentRoutingProblems {
fmt.Fprintf(w, " ⚠ ROUTING NOT RUNNABLE: %s\n", p)
}
}
if len(rep.Chapters) > 0 {
// style = cheap deterministic style/number flags (observability, not a disposition) — a per-
// chapter glance count so a human sees where the linters fired without opening `report` (D20.4).
fmt.Fprintf(w, "\n%-5s %-9s %-10s %-6s %-4s %-6s %-16s %10s\n", "ch", "done/all", "verdict", "flag", "esc", "style", "worst_flag", "cost_usd")
for _, p := range rep.Chapters {
fmt.Fprintf(w, "%-5d %d/%-7d %-10s %-6d %-4d %-6d %-16s %10.6f\n",
p.Chapter, p.UnitsDone, p.UnitsTotal, p.Verdict, p.UnitsFlagged, p.Escalations, p.StyleFlags,
dashIfEmpty(p.WorstFlagReason), p.CostUSD)
}
}
// flagged≠failed (jobs.status.failed = infra-only, D12): flagged chunks are a "clean run,
// attention needed" signal. Two kinds need DIFFERENT actions (minor 1d): a chunk_status flag
// (soft_refusal, length, …) is re-drivable with `tmctl redrive`; a gate-promoted glossary_miss
// is NOT (redrive is a no-op — the miss re-derives from the unchanged glossary), so it needs a
// seed fix + `translate --resnapshot`. Advise each set separately instead of one blanket hint.
if rep.Flagged > 0 {
if redrivable := rep.Flagged - rep.GlossaryMissFlagged; redrivable > 0 {
fmt.Fprintf(w, "\n%d flagged chunk(s) need attention — re-attack: tmctl redrive --config %s [--chapter N --chunk M --reason R]\n",
redrivable, cfgPath)
}
if rep.GlossaryMissFlagged > 0 {
fmt.Fprintf(w, "\n%d chunk(s) flagged by the post-check gate (glossary_miss) — redrive is a no-op for them: fix the seed glossary (term/dst) and re-run `tmctl translate --config %s --resnapshot` (re-pays the affected chunks)\n",
rep.GlossaryMissFlagged, cfgPath)
}
return &pipeline.CompletedWithFlags{Flagged: rep.Flagged, Total: rep.TotalUnits}
}
return nil
}
// dashIfEmpty renders "" as "—" for a table cell.
func dashIfEmpty(s string) string {
if s == "" {
return "—"
}
return s
}
// renderRedrive prints the redrive plan and (after a live re-run) the re-run
// totals. ledger may return an error — it is DELIBERATELY swallowed without the ledger
// line (original behaviour: flaky reads must not change the redrive exit code).
func renderRedrive(w io.Writer, bookID string, summary *pipeline.RedriveSummary, res *pipeline.BookResult, ledger func() (committed, reserved float64, err error)) error {
fmt.Fprintf(w, "=== REDRIVE: %s ===\n", bookID)
if len(summary.Targets) == 0 {
fmt.Fprintln(w, "No flagged chunks match the selector — nothing to re-attack.")
return nil
}
fmt.Fprintf(w, "Targets: %d flagged chunk(s) — reset the terminal flag + a fresh retry/escalation budget (DispOK untouched):\n", len(summary.Targets))
for _, t := range summary.Targets {
fmt.Fprintf(w, " ch%d/chunk%d (%s) → stages: %s\n", t.Chapter, t.ChunkIdx, t.FlagReason, strings.Join(t.Stages, ", "))
}
if summary.DryRun {
fmt.Fprintln(w, "[dry-run: nothing reset, no calls made]")
return nil
}
fmt.Fprintln(w, "[reset; re-running the durable loop — DispOK chunks resume at $0, reset ones re-attack]")
fmt.Fprintln(w)
if res != nil {
fmt.Fprintf(w, "TOTAL re-run (this run): $%.6f — chunks %d, flags %d\n", res.TotalUSD, len(res.Chunks), res.Flagged)
committed, reserved, serr := ledger()
if serr == nil {
fmt.Fprintf(w, "Book ledger: committed=$%.6f reserved=$%.6f\n", committed, reserved)
}
if res.Flagged > 0 {
return &pipeline.CompletedWithFlags{Flagged: res.Flagged, Total: len(res.Chunks)}
}
}
return nil
}