Curated against English had quietly become curated against writing

Seen live: "Esta manhã acordei cedo e fui correr ao longo da marginal.
O ar estava fresco e havia poucas pessoas na rua." — unremarkable
Portuguese, two marker hits, zero English hits, and a verdict of
English. Corrected as English, read aloud in an American voice.

The list was missing the ordinary machinery of the language: the
contractions (ao, à, num), the tenses a diary is written in (estava,
havia, fomos), the words that join two clauses (até, depois, então,
onde). Each clears the bar the list already set — an English sentence
has no reason to contain them — so their absence bought nothing.

The floor stays at three. What changed is that three is now reachable
by prose rather than only by a paragraph that argues its own case. fr
and es get the same additions by analogy; neither has an account yet to
catch it live, which is exactly how this one survived. "sin" and "tan"
stay out of the es list: both are English words.

Two regression tests, pointed in opposite directions — ordinary
Portuguese must read as hers, and English about Portugal, English
quoting Portuguese, and a plain English diary must all still read as
English, held verdict included.

Claude-Session: https://claude.ai/code/session_01GJHNvirh7Hzhc9RL3HAvz7
This commit is contained in:
prosolis
2026-07-29 00:28:32 -07:00
parent e67f77eb05
commit acb35108c0
2 changed files with 87 additions and 0 deletions
+52
View File
@@ -446,3 +446,55 @@ func TestPassAnnouncesItsVerdict(t *testing.T) {
t.Fatal("empty document answered with no verdict header at all")
}
}
// TestOrdinaryProseIsEnoughEvidence is the regression for what the marker lists
// were caught doing on 2026-07-29, live, in a browser: unremarkable Portuguese
// read as English, because the list was curated against English so tightly that
// it had also been curated against ordinary writing. The document below scored
// two pair markers and zero English ones, and two is below the corroboration
// floor — so a paragraph with no evidence of English in it at all came back
// English, and was corrected and read aloud as English.
//
// Every sample here is prose a person might actually write, not prose chosen to
// contain markers. That is the whole point of the test: the failure was invisible
// to a suite whose fixtures all argued their own case.
func TestOrdinaryProseIsEnoughEvidence(t *testing.T) {
samples := []struct{ name, text string }{
{"the one seen live", "Esta manhã acordei cedo e fui correr ao longo da marginal. O ar estava fresco e havia poucas pessoas na rua. Depois comprei um jornal e li-o sentado num banco ao sol."},
{"an afternoon out", "Hoje o céu estava limpo e fomos até ao jardim junto ao rio. A minha mãe trouxe uma manta velha e sentámos-nos debaixo de uma árvore."},
{"plans", "Amanhã vamos ao cinema depois do trabalho. Ontem estava demasiado cansada para sair de casa."},
}
for _, s := range samples {
if got := documentLang(s.text, "pt-PT", ""); got != docLangPair {
t.Errorf("%s: documentLang = %q, want %q — ordinary Portuguese must not read as English\n%s",
s.name, got, docLangPair, s.text)
}
}
}
// TestEnglishDidNotGetEasierToMistake is the other half, and the reason the
// additions were held to "a word an English sentence has no reason to contain".
// Widening a marker list is only safe if it widens in one direction: these are
// English documents, including ones about Portugal and ones quoting Portuguese,
// and every one of them must still come back English.
func TestEnglishDidNotGetEasierToMistake(t *testing.T) {
samples := []struct{ name, text string }{
{"plain English", "This morning I woke up early and went for a run along the seafront. The air was fresh and there were few people about. Afterwards I bought a newspaper and read it on a bench."},
{"English about Portugal", "We spent a week in Lisbon last summer. The trams were crowded but the food was wonderful, and we walked up to the castle every evening."},
{"English quoting her", "My mother always says \"até amanhã\" when she leaves, never goodbye. I asked her why once and she said it sounded less final to her."},
{"an English diary", "Today was long. I had two meetings before lunch and another one after, and by the time I got home I could not think straight. Tomorrow should be quieter."},
}
for _, s := range samples {
if got := documentLang(s.text, "pt-PT", ""); got != docLangEnglish {
t.Errorf("%s: documentLang = %q, want %q — the widened list must not pull English across\n%s",
s.name, got, docLangEnglish, s.text)
}
}
// And the same document must not flip once it is already sitting in English:
// the hysteresis band is only a safety net if the low side holds too.
for _, s := range samples {
if got := documentLang(s.text, "pt-PT", docLangEnglish); got != docLangEnglish {
t.Errorf("%s: held verdict flipped to %q", s.name, got)
}
}
}
+35
View File
@@ -155,6 +155,19 @@ func distinctMarkers(s string, markers map[string]bool) int {
//
// A single marker is not enough (see readsAsPairLang), so these lists are read
// as evidence to be corroborated rather than as a decision.
//
// **Curated against English is not the same as curated thinly**, and the first
// version of these lists confused the two. Seen live 2026-07-29: "Esta manhã
// acordei cedo e fui correr ao longo da marginal. O ar estava fresco e havia
// poucas pessoas na rua." — unremarkable Portuguese, two marker hits, *zero*
// English hits, and a verdict of English, because the document-level floor wants
// three. The list was missing the ordinary machinery of the language: the
// contractions (ao, à, num), the past tenses a diary is written in (estava,
// havia, fomos), and the words that join two clauses (até, depois, então,
// onde). Every one of them clears the bar above — an English sentence has no
// reason to contain them — so their absence bought nothing and cost the verdict.
// The floor stays at three; what changed is that three is now reachable by
// prose rather than only by a paragraph that happens to argue with itself.
var latinMarkers = map[string]map[string]bool{
"fr": words(
"je", "tu", "il", "elle", "ils", "elles", "nous", "vous", "est", "sont",
@@ -164,6 +177,10 @@ var latinMarkers = map[string]map[string]bool{
"beaucoup", "toujours", "jamais", "quand", "bien", "chose", "temps",
"moi", "toi", "lui", "peux", "veux", "sais", "faire", "dit", "aujourd",
"hui", "quelque", "chez", "tout", "tous", "rien", "déjà", "encore",
// The same gap the pt-PT list was caught with, closed by analogy rather
// than by observation — no fr account exists yet to catch it live.
"aux", "après", "où", "avait", "étaient", "depuis", "jusqu", "chaque",
"autre", "même", "hier", "demain", "matin", "soir", "nôtre", "leurs",
),
"pt-PT": words(
"eu", "você", "ele", "ela", "eles", "elas", "nós", "são", "uma", "os",
@@ -173,6 +190,18 @@ var latinMarkers = map[string]map[string]bool{
"nunca", "bem", "obrigado", "obrigada", "gosto", "tenho", "tem", "foi",
"ser", "ter", "mais", "já", "ainda", "aqui", "ali", "nada", "tudo",
"todos", "para", "pela", "pelo", "sobre", "assim",
// The contractions, which no English sentence has any use for.
"ao", "aos", "à", "às", "num", "numa", "dum", "duma", "pelos", "pelas",
"neste", "nesta", "disso", "deste", "desta",
// The tenses a journal is actually written in.
"estava", "estavam", "estão", "estamos", "havia", "houve", "era", "eram",
"fui", "fomos", "foram", "vai", "vamos", "tinha", "tinham",
// The joins between two clauses.
"até", "depois", "antes", "onde", "então", "enquanto", "embora",
"sem", "quem", "entre",
// And the everyday determiners and time words a diary can hardly avoid.
"nosso", "nossa", "outro", "outra", "mesmo", "mesma", "tão",
"muitos", "muitas", "poucos", "poucas", "hoje", "ontem", "amanhã",
),
"es": words(
"yo", "él", "ella", "ellos", "ellas", "nosotros", "una", "los", "las",
@@ -181,6 +210,12 @@ var latinMarkers = map[string]map[string]bool{
"hacer", "siempre", "nunca", "bien", "gracias", "tengo", "tiene", "fue",
"ser", "tener", "más", "aquí", "allí", "nada", "todos",
"para", "sobre", "así", "hola", "señor", "usted", "muchas",
// Likewise by analogy: no es account exists yet either. "sin" and "tan"
// stay out — both are English words, which is the one disqualification.
"al", "después", "antes", "donde", "entonces", "mientras", "aunque",
"estaba", "estaban", "están", "había", "hubo", "fuimos", "fueron",
"nuestro", "nuestra", "otro", "otra", "mismo", "misma", "quién", "quien",
"muchos", "pocas", "pocos", "hoy", "ayer", "mañana",
),
}