engram

package module
v0.1.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 14, 2026 License: MIT Imports: 3 Imported by: 0

README

engram

A storage-agnostic spaced-repetition engine for Go, wrapping the FSRS algorithm.

Why

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.

Design principles

  • Deterministic. Time is always an explicit now time.Time parameter; randomness is always an injected *rand.Rand. No hidden clock reads, no global state. Fully reproducible in tests.
  • Storage-agnostic. engram defines SkillStore and ReviewStore interfaces; the application owns persistence. An in-memory memstore ships 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.

Install

go get github.com/supercakecrumb/engram

Requires Go 1.26 (the module targets go 1.26; the toolchain is fetched automatically).

Quickstart

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
}

API tour

  • Root package engram: types Deck, Skill, CardState, Review, State, Rating; Scheduler (via NewScheduler with options WithRetention, WithMaxInterval, WithWeights) exposing NewCardState(now) and Next(cs, now, rating); the answer→rating policy RatingForAnswer(correct).
  • Queue: NextDue(items, newIntroducedToday, cfg, now) (due-first ordering, MaxNewPerDay cap) and CountNewIntroduced(log, now, loc).
  • Stats (pure functions over review logs): Accuracy, Streak, DueForecast.
  • Storage interfaces: SkillStore, ReviewStore; in-memory memstore.Store implements both.
  • quiz subpackage: Generate (seeded, shuffled, exactly one correct option), Exercise.Grade, and Confusion for "you mistake Portuguese for Spanish 40% of the time"-style analysis.

Status

v0.x, first consumer is a Telegram GeoGuessr language trainer. Local development targets Go 1.26.

License

MIT © Aurora Kiel — see LICENSE.

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

Constants

This section is empty.

Variables

This section is empty.

Functions

func Accuracy

func Accuracy(log []Review) float64

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

func CountNewIntroduced(log []Review, now time.Time, loc *time.Location) int

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

func DueForecast(cards []CardState, days int, now time.Time, loc *time.Location) []int

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 Streak

func Streak(log []Review, now time.Time, loc *time.Location) int

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 ContentID

type ContentID string

ContentID identifies a piece of quiz content.

type Deck

type Deck struct {
	ID   DeckID
	Name string
}

Deck groups related skills (e.g. "Romance languages").

type DeckID

type DeckID string

DeckID identifies a Deck.

type QueueConfig

type QueueConfig struct {
	MaxNewPerDay int // 0 = unlimited
}

QueueConfig configures NextDue's new-card cap.

type QueueItem

type QueueItem struct {
	Skill Skill
	Card  CardState
}

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.

type Rating

type Rating int

Rating is the grade a reviewer gives when answering a card.

const (
	Again Rating = iota + 1
	Hard
	Good
	Easy
)

func RatingForAnswer

func RatingForAnswer(correct bool) Rating

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) NewCardState

func (s *Scheduler) NewCardState(now time.Time) CardState

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

func (s *Scheduler) Next(cs CardState, now time.Time, r Rating) (CardState, Review)

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.

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 SkillID

type SkillID string

SkillID identifies a Skill.

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.

type State

type State int

State is the FSRS memory state of a card.

const (
	StateNew State = iota
	StateLearning
	StateReview
	StateRelearning
)

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.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL