When she writes in Chinese, say Translate — not Clarity

She reaches for her own language mid-sentence when English won't come, and
Petal already handled it: it found the span and rendered it into English. It
just filed the result as a Clarity fix, so the pair model's flagship moment
read as tidying up her Chinese.

The type is now derived from the span rather than asked of the model. A type is
structural, and a model that re-reasons every pass would drift between labels
for a sentence nobody had touched — the instability the last session spent
itself removing. The label the model volunteers is still ignored.

Only the grammar checkpoint can be promoted. A pass with a forced type owns its
family: voice reads paragraphs for tone and its rows carry no replacement, so a
"translation" there would be a card offering nothing to accept.

zh is a different script and counting Han runes is close to certain. The Latin
pairs share an alphabet with English and get none of that, so they fall back to
function words and need two before Petal claims anything — with every word that
is also English left out, even the common ones. The heuristic is justified by
how cheap being wrong is: it changes a coloured pill, and nothing else.

The pill is the one bilingual type name in the rail. Every other type stays
English because those are the terms she is learning; this card's whole subject
is her own language. And it stops truncating its two lines — elsewhere the diff
is a word and the explanation is what she reads, but here the two sentences are
the card.

Two things only the running page could report. The inline underline was
invisible: the decoration carries a per-type class and the base rule is a
transparent border, so a type with no colour rule gets no mark at all. And at
1517×810 with the document list open there is no rail — the margin is 258 where
railEnabled wants 348 — so what she gets is the inline hover card. Item 7 is
written the other way round.

Migration 0015 rebuilds the suggestions table for the CHECK, which makes it the
first one here that could quietly drop her rows; there is a test that carries
every column, both timestamps and both indexes across it.

Claude-Session: https://claude.ai/code/session_016y6gyuHkQXPiEuW8RGQyua
This commit is contained in:
prosolis
2026-07-28 00:09:42 -07:00
parent 3bcc967f51
commit 25e415daa2
20 changed files with 917 additions and 15 deletions
+107
View File
@@ -399,6 +399,106 @@ card's strikethrough line so she can still see what she wrote).
card within one check cycle; accepting swaps in the English; an
English-only doc and a Chinese-only doc are unaffected.
### 5 — DONE (sixth session). The label was the whole gap, and it isn't the model's to give.
The re-scoped premise held: Petal already found the span and already rendered
it into English. Only the type was wrong. So the work was to decide that type
*structurally* rather than ask for it — a model that re-reasons every pass
would drift between labels for a sentence that hadn't changed, which is exactly
the instability item 2 just spent a session removing.
**Implemented:**
- `language.go``isTranslation(original, replacement, pairLang)`. Both halves
must hold: the span reads as her language *and* what Petal offers back reads
as English. The second half is not decoration — a Chinese span rewritten into
different Chinese is something else, and Petal has no business calling it a
translation.
- `reconcile.go` — the promotion sits at the single point where a type is
stamped, and only on the open-typed grammar checkpoint. A pass with a
`forceType` owns its family outright: voice reads paragraphs for tone and its
rows carry no replacement, so a "translation" there would be a card offering
nothing to accept. There is a test for exactly that.
- `normalizeType` still refuses `translate` from the model, deliberately. A
model that volunteers the label lands on grammar and is then promoted — or
not — on the evidence.
- Migration `0015` rebuilds the suggestions table for the extended `type` CHECK
(SQLite can't ALTER one), as `0005` and `0008` did before it.
- Client: `--color-jade`, the `translate` entry in `TYPE_META`, and
`.petal-suggestion-translate` for the inline underline.
- The pill is the one bilingual type name in the rail — `翻译 · Translate`,
`Tradução · Translate`, `Traduction · Translate`, from the pack. Every other
type stays English on purpose: those are the terms she is learning, and she is
learning them in English. This card's whole subject is her own language.
- The rail card stops truncating its two lines for a translation. Elsewhere the
original and replacement differ by a word and the explanation below is what
she reads; here the two lines are a whole sentence in each language and *are*
the card.
**The pair families need different tests, and pretending otherwise was the
trap.** The item's plan says to reuse the CJK detection from spellcheck — which
works, for zh, because it is a different script. It gives nothing for pt-PT, fr
or es, where no such signal exists. Those fall back to function words, and need
two distinct markers before Petal will claim anything; the lists deliberately
omit every word that is *also* English (`do`, `con`, `ya`, `todo`, and the
pan-Romance shorts), even where that costs a very common one. A single marker is
never enough, so a one-word Portuguese span won't trip it — a single word is a
vocabulary question, not a translation. The whole heuristic is justified by how
cheap its failure is: a wrong answer changes a coloured pill and nothing else,
because the replacement, the explanation and the Accept button are identical
either way.
**Two things only the browser could have told me.**
- **The inline underline was invisible.** The decoration plugin emits a
per-type class, and `.petal-suggestion` sets `border-bottom: 2px solid
transparent` — so a type with no colour rule renders with no underline at
all. Every test passed; the flagship span simply had no mark under it on
screen. Found by looking at the page, and it is the reason this doc keeps
insisting on that.
- **At her viewport there is no rail.** `railEnabled` needs 348 px beside the
editor, and at 1517×810 with the document list open the margin is 258 — so
the card she actually gets is the inline hover panel, not a rail card. Worth
knowing before item 7 is scoped: that item assumes the rail is what she sees
and treats the anchored popover as the missing half. On this screen it is the
other way round. (The fifth session measured rail cards at the same width, so
some state does reach it; not chased here.)
**Verified against the running binary**, with a stand-in model server so no VPN
or GPU was involved. The stub types the Chinese finding `"clarity"` — exactly
what the live build did — so the label on screen can only have come from Petal's
own detection. Through the real `/check`: `grammar` for the English sentence,
`translate` for the Chinese one. In the browser: the jade underline distinct
from grammar's mint, and the card showing the `翻译 · Translate` pill, the
Chinese struck through and wrapped over two lines rather than clipped, the
English rendering, the bilingual explanation, and Accept.
**Not verified in a browser:** the *rail* card's version of the same thing. The
rail is unreachable on this 1517 px display (above), and forcing it by hand kept
being overridden by React's own layout. Its label goes through the same
`typeLabel` call the hover card just proved, and its diff differs only by a
class toggle — but that is a reading, not a measurement, and it is written down
as one.
Coverage: `language_test.go` (the flagship sentence; a lone Han rune; one
Chinese word inside English prose; Chinese→Chinese; a Chinese span on the wrong
pair; all three Latin pairs; an English sentence stuffed with pan-Romance
lookalikes; French elision; unknown and absent pairs), `translate_type_test.go`
(the type through the real `/check`, an English correction keeping its own type,
and voice unable to mint one), `suggestionMeta.test.ts` (every type has a colour
and a name, translate's colour is its own, the pill is bilingual per pair and
every other pill isn't), and `db_test.go`'s
`TestTranslateTypeMigrationPreservesRows` — 0015 rebuilds the table, so it is
the first migration here that could silently *drop her rows*; every column,
both timestamps and both indexes are asserted across it.
One test changed contract: the pt-PT pack's Brazilian-forms grep searched
`JSON.stringify(pack)`, which includes field *names* — and duly failed on
`translateLabel`, since it lowercases to "transla·tela·bel" and so "contains"
the pt-BR *tela*. It now searches the pack's copy only, with two canaries, since
every assertion in it is a negative and a haystack that quietly went empty would
make the whole test pass by having nothing to search.
## 6. Ask Petal answers: bilingual, and room to read
The card's *explanation* is bilingual, but the Ask Petal *answer* came back
@@ -504,6 +604,13 @@ vitest tests. So the three sessions of work she hasn't seen, she can now
see. Untouched: 6, 7, 8, item 3's incremental half, item 5's re-scoped
Translate card type.)*
*(Sixth session, second half: item 5 done — see the subsection under it. It sits
on `feat/translate-card`, **not deployed**; `main` is at the stack merge. Two
findings there are worth reading before picking the next item: the inline
underline needs a per-type CSS rule or it renders invisibly, and at her actual
viewport the rail is disabled — the inline hover card is what she sees, which
inverts item 7's premise. Untouched: 6, 7, 8, item 3's incremental half.)*
**Suggested next (sixth session onward):** items 6, 7, 8 are all untouched
and all small; item 5's re-scoped Translate card type is the cheapest
*visible* win (see its Status note — the span is already detected and