Correct the language she wrote in, not the one she was practising

Every pass was English-shaped: CheckpointMessages took the text and the tone and
nothing else, so there was never a language decision to get wrong. On the live
build two pt-PT sentences drew no cards at all — Petal read the Portuguese, said
nothing about it, and filed a mechanics note about the one English line.

The rule is two decisions reading different state. What gets corrected follows
the document. What language the explanation is written in follows the writer —
the half of her pair she is not learning, from users.direction — because an
explanation is teaching, and teaching lands in the language she reads most
easily. Those coincide for every account that exists today (learnerPairs is
{"zh"}), which is a fact about the roster and not about the design, so Target
keeps them apart. It carries a third language too: the collocation gloss is
addressed to her rather than to the document, and folding it into Explain would
have quietly moved it into English on every English document.

The document verdict is a proportion, not a presence — one Portuguese quotation
must not flip an English essay. Per sentence, three-way: pair, English, or no
answer. The third value is the load-bearing one; counting the undecided as
English is exactly what would hold a journal of short Portuguese sentences in
English forever, so the Latin pairs needed an englishMarkers list curated against
pt/fr/es as carefully as latinMarkers was curated against English. Hysteresis at
70/40 because a bilingual paragraph would otherwise alternate its cards' language
every few keystrokes, and hysteresis needs a yesterday — hence the column. Plus a
corroboration floor: a ratio computed over "Não. Eu." is 100% of nothing, and a
flip rewrites every card in the document.

The verdict folds into the chunk salt beside the tone, so a document that changes
language re-opens every sentence rather than serving back cards in a language it
no longer speaks.

checkpointSystemPrompt could not simply take a language — it opens by naming the
reader an ESL learner, and appending "explain in Portuguese" hands the model two
contradictory framings. Separate constants, sharing the JSON contract below the
framing. Both carry a "never translate it into English" line, which is the
instruction the model will most want to disobey. The English prompts are
untouched byte for byte, and a golden says so out loud.

Collocation deliberately did not move: its prompt is per-language knowledge, not
framing, and "natives usually say" for Portuguese is a claim Petal cannot back.

Not deployed and not smoked against a real model. The tests drive the real router
and a real DB; what none of them prove is how Qwen behaves on a Portuguese
document, in particular whether the never-translate line holds.

Claude-Session: https://claude.ai/code/session_01GJHNvirh7Hzhc9RL3HAvz7
This commit is contained in:
prosolis
2026-07-28 23:20:53 -07:00
parent db9cfb7abf
commit 76dede8856
12 changed files with 836 additions and 31 deletions
+54
View File
@@ -0,0 +1,54 @@
package llm
// Which language a pass corrects, and which language it explains in.
//
// Until Phase 28 there was no question to answer: every prompt was written
// around English prose explained in English, and the pair language reached her
// only when she asked for it (Ask Petal, the explanation translator). That is
// the right default for a writer practising English and the wrong one for a
// document she wrote in her own language, where Petal would read Portuguese,
// say nothing about it, and file a mechanics note about the one English
// sentence at the end.
//
// The two fields are two different decisions reading two different pieces of
// state, and collapsing them would be the bug:
//
// - Correct follows the DOCUMENT. Portuguese prose gets Portuguese
// corrections; that is the whole point.
// - Explain follows the WRITER — the half of her pair she is *not* learning
// (users.direction), because an explanation is teaching, and teaching lands
// in the language she reads most easily.
//
// Today those two coincide for every account that exists: `learnerPairs` is
// {"zh"}, so fr, es and pt-PT writers are all `learning_en` and their
// non-learned half *is* the pair language. That equality is a fact about
// today's roster, not about the design — the same shape of assumption that had
// to be unpicked from `pair_lang` in migration 0016. Keep them apart.
type Target struct {
// Correct is the language the writing is in, and so the language both
// `original` and `replacement` must be written in.
Correct Lang
// Explain is the language each explanation is written in.
Explain Lang
// Pair is the writer's pair language regardless of what this document is
// written in. The collocation coach's parenthetical gloss is addressed to
// her rather than to the document, so it reads this and not Correct.
Pair Lang
}
// English as the prompts name it. Not in `langs`: that map answers "which
// language is the writer's half of the pair", and English is the constant on
// the other side of every pair Petal supports.
var English = Lang{Code: "en", Name: "English", Why: "why"}
// EnglishTarget is the pre-Phase-28 behaviour, made explicit: an English
// document, corrected and explained in English, for a writer whose pair
// language is `pair`. Every existing user is on this path and the prompt it
// produces is byte-identical to the one that shipped before this phase.
func EnglishTarget(pair Lang) Target {
return Target{Correct: English, Explain: English, Pair: pair}
}
// Flipped reports whether this document is in the pair language rather than in
// English — i.e. whether the pass is reading her own language.
func (t Target) Flipped() bool { return t.Correct.Code != English.Code }