prosolis 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
2026-07-29 18:40:22 -07:00

🌸 Petal

A self-hosted, privacy-first writing editor with warm bubbly design, auto-save, and local-LLM grammar/ESL suggestions. See petal-spec.md for the full design spec and BUILD_PLAN.md for build progress.

Stack

Go + chi backend · SQLite (modernc, pure Go) · React + Vite + Tiptap + Tailwind v4 frontend · local vLLM/Ollama for AI suggestions. Single-binary deployment (frontend embedded via go:embed).

Local development

Two processes during development:

# 1. Backend (serves /api on :8080)
go run ./cmd/server

# 2. Frontend dev server (HMR on :5173, proxies /api → :8080)
cd web && npm install && npm run dev

Open http://localhost:5173 while developing.

Production build (single binary)

cd web && npm run build      # emits web/dist (embedded by the Go binary)
cd .. && go build -o petal ./cmd/server
./petal                      # serves UI + API on :8080

Configuration is via environment variables — copy .env.example to .env.

Deployment

docker compose up -d --build     # petal + the two Piper read-aloud sidecars

Behind Traefik on the parodia.dev VPS; vLLM stays on millenia over headscale. Full runbook — first deploy, the LLM link, backups and restore — in deploy/README.md.

Status

Early build, multi-session. Auth (Authentik OIDC) is next; Copyleaks plagiarism is still parked — see BUILD_PLAN.md.

S
Description
Petal is a self-hosted, privacy-first writing editor for an ESL user. It replaces Grammarly with a warm, bubbly web app that combines Tiptap rich text editing, auto-save cloud storage, and periodic AI-powered grammar/ESL suggestions backed by a local vLLM inference endpoint.
Readme
27 MiB
Languages
TypeScript 49.2%
Go 44.1%
Python 3.8%
CSS 1.7%
Shell 0.9%
Other 0.2%