textmachine/backend/internal/pipeline/bankbasis_m1_probe_test.go

881 lines
40 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 pipeline
import (
"context"
"fmt"
"os"
"path/filepath"
"sort"
"strings"
"testing"
"textmachine/backend/internal/chunk"
"textmachine/backend/internal/miner"
"textmachine/backend/internal/obs"
"textmachine/backend/internal/store"
"textmachine/backend/internal/terminology"
"textmachine/backend/internal/text"
)
// bankbasis_m1_probe_test.go: measurement M1 — an opt-in INSTRUMENT over BOUGHT material, not a unit test.
// It answers the question the settled basis has to be designed against before a carrier is written: on the
// runs this project actually paid for, how many candidates would the predicate serve from memory, and for
// which reasons would the rest be bought again.
//
// ⛔ $0. Nothing here reaches a provider. The runner is opened on the READ path (openRunner forWrite=false),
// which skips models.CheckKeys entirely, and the only engine calls made are the $0 assembly ones — ingest,
// split, mine, merge, KWIC, score. runTerminologist, runClassifier and TranslateBook are never called.
//
// HOW TO PREPARE THE MATERIAL (the instrument refuses to guess, and every path is named):
//
// cp -a /home/ubuntu/tm-coldrun-a/stand/books/bk_ROEHZBD46ALFI43E /tmp/m1/A
// cp -a /home/ubuntu/tm-coldrun-b/stand/books/bk_NH6275ZIRFFTLZHG /tmp/m1/B
// rm -f /tmp/m1/*/project.db.lock
// sed -i 's#/home/ubuntu-26/#/home/ubuntu/#g' /tmp/m1/*/book.yaml
//
// # ⚠ RUN A NEEDS ONE MORE STEP, and without it the instrument FAILS on A while passing on B — which
// # reads as a defect of the pack and is a gap in the ARCHIVE. A's pipeline resolves its role prompts
// # relative to its own directory (`../prompts/zh-ru/…`) and that directory was not archived beside the
// # config mirror; B's was. The prompts themselves are in the freeze and are byte-identical to B's
// # archived copy (`diff -rq` empty, 11 files against 11), and they touch no number here — the
// # instrument makes no calls — so assembling them is restoring the archive, not choosing an input:
// mkdir -p /tmp/m1/A/cfg && cp -a /home/ubuntu/tm-coldrun-a/mirror/cfg/. /tmp/m1/A/cfg/
// cp -a /home/ubuntu/tm-coldrun-a/freeze/backend/prompts /tmp/m1/A/prompts
// sed -i 's#^pipeline: .*#pipeline: /tmp/m1/A/cfg/pipeline-c1.yaml#' /tmp/m1/A/book.yaml
//
// TM_BASIS_M1=/tmp/m1/A,/tmp/m1/B go test ./internal/pipeline/ -run TestProbeBankBasisM1 -v
//
// ⚠ COPIES, ALWAYS. The material is bought and unrecoverable, and run A carries store schema 16 against a
// head of 18: OpenReadOnly REFUSES it, so the instrument migrates the copy with store.Open first (v17/v18
// are additive — CREATE TABLE IF NOT EXISTS and ADD COLUMN, no DROP/RENAME/DELETE). Run this against an
// original and the original is migrated in place.
//
// ⚠ THE book.yaml OF EVERY BOUGHT RUN POINTS AT /home/ubuntu-26, WHICH DOES NOT EXIST HERE. Re-point it on
// the copy, as the recipe does. Leaving it is not a loud failure in the interesting direction: the config
// loader would take the HEAD configs and the instrument would silently measure a different pipeline than
// the one the money was spent under, which is why the run below PRINTS the knobs it resolved.
func TestProbeBankBasisM1(t *testing.T) {
roots := os.Getenv("TM_BASIS_M1")
if roots == "" {
t.Skip("instrument: set TM_BASIS_M1=<copy of a bought run dir>[,<another>] to measure the settled basis (see the file docblock)")
}
for _, dir := range strings.Split(roots, ",") {
dir = strings.TrimSpace(dir)
if dir == "" {
continue
}
t.Run(filepath.Base(dir), func(t *testing.T) { probeBasisOneRun(t, dir) })
}
}
func probeBasisOneRun(t *testing.T, dir string) {
t.Helper()
m := openBasisMaterial(t, dir)
defer m.r.Close()
// ── THE KNOBS THIS MEASUREMENT WAS TAKEN UNDER ──────────────────────────────────────────────────
// Printed, not assumed: «the config of the bought run» and «the config of HEAD, silently substituted»
// produce the same numbers-shaped output, and only these lines tell them apart.
batchRunes, kwicPer, kwicWidth := m.r.terminologyOpts()
t.Logf("KNOBS book=%s pair=%s contrast=%s batch_runes=%d kwic_per_term=%d kwic_width=%d basis_width=%d model=%s",
m.r.Book.BookID, m.r.Book.LangPair(), m.r.Pipeline.Mining.ContrastPath,
batchRunes, kwicPer, kwicWidth, m.r.basisWidth(), m.r.Pipeline.Gates.Terminology.Model)
// ── FIDELITY OF THE RECONSTRUCTION ─────────────────────────────────────────────────────────────
// The instrument rebuilds the candidate list from the source; the run that was paid for left its own
// list behind in the stop table sidecar. If the two disagree, every number below is about a different
// book than the one the money was spent on — so this is asserted, not hoped for.
m.assertMatchesBoughtTable(t)
// ── THE BASELINE: the basis written by this very purchase, read by the next one on unchanged source ─
// ⚠ NOT A CONTROL. Two identical runs agreeing measures the determinism of the harness, not the force
// of the predicate; it is reported as the SAVING, with its denominator, and the discriminating
// controls are below.
base := m.verdicts(m.basisFromRun(), m.nsource)
m.report(t, "BASELINE unchanged-source", base)
m.controlUnitShape(t, base)
m.measureAutoTail(t, base)
m.measureRechunkResidue(t)
m.measureUnitRuleVariants(t)
// ── DISCRIMINATING CONTROL 1: the source moves around ONE occurrence of ONE candidate ──────────────
// Exactly that candidate and its unit must be bought again, and nothing else. A predicate that ignored
// the source would show 0 here; one that hashed too much would show the whole book.
m.controlOneOccurrenceMoved(t, base)
// ── DISCRIMINATING CONTROL 2: two passes under ONE basis produce byte-identical batches ────────────
m.controlBatchesStable(t, base)
// ── KNOB INVARIANCE: the window SELECTION must not move the settled set ────────────────────────────
// This is the property the whole fingerprint shape exists for: the day a stratified selection lands,
// or anyone tunes how much context the model is shown, no book may lose its basis.
m.controlKnobsDoNotMatter(t, base)
// ── THE GROWING BOOK, AS A NUMBER RATHER THAN AS A GUARD ───────────────────────────────────────────
// The premise «the source arrives whole» is an OPEN question to the owner, so the engine must not
// answer it with a watchdog. What it can do is say what growth costs.
m.measureGrowth(t)
}
// ── material ────────────────────────────────────────────────────────────────────────────────────────
// basisMaterial is one bought run, reconstructed far enough to ask the predicate a question.
type basisMaterial struct {
dir string
r *Runner
doc *chunk.Document
chunks []chunk.Chunk
nsource string // the book's normalized source — the one definition, r.bankSrc's own
cands []terminology.Candidate // as the stop would have built them, before any paid call
bank []store.GlossaryEntry // the bank as the bought run left it
rows map[string]store.GlossaryEntry
ambiguous map[string]bool
// consider is the candidates the BANK filter kept — the only population production offers the basis.
consider map[string]bool
bankSettled int
unitID map[string]int
width int
}
func openBasisMaterial(t *testing.T, dir string) *basisMaterial {
t.Helper()
bookPath := filepath.Join(dir, "book.yaml")
if _, err := os.Stat(bookPath); err != nil {
t.Fatalf("material: %s has no book.yaml — point TM_BASIS_M1 at a COPY of a bought run dir: %v", dir, err)
}
// Migrate the copy first: run A is schema 16 and the read door refuses it outright (backlog row 475).
// Done explicitly rather than by opening for write, so nothing here takes the run lock or a key check.
if st, err := store.Open(filepath.Join(dir, "project.db")); err != nil {
t.Fatalf("material: could not open/migrate the COPY at %s: %v", dir, err)
} else {
st.Close()
}
r, err := openRunner(bookPath, obs.NewLogger(), false)
if err != nil {
t.Fatalf("material: %s does not load — did you re-point its /home/ubuntu-26 paths? %v", dir, err)
}
if r.pack == nil {
t.Fatalf("material: %s loaded no langpack for pair %q, so the miner cannot run and every count below would be a zero about the instrument", dir, r.Book.LangPair())
}
m := &basisMaterial{dir: dir, r: r, width: r.basisWidth()}
m.build(t)
return m
}
// build runs the $0 half of the bank-mining stop — the same sequence runBankMiningStop takes before its
// first paid call, in the same order and through the same functions.
func (m *basisMaterial) build(t *testing.T) {
t.Helper()
doc, err := m.r.ingestSource()
if err != nil {
t.Fatalf("ingest %s: %v", m.dir, err)
}
m.doc = doc
m.chunks, _, _ = chunk.SplitChunksWithChapters(doc.Chapters, m.r.segBudget(), m.r.chapterRule(), m.r.sentenceAbbrevs())
if len(m.chunks) == 0 {
t.Fatalf("premise broken: %s produced no chunks", m.dir)
}
m.r.bankSrc = newBankSourceIndex(m.chunks)
m.nsource = m.r.bankSrc.book
m.bank, err = m.r.Store.GlossaryForBook(m.r.Book.BookID)
if err != nil {
t.Fatalf("read the bank of %s: %v", m.dir, err)
}
if len(m.bank) == 0 {
t.Fatalf("premise broken: %s holds an EMPTY bank, so every «settled» count below would be zero for a reason that has nothing to do with the predicate", m.dir)
}
m.rows, m.ambiguous = bankBasisRows(m.bank, text.NormalizeSourceKey)
m.cands = m.candidatesOver(t, m.chunks)
m.unitID = bankBasisUnits(m.cands, basisSeriesParams(m.r.Book.SourceLang), m.r.familyParams)
// ⛔ THE INSTRUMENT ASKS THE PREDICATE EXACTLY WHAT THE ENGINE ASKS IT, AND NOT ONE CANDIDATE MORE.
// Production offers the basis only the candidates the BANK filter kept: a surface the seed renders and
// every draft agreed with is a different saving with a different sentence on the sheet, and offering it
// to both is the double-marking this pack had to fix. An instrument that measured over ALL candidates
// would report a saving the engine never claims — a number about a question nobody asks.
kept, dropped := m.r.dropBankSettled(context.Background(), m.cands)
m.consider = map[string]bool{}
for _, i := range kept {
m.consider[m.cands[i].Key] = true
}
m.bankSettled = dropped
}
// candidatesOver rebuilds the merged candidate list over a given chunk set — parameterised so the growth
// and re-chunk measurements can ask the same question of a different source.
func (m *basisMaterial) candidatesOver(t *testing.T, chunks []chunk.Chunk) []terminology.Candidate {
t.Helper()
f, err := os.Open(m.r.Pipeline.Mining.ContrastPath)
if err != nil {
t.Fatalf("open the mining contrast %s: %v", m.r.Pipeline.Mining.ContrastPath, err)
}
defer f.Close()
contrast, err := miner.LoadContrast(f)
if err != nil {
t.Fatalf("load the mining contrast: %v", err)
}
minerChunks := make([]miner.Chunk, len(chunks))
tchunks := make([]terminology.Chunk, len(chunks))
for i, ch := range chunks {
n := text.NormalizeSourceKey(ch.Text)
minerChunks[i] = miner.Chunk{Chapter: ch.Chapter, ChunkIdx: ch.ChunkIdx, NSource: n}
tchunks[i] = terminology.Chunk{Chapter: ch.Chapter, ChunkIdx: ch.ChunkIdx, NSource: n}
}
rejects, err := m.r.loadMinedRejects()
if err != nil {
t.Fatalf("load the mined rejects: %v", err)
}
mined, _ := miner.MineBankStats(minerChunks, contrast, unsignedEngineSurfaces(m.bank), rejects, miner.FrozenConfig(), m.r.pack)
observed, _, err := m.r.bankObservedForBook()
if err != nil {
t.Fatalf("read the banknote proposals: %v", err)
}
return m.r.buildBankCandidates(mined, observed, tchunks)
}
// ── fidelity ────────────────────────────────────────────────────────────────────────────────────────
// assertMatchesBoughtTable compares the reconstructed candidate list against the one the PAID run wrote to
// its stop-table sidecar.
//
// ⛔ WHY THIS IS AN ASSERT AND NOT A LOG LINE. Every number this file prints is a statement about the runs
// the project paid for. Reconstructed from a source with the wrong chunker, the wrong contrast or the HEAD
// config, the instrument still prints a full, plausible, well-formed table — about a book nobody bought.
// The sidecar is the bought run's own account of which candidates existed, so agreeing with it is the one
// cheap proof that the subject here is the subject there.
func (m *basisMaterial) assertMatchesBoughtTable(t *testing.T) {
t.Helper()
raw, err := os.ReadFile(m.r.bankStopTablePath())
if err != nil {
t.Fatalf("fidelity: %s has no bought stop table beside its DB, so the reconstruction cannot be checked against anything: %v", m.dir, err)
}
bought := map[string]bool{}
for _, ln := range strings.Split(string(raw), "\n") {
// One term opens a block with «src<TAB>dst»; every other line of the block is indented.
if ln == "" || strings.HasPrefix(ln, " ") || !strings.Contains(ln, "\t") {
continue
}
bought[text.NormalizeSourceKey(strings.SplitN(ln, "\t", 2)[0])] = true
}
if len(bought) == 0 {
t.Fatalf("fidelity: parsed ZERO terms out of the bought stop table %s (%d bytes) — the parser, not the book", m.r.bankStopTablePath(), len(raw))
}
got := map[string]bool{}
for _, c := range m.cands {
got[c.Key] = true
}
missing, extra := diffKeys(bought, got), diffKeys(got, bought)
t.Logf("FIDELITY bought_table_terms=%d reconstructed_candidates=%d missing=%d extra=%d",
len(bought), len(got), len(missing), len(extra))
if len(missing) > 0 || len(extra) > 0 {
t.Fatalf("fidelity: the reconstruction is NOT the bought candidate set — every number below would be about another book.\n in the bought table, not reconstructed (%d): %s\n reconstructed, not in the bought table (%d): %s",
len(missing), strings.Join(capList(missing, 12), ", "), len(extra), strings.Join(capList(extra, 12), ", "))
}
}
// ── the predicate ───────────────────────────────────────────────────────────────────────────────────
// basisFromRun is the memory the bought purchase WOULD have written at its boundary: for every candidate
// the bank holds as a decided engine row, the answer it holds and the fingerprint of the evidence as it
// stands now.
//
// ⚠ THE FINGERPRINT IS TAKEN ON THE PRE-ROLE CANDIDATES on purpose, and the production writer must do the
// same: the classifier pass MUTATES Candidate.Type in place (applyTypes), so a fingerprint computed after
// the roles have run would hash the CLASSIFIER's type — and then learning whether the terminologist may be
// skipped would require buying the classifier first, every purchase, which is the cost the basis exists
// to remove.
func (m *basisMaterial) basisFromRun() map[string]bankBasisRecord {
canon := approvedNeighbours(m.bank)
out := make(map[string]bankBasisRecord, len(m.cands))
for _, c := range m.cands {
row, ok := m.rows[c.Key]
if !ok || row.Status != "draft" || strings.TrimSpace(row.Dst) == "" {
continue // the purchase settled nothing about this surface, so it writes no record
}
out[c.Key] = bankBasisRecord{
Key: c.Key,
FP: candidateBasisFingerprint(c, m.nsource, canon, m.width),
Answer: bankBasisAnswer{Dst: row.Dst, Type: row.Type, Gender: row.Gender},
}
}
return out
}
// verdicts runs the predicate over the current candidates against a given basis and source.
func (m *basisMaterial) verdicts(basis map[string]bankBasisRecord, nsource string) map[string]bankBasisVerdict {
canon := approvedNeighbours(m.bank)
fps := make(map[string]bankBasisFP, len(m.cands))
for _, c := range m.cands {
fps[c.Key] = candidateBasisFingerprint(c, nsource, canon, m.width)
}
return m.scoped(bankBasisSettle(m.cands, fps, basis, m.rows, m.ambiguous, m.unitID, "s"))
}
// scoped drops the candidates the bank filter already took, so every count below is about the population
// the engine actually offers the basis.
func (m *basisMaterial) scoped(v map[string]bankBasisVerdict) map[string]bankBasisVerdict {
out := make(map[string]bankBasisVerdict, len(v))
for k, verdict := range v {
if !m.consider[k] {
continue
}
out[k] = verdict
}
return out
}
// report prints the counts with every denominator beside them.
func (m *basisMaterial) report(t *testing.T, label string, v map[string]bankBasisVerdict) {
t.Helper()
served, byReason := countBasisReasons(v)
parts := make([]string, 0, len(byReason))
for _, r := range byReason {
parts = append(parts, fmt.Sprintf("%s=%d", r.Reason, r.N))
}
signed := 0
for _, e := range m.bank {
if e.Status == "approved" {
signed++
}
}
t.Logf("%s candidates=%d offered_to_the_basis=%d (bank filter took %d) bank_rows=%d signed_rows=%d | SERVED=%d re-asked=%d | %s",
label, len(m.cands), len(m.consider), m.bankSettled, len(m.bank), signed, served, len(m.consider)-served, strings.Join(parts, " "))
}
// ── controls ────────────────────────────────────────────────────────────────────────────────────────
// controlOneOccurrenceMoved is the first discriminating control: the source moves around ONE occurrence of
// ONE settled candidate, and the set bought again afterwards must be EXACTLY the set the edit can reach.
//
// ⛔ WHAT «EXACTLY» MEANS HERE, and why the obvious formulation is wrong. The first version of this control
// asserted «the victim and its unit, and nobody else», and it failed on real material — correctly. An edit
// is a point in the text, and it lands inside the evidence window of every surface whose occurrence is
// near it, including surfaces the victim CONTAINS (三转蛊师 contains 蛊师) and neighbours a few runes away.
// Re-asking those is not coupling, it is the mechanism working: their evidence really did change. Asserting
// otherwise would have forced the fingerprint to read LESS of the source than it must.
//
// So the assertion is a CROSS-CHECK between two independent routes to the same set: the predicate's verdict
// (a hash over evidence + canon + identity) against a direct positional walk of the windows alone. They
// agree only if nothing except the windows contributed to the change — which is the property that matters,
// and which the naive formulation could not see at all.
func (m *basisMaterial) controlOneOccurrenceMoved(t *testing.T, base map[string]bankBasisVerdict) {
t.Helper()
var victim terminology.Candidate
for _, c := range m.cands {
if base[c.Key].Reason == basisServed && strings.Contains(m.nsource, c.Key) {
victim = c
break
}
}
if victim.Key == "" {
t.Fatalf("control 1: no served candidate occurs in the source, so this control would pass without measuring anything")
}
at := strings.Index(m.nsource, victim.Key)
moved := m.nsource[:at] + "ЗЗЗ" + m.nsource[at:]
// ROUTE A — the direct positional truth: whose windows actually moved. Uses no hash, no basis, no unit.
touched := map[string]bool{}
for _, c := range m.cands {
if !equalStrings(terminology.EvidenceWindows(m.nsource, c.Key, m.width),
terminology.EvidenceWindows(moved, c.Key, m.width)) {
touched[c.Key] = true
}
}
if !touched[victim.Key] {
t.Fatalf("control 1: the edit did not even move the windows of its own victim %q — the fixture, not the predicate", victim.Src)
}
if len(touched) == len(m.cands) {
t.Fatalf("control 1: a single edit moved the windows of EVERY candidate (%d) — the control cannot separate the classes on this material", len(m.cands))
}
// …expanded by the unit rule, which is the predicate's own promise: a family is decided whole.
hotUnits := map[int]bool{}
for k := range touched {
hotUnits[m.unitID[k]] = true
}
expected := map[string]bool{}
for _, c := range m.cands {
if hotUnits[m.unitID[c.Key]] && base[c.Key].Reason == basisServed {
expected[c.Key] = true
}
}
// ROUTE B — the predicate.
after := m.verdicts(m.basisFromRun(), moved)
observed := map[string]bool{}
for _, c := range m.cands {
if base[c.Key].Reason == basisServed && after[c.Key].Reason != basisServed {
observed[c.Key] = true
}
}
missing, extra := diffKeys(expected, observed), diffKeys(observed, expected)
t.Logf("CONTROL-1 one-occurrence-moved victim=%q windows_moved=%d expected_re-asked=%d observed_re-asked=%d missing=%d extra=%d | %s",
victim.Src, len(touched), len(expected), len(observed), len(missing), len(extra),
strings.Join(capList(sortedSet(observed), 8), ", "))
if len(observed) == 0 {
t.Fatalf("control 1: moving the source around %q re-asked NOTHING — the fingerprint does not read the source, and the whole predicate is blind", victim.Src)
}
if len(missing) > 0 || len(extra) > 0 {
t.Fatalf("control 1: the predicate's re-ask set is NOT the set the edit can reach.\n windows moved, predicate kept serving (%d): %s\n predicate re-asked, windows did not move (%d): %s",
len(missing), strings.Join(capList(missing, 10), ", "), len(extra), strings.Join(capList(extra, 10), ", "))
}
}
// controlUnitShape is not a pass/fail control but the measurement the unit rule has to be judged by: how
// much of the saving the «a family is decided whole» rule costs, and whether the cost comes from the rule
// or from the unit DETECTION being run over a different population than the batcher's.
//
// ⚠ IT PRINTS BOTH POPULATIONS ON PURPOSE. The predicate detects units over ALL candidates (bankBasisUnits)
// while the batcher detects them over the PAID subset; that difference is a decision of this pack, and a
// decision whose price is not printed is a decision nobody can review.
func (m *basisMaterial) controlUnitShape(t *testing.T, base map[string]bankBasisVerdict) {
t.Helper()
sizes := map[int]int{}
for _, c := range m.cands {
sizes[m.unitID[c.Key]]++
}
biggest, singles := 0, 0
for _, n := range sizes {
if n > biggest {
biggest = n
}
if n == 1 {
singles++
}
}
// What the answer would be with no unit rule at all — the ceiling the rule is spending against.
own := 0
for _, c := range m.cands {
if base[c.Key].Reason == basisServed || base[c.Key].Reason == basisUnit {
own++
}
}
// Which candidates do the blocking, and why: the operator's question is «who is holding my family open».
blockers := map[basisReason][]string{}
blocked := map[int]bool{}
for _, c := range m.cands {
if r := base[c.Key].Reason; r != basisServed && r != basisUnit {
blocked[m.unitID[c.Key]] = true
blockers[r] = append(blockers[r], c.Src)
}
}
t.Logf("UNIT-SHAPE units=%d singletons=%d largest_unit=%d | settled_on_their_own=%d served_after_unit_rule=%d cost_of_the_rule=%d | units_blocked=%d",
len(sizes), singles, biggest, own, countServed(base), own-countServed(base), len(blocked))
for _, r := range basisReasonNames {
if names := blockers[r]; len(names) > 0 {
sort.Strings(names)
t.Logf("UNIT-SHAPE blocked by %-16s n=%d | %s", r, len(names), strings.Join(capList(names, 10), ", "))
}
}
}
// controlBatchesStable is the second discriminating control: with one basis, two passes build byte-identical
// batches. This is what makes a resume replay for $0 — a bank batch is addressed by the hash of its request,
// so a composition that differs between passes finds no checkpoint and buys the pass again.
func (m *basisMaterial) controlBatchesStable(t *testing.T, base map[string]bankBasisVerdict) {
t.Helper()
batchRunes, _, _ := m.r.terminologyOpts()
render := func() string {
var paid []terminology.Candidate
for _, c := range m.cands {
if base[c.Key].Reason != basisServed {
paid = append(paid, c)
}
}
// The batcher exactly as production runs it: units detected over the PAID subset, not over all
// candidates. The predicate uses its own unit pass (bankBasisUnits) and must not have moved this.
seriesID := terminology.DetectSeries(paid, basisSeriesParams(m.r.Book.SourceLang))
fams := terminology.DetectFamilies(paid, m.r.familyParams)
unitID, _ := terminology.MergeUnits(seriesID, fams, m.r.familyParams)
var b strings.Builder
for i, batch := range terminology.Batch(paid, batchRunes, unitID) {
fmt.Fprintf(&b, "#%d\n%s\n", i, terminology.RenderBatch(batch))
}
return b.String()
}
first, second := render(), render()
t.Logf("CONTROL-2 batches-under-one-basis paid_candidates=%d rendered_bytes=%d identical=%v",
len(m.cands)-countServed(base), len(first), first == second)
if first != second {
t.Fatalf("control 2: two passes under ONE basis rendered DIFFERENT batches — every resume would re-buy the pass")
}
if len(first) == 0 {
t.Fatalf("control 2: the batcher rendered ZERO bytes, so identical-to-itself proves nothing")
}
}
// controlKnobsDoNotMatter is the property the fingerprint shape exists for: turning the knobs that decide
// WHICH contexts the model is shown must not move the settled set.
//
// ⛔ THE ONE KNOB THAT IS ALLOWED TO MOVE IT is basis_width, and that is stated here rather than discovered:
// it is the pair's statement of what counts as the same evidence, so changing it IS a change of the
// question. Every other window knob — how many contexts, how wide the model's own view — is prompt shape.
func (m *basisMaterial) controlKnobsDoNotMatter(t *testing.T, base map[string]bankBasisVerdict) {
t.Helper()
basis := m.basisFromRun()
for _, knob := range []struct {
name string
kwicPer int
kwicWidth int
basisWidth int
mustNotChange bool
}{
{"kwic_per_term 3→1", 1, 0, 0, true},
{"kwic_per_term 3→40", 40, 0, 0, true},
{"kwic_width 40→120", 0, 120, 0, true},
{"basis_width 40→80 (ALLOWED to move it)", 0, 0, 80, false},
} {
_, per, wid := m.r.terminologyOpts()
if knob.kwicPer > 0 {
per = knob.kwicPer
}
if knob.kwicWidth > 0 {
wid = knob.kwicWidth
}
// Rebuild the candidates with the knob turned: KWIC is attached during the merge, so a knob that
// leaks into the fingerprint can only be caught by rebuilding, never by re-hashing the same slice.
turned := m.rebuiltWithKWIC(t, per, wid)
width := m.width
if knob.basisWidth > 0 {
width = knob.basisWidth
}
canon := approvedNeighbours(m.bank)
fps := make(map[string]bankBasisFP, len(turned))
for _, c := range turned {
fps[c.Key] = candidateBasisFingerprint(c, m.nsource, canon, width)
}
after := m.scoped(bankBasisSettle(turned, fps, basis, m.rows, m.ambiguous, m.unitID, "s"))
lost := 0
for _, c := range m.cands {
if base[c.Key].Reason == basisServed && after[c.Key].Reason != basisServed {
lost++
}
}
t.Logf("CONTROL-3 knob=%-40s served_before=%d lost_from_settled=%d", knob.name, countServed(base), lost)
if knob.mustNotChange && lost > 0 {
t.Fatalf("control 3: turning %s lost %d settled candidate(s) — a prompt-shape knob is re-buying banks", knob.name, lost)
}
if !knob.mustNotChange && lost == 0 {
t.Fatalf("control 3: turning %s lost NOTHING, so this row proves the instrument cannot see the class at all", knob.name)
}
}
}
// rebuiltWithKWIC re-attaches KWIC at other settings over the same merged candidates.
func (m *basisMaterial) rebuiltWithKWIC(t *testing.T, per, width int) []terminology.Candidate {
t.Helper()
tchunks := make([]terminology.Chunk, len(m.chunks))
for i, ch := range m.chunks {
tchunks[i] = terminology.Chunk{Chapter: ch.Chapter, ChunkIdx: ch.ChunkIdx, NSource: text.NormalizeSourceKey(ch.Text)}
}
out := make([]terminology.Candidate, len(m.cands))
copy(out, m.cands)
for i := range out {
out[i].KWIC = nil
}
return terminology.AttachKWIC(out, tchunks, per, width)
}
// measureGrowth answers the growing-book question with a number instead of a watchdog: decide the bank on a
// PREFIX of the book, then let the book grow, and count how many of the decided rows the predicate buys
// again.
//
// ⚠ THIS IS THE COST OF THE DESIGN, NOT A DEFECT OF IT. A fingerprint over every occurrence necessarily
// moves when new chapters add occurrences of an old term — and that is the honest reading: a term the new
// chapters talk about HAS new evidence. The number below is what the owner needs in order to answer whether
// a growing serial wants a different rule, and it is the one thing the engine must not decide for them.
func (m *basisMaterial) measureGrowth(t *testing.T) {
t.Helper()
chapters := map[int]bool{}
for _, ch := range m.chunks {
chapters[ch.Chapter] = true
}
if len(chapters) < 2 {
t.Logf("GROWTH skipped: the book has %d chapter(s), so a prefix cannot be formed — the class is unmeasured here, not absent", len(chapters))
return
}
last := 0
for c := range chapters {
if c > last {
last = c
}
}
var prefix []chunk.Chunk
for _, ch := range m.chunks {
if ch.Chapter < last {
prefix = append(prefix, ch)
}
}
prefixSrc := newBankSourceIndex(prefix).book
// ⛔ THE PREFIX HAS TO BE SMALLER, AND IT IS ASSERTED RATHER THAN ASSUMED. A chapter rule that did not
// fire leaves every chunk in «chapter 0», the prefix then equals the whole book, and the measurement
// below prints a beautifully plausible «growth costs nothing» — a right-looking number from a method
// that never grew anything.
if len(prefix) == 0 || len(prefix) == len(m.chunks) {
t.Fatalf("growth: the prefix is %d chunk(s) of %d — the chapter split did not separate a prefix, so nothing below would be about growth", len(prefix), len(m.chunks))
}
prefixBytes, wholeBytes := len(prefixSrc), len(m.nsource)
prefixCands := m.candidatesOver(t, prefix)
canon := approvedNeighbours(m.bank)
// The basis as the PREFIX purchase would have written it.
basis := map[string]bankBasisRecord{}
for _, c := range prefixCands {
row, ok := m.rows[c.Key]
if !ok || row.Status != "draft" || strings.TrimSpace(row.Dst) == "" {
continue
}
basis[c.Key] = bankBasisRecord{Key: c.Key,
FP: candidateBasisFingerprint(c, prefixSrc, canon, m.width),
Answer: bankBasisAnswer{Dst: row.Dst, Type: row.Type, Gender: row.Gender}}
}
// The next purchase: the whole book, the prefix's basis.
after := m.verdicts(basis, m.nsource)
kept, reasked := 0, 0
for key := range basis {
if after[key].Reason == basisServed {
kept++
} else {
reasked++
}
}
// The breakdown matters more than the total: a decision re-asked because ITS OWN evidence grew is the
// mechanism working, while one re-asked because a 44-member unit holds a single grown member is the
// UNIT RULE's bill, and the two have entirely different cures.
own, byUnit := 0, 0
for key := range basis {
switch after[key].Reason {
case basisServed:
case basisUnit:
byUnit++
default:
own++
}
}
t.Logf("GROWTH chapters=%d prefix_chapters=%d prefix_chunks=%d/%d prefix_bytes=%d/%d prefix_candidates=%d decided_in_prefix=%d | still_served=%d re-asked=%d (own_evidence_grew=%d held_by_unit=%d)",
len(chapters), len(chapters)-1, len(prefix), len(m.chunks), prefixBytes, wholeBytes,
len(prefixCands), len(basis), kept, reasked, own, byUnit)
if len(basis) == 0 {
t.Fatalf("growth: the prefix purchase decided NOTHING, so both numbers above are zeros about the fixture rather than about growth")
}
}
// ── small helpers ───────────────────────────────────────────────────────────────────────────────────
func countServed(v map[string]bankBasisVerdict) int {
n := 0
for _, x := range v {
if x.Reason == basisServed {
n++
}
}
return n
}
func diffKeys(a, b map[string]bool) []string {
var out []string
for k := range a {
if !b[k] {
out = append(out, k)
}
}
sort.Strings(out)
return out
}
func capList(xs []string, n int) []string {
if len(xs) <= n {
return xs
}
return append(append([]string{}, xs[:n]...), fmt.Sprintf("…(+%d more)", len(xs)-n))
}
func equalStrings(a, b []string) bool {
if len(a) != len(b) {
return false
}
for i := range a {
if a[i] != b[i] {
return false
}
}
return true
}
func sortedSet(m map[string]bool) []string {
out := make([]string, 0, len(m))
for k := range m {
out = append(out, k)
}
sort.Strings(out)
return out
}
// measureUnitRuleVariants prices the «a family is decided whole» rule against the alternatives, so the rule
// is CHOSEN with its cost printed rather than inherited from a sentence.
//
// ⛔ ONE OF THESE ROWS WAS A LIE IN ITS FIRST VERSION, and the correction is the reason the row survives.
// It was labelled «signed rows count as settled» and implemented by handing the predicate a basis RECORD
// for each signed row — which measures nothing, because the predicate then rejects the row on its bank
// STATUS (`approved`, not `draft`) and the unit stays blocked exactly as before. The row printed the same
// number as the row above it and read like a finding. A variant that does not implement its own label is
// not a weak measurement, it is a false one.
//
// What the row asks now is the question that is actually open: a signed row IS still bought today, on
// purpose (the two-condition filter pays for banked surfaces so the sheet keeps its dispute marks, backlog
// row 447) — so should it also HOLD ITS FAMILY OPEN? Being bought and blocking a family are two different
// consequences, and only the second is free to change without touching product behaviour.
func (m *basisMaterial) measureUnitRuleVariants(t *testing.T) {
t.Helper()
basis := m.basisFromRun()
canon := approvedNeighbours(m.bank)
fps := make(map[string]bankBasisFP, len(m.cands))
for _, c := range m.cands {
fps[c.Key] = candidateBasisFingerprint(c, m.nsource, canon, m.width)
}
familyOnly := func() map[string]int {
fams := terminology.DetectFamilies(m.cands, m.r.familyParams)
id := map[string]int{}
for i, f := range fams {
for _, k := range f.Keys {
if _, taken := id[k]; !taken {
id[k] = i + 1
}
}
}
return totalUnits(m.cands, id)
}()
signed := map[string]bool{}
for _, c := range m.cands {
if row, ok := m.rows[c.Key]; ok && row.Status == "approved" && strings.TrimSpace(row.Dst) != "" {
signed[c.Key] = true
}
}
// own-verdicts: the per-candidate half, computed once — every variant differs only in how units block.
own := make(map[string]bankBasisVerdict, len(m.cands))
for _, c := range m.cands {
own[c.Key] = basisVerdictFor(c, fps[c.Key], basis, m.rows, m.ambiguous, "s")
}
// ⛔ basisSigned IS EXEMPT IN EVERY ROW, because production exempts it (bankBasisSettle) and a row
// labelled «the design» that does not implement the design is not a weak measurement, it is a false
// one — the very thing this function's own docblock refuses. Measured before the fix: the row printed
// 62 of 66 where the engine gives 64, so a reader concluded the signature exemption was an unbuilt
// alternative worth +2, when it was already built. Found by adversarial review.
count := func(units map[string]int, exempt map[string]bool) int {
blocked := map[int]bool{}
for _, c := range m.cands {
if r := own[c.Key].Reason; r != basisServed && r != basisSigned && !exempt[c.Key] {
blocked[units[c.Key]] = true
}
}
n := 0
for _, c := range m.cands {
if own[c.Key].Reason == basisServed && !blocked[units[c.Key]] {
n++
}
}
return n
}
none := map[string]int{}
for i, c := range m.cands {
none[c.Key] = i + 1
}
for _, v := range []struct {
name string
units map[string]int
exempt map[string]bool
}{
{"none — no unit rule at all (the ceiling)", none, nil},
{"family-only", familyOnly, nil},
{"merged series+family (THE DESIGN, signature exempt)", m.unitID, nil},
{"merged, a signed row ALSO counted as a hole (pre-17.09 shape)", m.unitID, nil},
} {
n := count(v.units, v.exempt)
t.Logf("UNIT-RULE %-48s SERVED=%2d of %d (%.0f%%)", v.name, n, len(m.cands),
100*float64(n)/float64(len(m.cands)))
}
t.Logf("UNIT-RULE signed rows on this book: %d (%s) — they are BOUGHT either way (backlog row 447); the rows above only ask whether they hold a family open",
len(signed), strings.Join(capList(sortedSet(signed), 6), ", "))
}
// totalUnits turns a PARTIAL unit map into a total one, giving every unlisted candidate a unit of its own.
// Same trap as bankBasisUnits guards, same cure: a missing key reads as unit 0, and unit 0 is a unit.
func totalUnits(cands []terminology.Candidate, partial map[string]int) map[string]int {
out := make(map[string]int, len(cands))
next := 1 << 30
for _, c := range cands {
if id, ok := partial[c.Key]; ok && id != 0 {
out[c.Key] = id
continue
}
next++
out[c.Key] = next
}
return out
}
// measureAutoTail prints the price the consilium named and nobody had put a number on: a surface the model
// steadily declines to render is never SETTLED, so it goes to the paid role on EVERY purchase, on input
// that has not changed by a byte. The basis cannot help it — «no rendering» is not a decision, and storing
// it would let a later purchase serve emptiness back — so this is a standing cost of the mechanism rather
// than a defect of it, and a cost with no number attached is one nobody can decide about.
func (m *basisMaterial) measureAutoTail(t *testing.T, base map[string]bankBasisVerdict) {
t.Helper()
tail, noRow, byStatus := 0, 0, map[string]int{}
for _, c := range m.cands {
row, ok := m.rows[c.Key]
if !ok {
noRow++
continue
}
byStatus[row.Status]++
if row.Status != "approved" && (row.Status != "draft" || strings.TrimSpace(row.Dst) == "") {
tail++
}
}
// ⚠ THE DENOMINATOR AND THE STATUS HISTOGRAM RIDE WITH IT, because a zero here has two readings that
// look identical: «this book has no undecided tail» and «this book's rows are not where the instrument
// looked». The histogram is what separates them.
t.Logf("AUTO-TAIL candidates=%d bank_rows=%d with_no_bank_row=%d | re-asked EVERY purchase on unchanged input=%d | statuses=%v served_now=%d",
len(m.cands), len(m.bank), noRow, tail, byStatus, countServed(base))
}
// measureRechunkResidue puts a NUMBER on the one residue of the fingerprint's shape its author can name but
// could not, at first, measure.
//
// ⚠ THE RESIDUE. The windows are taken from the book's normalized source as the engine holds it —
// r.bankSrc.book, the chunks' normalized texts joined by a newline. That is ONE definition of «the book's
// source», shared with the banknote attestation check, and keeping one definition is worth more than the
// precision it costs. What it costs is this: an occurrence sitting within basis_width runes of a CHUNK JOIN
// carries that join's newline inside its window, so re-chunking the book at another segmentation budget
// moves that window and re-asks the term. A re-cut of CHAPTERS is a named, priced event; a change of the
// chunk budget is not, and «I named the residue but did not measure it» is a blind spot rather than a
// property. This is the measurement.
func (m *basisMaterial) measureRechunkResidue(t *testing.T) {
t.Helper()
base := m.r.segBudget()
canon := approvedNeighbours(m.bank)
for _, factor := range []float64{0.5, 2.0} {
b := base
b.DraftBudgetOut = base.DraftBudgetOut * factor
other, _, _ := chunk.SplitChunksWithChapters(m.doc.Chapters, b, m.r.chapterRule(), m.r.sentenceAbbrevs())
if len(other) == len(m.chunks) {
t.Logf("RECHUNK budget×%.1f produced the SAME %d chunk(s): this factor measures nothing on this book", factor, len(other))
continue
}
src := newBankSourceIndex(other).book
moved, attested := 0, 0
for _, c := range m.cands {
if len(terminology.EvidenceWindows(m.nsource, c.Key, m.width)) > 0 {
attested++
}
if candidateBasisFingerprint(c, m.nsource, canon, m.width).Whole !=
candidateBasisFingerprint(c, src, canon, m.width).Whole {
moved++
}
}
// The denominator and the attested count ride along: a zero over candidates the source does not
// contain would be a zero about the fixture, not about the residue.
t.Logf("RECHUNK budget×%.1f chunks %d→%d bytes %d→%d | fingerprints moved=%d of %d candidates (attested in the source: %d)",
factor, len(m.chunks), len(other), len(m.nsource), len(src), moved, len(m.cands), attested)
}
}