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 1–10, 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 // `.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) } }