active_recall 2.3.1 → 2.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CLAUDE.md +1 -0
- data/Gemfile.lock +1 -1
- data/README.md +8 -2
- data/gemfiles/rails_7_0.gemfile.lock +1 -1
- data/gemfiles/rails_7_1.gemfile.lock +1 -1
- data/gemfiles/rails_8_0.gemfile.lock +1 -1
- data/lib/active_recall/algorithms/sm2.rb +27 -16
- data/lib/active_recall/models/item.rb +7 -1
- data/lib/active_recall/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 32cd4469689b6a3cdd5fb86aef54cdb886a41eeb0d3feafe8378faa5748fa267
|
|
4
|
+
data.tar.gz: a4e4711f9a8ebb85339b2b66c83f1ca9b9c54a360d9aa89a3aacb53998d9db4c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: feb48c5d0f80cc200dbd61322e647c3af982e92833e4727c3d801c5b77e6316af1d83315ee1bce4479f9aad537a026ba331d075a271e6a726d8c07cc0fdebf34
|
|
7
|
+
data.tar.gz: a2ffe9ade4ab0703572e7e5168b0326a46601c5ce84fc136f82433927787c395d754a47ccef7020a264ad6f15e6dcd4620a2d7298db7e948cb7f7afb219520de
|
data/CLAUDE.md
CHANGED
|
@@ -95,6 +95,7 @@ bundle exec rake release
|
|
|
95
95
|
- Binary algorithms: LeitnerSystem (default), SoftLeitnerSystem, FibonacciSequence
|
|
96
96
|
- Gradable algorithm: SM2
|
|
97
97
|
- All algorithms are stateless; they accept current state and return new state as a hash
|
|
98
|
+
- SM2 follows the canonical recurrence `I(1)=1, I(2)=6, I(n)=round(I(n-1) * EF)`, recovering the prior interval from the card's `last_reviewed`/`next_review` (no extra columns), updates EF on every grade (clamped to 1.3), and schedules failures one day out. Because SM2 reads those timestamps as its own state, switching algorithms on already-reviewed cards is unsupported without a state reset/migration.
|
|
98
99
|
|
|
99
100
|
5. **Configuration** (lib/active_recall/configuration.rb)
|
|
100
101
|
- Global configuration via `ActiveRecall.configure`
|
data/Gemfile.lock
CHANGED
data/README.md
CHANGED
|
@@ -160,17 +160,23 @@ user.words.expired #=> [word]
|
|
|
160
160
|
| `1` | Incorrect response with familiarity |
|
|
161
161
|
| `0` | Complete blackout |
|
|
162
162
|
|
|
163
|
-
Grades **≥ 3** count as a success: the box advances and `times_right` increments. Grades **< 3** reset the box to `0` and increment `times_wrong`.
|
|
163
|
+
Grades **≥ 3** count as a success: the box advances and `times_right` increments. Grades **< 3** reset the box to `0` and increment `times_wrong`. The `easiness_factor` is updated on **every** grade (including failures), starts at `2.5`, and is clamped to a minimum of `1.3` — so a poor grade lowers the EF and shortens future intervals.
|
|
164
|
+
|
|
165
|
+
Intervals follow the canonical SM-2 recurrence: `I(1) = 1`, `I(2) = 6`, and `I(n) = round(I(n − 1) × EF)` for `n > 2`. Fractional intervals use ordinary rounding (matching the published SuperMemo 2 source), so six consecutive perfect reviews schedule at `1, 6, 16, 45, 131, 393` days. The prior interval is recovered from the card's stored `last_reviewed`/`next_review`, so **no extra columns are required**.
|
|
166
|
+
|
|
167
|
+
A failed card is scheduled one day out (`next_review = now + 1 day`) and does **not** reappear in `review`/`failed` until that day arrives.
|
|
164
168
|
|
|
165
169
|
```ruby
|
|
166
170
|
user.words << word
|
|
167
171
|
|
|
168
172
|
user.score!(5, word) # perfect recall — box advances, EF rises
|
|
169
|
-
user.score!(2, word) # incorrect — box resets to 0
|
|
173
|
+
user.score!(2, word) # incorrect — box resets to 0, EF drops, due again tomorrow
|
|
170
174
|
```
|
|
171
175
|
|
|
172
176
|
Calling `user.right_answer_for!(word)` while SM2 is configured raises `ActiveRecall::IncompatibleAlgorithmError` — use `score!` instead.
|
|
173
177
|
|
|
178
|
+
> **Switching algorithms on existing cards is unsupported.** Each card's `box`, `next_review`, and `last_reviewed` are written by whichever algorithm scored it, and SM-2 derives its prior interval from those timestamps. Pointing SM-2 at cards previously scheduled by another algorithm will misread their state. Choose an algorithm before reviewing, or reset/migrate card state before switching.
|
|
179
|
+
|
|
174
180
|
## Usage with FSRS
|
|
175
181
|
|
|
176
182
|
[FSRS](https://github.com/open-spaced-repetition/fsrs4anki) uses a 1–4 grade scale matching the familiar Anki buttons:
|
|
@@ -8,13 +8,15 @@ module ActiveRecall
|
|
|
8
8
|
REQUIRED_ATTRIBUTES
|
|
9
9
|
end
|
|
10
10
|
|
|
11
|
-
def self.score(box:, easiness_factor:, times_right:, times_wrong:, grade:, current_time: Time.current)
|
|
11
|
+
def self.score(box:, easiness_factor:, times_right:, times_wrong:, grade:, last_reviewed: nil, next_review: nil, current_time: Time.current)
|
|
12
12
|
new(
|
|
13
13
|
box: box,
|
|
14
14
|
easiness_factor: easiness_factor,
|
|
15
15
|
times_right: times_right,
|
|
16
16
|
times_wrong: times_wrong,
|
|
17
17
|
grade: grade,
|
|
18
|
+
last_reviewed: last_reviewed,
|
|
19
|
+
next_review: next_review,
|
|
18
20
|
current_time: current_time
|
|
19
21
|
).score
|
|
20
22
|
end
|
|
@@ -23,24 +25,22 @@ module ActiveRecall
|
|
|
23
25
|
:gradable
|
|
24
26
|
end
|
|
25
27
|
|
|
26
|
-
def initialize(box:, easiness_factor:, times_right:, times_wrong:, grade:, current_time: Time.current)
|
|
28
|
+
def initialize(box:, easiness_factor:, times_right:, times_wrong:, grade:, last_reviewed: nil, next_review: nil, current_time: Time.current)
|
|
27
29
|
@box = box # box serves as repetition number n
|
|
28
30
|
@easiness_factor = easiness_factor || 2.5
|
|
29
31
|
@times_right = times_right
|
|
30
32
|
@times_wrong = times_wrong
|
|
31
33
|
@grade = grade
|
|
34
|
+
@last_reviewed = last_reviewed # the card's prior review time
|
|
35
|
+
@previous_next_review = next_review # the card's prior scheduled due date
|
|
32
36
|
@current_time = current_time
|
|
33
|
-
@interval =
|
|
34
|
-
when 0 then 1 # First review
|
|
35
|
-
when 1 then 6 # Second review
|
|
36
|
-
else 17 # Will be overwritten for boxes > 1
|
|
37
|
-
end
|
|
37
|
+
@interval = 1
|
|
38
38
|
end
|
|
39
39
|
|
|
40
40
|
def score
|
|
41
41
|
raise "Grade must be between 0-5!" unless GRADES.include?(@grade)
|
|
42
42
|
old_ef = @easiness_factor
|
|
43
|
-
update_easiness_factor
|
|
43
|
+
update_easiness_factor
|
|
44
44
|
update_repetition_and_interval(old_ef)
|
|
45
45
|
|
|
46
46
|
{
|
|
@@ -69,7 +69,9 @@ module ActiveRecall
|
|
|
69
69
|
:easiness_factor,
|
|
70
70
|
:grade,
|
|
71
71
|
:times_right,
|
|
72
|
-
:times_wrong
|
|
72
|
+
:times_wrong,
|
|
73
|
+
:last_reviewed,
|
|
74
|
+
:next_review
|
|
73
75
|
].freeze
|
|
74
76
|
|
|
75
77
|
def update_easiness_factor
|
|
@@ -79,13 +81,13 @@ module ActiveRecall
|
|
|
79
81
|
|
|
80
82
|
def update_repetition_and_interval(old_ef)
|
|
81
83
|
if @grade >= 3
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
84
|
+
# Canonical SM-2 recurrence: I(1)=1, I(2)=6, I(n)=round(I(n-1) * EF).
|
|
85
|
+
# EF here is the value from before this review's update (old_ef), which
|
|
86
|
+
# reproduces the published Delphi sequence (1, 6, 16, 45, 131, 393).
|
|
87
|
+
@interval = case @box
|
|
88
|
+
when 0 then 1
|
|
89
|
+
when 1 then 6
|
|
90
|
+
else (previous_interval * old_ef).round
|
|
89
91
|
end
|
|
90
92
|
|
|
91
93
|
@box += 1
|
|
@@ -97,6 +99,15 @@ module ActiveRecall
|
|
|
97
99
|
end
|
|
98
100
|
end
|
|
99
101
|
|
|
102
|
+
# Recover the card's previously scheduled interval (in days) from its stored
|
|
103
|
+
# timestamps. Falls back to I(2)=6 when the prior schedule is unavailable
|
|
104
|
+
# (e.g. a card whose history predates this calculation).
|
|
105
|
+
def previous_interval
|
|
106
|
+
return 6 unless @last_reviewed && @previous_next_review
|
|
107
|
+
|
|
108
|
+
[((@previous_next_review - @last_reviewed) / 1.day).round, 1].max
|
|
109
|
+
end
|
|
110
|
+
|
|
100
111
|
def next_review
|
|
101
112
|
@current_time + @interval.days
|
|
102
113
|
end
|
|
@@ -6,9 +6,15 @@ module ActiveRecall
|
|
|
6
6
|
|
|
7
7
|
belongs_to :deck
|
|
8
8
|
|
|
9
|
-
scope :failed, -> { where(["box = ? and last_reviewed is not null", 0]) }
|
|
10
9
|
scope :untested, -> { where(["box = ? and last_reviewed is null", 0]) }
|
|
11
10
|
|
|
11
|
+
# Lapsed (box 0, already reviewed) cards that are due now. A null next_review
|
|
12
|
+
# means "review immediately" (binary algorithms reset this way); a future
|
|
13
|
+
# next_review (e.g. SM-2's one-day failure interval) is excluded until due.
|
|
14
|
+
def self.failed(current_time: Time.current)
|
|
15
|
+
where(["box = ? and last_reviewed is not null and (next_review is null or next_review <= ?)", 0, current_time])
|
|
16
|
+
end
|
|
17
|
+
|
|
12
18
|
def self.expired(current_time: Time.current)
|
|
13
19
|
where(["box > ? and next_review <= ?", 0, current_time])
|
|
14
20
|
end
|