7fa98d03c7de4e182cb9ec28fc71c92e88350392
6
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
7fa98d03c7 |
Both sections of the advice arrived in the language she is not learning
Reported from the phone: "I have my main language set as Portuguese and I say
I'm learning English but yet Petal presents the Ask Petal advice in both
sections as Portuguese."
Nothing was wrong with targetFor again. A Portuguese document by a Portuguese
writer is explained in Portuguese, which is the whole point of Phase 28. The
card was right. What was wrong was the tap underneath it: /suggestions/{id}
/translate answered "" for exactly that case, on the reasoning that an
unasked-for English rendering of an explanation she can already read is not a
seed but noise. That reasoning had the writer facing the wrong way. She is
learning English. The half she is *practising* is the half worth a tap, and the
bubble sits directly beneath the explanation inside the same card, so answering
"" left her with Portuguese, the same Portuguese again, and no English anywhere
on the card. targetFor's own comment promised the other language stays one tap
away in both directions; only one direction had ever been built.
So the endpoint keeps the one rule it always claimed: render into whichever
half the explanation is not already in. English explanation into her language,
as before; her language into the English she is learning, which is new. Both
ends of that are now parameters — TranslateMessages took the source language
for granted as English because until Phase 28 it always was. The zh prompt is
unchanged byte for byte, which its test still pins.
The client fallback was the same symptom from a different cause and would have
survived the server fix: an empty answer, or an unreachable model, seeded the
bubble with the explanation itself — a verbatim repeat of the line two above
it, which reads as Petal replying in the language the tap was pressed to
escape. With the endpoint always having somewhere to go, empty now means only
that the model didn't answer, so the panel opens with no bubble at all and the
input where she can ask. AskPetal no longer takes the explanation as a prop; it
never needed anything but the id.
The test that pinned the refusal now pins the rendering, and carries the report.
Claude-Session: https://claude.ai/code/session_01KGACAtTPjvZ2PipDZ5qD99
|
||
|
|
3cc23b8ea4 |
The advice arrived in the language she was trying to read her way out of
Reported as "the Portuguese option isn't translating the advice in English — it's just reprinting Portuguese." Nothing was wrong with targetFor. It was reading a direction the account could not leave. learnerPairs held only zh, so SetPair refused learning_pair for pt-PT and every Portuguese account was learning_en by force. targetFor then did exactly what it says: explanations follow the half of the pair she is not learning, which for a forced learning_en account is Portuguese. A Portuguese document, corrected in Portuguese, explained in Portuguese, with no way to ask for English — correct behaviour derived from a fact about the roster that was no longer true. The note in learnerPairs was written one phase too early to see it. It said turning a pair around needs a word list and a dictionary reading into English, and that fr, es and pt-PT had neither. Portuguese has both. Word boundaries are spaces — the megabyte jieba needs is a property of a writing system that doesn't use them, not a debt every pair owes. And the dictionary arrived with dict.db, which reads pt→en as readily as en→pt; dreamProvider.reverse has been answering that question since the pair shipped. What was actually blocking the pair a native English speaker learning Portuguese needs was this list. So pt-PT joins it, and the pt-PT pack gets the learner copy the control renders from — each label in the language of whoever would pick it, since someone on the wrong side of that switch cannot read the side they are reaching for. fr and es clear the same two bars through the same dict.db and stay out: their packs carry no learner block yet, which is a translation question rather than a data one, and the server should keep saying no until one is written. Two things that assumed learning_pair meant Chinese, now that it doesn't. The segmenter gate reads the pair as well as the direction, or a Portuguese learner would load a megabyte of Chinese word list and hover Portuguese words at /api/hanzi. And that endpoint's own comment justified skipping providerFor with a guarantee it no longer has; the real guarantee was always the caller's — it is only ever asked about tokens the Chinese segmenter found — and a stray lookup was already safe, answering a miss with an empty 200. Tests in both packages. The auth test that pinned pt-PT's refusal now pins its acceptance, with fr and es still refused; the suggestions test pins the consequence where it actually lands, which is the language she reads her advice in. Claude-Session: https://claude.ai/code/session_01GJHNvirh7Hzhc9RL3HAvz7 |
||
|
|
acb35108c0 |
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 |
||
|
|
e67f77eb05 |
The pass announces the verdict it just decided
Storing doc_lang on the document row is not enough on its own. The editor sees that row when the document is opened or saved, and the pass that decides the verdict runs after a save — so the client was always one save behind, and read-aloud is reached for precisely when she has stopped typing and no further save is coming. Heard in a browser: a Portuguese paragraph read in an American voice, twice, until another keystroke went in. /check, /voice and /collocation now answer with X-Petal-Doc-Lang. A header rather than a wider body: all three answer with a bare array of the unified pending set and every caller reads it as one, and a verdict is metadata about the pass rather than another suggestion. It reaches the app through the same handler shape onUnauthorized already uses. Claude-Session: https://claude.ai/code/session_01GJHNvirh7Hzhc9RL3HAvz7 |
||
|
|
29eb2fe1fc |
The translate card, pointed the other way, and a call that no longer happens
Phase 28's step (b): both remaining items are about direction, and both had
a wrong answer that looked right.
isTranslation could not simply be read backwards. readsAsEnglish is a
deliberately low bar — Latin letters, not swamped by another script — which
every Portuguese sentence clears as easily as English does, so swapping its
two halves would have called every genuine Portuguese correction inside a
Portuguese document a translation. The flipped direction uses sentenceLang
from doclang.go instead, where English has its own curated marker list and
has to out-evidence the pair language to win. The English-document path is
untouched; reconcilePending carries the verdict to ask the question the
right way round.
The tap-through's whole observable change is a model call that stops
happening. /suggestions/{id}/translate now recovers the explanation's
language by re-running targetFor rather than assuming the pair, which gives
today's answer everywhere except the case that was broken: the Portuguese
writer whose explanation already arrived in Portuguese, previously
round-tripped through the model into Portuguese again. It answers "" there,
and the client's existing `res.translation.trim() || explanation` fallback
seeds the bubble with the explanation itself — no frontend change at all.
It deliberately does not render that explanation into English on the
grounds that English is technically the other half: an unasked-for
rendering into the language she is practising is noise, not a seed.
Tests pin both directions of the detector, with Portuguese-in-Portuguese as
the case the file exists for, plus four handler tests through the real
/check and /translate paths — including the skipped seed asserting the
model was never called, and the learning_pair zh learner whose English
explanation still renders into Chinese.
Left of the phase: (c) the garden's language tagging and read-aloud.
Claude-Session: https://claude.ai/code/session_01GJHNvirh7Hzhc9RL3HAvz7
|
||
|
|
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
|