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
fi
rm -f out/build.ninja
./ninja pylib qt check
echo "--- Ensure libs importable"

View file

@ -136,6 +136,19 @@ impl Collection {
let deckconfig_id = deck.config_id().unwrap();
// reschedule it
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(
Some(state.stability),
desired_retention,
@ -146,7 +159,7 @@ impl Collection {
.and_then(|r| {
r.find_interval(
interval,
1,
min_interval(interval as u32),
req.max_interval,
days_elapsed as u32,
deckconfig_id,
@ -157,7 +170,7 @@ impl Collection {
with_review_fuzz(
card.get_fuzz_factor(true),
interval,
1,
min_interval(interval as u32),
req.max_interval,
)
});
@ -310,6 +323,9 @@ pub(crate) struct LastRevlogInfo {
/// reviewed the card and now, so that we can determine an accurate period
/// when the card has subsequently been rescheduled to a different day.
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.
@ -321,14 +337,27 @@ pub(crate) fn get_last_revlog_info(revlogs: &[RevlogEntry]) -> HashMap<CardId, L
.into_iter()
.for_each(|(card_id, group)| {
let mut last_reviewed_at = None;
let mut previous_interval = None;
for e in group.into_iter() {
if e.has_rating_and_affects_scheduling() {
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() {
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
}

View file

@ -115,13 +115,14 @@ impl Rescheduler {
pub fn find_interval(
&self,
interval: f32,
minimum: u32,
maximum: u32,
minimum_interval: u32,
maximum_interval: u32,
days_elapsed: u32,
deckconfig_id: DeckConfigId,
fuzz_seed: Option<u64>,
) -> 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
if after_days < days_elapsed {