textmachine/backend/cmd/tmctl/render.go

1020 lines
58 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"
"strconv"
"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.
// gapMarker is the IN-TEXT marker for a unit that ships text with a piece MISSING — the c-lite
// member drop, where the editor edited the unit's clean members and left a flagged member's text
// out entirely. Without it the reader gets a seamless concatenation with a chunk-sized hole in it
// and no way to know: the hole has no seam, because the editor rewrote the remainder around it.
//
// WHY IT LIVES IN THE RENDERER and not in the export projection, where the deterministic chapter
// title is applied. The title is part of the BOOK and must reach the platform; this marker is
// metadata ABOUT the text, and the wire decides `translated` vs `withheld` by asking whether the
// unit's text is EMPTY (runevents UnitDone.Shipped ← oc.FinalText != ""). A marker written into
// that text would turn every withheld unit into a translated one and break the very distinction
// the contract promises. So it is applied one layer further out, where nothing but a human reads.
//
// WHY IT IS NOT TARGET-LANGUAGE TEXT. Every banner on this surface is English operator vocabulary
// («leak cleaned, verify», «not translated, flagged for a human»), and this surface is an AUDIT
// concatenation — it interleaves per-unit banners with prose, so it is read by the operator, not
// sold to a reader. Keeping the marker in that same vocabulary makes it pair-independent by
// construction: a target language with no langpack at all gets byte-identical output, and there is
// no per-pair string to forget. A target-language marker would belong to the reader-facing artifact
// the PLATFORM builds, and that is not this file.
//
// ⚠ THE GUARD AT BOTH CALL SITES (FinalText != "") IS LOAD-BEARING. Members are counted whenever they
// drop, INCLUDING when every member of the unit drops — and then the unit ships nothing at all. Saying
// «a fragment is missing from the text below» over an empty body would be a second false statement,
// pointing at text that does not exist; the honest banner there is the one that already existed,
// «not translated, flagged for a human». The marker is for a unit that ships SOME of its text.
// ⚠ THE REASON IS THE DROP'S OWN, never the unit's FlagReason. A unit whose edit flagged for a
// cosmetic sanitizer strip AND also lost a member carries the STRIP as its FlagReason; printing that
// as the cause of the hole tells the reader a clean-up ate a chunk of the book — the same false claim
// the banner used to make, moved into the prose. The banner still shows the unit's verdict; the marker
// shows the hole. They are different facts and are now carried by different fields.
func gapMarker(dropped int, reason string) string {
frag, verb := "fragment", "is"
if dropped != 1 {
frag, verb = "fragments", "are"
}
m := fmt.Sprintf("[⚠ TEXT MISSING — %d source %s of this unit could not be translated and %s NOT in the text below",
dropped, frag, verb)
if reason != "" {
m += ": " + reason
}
return m + "]"
}
// renderTranslate prints the per-chunk translation report and returns the
// CompletedWithFlags sentinel when chunks were flagged (exit 2).
// renderStoppedRun is what a person is told after they stop a run, and it exists because until backlog
// row 389 they were told NOTHING: a stopped run returns no result, every renderer here takes one, and the
// operator was left with a single line of error text about a run that may have translated half a book.
//
// ⛔ IT REPORTS THE STORE, NOT A RESULT, and that is the whole of its honesty. The run was cut; there is
// no assembled BookResult to print and inventing one would mean describing work nobody verified. What the
// store holds is durable and true whatever the run did next — the ledger it committed, the positions that
// reached a verdict, and the ones a cut left marked for re-doing. Those are the two questions a person
// has after pressing the button: what do I have, and what will be bought again.
//
// A read that fails is SAID, not swallowed: the alternative is a confident zero, and «nothing was bought»
// is the most expensive wrong sentence this surface can print.
func renderStoppedRun(w io.Writer, bookID string, ledger func() (committed, reserved float64, err error),
rows func() ([]store.ChunkStatus, error)) {
fmt.Fprintf(w, "=== RUN STOPPED — %s ===\n", bookID)
if committed, reserved, err := ledger(); err != nil {
fmt.Fprintf(w, " the book's ledger could not be read, so what this run spent is UNKNOWN here (it is in the project database): %v\n", err)
} else {
fmt.Fprintf(w, " book ledger: committed=$%.6f reserved=$%.6f\n", committed, reserved)
}
cs, err := rows()
if err != nil {
fmt.Fprintf(w, " the stored dispositions could not be read, so how far the book got is UNKNOWN here: %v\n", err)
return
}
resolved, stopped := 0, 0
for _, row := range cs {
if pipeline.FlagReason(row.FlagReason) == pipeline.FlagCancelled {
stopped++
continue
}
resolved++
}
fmt.Fprintf(w, " positions with a verdict: %d; positions the stop cut mid-call: %d (the resume re-does those and serves the rest for $0)\n",
resolved, stopped)
fmt.Fprintln(w, " `tmctl status --json` reports the same numbers in full, and `tmctl export` ships what is finished.")
}
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.DroppedMembers > 0 && ch.FinalText != "":
// c-lite member drop: the editor shipped the CLEAN members and left a flagged member's
// text out. The text below is real and paid for, but it is INCOMPLETE, and saying
// «leak cleaned» here — which this branch used to do for every flagged-with-text unit,
// whatever the reason — told the reader the opposite of what happened.
fmt.Fprintf(w, "[FLAG %s] ↓\n", ch.FlagReason)
fmt.Fprintln(w, gapMarker(ch.DroppedMembers, string(ch.DroppedReason)))
fmt.Fprintln(w, ch.FinalText)
case ch.FlagReason == pipeline.FlagSanitizerStripped && 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)
case ch.FinalText != "":
// Flagged, with text, and neither of the two known causes. The engine produces no such
// unit today; printing a neutral banner keeps an unforeseen one from inheriting either
// of the specific claims above.
fmt.Fprintf(w, "[FLAG %s — 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)
// The stop NAMES its ceiling. A run bounded by --max-units ends with exit 0 like any other completed
// run, so without this line the operator sees "TOTAL … chunks 3" on a hundred-unit book and has no way
// to tell "the volume I bought is done" from "something went quietly wrong" — the silent-limit
// complaint D39.165 §1б took from OpenHands. It names the ceiling, what was delivered as against
// re-made, and what remains of each kind, so the next action is readable off the line itself.
if v := res.Volume; v != nil {
fmt.Fprintf(w, "VOLUME CEILING: %s\n", v)
fmt.Fprintf(w, " (this is a COMPLETION, not a pause — the run did what it was granted; the money ceiling is a separate stop with its own exit code.)\n")
// ⚠ The invitation to buy more is spoken ONLY about units that were never delivered. The first
// version said "run again to buy more" unconditionally, which on a fully-translated book with a
// moved bank offered the reader chapters they already own — re-payment presented as stock.
//
// Both remainders are reported when both exist. An if/else here would have printed the fresh half
// and silently dropped the other, leaving an operator who has BOTH kinds pending believing the
// first number was all of it.
if v.LeftFresh > 0 {
fmt.Fprintf(w, " Next: %d output unit(s) of this book have never been delivered — `--max-units` again to buy them (units, NOT chapters: a chapter can be several units; the manifest is what converts).\n", v.LeftFresh)
}
if v.LeftRework > 0 {
if v.LeftFresh > 0 {
fmt.Fprintf(w, " Also pending, and NOT a purchase: %d already-delivered unit(s) carry a superseded snapshot and would be RE-MADE. Buying them delivers no new chapter.\n", v.LeftRework)
} else {
fmt.Fprintf(w, " Next: every unit of this book HAS been delivered; the %d left carry a superseded snapshot and would be RE-MADE, not delivered. That is a re-pass, not a purchase of new book.\n", v.LeftRework)
}
}
if v.Delivered == 0 && v.Reworked > 0 {
fmt.Fprintf(w, " ⚠ This run delivered NO new unit: all %d paid unit(s) were re-made under a moved snapshot.\n", v.Reworked)
}
}
// 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 delta holds terms no earlier stop presented (D39.144 flag
// model). It surfaces the term count + the signature-map path + the resume path (decide whatever you
// wish via `tmctl bank-apply`, then re-run — the stop will not re-fire on what it just showed).
// Distinct from a flag/crash — main() maps the sentinel to exit 3.
func renderSignatureStop(w io.Writer, s *pipeline.WaveSignatureStop) {
// s.Terms is the size of the WHOLE map — new and previously-presented-but-undecided together. Under
// the flag model the map deliberately mixes the two, so the count must not be labelled «new»
// (review-workflow finding: run 2 of the flag chain stops with 2 terms of which 1 is new).
fmt.Fprintf(w, "=== BANK-MINING: STOP FOR SIGNATURE (%d term(s) in the map) ===\n", s.Terms)
fmt.Fprintf(w, "The run STOPPED before the edit wave: the mined delta holds term(s) no stop has shown you before.\n")
renderBankConsolidation(w, s.Consolidation)
renderBankStopRows(w, s.Rows)
fmt.Fprintf(w, "Signature map: %s\n", s.SignaturePath)
if s.TablePath != "" {
fmt.Fprintf(w, "Full table: %s\n", s.TablePath)
}
fmt.Fprintln(w, "Next: review the map, decide as much or as little as you wish (`tmctl bank-apply` — one act over the")
fmt.Fprintln(w, "whole bank, one term, or nothing), then re-run `tmctl translate`. The run will NOT stop again on the")
fmt.Fprintln(w, "terms above — only on new ones — and whatever you leave undecided still goes to the model, as part of the same\n"+
"law block as the terms you signed (D39.104: the bank on the wire is law for every row; an unsigned rendering\n"+
"is used consistently and corrected one level up, by a bank edit and a re-edit).")
// ⚠ These lines used to name the ⟨проверить⟩ marker, which row 134 removed from the wire (D39.104 п.2). They
// now describe what actually reaches the model: the same block, no mark. The engine has NOT stopped telling
// the two apart — `Unsigned bank terms` counts them, the signature map lists them, and the post-check reports
// them apart — it has stopped telling the MODEL apart, which is what the doctrine changed.
fmt.Fprintln(w, "To run WITHOUT this pause, drop --verify-bank: the run then carries the unsigned bank forward and uses it.")
}
// renderBankConsolidation states how complete the bank being signed is, ABOVE the table and never inside
// it (backlog row 253б).
//
// ⛔ THE POSITION IS THE POINT, not layout. The table below is capped at 20 rows and ordered least-sure
// first, which puts a row with no rendering — every row a cut budget left unbought, and every row the bank
// already settled — at rank 1000, i.e. LAST. On a book with twenty candidates the whole class the owner
// most needs to see is exactly the class that falls off the bottom, so a per-row mark alone would be a
// green fixture and a blind screen. This line is outside the cap and cannot be lost.
//
// It always says something. A screen that is silent when the bank is whole makes its own silence carry
// meaning, which no reader can distinguish from the screen not knowing — and «did not measure» is a third
// state here, not a shade of «complete».
func renderBankConsolidation(w io.Writer, c *pipeline.BankConsolidation) {
if c == nil {
fmt.Fprintln(w, "Bank completeness: NOT MEASURED — the paid terminology pass did not run for this stop, "+
"so nothing here says whether the renderings below are all the book has.")
return
}
switch {
case c.Complete && c.Consolidated+c.Declined+c.Unanswered == 0:
// ⛔ THE PAID ROLE WAS ASKED NOTHING — every candidate was already settled by the bank, so no batch
// was ever planned. «Every render batch was bought» is technically true of zero batches and reads as
// a report on work that happened, which is the exact shape this surface exists to remove: an
// instrument answering its own question (D39.202). Reachable whenever the already-banked filter
// empties the paid set.
fmt.Fprintln(w, "Bank completeness: WHOLE — the paid role was asked for nothing, because the bank "+
"already renders every candidate this run found. No batch was planned and none was bought.")
case c.Complete:
fmt.Fprintf(w, "Bank completeness: WHOLE — every render batch was bought (%d consolidated, %d declined, %d unanswered).\n",
c.Consolidated, c.Declined, c.Unanswered)
default:
// The money, said as money: these terms are not undecided by the role, they were never offered to it.
fmt.Fprintf(w, "⚠ Bank completeness: PARTIAL — a budget cut the render pass and %d batch(es) were never "+
"bought, so some terms were never offered to the role at all (%d consolidated, %d declined, %d "+
"unanswered — the last figure counts the unbought terms together with the ones the role saw and "+
"skipped). Raise `gates.terminology.budget_usd` and re-run to finish the bank.\n",
c.RenderBatchesDropped, c.Consolidated, c.Declined, c.Unanswered)
}
if c.ClassifyBatchesDropped > 0 {
// A DIFFERENT fact from the one above, on a different budget: the bank can be whole and this still
// non-zero. Merging the two would raise a false alarm about the bank on every classify-only cut.
fmt.Fprintf(w, " Term TYPES are unrefined: the classifier's budget cut %d batch(es) — the renderings "+
"above are unaffected; raise `gates.terminology.classify_budget_usd`.\n", c.ClassifyBatchesDropped)
}
if c.NeverAsked > 0 {
// The saving, and it is stated because an empty rendering in the table below is the same glyph as
// «the role declined» and «the budget did not reach it» — three facts, one of which is good news.
fmt.Fprintf(w, " %d term(s) were NOT ASKED about: the bank already renders those surfaces and every "+
"draft agreed — nothing to decide, and they are marked in the table and the full sidecar.\n", c.NeverAsked)
}
}
// bankStopStdoutCap bounds the stdout table. The emission cap is 200 terms (miner.emitRankCap), and 200
// blocks of source contexts is a scroll, not a review surface — the FULL table always lands in the
// sidecar, and the banner says so, so the cap can never read as "that was all of it".
const bankStopStdoutCap = 20
// renderBankStopRows prints the bank verification table D39.36 specified — src · proposed dst · frequency ·
// variant spread · evidence — capped, with the omitted count stated out loud.
func renderBankStopRows(w io.Writer, rows []pipeline.BankStopRow) {
if len(rows) == 0 {
return
}
// THE ONLY use the confidence is allowed to have (D39.102): the capped stdout view is the REVIEW surface,
// so it is ordered least-sure-first — an ordinal inside one model's reply and nothing more. It is not a
// weight, not a threshold, not comparable between models, and it never decides which rendering wins; the
// full sidecar keeps the source-key order, because that is the reference document.
shown := append([]pipeline.BankStopRow(nil), rows...)
sort.SliceStable(shown, func(i, j int) bool { return reviewRank(shown[i]) < reviewRank(shown[j]) })
if len(shown) > bankStopStdoutCap {
shown = shown[:bankStopStdoutCap]
}
fmt.Fprintln(w, "")
fmt.Fprintln(w, " (least-confident first — the role's own ordering of what to read)")
fmt.Fprintln(w, " src proposed dst origin freq spread conv conf")
for _, r := range shown {
fmt.Fprintf(w, " %-20s %-25s %-9s %5d %6d %5d %5s\n", trunc(r.Src, 20), trunc(dashIfEmpty(r.Dst), 25),
r.Origin, r.Freq, r.Spread, r.Conventions, confCell(r.Conf))
// WHY this row has this dst — the question the artifacts could not answer before the fix-pack. The
// signals are the ranking factors that fired for the winning variant; "invented" says the rendering is
// the role's own, not one the drafts proposed (legitimate — it saw the whole book — and worth a look).
if len(r.Signals) > 0 || r.Invented {
why := strings.Join(r.Signals, ", ")
if r.Invented {
why = strings.TrimPrefix(why+" · invented (no draft proposed it)", " · ")
}
fmt.Fprintf(w, " why: %s\n", trunc(why, 90))
}
if r.SettledByBank {
// The row prints an empty dst, which on this sheet is the glyph of three other facts — the role
// declined, no reply covered it, the budget never reached it — and this one is «nothing to
// decide». The same sentence the text sidecar carries, so the two cannot describe a row
// differently. The count above is what survives the cap; this is what a shown row says.
fmt.Fprintln(w, " NOT ASKED: the bank already renders this surface and every draft agreed — nothing to decide")
}
if len(r.Contradicts) > 0 {
// A contradiction against this run's OWN consolidations: the compound dropped the rendering the
// same reply gave its part. Loudest line of the row — it is a canon breaking inside one call.
fmt.Fprintf(w, " ⚠ contradicts: %s\n", trunc(strings.Join(r.Contradicts, "; "), 80))
}
if len(r.BankHolds) > 0 {
// The other disagreement, and a different decision: the book already renders this surface some
// other way. It rides the same screen as its sibling above, or the operator's first view of the
// stop shows one and hides the other.
fmt.Fprintf(w, " ⚠ bank holds: %s\n", trunc(strings.Join(r.BankHolds, "; "), 80))
}
if len(r.Variants) > 1 {
// The disagreement is the reason to sign: one term coming back several ways is exactly what a
// canon exists to close, and hiding it behind one winner throws that reason away.
fmt.Fprintf(w, " drafts: %s\n", trunc(strings.Join(r.VariantLabels(), " | "), 90))
}
if len(r.Contexts) > 0 {
fmt.Fprintf(w, " ctx: %s\n", trunc(r.Contexts[0], 90))
}
}
if len(rows) > len(shown) {
fmt.Fprintf(w, " … %d more term(s) — the full table is in the sidecar below\n", len(rows)-len(shown))
}
fmt.Fprintln(w, "")
}
// reviewRank orders the review surface: rows the role was least sure of first, rows it said nothing about
// after them (silence is not a low score), and rows it never consolidated last.
func reviewRank(r pipeline.BankStopRow) int {
switch {
case r.Dst == "":
return 1000
case r.Conf < 0:
return 200
default:
return r.Conf
}
}
// confCell renders the role's stated confidence, or "—" when the reply carried none (a negative). Printing
// a bare 0 for both would hide the single most important row on the sheet — the one the role itself said it
// was least sure of — behind the rows it never mentioned.
func confCell(conf int) string {
if conf < 0 {
return "—"
}
return strconv.Itoa(conf)
}
// trunc shortens s to n runes with an ellipsis (rune-safe, so a CJK surface is never split mid-character).
func trunc(s string, n int) string {
rs := []rune(s)
if len(rs) <= n {
return s
}
if n <= 1 {
return "…"
}
return string(rs[:n-1]) + "…"
}
// 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 with the offending src→dst. Two limits: a
// chunk is listed on its CONFIRMED miss count while the detail beside it carries every
// miss including the unsigned ones, so a listed line is not necessarily an approved
// term; and a miss means the rendering is ABSENT — a wrong dst the model OBEYED is
// present in the output and appears in no line here (mempostcheck.go).
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)
}
// Banknote channel (pack-20 / D39.42 п.4): the draft-side WHAT proposals are counted per chunk in
// retrieval_state, but until now NO surface printed them — the cold run of 31.07 had to read them
// out of the DB by hand to answer "did the channel produce anything, and what did the parser
// reject". Chunk coverage matters as much as the total: 0 lines on most chunks is a channel
// failure that a healthy grand total can hide. Printed only when the channel produced or rejected
// something, so a banknote-off book's report stays byte-identical.
var bankLines, bankChunks, bankParseFail, bankTruncated int
for _, rs := range states {
bankLines += rs.NBanknoteLines
if rs.NBanknoteLines > 0 {
bankChunks++
}
if rs.BanknoteParseFail != 0 {
bankParseFail++
}
if rs.BanknoteTruncated != 0 {
bankTruncated++
}
}
if bankLines > 0 || bankParseFail > 0 || bankTruncated > 0 {
fmt.Fprintf(w, "\n=== BANKNOTE (draft-side WHAT channel, observability) === lines=%d over %d/%d chunk(s) · parse-fail chunks=%d · truncated chunks=%d\n",
bankLines, bankChunks, len(states), bankParseFail, bankTruncated)
}
// 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
}
// ⛔ ONE ROW IS ONE LINE. `errors.Join` separates its members with a newline, and an engine error
// that carries both a cut and what ended the chain is exactly such a join — so a table row printed
// its tail across two lines, breaking the column alignment of everything under it and pushing the
// second half where no reader looks for it.
s = strings.Join(strings.Fields(s), " ")
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).
// The recovered share is spelled out rather than hidden: an echo the escalation fixed still measures
// the translator (the D18 mine is live), but it cost the book nothing beyond the wasted primary call.
recovered := ""
if q.EchoDraftRecovered > 0 {
recovered = fmt.Sprintf(", %d recovered by escalation", q.EchoDraftRecovered)
}
fmt.Fprintf(w, "STRIPS/ECHO: cosmetic-strip units=%d (%.1f%%, markdown+CJK) · echo draft=%d (%.1f%%%s) · echo edit=%d (%.1f%%)\n",
q.CosmeticStripUnits, 100*q.CosmeticStripRate, q.EchoDraftChunks, 100*q.EchoDraftRate, recovered, q.EchoEditUnits, 100*q.EchoEditRate)
// The UNSIGNED-BANK line (pack-20 / D39.42 п.4): in the auto mode part of the bank the model is shown
// carries renderings nobody approved, and that has to be visible rather than implied. The follow rate
// is NOT a quality verdict — the unverified section explicitly grants the model the right to translate
// otherwise — it is a measurement of the channel. Printed only when there is an unsigned bank at all,
// so a fully signed book's report is byte-identical to before.
if q.UnsignedBankTerms > 0 || q.UnverifiedShown > 0 {
followed := ""
if q.UnverifiedShown > 0 {
followed = fmt.Sprintf(" (%.0f%%)", 100*float64(q.UnverifiedFollowed)/float64(q.UnverifiedShown))
}
// A ROW count, not a unit count: one unit can carry several, and labelling it a unit count would
// understate the exposure. "checked", not "shown": the counter increments once per unsigned row whose
// key FIRED in that unit, and a sticky carry is put in front of the model without being counted —
// its src is in the previous chunk, so the output is not expected to answer for it
// (membank.PostcheckResult). The follow percentage is a fraction OF THIS, so calling it a count of
// showings would have the operator read a ratio other than the one printed.
fmt.Fprintf(w, "UNSIGNED BANK: terms=%d · rows-checked=%d · model followed=%d%s — proposals, not canon; review the signature map beside the DB and decide via `tmctl bank-apply`\n",
q.UnsignedBankTerms, q.UnverifiedShown, q.UnverifiedFollowed, followed)
}
// The pack-19 flaggers (D39.55). They were aggregated into the report struct from the start but no
// surface ever PRINTED them, so the only run that had them on could not read its own axes. Counts are
// meaningless without their denominators (2 flags out of 3 attributed replies is a different fact from
// 2 out of 300), so the line always carries them. Axis D is reported apart from the AC total because
// its addressee comes from a heuristic, and the rule version travels with the numbers (the voice gate
// is deliberately not snapshot-folded). Spoiler leaks sit here too: the one safety signal of the four.
// Printed only when the flagger actually ran, so a book without voice content is byte-identical.
if q.VoiceCheckVersion != "" || q.VoiceFlags > 0 || q.SpoilerLeaks > 0 {
fmt.Fprintf(w, "VOICE (axes AC, observability): flags=%d over attributed=%d of replies=%d · axis-D pair-register=%d · spoiler-leaks=%d · rules=%s\n",
q.VoiceFlags, q.VoiceAttributed, q.VoiceReplies, q.VoicePairRegister, q.SpoilerLeaks, dashIfEmpty(q.VoiceCheckVersion))
}
// 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.
// WHAT THE MONEY BOUGHT. The total has always been printed; this is the SPLIT, and it leads with the
// part that did not become text because that is the number an operator can act on. Every word is
// literally true of the rows it sums: «superseded» means a later paid call for the same position
// replaced this one — not «wasted», which would be a verdict this report has no standing to reach.
if t := q.PaidTail; t != nil {
fmt.Fprintf(w, "MONEY BY WHAT IT BOUGHT: shipped text $%.6f (%d call(s)) · the book's TERMINOLOGY $%.6f (%d) · superseded-by-a-later-call $%.6f (%d) · bought-nothing-shippable $%.6f (%d) — total $%.6f\n",
t.ShippedUSD, t.ShippedCalls, t.BankUSD, t.BankCalls,
t.SupersededUSD, t.SupersededCalls, t.WithheldUSD, t.WithheldCalls, t.TotalUSD)
if lost := t.LostUSD(); lost > 0 {
pct := 0.0
if t.TotalUSD > 0 {
pct = 100 * lost / t.TotalUSD
}
// ⚠ «Bought nothing» and NOT «did not become shipped text»: the bank roles buy the book's
// terminology and never a chunk of its text, so the older wording called a glossary pass that
// worked perfectly a total loss — the false-cause defect this section was built to remove,
// committed by the section itself (found by the acceptance).
fmt.Fprintf(w, " ⚠ $%.6f of that (%.1f%%) bought NOTHING — neither text nor terminology", lost, pct)
if t.WorstPosition != "" {
fmt.Fprintf(w, "; the largest single loss is %s at $%.6f", t.WorstPosition, t.WorstUSD)
}
fmt.Fprintln(w)
}
}
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.
// The header counts WITHHELD units separately. It used to fold them into `exported`
// (exported = total pending), so a book that shipped nothing for two units still
// announced them as exported — the same lie as the mislabelled banner below, told in
// numbers: a reader who trusts the summary never learns to look.
// The predicate is pipeline.UnitHole — ONE definition shared with the book writer, so the
// numbers this header prints and the holes `tmctl build` marks can never disagree.
withheld, incomplete := pipeline.HoleCounts(exp)
// `incomplete` is a SUBSET of `exported`, not a fourth part of the total: those units DID ship
// text, with a piece of it missing. Spelled that way so a reader adding the numbers up is not
// misled into thinking they partition the book.
fmt.Fprintf(w, "# export %s — total units=%d, exported=%d (of them incomplete=%d), withheld=%d, pending=%d",
exp.BookID, exp.TotalUnits, exp.TotalUnits-exp.PendingUnits-withheld, incomplete, withheld, 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.DroppedMembers > 0 && ce.FinalText != "":
// c-lite member drop: real text, but a member chunk's worth of it is MISSING. The
// banner says so and the marker repeats it INSIDE the text stream, because a reader
// scrolling prose passes the banner once and the hole has no seam of its own.
fmt.Fprintf(w, "=== CHAPTER %d CHUNK %d — %s%s (INCOMPLETE) ===\n",
ce.Chapter, ce.ChunkIdx, ce.Disposition, flagParen(ce.FlagReason))
fmt.Fprintln(w, gapMarker(ce.DroppedMembers, ce.DroppedReason))
case ce.FlagReason == string(pipeline.FlagSanitizerStripped) && 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))
case ce.FinalText != "":
// Flagged, with text, neither known cause — a neutral banner rather than an
// inherited claim (see renderTranslate for the same reasoning).
fmt.Fprintf(w, "=== CHAPTER %d CHUNK %d — %s%s (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 + ")"
}
// renderBuild prints the build report as indented JSON: the versioned envelope every JSON output of the
// engine carries (17-seam-inbound-law п.3), and the surface the platform's export door will read the
// file paths from. JSON only: the consumer is a caller that opens the files, and the human reads paths
// out of JSON as well as out of prose.
func renderBuild(w io.Writer, rep *pipeline.BuildReport) error {
enc := json.NewEncoder(w)
enc.SetIndent("", " ")
return enc.Encode(rep)
}
// renderManifest prints the persisted chapter/chunk manifest (row 100). The default is a human summary —
// a 2283-chapter tree is not something anyone reads in a terminal — and --json emits the document itself,
// byte-identical to the sidecar, so a consumer can pipe it instead of knowing where the file lives.
func renderManifest(w io.Writer, m *pipeline.BookManifest, path string, asJSON bool) error {
if asJSON {
enc := json.NewEncoder(w)
enc.SetIndent("", " ")
return enc.Encode(m)
}
fmt.Fprintf(w, "=== MANIFEST: %s ===\n", m.BookID)
fmt.Fprintf(w, "chapters=%d units=%d chunks=%d\n", m.ChaptersTotal, m.UnitsTotal, m.ChunksTotal)
fmt.Fprintf(w, "source: %d bytes (sha256 %.12s) · encoding %s · %s→%s\n",
m.SourceBytes, m.SourceSHA256, m.Encoding, m.SourceLang, m.TargetLang)
fmt.Fprintf(w, "cut by: %s · validity key %.12s\n", m.ChunkerVersion, m.Key)
// WHERE the chapter boundaries came from, in the human view too: an operator deciding whether to
// offer an order «through chapter N» has to know whether N names something the file said or something
// this engine inferred from prose (BookManifest.Structure).
if m.Structure != "" {
fmt.Fprintf(w, "chapter structure: %s\n", m.Structure)
}
// The a-priori price. `step_max_usd` is on the same line as the total on purpose: it is the number a
// ceiling has to clear before the run can move AT ALL, and an operator reading only the total would
// set a ceiling that admits nothing.
if p := m.Price; p != nil {
// ⚠ THE BOOK-LEVEL PART IS NAMED A CEILING, NOT A FORECAST, and on a short book that is the whole
// of the honesty: the terminology passes are bounded by their configured budgets and their input
// does not exist until the drafts do, so the figure is what the engine will NOT exceed. On a book
// of a few chapters it can dominate the total — an operator reading «expected cost» as a bill
// would think a short book dearer than a long one per chapter, which is backwards (acceptance F3).
fmt.Fprintf(w, "expected cost: $%.6f for the whole book (%d source chars)\n", p.ExpectedUSD, p.SourceChars)
if p.BookOnceUSD > 0 {
fmt.Fprintf(w, " …of which $%.6f is the book-level CEILING of the terminology passes — a bound the run will not exceed, not a forecast; it is charged once per book, so it weighs most on a short one\n", p.BookOnceUSD)
}
fmt.Fprintf(w, "smallest workable ceiling: $%.6f — the largest single reservation; under it NOTHING is admitted\n", p.StepMaxUSD)
}
fmt.Fprintf(w, "written: %s\n", path)
return nil
}
// 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 {
// ⛔ THIS LINE NO LONGER ASSERTS THE MONEY, and that is the point. It used to end «translate will
// require --resnapshot = re-paying for the book», which is a claim about SPEND made by a flag that
// never computed any. The two come apart the moment status learned to see a DROPPED stage:
// projectRebill deliberately skips rows whose stage the current pipeline does not run («a stage the
// current pipeline does not run is never re-billed»), so that drift is real and its re-payment is
// genuinely zero — and the old sentence would have invented a cost. The drift line now says what
// drifted; the RE-PAYMENT lines below say what it costs, from the projection that actually ran.
drift += " ⚠ CONFIG-DRIFT (the current config renders a different snapshot than the stored rows — an edit since the run; translate needs --resnapshot to proceed)"
}
// The basis behind the boolean: `false` means two different things and only one of them is an answer.
if rep.ConfigDriftBasis == pipeline.DriftBasisUnknown {
drift += " ⚠ CONFIG-DRIFT UNKNOWN (the check could not run — the flag above is false because nothing was established, NOT because the config matches; see the log)"
}
// The drift flags say THAT the config moved; this says what continuing would COST (spec D15.2 §9).
// It is the same projection `translate` refuses on, so the operator reads the decision number here
// instead of discovering it in a refusal.
if rep.RebillUnits > 0 {
// BOTH denominations, always, and labelled. The chunk×stage count is what the money is billed in;
// the output-unit count is what a purchase is sized in. Printing only the first is how a reader
// hands `--max-units` a number several times too large.
// And the figure's BASIS, in the consent gate's own words (RebillFigureBasis): what the number is
// made of, that it errs upward, and how much of it is priced across a model move or carried at last
// season's money. A figure printed without it invites the reader to take an estimate for a quote.
drift += fmt.Sprintf(" ⚠ RE-PAYMENT: %d chunk×stage unit(s) already billed under a superseded snapshot would be paid for again — that is %d OUTPUT unit(s) of the book, the granularity --max-units counts in — ~$%.6f (%s; translate needs --accept-rebill above the book's consent threshold). ⚠ Sizing --max-units from that number does NOT buy those units back: a grant goes to NEVER-DELIVERED units first and only reaches re-making when none are left, so on a book that still has undelivered units it will deliver new ones instead",
rep.RebillUnits, rep.RebillOutputUnits, rep.RebillUSD, rep.RebillFigureBasis())
}
// Drift with nothing to re-pay is a real and non-obvious state — a stage dropped from the config puts
// every book in it — and leaving the operator to infer it from the ABSENCE of a re-payment line is how
// «no line» gets read as «no drift consequence».
if rep.ConfigDrift && rep.RebillUnits == 0 && rep.RebillBasis != pipeline.RebillBasisFailed {
drift += " ⚠ …and NOTHING already billed is re-paid by it: the drift is in rows the current config no longer runs, so a re-run buys those units anew rather than re-buying them"
}
// A zero is not self-explanatory, and printing it as though it were is the mistake the `rebill_basis`
// field exists to stop: "nothing to re-pay" and "we could not work out what a re-pass would cost" are
// the same two zeroes. Only the two states that are NOT an answer speak here — a computed figure needs
// no caveat, and a book with nothing stored has nothing to say about re-payment at all.
switch rep.RebillBasis {
case pipeline.RebillBasisFailed:
drift += " ⚠ RE-PAYMENT UNKNOWN (the projection failed — the figures below are zero because they could not be computed, NOT because continuing is free; see the log)"
case pipeline.RebillBasisStored:
drift += " ⚠ RE-PAYMENT FIGURES ARE STALE (the book's bank could not be folded, so they describe the glossary the LAST run stored, not what the next run would build — fix the bank and re-read; see the log)"
}
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)
// Per-wave progress (row 99). The line above needs BOTH waves to have resolved a unit ok, and the edit
// wave does not start before the bank stop — so it sits at 0/N for the whole draft wave and says nothing
// about the drafting that IS happening. This line is the split; "resolved" there includes flagged units
// (see PhaseProgress), which is why it can exceed the done count above.
fmt.Fprintf(w, "Phases (units resolved by wave): draft %s · edit %s\n",
waveCell(rep.Progress.Draft), waveCell(rep.Progress.Edit))
fmt.Fprintf(w, "Escalations: %d · post-check misses (confirmed): %d · style-flags (observability): %d\n",
rep.Escalations, rep.PostcheckMisses, rep.StyleFlags)
// Printed only when the bank actually holds unsigned rows, so a book translating entirely against signed
// canon keeps its exact prior bytes.
if rep.UnsignedBankTerms > 0 {
fmt.Fprintf(w, "Unsigned bank terms: %d — the engine's own proposals, used on the wire like any other bank row and not marked apart for the model; review the signature map beside the DB and decide via `tmctl bank-apply`\n", rep.UnsignedBankTerms)
}
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)
// The estimated share of that committed figure, on the surface an operator reads before deciding
// whether to keep paying. The same pair rides in --json for the platform; printing it here too is
// what makes «committed» readable as a range rather than as a measurement.
if rep.EstimatedRows > 0 {
fmt.Fprintf(w, " of which ESTIMATED: $%.6f over %d call(s) — booked at the reservation estimate because the provider never reported usage for them (a body that would not decode, a call our own deadline or a stop cut short, a 2xx with no usage)\n",
rep.EstimatedUSD, rep.EstimatedRows)
}
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
}
// waveCell renders one wave counter, or "—" for a wave this pipeline does not have (a draft-only
// pipeline has no edit wave, and printing "0/0" for it would read as work that never starts).
func waveCell(c pipeline.WaveCounter) string {
if c.Total == 0 {
return "—"
}
return fmt.Sprintf("%d/%d", c.Done, c.Total)
}
// dashIfEmpty renders "" as "—" for a table cell.
// dashIfEmpty renders an empty (or blank) cell as an em dash. ONE definition: pack-20 briefly added a
// second, near-identical `dash` 400 lines above, and the two disagreed on whitespace — the kind of split
// where two tables in the same report start describing the same missing value differently.
func dashIfEmpty(s string) string {
if strings.TrimSpace(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
}