package pipeline import ( "testing" "textmachine/backend/internal/store" ) // memory_e1_test.go: the E1 MEASUREMENT the onboarding contract requires BEFORE the // post-check may be trusted as a hard gate (research/13 §2 "post-check в русском — // несущий И невалидированный"; research/14 §2 quantified it at 18–36% false flags for a // naive matcher). It measures the decl-aware post-check's false-flag rate on REAL // inflected Russian (the adaptive_levers/adaptive_reask canon: 阿Q ch.5–9, Rogov), // empirically — not "примерно" (the agent's explicit ask). // // It answers two things the agent flagged: // 1. Stored-decl WORKS when the forms are complete → 0 false flags on a correctly // rendered, inflected chunk (the arm that lets the post-check be a real signal). // 2. Stored-decl is NOT free-reliable: with only the base form (an under-filled decl, // the OOV-translit risk — one cheap LLM call may miss cases) the SAME chunk yields // false flags. This is why v1 keeps the post-check a FLAGGER, not a hard gate: if // the seed under-fills decl, it must degrade to observability, not block chunks. // // The inflection cases are the exact ones research/14 measured a naive matcher losing: // «Вэйчжуане» (loc, a SUFFIX inflection substring-matching handles), «Бородатого Вана» // (a modifier that inflects INTERNALLY — substring of the base fails), «сюцая» // (stem-final й→я — the base is not a substring), «Маленького Дэна» (internal). // e1Entity is one canon entity with its full decl set. type e1Entity struct { src string baseDst string fullDecl []string } var e1Canon = []e1Entity{ {"阿Q", "А-кью", []string{"А-кью"}}, // hyphenated translit, invariant {"未庄", "Вэйчжуан", []string{"Вэйчжуан", "Вэйчжуане", "Вэйчжуана", "Вэйчжуану", "Вэйчжуаном"}}, {"王胡", "Бородатый Ван", []string{"Бородатый Ван", "Бородатого Вана", "Бородатому Вану", "Бородатым Ваном", "Бородатом Ване"}}, {"秀才", "сюцай", []string{"сюцай", "сюцая", "сюцаю", "сюцае", "сюцаем"}}, {"小D", "Маленький Дэн", []string{"Маленький Дэн", "Маленького Дэна", "Маленькому Дэну", "Маленьким Дэном", "Маленьком Дэне"}}, {"邹七嫂", "тётушка Цзоу Седьмая", []string{"тётушка Цзоу Седьмая", "тётушки Цзоу Седьмой", "тётушку Цзоу Седьмую"}}, } // e1Source contains every src key so all six entities are injected. const e1Source = "阿Q在未庄遇见王胡和秀才,又碰到小D,邹七嫂在一旁看着。" // e1CorrectOutput is a publisher-quality rendering: every entity present but INFLECTED // (the case a boundary regexp / a base-only matcher false-flags). const e1CorrectOutput = "В Вэйчжуане Бородатого Вана и сюцая знали все. " + "А-кью повстречал Маленького Дэна у реки; тётушка Цзоу Седьмая молча прошла мимо." func e1Glossary(useFullDecl bool) []store.GlossaryEntry { var out []store.GlossaryEntry for _, e := range e1Canon { g := gl(e.src, e.baseDst, "", "approved") if useFullDecl { g.Decl = declJSON(false, e.fullDecl...) } else { g.Decl = declJSON(true, e.baseDst) // naive: only the base form } out = append(out, g) } return out } func TestE1PostcheckFalseFlagRate(t *testing.T) { // Arm A — full decl: the post-check must NOT false-flag a correct inflected output. bFull := bankFrom(e1Glossary(true)) selFull := bFull.Select(e1Source, 1, nil, 0) if len(selFull.injected) != len(e1Canon) { t.Fatalf("expected all %d canon entities injected, got %d", len(e1Canon), len(selFull.injected)) } fullMiss := bFull.postcheck(selFull.injected, e1CorrectOutput) if len(fullMiss) != 0 { t.Errorf("STORED-DECL FALSE FLAGS on correct output: %d/%d — %v", len(fullMiss), len(selFull.injected), fullMiss) } // Arm B — base only (under-filled decl): the SAME correct output now false-flags. bBase := bankFrom(e1Glossary(false)) selBase := bBase.Select(e1Source, 1, nil, 0) baseMiss := bBase.postcheck(selBase.injected, e1CorrectOutput) fullRate := float64(len(fullMiss)) / float64(len(selFull.injected)) baseRate := float64(len(baseMiss)) / float64(len(selBase.injected)) t.Logf("E1 false-flag rate on real inflected Russian: full-decl %.0f%% (%d/%d), base-only %.0f%% (%d/%d)", fullRate*100, len(fullMiss), len(selFull.injected), baseRate*100, len(baseMiss), len(selBase.injected)) // The measurement's verdict: decl-awareness is essential (full ≪ base), and the // stored-decl matcher is trustworthy ONLY when complete. Base-only reproduces the // research/14 failure — the reason the post-check stays a flagger until validated. if len(baseMiss) == 0 { t.Error("base-only decl produced 0 false flags — the E1 measurement lost its hard inflection cases; check the fixture") } if len(fullMiss) >= len(baseMiss) { t.Errorf("full decl (%d) did not reduce false flags vs base (%d)", len(fullMiss), len(baseMiss)) } // The specific research/14 internal-inflection cases MUST be the base-only misses. for _, src := range []string{"王胡", "秀才", "小D"} { if !hasMiss(baseMiss, src) { t.Errorf("base-only decl did not false-flag %s (expected an internal-inflection false flag)", src) } } } // TestPostcheckWhitespaceAndDashRobust covers self-review #2 (multi-word name wrapped // across a newline / doubled space / NBSP) and #8 (hyphen/dash variants): a correctly // rendered term in ANY of these forms must NOT be false-flagged. func TestPostcheckWhitespaceAndDashRobust(t *testing.T) { wang := gl("王胡", "Бородатый Ван", "", "approved") wang.Decl = declJSON(false, "Бородатый Ван", "Бородатого Вана") ahq := gl("阿Q", "А-кью", "", "approved") ahq.Decl = declJSON(true, "А-кью") b := bankFrom([]store.GlossaryEntry{wang, ahq}) sel := b.Select("王胡和阿Q在场。", 1, nil, 0) robust := []string{ "Бородатого Вана и А-кью видели.", // baseline "Бородатого\nВана и А-кью видели.", // #2 newline between the name's words "Бородатого Вана и А-кью видели.", // #2 doubled space "Бородатого Вана и А-кью видели.", // #2 NBSP "Бородатого Вана и А–кью видели.", // #8 en-dash in А-кью "Бородатого Вана и А‑кью видели.", // #8 non-breaking hyphen } for _, out := range robust { if m := b.postcheck(sel.injected, out); len(m) != 0 { t.Errorf("false flag on a correctly-rendered variant %q: %v", out, m) } } } // TestPostcheckWholeWordNoMasking covers self-review #3: a DROPPED short translit name // must not be masked by an unrelated common word that merely contains its letters. func TestPostcheckWholeWordNoMasking(t *testing.T) { van := gl("王", "Ван", "", "approved") van.AllowShort = true // single-char src, allowed here to exercise the target-side check van.Decl = declJSON(false, "Ван", "Вана", "Вану", "Ваном", "Ване") b := bankFrom([]store.GlossaryEntry{van}) sel := b.Select("王走了。", 1, nil, 0) if len(sel.injected) != 1 { t.Fatalf("expected 王 injected, got %d", len(sel.injected)) } // 王 is ABSENT from each output, yet a naive substring would find «ван» inside these. for _, masked := range []string{"Мимо прошёл караван верблюдов.", "Он сел на диван и молчал.", "Иван Петрович кивнул."} { if m := b.postcheck(sel.injected, masked); !hasMiss(m, "王") { t.Errorf("post-check masked a dropped name by an unrelated word: %q → %v", masked, m) } } // A genuine rendering (inflected) is found as a whole word. if m := b.postcheck(sel.injected, "К нему подошёл Ван."); len(m) != 0 { t.Errorf("false flag on a correct whole-word rendering: %v", m) } if m := b.postcheck(sel.injected, "Он говорил с Ваном о делах."); len(m) != 0 { t.Errorf("false flag on a correct inflected rendering: %v", m) } } // TestPostcheckBaseDstAlwaysChecked is the D24.4 repro for the acceptance-stage-A // nominative_gap (37/55 false misses): an entry whose decl.forms lists ONLY oblique // cases while the approved base dst appears in the NOMINATIVE in the output. The base // dst is itself an approved form, so it must ALWAYS be in the accepted set — not only as // a fallback when decl is empty. Mutation: revert dstFormPresent to the empty-decl // fallback (`forms := e.declForms; if len(forms)==0 {forms=[base]}`) and the positive // case below false-flags again → the test goes red. func TestPostcheckBaseDstAlwaysChecked(t *testing.T) { // 方源→Фан Юань, decl carries only oblique cases (the under-seeded reality of stage A). fang := gl("方源", "Фан Юань", "", "approved") fang.Decl = declJSON(false, "Фан Юаня", "Фан Юаню", "Фан Юанем") // oblique-only, NO nominative base b := bankFrom([]store.GlossaryEntry{fang}) sel := b.Select("方源走了。", 1, nil, 0) if len(sel.injected) != 1 { t.Fatalf("expected 方源 injected, got %d", len(sel.injected)) } // Positive (the fix): the nominative base present but ABSENT from the oblique-only // decl → must NOT be a miss now that the base is always accepted. if m := b.postcheck(sel.injected, "На поляну вышел Фан Юань, окружённый гу."); len(m) != 0 { t.Fatalf("nominative base dst present yet flagged — the base form must always be accepted (D24.4 nominative_gap): %v", m) } // The oblique decl forms still work (decl-awareness retained, not replaced). if m := b.postcheck(sel.injected, "Все боялись Фан Юаня."); len(m) != 0 { t.Fatalf("oblique decl form present yet flagged: %v", m) } // Negative (precision preserved): neither the base nor any decl form present → a real // miss. The union only ADDS an accepted form; it must not blind a genuine drop. if m := b.postcheck(sel.injected, "На поляну никто не вышел."); !hasMiss(m, "方源") { t.Fatalf("dst and all forms absent must still flag a miss (union must not blind precision): %v", m) } } // TestE1PostcheckCatchesPoisoning: with complete decl, a WRONG rendering (poisoning / // the model ignoring the glossary) is still a true-positive flag — the post-check's // actual job. Precision does not come for free: the decl forms are inflections of the // FULL dst phrase, so a foreign name that merely shares the head noun does NOT slip past. func TestE1PostcheckCatchesPoisoning(t *testing.T) { b := bankFrom(e1Glossary(true)) sel := b.Select("王胡走过来。", 1, nil, 0) // only 王胡 fires if len(sel.injected) != 1 { t.Fatalf("expected 1 injected, got %d", len(sel.injected)) } // Correct inflected rendering → no flag. if m := b.postcheck(sel.injected, "К нему подошёл Бородатый Ван."); len(m) != 0 { t.Errorf("false flag on correct rendering: %v", m) } // Poisoned rendering (a different name) → flagged (true positive). if m := b.postcheck(sel.injected, "К нему подошёл Ван Ху."); !hasMiss(m, "王胡") { t.Errorf("post-check missed the poisoning (Ван Ху ≠ Бородатый Ван): %v", m) } }