595 lines
22 KiB
Go
595 lines
22 KiB
Go
// Command tmbankprobe is the $0 replay probe for the bank's «contested» predicates: it reads stop
|
||
// projections a PAID run already wrote, rebuilds the engine's own rows out of them, and measures how many
|
||
// candidates each threshold selects — before anybody builds a router that spends money on the answer.
|
||
//
|
||
// ⛔ WHY IT READS ARTIFACTS RATHER THAN RE-RUNNING THE ROLE. The question is «what did the population look
|
||
// like AT THE STOP», and the state of a project database moves after the stop: on the cold run A the
|
||
// glossary is EMPTY at the stop and holds 69 rows afterwards, so a naive replay would see a full bank and
|
||
// report as «already settled by the bank» rows nobody settled when the money was spent. The stop
|
||
// projection is the engine's own record of that moment, written by the same functions the router would
|
||
// read (bankStopRows → BankExportProposal), so it is the ONE source that cannot drift under the question.
|
||
//
|
||
// ⛔ AND IT NEVER WRITES. Every database it opens is opened read-only AND immutable, which is stronger
|
||
// than it sounds: a plain `mode=ro` connection still creates or updates the -shm sidecar beside the file
|
||
// it reads, and these files are bought evidence. Nothing here migrates, and a schema this binary does not
|
||
// match is REPORTED rather than repaired — the engine's own store refuses such a database on purpose, and
|
||
// a probe that quietly migrated an archived run would destroy the thing it came to measure.
|
||
//
|
||
// Usage:
|
||
//
|
||
// go run ./cmd/tmbankprobe -stop A=/path/a.bank.json -stop B=/path/b.bank.json
|
||
// go run ./cmd/tmbankprobe -stop B=/path/b.bank.json -db B=/path/project.db
|
||
// go run ./cmd/tmbankprobe -stop A=... -stop B=... -compare A,B
|
||
package main
|
||
|
||
import (
|
||
"database/sql"
|
||
"encoding/json"
|
||
"flag"
|
||
"fmt"
|
||
"os"
|
||
"sort"
|
||
"strings"
|
||
|
||
_ "modernc.org/sqlite"
|
||
|
||
"textmachine/backend/internal/pipeline"
|
||
"textmachine/backend/internal/store"
|
||
"textmachine/backend/internal/terminology"
|
||
)
|
||
|
||
// source is one stop projection, rebuilt into the engine's types plus the controls that say what the file
|
||
// actually carried. The controls are printed beside every number: a zero from a field the file does not
|
||
// have and a zero from a field it has are the same zero on a screen.
|
||
type source struct {
|
||
name string
|
||
path string
|
||
rows []pipeline.BankStopRow
|
||
cands []terminology.Candidate
|
||
asOf string
|
||
runID string
|
||
withConv int // records that carried `conventions` (A's export predates the field)
|
||
withInv int
|
||
withConf int
|
||
withCtr int
|
||
withHold int
|
||
labels int // variant labels this binary could read back
|
||
badLabel []string
|
||
consol map[string]any
|
||
// blindToSettled marks a source whose artifact cannot say which rows the bank SETTLED. The JSON
|
||
// sidecar carries no such field (bankexport.go), so on a purchase that settled anything its rows look
|
||
// exactly like rows the role answered with nothing — and both denominators below would be wrong
|
||
// without anybody seeing it. The stop TABLE carries the mark; this flag is what makes the difference
|
||
// visible instead of silent.
|
||
blindToSettled bool
|
||
}
|
||
|
||
type bankJSON struct {
|
||
BookID string `json:"book_id"`
|
||
AsOf string `json:"as_of"`
|
||
RunID string `json:"run_id"`
|
||
Terms []json.RawMessage `json:"terms"`
|
||
Proposed []map[string]json.RawMessage `json:"proposed"`
|
||
Consolidation map[string]any `json:"consolidation"`
|
||
}
|
||
|
||
func main() {
|
||
var stops, sheets, dbs multiFlag
|
||
flag.Var(&stops, "stop", "NAME=PATH of a stop projection (project.db.bank.json); repeatable")
|
||
flag.Var(&sheets, "sheet", "NAME=PATH of a stop TABLE (project.db.bank-stop.txt) — the richer artifact: it carries the source contexts and the settled mark; repeatable")
|
||
flag.Var(&dbs, "db", "NAME=PATH of a project database to read CONTROL counts from, read-only; repeatable")
|
||
var passes multiFlag
|
||
flag.Var(&passes, "passes", "NAME=FIRST,SECOND — a book's two stop TABLES: what changed in a row's OWN input between its two purchases; repeatable")
|
||
compare := flag.String("compare", "", "NAME,NAME — join two stop projections by source surface and report what changed between the two purchases")
|
||
flag.Parse()
|
||
if len(stops)+len(sheets)+len(passes) == 0 {
|
||
fmt.Fprintln(os.Stderr, "tmbankprobe: give it something to read — at least one -stop, -sheet or -passes")
|
||
os.Exit(2)
|
||
}
|
||
byName := map[string]*source{}
|
||
var order []string
|
||
for _, spec := range sheets {
|
||
name, path, ok := strings.Cut(spec, "=")
|
||
if !ok {
|
||
fmt.Fprintf(os.Stderr, "tmbankprobe: -sheet wants NAME=PATH, got %q\n", spec)
|
||
os.Exit(2)
|
||
}
|
||
s, err := loadSheetSource(name, path)
|
||
if err != nil {
|
||
fmt.Fprintf(os.Stderr, "tmbankprobe: %v\n", err)
|
||
os.Exit(1)
|
||
}
|
||
byName[name] = s
|
||
order = append(order, name)
|
||
}
|
||
for _, spec := range stops {
|
||
name, path, ok := strings.Cut(spec, "=")
|
||
if !ok {
|
||
fmt.Fprintf(os.Stderr, "tmbankprobe: -stop wants NAME=PATH, got %q\n", spec)
|
||
os.Exit(2)
|
||
}
|
||
s, err := loadStop(name, path)
|
||
if err != nil {
|
||
fmt.Fprintf(os.Stderr, "tmbankprobe: %v\n", err)
|
||
os.Exit(1)
|
||
}
|
||
byName[name] = s
|
||
order = append(order, name)
|
||
}
|
||
for _, name := range order {
|
||
byName[name].printControls()
|
||
}
|
||
for _, spec := range dbs {
|
||
name, path, ok := strings.Cut(spec, "=")
|
||
if !ok {
|
||
fmt.Fprintf(os.Stderr, "tmbankprobe: -db wants NAME=PATH, got %q\n", spec)
|
||
os.Exit(2)
|
||
}
|
||
if err := printDBControls(name, path); err != nil {
|
||
fmt.Fprintf(os.Stderr, "tmbankprobe: %v\n", err)
|
||
os.Exit(1)
|
||
}
|
||
}
|
||
printPreCallGrid(order, byName)
|
||
printPostCallGrid(order, byName)
|
||
for _, spec := range passes {
|
||
name, paths, ok := strings.Cut(spec, "=")
|
||
first, second, ok2 := strings.Cut(paths, ",")
|
||
if !ok || !ok2 {
|
||
fmt.Fprintf(os.Stderr, "tmbankprobe: -passes wants NAME=FIRST,SECOND, got %q\n", spec)
|
||
os.Exit(2)
|
||
}
|
||
if err := printPassComparison(name, first, second); err != nil {
|
||
fmt.Fprintf(os.Stderr, "tmbankprobe: %v\n", err)
|
||
os.Exit(1)
|
||
}
|
||
}
|
||
if *compare != "" {
|
||
a, b, ok := strings.Cut(*compare, ",")
|
||
if !ok || byName[a] == nil || byName[b] == nil {
|
||
fmt.Fprintf(os.Stderr, "tmbankprobe: -compare wants two loaded -stop names, got %q\n", *compare)
|
||
os.Exit(2)
|
||
}
|
||
printComparison(byName[a], byName[b])
|
||
}
|
||
}
|
||
|
||
type multiFlag []string
|
||
|
||
func (m *multiFlag) String() string { return strings.Join(*m, ",") }
|
||
func (m *multiFlag) Set(v string) error { *m = append(*m, v); return nil }
|
||
|
||
// loadSheetSource builds a source from the stop TABLE through the engine's own reader. It is the richer
|
||
// artifact — the table carries the source contexts and the «not asked» mark, which the JSON sidecar does
|
||
// not — and it doubles as the SECOND INSTRUMENT on a purchase whose sidecar is also present: two files
|
||
// written by two renderers from the same rows, compared with -compare.
|
||
func loadSheetSource(name, path string) (*source, 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)
|
||
}
|
||
s := &source{name: name, path: path, asOf: "(stop table)", runID: "(not carried by the table)"}
|
||
for _, r := range rows {
|
||
if r.Conventions > 0 {
|
||
s.withConv++
|
||
}
|
||
if r.Conf >= 0 {
|
||
s.withConf++
|
||
}
|
||
if r.Invented {
|
||
s.withInv++
|
||
}
|
||
if len(r.Contradicts) > 0 {
|
||
s.withCtr++
|
||
}
|
||
if len(r.BankHolds) > 0 {
|
||
s.withHold++
|
||
}
|
||
s.labels += len(r.Variants)
|
||
cand := terminology.Candidate{Key: r.Src, Src: r.Src, Type: r.Type, Freq: r.Freq, KWIC: r.Contexts}
|
||
for _, v := range r.Variants {
|
||
cand.Variants = append(cand.Variants, terminology.Variant{Dst: v.Dst, Chunks: v.Chunks, Forms: 1, Via: v.Via})
|
||
}
|
||
s.rows = append(s.rows, r)
|
||
s.cands = append(s.cands, cand)
|
||
}
|
||
return s, nil
|
||
}
|
||
|
||
func loadStop(name, path string) (*source, error) {
|
||
raw, err := os.ReadFile(path)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
var doc bankJSON
|
||
if err := json.Unmarshal(raw, &doc); err != nil {
|
||
return nil, fmt.Errorf("%s: %w", path, err)
|
||
}
|
||
s := &source{name: name, path: path, asOf: doc.AsOf, runID: doc.RunID, consol: doc.Consolidation, blindToSettled: true}
|
||
for _, rec := range doc.Proposed {
|
||
row := pipeline.BankStopRow{}
|
||
getStr := func(k string) string {
|
||
if v, ok := rec[k]; ok {
|
||
var out string
|
||
if json.Unmarshal(v, &out) == nil {
|
||
return out
|
||
}
|
||
}
|
||
return ""
|
||
}
|
||
getInt := func(k string) (int, bool) {
|
||
if v, ok := rec[k]; ok {
|
||
var out int
|
||
if json.Unmarshal(v, &out) == nil {
|
||
return out, true
|
||
}
|
||
}
|
||
return 0, false
|
||
}
|
||
getStrs := func(k string) ([]string, bool) {
|
||
if v, ok := rec[k]; ok {
|
||
var out []string
|
||
if json.Unmarshal(v, &out) == nil {
|
||
return out, true
|
||
}
|
||
}
|
||
return nil, false
|
||
}
|
||
row.Src, row.Dst, row.Type, row.Origin = getStr("src"), getStr("dst"), getStr("kind"), getStr("channel")
|
||
row.Freq, _ = getInt("freq")
|
||
row.Spread, _ = getInt("spread")
|
||
if conv, ok := getInt("conventions"); ok {
|
||
row.Conventions = conv
|
||
s.withConv++
|
||
}
|
||
// The export always carries the field and writes -1 for «the reply stated none», so the control
|
||
// below counts rows that carry a CONFIDENCE — not rows that carry the field. The sheet loader counts
|
||
// the same thing, and two controls with one name and two meanings is how a comparison between two
|
||
// sources stops meaning anything.
|
||
row.Conf = -1
|
||
if conf, ok := getInt("conf"); ok {
|
||
row.Conf = conf
|
||
}
|
||
if row.Conf >= 0 {
|
||
s.withConf++
|
||
}
|
||
if inv, ok := rec["invented"]; ok {
|
||
var b bool
|
||
if json.Unmarshal(inv, &b) == nil && b {
|
||
row.Invented = true
|
||
s.withInv++
|
||
}
|
||
}
|
||
if ctr, ok := getStrs("contradicts"); ok && len(ctr) > 0 {
|
||
row.Contradicts = ctr
|
||
s.withCtr++
|
||
}
|
||
if h, ok := getStrs("bank_holds"); ok && len(h) > 0 {
|
||
row.BankHolds = h
|
||
s.withHold++
|
||
}
|
||
labels, _ := getStrs("variants")
|
||
cand := terminology.Candidate{Key: row.Src, Src: row.Src, Type: row.Type, Freq: row.Freq}
|
||
for _, l := range labels {
|
||
v, ok := pipeline.ParseBankStopVariantLabel(l)
|
||
if !ok {
|
||
s.badLabel = append(s.badLabel, l)
|
||
continue
|
||
}
|
||
s.labels++
|
||
row.Variants = append(row.Variants, v)
|
||
cand.Variants = append(cand.Variants, terminology.Variant{Dst: v.Dst, Chunks: v.Chunks, Forms: 1, Via: v.Via})
|
||
}
|
||
// ⚠ THE SCHEMA DRIFT, handled where it happens rather than in a comment elsewhere: the older export
|
||
// (cold run A) carries NO `conventions` field at all. Recomputing it from the variants is exactly
|
||
// what the newer export documents the field to be — «the length of Variants» — and a probe that
|
||
// counted the absent field as zero would report «nothing was contested» about a file that never
|
||
// held the answer.
|
||
if _, ok := getInt("conventions"); !ok {
|
||
row.Conventions = len(row.Variants)
|
||
}
|
||
s.rows = append(s.rows, row)
|
||
s.cands = append(s.cands, cand)
|
||
}
|
||
return s, nil
|
||
}
|
||
|
||
func (s *source) printControls() {
|
||
fmt.Printf("\n== %s %s\n", s.name, s.path)
|
||
fmt.Printf(" as_of=%s run_id=%s\n", s.asOf, s.runID)
|
||
fmt.Printf(" rows read: %d | variant labels parsed: %d | labels this binary could NOT read: %d %v\n",
|
||
len(s.rows), s.labels, len(s.badLabel), first(s.badLabel, 3))
|
||
fmt.Printf(" of %d rows: conventions carried=%d · a stated confidence=%d · invented=%d · contradicts=%d · bank_holds=%d\n",
|
||
len(s.rows), s.withConv, s.withConf, s.withInv, s.withCtr, s.withHold)
|
||
if s.withConv == 0 && len(s.rows) > 0 {
|
||
fmt.Printf(" ⚠ this export predates the `conventions` field; it is recomputed as len(variants), which is what the field is defined to be\n")
|
||
}
|
||
if s.withConv > 0 {
|
||
bad := 0
|
||
for _, r := range s.rows {
|
||
if r.Conventions != len(r.Variants) {
|
||
bad++
|
||
}
|
||
}
|
||
fmt.Printf(" control: records where conventions != len(variants): %d (the export defines them equal)\n", bad)
|
||
}
|
||
if s.consol != nil {
|
||
fmt.Printf(" consolidation block: %s\n", compactJSON(s.consol))
|
||
}
|
||
pre, ans, settled, unresolved := s.denominators()
|
||
fmt.Printf(" DENOMINATORS — pre-call (candidates at the role's input): %d | post-call (rows the role ANSWERED): %d\n", pre, ans)
|
||
fmt.Printf(" components: settled by the bank (never asked) %d · unresolved (no rendering came back) %d\n", settled, unresolved)
|
||
if s.blindToSettled {
|
||
fmt.Printf(" ⚠ THIS ARTIFACT CANNOT SAY WHICH ROWS THE BANK SETTLED — the JSON sidecar carries no «not asked»\n")
|
||
fmt.Printf(" field, so a settled row is counted above as «unresolved» and stays in the pre-call denominator.\n")
|
||
fmt.Printf(" On a first purchase that settles nothing the two readings agree; on any later one they do not.\n")
|
||
fmt.Printf(" Use the stop TABLE (-sheet) for those: it carries the mark.\n")
|
||
}
|
||
}
|
||
|
||
// denominators returns the two the measurement is ordered to use, plus the components that make them
|
||
// differ — printed, because on the first purchase of each run they happen to be EQUAL and a reader has to
|
||
// see why.
|
||
//
|
||
// ⛔ A ROW THE BANK SETTLED IS IN NEITHER, and it is the one correction this function needed: the settled
|
||
// filter runs BEFORE the first paid call, so such a row is not «a candidate at the role's input» any more
|
||
// than it is «a row the role answered». Counting it on the pre-call side alone would make the two
|
||
// denominators differ by a row that reached neither, and every share on that side would be quietly diluted.
|
||
func (s *source) denominators() (preCall, answered, settled, unresolved int) {
|
||
for _, r := range s.rows {
|
||
switch {
|
||
case r.SettledByBank:
|
||
settled++
|
||
case strings.TrimSpace(r.Dst) == "":
|
||
unresolved++
|
||
}
|
||
}
|
||
preCall = len(s.rows) - settled
|
||
return preCall, preCall - unresolved, settled, unresolved
|
||
}
|
||
|
||
func printPreCallGrid(order []string, by map[string]*source) {
|
||
fmt.Printf("\n== PRE-CALL «contested»: the drafts disagreed, measured before a single call\n")
|
||
fmt.Printf(" denominator: candidates at the role's input\n")
|
||
fmt.Printf(" %-22s %s\n", "threshold", strings.Join(order, " "))
|
||
for _, minConv := range []int{2, 3, 4} {
|
||
for _, share := range []float64{0, 0.75, 0.5} {
|
||
label := fmt.Sprintf("conv>=%d", minConv)
|
||
if share > 0 {
|
||
label += fmt.Sprintf(" lead<=%.2f", share)
|
||
} else {
|
||
label += " lead: off"
|
||
}
|
||
var cells []string
|
||
for _, name := range order {
|
||
s := by[name]
|
||
n := 0
|
||
for i, c := range s.cands {
|
||
if s.rows[i].SettledByBank {
|
||
continue // never reached the role: not in the denominator, not in the count
|
||
}
|
||
if c.Contest(terminology.ContestOpts{MinConventions: minConv, MaxLeaderShare: share}).Contested {
|
||
n++
|
||
}
|
||
}
|
||
pre, _, _, _ := s.denominators()
|
||
cells = append(cells, fmt.Sprintf("%3d/%3d = %5.1f%%", n, pre, pct(n, pre)))
|
||
}
|
||
fmt.Printf(" %-22s %s\n", label, strings.Join(cells, " "))
|
||
}
|
||
}
|
||
fmt.Printf(" unsupported (no draft rendered it at all — not a contest, and not agreement):\n")
|
||
for _, name := range order {
|
||
s := by[name]
|
||
n := 0
|
||
for i, c := range s.cands {
|
||
if s.rows[i].SettledByBank {
|
||
continue
|
||
}
|
||
if c.Contest(terminology.ContestOpts{MinConventions: 2}).Unsupported {
|
||
n++
|
||
}
|
||
}
|
||
pre, _, _, _ := s.denominators()
|
||
fmt.Printf(" %-10s %d of %d\n", name, n, pre)
|
||
}
|
||
}
|
||
|
||
func printPostCallGrid(order []string, by map[string]*source) {
|
||
fmt.Printf("\n== POST-CALL «contested»: the answer is worth a second opinion\n")
|
||
fmt.Printf(" denominator: rows the role ANSWERED\n")
|
||
type arm struct {
|
||
label string
|
||
opts pipeline.StopContestOpts
|
||
}
|
||
arms := []arm{
|
||
{"unresolved only", pipeline.StopContestOpts{CountUnresolved: true}},
|
||
{"conflicts only", pipeline.StopContestOpts{CountConflicts: true}},
|
||
{"invented only", pipeline.StopContestOpts{CountInvented: true}},
|
||
{"conf<=50", pipeline.StopContestOpts{UseConf: true, MaxConf: 50}},
|
||
{"conf<=60", pipeline.StopContestOpts{UseConf: true, MaxConf: 60}},
|
||
{"conf<=70", pipeline.StopContestOpts{UseConf: true, MaxConf: 70}},
|
||
{"conf<=80", pipeline.StopContestOpts{UseConf: true, MaxConf: 80}},
|
||
{"conf<=70 + invented", pipeline.StopContestOpts{UseConf: true, MaxConf: 70, CountInvented: true}},
|
||
{"conf<=70 + invented + conflicts + unresolved", pipeline.StopContestOpts{UseConf: true, MaxConf: 70, CountInvented: true, CountConflicts: true, CountUnresolved: true}},
|
||
{"conf<=70 + conv>=2", pipeline.StopContestOpts{UseConf: true, MaxConf: 70, CountPreCall: true, MinConventions: 2}},
|
||
}
|
||
fmt.Printf(" %-46s %s\n", "signals", strings.Join(order, " "))
|
||
for _, a := range arms {
|
||
var cells []string
|
||
for _, name := range order {
|
||
s := by[name]
|
||
_, ans, _, _ := s.denominators()
|
||
n := 0
|
||
for _, r := range s.rows {
|
||
if r.Contest(a.opts).Contested {
|
||
n++
|
||
}
|
||
}
|
||
cells = append(cells, fmt.Sprintf("%3d/%3d = %5.1f%%", n, ans, pct(n, ans)))
|
||
}
|
||
fmt.Printf(" %-46s %s\n", a.label, strings.Join(cells, " "))
|
||
}
|
||
fmt.Printf(" (the selected count IS the answer to «how many rows would go to a second call»)\n")
|
||
}
|
||
|
||
// printComparison is the §4.4 instrument: two purchases of the SAME material, joined by surface, asking
|
||
// what actually differed in the role's INPUT where its decision differed.
|
||
func printComparison(a, b *source) {
|
||
fmt.Printf("\n== TWO PURCHASES COMPARED: %s ↔ %s\n", a.name, b.name)
|
||
ai, bi := index(a.rows), index(b.rows)
|
||
var shared []string
|
||
for src := range ai {
|
||
if _, ok := bi[src]; ok {
|
||
shared = append(shared, src)
|
||
}
|
||
}
|
||
sort.Strings(shared)
|
||
fmt.Printf(" surfaces: %s=%d %s=%d shared=%d | only in %s=%d only in %s=%d\n",
|
||
a.name, len(ai), b.name, len(bi), len(shared), a.name, len(ai)-len(shared), b.name, len(bi)-len(shared))
|
||
var dstChanged, sameEvidence, evidenceChanged int
|
||
var examples []string
|
||
for _, src := range shared {
|
||
x, y := ai[src], bi[src]
|
||
if x.Dst == y.Dst {
|
||
continue
|
||
}
|
||
dstChanged++
|
||
same, diffs := sameInput(x, y)
|
||
if same {
|
||
sameEvidence++
|
||
if len(examples) < 8 {
|
||
examples = append(examples, fmt.Sprintf("%s: %q → %q (input identical in every field the artifact carries)", src, x.Dst, y.Dst))
|
||
}
|
||
continue
|
||
}
|
||
evidenceChanged++
|
||
if len(examples) < 8 {
|
||
examples = append(examples, fmt.Sprintf("%s: %q → %q (%s)", src, x.Dst, y.Dst, strings.Join(diffs, ", ")))
|
||
}
|
||
}
|
||
fmt.Printf(" rows whose rendering CHANGED between the purchases: %d of %d shared\n", dstChanged, len(shared))
|
||
fmt.Printf(" of those, the artifact's input fields were IDENTICAL: %d\n", sameEvidence)
|
||
fmt.Printf(" of those, some input field differed: %d\n", evidenceChanged)
|
||
for _, e := range examples {
|
||
fmt.Printf(" · %s\n", e)
|
||
}
|
||
fmt.Printf(" ⚠ LIMIT OF THIS INSTRUMENT, and it is not small: «input» here is what the STOP PROJECTION\n")
|
||
fmt.Printf(" carries — freq, type, the draft variants and their chunk counts. The request the role was\n")
|
||
fmt.Printf(" actually sent also carried the KWIC windows and the canon anchor, and neither is in any\n")
|
||
fmt.Printf(" artifact. «Identical here» therefore means «identical in the evidence that was kept», which\n")
|
||
fmt.Printf(" is a weaker statement than «the role saw the same request».\n")
|
||
}
|
||
|
||
func index(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
|
||
}
|
||
|
||
// sameInput compares everything about a row that was an INPUT to the role — never the answer.
|
||
func sameInput(x, y pipeline.BankStopRow) (bool, []string) {
|
||
var diffs []string
|
||
if x.Freq != y.Freq {
|
||
diffs = append(diffs, fmt.Sprintf("freq %d→%d", x.Freq, y.Freq))
|
||
}
|
||
if x.Type != y.Type {
|
||
diffs = append(diffs, fmt.Sprintf("type %s→%s", x.Type, y.Type))
|
||
}
|
||
if x.Origin != y.Origin {
|
||
diffs = append(diffs, fmt.Sprintf("channel %s→%s", x.Origin, y.Origin))
|
||
}
|
||
if x.Conventions != y.Conventions {
|
||
diffs = append(diffs, fmt.Sprintf("conventions %d→%d", x.Conventions, y.Conventions))
|
||
}
|
||
if vx, vy := variantKey(x), variantKey(y); vx != vy {
|
||
diffs = append(diffs, fmt.Sprintf("draft variants %s → %s", vx, vy))
|
||
}
|
||
return len(diffs) == 0, diffs
|
||
}
|
||
|
||
func variantKey(r pipeline.BankStopRow) string {
|
||
parts := make([]string, 0, len(r.Variants))
|
||
for _, v := range r.Variants {
|
||
parts = append(parts, fmt.Sprintf("%s×%d", v.Dst, v.Chunks))
|
||
}
|
||
sort.Strings(parts)
|
||
return "[" + strings.Join(parts, " | ") + "]"
|
||
}
|
||
|
||
// printDBControls reads the counts that say what state the database was in — the ones that make a replay
|
||
// honest or misleading. Read-only AND immutable, so the evidence is not even touched by an -shm.
|
||
func printDBControls(name, path string) error {
|
||
db, err := sql.Open("sqlite", "file:"+path+"?mode=ro&immutable=1")
|
||
if err != nil {
|
||
return err
|
||
}
|
||
defer db.Close()
|
||
fmt.Printf("\n== %s %s (read-only, immutable)\n", name, path)
|
||
var schema int
|
||
if err := db.QueryRow(`SELECT COALESCE(MAX(version),0) FROM schema_version`).Scan(&schema); err != nil {
|
||
fmt.Printf(" schema_version: unreadable (%v)\n", err)
|
||
} else {
|
||
// ⛔ PRINTED BESIDE THIS BINARY'S OWN HEAD, because the difference is not cosmetic: store.Open() on a
|
||
// database BELOW the head MIGRATES it (migrate.go, the beforeApply branch), and these files are bought
|
||
// evidence — a replay through the engine's writer would rewrite the schema of the very run it came to
|
||
// measure. store.OpenReadOnly refuses the mismatch outright, which is why this probe reads the file
|
||
// itself, immutably, instead of asking the engine for a reader it cannot give.
|
||
fmt.Printf(" schema_version: %d (this binary's head: %d — %s)\n", schema, store.SchemaHead(),
|
||
map[bool]string{true: "equal, the engine could read it", false: "DIFFERENT: the engine's read-only door refuses it, and a WRITE open would migrate the evidence"}[schema == store.SchemaHead()])
|
||
}
|
||
var tables int
|
||
if err := db.QueryRow(`SELECT COUNT(*) FROM sqlite_master WHERE type='table'`).Scan(&tables); err == nil {
|
||
fmt.Printf(" tables: %d\n", tables)
|
||
}
|
||
for _, t := range []string{"glossary", "bank_stop_presented", "request_log", "checkpoints"} {
|
||
var n int
|
||
if err := db.QueryRow("SELECT COUNT(*) FROM " + t).Scan(&n); err != nil {
|
||
fmt.Printf(" %-20s NO SUCH TABLE IN THIS DATABASE (%v)\n", t, err)
|
||
continue
|
||
}
|
||
fmt.Printf(" %-20s %d\n", t, n)
|
||
}
|
||
rows, err := db.Query(`SELECT role, COUNT(*), SUM(prompt_tokens=0 AND completion_tokens=0) FROM request_log GROUP BY role ORDER BY role`)
|
||
if err != nil {
|
||
return nil
|
||
}
|
||
defer rows.Close()
|
||
for rows.Next() {
|
||
var role string
|
||
var n, replayed int
|
||
if err := rows.Scan(&role, &n, &replayed); err != nil {
|
||
return err
|
||
}
|
||
fmt.Printf(" request_log role=%-14s calls=%d of which recorded with zero tokens (replayed/unbilled)=%d\n", role, n, replayed)
|
||
}
|
||
return rows.Err()
|
||
}
|
||
|
||
func pct(n, d int) float64 {
|
||
if d == 0 {
|
||
return 0
|
||
}
|
||
return 100 * float64(n) / float64(d)
|
||
}
|
||
|
||
func first(s []string, n int) []string {
|
||
if len(s) <= n {
|
||
return s
|
||
}
|
||
return s[:n]
|
||
}
|
||
|
||
func compactJSON(v any) string {
|
||
b, err := json.Marshal(v)
|
||
if err != nil {
|
||
return fmt.Sprintf("%v", v)
|
||
}
|
||
return string(b)
|
||
}
|