A storage-agnostic spaced-repetition engine for Go, wrapping the FSRS algorithm.
github.com/open-spaced-repetition/go-fsrs implements only the FSRS scheduling algorithm — one card in, a schedule out. Everything an app actually needs around it — decks, due queues, daily new-item caps, review logs, statistics, multiple-choice quiz generation — everyone hand-rolls. engram is that missing layer. It has exactly one dependency: go-fsrs itself.
- Deterministic. Time is always an explicit
now time.Timeparameter; randomness is always an injected*rand.Rand. No hidden clock reads, no global state. Fully reproducible in tests. - Storage-agnostic. engram defines
SkillStoreandReviewStoreinterfaces; the application owns persistence. An in-memorymemstoreships for tests and small apps. - Generic over "skill". A skill is whatever unit you track (a language to recognise, a chord, a capital city) — engram schedules it and samples fresh content each review.
- v0.x until the API settles. The surface is pinned but may evolve from real use before v1.0.
go get github.com/supercakecrumb/engram
Requires Go 1.26 (the module targets go 1.26; the toolchain is fetched automatically).
Play a deterministic Romance-languages flashcard session:
go run ./examples/cli
It demonstrates the scheduler, due queue, quiz generation, and stats working together over the in-memory store.
Here's the core loop:
package main
import (
"context"
"fmt"
"math/rand"
"time"
"github.com/supercakecrumb/engram"
"github.com/supercakecrumb/engram/quiz"
)
func main() {
sched := engram.NewScheduler() // retention 0.9, max interval 365d, deterministic
now := time.Now()
// A never-reviewed skill starts due immediately.
card := sched.NewCardState(now)
// Build a multiple-choice exercise from a deck of skills.
deck := []engram.Skill{
{ID: "sk-por", DeckID: "romance", Key: "por", Label: "Portuguese"},
{ID: "sk-spa", DeckID: "romance", Key: "spa", Label: "Spanish"},
}
rng := rand.New(rand.NewSource(1))
ex := quiz.Generate(rng, deck[0], deck, quiz.Content{ID: "c1", Payload: "Olá, tudo bem?"})
// Grade an answer and advance the schedule.
correct, rating := ex.Grade("por")
next, review := sched.Next(card, now, rating)
review.SkillID = ex.SkillID
fmt.Printf("correct=%v next due in %s\n", correct, next.Due.Sub(now))
_ = context.Background()
_ = review
}- Root package
engram: typesDeck,Skill,CardState,Review,State,Rating;Scheduler(viaNewSchedulerwith optionsWithRetention,WithMaxInterval,WithWeights) exposingNewCardState(now)andNext(cs, now, rating); the answer→rating policyRatingForAnswer(correct). - Queue:
NextDue(items, newIntroducedToday, cfg, now)(due-first ordering,MaxNewPerDaycap) andCountNewIntroduced(log, now, loc). - Stats (pure functions over review logs):
Accuracy,Streak,DueForecast. - Storage interfaces:
SkillStore,ReviewStore; in-memorymemstore.Storeimplements both. quizsubpackage:Generate(seeded, shuffled, exactly one correct option),Exercise.Grade, andConfusionfor "you mistake Portuguese for Spanish 40% of the time"-style analysis.
v0.x, first consumer is a Telegram GeoGuessr language trainer. Local development targets Go 1.26.
MIT © Aurora Kiel — see LICENSE.