Let the garden keep what she was given, not only what she sought

Two halves of the same idea, both read out of work Petal already
records.

Planting: an accepted collocation is a learnable chunk, so it becomes a
phrase card. The scheduler didn't need to know — a three-word chunk
climbs the ladder exactly like a looked-up word. What needed care was
deciding what *isn't* a chunk (single words are word choice; a
six-word-plus "collocation" is a rewritten sentence, and sentences make
miserable flashcards), and that the example must be the *corrected*
sentence — the stored draft still holds the phrasing she just left
behind. Re-accepting the same chunk leaves the existing card alone
rather than resetting a schedule it has been climbing. The whole thing
is best-effort: accepting an edit must never fail because a flashcard
couldn't be made.

The growth journal: kept this month beside kept the month before, the
phrasing that stuck, the patterns that faded. The queries were the easy
part; the honesty is the feature. "Stuck" needs the phrase in a *second*
document, because one document is just the edit where she left it.
"Faded" says nothing at all unless she has been writing lately —
otherwise a month away from Petal comes back to her as progress, which
is the one way this could lie. And a suggestion had to start recording
when she *decided* it, not when the model proposed it, so 0012 adds
resolved_at and backfills the old rows to their created_at.

It lives as a second tab in the garden, and it feeds the kitten: after
an accept she now sometimes hears something true of her alone, once per
line, half the time, never waited for.

Claude-Session: https://claude.ai/code/session_016y6gyuHkQXPiEuW8RGQyua
This commit is contained in:
prosolis
2026-07-27 14:16:59 -07:00
parent 7b845644be
commit e9b8595456
19 changed files with 1328 additions and 5 deletions
+19 -2
View File
@@ -257,8 +257,25 @@ Phase 20 left this ready: `dict.db` on the VPS now holds all five languages, and
### Phase 22 — Learning loop + code-first layers
Each item independent and small; order within is free (SUGGESTIONS §5–§6).
- [ ] **Growth journal** (Q3 settled): local aggregation over accepted suggestions; growth-only, self-comparison-only framing; feeds companion cheers
- [ ] **Plant accepted collocations** in the vocabulary garden as phrase cards (scheduler unchanged)
**First two built 2026-07-27** (user: "continue the build plan"; code only, no VPS work — not deployed, and there is no migration to undo, so it is a rebuild whenever the user wants it).
- [x] **Growth journal** (Q3 settled) ✅ (2026-07-27) — `GET /api/suggestions/growth`, a read-side view of a table Petal already keeps: no new capture, no model call, nothing leaves the box. Three signals, and the work was in deciding which ones are *honest* rather than in computing them.
- **Kept** — edits she took on board in the last 30 days, with the 30 before it offered flat beside it. That second number is the whole of the self-comparison rule: there is no target, no average and no other account anywhere in these queries.
- **Stuck** — accepted phrasing that now appears in **two or more** of her own documents. One document is not evidence: it is the edit itself, still sitting where it was applied. The second is her reaching for the phrase on her own, which is the only thing the line actually claims. Candidate phrases are filtered through `vocab.PhraseKey`, the *same* definition of "a learnable chunk" the garden plants, so the journal and the garden can never disagree about what counts.
- **Faded** — a pattern corrected ≥2× in the earlier window and not since. **Guarded by "has she written lately?"**: without that check, a month away from Petal is reported back to her as progress, which is the one way this feature could lie. Test named for the guard, not the query.
- **The dates had to come from her decision, not the model's proposal** — migration `0012_suggestion_resolved_at`. `created_at` is when a checkpoint *offered* an edit; a suggestion offered in April and accepted in June is June's growth. Existing rows backfill to `created_at`, which is exactly the approximation the journal would otherwise have had to make (and is very nearly right — edits are settled minutes after a checkpoint); pending rows keep NULL, because nothing has been decided. Tested against a database rewound to before the column, since that is the only shape the live box will ever present.
- **Surface**: a second tab *inside* the garden (🌷 Garden / 🌱 Growth) rather than new chrome — same idea seen twice, the garden as objects and the journal as change over time. A review session hides the tabs: mid-flashcard is no moment to be offered a different page.
- **Feeds the companion**, which was the point: on an accept the kitten prefers a line that is true of *her* ("you're using 'make a decision' on your own now! 🌱") over one that would fit anybody — half the time, so it stays a surprise, once per line per session, so personal praise never becomes wallpaper. The journal is fetched on the first accept and **never awaited**: the cheer goes out now, personal or not.
- Copy is bound by the same two rules as the SQL, and a test greps both packs for *error/mistake/wrong/streak/average/erro/errada/错误* — the framing is the feature, and it's the part a future edit would quietly undo.
- [x] **Plant accepted collocations** in the vocabulary garden as phrase cards ✅ (2026-07-27) — scheduler untouched, as predicted: `vocab.Plant` writes the same row `capture` does, so a three-word chunk climbs the SM-2-lite ladder exactly like a looked-up word, blossoms with `reps`, and cloze-blanks in review. The garden now holds both halves of learning — what she sought out, and what she was gently given.
- **Only collocations.** The other families fix *this* sentence (a comma, "their"→"there"); a collocation is the one that hands over something reusable, and reusable is the only thing worth reviewing in a week.
- **What isn't a chunk**: `PhraseKey` rejects single words (that's word choice, and lookup already gardens it), anything over 6 words or 60 runes (a rewritten sentence wearing a collocation's label makes a miserable flashcard), and digit/symbol-only text. The cap counts **runes** — a byte cap would drop Portuguese chunks for being accented.
- **The example is the *corrected* sentence.** The stored `content_text` is still the pre-accept draft (the client applies the replacement in the editor), so the sentence around `original` is extracted and swapped server-side. Otherwise the flashcard would quiz her on the phrasing she had just left behind.
- **ON CONFLICT DO NOTHING**, unlike capture's refresh-the-context upsert. Accepting the same collocation again months later is evidence the chunk is still being learned; the worst possible response is to overwrite its first context and reset a schedule it has been climbing. Test asserts the card keeps `interval_days = 7`.
- **Best-effort, always.** Planting runs after the status write and swallows its own errors: accepting an edit is what she asked for, and it must not fail — or feel slower — because a flashcard couldn't be made. A rewrite too long to plant still returns 204.
- Verified live on a throwaway DB (:8099, no dictionary, no LLM): accept → card `make a decision` with example *"I had to make a decision about the job."* bounded to its own sentence, then the journal reporting `kept:1`, `stuck:[{make a decision, docs:2}]` once the phrase appeared in a second document, and a seeded two-month-old pattern surfacing under `faded`.
- Tests: `internal/vocab/plant_test.go` (PhraseKey table incl. rune-vs-byte, plant-once, unplantable is a silent no-op), `internal/suggestions/plant_test.go` (corrected-sentence example, only-collocations, idempotent-and-never-resets, sentence-rewrite skipped without failing the accept), `internal/suggestions/growth_test.go` (both windows, stuck needs a second document, the wrote-recently guard, still-happening excluded, and a per-writer isolation test seeding bob), `internal/db/db_test.go` (the backfill). Frontend: `journalCheers.test.ts` (silent before the fetch lands, once per line, one fetch however often warmed, silent on failure, pack resolved at call time) plus journal assertions in `i18n.test.ts`.
- Verified: go build/vet, `go test ./internal/...` clean, tsc, vite build, vitest 131/131.
- ⚠️ **Not deployed and not seen in a browser.** Same standing gap as Phase 21: no pt-PT account exists, and this was a code-only session. The pt-PT journal copy is part of the pack a native speaker still has not reviewed.
- [ ] **Daily writing invitation** from the companion (no streaks, declining is fine)
- [ ] **False-friend list** per pair (curated data, WordCard heads-up + gentle flag)
- [ ] **Embedded miscollocation list** (code-first under the collocation family; LLM adds the long tail when reachable)