Commit Graph

6 Commits

Author SHA1 Message Date
prosolis
9d2501a625 Suppress fickle re-edits of sentences the user already settled
Suppression keyed on the exact original->replacement pair, which the
model routinely sidestepped: it reverses an accepted edit (reverse
pair), re-polishes its own accepted output (new original == accepted
replacement), and the editor's smart-quote churn ("..." -> '...')
defeated even a byte-exact match. Result: a few sentences got nudged
back and forth pass after pass.

Replace the exact-pair actionedKeys with a suppressor that compares
under a normalization folding all quote variants and collapsing
whitespace, and drops a fresh suggestion when it re-touches an
already-settled span: same edit re-proposed, an original the user
already accepted/dismissed, the model re-touching its own accepted
output, or a multi-word sub-clause contained in an accepted span.

Tradeoff: once a sentence is accepted/dismissed it won't be re-flagged
until its text changes — stability over marginal improvement, the right
call for the calm ESL persona.

Claude-Session: https://claude.ai/code/session_016Yr6jELuRc7hyzYLccQKZd
2026-06-27 00:18:23 -07:00
prosolis
96f68a91ee Add deterministic mechanics suggestion family (rule-based, no LLM)
Reuse the companion's prose.ts rules engine as the single source of
deterministic detection instead of duplicating it. Applyable rules now
also emit exact-span fixes (original -> replacement) that surface as
suggestion cards; awareness-only rules (run-ons, splices, ...) stay
companion bubbles. The companion hides fix-bearing hints so a span is
never both a bubble and a card.

Spans are widened to a distinctive phrase ("a old" -> "an old",
"She have" -> "She has") so they re-anchor by string in the editor; a
lone lowercase "i" stays awareness-only since a single char can't anchor.

Backend: detection lives client-side, so the new persist-only
POST /docs/{id}/mechanics endpoint receives findings and stores them as
the 'mechanics' family with their exact offsets. It honours
actioned-suppression, leaves the LLM families untouched, and a checkpoint
no longer wipes it. fetchPending dedupes spans with mechanics winning any
collision against an LLM card (its span is exact). Migration 0008 adds the
'mechanics' suggestion type.

Client renders the mechanics fixes immediately (no LLM wait) and the cards
use a calm sage "Tidy-up" accent.

Verified end-to-end in a real browser on millenia: detect -> persist ->
render -> accept applies the fix.

Claude-Session: https://claude.ai/code/session_016Yr6jELuRc7hyzYLccQKZd
2026-06-26 20:43:37 -07:00
prosolis
8aa437ec82 Phase 12 + 13: collocation coach + vocabulary garden
Phase 12 — collocation coach: a third suggestion family for gentle
"natives usually say…" hints on non-native word pairings, reusing the
existing runPass/pendingScope/rail machinery.
- llm/collocation.go (RunCollocation, 25s floor, reuses ParseCheckpoint)
  + collocationSystemPrompt/CollocationMessages (warm, Mandarin gloss,
  defers grammar/spelling to the grammar family)
- migration 0005 rebuilds the suggestions table to extend the type CHECK
  (SQLite can't ALTER a CHECK)
- collocationScope + CollocationLimit + POST /{id}/collocation
- fix: grammarScope was `type != 'voice'` and would wipe the new
  collocation flags; now `type NOT IN ('voice','collocation')`
- frontend: --color-blossom, "Make it sound natural 🌸" pill,
  collocating/runCollocation in useCheckpoint, StatusBar dot

Phase 13 — vocabulary garden: capture looked-up words and surface them
for gentle spaced repetition.
- new internal/vocab package: migration 0006 (vocab_words, SM-2-lite
  columns, doc_id ON DELETE SET NULL, UNIQUE(user_id,word)),
  scheduler.go (Leitner ladder 1/3/7/16/35 then geometric; gentle
  "again", no streak-shaming), handlers (capture-upsert/list/due/
  review/delete, owner-scoped, SQLite-side datetime math)
- auto-capture on word lookup (dictionary-known words only, captures
  the surrounding sentence + doc_id) + 🤍/💚 toggle on WordCard
- GardenPanel: blossom grid (bloom by reps), flashcard review (sentence
  blanked, flip, again/good/easy, recognition↔production), sleepy-kitten
  footer; opened from a global 🌷 header button

Tests: TestCollocationPassCoexists, vocab scheduler + handlers, db CHECK
extended. go build/vet/test + tsc + vite + vitest (51/51) clean;
migration verified against a copy of the live DB; live backend smoke
walked the full vocab lifecycle + the warm-502 collocation path.

Claude-Session: https://claude.ai/code/session_016Yr6jELuRc7hyzYLccQKZd
2026-06-26 15:50:25 -07:00
prosolis
6783ce7a51 Suggestions: stop re-nagging resolved edits + fix typographic anchoring
Two fixes for the "accept Petal's change, then it nags about the same
sentence moments later" report:

- replacePending now suppresses any freshly-generated suggestion whose
  original->replacement matches one the user already accepted or
  dismissed for that doc. The model has no memory between passes, so
  without this it re-proposes the identical edit on the next checkpoint.

- findRange anchored suggestions by exact string match, which missed
  whenever the model echoed an `original` with plain ASCII (straight
  quotes, --, ...) while the document held the Typography-converted
  glyphs (curly quotes, em-dash, single-char ellipsis). The miss meant
  no highlight AND a silent no-op on accept, which then fed the re-nag
  above. foldTypography canonicalizes those variants (with a source
  index map for length changes) so matching survives the mismatch.

Covered by a server-side regression test (accept+dismiss then re-check
returns nothing) and frontend unit tests for the fold and anchoring.

Claude-Session: https://claude.ai/code/session_016Yr6jELuRc7hyzYLccQKZd
2026-06-26 10:35:57 -07:00
prosolis
0fa70979a0 Phase 5: voice consistency pass
Tier-1 voice-consistency pass: whole-document LLM review surfacing passages
that read tonally out of place (formal/over-polished/paraphrased-too-closely),
as honey-decorated `voice` flags with no correction (awareness-only).

- internal/llm/voice.go: RunVoice sends the whole document (no TruncateDoc),
  MaxTokens 2048, 20s per-doc floor (VoiceInterval). Standalone voice prompt
  in prompts.go (not bundled with the grammar checkpoint, per spec).
- internal/suggestions: POST /api/docs/:id/voice. replacePending is now
  family-scoped (pendingScope) so grammar and voice never clobber each other's
  pending flags; both passes return the unified pending set. check/voice share
  one runPass helper. TestVoicePassCoexists covers both directions.
- Frontend: api.voiceDoc, useCheckpoint voicing/runVoice, honey "Check my
  voice" toolbar pill, breathing honey dot in StatusBar.

Claude-Session: https://claude.ai/code/session_016Yr6jELuRc7hyzYLccQKZd
2026-06-25 21:16:53 -07:00
prosolis
a4069d5755 Phase 3: LLM grammar checkpoint
Backend (internal/llm): backend-agnostic LLMClient interface + factory
with vLLM (OpenAI-compat) and Ollama (native) clients, each Complete +
Stream. prompts.go holds the checkpoint and Ask Petal templates;
checkpoint.go salvages JSON from model output (brace-matched), enforces a
per-doc 30s RateLimiter, and truncates the doc to a latency cap.

internal/suggestions: POST /api/docs/:id/check runs a checkpoint and
replaces the doc's pending suggestions in one tx (accepted/rejected kept
as history); GET /api/docs/:id/suggestions lists pending;
POST /api/suggestions/:id/{accept,dismiss} resolves one. Throttled checks
return the current set rather than erroring.

Frontend: useCheckpoint (4s debounce, loads existing on open, stale-guard
tokens); SuggestionHighlight renders ProseMirror decorations re-anchored
by the `original` string on every doc change (not stored marks), with
precise textblock-offset→PM-position mapping; SuggestionCard shows the
type tag + diff + explanation and applies the replacement in-editor on
accept; breathing rose checkpoint dot in the StatusBar; fade-float +
breathe animations.

Tests: llm parse/rate-limit/truncate; suggestions full flow + rate-limit
over httptest with a stub client. Smoke-tested end-to-end against a fake
vLLM endpoint (anchoring verified) and the LLM-unreachable 502 path.

Claude-Session: https://claude.ai/code/session_016Yr6jELuRc7hyzYLccQKZd
2026-06-25 20:45:30 -07:00