textmachine/backend/cmd/tmbankprobe/passes.go

159 lines
6.4 KiB
Go

package main
import (
"fmt"
"os"
"sort"
"strings"
"textmachine/backend/internal/pipeline"
)
// passes.go: the §4.4 instrument — ONE book, its TWO purchases of the bank role, asking what actually
// differed in a row's own input where the role's decision differed.
//
// ⛔ IT READS THE STOP TABLES, and the first version of it read the signature maps instead. That version
// was WRONG and the wrongness is worth keeping written down: the map's note names the chunk COUNT of the
// top draft proposal but never its TEXT — only the other proposals are quoted — so a comparison built on
// it folds the CONSOLIDATED rendering into the evidence and reports «the evidence moved» for every single
// row whose rendering moved. It produced 0 of 12 and 0 of 14 «identical», which is not a fact about the
// runs but a fact about the reader. The stop table carries the drafts, their counts, the frequency, the
// type and the source contexts — the row's whole input, as the role was shown it.
//
// ⚠ WHAT IS STILL OUTSIDE THE ARTIFACT, and it is what the residue of this question is made of: the BATCH
// the row travelled in (its neighbours) and the canon anchor built for that batch. Two rows with identical
// blocks can therefore still have been asked in different company, and the artifacts cannot say so.
// rowInput renders everything the stop table records about a row's INPUT — never the answer. The
// consolidated rendering, the confidence and the INVENTED mark are the role's OUTPUT and are deliberately
// absent: folding an answer into the evidence is exactly how a comparison comes to say «the evidence
// changed» every time the answer did.
func rowInput(r pipeline.BankStopRow, withCtx bool) string {
parts := make([]string, 0, len(r.Variants))
for _, v := range r.Variants {
parts = append(parts, v.Label())
}
sort.Strings(parts)
s := fmt.Sprintf("type=%s origin=%s freq=%d drafts=[%s] evidence=[%s]",
r.Type, r.Origin, r.Freq, strings.Join(parts, " | "), strings.Join(r.Evidence, "; "))
if withCtx {
ctx := append([]string(nil), r.Contexts...)
sort.Strings(ctx)
s += " ctx=[" + strings.Join(ctx, " ¦ ") + "]"
}
return s
}
// printPassComparison joins a book's two stop tables and reports the numbers the economics of «do not
// re-ask a settled row» stands on.
func printPassComparison(name, first, second string) error {
a, err := loadSheetRows(first)
if err != nil {
return err
}
b, err := loadSheetRows(second)
if err != nil {
return err
}
idx := func(rows []pipeline.BankStopRow) map[string]pipeline.BankStopRow {
out := make(map[string]pipeline.BankStopRow, len(rows))
for _, r := range rows {
out[r.Src] = r
}
return out
}
ai, bi := idx(a), idx(b)
var shared []string
for src := range ai {
if _, ok := bi[src]; ok {
shared = append(shared, src)
}
}
sort.Strings(shared)
fmt.Printf("\n== %s: ONE BOOK, TWO PURCHASES OF THE BANK ROLE\n", name)
fmt.Printf(" first : %s (%d rows)\n second: %s (%d rows)\n", first, len(a), second, len(b))
fmt.Printf(" shared surfaces: %d | only in the first: %d | only in the second: %d\n",
len(shared), len(ai)-len(shared), len(bi)-len(shared))
// CONTROLS, printed rather than assumed: a comparison of two documents that carry no drafts and no
// contexts would find everything identical and mean nothing by it.
withDrafts, withCtx := 0, 0
for _, src := range shared {
if len(ai[src].Variants) > 0 && len(bi[src].Variants) > 0 {
withDrafts++
}
if len(ai[src].Contexts) > 0 && len(bi[src].Contexts) > 0 {
withCtx++
}
}
fmt.Printf(" control: shared rows carrying draft proposals on BOTH sides: %d of %d · carrying source contexts on both: %d of %d\n",
withDrafts, len(shared), withCtx, len(shared))
var changed, sameBlock, sameBlockAndCtx, settled, unresolvedBoth int
var examples []string
for _, src := range shared {
x, y := ai[src], bi[src]
// ⛔ EVERY SHARED ROW IS CLASSIFIED, and the two classes below are pulled out of «unchanged»
// deliberately. A row the bank settled was never asked in the second purchase, and a row left
// unresolved in BOTH was never decided in either: counting them among «rows a do-not-re-ask rule
// would have saved» would credit the rule with rows nobody re-asked and nobody answered.
if x.SettledByBank || y.SettledByBank {
settled++
continue
}
if strings.TrimSpace(x.Dst) == "" && strings.TrimSpace(y.Dst) == "" {
unresolvedBoth++
continue
}
if x.Dst == y.Dst {
continue
}
changed++
blockSame := rowInput(x, false) == rowInput(y, false)
ctxSame := rowInput(x, true) == rowInput(y, true)
if blockSame {
sameBlock++
}
if ctxSame {
sameBlockAndCtx++
}
if len(examples) < 12 {
verdict := "the row's own input MOVED"
switch {
case ctxSame:
verdict = "input IDENTICAL, contexts included"
case blockSame:
verdict = "block identical, CONTEXTS moved"
}
examples = append(examples, fmt.Sprintf("%s: %q → %q — %s", src, x.Dst, y.Dst, verdict))
if !blockSame {
examples = append(examples,
fmt.Sprintf(" before: %s", rowInput(x, false)),
fmt.Sprintf(" after : %s", rowInput(y, false)))
}
}
}
fmt.Printf(" rows a purchase never asked about (the bank had settled them): %d — excluded below, they are not re-decisions\n", settled)
fmt.Printf(" rows left UNRESOLVED in both purchases: %d — also excluded: nobody decided them either time\n", unresolvedBoth)
fmt.Printf(" rows whose rendering CHANGED between the two purchases: %d of %d shared\n", changed, len(shared))
fmt.Printf(" of those, the row's own BLOCK was identical (drafts, freq, type, evidence): %d\n", sameBlock)
fmt.Printf(" of those, identical INCLUDING the source contexts the role was shown: %d\n", sameBlockAndCtx)
for _, e := range examples {
fmt.Printf(" · %s\n", e)
}
// The other half of the population, and it is the one the economics is actually about.
unchanged := len(shared) - changed - settled - unresolvedBoth
fmt.Printf(" rows whose rendering did NOT change: %d of %d — these are the ones a «do not re-ask what is settled» rule would have saved\n", unchanged, len(shared))
return nil
}
func loadSheetRows(path string) ([]pipeline.BankStopRow, error) {
raw, err := os.ReadFile(path)
if err != nil {
return nil, err
}
rows, err := pipeline.ParseBankStopTable(raw)
if err != nil {
return nil, fmt.Errorf("%s: %w", path, err)
}
return rows, nil
}