Documentation
¶
Overview ¶
Package engram is a storage-agnostic spaced-repetition engine that wraps go-fsrs (the Free Spaced Repetition Scheduler). It knows nothing about any particular storage backend, UI, or transport — callers own persistence via the SkillStore and ReviewStore interfaces and drive the engine explicitly.
The package is deterministic by design: time is always passed in as an explicit now time.Time parameter rather than read from the clock, and any randomness needed by callers (e.g. in the quiz subpackage) is supplied via an injected *rand.Rand. This makes scheduling, queueing, and quiz generation fully reproducible in tests.
Index ¶
- func Accuracy(log []Review) float64
- func CountNewIntroduced(log []Review, now time.Time, loc *time.Location) int
- func DueForecast(cards []CardState, days int, now time.Time, loc *time.Location) []int
- func RemainingIntroBudget(cap, introducedToday int) int
- func Streak(log []Review, now time.Time, loc *time.Location) int
- type CardState
- type ContentID
- type Deck
- type DeckID
- type IntroConfig
- type IntroItem
- type IntroOutcome
- type Lifecycle
- type QueueConfig
- type QueueItem
- type Rating
- type Review
- type ReviewStore
- type Scheduler
- func (s *Scheduler) Introduce(o IntroOutcome, now time.Time) (life Lifecycle, cs CardState, hasCard bool)
- func (s *Scheduler) NewCardState(now time.Time) CardState
- func (s *Scheduler) Next(cs CardState, now time.Time, r Rating) (CardState, Review)
- func (s *Scheduler) SeedCardKnown(now time.Time) CardState
- type SchedulerOption
- type Skill
- type SkillID
- type SkillStore
- type State
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func Accuracy ¶
Accuracy is the share of reviews in log with Rating != Again (Hard, Good, and Easy all count as correct). Returns 0 for an empty log.
func CountNewIntroduced ¶
CountNewIntroduced counts entries in log whose StateBefore is StateNew and whose ReviewedAt falls on the same calendar day as now, both converted to loc (nil defaults to time.UTC).
func DueForecast ¶
DueForecast buckets cards by the calendar day (in loc, nil defaulting to time.UTC) their Due falls on, relative to now's day. Bucket 0 is "due today or earlier" — overdue cards clamp into today's bucket — bucket i (i>0) is now's day + i. Cards due beyond now's day + days - 1 are not counted. Returns a slice of length days, or an empty non-nil slice if days <= 0.
func RemainingIntroBudget ¶ added in v0.3.0
RemainingIntroBudget returns max(0, cap-introducedToday); cap==0 → math.MaxInt (unlimited). Small pure helper mirroring the review-cap arithmetic.
func Streak ¶
Streak returns the number of consecutive calendar days (in loc, nil defaulting to time.UTC) with at least one review, counting back from now's day. If now's day itself has no review yet, the count still starts from yesterday instead of resetting to 0 — a day that "just hasn't been studied yet" doesn't erase an otherwise-current streak. Returns 0 if log is empty or neither today nor yesterday (relative to now) has a review.
Types ¶
type CardState ¶
type CardState struct {
Due time.Time
Stability float64
Difficulty float64
Reps int
Lapses int
State State
LastReview time.Time // zero if never reviewed
}
CardState is the per-user FSRS memory state for one skill.
type IntroConfig ¶ added in v0.3.0
type IntroConfig struct {
MaxNewPerDay int // 0 = unlimited
}
IntroConfig configures the daily introduction budget.
type IntroItem ¶ added in v0.3.0
IntroItem pairs an item id with its lifecycle for intro selection.
func NextIntroductions ¶ added in v0.3.0
func NextIntroductions(items []IntroItem, introducedToday int, cfg IntroConfig) []IntroItem
NextIntroductions returns up to the remaining daily budget of items whose Lifecycle == LifecycleNew, PRESERVING the caller's slice order (which encodes priority). introducedToday is how many intros were already delivered today. Deterministic: no shuffling, stable selection. Returns an empty non-nil slice when the budget is exhausted or nothing is New.
type IntroOutcome ¶ added in v0.3.0
type IntroOutcome int8
IntroOutcome is the user's response to an introduction card (the three buttons).
const ( IntroGotIt IntroOutcome = iota // → Introduced, fresh FSRS card (StateNew, due now) IntroKnown // → Known, no card, never quizzed IntroTestMe // → Reviewing, FSRS card seeded with high initial stability )
type Lifecycle ¶ added in v0.3.0
type Lifecycle int8
Lifecycle is the gating state of an item for one user. It is ORTHOGONAL to CardState (the FSRS memory state) and is persisted as a sibling field, never inside CardState — CardState must remain a lossless mirror of fsrs.Card.
const ( LifecycleNew Lifecycle = iota // 0: not introduced; no card; eligible for the intro queue; never quizzed LifecycleIntroduced // 1: intro acknowledged; card exists; FSRS State New/Learning (not yet graduated) LifecycleReviewing // 2: graduated into durable review; FSRS State Review/Relearning LifecycleKnown // 3: user asserted mastery; terminal; no active card; reversible later )
func LifecycleFor ¶ added in v0.3.0
LifecycleFor derives the Introduced/Reviewing split from a card's FSRS state. Known and New are lifecycle-only (no card) and are never returned here; the caller sets those explicitly on the intro outcome. This keeps the coarse progress label cheap to store yet always consistent with the card.
type QueueConfig ¶
type QueueConfig struct {
MaxNewPerDay int // 0 = unlimited
}
QueueConfig configures NextDue's new-card cap.
type QueueItem ¶
QueueItem pairs a Skill with its current FSRS card state, as loaded from a SkillStore for the skills in a deck.
func NextDue ¶
func NextDue(items []QueueItem, newIntroducedToday int, cfg QueueConfig, now time.Time) (item QueueItem, ok bool)
NextDue picks the next item to study out of items. A due-or-overdue review-state card (State != StateNew, Due <= now) always wins, choosing the earliest Due (ties keep whichever was encountered first, for determinism). Only when no review-state card is due is a new (StateNew) card offered instead — again the earliest-Due one whose Due <= now — and only while newIntroducedToday is below cfg.MaxNewPerDay (0 means unlimited). ok=false with a zero QueueItem means nothing is studyable right now.
func NextReview ¶ added in v0.3.0
NextReview picks the next due card from items, all of which already carry an FSRS card (Lifecycle Introduced or Reviewing — the app filters New/Known out before calling). A due review/relearning card wins by earliest Due; otherwise the earliest-due Learning-or-New card. There is NO daily-new cap: novelty is gated upstream at introduction. ok=false means nothing is due at now.
type Rating ¶
type Rating int
Rating is the grade a reviewer gives when answering a card.
func RatingForAnswer ¶
RatingForAnswer is the v1 answer→rating policy: wrong → Again, correct → Good.
type Review ¶
type Review struct {
SkillID SkillID
Rating Rating
ReviewedAt time.Time
StabilityBefore float64
DifficultyBefore float64
StabilityAfter float64
DifficultyAfter float64
StateBefore State
ScheduledDays int
ElapsedDays int
}
Review is one append-only review-log entry. SkillID is set by the caller.
type ReviewStore ¶
type ReviewStore interface {
// Append records one review-log entry.
Append(ctx context.Context, r Review) error
// Log returns the review entries at or after since, in the order the store
// chooses (callers that need ordering should sort).
Log(ctx context.Context, since time.Time) ([]Review, error)
}
ReviewStore is the append-only review log for the single user this store is scoped to.
type Scheduler ¶
type Scheduler struct {
// contains filtered or unexported fields
}
Scheduler wraps go-fsrs/v3 and applies engram's fixed defaults (RequestRetention 0.9, MaximumInterval 365 days, fuzz disabled for determinism).
func NewScheduler ¶
func NewScheduler(opts ...SchedulerOption) *Scheduler
NewScheduler builds a Scheduler starting from go-fsrs's defaults, then overrides MaximumInterval to engram's contract default of 365 days (go-fsrs defaults to 36500), and finally applies opts. EnableFuzz is always left false: fuzz injects its own randomness outside the caller's control, which would break engram's determinism guarantee.
func (*Scheduler) Introduce ¶ added in v0.3.0
func (s *Scheduler) Introduce(o IntroOutcome, now time.Time) (life Lifecycle, cs CardState, hasCard bool)
Introduce applies an introduction outcome at now and returns the resulting lifecycle, the CardState to persist, and hasCard (false for IntroKnown, where the returned CardState is the zero value). Deterministic; depends only on the scheduler's weights and now.
func (*Scheduler) NewCardState ¶
NewCardState returns the state for a never-reviewed skill: StateNew, zero reps/lapses/stability/difficulty, zero LastReview, and Due set to now (due immediately).
func (*Scheduler) Next ¶
Next applies rating r to cs at time now and returns the updated card state plus the corresponding review-log entry. Review.SkillID is left zero; the caller fills it in.
func (*Scheduler) SeedCardKnown ¶ added in v0.3.0
SeedCardKnown returns a CardState for an item the user asserts they already know but still wants tested occasionally: equivalent to a fresh card that received a first Easy rating at now. Concretely (go-fsrs default weights): State=StateReview, Stability≈w[3]≈15.69 (days), Difficulty=the Easy-initial difficulty, Reps=1, Lapses=0, LastReview=now, Due≈now+16d at retention 0.9. NO review-log entry is produced — an introduction is not a review. Deterministic. Because SeedCardKnown reuses Next(_, Easy), it automatically tracks any WithWeights/WithRetention override.
type SchedulerOption ¶
type SchedulerOption func(*Scheduler)
SchedulerOption configures a Scheduler built by NewScheduler.
func WithMaxInterval ¶
func WithMaxInterval(days int) SchedulerOption
WithMaxInterval sets the maximum interval in days between reviews (default 365).
func WithRetention ¶
func WithRetention(r float64) SchedulerOption
WithRetention sets the target request retention (default 0.9).
func WithWeights ¶
func WithWeights(w []float64) SchedulerOption
WithWeights overrides the FSRS model weights (default: go-fsrs defaults). If w has fewer than 19 elements, only the leading len(w) default weights are overridden and the rest keep their default values; if w has more than 19, the extras are ignored. This never panics.
type Skill ¶
type Skill struct {
ID SkillID
DeckID DeckID
Key string // app-defined answer key, e.g. ISO-639-3 "por"
Label string // human label, e.g. "Portuguese"
}
Skill is a single trackable unit of spaced repetition (e.g. "recognise Portuguese in the Romance group").
type SkillStore ¶
type SkillStore interface {
// Skills returns every skill in the given deck.
Skills(ctx context.Context, deck DeckID) ([]Skill, error)
// Card returns the stored card state for a skill. The bool is false when
// the skill has never been seen (no row), in which case the caller should
// treat it as a fresh card via Scheduler.NewCardState.
Card(ctx context.Context, skill SkillID) (CardState, bool, error)
// PutCard upserts the card state for a skill.
PutCard(ctx context.Context, skill SkillID, cs CardState) error
}
SkillStore persists the set of skills in a deck and the per-skill card state for the single user this store is scoped to.
Source Files
¶
Directories
¶
| Path | Synopsis |
|---|---|
|
examples
|
|
|
cli
command
Command cli is a small, deterministic, self-playing demo of the engram spaced-repetition engine: it builds a "Romance languages" deck, runs a simulated study session through the scheduler + queue + quiz + memstore packages, and prints a stats summary at the end.
|
Command cli is a small, deterministic, self-playing demo of the engram spaced-repetition engine: it builds a "Romance languages" deck, runs a simulated study session through the scheduler + queue + quiz + memstore packages, and prints a stats summary at the end. |
|
Package memstore is an in-memory implementation of engram's SkillStore and ReviewStore interfaces, intended for tests and small apps.
|
Package memstore is an in-memory implementation of engram's SkillStore and ReviewStore interfaces, intended for tests and small apps. |
|
Package quiz builds multiple-choice exercises over engram skills and grades the answers.
|
Package quiz builds multiple-choice exercises over engram skills and grades the answers. |