Files
petal/BUILD_PLAN.md
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

77 lines
8.3 KiB
Markdown

# Petal — Build Plan & Progress
Multi-session build. **Source of truth for what's done and what's next.** Update the checkboxes as work completes. `petal-spec.md` is the design spec; this file tracks execution.
## Decisions locked in (see also memory: petal-design-north-star)
- **Auth deferred** — no Authentik yet. Seed a single hardcoded local user (`id = "local"`); keep the `user_id` column so auth drops in later without a schema migration.
- **Copyleaks deferred** — needs a public webhook; skip Tier-2 plagiarism until there's a public endpoint. Tier-1 voice-consistency (local) is in scope.
- **Traefik/deploy deferred** — local dev first.
- **LLM**: Qwen 3.5 (256K context) on 64GB dual-GPU. Grammar checkpoint cap ~10K tokens (latency guard); voice pass sends whole document.
- **Suggestion anchoring**: resolve by `original` string in ProseMirror coords at render time; stored `from_pos`/`to_pos` are plaintext offsets for server-side use only. (Spec Note #6.)
- **Aesthetic is an acceptance criterion**: pretty, warm, Chinese-woman-friendly; CJK fonts first-class.
## Phases
### Phase 0 — Foundation / scaffold ✅
- [x] `git init`, `.gitignore`, remote → gitea.parodia.dev/drwily/petal
- [x] Go module (`go.mod`), directory skeleton per spec
- [x] `internal/config` env loading (local-dev defaults; auth/copyleaks fields kept for later)
- [x] Vite + React 19 + TS + Tailwind v4 scaffold in `web/` (design tokens in `@theme`, Google fonts)
- [x] Frontend embedded via `web/embed.go` (`go:embed all:dist`) + SPA handler in `cmd/server/main.go`
- [x] Dev workflow documented in README; `.env.example` added
- [x] Verified end-to-end: binary serves `/api/health` + embedded SPA + SPA fallback
### Phase 1 — Data layer ✅
- [x] SQLite (modernc) init + migrations (`internal/db/db.go`) — versioned `schema_migrations` runner, WAL + foreign keys, single writer conn
- [x] Models: User, Document, Suggestion (`internal/db/models.go`) — + type/status constants
- [x] Seed hardcoded `local` user (idempotent on startup)
- [x] Schema includes `voice` in suggestions type CHECK (full spec schema incl. plagiarism_reports, to avoid a later migration)
- [x] `db.Open` wired into `cmd/server/main.go`; `db_test.go` covers migrate/seed idempotency, CHECK constraint, FK cascade
### Phase 2 — Document CRUD + auto-save ← first "it works" milestone ✅
- [x] Doc handlers: list/create/get/update/delete (`internal/docs/handlers.go`) — chi sub-router mounted at `/api/docs`, all scoped to `local` user, partial-update via COALESCE so rename and full save share one PUT; `handlers_test.go` covers the lifecycle
- [x] Frontend DocList sidebar (create/rename/delete) — `DocList`/`DocListItem`, optimistic title/word-count patching
- [x] Tiptap EditorCore (StarterKit, Underline, TextAlign, Placeholder, CharacterCount) + inline `Toolbar` (B/I/U, H1/H2, bullets, align)
- [x] `useAutoSave` (1.5s debounce) → PUT /api/docs/:id, with `saveNow()` flush before doc switch/create
- [x] StatusBar: word count + save status (Editing→Saving→Saved, fades after 3s)
- [x] Keep `content` (Tiptap JSON) and `content_text` (plain) in sync on save — editor emits both + word_count together
### Phase 3 — LLM grammar checkpoint ✅
- [x] `LLMClient` interface + factory (`internal/llm/client.go`) — chat-model fallback in factory; doc/history truncation helpers
- [x] `vllm.go` (OpenAI-compat), `ollama.go` (native) — both behind interface; Complete + Stream; no client-level timeout (ctx deadline for Complete, open stream for SSE)
- [x] `checkpoint.go` (30s/doc `RateLimiter`), `prompts.go` — brace-matched JSON salvage from model output, empty-original drop, type normalization
- [x] `POST /api/docs/:id/check` (+ `GET /api/docs/:id/suggestions`, `POST /api/suggestions/:id/{accept,dismiss}`) in `internal/suggestions`; replaces pending set per check, leaves accepted/rejected as history; throttled checks return current set
- [x] `useCheckpoint` (4s debounce) + breathing rose checkpoint dot in StatusBar
- [x] `SuggestionHighlight` (ProseMirror **decorations**, re-anchored by `original` string on every doc change — not stored marks) + `SuggestionCard` (accept applies replacement in-editor then PATCHes; dismiss)
- [x] Suggestion colors: grammar=mint, phrasing=peach, idiom=lavender, clarity=sky (honey reserved for voice)
### Phase 4 — Ask Petal (conversational follow-up)
- [ ] `POST /api/suggestions/:id/chat` SSE streaming; server-side context injection
- [ ] AskPetal component, token-by-token render, no persistence
- [ ] CJK font fallbacks on chat bubbles (spec Note #17)
### Phase 5 — Voice consistency pass (Tier 1)
- [ ] `POST /api/docs/:id/voice`, whole-document, slow cadence / explicit action
- [ ] `voice` suggestion type, honey decoration (`--color-honey`)
### Phase 6 — Design system & polish
- [ ] Full pastel tokens, Nunito + Lora + JetBrains Mono
- [ ] Shape language, shadows, transitions
- [ ] Signature animations (suggestion fade-float, accept confetti, breathing checkpoint dot)
- [ ] Distraction-free mode
### Phase 7 — Spell check
- [ ] nspell browser-side (en-US), vendor dictionaries
### Deferred (post-v1-local)
- [ ] Authentik OIDC auth + session middleware
- [ ] Copyleaks Tier-2 + webhook HMAC
- [ ] Dockerfile, docker-compose, Traefik, deploy to write.parodia.dev
## Session log
- 2026-06-25: Spec reviewed & amended (voice/grammar decoupled, ctx cap, routes, voice DB type, honey color, string-anchoring). Build plan created.
- 2026-06-25: **Phase 0 complete.** Go module + chi server, config loader, React/Vite/Tailwind-v4 scaffold with full design tokens, frontend embedded & served by the binary, verified end-to-end. Toolchain: Go 1.24.4, Node 22, npm 10. Next: **Phase 1 (data layer)** — SQLite via modernc, models, seed `local` user.
- 2026-06-25: **Phase 1 complete.** `internal/db` package: modernc.org/sqlite (pulled go toolchain → 1.25), `Open()` does mkdir + WAL/foreign-keys DSN + versioned migration runner + idempotent local-user seed. Models with type/status constants. Wired into `main.go`; tests pass (migrate/seed idempotency, CHECK reject, FK cascade). Verified server boots and writes `petal.db`. Next: **Phase 2 (document CRUD + auto-save)** — first "it works" milestone.
- 2026-06-25: **Phase 2 complete.** Backend `internal/docs`: chi sub-router (list/create/get/update/delete) mounted at `/api/docs`, local-user scoped, RETURNING on create, COALESCE partial-update (one PUT serves rename + full save), 404/400 JSON errors; `handlers_test.go` walks the full lifecycle. Frontend: `api/client.ts`, `useAutoSave` (1.5s debounce + `saveNow` flush), `EditorCore` (Tiptap StarterKit/Underline/TextAlign/Placeholder/CharacterCount) + `Toolbar`, `DocList`/`DocListItem`, `StatusBar`, rewritten `App.tsx` orchestrating load/select/create/delete with optimistic sidebar patching. `.petal-prose` styles (Lora body, Nunito headings). tsc clean, vite build OK, go build OK; smoke-tested full CRUD incl. CJK title round-trip + SPA serve. Next: **Phase 3 (LLM grammar checkpoint).**
- 2026-06-25: **Phase 3 complete.** Backend `internal/llm`: `LLMClient` interface + factory (vLLM OpenAI-compat + Ollama native, both Complete/Stream), `prompts.go` (checkpoint + Ask Petal templates), `checkpoint.go` (brace-matched JSON salvage, per-doc 30s `RateLimiter`, doc/history truncation). `internal/suggestions`: `/api/docs/:id/check` + `:id/suggestions` + `/api/suggestions/:id/{accept,dismiss}`; each check replaces the pending set in a tx (accepted/rejected kept as history), throttled checks return the current set, positions located by `strings.Index` (advisory only). Frontend: `useCheckpoint` (4s debounce, loads existing on doc open, run-token guards stale responses), `SuggestionHighlight` Tiptap extension rendering ProseMirror **decorations** re-anchored by `original` string on every doc change (precise textblock offset→PM-pos mapping, handles inline atoms), `SuggestionCard` (type-colored tag, original→replacement diff, accept applies replacement in-editor + PATCHes, hover-bridge with close delay), breathing rose checkpoint dot in StatusBar, suggestion fade-float + breathe CSS. Tests: llm parse/rate-limit/truncate, suggestions full flow + rate-limit over httptest with a stub client. go build/vet/test clean, tsc clean, vite build OK; end-to-end smoke-tested against a fake vLLM endpoint (anchoring verified: `I has`→0:5, `two apple`→6:15) and 502 path when LLM unreachable. Next: **Phase 4 (Ask Petal SSE chat).**