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:
+95
-8
@@ -45,11 +45,55 @@ func toneGuidance(tone string) string {
|
||||
"be improved, prefer suggestions that fit that tone, and gently flag wording that clashes with it."
|
||||
}
|
||||
|
||||
// pairCheckpointSystemPrompt is the grammar checkpoint for a document written in
|
||||
// the writer's own language rather than in English.
|
||||
//
|
||||
// It is a separate constant rather than a language clause appended to
|
||||
// checkpointSystemPrompt, because that prompt opens by naming the reader as an
|
||||
// ESL learner and asks for "common ESL patterns" — appending "and explain in
|
||||
// Portuguese" would hand the model two contradictory framings. Only the framing
|
||||
// differs; the JSON contract and the tone clause below it are the same
|
||||
// instructions in the same order, so the two prompts stay comparable.
|
||||
//
|
||||
// The "never translate" line is the one the model most wants to disobey: asked
|
||||
// to improve Portuguese while being an English writing assistant by training, it
|
||||
// will happily hand back an English rendering, which is a translation card
|
||||
// (Phase 25's `isTranslation`) and not a correction.
|
||||
const pairCheckpointSystemPrompt = `You are a warm, encouraging writing assistant. The person you are helping is ` +
|
||||
`writing in %[1]s, and the text below is %[1]s. ` +
|
||||
`Analyze it and identify up to 5 issues: grammar errors, unnatural phrasing, ` +
|
||||
`incorrect idiom usage, or unclear sentences.
|
||||
|
||||
Both "original" and "replacement" must be written in %[1]s. You are improving their %[1]s writing — ` +
|
||||
`never translate it into English, and never suggest they write in English instead.
|
||||
Write every "explanation" in %[2]s.
|
||||
|
||||
Be specific, friendly, and explain WHY each suggestion improves the writing.%[3]s
|
||||
|
||||
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": []}`
|
||||
|
||||
// CheckpointMessages builds the message array for a grammar checkpoint over the
|
||||
// given (already-truncated) document text, steered toward the document's tone.
|
||||
func CheckpointMessages(contentText, tone string) []Message {
|
||||
// given (already-truncated) document text, steered toward the document's tone
|
||||
// and aimed at the language the document is actually written in.
|
||||
func CheckpointMessages(contentText, tone string, t Target) []Message {
|
||||
system := fmt.Sprintf(checkpointSystemPrompt, toneGuidance(tone))
|
||||
if t.Flipped() {
|
||||
system = fmt.Sprintf(pairCheckpointSystemPrompt, t.Correct.Name, t.Explain.Name, toneGuidance(tone))
|
||||
}
|
||||
return []Message{
|
||||
{Role: "system", Content: fmt.Sprintf(checkpointSystemPrompt, toneGuidance(tone))},
|
||||
{Role: "system", Content: system},
|
||||
{Role: "user", Content: contentText},
|
||||
}
|
||||
}
|
||||
@@ -82,12 +126,48 @@ Respond ONLY with valid JSON. No preamble, no markdown fences. Format:
|
||||
|
||||
If the voice is consistent throughout, return: {"suggestions": []}`
|
||||
|
||||
// pairVoiceSystemPrompt is the voice pass for a document in the writer's own
|
||||
// language. Voice consistency is the one pass that transfers across languages
|
||||
// unchanged — a paragraph that reads as pasted from elsewhere reads that way in
|
||||
// any language — so only the framing and the explanation language move.
|
||||
const pairVoiceSystemPrompt = `You are a warm, encouraging writing assistant. The person you are helping is writing ` +
|
||||
`in %[1]s. You are reviewing a COMPLETE %[1]s document for VOICE CONSISTENCY only — not grammar.
|
||||
|
||||
Read the whole document to learn the writer's natural voice, then identify any passages (2 or more sentences) ` +
|
||||
`that feel tonally inconsistent with the surrounding writing — unusually formal, unusually polished, or phrased ` +
|
||||
`in a way that differs from the writer's established voice elsewhere in the document. These often signal text ` +
|
||||
`that was paraphrased too closely from another source. Do not flag the first paragraph (there is no baseline yet). ` +
|
||||
`Do not flag grammar or spelling mistakes — only voice.
|
||||
|
||||
Quote each passage exactly as it appears, in %[1]s. Write every "explanation" in %[2]s.
|
||||
|
||||
Respond ONLY with valid JSON. No preamble, no markdown fences. Format:
|
||||
{
|
||||
"suggestions": [
|
||||
{
|
||||
"original": "exact passage from the document that feels inconsistent",
|
||||
"replacement": null,
|
||||
"explanation": "friendly one-sentence note about why this passage sounds unlike the rest",
|
||||
"type": "voice"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
If the voice is consistent throughout, return: {"suggestions": []}`
|
||||
|
||||
// VoiceMessages builds the message array for a voice-consistency pass. Unlike
|
||||
// the checkpoint, the caller passes the WHOLE document (no truncation) — voice
|
||||
// consistency is judged against the established voice everywhere else.
|
||||
func VoiceMessages(contentText string) []Message {
|
||||
//
|
||||
// The pass had no language argument at all before Phase 28, which was the same
|
||||
// English assumption the checkpoint made, just unstated.
|
||||
func VoiceMessages(contentText string, t Target) []Message {
|
||||
system := voiceSystemPrompt
|
||||
if t.Flipped() {
|
||||
system = fmt.Sprintf(pairVoiceSystemPrompt, t.Correct.Name, t.Explain.Name)
|
||||
}
|
||||
return []Message{
|
||||
{Role: "system", Content: voiceSystemPrompt},
|
||||
{Role: "system", Content: system},
|
||||
{Role: "user", Content: contentText},
|
||||
}
|
||||
}
|
||||
@@ -133,10 +213,17 @@ If every pairing already sounds natural, return: {"suggestions": []}`
|
||||
// CollocationMessages builds the message array for a collocation pass over the
|
||||
// WHOLE document (no truncation), gently steered toward the document's tone so a
|
||||
// hint can prefer a register-appropriate pairing. The parenthetical gloss is
|
||||
// written in the writer's own language.
|
||||
func CollocationMessages(contentText, tone string, lang Lang) []Message {
|
||||
// written in the writer's own language — `Pair`, not `Explain`: the gloss is
|
||||
// addressed to her rather than to the document.
|
||||
//
|
||||
// The coach itself remains English-only. Collocation lists are the one thing
|
||||
// here that is genuinely per-language knowledge rather than framing, and
|
||||
// "natives usually say" for Portuguese is a claim this prompt has no grounds to
|
||||
// make yet; a flipped document simply gets the pass it always got. (Phase 28
|
||||
// moved the checkpoint and the voice pass; this one waits for evidence.)
|
||||
func CollocationMessages(contentText, tone string, t Target) []Message {
|
||||
return []Message{
|
||||
{Role: "system", Content: fmt.Sprintf(collocationSystemPrompt, toneGuidance(tone), lang.Name)},
|
||||
{Role: "system", Content: fmt.Sprintf(collocationSystemPrompt, toneGuidance(tone), t.Pair.Name)},
|
||||
{Role: "user", Content: contentText},
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user