Code-review fixes for collocation coach + vocab garden
Correctness: - useCheckpoint: clear the busy flag unconditionally so overlapping explicit passes don't strand each other's spinner; explicit actions now also supersede a queued auto-check and clear the stranded "checking" dot. Deduped runVoice/runCollocation into runExplicitPass. - EditorCore: token-guard the auto-capture so a late capture can't resurrect a removed word; move toggleSaveWord side effects out of the setWordInfo updater (StrictMode double-fire); fix sentenceAround offset desync via shared exampleAt (textBetween + parentOffset, single resolve); optimistic saved state so the heart doesn't flash unsaved. - vocab capture: normalize word to lower+trim (matches lexicon) so "Apple"/"apple" don't make duplicate cards; check rows.Err() in queryList. - GardenPanel: Promise.allSettled so a /due failure doesn't blank the whole garden; scrim click during review ends the review (mirrors Esc); gate footer on !error; O(1) due lookup via a Set. Features requested in review: - Definition-only review card: add vocab_words.definition (migration 0007) as an English fallback meaning, threaded through capture and used by review/garden when there's no Chinese gloss. - Scheduler caps: maxEase 3.0 + maxInterval 365d so "easy" growth can't push a word out of rotation for years. Tests: TestCaptureCaseInsensitive, TestCaptureStoresDefinitionFallback, TestCapsBoundGrowth. go build/vet/test, tsc, vitest 51/51, vite build clean. Claude-Session: https://claude.ai/code/session_016Yr6jELuRc7hyzYLccQKZd
This commit is contained in:
@@ -23,10 +23,17 @@ var ladder = []int{1, 3, 7, 16, 35}
|
||||
|
||||
const (
|
||||
minEase = 1.3
|
||||
maxEase = 3.0 // ease ceiling — keeps "easy" from compounding growth without bound
|
||||
startEase = 2.5
|
||||
easyBonus = 1.3 // multiplier applied on top of a "good" step for "easy"
|
||||
easeAgainDelta = -0.20 // ease nudged down on a lapse (still floored at minEase)
|
||||
easeEasyDelta = 0.15 // ease nudged up when a card feels easy
|
||||
|
||||
// maxInterval caps how far a card can drift into the future. Even a word
|
||||
// graded "easy" many times resurfaces about once a year, so nothing silently
|
||||
// leaves the garden forever (the whole point is to keep words in gentle
|
||||
// rotation, not to retire them).
|
||||
maxInterval = 365
|
||||
)
|
||||
|
||||
// State is the spaced-repetition state of one card. It mirrors the scheduling
|
||||
@@ -61,7 +68,7 @@ func (s State) next(grade Grade) State {
|
||||
reps := s.Reps + 1
|
||||
return State{
|
||||
Reps: reps,
|
||||
Interval: int(math.Round(float64(goodInterval(reps, s.Interval, ease)) * easyBonus)),
|
||||
Interval: clampInterval(int(math.Round(float64(goodInterval(reps, s.Interval, ease)) * easyBonus))),
|
||||
Ease: ease,
|
||||
Lapses: s.Lapses,
|
||||
}
|
||||
@@ -69,7 +76,7 @@ func (s State) next(grade Grade) State {
|
||||
reps := s.Reps + 1
|
||||
return State{
|
||||
Reps: reps,
|
||||
Interval: goodInterval(reps, s.Interval, ease),
|
||||
Interval: clampInterval(goodInterval(reps, s.Interval, ease)),
|
||||
Ease: ease,
|
||||
Lapses: s.Lapses,
|
||||
}
|
||||
@@ -93,5 +100,15 @@ func clampEase(e float64) float64 {
|
||||
if e < minEase {
|
||||
return minEase
|
||||
}
|
||||
if e > maxEase {
|
||||
return maxEase
|
||||
}
|
||||
return e
|
||||
}
|
||||
|
||||
func clampInterval(d int) int {
|
||||
if d > maxInterval {
|
||||
return maxInterval
|
||||
}
|
||||
return d
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user