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 }