aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--working/fsrs-spec/fsrs-spec.org354
1 files changed, 354 insertions, 0 deletions
diff --git a/working/fsrs-spec/fsrs-spec.org b/working/fsrs-spec/fsrs-spec.org
new file mode 100644
index 0000000..d0f0bc1
--- /dev/null
+++ b/working/fsrs-spec/fsrs-spec.org
@@ -0,0 +1,354 @@
+#+TITLE: FSRS scheduler for org-drill — v0 design spec
+#+AUTHOR: Craig Jennings
+#+DATE: 2026-05-27
+
+* Status
+
+Draft v0 — pending decisions marked =DECIDE:= inline. The companion todo
+entry is =todo.org=:125 [#B] "FSRS (Free Spaced Repetition Scheduler)
+Algorithm". No implementation has started.
+
+* Context and motivation
+
+org-drill ships three SuperMemo-family schedulers — SM2, SM5, and Simple8.
+They share an interval/repeats/EF/failures/meanq/total-repeats state model
+inherited from the SuperMemo 2 paper and refined since. The card-state
+struct refactor (#147) bundles those fields into =org-drill-card-state= and
+makes plugging a new scheduler into the =cl-case= dispatch a one-arm change
+in =org-drill-smart-reschedule= and =org-drill-hypothetical-next-review-date=.
+
+FSRS (Free Spaced Repetition Scheduler) is the algorithm Anki has migrated
+to since 2024. It uses the DSR memory model (Difficulty, Stability,
+Retrievability) and a 17-to-21-parameter formula family fit on hundreds of
+millions of real reviews. Published evaluations show materially better
+retention than SM* at the same review load. Adding it as a fourth option
+to =org-drill-spaced-repetition-algorithm= lets a user opt in without
+disturbing existing SM-family decks.
+
+* Goals
+
+- A fourth value =fsrs= for =org-drill-spaced-repetition-algorithm=.
+- The =fsrs= arm of the =cl-case= in =smart-reschedule= and
+ =hypothetical-next-review-date= calls =org-drill-determine-next-interval-fsrs=
+ with the same shape the SM-family arms now use (a state value + the
+ rating quality + algorithm-specific extras).
+- Per-card FSRS state persisted to org properties. Existing SM-style
+ properties stay untouched on cards that haven't been switched.
+- Test coverage at parity with the SM* test files: Normal / Boundary /
+ Error per public function, plus reference-vector tests checking the
+ algorithm output against the canonical FSRS implementation.
+
+* Non-goals (v1)
+
+- *No parameter optimizer.* Anki's FSRS shines because of parameter
+ fitting on each user's review history. v1 ships fixed default
+ parameters only; the optimizer becomes its own todo and ticket. Once
+ the algorithm is in the tree, plugging an optimizer in front of it is
+ additive.
+- *No retro-migration heuristic.* A card switched from SM* to FSRS
+ cold-starts on its next review — FSRS treats it as a fresh first review.
+ Anki has heuristics that estimate initial =D= and =S= from prior review
+ history; we explicitly defer that as a follow-on.
+- *No bulk rescheduling pass.* Switching algorithms does not retroactively
+ rewrite SCHEDULED dates on existing cards. Cards keep their current
+ SCHEDULED until their next review.
+
+* Version pin
+
+=DECIDE:= FSRS-4.5 (recommended) vs FSRS-6.x.
+
+The current stable upstream is FSRS-6.3.1 (March 2026) with 21
+parameters. FSRS-4.5 is the last release whose generic defaults provide
+meaningful retention improvement over SM-family scheduling without
+optimization. The FSRS community's own guidance — quoting Expertium's
+algorithm explainer, March 2026 — is that "generic v6 defaults offer
+minimal improvement over v4.5" because v6's gains come from optimizing
+=w[17]..w[20]= on a user's data.
+
+Since v1 ships without an optimizer (above), the pragmatic pin is
+*FSRS-4.5*: 17 parameters, well-documented defaults, the most-published
+test vectors, and the version with the largest delta over SM* in the
+no-optimizer regime. Upgrading to v6 becomes a separate ticket alongside
+the optimizer one — by then there'd be a real reason to take v6's
+additional surface.
+
+Reference parameters (FSRS-4.5 defaults, source: =py-fsrs= up to v4 and
+the original =fsrs4anki= release notes):
+
+#+begin_src elisp
+;; w[0..16], FSRS-4.5
+(0.4 0.6 2.4 5.8 4.93 0.94 0.86 0.01 1.49 0.14 0.94 2.18 0.05 0.34 1.26 0.29 2.61)
+#+end_src
+
+(=DECIDE:= confirm this exact tuple before code lands. The 4.5 defaults
+were tuned twice during 4.x's life; pinning the final 4.5-stable tuple is
+a one-line lookup against the =py-fsrs= 4.x release tag, deferred to
+implementation.)
+
+* Algorithm shape (v4.5)
+
+The four-rating scale FSRS uses internally:
+
+| Rating | Name | Meaning |
+|--------+-------+------------------------|
+| 1 | Again | failed recall |
+| 2 | Hard | recalled with struggle |
+| 3 | Good | recalled normally |
+| 4 | Easy | trivial recall |
+
+org-drill uses a 0–5 quality scale. The mapping respects
+=org-drill-failure-quality= (default 2):
+
+| org-drill quality | FSRS rating |
+|-------------------+-------------|
+| 0, 1, 2 | Again (1) |
+| 3 | Hard (2) |
+| 4 | Good (3) |
+| 5 | Easy (4) |
+
+=DECIDE:= mapping table. Above is the natural reading of
+=org-drill-failure-quality=, but org-drill historically lets users
+remap session keys, and the mapping should respect that or be its
+own defcustom. Simplest: pin the mapping above for v1 and add
+=org-drill-fsrs-quality-mapping= as a future enhancement.
+
+** State model
+
+Per-card persisted state:
+
+| Property | Type | Meaning |
+|-----------------------+--------+--------------------------------------|
+| =DRILL_FSRS_STABILITY= | float | =S=, the memory-stability estimate |
+| =DRILL_FSRS_DIFFICULTY= | float | =D=, the difficulty estimate (1..10) |
+| =DRILL_FSRS_REVIEWS= | int | review count under FSRS (≥ 0) |
+| =DRILL_FSRS_LAPSES= | int | failures (rating=Again) under FSRS |
+| =DRILL_LAST_REVIEWED= | time | reused from the existing SM property |
+
+The last-reviewed timestamp is already written by the SM path
+(=org-drill-store-item-data= writes it via =org-set-property= elsewhere in
+the flow); FSRS reads the same property.
+
+A virgin FSRS card has all four =DRILL_FSRS_*= properties absent; the
+scheduler treats absence as "first review."
+
+** Update equations (v4.5 form)
+
+First review (no prior FSRS state) with rating R ∈ {1..4}:
+
+#+begin_src elisp
+S0 = w[R-1] ; initial stability per rating
+D0 = clamp (- w[4] (* w[5] (- R 2))) 1 10
+#+end_src
+
+Subsequent review at time =elapsed-days= since last review, with current
+stability =S=, difficulty =D=, and rating =R=:
+
+#+begin_src elisp
+;; Retrievability when the card is shown
+R-retrieval = (expt (1+ (/ elapsed-days (* 9 S))) -1)
+
+;; Difficulty update (linear with mean reversion toward w[4])
+D-delta = (* w[6] (- (- R 3))) ; positive on Again, neutral on Good
+D-new-raw = (+ D D-delta)
+D-new = (+ (* w[7] w[4]) (* (- 1 w[7]) D-new-raw)) ; mean reversion
+D-new = (clamp D-new 1 10)
+
+;; Stability update — success (R ≥ 2)
+S-new = S * (1 +
+ (exp w[8]) *
+ ((11 - D) / 9) *
+ (S ^ (-w[9])) *
+ ((exp ((1 - R-retrieval) * w[10])) - 1) *
+ (R == Hard ? w[15] : 1) *
+ (R == Easy ? w[16] : 1))
+
+;; Stability update — lapse (R = 1, Again)
+S-new = w[11] *
+ D^(-w[12]) *
+ ((S + 1)^w[13] - 1) *
+ exp((1 - R-retrieval) * w[14])
+#+end_src
+
+Next interval given desired retention =DR= and new stability =S=:
+
+#+begin_src elisp
+I = (* 9 S (- (expt DR -1) 1)) ; days, rounded down (floor)
+#+end_src
+
+Default desired retention is *0.9* (a common Anki default). Pinned as
+=org-drill-fsrs-desired-retention= defcustom.
+
+=DECIDE:= the exact form of the v4.5 update equations above is paraphrased
+from Expertium's algorithm walkthrough (March 2026, v6-oriented) and the
+=py-fsrs= reference implementation. Implementation will cross-check the
+exact constants and grouping against a tagged =py-fsrs= v4.x release
+before writing the function body.
+
+** Default parameters (org-drill defcustoms)
+
+#+begin_src elisp
+(defcustom org-drill-fsrs-parameters
+ '(0.4 0.6 2.4 5.8 4.93 0.94 0.86 0.01 1.49 0.14 0.94 2.18 0.05 0.34 1.26 0.29 2.61)
+ "17-tuple FSRS-4.5 default parameters. See =docs/fsrs-spec.org=.")
+
+(defcustom org-drill-fsrs-desired-retention 0.9
+ "Target retention for FSRS scheduling. Higher = shorter intervals.")
+#+end_src
+
+* Integration points
+
+** Public function
+
+#+begin_src elisp
+(defun org-drill-determine-next-interval-fsrs (state quality)
+ "Return next-interval (in days) plus updated FSRS state for STATE after
+QUALITY (0-5). STATE here is a 5-tuple (S D reviews lapses last-reviewed),
+not an `org-drill-card-state' — FSRS stores its own state and treats the SM
+slots as opaque.")
+#+end_src
+
+The shape =(state quality)= matches the post-#147 scheduler signature
+convention. =state= for FSRS is a *separate* struct =org-drill-fsrs-state=
+(or a plist — =DECIDE:= which), populated from the =DRILL_FSRS_*=
+properties at the call site.
+
+=DECIDE:= the return shape. Options:
+
+1. Same shape as the SM* schedulers (a list with =next-interval=
+ first plus updated state). Consistent but the trailing slots are
+ irrelevant to FSRS.
+2. A small =org-drill-fsrs-result= struct (=next-interval=,
+ =new-stability=, =new-difficulty=, =new-reviews=, =new-lapses=).
+ Cleaner for FSRS but introduces a return-shape special case in the
+ =cl-case= caller.
+
+Recommended: option 2. =cl-case= already special-cases =new-ofmatrix=
+for SM5; one more is fine and the FSRS state shape is genuinely different.
+
+** Caller integration
+
+In =org-drill-smart-reschedule=:
+
+#+begin_src elisp
+;; sketch — exact destructure shape TBD per the return-shape DECIDE
+(fsrs
+ (let ((fsrs-state (org-drill-get-fsrs-state)))
+ (org-drill-fsrs-apply-result
+ (org-drill-determine-next-interval-fsrs fsrs-state quality))))
+#+end_src
+
+Two new helpers parallel the existing item-data round-trip:
+
+- =org-drill-get-fsrs-state= — reads the =DRILL_FSRS_*= properties, returns
+ the state struct or nil for a virgin card.
+- =org-drill-store-fsrs-state= (or =org-drill-fsrs-apply-result=) — writes
+ the updated state back. The =SCHEDULED= timestamp is set via
+ =org-schedule= using the returned =next-interval=, the same way
+ =smart-reschedule= already does for SM*.
+
+Same in =hypothetical-next-review-date= (read-only, no store).
+
+** Defcustom entry
+
+=org-drill-spaced-repetition-algorithm= grows a fourth =:option= line:
+
+#+begin_src elisp
+- FSRS :: Free Spaced Repetition Scheduler, the DSR-based algorithm
+ used in Anki since 2024. See `docs/fsrs-spec.org` for the algorithm
+ and the state model.
+#+end_src
+
+* Backward compatibility
+
+A card last reviewed under SM* has =DRILL_LAST_INTERVAL=,
+=DRILL_REPEATS_SINCE_FAIL=, etc., but no =DRILL_FSRS_*=. On the first
+FSRS review after a switch:
+
+- The FSRS scheduler sees absent =DRILL_FSRS_*= and treats this as a
+ virgin first review (initial =S₀=, =D₀= from the rating).
+- The SM-style properties are left in place. If the user switches back,
+ the SM scheduler reads them as before.
+- The =DRILL_FSRS_*= properties are written from this point forward.
+ Cards drilled under both algorithms accumulate both property sets.
+
+The cold-start tradeoff is a known one-time loss of the SM-derived
+estimate of card difficulty. Anki's migration heuristic estimates
+initial =D= and =S= from the SM2 ease factor and interval; pulling that in
+is its own ticket (=todo:= "FSRS migration heuristic from SM history").
+
+=DECIDE:= whether to also surface a one-line "first FSRS review on this
+card — cold-starting" message during the prompt for the first FSRS review.
+Probably not — users don't need to know.
+
+* Test strategy
+
+Three categories per public function, matching the existing scheduler
+test files:
+
+1. =tests/test-org-drill-determine-next-interval-fsrs.el= — Normal /
+ Boundary / Error coverage of =org-drill-determine-next-interval-fsrs=.
+ *Reference-vector tests* check =(state, rating) -> (next-interval,
+ new-state)= against pre-computed outputs from =py-fsrs= at a tagged
+ v4.x release. About a dozen vectors covering the four ratings × first
+ review / nth review / lapse / long-elapsed-time, plus boundary ones
+ (=elapsed-days=0=, =D= and =S= at clamp limits).
+
+2. =tests/test-org-drill-fsrs-state-roundtrip.el= — get-fsrs-state /
+ store-fsrs-state round-trip parity, mirroring the SM
+ item-data-roundtrip test.
+
+3. =tests/test-org-drill-fsrs-integration.el= — end-to-end through
+ =smart-reschedule= with =org-drill-spaced-repetition-algorithm= set to
+ =fsrs=, covering: virgin card first review, returning card under
+ FSRS, switched-from-SM card cold-starts cleanly, both algorithms can
+ coexist on the same buffer.
+
+Mapping helpers (org-drill quality 0–5 → FSRS rating 1–4) and the
+defcustoms get their own small tests.
+
+The reference vectors get committed alongside the tests as a
+machine-checkable expected-output table, generated once from =py-fsrs=
+and pinned. The generation script lives at =tests/fixtures/fsrs-vectors.py=
+so re-generation is reproducible.
+
+* Effort estimate
+
+Multi-day, plausibly spanning sessions:
+
+- Algorithm function + helpers: 1 day.
+- State round-trip + integration in =smart-reschedule=/=hypothetical=: 0.5
+ day.
+- Test scaffolding + reference vectors + Normal/Boundary/Error: 1 day.
+- Documentation (manual entry, README option list, defcustom docstrings,
+ this spec ratified): 0.5 day.
+
+Realistic: a session to implement-and-test the algorithm + state
+round-trip with reference-vector tests; a follow-up session for the
+integration + docs. No optimizer.
+
+* Open decisions index
+
+Pinned in one place for the implementation gate:
+
+- =DECIDE:= version pin (FSRS-4.5 vs newer). Recommended: 4.5.
+- =DECIDE:= the exact FSRS-4.5 default-parameter tuple. One-line lookup
+ against the =py-fsrs= 4.x final release.
+- =DECIDE:= quality-mapping table (0–5 → 1–4). Recommended: above.
+- =DECIDE:= scheduler return shape — list (SM-shape) or
+ =org-drill-fsrs-result= struct. Recommended: struct.
+- =DECIDE:= whether to surface a "first FSRS review on this card"
+ message. Recommended: no.
+- =DECIDE:= the exact form of the update equations after cross-checking
+ =py-fsrs= 4.x source.
+
+* References
+
+- [[https://github.com/open-spaced-repetition/py-fsrs][py-fsrs]] — the canonical Python reference implementation. Tagged
+ v4.x releases pin FSRS-4.5; v6.x is the current line.
+- [[https://github.com/open-spaced-repetition/fsrs4anki][fsrs4anki]] — the Anki integration; its wiki has user-facing notes on
+ algorithm history and migration.
+- [[https://expertium.github.io/Algorithm.html][Expertium: A technical explanation of FSRS]] (March 2026, v6-oriented)
+ — the most accessible English-language walkthrough of the formulas
+ and parameter roles.
+- [[https://help.remnote.com/en/articles/9124137-the-fsrs-spaced-repetition-algorithm][RemNote: The FSRS Spaced Repetition Algorithm]] — third-party
+ implementer's overview, useful as a sanity check on terminology.