Files
prosolis 76dede8856 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
2026-07-28 23:20:53 -07:00

116 lines
5.3 KiB
Go

package llm
import (
"strings"
"testing"
)
// The prompt every account is on today, written out in full.
//
// Phase 28 gave the checkpoint a second framing for documents in the writer's
// own language, and the risk of that change is not that the new prompt is wrong
// — it is that the old one moved by a word while nobody was looking. Every user
// who exists is a Mandarin native writing English, so this string is the one
// Petal actually sends, all day. It is duplicated here on purpose: a golden
// copied from the constant it guards guards nothing.
const goldenEnglishCheckpointPrompt = `You are a warm, encouraging writing assistant helping someone who speaks English as a second language. Analyze the text below and identify up to 5 issues: grammar errors, unnatural phrasing, incorrect idiom usage, or unclear sentences that are common ESL patterns.
Be specific, friendly, and explain WHY each suggestion improves the writing.
Respond ONLY with valid JSON. No preamble, no markdown fences. Format:
{
"suggestions": [
{
"original": "exact text from the document that needs fixing",
"replacement": "corrected version",
"explanation": "friendly one-sentence explanation",
"type": "grammar|phrasing|idiom|clarity"
}
]
}
If the writing looks good, return: {"suggestions": []}`
func TestEnglishDocumentPromptIsUnchanged(t *testing.T) {
msgs := CheckpointMessages("I has two apple.", "", EnglishTarget(LangFor("zh")))
if got := msgs[0].Content; got != goldenEnglishCheckpointPrompt {
t.Fatalf("the English checkpoint prompt moved:\n--- got ---\n%s\n--- want ---\n%s", got, goldenEnglishCheckpointPrompt)
}
if msgs[1].Content != "I has two apple." {
t.Fatalf("document text mangled: %q", msgs[1].Content)
}
// The tone clause still lands, in the same place it always did.
toned := CheckpointMessages("x", "academic", EnglishTarget(LangFor("zh")))[0].Content
if !strings.Contains(toned, "formal, academic, and objective") {
t.Fatalf("English checkpoint lost its tone guidance:\n%s", toned)
}
}
// A document in her own language gets a prompt that names that language, keeps
// the corrections inside it, and drops the framing that only makes sense when
// the thing being written is English.
func TestFlippedCheckpointPrompt(t *testing.T) {
pt := LangFor("pt-PT")
system := CheckpointMessages("Hoje foi um dia bom.", "casual", Target{Correct: pt, Explain: pt, Pair: pt})[0].Content
if !strings.Contains(system, "European Portuguese") {
t.Fatalf("flipped checkpoint doesn't name the language:\n%s", system)
}
if strings.Contains(system, "second language") || strings.Contains(system, "ESL") {
t.Fatalf("flipped checkpoint kept the ESL framing:\n%s", system)
}
if !strings.Contains(system, "never translate it into English") {
t.Fatalf("flipped checkpoint doesn't forbid translating:\n%s", system)
}
// The shared contract below the framing has to survive the split.
for _, want := range []string{`"suggestions"`, `"replacement"`, "grammar|phrasing|idiom|clarity", "relaxed, friendly, and conversational"} {
if !strings.Contains(system, want) {
t.Fatalf("flipped checkpoint dropped %q:\n%s", want, system)
}
}
if strings.Contains(system, "%!") {
t.Fatalf("flipped checkpoint has a formatting error:\n%s", system)
}
}
// The two decisions are separate arguments and must reach the prompt separately:
// corrections in the document's language, the explanation in the language she
// reads most easily. Only the zh pair can be travelled both ways today, so it is
// the only one that can prove they haven't been quietly collapsed into one.
func TestFlippedPromptsExplainInTheirOwnLanguage(t *testing.T) {
zh := LangFor("zh")
// Native Mandarin, practising English, writing Chinese: both halves Chinese.
both := CheckpointMessages("今天天气很好。", "", Target{Correct: zh, Explain: zh, Pair: zh})[0].Content
if strings.Count(both, "Simplified Chinese (Mandarin)") < 2 {
t.Fatalf("expected corrections and explanations both in Chinese:\n%s", both)
}
if strings.Contains(both, "explanation"+`" in English`) {
t.Fatalf("explanation language leaked to English:\n%s", both)
}
// Native English, learning Chinese, writing Chinese: Chinese corrections,
// English explanations.
split := CheckpointMessages("今天天气很好。", "", Target{Correct: zh, Explain: English, Pair: zh})[0].Content
if !strings.Contains(split, `Write every "explanation" in English.`) {
t.Fatalf("learner direction didn't get English explanations:\n%s", split)
}
if !strings.Contains(split, "writing in Simplified Chinese (Mandarin)") {
t.Fatalf("learner direction lost its Chinese corrections:\n%s", split)
}
// Same for the voice pass, which had no language at all before this phase.
voice := VoiceMessages("今天天气很好。", Target{Correct: zh, Explain: English, Pair: zh})[0].Content
if !strings.Contains(voice, `Write every "explanation" in English.`) || !strings.Contains(voice, "Simplified Chinese") {
t.Fatalf("flipped voice prompt got its languages wrong:\n%s", voice)
}
if strings.Contains(voice, "second language") {
t.Fatalf("flipped voice prompt kept the ESL framing:\n%s", voice)
}
// An English document still gets exactly the voice prompt it always got.
if got := VoiceMessages("x", EnglishTarget(zh))[0].Content; got != voiceSystemPrompt {
t.Fatalf("English voice prompt moved:\n%s", got)
}
}