textmachine/backend/internal/pipeline/bankbasis_review_test.go

648 lines
38 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"
"os"
"strings"
"testing"
"textmachine/backend/internal/config"
"textmachine/backend/internal/lang"
"textmachine/backend/internal/store"
"textmachine/backend/internal/terminology"
)
// bankbasis_review_test.go: the pins an ADVERSARIAL pass over this pack's own finished work asked for.
// Every one of them guards a decision the pack had DECLARED and nothing had been asserting — which is the
// state where a declaration is prose, not a guarantee.
// TestTheBankedTypeReachesTheSheetWhenTheBasisAnswers pins the reader the pack's first reader-table missed.
//
// ⛔ THE SHAPE OF THE MISS. The sheet, the stop table and the sidecar's `kind` all read Candidate.Type, and
// applyTypes — the only thing that writes it — runs BEFORE the basis answers reach the maps it reads. So the
// banked type travelled to the auto-bank and not to the sheet: the row the owner signs by said `place` while
// the bank row for the same term said `term`, in the steady state, for as long as the basis kept answering.
func TestTheBankedTypeReachesTheSheetWhenTheBasisAnswers(t *testing.T) {
rec := &reqRec{}
srv := newJSONProvider(rec, func(body string) (string, string) {
if isClassifierBody(body) {
// The heuristic drafted 青茅山 as a place; the classifier says otherwise. THAT is the byte that
// has to survive being answered from memory.
return "青茅山\tterm\tnone", "stop"
}
if isTerminologyBody(body) {
return "青茅山\tгора Цинмао", "stop"
}
return "Фан Юань пришёл." + "\n" + bankBlockForMining, "stop"
})
defer srv.Close()
bookPath := setupMiningStopProject(t, srv.URL, miningStopOpts{terminology: true, classify: true})
r1 := newRunner(t, bookPath)
if _, err := r1.TranslateBook(context.Background()); err != nil {
t.Fatal(err)
}
if r1.lastTerminology.Reclassified != 1 {
t.Fatalf("premise broken: the classifier must CHANGE a type on run 1, or there is nothing for the basis to carry: %+v", r1.lastTerminology)
}
want := sheetTypeOf(t, sheetRowsOf(t, r1), "青茅山")
if want != "term" {
t.Fatalf("premise broken: run 1's sheet must already carry the classifier's type, got %q", want)
}
reclassifiedOnRun1 := r1.lastTerminology.Reclassified
r1.Close()
// Run 2 pays the one-time re-pack; run 3 is the steady state where the basis answers.
for i := 2; i <= 3; i++ {
r := newRunner(t, bookPath)
if _, err := r.TranslateBook(context.Background()); err != nil {
t.Fatalf("run %d: %v", i, err)
}
if r.lastTerminology.BasisServed == 0 {
t.Fatalf("premise broken: run %d must be served by the basis: %+v", i, r.lastTerminology)
}
if got := sheetTypeOf(t, sheetRowsOf(t, r), "青茅山"); got != want {
t.Errorf("run %d: the SHEET says type=%q while the answer the basis carries says %q — the owner signs by a row that disagrees with the bank row for the same term",
i, got, want)
}
// The other half of the same decision: the counter must NOT have absorbed the stamp. `reclassified`
// means «how many types the CLASSIFIER changed», and the classifier saw nothing this run.
if r.lastTerminology.Reclassified != 0 {
t.Errorf("run %d: reclassified=%d, want 0 — the basis's stamp inflated a counter that answers a different question (run 1 legitimately had %d)",
i, r.lastTerminology.Reclassified, reclassifiedOnRun1)
}
r.Close()
}
}
// sheetRowsOf reads the sheet the owner actually gets — the stop-table SIDECAR beside the database, which
// every run writes — rather than r.lastBankStopRows, which is set only on the stopping branch and is empty
// on an auto-continuing run. Reading the shipped artifact also puts its parser under the same assertion.
func sheetRowsOf(t *testing.T, r *Runner) []BankStopRow {
t.Helper()
raw, err := os.ReadFile(r.bankStopTablePath())
if err != nil {
t.Fatalf("every run writes the stop table sidecar; this one did not: %v", err)
}
rows, err := ParseBankStopTable(raw)
if err != nil {
t.Fatalf("parse the stop table the run just wrote: %v", err)
}
if len(rows) == 0 {
t.Fatalf("the stop table holds no rows (%d bytes): every assertion against it would be vacuous", len(raw))
}
return rows
}
func sheetTypeOf(t *testing.T, rows []BankStopRow, src string) string {
t.Helper()
for _, r := range rows {
if r.Src == src {
return r.Type
}
}
t.Fatalf("%q is not on the sheet at all (%d row(s)) — the fixture, not the subject", src, len(rows))
return ""
}
// TestTheOwnersSignedRowIsNeverRecordedInTheBasis pins the refusal at the WRITE. A signed surface still goes
// to the paid role on purpose (backlog row 447), so its consolidation does reach the recorder — and recording
// it puts the ENGINE's word into the memory of decisions under a surface whose law is the OWNER's. The day
// that signature is lifted, the predicate finds a record, sees an unsigned draft row, and serves the
// engine's rendering as «this book decided it earlier», over the owner's own.
func TestTheOwnersSignedRowIsNeverRecordedInTheBasis(t *testing.T) {
cands := []terminology.Candidate{{Key: "方源"}, {Key: "青茅山"}, {Key: "元石"}}
out := map[string]string{"方源": "Фан Юань", "青茅山": "гора Цинмао", "元石": "юаньши"}
fps := map[string]bankBasisFP{"方源": {Whole: "a"}, "青茅山": {Whole: "b"}, "元石": {Whole: "c"}}
rows := map[string]store.GlossaryEntry{
"方源": {Src: "方源", Dst: "Фан Юань", Status: "approved"}, // the owner's word
"青茅山": {Src: "青茅山", Dst: "гора Цинмао", Status: "draft", Source: "mined"},
}
ambiguous := map[string]bool{"元石": true} // two senses or two windows: nothing can say which is the answer
got := basisRowsToStore(cands, out, nil, nil, fps, rows, ambiguous, basisShape(40, "p"))
keys := map[string]bool{}
for _, r := range got {
keys[r.SrcKey] = true
}
if keys["方源"] {
t.Error("a surface the OWNER signed was recorded in the basis: lift the signature and the engine's word is served over his, which merges the two words the bank ontology keeps apart")
}
if keys["元石"] {
t.Error("an AMBIGUOUS surface was recorded: the carrier's key is (book, surface), so two senses of one surface cannot be remembered apart and whichever was written last answers for both")
}
if !keys["青茅山"] {
t.Fatalf("control: the ordinary engine-decided row was NOT recorded either (%d row(s)) — this test would then pass on a recorder that stores nothing", len(got))
}
if len(got) != 1 {
t.Fatalf("recorded %d row(s), want exactly the one: %+v", len(got), got)
}
}
// TestAnAmbiguousSurfaceIsReAskedRatherThanGuessed pins the read side of the same class. The bank's
// uniqueness key is the FOUR-tuple (src, sense, since_ch, until_ch); a candidate carries none of sense or
// window. A map written as `out[key] = row` keeps whichever row the store returned last — it orders by that
// tuple, so the LATEST window wins — and answers for the other.
func TestAnAmbiguousSurfaceIsReAskedRatherThanGuessed(t *testing.T) {
bank := []store.GlossaryEntry{
// One surface, two LEGITIMATE rows: the owner signed chapters 110, the engine drafted 11 onward.
{Src: "古月", Dst: "Гуюэ", Status: "approved", SinceCh: 1, UntilCh: 10},
{Src: "古月", Dst: "клан Гу Юэ", Status: "draft", SinceCh: 11, Source: "mined"},
{Src: "元石", Dst: "юаньши", Status: "draft", Source: "mined"},
}
rows, ambiguous := bankBasisRows(bank, func(s string) string { return s })
if !ambiguous["古月"] {
t.Fatal("a surface the bank answers with TWO rows was indexed as if it had one: the predicate would serve whichever row sorted last, and a signature on the other window would be invisible to it")
}
if _, indexed := rows["古月"]; indexed {
t.Fatal("the ambiguous surface is ALSO in the unambiguous index: a caller reading that map gets the collapsed answer anyway")
}
if _, ok := rows["元石"]; !ok || len(rows) != 1 {
t.Fatalf("control: the unambiguous surface must still be indexed, got %d row(s) %+v — otherwise the refusal above is «the index is empty», not «this one is ambiguous»", len(rows), rows)
}
// And the predicate says WHY, in its own class: «ambiguous» and «the source moved» call for different
// reactions, and a re-purchase that cannot name its cause is one nobody can act on.
fp := bankBasisFP{Whole: "x"}
basis := map[string]bankBasisRecord{"古月": {Key: "古月", FP: fp, Answer: bankBasisAnswer{Dst: "клан Гу Юэ"}}}
v := basisVerdictFor(terminology.Candidate{Key: "古月"}, fp, basis, rows, ambiguous, "s")
if v.Reason != basisAmbiguous {
t.Fatalf("verdict = %q, want %q", v.Reason, basisAmbiguous)
}
if v.Answer.Dst != "" {
t.Fatalf("an ambiguous surface was handed an answer (%q) — the guess this class exists to refuse", v.Answer.Dst)
}
}
// TestBasisWidthIsReadFromThePairWhenThePairStatesIt pins the branch the canon's standing review question
// turns on — «would a pair that is not in the repository work without a Go edit» — and it exists because the
// pack's first pin for it exercised only the DEFAULT: it built a Runner with no pack at all, so a planting
// that read kwic_width instead of basis_width survived every test in the tree.
func TestBasisWidthIsReadFromThePairWhenThePairStatesIt(t *testing.T) {
r := &Runner{pack: &lang.Pack{Terminology: &lang.TerminologySizing{KWICWidth: 11, BasisWidth: 7}}}
if got := r.basisWidth(); got != 7 {
t.Fatalf("basisWidth() = %d, want the pair's basis_width (7). Reading %d would mean the KWIC width — a prompt-shape knob — decides which stored decisions survive, so tuning the prompt re-buys every book's bank",
got, r.pack.Terminology.KWICWidth)
}
// The other half: a pair that states nothing falls through to the engine default, so a pair with no
// terminology.txt keeps working.
bare := &Runner{pack: &lang.Pack{Terminology: &lang.TerminologySizing{KWICWidth: 11}}}
if got := bare.basisWidth(); got != terminologyDefaultBasisWidth {
t.Fatalf("a pair stating no basis_width must fall through to the engine default, got %d", got)
}
}
// TestASettledRowIsNotAlsoMarkedNeverAsked pins the exclusivity of the two savings. They are different facts
// with different sentences on the sheet — «the bank already renders this surface and every draft agreed»
// versus «this book decided it earlier» — and one candidate carrying both prints two mutually exclusive
// sentences, is counted twice in the read-out, and, worst of the three, is lifted out of the contested queue
// by SettledByBank, which is exactly the exemption the comment on that queue refuses for a basis row.
func TestASettledRowIsNotAlsoMarkedNeverAsked(t *testing.T) {
rec := &reqRec{}
srv := newJSONProvider(rec, func(body string) (string, string) {
if isTerminologyBody(body) {
// BOTH surfaces are answered, and that is what puts an ENGINE row — and therefore a basis
// record — on 方源, while 青茅山 becomes the SEED surface the bank filter takes. One run, two
// savings, which is the state this pin needs and the only state in which «no row carries
// both marks» is a measurement rather than a tautology.
return "方源\tФан Юань\n青茅山\tгора Цинмао", "stop"
}
return "Фан Юань пришёл." + "\n" + bankBlockForMining, "stop"
})
defer srv.Close()
// ⚠ THE FIXTURE HAS TO BUILD A STATE NEITHER SAVING REACHES ALONE, and it does it the way a real book
// does. Run 1: the seed DISAGREES with the drafts (Цинмаошань against гора Цинмао), so the bank filter
// leaves 青茅山 to the paid role — which is what puts a BASIS record on a surface the seed also holds.
// Then the owner edits the seed to the rendering the drafts actually use, and from run 2 the bank
// filter takes the same surface as well. One candidate, both savings, which is the overlap this pin is
// about.
seed := "terms:\n - { src: 青茅山, dst: Цинмаошань, status: draft }\n"
bookPath := setupMiningStopProject(t, srv.URL, miningStopOpts{terminology: true, glossarySeed: seed})
r1 := newRunner(t, bookPath)
if _, err := r1.TranslateBook(context.Background()); err != nil {
t.Fatal(err)
}
if r1.lastTerminology == nil || r1.lastTerminology.Consolidated == 0 {
t.Fatalf("premise broken: run 1 must BUY 青茅山, or no basis record exists for it: %+v", r1.lastTerminology)
}
seedPath := r1.Book.GlossarySeed
r1.Close()
writeFile(t, seedPath, "terms:\n - { src: 青茅山, dst: гора Цинмао, status: draft }\n")
var last *Runner
for i := 2; i <= 3; i++ {
r := newRunner(t, bookPath)
// Editing the seed legitimately moves the edit-wave snapshot, and the engine refuses to re-pay for
// it without consent — correct, loud behaviour that has nothing to do with this pin. Consent, as
// the owner would.
r.Resnapshot = true
r.AcceptRebill = RebillConsent{Given: true}
if _, err := r.TranslateBook(context.Background()); err != nil {
t.Fatalf("run %d: %v", i, err)
}
if i == 3 {
last = r
break
}
r.Close()
}
defer last.Close()
rows := sheetRowsOf(t, last)
both := 0
for _, row := range rows {
if row.SettledByBank && row.SettledByBasis {
both++
t.Errorf("%q carries BOTH marks: the sheet prints «the bank already renders this surface and every draft agreed» AND «this book decided it earlier» about one row, the read-out counts it twice, and SettledByBank lifts it out of the contested queue — the exemption stopcontest.go explicitly refuses for a basis row",
row.Src)
}
}
tres := last.lastTerminology
if tres.BankSettled+tres.BasisServed > len(rows) {
t.Errorf("the two savings sum to %d over %d candidates: they overlap, so the read-out's `never_asked` and `settled_earlier` double-count",
tres.BankSettled+tres.BasisServed, len(rows))
}
_ = both
// ⛔ THE PREMISE IS THAT BOTH SAVINGS ARE LIVE IN THIS RUN — not that both claimed one surface, which
// is now impossible for a reason deeper than this test, and the reason is worth stating because an
// earlier revision of this pin DID assert it and had to be rewritten.
//
// The bank filter takes only SEED surfaces (dropBankSettled → unsignedEngineSurfaces), and the basis
// records only ENGINE rows (basisRowsToStore → membank.IsEngineUnsigned). Those two populations are
// DISJOINT BY CONSTRUCTION, so no surface can carry a record and be taken by the filter. Before the
// «whose word is this» fix they were not disjoint: the recorder asked `Status == "approved"` instead,
// so an owner's hand-written unsigned seed row read as the engine's, got a basis record, and became
// exactly the contended surface this test used to build. ⇒ the fixture that built it is now
// unbuildable, and asserting an unbuildable state would be asserting nothing.
if tres.BankSettled == 0 || tres.BasisServed == 0 {
t.Fatalf("premise broken: this run must produce BOTH savings for the exclusivity to be about anything (bank_settled=%d basis_served=%d)", tres.BankSettled, tres.BasisServed)
}
// And the disjointness itself, asserted rather than trusted: nothing the bank filter took this run has
// a row in the memory of decisions.
stored, err := last.Store.BankBasisForBook(last.Book.BookID)
if err != nil {
t.Fatal(err)
}
if len(stored) == 0 {
t.Fatalf("premise broken: the basis holds nothing at all, so «no overlap» is a statement about an empty memory")
}
// ⛔ AND THE CLASSES MUST ACCOUNT FOR THE OFFERED POPULATION AND NOTHING MORE. This is what the scoping
// still guards now that «whose row is this» is asked properly: the two savings can no longer collide on
// one surface — the bank filter takes SEED surfaces, the recorder keeps ENGINE rows, and those are
// disjoint — so the visible damage of asking the predicate about candidates nobody offered it is no
// longer a double MARK but a double COUNT: every bank-taken candidate lands in a re-ask class, and the
// run reports terms as «bought again» that were never a question for this mechanism.
offered := len(rows) - tres.BankSettled
counted := tres.BasisServed
for _, n := range tres.BasisReAsked {
counted += n
}
if counted != offered {
t.Errorf("the basis reported on %d candidate(s) while only %d were offered to it (%d of %d taken by the bank filter): its classes are counting a population it was never asked about, so every re-ask number the run prints is inflated by the OTHER saving",
counted, offered, tres.BankSettled, len(rows))
}
for _, k := range tres.BankSettledKeys {
if _, held := stored[k]; held {
t.Errorf("%q was taken by the BANK filter and also holds a basis record (%d stored: %v): the two populations are meant to be disjoint by construction — the filter takes seed surfaces, the recorder keeps engine rows — and this surface is in both",
k, len(stored), basisKeysOf(stored))
}
}
}
func basisKeysOf(m map[string]store.BankBasisRow) []string {
out := make([]string, 0, len(m))
for k := range m {
out = append(out, k)
}
return out
}
// TestTheThreeDeclaredDecisionsAboutABasisRowAreGuarded pins three choices this pack DECLARED in comments
// and nothing was asserting — the state in which a declaration is prose rather than a guarantee. All three
// were found by planting: each mutation survived the whole of ./internal/pipeline/ and ./cmd/tmctl/.
func TestTheThreeDeclaredDecisionsAboutABasisRowAreGuarded(t *testing.T) {
basisRow := BankStopRow{
Src: "元石", Dst: "юаньши", Type: "name", Origin: "mined", Freq: 9, Conf: -1,
SettledByBasis: true,
// The drafts of THIS run proposed something else entirely — which is the whole point: the rendering
// came from an earlier purchase, so «no draft proposed it» is true and meaningless.
Variants: []BankStopVariant{{Dst: "камень юань", Chunks: 3}},
}
// (a) INVENTED must not fire. It means «the engine produced a rendering no draft proposed» and is a
// finding about THIS run's drafts; on a row answered from memory it is a false accusation printed
// against the book's own established canon, on the sheet the owner signs by.
rows := bankStopRows([]terminology.Candidate{{
Key: "元石", Src: "元石", Type: "name", Origin: terminology.OriginMined, Freq: 9,
Variants: []terminology.Variant{{Dst: "камень юань", Chunks: 3}},
}}, map[string]string{"元石": "юаньши"}, &terminologyResult{BasisServedKeys: []string{"元石"}})
if len(rows) != 1 {
t.Fatalf("premise broken: projected %d row(s), want 1", len(rows))
}
if !rows[0].SettledByBasis {
t.Fatal("premise broken: the row must be marked as answered by the basis, or (a) tests nothing")
}
if rows[0].Invented {
t.Error("(a) a row answered from the basis was flagged INVENTED: the drafts of THIS run were never offered the term, so the flag accuses the book's own canon of being made up")
}
// The control: the flag is NOT simply dead — an ordinary row with an unproposed rendering still raises it.
ctl := bankStopRows([]terminology.Candidate{{
Key: "元石", Src: "元石", Origin: terminology.OriginMined,
Variants: []terminology.Variant{{Dst: "камень юань", Chunks: 3}},
}}, map[string]string{"元石": "юаньши"}, &terminologyResult{})
if !ctl[0].Invented {
t.Fatal("control: INVENTED did not fire on an ordinary row with an unproposed rendering, so the assertion above holds for the wrong reason")
}
// (b) The contested queue must NOT exempt it. SettledByBank means «the bank renders the surface and
// every draft agreed» — genuinely nothing to doubt. A basis row carries a real rendering chosen by a
// paid role, and neither half of that sentence is true of it, so it is judged on the same signals as
// any other answered row.
opts := StopContestOpts{CountInvented: true, CountUnresolved: true, UseConf: true, MaxConf: 50, CountConflicts: true}
doubted := basisRow
doubted.Contradicts = []string{`this run also renders 元 as "юань"`}
if got := doubted.Contest(opts); !got.Contested {
t.Error("(b) a basis row with a live contradiction was NOT contested: the basis was exempted by its resemblance to SettledByBank, and the queue goes silent over most of a settled book")
}
// The control: a BANK-settled row still IS exempt, so (b) is about the basis and not about the queue.
bankSettled := basisRow
bankSettled.SettledByBasis, bankSettled.SettledByBank = false, true
bankSettled.Contradicts = []string{`this run also renders 元 as "юань"`}
if got := bankSettled.Contest(opts); got.Contested {
t.Fatal("control: a BANK-settled row became contested, so the exemption this pack kept is gone and (b) proves nothing about the basis")
}
// (c) The stop table must round-trip BOTH marks. They are trailing clauses of one line, so a reader that
// cuts at the first label it finds swallows the other — and the renderer prints NOT ASKED first, so the
// order is not theoretical.
both := BankStopRow{Src: "青茅山", Dst: "гора Цинмао", Type: "place", Origin: "mined", Conf: -1,
SettledByBank: true, SettledByBasis: true}
parsed, err := ParseBankStopTable([]byte(renderBankStopTable([]BankStopRow{both})))
if err != nil {
t.Fatalf("(c) the table this engine just wrote did not parse: %v", err)
}
if len(parsed) != 1 {
t.Fatalf("(c) parsed %d row(s), want 1", len(parsed))
}
if !parsed[0].SettledByBank || !parsed[0].SettledByBasis {
t.Errorf("(c) a row carrying BOTH labels round-tripped as bank=%v basis=%v: one label was swallowed by the other's cut, and a reader of the shipped table loses a state the sheet shows",
parsed[0].SettledByBank, parsed[0].SettledByBasis)
}
}
// TestAProducerChangeReportsItsOwnCause pins BOTH halves of the producer rule, and the second half is the
// one the pack's first attempt missed.
//
// ⛔ HALF ONE — it must move something. Before the settled basis, improving the terminologist's prompt or
// moving it to another model missed every checkpoint (RequestHash folds model, effort and snapshot) and the
// book re-bought its bank from the new producer. Leave the producer out entirely and a settled row is never
// asked again no matter who is asking now: a better prompt reaches the unsettled TAIL of every book and
// nothing else, permanently. Improving a bank role's prompt is a direct lever on translationese.
//
// ⛔ HALF TWO — it must move the RIGHT thing, and the pack's first attempt hashed the producer in with the
// EVIDENCE. The re-purchase then arrived as `evidence-moved`, «the source around this surface changed» —
// told to the operator on every row, about a book nobody had touched. A producer is part of the RULE that
// decided a row, exactly as the evidence width is, and the rule is compared before the evidence so each
// part can name itself. The first version of this pin asserted only that the hash MOVED and was green and
// empty on precisely this.
func TestAProducerChangeReportsItsOwnCause(t *testing.T) {
c := terminology.Candidate{Key: "元石"}
fp := candidateBasisFingerprint(c, basisSource, nil, 40)
rows := map[string]store.GlossaryEntry{"元石": {Src: "元石", Dst: "юаньши", Status: "draft", Source: "mined"}}
rec := func(shape string) map[string]bankBasisRecord {
return map[string]bankBasisRecord{"元石": {Key: "元石", Shape: shape, FP: fp, Answer: bankBasisAnswer{Dst: "юаньши"}}}
}
was := basisShape(40, "producer-A")
// The premise: under the SAME rule the row is served, or every assertion below holds trivially.
if v := basisVerdictFor(c, fp, rec(was), rows, nil, was); v.Reason != basisServed {
t.Fatalf("premise broken: an unchanged rule must serve, got %q", v.Reason)
}
for _, tc := range []struct {
name string
now string
want basisReason
}{
{"the role's prompt or model changed", basisShape(40, "producer-B"), basisProducerMoved},
{"the pair's evidence width changed", basisShape(80, "producer-A"), basisWidthMoved},
} {
t.Run(tc.name, func(t *testing.T) {
v := basisVerdictFor(c, fp, rec(was), rows, nil, tc.now)
if v.Reason != tc.want {
t.Fatalf("verdict = %q, want %q. The evidence did not move — the RULE did, and reporting it as «the source around this term changed» sends the owner looking at the book for a change somebody made to a prompt or to a knob",
v.Reason, tc.want)
}
if v.Answer.Dst != "" {
t.Fatalf("a row decided under another rule was served anyway (%q)", v.Answer.Dst)
}
})
}
// AND the cause must not be «never decided»: a record whose rule moved is kept, not dropped, because a
// dropped record reads as `new` — false of every one of these rows, and the wrong cause printed per row
// while the summary line states the right one.
if v := basisVerdictFor(c, fp, rec(basisShape(40, "producer-B")), rows, nil, was); v.Reason == basisNew {
t.Fatal("a row whose producer changed was reported as never decided")
}
}
// TestTheProducerIdentityIsResolvedInOnePlace pins that the identity is one function, and that it is built
// from what the role's system turn ACTUALLY says on this book rather than from the template's own hash.
//
// ⛔ THE RENDERED TEXT IS THE POINT, not tidiness. The pair's terminologist prompt reads the BRIEF —
// `{{title}}`, `{{audience}}`, `{{honorifics}}`, `{{transcription}}`, `{{venuti}}`. Change `honorifics: keep`
// to `adapt`, or the transcription convention, and both waves of the book are re-bought through BriefHash,
// while an identity keyed on the template's SHA would go on serving renderings decided under the OLD law
// into waves paid for under the new one. Neither the evidence nor the model would have moved; the law the
// role was told to apply would have. ⚠ And it is pair-agnostic BY SHAPE rather than by a list of brief
// fields in Go: whatever a pair's own prompt chooses to read enters because it enters the rendered text.
func TestTheProducerIdentityIsResolvedInOnePlace(t *testing.T) {
newRun := func(book *config.Book, model string) *Runner {
r := &Runner{Pipeline: &config.Pipeline{}, Book: book}
r.Pipeline.Gates.Terminology.Model = model
r.Pipeline.Gates.Terminology.Reasoning = "low"
r.terminologyTemplate = &PromptTemplate{
System: "Книга: «{{title}}». Хонорифики: {{honorifics}}.", SHA256: "fixed-template-sha"}
return r
}
keep := &config.Book{BookID: "b", Title: "蛊真人", Honorifics: "keep"}
adapt := &config.Book{BookID: "b", Title: "蛊真人", Honorifics: "adapt"}
base := newRun(keep, "deepseek-v4-flash").bankRoleProducer()
if base == "" {
t.Fatal("a configured role must produce a comparable identity")
}
if same := newRun(keep, "deepseek-v4-flash").bankRoleProducer(); same != base {
t.Fatal("the same configuration produced two identities: every purchase would re-buy everything")
}
// THE BRIEF: the template's own SHA is identical in both runs, so a producer keyed on it could not tell
// these apart — and the law the role applies is not the same.
if brief := newRun(adapt, "deepseek-v4-flash").bankRoleProducer(); brief == base {
t.Fatal("changing the book's `honorifics` left the producer identity unmoved although the role's system turn renders it: decisions taken under the old law would keep being served into waves bought under the new one")
}
// THE MODEL.
if other := newRun(keep, "deepseek-v4-pro").bankRoleProducer(); other == base {
t.Fatal("changing the bank role's model left the producer identity unmoved")
}
// THE CLASSIFIER, which authors the type and gender halves of every stored answer.
withClassifier := newRun(keep, "deepseek-v4-flash")
withClassifier.classifierTemplate = &PromptTemplate{System: "classify", SHA256: "c"}
if withClassifier.bankRoleProducer() == base {
t.Fatal("turning the CLASSIFIER on left the producer identity unmoved, although it authors the type and gender halves of every stored answer")
}
// A TEMPLATE THAT CANNOT RENDER must not hash like an absent role: «could not be described» and «off»
// are different states, and the first must re-buy rather than serve.
broken := newRun(keep, "deepseek-v4-flash")
broken.terminologyTemplate = &PromptTemplate{System: "{{no_such_field}}", SHA256: "fixed-template-sha"}
off := newRun(keep, "deepseek-v4-flash")
off.terminologyTemplate = nil
if broken.bankRoleProducer() == off.bankRoleProducer() {
t.Fatal("an unrenderable prompt hashed the same as an absent one")
}
}
// TestAnOwnersUNSIGNEDRowIsNotTheEnginesMemoryEither pins the other half of «whose word is this», and it is
// the half the pack's first fix missed: it asked `Status == "approved"` — a SECOND spelling of a question
// membank.IsEngineUnsigned already answers — so a term the owner wrote BY HAND and left unsigned
// (`source: seed`, `status: draft`, a shape the loader admits and even requires a rendering for) read as
// «not the owner's», and the engine recorded its own consolidation under it.
func TestAnOwnersUNSIGNEDRowIsNotTheEnginesMemoryEither(t *testing.T) {
cands := []terminology.Candidate{{Key: "青茅山"}, {Key: "元石"}}
out := map[string]string{"青茅山": "гора Цинмао", "元石": "юаньши"}
fps := map[string]bankBasisFP{"青茅山": {Whole: "a"}, "元石": {Whole: "b"}}
rows := map[string]store.GlossaryEntry{
// The owner's own hand, unsigned: Source "seed" is what makes it his, NOT the status.
"青茅山": {Src: "青茅山", Dst: "Цинмаошань", Status: "draft", Source: "seed"},
// The engine's own unsigned row: same STATUS, different author.
"元石": {Src: "元石", Dst: "юаньши", Status: "draft", Source: "mined"},
}
// THE WRITE side: the owner's row must not enter the memory of decisions.
got := basisRowsToStore(cands, out, nil, nil, fps, rows, nil, basisShape(40, "p"))
if len(got) != 1 || got[0].SrcKey != "元石" {
t.Fatalf("recorded %+v, want only the ENGINE's row: a seed term the owner left unsigned is his word, and storing the engine's answer under it means serving that answer back as «this book decided it earlier»", got)
}
// THE READ side: and it is not served, with a class of its own — «you signed it» and «you wrote it and
// left it unsigned» are different sentences to the owner.
basis := map[string]bankBasisRecord{"青茅山": {Key: "青茅山", FP: fps["青茅山"], Answer: bankBasisAnswer{Dst: "гора Цинмао"}}}
v := basisVerdictFor(cands[0], fps["青茅山"], basis, rows, nil, "s")
if v.Reason != basisOwnerRow {
t.Fatalf("verdict = %q, want %q", v.Reason, basisOwnerRow)
}
if v.Answer.Dst != "" {
t.Fatalf("the engine's rendering was handed back for the owner's own row (%q)", v.Answer.Dst)
}
// The control: the ENGINE's row on the same status IS served, so the refusal above is about authorship
// and not about `draft`.
basis["元石"] = bankBasisRecord{Key: "元石", FP: fps["元石"], Answer: bankBasisAnswer{Dst: "юаньши"}}
if e := basisVerdictFor(cands[1], fps["元石"], basis, rows, nil, "s"); e.Reason != basisServed {
t.Fatalf("control: the engine's own draft row was refused too (%q) — the predicate is reading the STATUS, not the author", e.Reason)
}
}
// TestAWidthChangeReportsItsOwnCauseAndNotTheSources pins D1: the evidence width lives in the stored SHAPE,
// so turning it is reported as «the rule that decided this is no longer the rule» — with both widths — and
// never as «the source around this term changed».
//
// ⛔ WHY THE CAUSE MATTERS MORE THAN THE COUNT. Both readings re-buy the same rows; they ask the owner for
// opposite reactions. «The source moved» is nothing to do — the book changed, the engine noticed. «The width
// changed» is a knob somebody turned, and the owner is the only one who can say whether that was meant. A
// re-purchase reported under the wrong cause spends money and teaches the wrong lesson.
func TestAWidthChangeReportsItsOwnCauseAndNotTheSources(t *testing.T) {
if basisShape(40, "p") == basisShape(80, "p") {
t.Fatal("the shape does not carry the width: a change of basis_width would be indistinguishable from a change of the source, and every re-purchase it causes would be reported under the wrong cause")
}
if !strings.Contains(basisShape(40, "p"), basisFingerprintVersion) {
t.Fatalf("the shape must still carry the version it extends, got %q", basisShape(40, "p"))
}
// And the two halves stay independent: the same width under a different version, and the same version
// under a different width, must both be distinguishable — otherwise one of them silently masks the other.
if basisShape(40, "p") == basisShape(41, "p") {
t.Fatal("two adjacent widths collapse to one shape")
}
// The row is STORED under the shape, not under the bare version — checked on the value the store gets,
// because that is the string a later purchase compares against.
rows := basisRowsToStore(
[]terminology.Candidate{{Key: "元石"}},
map[string]string{"元石": "юаньши"},
nil, nil,
map[string]bankBasisFP{"元石": {Whole: "h"}},
map[string]store.GlossaryEntry{"元石": {Src: "元石", Dst: "юаньши", Status: "draft", Source: "mined"}},
nil, basisShape(40, "p"))
if len(rows) != 1 {
t.Fatalf("premise broken: recorded %d row(s), want 1", len(rows))
}
if rows[0].FPVersion != basisShape(40, "p") {
t.Fatalf("stored shape = %q, want %q — a row written under one width and read under another must report a SHAPE change, and it can only do that if the width is in the string it stores",
rows[0].FPVersion, basisShape(40, "p"))
}
if rows[0].FPVersion == basisFingerprintVersion {
t.Fatal("the row stored the bare version: the width is gone from the comparison, and a width change would be reported as «the source moved»")
}
}
// TestABankRowThatDisagreesWithTheMemoryIsReAsked pins D3: the predicate's sentence is «this book decided
// this AND the bank still holds that decision», and until this condition existed only the first half was
// checked — the stored answer was served and then travelled into the delta, overwriting whatever the bank
// held, with the sheet reporting the row as settled earlier.
//
// ⚠ ITS POPULATION IS NAMED, NOT CLAIMED: on the run that writes them the two agree by construction (the
// auto-bank is emitted from the same consolidation the record was built from), so this cannot fire on
// ordinary material and is NOT measured on the bought runs. The reachable way to diverge is a hand edit of
// `<project_db>.auto-bank.yaml`, which the bank ontology lists among the SOURCES.
func TestABankRowThatDisagreesWithTheMemoryIsReAsked(t *testing.T) {
c := terminology.Candidate{Key: "元石"}
fp := bankBasisFP{Whole: "h"}
basis := map[string]bankBasisRecord{"元石": {Key: "元石", FP: fp, Answer: bankBasisAnswer{Dst: "юаньши"}}}
agreeing := map[string]store.GlossaryEntry{"元石": {Src: "元石", Dst: "юаньши", Status: "draft", Source: "mined"}}
if v := basisVerdictFor(c, fp, basis, agreeing, nil, "s"); v.Reason != basisServed {
t.Fatalf("premise broken: an AGREEING row must still be served, got %q — otherwise the assertion below holds for the wrong reason", v.Reason)
}
diverged := map[string]store.GlossaryEntry{"元石": {Src: "元石", Dst: "камень юань", Status: "draft", Source: "mined"}}
v := basisVerdictFor(c, fp, basis, diverged, nil, "s")
if v.Reason != basisDiverged {
t.Fatalf("verdict = %q, want %q: the memory would have been served over a bank row that says something else, and the sheet would have called it «decided earlier»", v.Reason, basisDiverged)
}
if v.Answer.Dst != "" {
t.Fatalf("a diverged surface was handed an answer (%q) — the overwrite this condition exists to refuse", v.Answer.Dst)
}
// ⚠ AND THE COMPARISON IS UNDER THE TARGET-FORM FOLD, the same one the vote is counted with: a case or
// ё difference is ONE decision, and re-asking over it would spend money on a spelling.
folded := map[string]store.GlossaryEntry{"元石": {Src: "元石", Dst: "Юаньши", Status: "draft", Source: "mined"}}
if f := basisVerdictFor(c, fp, basis, folded, nil, "s"); f.Reason != basisServed {
t.Fatalf("a row differing only by case was treated as a disagreement (%q): the engine would buy a term again over a spelling", f.Reason)
}
}
// TestARecordWhoseRuleMovedIsKeptAndNamed pins the loading rule directly, because inside bankBasisPass it
// needs a store, a book and a runner — and a money rule that can only be exercised end to end is one nobody
// plants a mutation into.
func TestARecordWhoseRuleMovedIsKeptAndNamed(t *testing.T) {
now := basisShape(40, "producer-A")
stored := map[string]store.BankBasisRow{
"元石": {SrcKey: "元石", FPVersion: now, FPWhole: "a", Dst: "юаньши"},
"青茅山": {SrcKey: "青茅山", FPVersion: basisShape(40, "producer-B"), FPWhole: "b", Dst: "гора Цинмао"},
"古月": {SrcKey: "古月", FPVersion: basisShape(80, "producer-A"), FPWhole: "c", Dst: "Гуюэ"},
}
basis, changed, old := bankBasisRecordsFrom(stored, now)
if len(basis) != 3 {
t.Fatalf("kept %d record(s) of 3: a record whose rule moved was DROPPED, and a dropped record reads as «never decided» — the wrong cause on every one of those rows", len(basis))
}
if changed != 2 {
t.Fatalf("counted %d moved shape(s), want 2", changed)
}
if len(old) != 2 {
t.Fatalf("the operator line needs the shapes it is comparing against, got %v", old)
}
// Each kept record carries ITS OWN shape, which is what lets the verdict name the part that moved.
if basis["青茅山"].Shape == now || basis["古月"].Shape == now {
t.Fatal("a moved record was stamped with the CURRENT shape: the verdict can no longer tell which part of the rule changed, and every such row would report «the source moved»")
}
if basis["元石"].Shape != now {
t.Fatalf("the unmoved record lost its shape (%q): it would be re-asked for a rule change that did not happen", basis["元石"].Shape)
}
// The control that keeps the counts readable: with nothing moved, nothing is reported.
if _, c, o := bankBasisRecordsFrom(map[string]store.BankBasisRow{"元石": stored["元石"]}, now); c != 0 || len(o) != 0 {
t.Fatalf("control: an unchanged shape reported %d change(s) %v — the counter fires on its own", c, o)
}
}