Fix/Ensure fuzz doesn't go backward during rescheduling (#4364)

* Fix/Ensure fuzz doesn't go backward during rescheduling

Fixes https://github.com/ankitects/anki/issues/2694

* Fix

* Get previous_interval from LastRevlogInfo

* Fix

* Format

* Format

* Exclude lapses

* Force reconfigure in CI

The cached build.ninja may reference files that don't exist in the PR.
On a local build this tends to auto-fix itself as the build scripts detect
a quick failure and re-run the configure, but CI tends to be too slow.

https://github.com/ankitects/anki/pull/4364#issuecomment-3338026129

* Rename min/max to make it clear they restrict interval, not fuzz

* Wording tweaks/comments for clarity

---------

Co-authored-by: Damien Elmes <gpg@ankiweb.net>
This commit is contained in:
user1823 2025-09-27 12:13:34 +05:30 committed by GitHub
parent d8aa244a5a
commit b0665a8ef1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 37 additions and 6 deletions

View file

@ -16,6 +16,7 @@ if [ "$CLEAR_RUST" = "1" ]; then
rm -rf $BUILD_ROOT/rust rm -rf $BUILD_ROOT/rust
fi fi
rm -f out/build.ninja
./ninja pylib qt check ./ninja pylib qt check
echo "--- Ensure libs importable" echo "--- Ensure libs importable"

View file

@ -136,6 +136,19 @@ impl Collection {
let deckconfig_id = deck.config_id().unwrap(); let deckconfig_id = deck.config_id().unwrap();
// reschedule it // reschedule it
let original_interval = card.interval; let original_interval = card.interval;
let min_interval = |interval: u32| {
let previous_interval =
last_info.previous_interval.unwrap_or(0);
if interval > previous_interval {
// interval grew; don't allow fuzzed interval to
// be less than previous+1
previous_interval + 1
} else {
// interval shrunk; don't restrict negative fuzz
0
}
.max(1)
};
let interval = fsrs.next_interval( let interval = fsrs.next_interval(
Some(state.stability), Some(state.stability),
desired_retention, desired_retention,
@ -146,7 +159,7 @@ impl Collection {
.and_then(|r| { .and_then(|r| {
r.find_interval( r.find_interval(
interval, interval,
1, min_interval(interval as u32),
req.max_interval, req.max_interval,
days_elapsed as u32, days_elapsed as u32,
deckconfig_id, deckconfig_id,
@ -157,7 +170,7 @@ impl Collection {
with_review_fuzz( with_review_fuzz(
card.get_fuzz_factor(true), card.get_fuzz_factor(true),
interval, interval,
1, min_interval(interval as u32),
req.max_interval, req.max_interval,
) )
}); });
@ -310,6 +323,9 @@ pub(crate) struct LastRevlogInfo {
/// reviewed the card and now, so that we can determine an accurate period /// reviewed the card and now, so that we can determine an accurate period
/// when the card has subsequently been rescheduled to a different day. /// when the card has subsequently been rescheduled to a different day.
pub(crate) last_reviewed_at: Option<TimestampSecs>, pub(crate) last_reviewed_at: Option<TimestampSecs>,
/// The interval before the latest review. Used to prevent fuzz from going
/// backwards when rescheduling the card
pub(crate) previous_interval: Option<u32>,
} }
/// Return a map of cards to info about last review. /// Return a map of cards to info about last review.
@ -321,14 +337,27 @@ pub(crate) fn get_last_revlog_info(revlogs: &[RevlogEntry]) -> HashMap<CardId, L
.into_iter() .into_iter()
.for_each(|(card_id, group)| { .for_each(|(card_id, group)| {
let mut last_reviewed_at = None; let mut last_reviewed_at = None;
let mut previous_interval = None;
for e in group.into_iter() { for e in group.into_iter() {
if e.has_rating_and_affects_scheduling() { if e.has_rating_and_affects_scheduling() {
last_reviewed_at = Some(e.id.as_secs()); last_reviewed_at = Some(e.id.as_secs());
previous_interval = if e.last_interval >= 0 && e.button_chosen > 1 {
Some(e.last_interval as u32)
} else {
None
};
} else if e.is_reset() { } else if e.is_reset() {
last_reviewed_at = None; last_reviewed_at = None;
previous_interval = None;
} }
} }
out.insert(card_id, LastRevlogInfo { last_reviewed_at }); out.insert(
card_id,
LastRevlogInfo {
last_reviewed_at,
previous_interval,
},
);
}); });
out out
} }

View file

@ -115,13 +115,14 @@ impl Rescheduler {
pub fn find_interval( pub fn find_interval(
&self, &self,
interval: f32, interval: f32,
minimum: u32, minimum_interval: u32,
maximum: u32, maximum_interval: u32,
days_elapsed: u32, days_elapsed: u32,
deckconfig_id: DeckConfigId, deckconfig_id: DeckConfigId,
fuzz_seed: Option<u64>, fuzz_seed: Option<u64>,
) -> Option<u32> { ) -> Option<u32> {
let (before_days, after_days) = constrained_fuzz_bounds(interval, minimum, maximum); let (before_days, after_days) =
constrained_fuzz_bounds(interval, minimum_interval, maximum_interval);
// Don't reschedule the card when it's overdue // Don't reschedule the card when it's overdue
if after_days < days_elapsed { if after_days < days_elapsed {