Grammar lite, the false-friend list, the daily invitation and the offline miscollocations — the four remaining §5–§6 items, all client-side and all alive on a box that cannot reach the model. The offline collocations forced a schema change. `type` had been doubling as the answer to "which engine found this" — `mechanics` meant offline — and that stops being true the moment an offline rule proposes a collocation. Migration 0013 adds `source` (llm | local) and every pass now scopes its DELETE by engine; without it the coach silently wiped every offline chunk on the page. Existing rows backfill by type, so a pre-0013 collocation row is claimed as the coach's, which it was: the offline list did not exist yet. The rule pack is hand-curated rather than mined, and the entries left out are the point — `married with` is wrong until "married with children", `arrive to` wants at or in depending on the noun. A pack running on every keystroke must not correct correct writing. Claude-Session: https://claude.ai/code/session_016y6gyuHkQXPiEuW8RGQyua
136 lines
5.9 KiB
Go
136 lines
5.9 KiB
Go
package db
|
|
|
|
import "time"
|
|
|
|
// User is an account. Its ID is the OIDC subject for anyone who signed in, or
|
|
// LocalUserID for the pre-auth single user (and for local development, where
|
|
// StaticResolver still hands out that id).
|
|
type User struct {
|
|
ID string `json:"id"`
|
|
Email string `json:"email"`
|
|
DisplayName string `json:"display_name"`
|
|
CreatedAt time.Time `json:"created_at"`
|
|
|
|
// PairLang is the X in this writer's (English + X) language pair — "zh"
|
|
// today, "pt-PT"/"fr"/"es" once the langpacks land. It selects the UI copy
|
|
// and dictionary set, not the language they may type in.
|
|
PairLang string `json:"pair_lang"`
|
|
}
|
|
|
|
// Document is a single piece of writing. `Content` is the Tiptap JSON document
|
|
// (source of truth for the editor); `ContentText` is the flattened plain text
|
|
// kept in sync on every save and fed to the LLM.
|
|
type Document struct {
|
|
ID string `json:"id"`
|
|
UserID string `json:"user_id"`
|
|
Title string `json:"title"`
|
|
Content string `json:"content"` // Tiptap JSON
|
|
ContentText string `json:"content_text"` // plain text for the LLM
|
|
Tone string `json:"tone"` // target writing tone; steers LLM advice
|
|
WordCount int `json:"word_count"`
|
|
CreatedAt time.Time `json:"created_at"`
|
|
UpdatedAt time.Time `json:"updated_at"`
|
|
|
|
// PreserveHistory opts this document out of auto-snapshot pruning so its
|
|
// full writing trail survives as authorship evidence (see the passport).
|
|
PreserveHistory bool `json:"preserve_history"`
|
|
}
|
|
|
|
// DocumentVersion is a point-in-time snapshot of a document's body, captured so
|
|
// a writer can recover from a bad edit or an unwanted change. `Content` mirrors
|
|
// the document's Tiptap JSON at snapshot time; `Kind` records why it was taken
|
|
// (see the kind constants). List responses omit the heavy Content/ContentText
|
|
// fields (the `omitempty`-friendly zero strings) and load them only on preview
|
|
// or restore.
|
|
type DocumentVersion struct {
|
|
ID string `json:"id"`
|
|
DocID string `json:"doc_id"`
|
|
Title string `json:"title"`
|
|
Content string `json:"content,omitempty"` // Tiptap JSON; omitted in list view
|
|
ContentText string `json:"content_text,omitempty"` // plain text; omitted in list view
|
|
WordCount int `json:"word_count"`
|
|
Kind string `json:"kind"` // auto | manual | pre_restore
|
|
CreatedAt time.Time `json:"created_at"`
|
|
|
|
// ContentHash chains this snapshot to the previous one (PrevHash), so a
|
|
// history that was edited or thinned after the fact fails verification.
|
|
// Both are empty for snapshots taken before the chain existed. Omitted from
|
|
// list responses; the passport loads them explicitly.
|
|
ContentHash string `json:"content_hash,omitempty"`
|
|
PrevHash string `json:"prev_hash,omitempty"`
|
|
}
|
|
|
|
// Document version kinds, mirrored from the schema CHECK constraint.
|
|
const (
|
|
VersionKindAuto = "auto" // throttled background snapshot on save
|
|
VersionKindManual = "manual" // explicit "save a restore point"
|
|
VersionKindPreRestore = "pre_restore" // safety copy taken just before a restore
|
|
)
|
|
|
|
// Tag is a user-scoped label for organizing documents. `Color` is a palette key
|
|
// (rose, mint, peach, lavender, sky, honey) the frontend maps to a CSS color;
|
|
// storing the key (not a hex value) keeps tags in step with the design tokens.
|
|
// `DocCount` is populated only by the tag-list endpoint (how many documents wear
|
|
// the tag); it's omitted from per-document tag lists.
|
|
type Tag struct {
|
|
ID string `json:"id"`
|
|
Name string `json:"name"`
|
|
Color string `json:"color"`
|
|
DocCount int `json:"doc_count,omitempty"`
|
|
}
|
|
|
|
// Tag color palette keys, mirrored on the frontend. Kept small and aligned with
|
|
// the existing design tokens; unknown values fall back to rose client-side.
|
|
const (
|
|
TagColorRose = "rose"
|
|
TagColorMint = "mint"
|
|
TagColorPeach = "peach"
|
|
TagColorLavender = "lavender"
|
|
TagColorSky = "sky"
|
|
TagColorHoney = "honey"
|
|
)
|
|
|
|
// Suggestion is a single LLM-proposed edit anchored to a span of the document.
|
|
//
|
|
// FromPos/ToPos are plaintext offsets into ContentText for server-side use only;
|
|
// the frontend re-anchors by matching the `Original` string in ProseMirror
|
|
// coordinates at render time (spec Note #6). `Replacement` is empty for `voice`
|
|
// flags — those are awareness-only, with no correction to apply.
|
|
type Suggestion struct {
|
|
ID string `json:"id"`
|
|
DocID string `json:"doc_id"`
|
|
FromPos int `json:"from_pos"`
|
|
ToPos int `json:"to_pos"`
|
|
Original string `json:"original"`
|
|
Replacement string `json:"replacement"`
|
|
Explanation string `json:"explanation"`
|
|
Type string `json:"type"` // grammar | phrasing | idiom | clarity | voice | collocation
|
|
Status string `json:"status"` // pending | accepted | rejected
|
|
// Source names the engine that proposed the edit, not its family: an offline
|
|
// rule and the model can both propose a collocation, and the writer is never
|
|
// told which one spoke. It exists so each pass can replace its own rows.
|
|
Source string `json:"source"` // llm | local
|
|
CreatedAt time.Time `json:"created_at"`
|
|
}
|
|
|
|
// Suggestion type and status values, mirrored from the schema CHECK constraints.
|
|
const (
|
|
SuggestionTypeGrammar = "grammar"
|
|
SuggestionTypePhrasing = "phrasing"
|
|
SuggestionTypeIdiom = "idiom"
|
|
SuggestionTypeClarity = "clarity"
|
|
SuggestionTypeVoice = "voice"
|
|
SuggestionTypeCollocation = "collocation"
|
|
SuggestionTypeMechanics = "mechanics" // deterministic rule-based pass (no LLM)
|
|
|
|
// Who proposed it. The offline rule pack ('local') runs on every edit inside
|
|
// the browser and survives a VPN-down box; the model ('llm') adds the long
|
|
// tail when it is reachable.
|
|
SuggestionSourceLLM = "llm"
|
|
SuggestionSourceLocal = "local"
|
|
|
|
SuggestionStatusPending = "pending"
|
|
SuggestionStatusAccepted = "accepted"
|
|
SuggestionStatusRejected = "rejected"
|
|
)
|