Learning Engine

Four shared packages decide what a learner sees and when: learning (memory and mastery), planner (the feed), decisions (logging every choice) and experiments (variants). The client and the server import the same code, so they always agree.

flowchart TB
  LOGS[("keeps · attempts · review_logs")] --> REPLAY[replay cards<br/>FSRS]
  REPLAY --> CARDS[card state<br/>stability · difficulty · due]
  CARDS --> REVIEW[Review queue]
  CARDS --> MASTERY[topic mastery]
  MASTERY --> PLANNER
  INTERESTS[interests<br/>stated + behaviour] --> PLANNER
  CATALOG[catalog + build-on graph] --> PLANNER
  EXP[experiments<br/>deterministic variant] --> PLANNER
  PLANNER[planner<br/>policy: rules@3] --> FEED[Feed]
  PLANNER --> DL[(decision log<br/>candidates · scores · propensity · reasons)]

Cards and spaced repetition

  • A card is created when a learner keeps a box, or automatically when they answer a Try question wrongly with "Bet my chai" (the most valuable thing to review).
  • Scheduling uses FSRS (ts-fsrs 5.4, no fuzz). Each review records a rating: Forgot, Hard, Got it or Easy. The scheduler updates stability and difficulty and computes the next due date for the learner's memory target: 80, 85, 90 (default) or 95 percent.
  • Card state is never stored. It is replayed from the logs whenever needed: on device for the Review screen, on the server for reminders and learner state.
sequenceDiagram
  participant L as Logs
  participant F as FSRS
  participant C as Card
  L->>F: keep (t0) creates the card
  loop each review in time order (then id)
    L->>F: rating, reviewedAt
    F->>C: stability, difficulty, next due
  end
  Note over C: same logs → same card on every device

Review timing

FeatureHow it works
Due forecastCards are replayed and bucketed by due day, so Review shows how many come back each day for the next week
Daily limitA cap on new reviews per day; overflow rolls to the next day in due order
Memory targetThe learner chooses how much they want to remember; a higher target means more frequent reviews
Honest recall rateShown from actual first-try results, not from the target, so learners see how they are really doing

Mastery

Topic mastery combines kept and reviewed boxes, Try results weighted by confidence, and how deep a learner went into each box. It drives the topic maps, the "ready for this?" warnings when someone skips ahead of a box's prerequisites, and the planner.

The feed planner

The planner turns the catalog into a ranked feed at a named decision point (feed.rank).

  1. Candidates: unseen boxes in live worlds.
  2. Score: a band for the topic (picked topics first, "Show me less" topics last) times ten, plus a weight for the learner's world order.
  3. Order: best band first, a box only once its prerequisites are seen or already planned, alternating topics within a band.
  4. Budget: boxes are added until the learner's daily minutes are used (at the chosen depth).
  5. Recalls: due cards are interleaved, at most one per three boxes.
  6. Log: the policy feed.rank/rules@3 records the candidates, scores, chosen items, a propensity for each and a plain explanation ("Because you like Graphs"), plus the learner's experiment assignments.

decision Log propensities from day one. Recording how likely each shown item was to be chosen is what makes offline evaluation of a new policy possible.

warning Today's rules are deterministic, so every propensity is 1. Off-policy evaluation needs some randomness in what is shown; until a little exploration is added, the logs can't score a different policy. See Roadmap.

Experiments

  • Assignment is a deterministic hash of the experiment id and a pseudonymous unit id, so the client and server compute the same variant without a lookup.
  • Experiments live in layers (feed, content, review, notify, onboarding, monetisation). Experiments in different layers can overlap; within a layer they are mutually exclusive.
  • A learner's assignments are stored on every decision log. A small holdout stays out of all experiments.
  • exposures records which feed items were actually on screen, with their position and visible time.
  • The registry is in place, but no experiment is running yet.

Learner state on the server

The learner-state job (hourly, incremental) recomputes card summaries and mastery only for learners whose learning mutations arrived since the last run (with an overlap so none is missed), plus anyone marked in learner_dirty (for example after an account merge). Reminders, the retention job and research exports read this table instead of replaying logs on every request.

${sch.nav()}