Repository: st3v3nmw/obsidian-spaced-repetition
Pull request #1006 (2024-06-25 07:47:02)

feat: refactor code to support diff methods of storing the scheduling info, and diff SR algorithms
Hey there, finally have implemented and tested these non-trivial changes - and is now ready for review and beta testing.

https://github.com/st3v3nmw/obsidian-spaced-repetition/issues/878

From the user perspective, there shouldn't be any functional change. But the architecture has changed as follows:

# Code Changes

### Algorithms

The code for implementing the current algorithm has been extracted from the various files and placed in a new class `SrsAlgorithm_Osr` that implements a new interface `ISrsAlgorithm`.

Future algorithms would similarly need to implement the interface `ISrsAlgorithm`.


### Data Stores

The existing code for saving the scheduling info (both for notes in the frontmatter, as well as the HTML comments for the flashcards) have been placed in a new class `DataStore_StoreInNote` that implements a new interface `IDataStore`.

Methods for storing the schedule info in alternate ways would similarly need to implement the `IDataStore` interface.


### DataStoreAlgorithm

Although most of the existing code could be classified either algorithm or data store, some good not. This code was specific to the algorithm and data store combination.

That code was extracted and placed in a new class `DataStoreInNote_AlgorithmOsr` that implements the new interface `IDataStoreAlgorithm`


### Separation Of Core Logic From User Interface Code

https://github.com/st3v3nmw/obsidian-spaced-repetition/pull/751 (about 9 months ago) separated out much business logic from user interface code. Similar change was required in order to achieve the above.


### Unit testing

With more code separated from UI code, it was possible to include more unit test cases. About 30 more test cases were written.

# Next Steps

[ [FEAT] user defined "Easy, Good, Hard" values instead of or in addition to the algorithm defined one. #741 ](https://github.com/st3v3nmw/obsidian-spaced-repetition/issues/741) - Suggestion by @danhje as an easy way to further test these changes

[ [FEAT] Stop using YAML; Move plugin info and data to separate file #162 ](https://github.com/st3v3nmw/obsidian-spaced-repetition/issues/162) - Possibly necessary for supporting algorithms that require the storage of extensive scheduling info

[ [FEAT] sm-2 is outdated, can you please replace it with the fsrs algorithm? #748 ](https://github.com/st3v3nmw/obsidian-spaced-repetition/issues/748)


# Beta testing (help wanted!)

As you can appreciate, lots of work went into this. And even though more test cases were written, it's more than likely I inadvertently broke something (e.g. in functionality that I don't use myself).

[main_878A.zip](https://github.com/user-attachments/files/15966849/main_878A.zip)

@st3v3nmw would be great if you could have a look 


ronzulu: Hi @st3v3nmw , hope you are doing okay.

If you could please review this so that we can merge it into master.

Thanks
Ronny
ronzulu: Hi @st3v3nmw 

Great that I can merge this into master myself now. But probably better if you can review first.

Cheers
Ronny

![image](https://github.com/user-attachments/assets/3d03c37b-88ce-4e81-8caf-89ed51887a03)

ronzulu: Hi @st3v3nmw, just wondering when you will be able to review and merge this. Or are you happy for me to do that?
ronzulu: Hi @st3v3nmw I've updated this branch from master, and rectified the small number of conflicts.

I know that I can click "squash and merge" but thought I would leave it to you :-)

Cheers
Ronny
ronzulu: Hi @st3v3nmw there are a few PRs ready to merge and release.

I can release them as Beta, but only if they are first merged into master.

So I'll merge this and probably others into master now.

Cheers
Ronny

Patch:
diff --git a/.gitignore b/.gitignore
index a26f5ff..8e62b67 100644
--- a/.gitignore
+++ b/.gitignore
@@ -24,6 +24,7 @@ build
 # testing
 coverage
 tests/e2e/vault/
+tests/vaults/*/.obsidian
 
 # mkdocs
 site/
diff --git a/jest.config.js b/jest.config.js
index 6369022..4733813 100644
--- a/jest.config.js
+++ b/jest.config.js
@@ -11,10 +11,15 @@ module.exports = {
     roots: ["<rootDir>/src/", "<rootDir>/tests/unit/"],
     collectCoverageFrom: [
         "src/**/lang/*.ts",
+        "src/algorithms/**/*.ts",
+        "src/dataStore/**/*.ts",
+        "src/dataStoreAlgorithm/**/*.ts",
         "src/NoteEaseList.ts",
         "src/NoteFileLoader.ts",
         "src/NoteParser.ts",
         "src/NoteQuestionParser.ts",
+        "src/NoteReviewQueue.ts",
+        "src/OsrCore.ts",
         "src/TopicParser.ts",
         "src/parser.ts",
         "src/scheduling.ts",
@@ -22,6 +27,12 @@ module.exports = {
     ],
     coveragePathIgnorePatterns: [
         "/node_modules/",
+        "src/algorithms/base/ISrsAlgorithm",
+        "src/algorithms/base/RepItemScheduleInfo",
+        "src/algorithms/base/SrsAlgorithm",
+        "src/algorithms/osr/ObsidianVaultNoteLinkInfoFinder",
+        "src/dataStoreAlgorithm/DataStoreAlgorithm",
+        "src/dataStoreAlgorithm/IDataStoreAlgorithm",
         "src/lang/locale/",
         "src/constants",
         "src/icons",
@@ -33,8 +44,8 @@ module.exports = {
     coverageProvider: "v8",
     coverageThreshold: {
         global: {
-            statements: 100,
-            branches: 100,
+            statements: 99,
+            branches: 98,
         },
     },
 };
diff --git a/src/Card.ts b/src/Card.ts
index 2c4b259..4d28c61 100644
--- a/src/Card.ts
+++ b/src/Card.ts
@@ -1,22 +1,17 @@
 import { Question } from "./Question";
-import { CardScheduleInfo } from "./CardSchedule";
 import { CardListType } from "./Deck";
+import { RepetitionItem } from "./algorithms/base/RepetitionItem";
 
-export class Card {
+export class Card extends RepetitionItem {
     question: Question;
     cardIdx: number;
 
-    // scheduling
-    get hasSchedule(): boolean {
-        return this.scheduleInfo != null;
-    }
-    scheduleInfo?: CardScheduleInfo;
-
     // visuals
     front: string;
     back: string;
 
     constructor(init?: Partial<Card>) {
+        super();
         Object.assign(this, init);
     }
 
@@ -24,17 +19,9 @@ export class Card {
         return this.isNew ? CardListType.NewCard : CardListType.DueCard;
     }
 
-    get isNew(): boolean {
-        return !this.hasSchedule || this.scheduleInfo.isDummyScheduleForNewCard();
-    }
-
-    get isDue(): boolean {
-        return this.hasSchedule && this.scheduleInfo.isDue();
-    }
-
     formatSchedule(): string {
         let result: string = "";
-        if (this.hasSchedule) result = this.scheduleInfo.formatSchedule();
+        if (this.hasSchedule) result = this.scheduleInfo.formatCardScheduleForHtmlComment();
         else result = "New";
         return result;
     }
diff --git a/src/CardSchedule.ts b/src/CardSchedule.ts
deleted file mode 100644
index bfda157..0000000
--- a/src/CardSchedule.ts
+++ /dev/null
@@ -1,184 +0,0 @@
-import { Moment } from "moment";
-import {
-    LEGACY_SCHEDULING_EXTRACTOR,
-    MULTI_SCHEDULING_EXTRACTOR,
-    TICKS_PER_DAY,
-} from "./constants";
-import { INoteEaseList } from "./NoteEaseList";
-import { ReviewResponse, schedule } from "./scheduling";
-import { SRSettings } from "./settings";
-import { formatDate_YYYY_MM_DD } from "./util/utils";
-import { DateUtil, globalDateProvider } from "./util/DateProvider";
-
-export class CardScheduleInfo {
-    dueDate: Moment;
-    interval: number;
-    ease: number;
-    delayBeforeReviewTicks: number;
-
-    // A question can have multiple cards. The schedule info for all sibling cards are formatted together
-    // in a single <!--SR: --> comment, such as:
-    // <!--SR:!2023-09-02,4,270!2023-09-02,5,270!2023-09-02,6,270!2023-09-02,7,270-->
-    //
-    // However, not all sibling cards may have been reviewed. Therefore we need a method of indicating that a particular card
-    // has not been reviewed, and should be considered "new"
-    // This is done by using this magic value for the date
-    private static dummyDueDateForNewCard: string = "2000-01-01";
-
-    constructor(dueDate: Moment, interval: number, ease: number, delayBeforeReviewTicks: number) {
-        this.dueDate = dueDate;
-        this.interval = interval;
-        this.ease = ease;
-        this.delayBeforeReviewTicks = delayBeforeReviewTicks;
-    }
-
-    get delayBeforeReviewDaysInt(): number {
-        return Math.ceil(this.delayBeforeReviewTicks / TICKS_PER_DAY);
-    }
-
-    isDue(): boolean {
-        return this.dueDate.isSameOrBefore(globalDateProvider.today);
-    }
-
-    isDummyScheduleForNewCard(): boolean {
-        return this.formatDueDate() == CardScheduleInfo.dummyDueDateForNewCard;
-    }
-
-    static getDummyScheduleForNewCard(settings: SRSettings): CardScheduleInfo {
-        return CardScheduleInfo.fromDueDateStr(
-            CardScheduleInfo.dummyDueDateForNewCard,
-            CardScheduleInfo.initialInterval,
-            settings.baseEase,
-            0,
-        );
-    }
-
-    static fromDueDateStr(
-        dueDateStr: string,
-        interval: number,
-        ease: number,
-        delayBeforeReviewTicks: number,
-    ) {
-        const dueDateTicks: Moment = DateUtil.dateStrToMoment(dueDateStr);
-        return new CardScheduleInfo(dueDateTicks, interval, ease, delayBeforeReviewTicks);
-    }
-
-    static fromDueDateMoment(
-        dueDateTicks: Moment,
-        interval: number,
-        ease: number,
-        delayBeforeReviewTicks: number,
-    ) {
-        return new CardScheduleInfo(dueDateTicks, interval, ease, delayBeforeReviewTicks);
-    }
-
-    static get initialInterval(): number {
-        return 1.0;
-    }
-
-    formatDueDate(): string {
-        return formatDate_YYYY_MM_DD(this.dueDate);
-    }
-
-    formatSchedule() {
-        return `!${this.formatDueDate()},${this.interval},${this.ease}`;
-    }
-}
-
-export interface ICardScheduleCalculator {
-    getResetCardSchedule(): CardScheduleInfo;
-    getNewCardSchedule(response: ReviewResponse, notePath: string): CardScheduleInfo;
-    calcUpdatedSchedule(response: ReviewResponse, schedule: CardScheduleInfo): CardScheduleInfo;
-}
-
-export class CardScheduleCalculator {
-    settings: SRSettings;
-    noteEaseList: INoteEaseList;
-    dueDatesFlashcards: Record<number, number> = {}; // Record<# of days in future, due count>
-
-    constructor(settings: SRSettings, noteEaseList: INoteEaseList) {
-        this.settings = settings;
-        this.noteEaseList = noteEaseList;
-    }
-
-    getResetCardSchedule(): CardScheduleInfo {
-        const interval = CardScheduleInfo.initialInterval;
-        const ease = this.settings.baseEase;
-        const dueDate = globalDateProvider.today.add(interval, "d");
-        const delayBeforeReview = 0;
-        return CardScheduleInfo.fromDueDateMoment(dueDate, interval, ease, delayBeforeReview);
-    }
-
-    getNewCardSchedule(response: ReviewResponse, notePath: string): CardScheduleInfo {
-        let initial_ease: number = this.settings.baseEase;
-        if (this.noteEaseList.hasEaseForPath(notePath)) {
-            initial_ease = Math.round(this.noteEaseList.getEaseByPath(notePath));
-        }
-        const delayBeforeReview = 0;
-
-        const schedObj: Record<string, number> = schedule(
-            response,
-            CardScheduleInfo.initialInterval,
-            initial_ease,
-            delayBeforeReview,
-            this.settings,
-            this.dueDatesFlashcards,
-        );
-
-        const interval = schedObj.interval;
-        const ease = schedObj.ease;
-        const dueDate = globalDateProvider.today.add(interval, "d");
-        return CardScheduleInfo.fromDueDateMoment(dueDate, interval, ease, delayBeforeReview);
-    }
-
-    calcUpdatedSchedule(
-        response: ReviewResponse,
-        cardSchedule: CardScheduleInfo,
-    ): CardScheduleInfo {
-        const schedObj: Record<string, number> = schedule(
-            response,
-            cardSchedule.interval,
-            cardSchedule.ease,
-            cardSchedule.delayBeforeReviewTicks,
-            this.settings,
-            this.dueDatesFlashcards,
-        );
-        const interval = schedObj.interval;
-        const ease = schedObj.ease;
-        const dueDate = globalDateProvider.today.add(interval, "d");
-        const delayBeforeReview = 0;
-        return CardScheduleInfo.fromDueDateMoment(dueDate, interval, ease, delayBeforeReview);
-    }
-}
-
-export class NoteCardScheduleParser {
-    static createCardScheduleInfoList(questionText: string): CardScheduleInfo[] {
-        let scheduling: RegExpMatchArray[] = [...questionText.matchAll(MULTI_SCHEDULING_EXTRACTOR)];
-        if (scheduling.length === 0)
-            scheduling = [...questionText.matchAll(LEGACY_SCHEDULING_EXTRACTOR)];
-
-        const result: CardScheduleInfo[] = [];
-        for (let i = 0; i < scheduling.length; i++) {
-            const match: RegExpMatchArray = scheduling[i];
-            const dueDateStr = match[1];
-            const interval = parseInt(match[2]);
-            const ease = parseInt(match[3]);
-            const dueDate: Moment = DateUtil.dateStrToMoment(dueDateStr);
-            const delayBeforeReviewTicks: number =
-                dueDate.valueOf() - globalDateProvider.today.valueOf();
-
-            const info: CardScheduleInfo = new CardScheduleInfo(
-                dueDate,
-                interval,
-                ease,
-                delayBeforeReviewTicks,
-            );
-            result.push(info);
-        }
-        return result;
-    }
-
-    static removeCardScheduleInfo(questionText: string): string {
-        return questionText.replace(/<!--SR:.+-->/gm, "");
-    }
-}
diff --git a/src/DeckTreeStatsCalculator.ts b/src/DeckTreeStatsCalculator.ts
index 2375cd7..df47dba 100644
--- a/src/DeckTreeStatsCalculator.ts
+++ b/src/DeckTreeStatsCalculator.ts
@@ -8,8 +8,8 @@ import {
 } from "./DeckTreeIterator";
 import { Card } from "./Card";
 import { Stats } from "./stats";
-import { CardScheduleInfo } from "./CardSchedule";
 import { TopicPath } from "./TopicPath";
+import { RepItemScheduleInfo } from "./algorithms/base/RepItemScheduleInfo";
 
 export class DeckTreeStatsCalculator {
     private deckTree: Deck;
@@ -27,8 +27,12 @@ export class DeckTreeStatsCalculator {
         while (iterator.nextCard()) {
             const card: Card = iterator.currentCard;
             if (card.hasSchedule) {
-                const schedule: CardScheduleInfo = card.scheduleInfo;
-                result.update(schedule.delayBeforeReviewDaysInt, schedule.interval, schedule.ease);
+                const schedule: RepItemScheduleInfo = card.scheduleInfo;
+                result.update(
+                    schedule.delayedBeforeReviewDaysInt(),
+                    schedule.interval,
+                    schedule.latestEase,
+                );
             } else {
                 result.incrementNew();
             }
diff --git a/src/DueDateHistogram.ts b/src/DueDateHistogram.ts
new file mode 100644
index 0000000..1d0f5eb
--- /dev/null
+++ b/src/DueDateHistogram.ts
@@ -0,0 +1,137 @@
+import { Card } from "./Card";
+import { Deck } from "./Deck";
+import {
+    CardOrder,
+    DeckOrder,
+    DeckTreeIterator,
+    IDeckTreeIterator,
+    IIteratorOrder,
+} from "./DeckTreeIterator";
+import { NoteReviewDeck, SchedNote } from "./NoteReviewDeck";
+import { TopicPath } from "./TopicPath";
+import { RepItemScheduleInfo } from "./algorithms/base/RepItemScheduleInfo";
+import { OsrNoteGraph } from "./algorithms/osr/OsrNoteGraph";
+import { TICKS_PER_DAY } from "./constants";
+import { globalDateProvider } from "./util/DateProvider";
+
+export class DueDateHistogram {
+    // The key for dueDatesNotes is the number of days after today
+    // therefore the key to lookup how many cards are due today is 0
+    public static dueNowNDays: number = 0;
+
+    // Key - # of days in future
+    // Value - Count of notes due
+    dueDatesMap: Map<number, number> = new Map<number, number>();
+
+    constructor(rec: Record<number, number> = null) {
+        this.dueDatesMap = new Map<number, number>();
+        if (rec != null) {
+            Object.entries(rec).forEach(([key, value]) => {
+                this.dueDatesMap.set(Number(key), value);
+            });
+        }
+    }
+
+    get dueNotesCount(): number {
+        let result: number = 0;
+        if (this.dueDatesMap.has(DueDateHistogram.dueNowNDays))
+            result = this.dueDatesMap.get(DueDateHistogram.dueNowNDays);
+        return result;
+    }
+
+    hasEntryForDays(days: number): boolean {
+        return this.dueDatesMap.has(days);
+    }
+
+    set(days: number, value: number): void {
+        this.dueDatesMap.set(days, value);
+    }
+
+    get(days: number): number {
+        return this.dueDatesMap.get(days);
+    }
+
+    increment(days: number): void {
+        let value: number = 0;
+        if (this.dueDatesMap.has(days)) {
+            value = this.dueDatesMap.get(days);
+        }
+        this.dueDatesMap.set(days, value + 1);
+    }
+
+    decrement(days: number): void {
+        let value: number = 0;
+        if (this.dueDatesMap.has(days)) value = this.dueDatesMap.get(days);
+        if (value > 0) {
+            this.dueDatesMap.set(days, value - 1);
+        }
+    }
+
+    findLeastUsedIntervalOverRange(originalInterval: number, fuzz: number): number {
+        if (!this.hasEntryForDays(originalInterval)) {
+            // There are no entries for the interval originalInterval - can't get a better result
+            return originalInterval;
+        }
+        let interval: number = originalInterval;
+        outer: for (let i = 1; i <= fuzz; i++) {
+            for (const ivl of [originalInterval - i, originalInterval + i]) {
+                if (!this.hasEntryForDays(ivl)) {
+                    // There are no entries for the interval ivl - can't get a better result
+                    interval = ivl;
+                    break outer;
+                }
+
+                // We've found a better result, but keep searching
+                if (this.dueDatesMap.get(ivl) < this.dueDatesMap.get(interval)) interval = ivl;
+            }
+        }
+        return interval;
+    }
+}
+
+export class NoteDueDateHistogram extends DueDateHistogram {
+    calculateFromReviewDecksAndSort(
+        reviewDecks: Map<string, NoteReviewDeck>,
+        osrNoteGraph: OsrNoteGraph,
+    ): void {
+        this.dueDatesMap = new Map<number, number>();
+
+        const today: number = globalDateProvider.today.valueOf();
+        reviewDecks.forEach((reviewDeck: NoteReviewDeck) => {
+            reviewDeck.scheduledNotes.forEach((scheduledNote: SchedNote) => {
+                const nDays: number = Math.ceil((scheduledNote.dueUnix - today) / TICKS_PER_DAY);
+                this.increment(nDays);
+            });
+
+            reviewDeck.sortNotesByDateAndImportance(osrNoteGraph.pageranks);
+        });
+    }
+}
+
+export class CardDueDateHistogram extends DueDateHistogram {
+    calculateFromDeckTree(deckTree: Deck): void {
+        this.dueDatesMap = new Map<number, number>();
+
+        // Order doesn't matter as long as we iterate over everything
+        const iteratorOrder: IIteratorOrder = {
+            deckOrder: DeckOrder.PrevDeckComplete_Sequential,
+            cardOrder: CardOrder.DueFirstSequential,
+        };
+
+        // Iteration is a destructive operation on the supplied tree, so we first take a copy
+        const today: number = globalDateProvider.today.valueOf();
+        const iterator: IDeckTreeIterator = new DeckTreeIterator(iteratorOrder, deckTree.clone());
+        iterator.setIteratorTopicPath(TopicPath.emptyPath);
+        while (iterator.nextCard()) {
+            const card: Card = iterator.currentCard;
+            if (card.hasSchedule) {
+                const scheduledCard: RepItemScheduleInfo = card.scheduleInfo;
+
+                const nDays: number = Math.ceil(
+                    (scheduledCard.dueDateAsUnix - today) / TICKS_PER_DAY,
+                );
+                this.increment(nDays);
+            }
+        }
+    }
+}
diff --git a/src/FlashcardReviewSequencer.ts b/src/FlashcardReviewSequencer.ts
index 5ef7466..d2a9135 100644
--- a/src/FlashcardReviewSequencer.ts
+++ b/src/FlashcardReviewSequencer.ts
@@ -1,13 +1,16 @@
 import { Card } from "./Card";
 import { CardListType, Deck } from "./Deck";
 import { Question, QuestionText } from "./Question";
-import { ReviewResponse } from "./scheduling";
 import { SRSettings } from "./settings";
 import { TopicPath } from "./TopicPath";
-import { CardScheduleInfo, ICardScheduleCalculator } from "./CardSchedule";
 import { Note } from "./Note";
 import { IDeckTreeIterator } from "./DeckTreeIterator";
 import { IQuestionPostponementList } from "./QuestionPostponementList";
+import { ISrsAlgorithm } from "./algorithms/base/ISrsAlgorithm";
+import { ReviewResponse } from "./algorithms/base/RepetitionItem";
+import { RepItemScheduleInfo } from "./algorithms/base/RepItemScheduleInfo";
+import { DataStore } from "./dataStore/base/DataStore";
+import { DueDateHistogram } from "./DueDateHistogram";
 
 export interface IFlashcardReviewSequencer {
     get hasCurrentCard(): boolean;
@@ -21,7 +24,7 @@ export interface IFlashcardReviewSequencer {
     setCurrentDeck(topicPath: TopicPath): void;
     getDeckStats(topicPath: TopicPath): DeckStats;
     skipCurrentCard(): void;
-    determineCardSchedule(response: ReviewResponse, card: Card): CardScheduleInfo;
+    determineCardSchedule(response: ReviewResponse, card: Card): RepItemScheduleInfo;
     processReview(response: ReviewResponse): Promise<void>;
     updateCurrentQuestionText(text: string): Promise<void>;
 }
@@ -53,21 +56,24 @@ export class FlashcardReviewSequencer implements IFlashcardReviewSequencer {
     private reviewMode: FlashcardReviewMode;
     private cardSequencer: IDeckTreeIterator;
     private settings: SRSettings;
-    private cardScheduleCalculator: ICardScheduleCalculator;
+    private srsAlgorithm: ISrsAlgorithm;
     private questionPostponementList: IQuestionPostponementList;
+    private dueDateFlashcardHistogram: DueDateHistogram;
 
     constructor(
         reviewMode: FlashcardReviewMode,
         cardSequencer: IDeckTreeIterator,
         settings: SRSettings,
-        cardScheduleCalculator: ICardScheduleCalculator,
+        srsAlgorithm: ISrsAlgorithm,
         questionPostponementList: IQuestionPostponementList,
+        dueDateFlashcardHistogram: DueDateHistogram,
     ) {
         this.reviewMode = reviewMode;
         this.cardSequencer = cardSequencer;
         this.settings = settings;
-        this.cardScheduleCalculator = cardScheduleCalculator;
+        this.srsAlgorithm = srsAlgorithm;
         this.questionPostponementList = questionPostponementList;
+        this.dueDateFlashcardHistogram = dueDateFlashcardHistogram;
     }
 
     get hasCurrentCard(): boolean {
@@ -147,7 +153,7 @@ export class FlashcardReviewSequencer implements IFlashcardReviewSequencer {
             this.currentCard.scheduleInfo = this.determineCardSchedule(response, this.currentCard);
 
             // Update the source file with the updated schedule
-            await this.currentQuestion.writeQuestion(this.settings);
+            await DataStore.getInstance().questionWriteSchedule(this.currentQuestion);
         }
 
         // Move/delete the card
@@ -183,24 +189,26 @@ export class FlashcardReviewSequencer implements IFlashcardReviewSequencer {
         }
     }
 
-    determineCardSchedule(response: ReviewResponse, card: Card): CardScheduleInfo {
-        let result: CardScheduleInfo;
+    determineCardSchedule(response: ReviewResponse, card: Card): RepItemScheduleInfo {
+        let result: RepItemScheduleInfo;
 
         if (response == ReviewResponse.Reset) {
             // Resetting the card schedule
-            result = this.cardScheduleCalculator.getResetCardSchedule();
+            result = this.srsAlgorithm.cardGetResetSchedule();
         } else {
             // scheduled card
             if (card.hasSchedule) {
-                result = this.cardScheduleCalculator.calcUpdatedSchedule(
+                result = this.srsAlgorithm.cardCalcUpdatedSchedule(
                     response,
                     card.scheduleInfo,
+                    this.dueDateFlashcardHistogram,
                 );
             } else {
                 const currentNote: Note = card.question.note;
-                result = this.cardScheduleCalculator.getNewCardSchedule(
+                result = this.srsAlgorithm.cardGetNewSchedule(
                     response,
                     currentNote.filePath,
+                    this.dueDateFlashcardHistogram,
                 );
             }
         }
@@ -212,6 +220,6 @@ export class FlashcardReviewSequencer implements IFlashcardReviewSequencer {
 
         q.actualQuestion = text;
 
-        await this.currentQuestion.writeQuestion(this.settings);
+        await DataStore.getInstance().questionWrite(this.currentQuestion);
     }
 }
diff --git a/src/NextNoteReviewHandler.ts b/src/NextNoteReviewHandler.ts
new file mode 100644
index 0000000..5476d67
--- /dev/null
+++ b/src/NextNoteReviewHandler.ts
@@ -0,0 +1,83 @@
+import { App, Notice, TFile, Workspace } from "obsidian";
+import { SRSettings } from "./settings";
+import { NoteReviewQueue } from "./NoteReviewQueue";
+import { t } from "./lang/helpers";
+import { ReviewDeckSelectionModal } from "./gui/ReviewDeckSelectionModal";
+
+export class NextNoteReviewHandler {
+    private app: App;
+    private settings: SRSettings;
+    private workspace: Workspace;
+    private _noteReviewQueue: NoteReviewQueue;
+    private _lastSelectedReviewDeck: string;
+
+    get lastSelectedReviewDeck(): string {
+        return this._lastSelectedReviewDeck;
+    }
+
+    get noteReviewQueue(): NoteReviewQueue {
+        return this._noteReviewQueue;
+    }
+
+    constructor(
+        app: App,
+        settings: SRSettings,
+        workspace: Workspace,
+        noteReviewQueue: NoteReviewQueue,
+    ) {
+        this.app = app;
+        this.settings = settings;
+        this.workspace = workspace;
+        this._noteReviewQueue = noteReviewQueue;
+    }
+
+    async autoReviewNextNote(): Promise<void> {
+        if (this.settings.autoNextNote) {
+            if (!this._lastSelectedReviewDeck) {
+                const reviewDeckKeys: string[] = this._noteReviewQueue.reviewDeckNameList;
+                if (reviewDeckKeys.length > 0) this._lastSelectedReviewDeck = reviewDeckKeys[0];
+                else {
+                    // 2024-07-05 existing functionality: Code doesn't look at other decks
+                    new Notice(t("ALL_CAUGHT_UP"));
+                    return;
+                }
+            }
+            this.reviewNextNote(this._lastSelectedReviewDeck);
+        }
+    }
+
+    async reviewNextNoteModal(): Promise<void> {
+        const reviewDeckNames: string[] = this._noteReviewQueue.reviewDeckNameList;
+
+        if (reviewDeckNames.length === 1) {
+            // There is only one deck, so no need to ask the user to make a selection
+            this.reviewNextNote(reviewDeckNames[0]);
+        } else {
+            const deckSelectionModal = new ReviewDeckSelectionModal(this.app, reviewDeckNames);
+            deckSelectionModal.submitCallback = (deckKey: string) => this.reviewNextNote(deckKey);
+            deckSelectionModal.open();
+        }
+    }
+
+    async reviewNextNote(deckKey: string): Promise<void> {
+        if (!this._noteReviewQueue.reviewDeckNameList.contains(deckKey)) {
+            new Notice(t("NO_DECK_EXISTS", { deckName: deckKey }));
+            return;
+        }
+
+        this._lastSelectedReviewDeck = deckKey;
+        const deck = this._noteReviewQueue.reviewDecks.get(deckKey);
+        const notefile = deck.determineNextNote(this.settings.openRandomNote);
+
+        if (notefile) {
+            await this.openNote(deckKey, notefile.tfile);
+        } else {
+            new Notice(t("ALL_CAUGHT_UP"));
+        }
+    }
+
+    async openNote(deckName: string, file: TFile): Promise<void> {
+        this._lastSelectedReviewDeck = deckName;
+        await this.app.workspace.getLeaf().openFile(file);
+    }
+}
diff --git a/src/Note.ts b/src/Note.ts
index 1546693..74e457f 100644
--- a/src/Note.ts
+++ b/src/Note.ts
@@ -44,7 +44,7 @@ export class Note {
         let fileText: string = await this.file.read();
         for (const question of this.questionList) {
             if (question.hasChanged) {
-                fileText = question.updateQuestionText(fileText, settings);
+                fileText = question.updateQuestionWithinNoteText(fileText, settings);
             }
         }
         await this.file.write(fileText);
diff --git a/src/NoteEaseCalculator.ts b/src/NoteEaseCalculator.ts
deleted file mode 100644
index 24cc1be..0000000
--- a/src/NoteEaseCalculator.ts
+++ /dev/null
@@ -1,31 +0,0 @@
-import { Note } from "./Note";
-import { SRSettings } from "./settings";
-
-export class NoteEaseCalculator {
-    static Calculate(note: Note, settings: SRSettings): number {
-        let totalEase: number = 0;
-        let scheduledCount: number = 0;
-
-        note.questionList.forEach((question) => {
-            question.cards
-                .filter((card) => card.hasSchedule)
-                .forEach((card) => {
-                    totalEase += card.scheduleInfo.ease;
-                    scheduledCount++;
-                });
-        });
-
-        let result: number = 0;
-        if (scheduledCount > 0) {
-            const flashcardsInNoteAvgEase: number = totalEase / scheduledCount;
-            const flashcardContribution: number = Math.min(
-                1.0,
-                Math.log(scheduledCount + 0.5) / Math.log(64),
-            );
-            result =
-                flashcardsInNoteAvgEase * flashcardContribution +
-                settings.baseEase * (1.0 - flashcardContribution);
-        }
-        return result;
-    }
-}
diff --git a/src/NoteQuestionParser.ts b/src/NoteQuestionParser.ts
index 78d664e..31a926a 100644
--- a/src/NoteQuestionParser.ts
+++ b/src/NoteQuestionParser.ts
@@ -1,14 +1,15 @@
+import { RepItemScheduleInfo } from "./algorithms/base/RepItemScheduleInfo";
+import { DataStore } from "./dataStore/base/DataStore";
 import { TagCache } from "obsidian";
+import { Question, QuestionText } from "./Question";
 import { Card } from "./Card";
-import { CardScheduleInfo, NoteCardScheduleParser } from "./CardSchedule";
 import { parseEx, ParsedQuestionInfo, ParserOptions } from "./parser";
-import { Question, QuestionText } from "./Question";
 import { CardFrontBack, CardFrontBackUtil } from "./QuestionType";
 import { SRSettings, SettingsUtil } from "./settings";
 import { ISRFile, frontmatterTagPseudoLineNum } from "./SRFile";
 import { TopicPath, TopicPathList } from "./TopicPath";
+import { splitNoteIntoFrontmatterAndContent, splitTextIntoLineArray } from "./util/utils";
 import { TextDirection } from "./util/TextDirection";
-import { extractFrontmatter, splitTextIntoLineArray } from "./util/utils";
 
 export class NoteQuestionParser {
     settings: SRSettings;
@@ -63,7 +64,7 @@ export class NoteQuestionParser {
 
             // The following analysis can require fair computation.
             // There is no point doing it if there aren't any topic paths
-            [this.frontmatterText, this.contentText] = extractFrontmatter(noteText);
+            [this.frontmatterText, this.contentText] = splitNoteIntoFrontmatterAndContent(noteText);
 
             // Create the question list
             let textDirection: TextDirection = noteFile.getTextDirection();
@@ -116,8 +117,11 @@ export class NoteQuestionParser {
             );
 
             // And if the card has been reviewed, then scheduling info as well
-            let cardScheduleInfoList: CardScheduleInfo[] =
-                NoteCardScheduleParser.createCardScheduleInfoList(question.questionText.original);
+            let cardScheduleInfoList: RepItemScheduleInfo[] =
+                DataStore.getInstance().questionCreateSchedule(
+                    question.questionText.original,
+                    null,
+                );
 
             // we have some extra scheduling dates to delete
             const correctLength = cardFrontBackList.length;
@@ -137,7 +141,7 @@ export class NoteQuestionParser {
     private parseQuestions(): ParsedQuestionInfo[] {
         // We pass contentText which has the frontmatter blanked out; see extractFrontmatter for reasoning
         const parserOptions: ParserOptions = {
-            singleLineCardSeparator:this.settings.singleLineCardSeparator,
+            singleLineCardSeparator: this.settings.singleLineCardSeparator,
             singleLineReversedCardSeparator: this.settings.singleLineReversedCardSeparator,
             multilineCardSeparator: this.settings.multilineCardSeparator,
             multilineReversedCardSeparator: this.settings.multilineReversedCardSeparator,
@@ -170,7 +174,7 @@ export class NoteQuestionParser {
 
     private createCardList(
         cardFrontBackList: CardFrontBack[],
-        cardScheduleInfoList: CardScheduleInfo[],
+        cardScheduleInfoList: RepItemScheduleInfo[],
     ): Card[] {
         const siblings: Card[] = [];
 
@@ -179,15 +183,15 @@ export class NoteQuestionParser {
             const { front, back } = cardFrontBackList[i];
 
             const hasScheduleInfo: boolean = i < cardScheduleInfoList.length;
-            const schedule: CardScheduleInfo = cardScheduleInfoList[i];
+            const schedule: RepItemScheduleInfo = cardScheduleInfoList[i];
 
             const cardObj: Card = new Card({
                 front,
                 back,
                 cardIdx: i,
             });
-            cardObj.scheduleInfo =
-                hasScheduleInfo && !schedule.isDummyScheduleForNewCard() ? schedule : null;
+
+            cardObj.scheduleInfo = hasScheduleInfo ? schedule : null;
 
             siblings.push(cardObj);
         }
diff --git a/src/NoteReviewDeck.ts b/src/NoteReviewDeck.ts
new file mode 100644
index 0000000..0051e59
--- /dev/null
+++ b/src/NoteReviewDeck.ts
@@ -0,0 +1,99 @@
+import { t } from "src/lang/helpers";
+import { ISRFile } from "./SRFile";
+import { globalRandomNumberProvider } from "./util/RandomNumberProvider";
+
+export class SchedNote {
+    note: ISRFile;
+    dueUnix: number;
+
+    constructor(note: ISRFile, dueUnix: number) {
+        this.note = note;
+        this.dueUnix = dueUnix;
+    }
+
+    isDue(todayUnix: number): boolean {
+        return this.dueUnix <= todayUnix;
+    }
+}
+
+export class NoteReviewDeck {
+    // Deck name such as the default "#review"
+    private _deckName: string;
+
+    private _newNotes: ISRFile[] = [];
+    private _scheduledNotes: SchedNote[] = [];
+    private _dueNotesCount = 0;
+
+    // This stores the collapsed/expanded state of each folder (folder names being things like
+    // "TODAY", "NEW" or formatted dates).
+    private _activeFolders: Set<string>;
+
+    get deckName(): string {
+        return this._deckName;
+    }
+
+    get newNotes(): ISRFile[] {
+        return this._newNotes;
+    }
+
+    get scheduledNotes(): SchedNote[] {
+        return this._scheduledNotes;
+    }
+
+    get dueNotesCount(): number {
+        return this._dueNotesCount;
+    }
+
+    get activeFolders(): Set<string> {
+        return this._activeFolders;
+    }
+
+    constructor(name: string) {
+        this._deckName = name;
+        this._activeFolders = new Set([this._deckName, t("TODAY")]);
+    }
+
+    public calcDueNotesCount(todayUnix: number): void {
+        this._dueNotesCount = 0;
+        this.scheduledNotes.forEach((scheduledNote: SchedNote) => {
+            if (scheduledNote.isDue(todayUnix)) {
+                this._dueNotesCount++;
+            }
+        });
+    }
+
+    public sortNotesByDateAndImportance(pageranks: Record<string, number>): void {
+        // sort new notes by importance
+        this._newNotes = this.newNotes.sort(
+            (a: ISRFile, b: ISRFile) => (pageranks[b.path] || 0) - (pageranks[a.path] || 0),
+        );
+
+        // sort scheduled notes by date & within those days, sort them by importance
+        this._scheduledNotes = this.scheduledNotes.sort((a: SchedNote, b: SchedNote) => {
+            const result = a.dueUnix - b.dueUnix;
+            if (result != 0) {
+                return result;
+            }
+            return (pageranks[b.note.path] || 0) - (pageranks[a.note.path] || 0);
+        });
+    }
+
+    determineNextNote(openRandomNote: boolean): ISRFile {
+        // Review due notes before new ones
+        if (this.dueNotesCount > 0) {
+            const index = openRandomNote
+                ? globalRandomNumberProvider.getInteger(0, this.dueNotesCount - 1)
+                : 0;
+            return this.scheduledNotes[index].note;
+        }
+
+        if (this.newNotes.length > 0) {
+            const index = openRandomNote
+                ? globalRandomNumberProvider.getInteger(0, this.newNotes.length - 1)
+                : 0;
+            return this.newNotes[index];
+        }
+
+        return null;
+    }
+}
diff --git a/src/NoteReviewQueue.ts b/src/NoteReviewQueue.ts
new file mode 100644
index 0000000..bc05709
--- /dev/null
+++ b/src/NoteReviewQueue.ts
@@ -0,0 +1,78 @@
+import { NoteReviewDeck, SchedNote } from "./NoteReviewDeck";
+import { ISRFile } from "./SRFile";
+import { RepItemScheduleInfo } from "./algorithms/base/RepItemScheduleInfo";
+
+export class NoteReviewQueue {
+    private _reviewDecks: Map<string, NoteReviewDeck>;
+    private _dueNotesCount: number;
+
+    get reviewDecks(): Map<string, NoteReviewDeck> {
+        return this._reviewDecks;
+    }
+
+    get dueNotesCount(): number {
+        return this._dueNotesCount;
+    }
+
+    get reviewDeckNameList(): string[] {
+        return [...this._reviewDecks.keys()];
+    }
+
+    init(): void {
+        this._reviewDecks = new Map<string, NoteReviewDeck>();
+    }
+
+    public calcDueNotesCount(todayUnix: number): void {
+        this._dueNotesCount = 0;
+        this._reviewDecks.forEach((reviewDeck: NoteReviewDeck) => {
+            reviewDeck.calcDueNotesCount(todayUnix);
+            this._dueNotesCount += reviewDeck.dueNotesCount;
+        });
+    }
+
+    addNoteToQueue(
+        noteFile: ISRFile,
+        noteSchedule: RepItemScheduleInfo,
+        matchedNoteTags: string[],
+    ): void {
+        for (const matchedNoteTag of matchedNoteTags) {
+            if (!this.reviewDecks.has(matchedNoteTag)) {
+                this.reviewDecks.set(matchedNoteTag, new NoteReviewDeck(matchedNoteTag));
+            }
+        }
+        if (noteSchedule == null) {
+            for (const matchedNoteTag of matchedNoteTags) {
+                this.reviewDecks.get(matchedNoteTag).newNotes.push(noteFile);
+            }
+        } else {
+            // schedule the note
+            for (const matchedNoteTag of matchedNoteTags) {
+                this.reviewDecks
+                    .get(matchedNoteTag)
+                    .scheduledNotes.push(new SchedNote(noteFile, noteSchedule.dueDateAsUnix));
+            }
+        }
+    }
+
+    updateScheduleInfo(note: ISRFile, scheduleInfo: RepItemScheduleInfo): void {
+        this.reviewDecks.forEach((reviewDeck: NoteReviewDeck) => {
+            let wasDueInDeck = false;
+            for (const scheduledNote of reviewDeck.scheduledNotes) {
+                if (scheduledNote.note.path === note.path) {
+                    scheduledNote.dueUnix = scheduleInfo.dueDate.valueOf();
+                    wasDueInDeck = true;
+                    break;
+                }
+            }
+
+            // It was a new note, remove it from the new notes and schedule it.
+            if (!wasDueInDeck) {
+                reviewDeck.newNotes.splice(
+                    reviewDeck.newNotes.findIndex((newNote: ISRFile) => newNote.path === note.path),
+                    1,
+                );
+                reviewDeck.scheduledNotes.push(new SchedNote(note, scheduleInfo.dueDate.valueOf()));
+            }
+        });
+    }
+}
diff --git a/src/OsrAppCore.ts b/src/OsrAppCore.ts
new file mode 100644
index 0000000..5145601
--- /dev/null
+++ b/src/OsrAppCore.ts
@@ -0,0 +1,47 @@
+import { App, TFile } from "obsidian";
+import { SrTFile } from "./SRFile";
+import { OsrCore } from "./OsrCore";
+import { SettingsUtil } from "./settings";
+
+export class OsrAppCore extends OsrCore {
+    private app: App;
+    private _syncLock = false;
+
+    get syncLock(): boolean {
+        return this._syncLock;
+    }
+
+    constructor(app: App) {
+        super();
+        this.app = app;
+    }
+
+    async loadVault(): Promise<void> {
+        if (this._syncLock) {
+            return;
+        }
+        this._syncLock = true;
+
+        try {
+            this.loadInit();
+
+            const notes: TFile[] = this.app.vault.getMarkdownFiles();
+            for (const noteFile of notes) {
+                if (SettingsUtil.isPathInNoteIgnoreFolder(this.settings, noteFile.path)) {
+                    continue;
+                }
+
+                const file: SrTFile = this.createSrTFile(noteFile);
+                await this.processFile(file);
+            }
+
+            this.finaliseLoad();
+        } finally {
+            this._syncLock = false;
+        }
+    }
+
+    createSrTFile(note: TFile): SrTFile {
+        return new SrTFile(this.app.vault, this.app.metadataCache, note);
+    }
+}
diff --git a/src/OsrCore.ts b/src/OsrCore.ts
new file mode 100644
index 0000000..ebdb2ad
--- /dev/null
+++ b/src/OsrCore.ts
@@ -0,0 +1,237 @@
+import { Deck, DeckTreeFilter } from "./Deck";
+import { NoteEaseList } from "./NoteEaseList";
+import { NoteReviewQueue } from "./NoteReviewQueue";
+import { QuestionPostponementList } from "./QuestionPostponementList";
+import { ISRFile } from "./SRFile";
+import { OsrNoteGraph } from "./algorithms/osr/OsrNoteGraph";
+import { Stats } from "./stats";
+import { SRSettings, SettingsUtil } from "./settings";
+import { TopicPath } from "./TopicPath";
+import { SrsAlgorithm } from "./algorithms/base/SrsAlgorithm";
+import { Note } from "./Note";
+import { RepItemScheduleInfo } from "./algorithms/base/RepItemScheduleInfo";
+import { DataStoreAlgorithm } from "./dataStoreAlgorithm/DataStoreAlgorithm";
+import { FlashcardReviewMode } from "./FlashcardReviewSequencer";
+import { DeckTreeStatsCalculator } from "./DeckTreeStatsCalculator";
+import { NoteFileLoader } from "./NoteFileLoader";
+import { ReviewResponse } from "./algorithms/base/RepetitionItem";
+import { IOsrVaultNoteLinkInfoFinder } from "./algorithms/osr/ObsidianVaultNoteLinkInfoFinder";
+import { CardDueDateHistogram, NoteDueDateHistogram } from "./DueDateHistogram";
+import { globalDateProvider } from "./util/DateProvider";
+import { TextDirection } from "./util/TextDirection";
+
+export interface IOsrVaultEvents {
+    dataChanged: () => void;
+}
+
+export class OsrCore {
+    public defaultTextDirection: TextDirection;
+    protected settings: SRSettings;
+    private dataChangedHandler: () => void;
+    protected osrNoteGraph: OsrNoteGraph;
+    private osrNoteLinkInfoFinder: IOsrVaultNoteLinkInfoFinder;
+    private _easeByPath: NoteEaseList;
+    private _questionPostponementList: QuestionPostponementList;
+    private _noteReviewQueue: NoteReviewQueue;
+
+    private fullDeckTree: Deck;
+    private _reviewableDeckTree: Deck = new Deck("root", null);
+    private _remainingDeckTree: Deck;
+    private _cardStats: Stats;
+    private _dueDateFlashcardHistogram: CardDueDateHistogram;
+    private _dueDateNoteHistogram: NoteDueDateHistogram;
+
+    get noteReviewQueue(): NoteReviewQueue {
+        return this._noteReviewQueue;
+    }
+
+    get remainingDeckTree(): Deck {
+        return this._remainingDeckTree;
+    }
+
+    get reviewableDeckTree(): Deck {
+        return this._reviewableDeckTree;
+    }
+
+    get questionPostponementList(): QuestionPostponementList {
+        return this._questionPostponementList;
+    }
+
+    /* c8 ignore start */
+    get dueDateFlashcardHistogram(): CardDueDateHistogram {
+        return this._dueDateFlashcardHistogram;
+    }
+
+    get dueDateNoteHistogram(): NoteDueDateHistogram {
+        return this._dueDateNoteHistogram;
+    }
+
+    get easeByPath(): NoteEaseList {
+        return this._easeByPath;
+    }
+
+    get cardStats(): Stats {
+        return this._cardStats;
+    }
+    /* c8 ignore stop */
+
+    init(
+        questionPostponementList: QuestionPostponementList,
+        osrNoteLinkInfoFinder: IOsrVaultNoteLinkInfoFinder,
+        settings: SRSettings,
+        dataChangedHandler: () => void,
+    ): void {
+        this.settings = settings;
+        this.osrNoteLinkInfoFinder = osrNoteLinkInfoFinder;
+        this.dataChangedHandler = dataChangedHandler;
+        this._noteReviewQueue = new NoteReviewQueue();
+        this._questionPostponementList = questionPostponementList;
+        this._dueDateFlashcardHistogram = new CardDueDateHistogram();
+        this._dueDateNoteHistogram = new NoteDueDateHistogram();
+    }
+
+    protected loadInit(): void {
+        // reset notes stuff
+        this.osrNoteGraph = new OsrNoteGraph(this.osrNoteLinkInfoFinder);
+        this._noteReviewQueue.init();
+
+        // reset flashcards stuff
+        this.fullDeckTree = new Deck("root", null);
+    }
+
+    protected async processFile(noteFile: ISRFile): Promise<void> {
+        const schedule: RepItemScheduleInfo =
+            await DataStoreAlgorithm.getInstance().noteGetSchedule(noteFile);
+        let note: Note = null;
+
+        // Update the graph of links between notes
+        // (Performance note: This only requires accessing Obsidian's metadata cache and not loading the file)
+        this.osrNoteGraph.processLinks(noteFile.path);
+
+        // Does the note contain any tags that are specified as flashcard tags in the settings
+        // (Doing this check first saves us from loading and parsing the note if not necessary)
+        const topicPath: TopicPath = this.findTopicPath(noteFile);
+        if (topicPath.hasPath) {
+            note = await this.loadNote(noteFile, topicPath);
+            note.appendCardsToDeck(this.fullDeckTree);
+        }
+
+        // Give the algorithm a chance to do something with the loaded note
+        // e.g. OSR - calculate the average ease across all the questions within the note
+        // TODO:  should this move to this.loadNote
+        SrsAlgorithm.getInstance().noteOnLoadedNote(noteFile.path, note, schedule?.latestEase);
+
+        const tags = noteFile.getAllTagsFromCache();
+
+        const matchedNoteTags = SettingsUtil.filterForNoteReviewTag(this.settings, tags);
+        if (matchedNoteTags.length == 0) {
+            return;
+        }
+        const noteSchedule: RepItemScheduleInfo =
+            await DataStoreAlgorithm.getInstance().noteGetSchedule(noteFile);
+        this._noteReviewQueue.addNoteToQueue(noteFile, noteSchedule, matchedNoteTags);
+    }
+
+    protected finaliseLoad(): void {
+        this.osrNoteGraph.generatePageRanks();
+
+        // Reviewable cards are all except those with the "edit later" tag
+        this._reviewableDeckTree = DeckTreeFilter.filterForReviewableCards(this.fullDeckTree);
+
+        // sort the deck names
+        this._reviewableDeckTree.sortSubdecksList();
+        this._remainingDeckTree = DeckTreeFilter.filterForRemainingCards(
+            this._questionPostponementList,
+            this._reviewableDeckTree,
+            FlashcardReviewMode.Review,
+        );
+        const calc: DeckTreeStatsCalculator = new DeckTreeStatsCalculator();
+        this._cardStats = calc.calculate(this._reviewableDeckTree);
+
+        // Generate the histogram for the due dates for (1) all the notes (2) all the cards
+        this.calculateDerivedInfo();
+        this._dueDateFlashcardHistogram.calculateFromDeckTree(this._reviewableDeckTree);
+
+        // Tell the interested party that the data has changed
+        if (this.dataChangedHandler) this.dataChangedHandler();
+    }
+
+    async saveNoteReviewResponse(
+        noteFile: ISRFile,
+        response: ReviewResponse,
+        settings: SRSettings,
+    ): Promise<void> {
+        // Get the current schedule for the note (null if new note)
+        const originalNoteSchedule: RepItemScheduleInfo =
+            await DataStoreAlgorithm.getInstance().noteGetSchedule(noteFile);
+
+        // Calculate the new/updated schedule
+        let noteSchedule: RepItemScheduleInfo;
+        if (originalNoteSchedule == null) {
+            noteSchedule = SrsAlgorithm.getInstance().noteCalcNewSchedule(
+                noteFile.path,
+                this.osrNoteGraph,
+                response,
+                this._dueDateNoteHistogram,
+            );
+        } else {
+            noteSchedule = SrsAlgorithm.getInstance().noteCalcUpdatedSchedule(
+                noteFile.path,
+                originalNoteSchedule,
+                response,
+                this._dueDateNoteHistogram,
+            );
+        }
+
+        // Store away the new schedule info
+        await DataStoreAlgorithm.getInstance().noteSetSchedule(noteFile, noteSchedule);
+
+        // Generate the histogram for the due dates for all the notes
+        // (This could be optimized to make the small adjustments to the histogram, but simpler to implement
+        // by recalculating from scratch)
+        this._noteReviewQueue.updateScheduleInfo(noteFile, noteSchedule);
+        this.calculateDerivedInfo();
+
+        // If configured in the settings, bury all cards within the note
+        await this.buryAllCardsInNote(settings, noteFile);
+
+        // Tell the interested party that the data has changed
+        if (this.dataChangedHandler) this.dataChangedHandler();
+    }
+
+    private calculateDerivedInfo(): void {
+        const todayUnix: number = globalDateProvider.today.valueOf();
+        this.noteReviewQueue.calcDueNotesCount(todayUnix);
+        this._dueDateNoteHistogram.calculateFromReviewDecksAndSort(
+            this.noteReviewQueue.reviewDecks,
+            this.osrNoteGraph,
+        );
+    }
+
+    private async buryAllCardsInNote(settings: SRSettings, noteFile: ISRFile): Promise<void> {
+        if (settings.burySiblingCards) {
+            const topicPath: TopicPath = this.findTopicPath(noteFile);
+            const noteX: Note = await this.loadNote(noteFile, topicPath);
+
+            if (noteX.questionList.length > 0) {
+                for (const question of noteX.questionList) {
+                    this._questionPostponementList.add(question);
+                }
+                await this._questionPostponementList.write();
+            }
+        }
+    }
+
+    async loadNote(noteFile: ISRFile, topicPath: TopicPath): Promise<Note> {
+        const loader: NoteFileLoader = new NoteFileLoader(this.settings);
+        const note: Note = await loader.load(noteFile, this.defaultTextDirection, topicPath);
+        if (note.hasChanged) {
+            await note.writeNoteFile(this.settings);
+        }
+        return note;
+    }
+
+    private findTopicPath(note: ISRFile): TopicPath {
+        return TopicPath.getTopicPathOfFile(note, this.settings);
+    }
+}
diff --git a/src/PluginData.ts b/src/PluginData.ts
new file mode 100644
index 0000000..7d81b7c
--- /dev/null
+++ b/src/PluginData.ts
@@ -0,0 +1,18 @@
+import { DEFAULT_SETTINGS, SRSettings } from "./settings";
+
+export interface PluginData {
+    settings: SRSettings;
+    buryDate: string;
+    // hashes of card texts
+    // should work as long as user doesn't modify card's text
+    // which covers most of the cases
+    buryList: string[];
+    historyDeck: string | null;
+}
+
+export const DEFAULT_DATA: PluginData = {
+    settings: DEFAULT_SETTINGS,
+    buryDate: "",
+    buryList: [],
+    historyDeck: null,
+};
diff --git a/src/Question.ts b/src/Question.ts
index 6541ca2..5765601 100644
--- a/src/Question.ts
+++ b/src/Question.ts
@@ -1,11 +1,7 @@
 import { Card } from "./Card";
-import { CardScheduleInfo, NoteCardScheduleParser } from "./CardSchedule";
-import {
-    OBSIDIAN_BLOCK_ID_ENDOFLINE_REGEX,
-    OBSIDIAN_TAG_AT_STARTOFLINE_REGEX,
-    SR_HTML_COMMENT_BEGIN,
-    SR_HTML_COMMENT_END,
-} from "./constants";
+import { OBSIDIAN_BLOCK_ID_ENDOFLINE_REGEX, OBSIDIAN_TAG_AT_STARTOFLINE_REGEX } from "./constants";
+import { DataStore } from "./dataStore/base/DataStore";
+import { DataStoreAlgorithm } from "./dataStoreAlgorithm/DataStoreAlgorithm";
 import { Note } from "./Note";
 import { ParsedQuestionInfo } from "./parser";
 import { SRSettings } from "./settings";
@@ -134,7 +130,7 @@ export class QuestionText {
     }
 
     static splitText(original: string, settings: SRSettings): [TopicPathWithWs, string, string] {
-        const originalWithoutSR = NoteCardScheduleParser.removeCardScheduleInfo(original);
+        const originalWithoutSR = DataStore.getInstance().questionRemoveScheduleInfo(original);
         let actualQuestion: string = originalWithoutSR.trimEnd();
 
         let topicPathWithWs: TopicPathWithWs = null;
@@ -226,29 +222,14 @@ export class Question {
         this.cards.forEach((card) => (card.question = this));
     }
 
-    formatScheduleAsHtmlComment(settings: SRSettings): string {
-        let result: string = SR_HTML_COMMENT_BEGIN;
-
-        // We always want the correct schedule format, so we use this if there is no schedule for a card
-
-        for (let i = 0; i < this.cards.length; i++) {
-            const card: Card = this.cards[i];
-            const schedule: CardScheduleInfo = card.hasSchedule
-                ? card.scheduleInfo
-                : CardScheduleInfo.getDummyScheduleForNewCard(settings);
-            result += schedule.formatSchedule();
-        }
-        result += SR_HTML_COMMENT_END;
-        return result;
-    }
-
     formatForNote(settings: SRSettings): string {
         let result: string = this.questionText.formatTopicAndQuestion();
         const blockId: string = this.questionText.obsidianBlockId;
         const hasSchedule: boolean = this.cards.some((card) => card.hasSchedule);
         if (hasSchedule) {
             result = result.trimEnd();
-            const scheduleHtml = this.formatScheduleAsHtmlComment(settings);
+            const scheduleHtml =
+                DataStoreAlgorithm.getInstance().questionFormatScheduleAsHtmlComment(this);
             if (blockId) {
                 if (this.isCardCommentsOnSameLine(settings))
                     result += ` ${scheduleHtml} ${blockId}`;
@@ -263,7 +244,7 @@ export class Question {
         return result;
     }
 
-    updateQuestionText(noteText: string, settings: SRSettings): string {
+    updateQuestionWithinNoteText(noteText: string, settings: SRSettings): string {
         const originalText: string = this.questionText.original;
 
         // Get the entire text for the question including:
@@ -295,7 +276,7 @@ export class Question {
     async writeQuestion(settings: SRSettings): Promise<void> {
         const fileText: string = await this.note.file.read();
 
-        const newText: string = this.updateQuestionText(fileText, settings);
+        const newText: string = this.updateQuestionWithinNoteText(fileText, settings);
         await this.note.file.write(newText);
         this.hasChanged = false;
     }
diff --git a/src/QuestionPostponementList.ts b/src/QuestionPostponementList.ts
index cc0164c..1c15a58 100644
--- a/src/QuestionPostponementList.ts
+++ b/src/QuestionPostponementList.ts
@@ -1,3 +1,4 @@
+import { PluginData } from "./PluginData";
 import { Question } from "./Question";
 import SRPlugin from "./main";
 import { SRSettings } from "./settings";
@@ -20,6 +21,19 @@ export class QuestionPostponementList implements IQuestionPostponementList {
         this.list = list;
     }
 
+    async clearIfNewDay(data: PluginData): Promise<void> {
+        const now = window.moment(Date.now());
+        const todayDate: string = now.format("YYYY-MM-DD");
+
+        // clear bury list if we've changed dates
+        const isNewDay: boolean = todayDate !== data.buryDate;
+        if (isNewDay) {
+            data.buryDate = todayDate;
+            this.clear();
+            await this.write();
+        }
+    }
+
     clear(): void {
         this.list.splice(0);
     }
diff --git a/src/ReviewDeck.ts b/src/ReviewDeck.ts
deleted file mode 100644
index b35fbb2..0000000
--- a/src/ReviewDeck.ts
+++ /dev/null
@@ -1,56 +0,0 @@
-import { App, FuzzySuggestModal, TFile } from "obsidian";
-
-import { SchedNote } from "src/main";
-import { t } from "src/lang/helpers";
-
-export class ReviewDeck {
-    public deckName: string;
-    public newNotes: TFile[] = [];
-    public scheduledNotes: SchedNote[] = [];
-    public activeFolders: Set<string>;
-    public dueNotesCount = 0;
-
-    constructor(name: string) {
-        this.deckName = name;
-        this.activeFolders = new Set([this.deckName, t("TODAY")]);
-    }
-
-    public sortNotes(pageranks: Record<string, number>): void {
-        // sort new notes by importance
-        this.newNotes = this.newNotes.sort(
-            (a: TFile, b: TFile) => (pageranks[b.path] || 0) - (pageranks[a.path] || 0),
-        );
-
-        // sort scheduled notes by date & within those days, sort them by importance
-        this.scheduledNotes = this.scheduledNotes.sort((a: SchedNote, b: SchedNote) => {
-            const result = a.dueUnix - b.dueUnix;
-            if (result != 0) {
-                return result;
-            }
-            return (pageranks[b.note.path] || 0) - (pageranks[a.note.path] || 0);
-        });
-    }
-}
-
-export class ReviewDeckSelectionModal extends FuzzySuggestModal<string> {
-    public deckKeys: string[] = [];
-    public submitCallback: (deckKey: string) => void;
-
-    constructor(app: App, deckKeys: string[]) {
-        super(app);
-        this.deckKeys = deckKeys;
-    }
-
-    getItems(): string[] {
-        return this.deckKeys;
-    }
-
-    getItemText(item: string): string {
-        return item;
-    }
-
-    onChooseItem(deckKey: string, _: MouseEvent | KeyboardEvent): void {
-        this.close();
-        this.submitCallback(deckKey);
-    }
-}
diff --git a/src/SRFile.ts b/src/SRFile.ts
index bee9b32..3d37a8c 100644
--- a/src/SRFile.ts
+++ b/src/SRFile.ts
@@ -14,6 +14,8 @@ import { parseObsidianFrontmatterTag } from "./util/utils";
 export interface ISRFile {
     get path(): string;
     get basename(): string;
+    get tfile(): TFile;
+    getFrontmatter(): Promise<Map<string, string>>;
     getAllTagsFromCache(): string[];
     getAllTagsFromText(): TagCache[];
     getQuestionContext(cardLine: number): string[];
@@ -46,6 +48,24 @@ export class SrTFile implements ISRFile {
         return this.file.basename;
     }
 
+    get tfile(): TFile {
+        return this.file;
+    }
+
+    async getFrontmatter(): Promise<Map<string, string>> {
+        const fileCachedData = this.metadataCache.getFileCache(this.file) || {};
+
+        const frontmatter: FrontMatterCache = fileCachedData.frontmatter || {};
+        const result: Map<string, string> = new Map<string, string>();
+        // eslint-disable-next-line @typescript-eslint/no-explicit-any
+        for (const [key, value] of Object.entries(frontmatter) as [string, any][]) {
+            const v = Array.isArray(value) && value.length > 0 ? value[0] : value;
+            const vStr: string = v + "";
+            result.set(key, vStr);
+        }
+        return result;
+    }
+
     getAllTagsFromCache(): string[] {
         const fileCachedData = this.metadataCache.getFileCache(this.file) || {};
         const result: string[] = ObsidianGetAllTags(fileCachedData) || [];
diff --git a/src/TopicPath.ts b/src/TopicPath.ts
index 575edfa..919a6f7 100644
--- a/src/TopicPath.ts
+++ b/src/TopicPath.ts
@@ -38,6 +38,36 @@ export class TopicPath {
         return result;
     }
 
+    static getTopicPathOfFile(noteFile: ISRFile, settings: SRSettings): TopicPath {
+        let deckPath: string[] = [];
+        let result: TopicPath = TopicPath.emptyPath;
+
+        if (settings.convertFoldersToDecks) {
+            deckPath = noteFile.path.split("/");
+            deckPath.pop(); // remove filename
+            if (deckPath.length != 0) {
+                result = new TopicPath(deckPath);
+            }
+        } else {
+            const tagList: TopicPath[] = this.getTopicPathsFromTagList(
+                noteFile.getAllTagsFromCache(),
+            );
+
+            outer: for (const tagToReview of this.getTopicPathsFromTagList(
+                settings.flashcardTags,
+            )) {
+                for (const tag of tagList) {
+                    if (tagToReview.isSameOrAncestorOf(tag)) {
+                        result = tag;
+                        break outer;
+                    }
+                }
+            }
+        }
+
+        return result;
+    }
+
     isSameOrAncestorOf(topicPath: TopicPath): boolean {
         if (this.isEmptyPath) return topicPath.isEmptyPath;
         if (this.path.length > topicPath.path.length) return false;
@@ -52,6 +82,14 @@ export class TopicPath {
         return path?.length > 0 ? TopicPath.getTopicPathFromTag(path) : null;
     }
 
+    static getTopicPathsFromTagList(tagList: string[]): TopicPath[] {
+        const result: TopicPath[] = [];
+        for (const tag of tagList) {
+            if (this.isValidTag(tag)) result.push(TopicPath.getTopicPathFromTag(tag));
+        }
+        return result;
+    }
+
     static isValidTag(tag: string): boolean {
         if (tag == null || tag.length == 0) return false;
         if (tag[0] != "#") return false;
diff --git a/src/algorithms/base/ISrsAlgorithm.ts b/src/algorithms/base/ISrsAlgorithm.ts
new file mode 100644
index 0000000..6b9ec21
--- /dev/null
+++ b/src/algorithms/base/ISrsAlgorithm.ts
@@ -0,0 +1,33 @@
+import { RepItemScheduleInfo } from "./RepItemScheduleInfo";
+import { ReviewResponse } from "./RepetitionItem";
+import { Note } from "src/Note";
+import { OsrNoteGraph } from "../osr/OsrNoteGraph";
+import { DueDateHistogram } from "src/DueDateHistogram";
+
+export interface ISrsAlgorithm {
+    noteOnLoadedNote(path: string, note: Note, noteEase: number): void;
+    noteCalcNewSchedule(
+        notePath: string,
+        osrNoteGraph: OsrNoteGraph,
+        response: ReviewResponse,
+        dueDateNoteHistogram: DueDateHistogram,
+    ): RepItemScheduleInfo;
+    noteCalcUpdatedSchedule(
+        notePath: string,
+        noteSchedule: RepItemScheduleInfo,
+        response: ReviewResponse,
+        dueDateNoteHistogram: DueDateHistogram,
+    ): RepItemScheduleInfo;
+
+    cardGetResetSchedule(): RepItemScheduleInfo;
+    cardGetNewSchedule(
+        response: ReviewResponse,
+        notePath: string,
+        dueDateFlashcardHistogram: DueDateHistogram,
+    ): RepItemScheduleInfo;
+    cardCalcUpdatedSchedule(
+        response: ReviewResponse,
+        schedule: RepItemScheduleInfo,
+        dueDateFlashcardHistogram: DueDateHistogram,
+    ): RepItemScheduleInfo;
+}
diff --git a/src/algorithms/base/RepItemScheduleInfo.ts b/src/algorithms/base/RepItemScheduleInfo.ts
new file mode 100644
index 0000000..11ad2b5
--- /dev/null
+++ b/src/algorithms/base/RepItemScheduleInfo.ts
@@ -0,0 +1,29 @@
+import { Moment } from "moment";
+import { TICKS_PER_DAY } from "src/constants";
+import { globalDateProvider } from "src/util/DateProvider";
+import { formatDate_YYYY_MM_DD } from "src/util/utils";
+
+export abstract class RepItemScheduleInfo {
+    dueDate: Moment;
+    latestEase: number;
+    interval: number;
+    delayedBeforeReviewTicks: number;
+
+    get dueDateAsUnix(): number {
+        return this.dueDate.valueOf();
+    }
+
+    isDue(): boolean {
+        return this.dueDate && this.dueDate.isSameOrBefore(globalDateProvider.today);
+    }
+
+    formatDueDate(): string {
+        return formatDate_YYYY_MM_DD(this.dueDate);
+    }
+
+    delayedBeforeReviewDaysInt(): number {
+        return Math.max(0, Math.floor(this.delayedBeforeReviewTicks / TICKS_PER_DAY));
+    }
+
+    abstract formatCardScheduleForHtmlComment(): string;
+}
diff --git a/src/algorithms/base/RepetitionItem.ts b/src/algorithms/base/RepetitionItem.ts
new file mode 100644
index 0000000..b6d2b61
--- /dev/null
+++ b/src/algorithms/base/RepetitionItem.ts
@@ -0,0 +1,33 @@
+import { RepItemScheduleInfo } from "src/algorithms/base/RepItemScheduleInfo";
+import { RepItemStorageInfo } from "src/dataStore/base/RepItemStorageInfo";
+
+export enum ReviewResponse {
+    Easy,
+    Good,
+    Hard,
+    Reset,
+}
+export enum RepetitionPhase {
+    New,
+    Review,
+}
+
+export class RepetitionItem {
+    repetitionPhase: RepetitionPhase;
+
+    scheduleInfo: RepItemScheduleInfo;
+    storageInfo: RepItemStorageInfo;
+
+    // scheduling
+    get hasSchedule(): boolean {
+        return this.scheduleInfo != null;
+    }
+
+    get isNew(): boolean {
+        return !this.hasSchedule;
+    }
+
+    get isDue(): boolean {
+        return this.hasSchedule && this.scheduleInfo.isDue();
+    }
+}
diff --git a/src/algorithms/base/SrsAlgorithm.ts b/src/algorithms/base/SrsAlgorithm.ts
new file mode 100644
index 0000000..4feabea
--- /dev/null
+++ b/src/algorithms/base/SrsAlgorithm.ts
@@ -0,0 +1,12 @@
+import { ISrsAlgorithm } from "./ISrsAlgorithm";
+
+export class SrsAlgorithm {
+    static instance: ISrsAlgorithm;
+
+    public static getInstance(): ISrsAlgorithm {
+        if (!SrsAlgorithm.instance) {
+            throw Error("there is no SrsAlgorithm instance.");
+        }
+        return SrsAlgorithm.instance;
+    }
+}
diff --git a/src/algorithms/osr/NoteScheduling.ts b/src/algorithms/osr/NoteScheduling.ts
new file mode 100644
index 0000000..4fd4e67
--- /dev/null
+++ b/src/algorithms/osr/NoteScheduling.ts
@@ -0,0 +1,74 @@
+import { SRSettings } from "src/settings";
+import { t } from "src/lang/helpers";
+import { ReviewResponse } from "../base/RepetitionItem";
+import { DueDateHistogram } from "src/DueDateHistogram";
+import { TICKS_PER_DAY } from "src/constants";
+
+// Note that if dueDateHistogram is provided, then it is just used to assist with fuzzing.
+// (Unlike earlier versions, it is not updated based on the calculated schedule. The
+// caller needs to do that if needed.
+
+export function osrSchedule(
+    response: ReviewResponse,
+    originalInterval: number,
+    ease: number,
+    delayedBeforeReview: number,
+    settingsObj: SRSettings,
+    dueDateHistogram?: DueDateHistogram,
+): Record<string, number> {
+    const delayedBeforeReviewDays = Math.max(0, Math.floor(delayedBeforeReview / TICKS_PER_DAY));
+    let interval: number = originalInterval;
+
+    if (response === ReviewResponse.Easy) {
+        ease += 20;
+        interval = ((interval + delayedBeforeReviewDays) * ease) / 100;
+        interval *= settingsObj.easyBonus;
+    } else if (response === ReviewResponse.Good) {
+        interval = ((interval + delayedBeforeReviewDays / 2) * ease) / 100;
+    } else if (response === ReviewResponse.Hard) {
+        ease = Math.max(130, ease - 20);
+        interval = Math.max(
+            1,
+            (interval + delayedBeforeReviewDays / 4) * settingsObj.lapsesIntervalChange,
+        );
+    }
+
+    // replaces random fuzz with load balancing over the fuzz interval
+    if (dueDateHistogram !== undefined) {
+        interval = Math.round(interval);
+        // disable fuzzing for small intervals
+        if (interval > 4) {
+            let fuzz = 0;
+            if (interval < 7) fuzz = 1;
+            else if (interval < 30) fuzz = Math.max(2, Math.floor(interval * 0.15));
+            else fuzz = Math.max(4, Math.floor(interval * 0.05));
+
+            const fuzzedInterval = dueDateHistogram.findLeastUsedIntervalOverRange(interval, fuzz);
+            interval = fuzzedInterval;
+        }
+    }
+
+    interval = Math.min(interval, settingsObj.maximumInterval);
+    interval = Math.round(interval * 10) / 10;
+
+    return { interval, ease };
+}
+
+export function textInterval(interval: number, isMobile: boolean): string {
+    if (interval === undefined) {
+        return t("NEW");
+    }
+
+    const m: number = Math.round(interval / 3.04375) / 10,
+        y: number = Math.round(interval / 36.525) / 10;
+
+    if (isMobile) {
+        if (m < 1.0) return t("DAYS_STR_IVL_MOBILE", { interval });
+        else if (y < 1.0) return t("MONTHS_STR_IVL_MOBILE", { interval: m });
+        else return t("YEARS_STR_IVL_MOBILE", { interval: y });
+    } else {
+        if (m < 1.0) return t("DAYS_STR_IVL", { interval });
+        else if (y < 1.0) return t("MONTHS_STR_IVL", { interval: m });
+        else return t("YEARS_STR_IVL", { interval: y });
+    }
+}
diff --git a/src/algorithms/osr/ObsidianVaultNoteLinkInfoFinder.ts b/src/algorithms/osr/ObsidianVaultNoteLinkInfoFinder.ts
new file mode 100644
index 0000000..077df13
--- /dev/null
+++ b/src/algorithms/osr/ObsidianVaultNoteLinkInfoFinder.ts
@@ -0,0 +1,17 @@
+import { MetadataCache } from "obsidian";
+
+export interface IOsrVaultNoteLinkInfoFinder {
+    getResolvedTargetLinksForNotePath(sourcePath: string): Record<string, number>;
+}
+
+export class ObsidianVaultNoteLinkInfoFinder implements IOsrVaultNoteLinkInfoFinder {
+    private metadataCache: MetadataCache;
+
+    constructor(metadataCache: MetadataCache) {
+        this.metadataCache = metadataCache;
+    }
+
+    getResolvedTargetLinksForNotePath(path: string): Record<string, number> {
+        return this.metadataCache.resolvedLinks[path];
+    }
+}
diff --git a/src/algorithms/osr/OsrNoteGraph.ts b/src/algorithms/osr/OsrNoteGraph.ts
new file mode 100644
index 0000000..d541164
--- /dev/null
+++ b/src/algorithms/osr/OsrNoteGraph.ts
@@ -0,0 +1,97 @@
+import * as graph from "pagerank.js";
+import { INoteEaseList } from "src/NoteEaseList";
+import { isSupportedFileType } from "src/util/utils";
+import { IOsrVaultNoteLinkInfoFinder } from "./ObsidianVaultNoteLinkInfoFinder";
+
+export interface LinkStat {
+    sourcePath: string;
+    linkCount: number;
+}
+
+export interface NoteLinkStat {
+    linkTotal: number;
+    linkPGTotal: number;
+    totalLinkCount: number;
+}
+
+export class OsrNoteGraph {
+    private vaultNoteLinkInfoFinder: IOsrVaultNoteLinkInfoFinder;
+    // Key: targetFilename
+    // Value: Map<sourceFilename, linkCount>
+    // This is the number of links from sourceFilename to targetFilename
+    // For simplicity, we just store the filename without the directory or filename extension
+    incomingLinks: Record<string, LinkStat[]> = {};
+    pageranks: Record<string, number> = {};
+
+    constructor(vaultNoteLinkInfoFinder: IOsrVaultNoteLinkInfoFinder) {
+        this.vaultNoteLinkInfoFinder = vaultNoteLinkInfoFinder;
+        this.reset();
+    }
+
+    reset() {
+        this.incomingLinks = {};
+        this.pageranks = {};
+        graph.reset();
+    }
+
+    processLinks(path: string) {
+        if (this.incomingLinks[path] === undefined) {
+            this.incomingLinks[path] = [];
+        }
+
+        const targetLinks =
+            this.vaultNoteLinkInfoFinder.getResolvedTargetLinksForNotePath(path) ||
+            /* c8 ignore next */ {};
+        for (const targetPath in targetLinks) {
+            if (this.incomingLinks[targetPath] === undefined) this.incomingLinks[targetPath] = [];
+
+            // markdown files only
+            if (isSupportedFileType(targetPath)) {
+                const linkCount: number = targetLinks[targetPath];
+                this.incomingLinks[targetPath].push({
+                    sourcePath: path,
+                    linkCount,
+                });
+
+                graph.link(path, targetPath, linkCount);
+            }
+        }
+    }
+
+    calcNoteLinkStat(notePath: string, noteEaseList: INoteEaseList): NoteLinkStat {
+        let linkTotal = 0,
+            linkPGTotal = 0,
+            totalLinkCount = 0;
+
+        for (const statObj of this.incomingLinks[notePath] || /* c8 ignore next */ []) {
+            const ease: number = noteEaseList.getEaseByPath(statObj.sourcePath);
+            if (ease) {
+                linkTotal += statObj.linkCount * this.pageranks[statObj.sourcePath] * ease;
+                linkPGTotal += this.pageranks[statObj.sourcePath] * statObj.linkCount;
+                totalLinkCount += statObj.linkCount;
+            }
+        }
+
+        const outgoingLinks =
+            this.vaultNoteLinkInfoFinder.getResolvedTargetLinksForNotePath(notePath) ||
+            /* c8 ignore next */ {};
+        for (const outgoingLink in outgoingLinks) {
+            const ease: number = noteEaseList.getEaseByPath(outgoingLink);
+            const linkCount: number = outgoingLinks[outgoingLink];
+            const pageRank: number = this.pageranks[outgoingLink];
+            if (ease) {
+                linkTotal += linkCount * pageRank * ease;
+                linkPGTotal += pageRank * linkCount;
+                totalLinkCount += linkCount;
+            }
+        }
+
+        return { linkTotal, linkPGTotal, totalLinkCount };
+    }
+
+    generatePageRanks() {
+        graph.rank(0.85, 0.000001, (node: string, rank: number) => {
+            this.pageranks[node] = rank * 10000;
+        });
+    }
+}
diff --git a/src/algorithms/osr/RepItemScheduleInfo_Osr.ts b/src/algorithms/osr/RepItemScheduleInfo_Osr.ts
new file mode 100644
index 0000000..10e28db
--- /dev/null
+++ b/src/algorithms/osr/RepItemScheduleInfo_Osr.ts
@@ -0,0 +1,61 @@
+import { Moment } from "moment";
+import { RepItemScheduleInfo } from "../base/RepItemScheduleInfo";
+import { SRSettings } from "src/settings";
+import { DateUtil, globalDateProvider } from "src/util/DateProvider";
+
+export class RepItemScheduleInfo_Osr extends RepItemScheduleInfo {
+    // A question can have multiple cards. The schedule info for all sibling cards are formatted together
+    // in a single <!--SR: --> comment, such as:
+    // <!--SR:!2023-09-02,4,270!2023-09-02,5,270!2023-09-02,6,270!2023-09-02,7,270-->
+    //
+    // However, not all sibling cards may have been reviewed. Therefore we need a method of indicating that a particular card
+    // has not been reviewed, and should be considered "new"
+    // This is done by using this magic value for the date
+    public static dummyDueDateForNewCard: string = "2000-01-01";
+
+    constructor(
+        dueDate: Moment,
+        interval: number,
+        latestEase: number,
+        delayedBeforeReviewTicks: number | null = null,
+    ) {
+        super();
+        this.dueDate = dueDate;
+        this.interval = Math.round(interval);
+        this.latestEase = latestEase;
+        this.delayedBeforeReviewTicks = delayedBeforeReviewTicks;
+        if (dueDate && delayedBeforeReviewTicks == null) {
+            this.delayedBeforeReviewTicks = globalDateProvider.today.valueOf() - dueDate.valueOf();
+        }
+    }
+
+    formatCardScheduleForHtmlComment(): string {
+        // We always want the correct schedule format, so we use the dummy due date if there is no schedule for a card
+        const dateStr: string = this.dueDate
+            ? this.formatDueDate()
+            : RepItemScheduleInfo_Osr.dummyDueDateForNewCard;
+        return `!${dateStr},${this.interval},${this.latestEase}`;
+    }
+
+    static get initialInterval(): number {
+        return 1.0;
+    }
+
+    static getDummyScheduleForNewCard(settings: SRSettings): RepItemScheduleInfo_Osr {
+        return RepItemScheduleInfo_Osr.fromDueDateStr(
+            RepItemScheduleInfo_Osr.dummyDueDateForNewCard,
+            RepItemScheduleInfo_Osr.initialInterval,
+            settings.baseEase,
+        );
+    }
+
+    static fromDueDateStr(
+        dueDateStr: string,
+        interval: number,
+        ease: number,
+        delayedBeforeReviewTicks: number | null = null,
+    ) {
+        const dueDate: Moment = DateUtil.dateStrToMoment(dueDateStr);
+        return new RepItemScheduleInfo_Osr(dueDate, interval, ease, delayedBeforeReviewTicks);
+    }
+}
diff --git a/src/algorithms/osr/SrsAlgorithm_Osr.ts b/src/algorithms/osr/SrsAlgorithm_Osr.ts
new file mode 100644
index 0000000..c91882e
--- /dev/null
+++ b/src/algorithms/osr/SrsAlgorithm_Osr.ts
@@ -0,0 +1,210 @@
+import { globalDateProvider } from "src/util/DateProvider";
+import { RepItemScheduleInfo } from "../base/RepItemScheduleInfo";
+import { Moment } from "moment";
+import { RepItemScheduleInfo_Osr } from "./RepItemScheduleInfo_Osr";
+import { ReviewResponse } from "../base/RepetitionItem";
+import { SRSettings } from "src/settings";
+import { INoteEaseList, NoteEaseList } from "src/NoteEaseList";
+import { osrSchedule } from "src/algorithms/osr/NoteScheduling";
+import { ISrsAlgorithm } from "../base/ISrsAlgorithm";
+import { NoteLinkStat, OsrNoteGraph } from "./OsrNoteGraph";
+import { Question } from "src/Question";
+import { Note } from "src/Note";
+import moment from "moment";
+import { DueDateHistogram } from "src/DueDateHistogram";
+
+export class SrsAlgorithm_Osr implements ISrsAlgorithm {
+    private settings: SRSettings;
+    private noteEaseList: INoteEaseList;
+
+    constructor(settings: SRSettings) {
+        this.settings = settings;
+        this.noteEaseList = new NoteEaseList(settings);
+    }
+
+    static get initialInterval(): number {
+        return 1.0;
+    }
+
+    noteCalcNewSchedule(
+        notePath: string,
+        osrNoteGraph: OsrNoteGraph,
+        response: ReviewResponse,
+        dueDateNoteHistogram: DueDateHistogram,
+    ): RepItemScheduleInfo {
+        const noteLinkStat: NoteLinkStat = osrNoteGraph.calcNoteLinkStat(
+            notePath,
+            this.noteEaseList,
+        );
+
+        const linkContribution: number =
+            this.settings.maxLinkFactor *
+            Math.min(1.0, Math.log(noteLinkStat.totalLinkCount + 0.5) / Math.log(64));
+        let ease: number =
+            (1.0 - linkContribution) * this.settings.baseEase +
+            (noteLinkStat.totalLinkCount > 0
+                ? (linkContribution * noteLinkStat.linkTotal) / noteLinkStat.linkPGTotal
+                : linkContribution * this.settings.baseEase);
+
+        // add note's average flashcard ease if available
+        /* c8 ignore next 3 */
+        if (this.noteEaseList.hasEaseForPath(notePath)) {
+            ease = (ease + this.noteEaseList.getEaseByPath(notePath)) / 2;
+        }
+
+        // Don't know the due date until we know the calculated interval
+        const dueDate: Moment = null;
+        const interval: number = SrsAlgorithm_Osr.initialInterval;
+        ease = Math.round(ease);
+        const temp: RepItemScheduleInfo_Osr = new RepItemScheduleInfo_Osr(dueDate, interval, ease);
+
+        const result: RepItemScheduleInfo_Osr = this.calcSchedule(
+            temp,
+            response,
+            dueDateNoteHistogram,
+        );
+
+        // Calculate the due date now that we know the interval
+        result.dueDate = moment(globalDateProvider.today.add(result.interval, "d"));
+        return result;
+    }
+
+    noteOnLoadedNote(path: string, note: Note, noteEase: number): void {
+        let flashcardsInNoteAvgEase: number = null;
+        if (note) {
+            flashcardsInNoteAvgEase = SrsAlgorithm_Osr.calculateFlashcardAvgEase(
+                note.questionList,
+                this.settings,
+            );
+        }
+        let ease: number = null;
+        if (flashcardsInNoteAvgEase && noteEase) {
+            ease = (flashcardsInNoteAvgEase + noteEase) / 2;
+        } else {
+            ease = flashcardsInNoteAvgEase ? flashcardsInNoteAvgEase : noteEase;
+        }
+
+        if (ease) {
+            this.noteEaseList.setEaseForPath(path, ease);
+        }
+    }
+
+    static calculateFlashcardAvgEase(questionList: Question[], settings: SRSettings): number {
+        let totalEase: number = 0;
+        let scheduledCount: number = 0;
+
+        questionList.forEach((question) => {
+            question.cards
+                .filter((card) => card.hasSchedule)
+                .forEach((card) => {
+                    totalEase += card.scheduleInfo.latestEase;
+                    scheduledCount++;
+                });
+        });
+
+        let result: number = 0;
+        if (scheduledCount > 0) {
+            const flashcardsInNoteAvgEase: number = totalEase / scheduledCount;
+            const flashcardContribution: number = Math.min(
+                1.0,
+                Math.log(scheduledCount + 0.5) / Math.log(64),
+            );
+            result =
+                flashcardsInNoteAvgEase * flashcardContribution +
+                settings.baseEase * (1.0 - flashcardContribution);
+        }
+        return result;
+    }
+
+    noteCalcUpdatedSchedule(
+        notePath: string,
+        noteSchedule: RepItemScheduleInfo,
+        response: ReviewResponse,
+        dueDateNoteHistogram: DueDateHistogram,
+    ): RepItemScheduleInfo {
+        const noteScheduleOsr: RepItemScheduleInfo_Osr = noteSchedule as RepItemScheduleInfo_Osr;
+        const temp: RepItemScheduleInfo_Osr = this.calcSchedule(
+            noteScheduleOsr,
+            response,
+            dueDateNoteHistogram,
+        );
+        const interval: number = temp.interval;
+        const ease: number = temp.latestEase;
+
+        const dueDate: Moment = moment(globalDateProvider.today.add(interval, "d"));
+        this.noteEaseList.setEaseForPath(notePath, ease);
+        return new RepItemScheduleInfo_Osr(dueDate, interval, ease);
+    }
+
+    private calcSchedule(
+        schedule: RepItemScheduleInfo_Osr,
+        response: ReviewResponse,
+        dueDateHistogram: DueDateHistogram,
+    ): RepItemScheduleInfo_Osr {
+        const temp: Record<string, number> = osrSchedule(
+            response,
+            schedule.interval,
+            schedule.latestEase,
+            schedule.delayedBeforeReviewTicks,
+            this.settings,
+            dueDateHistogram,
+        );
+
+        return new RepItemScheduleInfo_Osr(globalDateProvider.today, temp.interval, temp.ease);
+    }
+
+    cardGetResetSchedule(): RepItemScheduleInfo {
+        const interval = SrsAlgorithm_Osr.initialInterval;
+        const ease = this.settings.baseEase;
+        const dueDate = globalDateProvider.today.add(interval, "d");
+        return new RepItemScheduleInfo_Osr(dueDate, interval, ease);
+    }
+
+    cardGetNewSchedule(
+        response: ReviewResponse,
+        notePath: string,
+        dueDateFlashcardHistogram: DueDateHistogram,
+    ): RepItemScheduleInfo {
+        let initial_ease: number = this.settings.baseEase;
+        /* c8 ignore next 3 */
+        if (this.noteEaseList.hasEaseForPath(notePath)) {
+            initial_ease = Math.round(this.noteEaseList.getEaseByPath(notePath));
+        }
+        const delayBeforeReview = 0;
+
+        const schedObj: Record<string, number> = osrSchedule(
+            response,
+            SrsAlgorithm_Osr.initialInterval,
+            initial_ease,
+            delayBeforeReview,
+            this.settings,
+            dueDateFlashcardHistogram,
+        );
+
+        const interval = schedObj.interval;
+        const ease = schedObj.ease;
+        const dueDate = globalDateProvider.today.add(interval, "d");
+        return new RepItemScheduleInfo_Osr(dueDate, interval, ease, delayBeforeReview);
+    }
+
+    cardCalcUpdatedSchedule(
+        response: ReviewResponse,
+        cardSchedule: RepItemScheduleInfo,
+        dueDateFlashcardHistogram: DueDateHistogram,
+    ): RepItemScheduleInfo {
+        const cardScheduleOsr: RepItemScheduleInfo_Osr = cardSchedule as RepItemScheduleInfo_Osr;
+        const schedObj: Record<string, number> = osrSchedule(
+            response,
+            cardScheduleOsr.interval,
+            cardSchedule.latestEase,
+            cardSchedule.delayedBeforeReviewTicks,
+            this.settings,
+            dueDateFlashcardHistogram,
+        );
+        const interval = schedObj.interval;
+        const ease = schedObj.ease;
+        const dueDate = globalDateProvider.today.add(interval, "d");
+        const delayBeforeReview = 0;
+        return new RepItemScheduleInfo_Osr(dueDate, interval, ease, delayBeforeReview);
+    }
+}
diff --git a/src/dataStore/base/DataStore.ts b/src/dataStore/base/DataStore.ts
new file mode 100644
index 0000000..6aa4e47
--- /dev/null
+++ b/src/dataStore/base/DataStore.ts
@@ -0,0 +1,24 @@
+import { RepItemScheduleInfo } from "src/algorithms/base/RepItemScheduleInfo";
+import { RepItemStorageInfo } from "./RepItemStorageInfo";
+import { Question } from "src/Question";
+
+export interface IDataStore {
+    questionCreateSchedule(
+        originalQuestionText: string,
+        storageInfo: RepItemStorageInfo,
+    ): RepItemScheduleInfo[];
+    questionRemoveScheduleInfo(questionText: string): string;
+    questionWrite(question: Question): Promise<void>;
+    questionWriteSchedule(question: Question): Promise<void>;
+}
+
+export class DataStore {
+    static instance: IDataStore;
+
+    public static getInstance(): IDataStore {
+        if (!DataStore.instance) {
+            throw Error("there is no DataStore instance.");
+        }
+        return DataStore.instance;
+    }
+}
diff --git a/src/dataStore/base/RepItemStorageInfo.ts b/src/dataStore/base/RepItemStorageInfo.ts
new file mode 100644
index 0000000..b8e53b4
--- /dev/null
+++ b/src/dataStore/base/RepItemStorageInfo.ts
@@ -0,0 +1,1 @@
+export class RepItemStorageInfo {}
diff --git a/src/dataStore/storeInNote/DataStore_StoreInNote.ts b/src/dataStore/storeInNote/DataStore_StoreInNote.ts
new file mode 100644
index 0000000..1c19415
--- /dev/null
+++ b/src/dataStore/storeInNote/DataStore_StoreInNote.ts
@@ -0,0 +1,72 @@
+import { RepItemScheduleInfo } from "src/algorithms/base/RepItemScheduleInfo";
+import { RepItemStorageInfo } from "../base/RepItemStorageInfo";
+import { LEGACY_SCHEDULING_EXTRACTOR, MULTI_SCHEDULING_EXTRACTOR } from "src/constants";
+import { Moment } from "moment";
+import { DateUtil, globalDateProvider } from "src/util/DateProvider";
+import { RepItemScheduleInfo_Osr } from "src/algorithms/osr/RepItemScheduleInfo_Osr";
+import { formatDate_YYYY_MM_DD } from "src/util/utils";
+import { Question } from "src/Question";
+import { SRSettings } from "src/settings";
+import { IDataStore } from "../base/DataStore";
+import { App } from "obsidian";
+import { NoteEaseList } from "src/NoteEaseList";
+
+export class DataStore_StoreInNote implements IDataStore {
+    private settings: SRSettings;
+    app: App;
+    easeByPath: NoteEaseList;
+
+    constructor(settings: SRSettings) {
+        this.settings = settings;
+    }
+
+    questionCreateSchedule(
+        originalQuestionText: string,
+        _: RepItemStorageInfo,
+    ): RepItemScheduleInfo[] {
+        let scheduling: RegExpMatchArray[] = [
+            ...originalQuestionText.matchAll(MULTI_SCHEDULING_EXTRACTOR),
+        ];
+        if (scheduling.length === 0)
+            scheduling = [...originalQuestionText.matchAll(LEGACY_SCHEDULING_EXTRACTOR)];
+
+        const result: RepItemScheduleInfo[] = [];
+        for (let i = 0; i < scheduling.length; i++) {
+            const match: RegExpMatchArray = scheduling[i];
+            const dueDateStr = match[1];
+            const interval = parseInt(match[2]);
+            const ease = parseInt(match[3]);
+            const dueDate: Moment = DateUtil.dateStrToMoment(dueDateStr);
+            let info: RepItemScheduleInfo;
+            if (
+                dueDate == null ||
+                formatDate_YYYY_MM_DD(dueDate) == RepItemScheduleInfo_Osr.dummyDueDateForNewCard
+            ) {
+                info = null;
+            } else {
+                const delayBeforeReviewTicks: number =
+                    dueDate.valueOf() - globalDateProvider.today.valueOf();
+
+                info = new RepItemScheduleInfo_Osr(dueDate, interval, ease, delayBeforeReviewTicks);
+            }
+            result.push(info);
+        }
+        return result;
+    }
+
+    questionRemoveScheduleInfo(questionText: string): string {
+        return questionText.replace(/<!--SR:.+-->/gm, "");
+    }
+
+    async questionWriteSchedule(question: Question): Promise<void> {
+        await this.questionWrite(question);
+    }
+
+    async questionWrite(question: Question): Promise<void> {
+        const fileText: string = await question.note.file.read();
+
+        const newText: string = question.updateQuestionWithinNoteText(fileText, this.settings);
+        await question.note.file.write(newText);
+        question.hasChanged = false;
+    }
+}
diff --git a/src/dataStoreAlgorithm/DataStoreAlgorithm.ts b/src/dataStoreAlgorithm/DataStoreAlgorithm.ts
new file mode 100644
index 0000000..49a8435
--- /dev/null
+++ b/src/dataStoreAlgorithm/DataStoreAlgorithm.ts
@@ -0,0 +1,12 @@
+import { IDataStoreAlgorithm } from "./IDataStoreAlgorithm";
+
+export class DataStoreAlgorithm {
+    static instance: IDataStoreAlgorithm;
+
+    public static getInstance(): IDataStoreAlgorithm {
+        if (!DataStoreAlgorithm.instance) {
+            throw Error("there is no DataStoreAlgorithm instance.");
+        }
+        return DataStoreAlgorithm.instance;
+    }
+}
diff --git a/src/dataStoreAlgorithm/DataStoreInNote_AlgorithmOsr.ts b/src/dataStoreAlgorithm/DataStoreInNote_AlgorithmOsr.ts
new file mode 100644
index 0000000..ed5784b
--- /dev/null
+++ b/src/dataStoreAlgorithm/DataStoreInNote_AlgorithmOsr.ts
@@ -0,0 +1,108 @@
+import { ISRFile } from "src/SRFile";
+import { IDataStoreAlgorithm } from "./IDataStoreAlgorithm";
+import { RepItemScheduleInfo } from "src/algorithms/base/RepItemScheduleInfo";
+import { RepItemScheduleInfo_Osr } from "src/algorithms/osr/RepItemScheduleInfo_Osr";
+import { Moment } from "moment";
+import moment from "moment";
+import {
+    ALLOWED_DATE_FORMATS,
+    SCHEDULING_INFO_REGEX,
+    SR_HTML_COMMENT_BEGIN,
+    SR_HTML_COMMENT_END,
+    YAML_FRONT_MATTER_REGEX,
+} from "src/constants";
+import { formatDate_YYYY_MM_DD } from "src/util/utils";
+import { Question } from "src/Question";
+import { Card } from "src/Card";
+import { SRSettings } from "src/settings";
+
+//
+// Algorithm: The original OSR algorithm
+//      (RZ: Perhaps not the original algorithm, but the only one available in 2023/early 2024)
+//
+// Data Store: With data stored in the note's markdown file
+//
+export class DataStoreInNote_AlgorithmOsr implements IDataStoreAlgorithm {
+    private settings: SRSettings;
+
+    constructor(settings: SRSettings) {
+        this.settings = settings;
+    }
+
+    async noteGetSchedule(note: ISRFile): Promise<RepItemScheduleInfo> {
+        let result: RepItemScheduleInfo = null;
+        const frontmatter: Map<string, string> = await note.getFrontmatter();
+
+        if (
+            frontmatter &&
+            frontmatter.has("sr-due") &&
+            frontmatter.has("sr-interval") &&
+            frontmatter.has("sr-ease")
+        ) {
+            const dueDate: Moment = moment(frontmatter.get("sr-due"), ALLOWED_DATE_FORMATS);
+            const interval: number = parseFloat(frontmatter.get("sr-interval"));
+            const ease: number = parseFloat(frontmatter.get("sr-ease"));
+            result = new RepItemScheduleInfo_Osr(dueDate, interval, ease);
+        }
+        return result;
+    }
+
+    async noteSetSchedule(note: ISRFile, repItemScheduleInfo: RepItemScheduleInfo): Promise<void> {
+        let fileText: string = await note.read();
+
+        const schedInfo: RepItemScheduleInfo_Osr = repItemScheduleInfo as RepItemScheduleInfo_Osr;
+        const dueString: string = formatDate_YYYY_MM_DD(schedInfo.dueDate);
+        const interval: number = schedInfo.interval;
+        const ease: number = schedInfo.latestEase;
+
+        // check if scheduling info exists
+        if (SCHEDULING_INFO_REGEX.test(fileText)) {
+            const schedulingInfo = SCHEDULING_INFO_REGEX.exec(fileText);
+            fileText = fileText.replace(
+                SCHEDULING_INFO_REGEX,
+                `---\n${schedulingInfo[1]}sr-due: ${dueString}\n` +
+                    `sr-interval: ${interval}\nsr-ease: ${ease}\n` +
+                    `${schedulingInfo[5]}---`,
+            );
+        } else if (YAML_FRONT_MATTER_REGEX.test(fileText)) {
+            // new note with existing YAML front matter
+            const existingYaml = YAML_FRONT_MATTER_REGEX.exec(fileText);
+            fileText = fileText.replace(
+                YAML_FRONT_MATTER_REGEX,
+                `---\n${existingYaml[1]}sr-due: ${dueString}\n` +
+                    `sr-interval: ${interval}\nsr-ease: ${ease}\n---`,
+            );
+        } else {
+            fileText =
+                `---\nsr-due: ${dueString}\nsr-interval: ${interval}\n` +
+                `sr-ease: ${ease}\n---\n\n${fileText}`;
+        }
+
+        await note.write(fileText);
+    }
+
+    questionFormatScheduleAsHtmlComment(question: Question): string {
+        let result: string = SR_HTML_COMMENT_BEGIN;
+
+        for (let i = 0; i < question.cards.length; i++) {
+            const card: Card = question.cards[i];
+            result += this.formatCardSchedule(card);
+        }
+        result += SR_HTML_COMMENT_END;
+        return result;
+    }
+
+    formatCardSchedule(card: Card) {
+        let result: string;
+        if (card.hasSchedule) {
+            const schedule = card.scheduleInfo as RepItemScheduleInfo_Osr;
+            const dateStr = schedule.dueDate
+                ? formatDate_YYYY_MM_DD(schedule.dueDate)
+                : RepItemScheduleInfo_Osr.dummyDueDateForNewCard;
+            result = `!${dateStr},${schedule.interval},${schedule.latestEase}`;
+        } else {
+            result = `!${RepItemScheduleInfo_Osr.dummyDueDateForNewCard},${RepItemScheduleInfo_Osr.initialInterval},${this.settings.baseEase}`;
+        }
+        return result;
+    }
+}
diff --git a/src/dataStoreAlgorithm/IDataStoreAlgorithm.ts b/src/dataStoreAlgorithm/IDataStoreAlgorithm.ts
new file mode 100644
index 0000000..d4971e1
--- /dev/null
+++ b/src/dataStoreAlgorithm/IDataStoreAlgorithm.ts
@@ -0,0 +1,9 @@
+import { Question } from "src/Question";
+import { ISRFile } from "src/SRFile";
+import { RepItemScheduleInfo } from "src/algorithms/base/RepItemScheduleInfo";
+
+export interface IDataStoreAlgorithm {
+    noteGetSchedule(note: ISRFile): Promise<RepItemScheduleInfo>;
+    noteSetSchedule(note: ISRFile, scheduleInfo: RepItemScheduleInfo): Promise<void>;
+    questionFormatScheduleAsHtmlComment(question: Question): string;
+}
diff --git a/src/generateParser.ts b/src/generateParser.ts
index 369c05f..9ce11d7 100644
--- a/src/generateParser.ts
+++ b/src/generateParser.ts
@@ -7,38 +7,41 @@ let parser: Parser | null = null;
 let oldOptions: ParserOptions;
 
 export function generateParser(options: ParserOptions): Parser {
-
     let grammar: string | null = null;
 
     // If the parser did not already exist or if the parser options changed since last the last
     // parser was generated, we generate a new parser. Otherwise, we skip the block to save
     // some execution time.
-    if(parser === null || !areParserOptionsEqual(options,oldOptions)) {
-
+    if (parser === null || !areParserOptionsEqual(options, oldOptions)) {
         /* GENERATE A NEW PARSER */
 
         oldOptions = copyParserOptions(options);
 
         grammar = generateGrammar(options);
 
-        if(debugParser) {
+        if (debugParser) {
             const t0 = Date.now();
             parser = generate(grammar);
-            const t1 = Date.now();    
+            const t1 = Date.now();
             console.log("New parser generated in " + (t1 - t0) + " milliseconds.");
         } else {
             parser = generate(grammar);
         }
     } else {
-        if(debugParser) console.log("Parser already existed. No need to generate a new parser.");
+        if (debugParser) console.log("Parser already existed. No need to generate a new parser.");
     }
 
-    if(debugParser) {
-        if(grammar === null) {
+    if (debugParser) {
+        if (grammar === null) {
             grammar = generateGrammar(options);
         }
-        console.log("The parsers grammar is provided below. You can test it with https://peggyjs.org/online.html.");
-        console.log({info: "Copy the grammar by right-clicking on the property grammar and copying it as a string. Then, paste it in https://peggyjs.org/online.html.", grammar: grammar});
+        console.log(
+            "The parsers grammar is provided below. You can test it with https://peggyjs.org/online.html.",
+        );
+        console.log({
+            info: "Copy the grammar by right-clicking on the property grammar and copying it as a string. Then, paste it in https://peggyjs.org/online.html.",
+            grammar: grammar,
+        });
     }
 
     return parser;
@@ -47,13 +50,13 @@ export function generateParser(options: ParserOptions): Parser {
 function generateGrammar(options: ParserOptions): string {
     const close_rules_list: string[] = [];
 
-        if(options.convertHighlightsToClozes) close_rules_list.push("close_equal");
-        if(options.convertBoldTextToClozes) close_rules_list.push("close_star");
-        if(options.convertCurlyBracketsToClozes) close_rules_list.push("close_bracket");
-        
-        const close_rules = close_rules_list.join(" / ");
+    if (options.convertHighlightsToClozes) close_rules_list.push("close_equal");
+    if (options.convertBoldTextToClozes) close_rules_list.push("close_star");
+    if (options.convertCurlyBracketsToClozes) close_rules_list.push("close_bracket");
 
-        return `{
+    const close_rules = close_rules_list.join(" / ");
+
+    return `{
   // The fallback case is important if we want to test the rules with https://peggyjs.org/online.html
   const CardTypeFallBack = {
     SingleLineBasic: 0,
@@ -265,4 +268,4 @@ optional_whitespaces
 
 whitespace_char = ([ \\f\\t\\v\\u0020\\u00a0\\u1680\\u2000-\\u200a\\u2028\\u2029\\u202f\\u205f\\u3000\\ufeff])
 `;
-}
\ No newline at end of file
+}
diff --git a/src/gui/FlashcardReviewView.tsx b/src/gui/FlashcardReviewView.tsx
index 18dd25e..f081d3b 100644
--- a/src/gui/FlashcardReviewView.tsx
+++ b/src/gui/FlashcardReviewView.tsx
@@ -3,7 +3,6 @@ import { App, Notice, Platform, setIcon } from "obsidian";
 
 import type SRPlugin from "src/main";
 import { SRSettings } from "src/settings";
-import { textInterval, ReviewResponse } from "src/scheduling";
 import { t } from "src/lang/helpers";
 import { Card } from "../Card";
 import { CardListType, Deck } from "../Deck";
@@ -14,7 +13,9 @@ import {
 } from "src/FlashcardReviewSequencer";
 import { Note } from "src/Note";
 import { RenderMarkdownWrapper } from "src/util/RenderMarkdownWrapper";
-import { CardScheduleInfo } from "src/CardSchedule";
+import { ReviewResponse } from "src/algorithms/base/RepetitionItem";
+import { textInterval } from "src/algorithms/osr/NoteScheduling";
+import { RepItemScheduleInfo } from "src/algorithms/base/RepItemScheduleInfo";
 import { FlashcardModalMode } from "./FlashcardModal";
 
 export class FlashcardReviewView {
@@ -255,7 +256,7 @@ export class FlashcardReviewView {
     private _displayCurrentCardInfoNotice() {
         const schedule = this._currentCard.scheduleInfo;
 
-        const currentEaseStr = t("CURRENT_EASE_HELP_TEXT") + (schedule?.ease ?? t("NEW"));
+        const currentEaseStr = t("CURRENT_EASE_HELP_TEXT") + (schedule?.latestEase ?? t("NEW"));
         const currentIntervalStr =
             t("CURRENT_INTERVAL_HELP_TEXT") + textInterval(schedule?.interval, false);
         const generatedFromStr = t("CARD_GENERATED_FROM", {
@@ -502,7 +503,7 @@ export class FlashcardReviewView {
         buttonName: string,
         reviewResponse: ReviewResponse,
     ) {
-        const schedule: CardScheduleInfo = this.reviewSequencer.determineCardSchedule(
+        const schedule: RepItemScheduleInfo = this.reviewSequencer.determineCardSchedule(
             reviewResponse,
             this._currentCard,
         );
diff --git a/src/gui/OsrSidebar.ts b/src/gui/OsrSidebar.ts
new file mode 100644
index 0000000..97a78a1
--- /dev/null
+++ b/src/gui/OsrSidebar.ts
@@ -0,0 +1,75 @@
+import { App, Plugin, WorkspaceLeaf } from "obsidian";
+import { SRSettings } from "src/settings";
+import { REVIEW_QUEUE_VIEW_TYPE, ReviewQueueListView } from "./ReviewQueueListView";
+import { NextNoteReviewHandler } from "src/NextNoteReviewHandler";
+
+export class OsrSidebar {
+    private plugin: Plugin;
+    private settings: SRSettings;
+    private nextNoteReviewHandler: NextNoteReviewHandler;
+    private reviewQueueListView: ReviewQueueListView;
+
+    private get app(): App {
+        return this.plugin.app;
+    }
+
+    constructor(
+        plugin: Plugin,
+        settings: SRSettings,
+        nextNoteReviewHandler: NextNoteReviewHandler,
+    ) {
+        this.plugin = plugin;
+        this.settings = settings;
+        this.nextNoteReviewHandler = nextNoteReviewHandler;
+    }
+
+    redraw(): void {
+        if (this.getActiveLeaf(REVIEW_QUEUE_VIEW_TYPE)) this.reviewQueueListView.redraw();
+    }
+
+    private getActiveLeaf(type: string): WorkspaceLeaf | null {
+        const leaves = this.app.workspace.getLeavesOfType(type);
+        if (leaves.length == 0) {
+            return null;
+        }
+
+        return leaves[0];
+    }
+
+    async init(): Promise<void> {
+        this.plugin.registerView(REVIEW_QUEUE_VIEW_TYPE, (leaf) => {
+            return (this.reviewQueueListView = new ReviewQueueListView(
+                leaf,
+                this.app,
+                this.nextNoteReviewHandler,
+                this.settings,
+            ));
+        });
+
+        if (
+            this.settings.enableNoteReviewPaneOnStartup &&
+            this.getActiveLeaf(REVIEW_QUEUE_VIEW_TYPE) == null
+        ) {
+            await this.activateReviewQueueViewPanel();
+        }
+    }
+
+    private async activateReviewQueueViewPanel(): Promise<void> {
+        await this.app.workspace.getRightLeaf(false).setViewState({
+            type: REVIEW_QUEUE_VIEW_TYPE,
+            active: true,
+        });
+    }
+
+    async openReviewQueueView(): Promise<void> {
+        let reviewQueueLeaf = this.getActiveLeaf(REVIEW_QUEUE_VIEW_TYPE);
+        if (reviewQueueLeaf == null) {
+            await this.activateReviewQueueViewPanel();
+            reviewQueueLeaf = this.getActiveLeaf(REVIEW_QUEUE_VIEW_TYPE);
+        }
+
+        if (reviewQueueLeaf !== null) {
+            this.app.workspace.revealLeaf(reviewQueueLeaf);
+        }
+    }
+}
diff --git a/src/gui/ReviewDeckSelectionModal.ts b/src/gui/ReviewDeckSelectionModal.ts
new file mode 100644
index 0000000..86d88f6
--- /dev/null
+++ b/src/gui/ReviewDeckSelectionModal.ts
@@ -0,0 +1,24 @@
+import { App, FuzzySuggestModal } from "obsidian";
+
+export class ReviewDeckSelectionModal extends FuzzySuggestModal<string> {
+    public deckKeys: string[] = [];
+    public submitCallback: (deckKey: string) => void;
+
+    constructor(app: App, deckKeys: string[]) {
+        super(app);
+        this.deckKeys = deckKeys;
+    }
+
+    getItems(): string[] {
+        return this.deckKeys;
+    }
+
+    getItemText(item: string): string {
+        return item;
+    }
+
+    onChooseItem(deckKey: string, _: MouseEvent | KeyboardEvent): void {
+        this.close();
+        this.submitCallback(deckKey);
+    }
+}
diff --git a/src/gui/Sidebar.tsx b/src/gui/ReviewQueueListView.ts
similarity index 86%
rename from src/gui/Sidebar.tsx
rename to src/gui/ReviewQueueListView.ts
index 47f89f7..7c9f09f 100644
--- a/src/gui/Sidebar.tsx
+++ b/src/gui/ReviewQueueListView.ts
@@ -1,19 +1,30 @@
-import { ItemView, WorkspaceLeaf, Menu, TFile } from "obsidian";
-
-import type SRPlugin from "src/main";
-import { COLLAPSE_ICON } from "src/constants";
-import { ReviewDeck } from "src/ReviewDeck";
+import { ItemView, WorkspaceLeaf, Menu, TFile, App } from "obsidian";
+import { COLLAPSE_ICON, TICKS_PER_DAY } from "src/constants";
+import { NoteReviewDeck } from "src/NoteReviewDeck";
 import { t } from "src/lang/helpers";
+import { NoteReviewQueue } from "src/NoteReviewQueue";
+import { SRSettings } from "src/settings";
+import { NextNoteReviewHandler } from "src/NextNoteReviewHandler";
 
 export const REVIEW_QUEUE_VIEW_TYPE = "review-queue-list-view";
 
 export class ReviewQueueListView extends ItemView {
-    private plugin: SRPlugin;
-
-    constructor(leaf: WorkspaceLeaf, plugin: SRPlugin) {
+    private get noteReviewQueue(): NoteReviewQueue {
+        return this.nextNoteReviewHandler.noteReviewQueue;
+    }
+    private settings: SRSettings;
+    private nextNoteReviewHandler: NextNoteReviewHandler;
+
+    constructor(
+        leaf: WorkspaceLeaf,
+        app: App,
+        nextNoteReviewHandler: NextNoteReviewHandler,
+        settings: SRSettings,
+    ) {
         super(leaf);
 
-        this.plugin = plugin;
+        this.nextNoteReviewHandler = nextNoteReviewHandler;
+        this.settings = settings;
         this.registerEvent(this.app.workspace.on("file-open", () => this.redraw()));
         this.registerEvent(this.app.vault.on("rename", () => this.redraw()));
     }
@@ -46,9 +57,7 @@ export class ReviewQueueListView extends ItemView {
         const rootEl: HTMLElement = createDiv("tree-item nav-folder mod-root");
         const childrenEl: HTMLElement = rootEl.createDiv("tree-item-children nav-folder-children");
 
-        for (const deckKey in this.plugin.reviewDecks) {
-            const deck: ReviewDeck = this.plugin.reviewDecks[deckKey];
-
+        for (const [deckKey, deck] of this.noteReviewQueue.reviewDecks) {
             const deckCollapsed = !deck.activeFolders.has(deck.deckName);
 
             const deckFolderEl: HTMLElement = this.createRightPaneFolder(
@@ -78,11 +87,10 @@ export class ReviewQueueListView extends ItemView {
                     }
                     this.createRightPaneFile(
                         newNotesFolderEl,
-                        newFile,
+                        newFile.tfile,
                         fileIsOpen,
                         !deck.activeFolders.has(t("NEW")),
                         deck,
-                        this.plugin,
                     );
                 }
             }
@@ -92,11 +100,11 @@ export class ReviewQueueListView extends ItemView {
                 let currUnix = -1;
                 let schedFolderEl: HTMLElement | null = null,
                     folderTitle = "";
-                const maxDaysToRender: number = this.plugin.data.settings.maxNDaysNotesReviewQueue;
+                const maxDaysToRender: number = this.settings.maxNDaysNotesReviewQueue;
 
                 for (const sNote of deck.scheduledNotes) {
                     if (sNote.dueUnix != currUnix) {
-                        const nDays: number = Math.ceil((sNote.dueUnix - now) / (24 * 3600 * 1000));
+                        const nDays: number = Math.ceil((sNote.dueUnix - now) / TICKS_PER_DAY);
 
                         if (nDays > maxDaysToRender) {
                             break;
@@ -132,11 +140,10 @@ export class ReviewQueueListView extends ItemView {
 
                     this.createRightPaneFile(
                         schedFolderEl,
-                        sNote.note,
+                        sNote.note.tfile,
                         fileIsOpen,
                         !deck.activeFolders.has(folderTitle),
                         deck,
-                        this.plugin,
                     );
                 }
             }
@@ -152,7 +159,7 @@ export class ReviewQueueListView extends ItemView {
         folderTitle: string,
         collapsed: boolean,
         hidden: boolean,
-        deck: ReviewDeck,
+        deck: NoteReviewDeck,
     ): HTMLElement {
         const folderEl: HTMLDivElement = parentEl.createDiv("tree-item nav-folder");
         const folderTitleEl: HTMLDivElement = folderEl.createDiv("tree-item-self nav-folder-title");
@@ -191,8 +198,7 @@ export class ReviewQueueListView extends ItemView {
         file: TFile,
         fileElActive: boolean,
         hidden: boolean,
-        deck: ReviewDeck,
-        plugin: SRPlugin,
+        deck: NoteReviewDeck,
     ): void {
         const navFileEl: HTMLElement = folderEl
             .getElementsByClassName("tree-item-children nav-folder-children")[0]
@@ -211,8 +217,7 @@ export class ReviewQueueListView extends ItemView {
             "click",
             async (event: MouseEvent) => {
                 event.preventDefault();
-                plugin.lastSelectedReviewDeck = deck.deckName;
-                await this.app.workspace.getLeaf().openFile(file);
+                await this.nextNoteReviewHandler.openNote(deck.deckName, file);
                 return false;
             },
             false,
diff --git a/src/gui/StatsModal.tsx b/src/gui/StatsModal.tsx
index 0607258..df733b8 100644
--- a/src/gui/StatsModal.tsx
+++ b/src/gui/StatsModal.tsx
@@ -16,12 +16,12 @@ import {
     ArcElement,
 } from "chart.js";
 
-import type SRPlugin from "src/main";
 import { getKeysPreserveType, getTypedObjectEntries } from "src/util/utils";
-import { textInterval } from "src/scheduling";
+import { textInterval } from "src/algorithms/osr/NoteScheduling";
 import { t } from "src/lang/helpers";
 import { Stats } from "../stats";
 import { CardListType } from "src/Deck";
+import { OsrCore } from "src/OsrCore";
 
 Chart.register(
     BarElement,
@@ -37,12 +37,12 @@ Chart.register(
 );
 
 export class StatsModal extends Modal {
-    private plugin: SRPlugin;
+    private osrCore: OsrCore;
 
-    constructor(app: App, plugin: SRPlugin) {
+    constructor(app: App, osrCore: OsrCore) {
         super(app);
 
-        this.plugin = plugin;
+        this.osrCore = osrCore;
 
         this.titleEl.setText(`${t("STATS_TITLE")} `);
         this.titleEl.addClass("sr-centered");
@@ -70,7 +70,7 @@ export class StatsModal extends Modal {
         contentEl.style.textAlign = "center";
 
         // Add forecast
-        const cardStats: Stats = this.plugin.cardStats;
+        const cardStats: Stats = this.osrCore.cardStats;
         let maxN: number = cardStats.delayedDays.getMaxValue();
         for (let dueOffset = 0; dueOffset <= maxN; dueOffset++) {
             cardStats.delayedDays.clearCountIfMissing(dueOffset);
@@ -170,7 +170,7 @@ export class StatsModal extends Modal {
         );
 
         // Add card types
-        const totalCardsCount: number = this.plugin.deckTree.getDistinctCardCount(
+        const totalCardsCount: number = this.osrCore.reviewableDeckTree.getDistinctCardCount(
             CardListType.All,
             true,
         );
diff --git a/src/main.ts b/src/main.ts
index 31e8d41..42dcfd0 100644
--- a/src/main.ts
+++ b/src/main.ts
@@ -1,26 +1,16 @@
+import { Notice, Plugin, TAbstractFile, TFile } from "obsidian";
 import {
-    Notice,
-    Plugin,
-    TAbstractFile,
-    TFile,
-    getAllTags,
-    FrontMatterCache,
-    WorkspaceLeaf,
-} from "obsidian";
-import * as graph from "pagerank.js";
-
-import { SRSettingTab, SRSettings, DEFAULT_SETTINGS, upgradeSettings } from "src/settings";
-import { FlashcardModal } from "src/gui/FlashcardModal";
-import { StatsModal } from "src/gui/StatsModal";
-import { ReviewQueueListView, REVIEW_QUEUE_VIEW_TYPE } from "src/gui/Sidebar";
-import { ReviewResponse, schedule } from "src/scheduling";
-import { YAML_FRONT_MATTER_REGEX, SCHEDULING_INFO_REGEX } from "src/constants";
-import { ReviewDeck, ReviewDeckSelectionModal } from "src/ReviewDeck";
+    SRSettingTab,
+    SRSettings,
+    DEFAULT_SETTINGS,
+    upgradeSettings,
+    SettingsUtil,
+} from "src/settings";
+import { REVIEW_QUEUE_VIEW_TYPE } from "src/gui/ReviewQueueListView";
 import { t } from "src/lang/helpers";
 import { appIcon } from "src/icons/appicon";
 import { TopicPath } from "./TopicPath";
 import { CardListType, Deck, DeckTreeFilter } from "./Deck";
-import { Stats } from "./stats";
 import {
     FlashcardReviewMode,
     FlashcardReviewSequencer as FlashcardReviewSequencer,
@@ -33,78 +23,73 @@ import {
     IIteratorOrder,
     DeckOrder,
 } from "./DeckTreeIterator";
-import { CardScheduleCalculator } from "./CardSchedule";
 import { Note } from "./Note";
 import { NoteFileLoader } from "./NoteFileLoader";
 import { ISRFile, SrTFile as SrTFile } from "./SRFile";
-import { NoteEaseCalculator } from "./NoteEaseCalculator";
-import { DeckTreeStatsCalculator } from "./DeckTreeStatsCalculator";
-import { NoteEaseList } from "./NoteEaseList";
 import { QuestionPostponementList } from "./QuestionPostponementList";
 import { TextDirection } from "./util/TextDirection";
 import { convertToStringOrEmpty } from "./util/utils";
-import { isEqualOrSubPath } from "./util/utils";
-import { generateParser } from "./generateParser";
+import { ReviewResponse } from "./algorithms/base/RepetitionItem";
+import { SrsAlgorithm } from "./algorithms/base/SrsAlgorithm";
+import { DataStore } from "./dataStore/base/DataStore";
+import { DataStoreAlgorithm } from "./dataStoreAlgorithm/DataStoreAlgorithm";
+import { DataStoreInNote_AlgorithmOsr } from "./dataStoreAlgorithm/DataStoreInNote_AlgorithmOsr";
+import { DataStore_StoreInNote } from "./dataStore/storeInNote/DataStore_StoreInNote";
+import { SrsAlgorithm_Osr } from "./algorithms/osr/SrsAlgorithm_Osr";
+import { OsrAppCore } from "./OsrAppCore";
+import { DEFAULT_DATA, PluginData } from "./PluginData";
+import { NextNoteReviewHandler } from "./NextNoteReviewHandler";
+import { OsrSidebar } from "./gui/OsrSidebar";
+import { ObsidianVaultNoteLinkInfoFinder } from "./algorithms/osr/ObsidianVaultNoteLinkInfoFinder";
+import { StatsModal } from "./gui/StatsModal";
+import { FlashcardModal } from "./gui/FlashcardModal";
 import { setDebugParser } from "./parser";
-
-interface PluginData {
-    settings: SRSettings;
-    buryDate: string;
-    // hashes of card texts
-    // should work as long as user doesn't modify card's text
-    // which covers most of the cases
-    buryList: string[];
-    historyDeck: string | null;
-}
-
-const DEFAULT_DATA: PluginData = {
-    settings: DEFAULT_SETTINGS,
-    buryDate: "",
-    buryList: [],
-    historyDeck: null,
-};
-
-export interface SchedNote {
-    note: TFile;
-    dueUnix: number;
-}
-
-export interface LinkStat {
-    sourcePath: string;
-    linkCount: number;
-}
+import { generateParser } from "./generateParser";
 
 export default class SRPlugin extends Plugin {
     private statusBar: HTMLElement;
-    private reviewQueueView: ReviewQueueListView;
     public data: PluginData;
-    public syncLock = false;
-
-    public reviewDecks: { [deckKey: string]: ReviewDeck } = {};
-    public lastSelectedReviewDeck: string;
-
-    public easeByPath: NoteEaseList;
-    private questionPostponementList: QuestionPostponementList;
-    private incomingLinks: Record<string, LinkStat[]> = {};
-    private pageranks: Record<string, number> = {};
-    private dueNotesCount = 0;
-    public dueDatesNotes: Record<number, number> = {}; // Record<# of days in future, due count>
-
-    public deckTree: Deck = new Deck("root", null);
-    private remainingDeckTree: Deck;
-    public cardStats: Stats;
+    private osrAppCore: OsrAppCore;
+    private osrSidebar: OsrSidebar;
+    private nextNoteReviewHandler: NextNoteReviewHandler;
 
     private debouncedGenerateParserTimeout: number | null = null;
 
     async onload(): Promise<void> {
+        console.log("onload: Branch: feat-878-support-multiple-sched, Date: 2024-07-24");
         await this.loadPluginData();
-        this.easeByPath = new NoteEaseList(this.data.settings);
-        this.questionPostponementList = new QuestionPostponementList(
+
+        this.initLogicClasses();
+
+        this.initGuiItems();
+    }
+
+    private initLogicClasses() {
+        const questionPostponementList: QuestionPostponementList = new QuestionPostponementList(
             this,
             this.data.settings,
             this.data.buryList,
         );
 
+        const osrNoteLinkInfoFinder: ObsidianVaultNoteLinkInfoFinder =
+            new ObsidianVaultNoteLinkInfoFinder(this.app.metadataCache);
+
+        this.osrAppCore = new OsrAppCore(this.app);
+        this.osrAppCore.init(
+            questionPostponementList,
+            osrNoteLinkInfoFinder,
+            this.data.settings,
+            this.onOsrVaultDataChanged.bind(this),
+        );
+    }
+
+    private initGuiItems() {
+        this.nextNoteReviewHandler = new NextNoteReviewHandler(
+            this.app,
+            this.data.settings,
+            this.app.workspace,
+            this.osrAppCore.noteReviewQueue,
+        );
         appIcon();
 
         this.statusBar = this.addStatusBarItem();
@@ -112,23 +97,41 @@ export default class SRPlugin extends Plugin {
         this.statusBar.setAttribute("aria-label", t("OPEN_NOTE_FOR_REVIEW"));
         this.statusBar.setAttribute("aria-label-position", "top");
         this.statusBar.addEventListener("click", async () => {
-            if (!this.syncLock) {
+            if (!this.osrAppCore.syncLock) {
                 await this.sync();
-                this.reviewNextNoteModal();
+                this.nextNoteReviewHandler.reviewNextNoteModal();
             }
         });
 
         this.addRibbonIcon("SpacedRepIcon", t("REVIEW_CARDS"), async () => {
-            if (!this.syncLock) {
+            if (!this.osrAppCore.syncLock) {
                 await this.sync();
                 this.openFlashcardModal(
-                    this.deckTree,
-                    this.remainingDeckTree,
+                    this.osrAppCore.reviewableDeckTree,
+                    this.osrAppCore.remainingDeckTree,
                     FlashcardReviewMode.Review,
                 );
             }
         });
 
+        this.addFileMenuItems();
+
+        this.addPluginCommands();
+
+        this.addSettingTab(new SRSettingTab(this.app, this));
+
+        this.osrSidebar = new OsrSidebar(this, this.data.settings, this.nextNoteReviewHandler);
+        this.app.workspace.onLayoutReady(async () => {
+            await this.osrSidebar.init();
+            setTimeout(async () => {
+                if (!this.osrAppCore.syncLock) {
+                    await this.sync();
+                }
+            }, 2000);
+        });
+    }
+
+    private addFileMenuItems() {
         if (!this.data.settings.disableFileMenuReviewOptions) {
             this.registerEvent(
                 this.app.workspace.on("file-menu", (menu, fileish: TAbstractFile) => {
@@ -141,7 +144,7 @@ export default class SRPlugin extends Plugin {
                             )
                                 .setIcon("SpacedRepIcon")
                                 .onClick(() => {
-                                    this.saveReviewResponse(fileish, ReviewResponse.Easy);
+                                    this.saveNoteReviewResponse(fileish, ReviewResponse.Easy);
                                 });
                         });
 
@@ -153,7 +156,7 @@ export default class SRPlugin extends Plugin {
                             )
                                 .setIcon("SpacedRepIcon")
                                 .onClick(() => {
-                                    this.saveReviewResponse(fileish, ReviewResponse.Good);
+                                    this.saveNoteReviewResponse(fileish, ReviewResponse.Good);
                                 });
                         });
 
@@ -165,21 +168,23 @@ export default class SRPlugin extends Plugin {
                             )
                                 .setIcon("SpacedRepIcon")
                                 .onClick(() => {
-                                    this.saveReviewResponse(fileish, ReviewResponse.Hard);
+                                    this.saveNoteReviewResponse(fileish, ReviewResponse.Hard);
                                 });
                         });
                     }
                 }),
             );
         }
+    }
 
+    private addPluginCommands() {
         this.addCommand({
             id: "srs-note-review-open-note",
             name: t("OPEN_NOTE_FOR_REVIEW"),
             callback: async () => {
-                if (!this.syncLock) {
+                if (!this.osrAppCore.syncLock) {
                     await this.sync();
-                    this.reviewNextNoteModal();
+                    this.nextNoteReviewHandler.reviewNextNoteModal();
                 }
             },
         });
@@ -192,7 +197,7 @@ export default class SRPlugin extends Plugin {
             callback: () => {
                 const openFile: TFile | null = this.app.workspace.getActiveFile();
                 if (openFile && openFile.extension === "md") {
-                    this.saveReviewResponse(openFile, ReviewResponse.Easy);
+                    this.saveNoteReviewResponse(openFile, ReviewResponse.Easy);
                 }
             },
         });
@@ -205,7 +210,7 @@ export default class SRPlugin extends Plugin {
             callback: () => {
                 const openFile: TFile | null = this.app.workspace.getActiveFile();
                 if (openFile && openFile.extension === "md") {
-                    this.saveReviewResponse(openFile, ReviewResponse.Good);
+                    this.saveNoteReviewResponse(openFile, ReviewResponse.Good);
                 }
             },
         });
@@ -218,7 +223,7 @@ export default class SRPlugin extends Plugin {
             callback: () => {
                 const openFile: TFile | null = this.app.workspace.getActiveFile();
                 if (openFile && openFile.extension === "md") {
-                    this.saveReviewResponse(openFile, ReviewResponse.Hard);
+                    this.saveNoteReviewResponse(openFile, ReviewResponse.Hard);
                 }
             },
         });
@@ -227,11 +232,11 @@ export default class SRPlugin extends Plugin {
             id: "srs-review-flashcards",
             name: t("REVIEW_ALL_CARDS"),
             callback: async () => {
-                if (!this.syncLock) {
+                if (!this.osrAppCore.syncLock) {
                     await this.sync();
                     this.openFlashcardModal(
-                        this.deckTree,
-                        this.remainingDeckTree,
+                        this.osrAppCore.reviewableDeckTree,
+                        this.osrAppCore.remainingDeckTree,
                         FlashcardReviewMode.Review,
                     );
                 }
@@ -243,7 +248,11 @@ export default class SRPlugin extends Plugin {
             name: t("CRAM_ALL_CARDS"),
             callback: async () => {
                 await this.sync();
-                this.openFlashcardModal(this.deckTree, this.deckTree, FlashcardReviewMode.Cram);
+                this.openFlashcardModal(
+                    this.osrAppCore.reviewableDeckTree,
+                    this.osrAppCore.reviewableDeckTree,
+                    FlashcardReviewMode.Cram,
+                );
             },
         });
 
@@ -273,9 +282,9 @@ export default class SRPlugin extends Plugin {
             id: "srs-view-stats",
             name: t("VIEW_STATS"),
             callback: async () => {
-                if (!this.syncLock) {
+                if (!this.osrAppCore.syncLock) {
                     await this.sync();
-                    new StatsModal(this.app, this).open();
+                    new StatsModal(this.app, this.osrAppCore).open();
                 }
             },
         });
@@ -284,20 +293,9 @@ export default class SRPlugin extends Plugin {
             id: "srs-open-review-queue-view",
             name: t("OPEN_REVIEW_QUEUE_VIEW"),
             callback: async () => {
-                await this.openReviewQueueView();
+                await this.osrSidebar.openReviewQueueView();
             },
         });
-
-        this.addSettingTab(new SRSettingTab(this.app, this));
-
-        this.app.workspace.onLayoutReady(async () => {
-            await this.initReviewQueueView();
-            setTimeout(async () => {
-                if (!this.syncLock) {
-                    await this.sync();
-                }
-            }, 2000);
-        });
     }
 
     onunload(): void {
@@ -313,7 +311,7 @@ export default class SRPlugin extends Plugin {
         const deckTree = new Deck("root", null);
         note.appendCardsToDeck(deckTree);
         const remainingDeckTree = DeckTreeFilter.filterForRemainingCards(
-            this.questionPostponementList,
+            this.osrAppCore.questionPostponementList,
             deckTree,
             reviewMode,
         );
@@ -325,24 +323,21 @@ export default class SRPlugin extends Plugin {
         remainingDeckTree: Deck,
         reviewMode: FlashcardReviewMode,
     ): void {
-        const deckIterator = SRPlugin.createDeckTreeIterator(this.data.settings, remainingDeckTree);
-        const cardScheduleCalculator = new CardScheduleCalculator(
-            this.data.settings,
-            this.easeByPath,
-        );
+        const deckIterator = SRPlugin.createDeckTreeIterator(this.data.settings);
         const reviewSequencer: IFlashcardReviewSequencer = new FlashcardReviewSequencer(
             reviewMode,
             deckIterator,
             this.data.settings,
-            cardScheduleCalculator,
-            this.questionPostponementList,
+            SrsAlgorithm.getInstance(),
+            this.osrAppCore.questionPostponementList,
+            this.osrAppCore.dueDateFlashcardHistogram,
         );
 
         reviewSequencer.setDeckTree(fullDeckTree, remainingDeckTree);
         new FlashcardModal(this.app, this, this.data.settings, reviewSequencer, reviewMode).open();
     }
 
-    private static createDeckTreeIterator(settings: SRSettings, baseDeck: Deck): IDeckTreeIterator {
+    private static createDeckTreeIterator(settings: SRSettings): IDeckTreeIterator {
         let cardOrder: CardOrder = CardOrder[settings.flashcardCardOrder as keyof typeof CardOrder];
         if (cardOrder === undefined) cardOrder = CardOrder.DueFirstSequential;
         let deckOrder: DeckOrder = DeckOrder[settings.flashcardDeckOrder as keyof typeof DeckOrder];
@@ -352,156 +347,22 @@ export default class SRPlugin extends Plugin {
             deckOrder,
             cardOrder,
         };
-        return new DeckTreeIterator(iteratorOrder, baseDeck);
+        return new DeckTreeIterator(iteratorOrder, null);
     }
 
     async sync(): Promise<void> {
-        if (this.syncLock) {
+        if (this.osrAppCore.syncLock) {
             return;
         }
-        this.syncLock = true;
-
-        // reset notes stuff
-        graph.reset();
-        this.easeByPath = new NoteEaseList(this.data.settings);
-        this.incomingLinks = {};
-        this.pageranks = {};
-        this.reviewDecks = {};
-
-        // reset flashcards stuff
-        const fullDeckTree = new Deck("root", null);
 
         const now = window.moment(Date.now());
-        const todayDate: string = now.format("YYYY-MM-DD");
-        // clear bury list if we've changed dates
-        if (todayDate !== this.data.buryDate) {
-            this.data.buryDate = todayDate;
-            this.questionPostponementList.clear();
-
-            // The following isn't needed for plug-in functionality; but can aid during debugging
-            await this.savePluginData();
-        }
-
-        const notes: TFile[] = this.app.vault.getMarkdownFiles();
-        for (const noteFile of notes) {
-            if (
-                this.data.settings.noteFoldersToIgnore.some((folder) =>
-                    isEqualOrSubPath(noteFile.path, folder),
-                )
-            ) {
-                continue;
-            }
-
-            if (this.incomingLinks[noteFile.path] === undefined) {
-                this.incomingLinks[noteFile.path] = [];
-            }
-
-            const links = this.app.metadataCache.resolvedLinks[noteFile.path] || {};
-            for (const targetPath in links) {
-                if (this.incomingLinks[targetPath] === undefined)
-                    this.incomingLinks[targetPath] = [];
-
-                // markdown files only
-                if (targetPath.split(".").pop().toLowerCase() === "md") {
-                    this.incomingLinks[targetPath].push({
-                        sourcePath: noteFile.path,
-                        linkCount: links[targetPath],
-                    });
-
-                    graph.link(noteFile.path, targetPath, links[targetPath]);
-                }
-            }
-
-            const note: Note = await this.loadNote(noteFile);
-            if (note.questionList.length > 0) {
-                const flashcardsInNoteAvgEase: number = NoteEaseCalculator.Calculate(
-                    note,
-                    this.data.settings,
-                );
-                note.appendCardsToDeck(fullDeckTree);
-
-                if (flashcardsInNoteAvgEase > 0) {
-                    this.easeByPath.setEaseForPath(note.filePath, flashcardsInNoteAvgEase);
-                }
-            }
-            const fileCachedData = this.app.metadataCache.getFileCache(noteFile) || {};
-
-            const frontmatter: FrontMatterCache | Record<string, unknown> =
-                fileCachedData.frontmatter || {};
-            const tags = getAllTags(fileCachedData) || [];
-
-            let shouldIgnore = true;
-            const matchedNoteTags = [];
-
-            for (const tagToReview of this.data.settings.tagsToReview) {
-                if (tags.some((tag) => tag === tagToReview || tag.startsWith(tagToReview + "/"))) {
-                    if (!Object.prototype.hasOwnProperty.call(this.reviewDecks, tagToReview)) {
-                        this.reviewDecks[tagToReview] = new ReviewDeck(tagToReview);
-                    }
-                    matchedNoteTags.push(tagToReview);
-                    shouldIgnore = false;
-                    break;
-                }
-            }
-            if (shouldIgnore) {
-                continue;
-            }
-
-            // file has no scheduling information
-            if (
-                !(
-                    Object.prototype.hasOwnProperty.call(frontmatter, "sr-due") &&
-                    Object.prototype.hasOwnProperty.call(frontmatter, "sr-interval") &&
-                    Object.prototype.hasOwnProperty.call(frontmatter, "sr-ease")
-                )
-            ) {
-                for (const matchedNoteTag of matchedNoteTags) {
-                    this.reviewDecks[matchedNoteTag].newNotes.push(noteFile);
-                }
-                continue;
-            }
-
-            const dueUnix: number = window
-                .moment(frontmatter["sr-due"], ["YYYY-MM-DD", "DD-MM-YYYY", "ddd MMM DD YYYY"])
-                .valueOf();
-
-            let ease: number;
-            if (this.easeByPath.hasEaseForPath(noteFile.path)) {
-                ease = (this.easeByPath.getEaseByPath(noteFile.path) + frontmatter["sr-ease"]) / 2;
-            } else {
-                ease = frontmatter["sr-ease"];
-            }
-            this.easeByPath.setEaseForPath(noteFile.path, ease);
-
-            // schedule the note
-            for (const matchedNoteTag of matchedNoteTags) {
-                this.reviewDecks[matchedNoteTag].scheduledNotes.push({ note: noteFile, dueUnix });
-            }
-        }
-
-        graph.rank(0.85, 0.000001, (node: string, rank: number) => {
-            this.pageranks[node] = rank * 10000;
-        });
+        this.osrAppCore.defaultTextDirection = this.getObsidianRtlSetting();
 
-        // Reviewable cards are all except those with the "edit later" tag
-        this.deckTree = DeckTreeFilter.filterForReviewableCards(fullDeckTree);
-
-        // sort the deck names
-        this.deckTree.sortSubdecksList();
-        this.remainingDeckTree = DeckTreeFilter.filterForRemainingCards(
-            this.questionPostponementList,
-            this.deckTree,
-            FlashcardReviewMode.Review,
-        );
-        const calc: DeckTreeStatsCalculator = new DeckTreeStatsCalculator();
-        this.cardStats = calc.calculate(this.deckTree);
-
-        if (this.data.settings.showDebugMessages) {
-            console.log(`SR: ${t("EASES")}`, this.easeByPath.dict);
-            console.log(`SR: ${t("DECKS")}`, this.deckTree);
-        }
+        await this.osrAppCore.loadVault();
 
         if (this.data.settings.showDebugMessages) {
+            // TODO: console.log(`SR: ${t("EASES")}`, this.easeByPath.dict);
+            console.log(`SR: ${t("DECKS")}`, this.osrAppCore.reviewableDeckTree);
             console.log(
                 "SR: " +
                     t("SYNC_TIME_TAKEN", {
@@ -509,48 +370,19 @@ export default class SRPlugin extends Plugin {
                     }),
             );
         }
-
-        this.updateAndSortDueNotes();
-
-        this.syncLock = false;
     }
 
-    private updateAndSortDueNotes() {
-        this.dueNotesCount = 0;
-        this.dueDatesNotes = {};
-
-        const now = window.moment(Date.now());
-        Object.values(this.reviewDecks).forEach((reviewDeck: ReviewDeck) => {
-            reviewDeck.dueNotesCount = 0;
-            reviewDeck.scheduledNotes.forEach((scheduledNote: SchedNote) => {
-                if (scheduledNote.dueUnix <= now.valueOf()) {
-                    reviewDeck.dueNotesCount++;
-                    this.dueNotesCount++;
-                }
-
-                const nDays: number = Math.ceil(
-                    (scheduledNote.dueUnix - now.valueOf()) / (24 * 3600 * 1000),
-                );
-                if (!Object.prototype.hasOwnProperty.call(this.dueDatesNotes, nDays)) {
-                    this.dueDatesNotes[nDays] = 0;
-                }
-                this.dueDatesNotes[nDays]++;
-            });
-
-            reviewDeck.sortNotes(this.pageranks);
-        });
-
+    private onOsrVaultDataChanged() {
         this.statusBar.setText(
             t("STATUS_BAR", {
-                dueNotesCount: this.dueNotesCount,
-                dueFlashcardsCount: this.remainingDeckTree.getDistinctCardCount(
+                dueNotesCount: this.osrAppCore.noteReviewQueue.dueNotesCount,
+                dueFlashcardsCount: this.osrAppCore.remainingDeckTree.getCardCount(
                     CardListType.All,
                     true,
                 ),
             }),
         );
-
-        if (this.getActiveLeaf(REVIEW_QUEUE_VIEW_TYPE)) this.reviewQueueView.redraw();
+        this.osrSidebar.redraw();
     }
 
     async loadNote(noteFile: TFile): Promise<Note> {
@@ -579,225 +411,28 @@ export default class SRPlugin extends Plugin {
         return convertToStringOrEmpty(v) == "true" ? TextDirection.Rtl : TextDirection.Ltr;
     }
 
-    async saveReviewResponse(note: TFile, response: ReviewResponse): Promise<void> {
-        const fileCachedData = this.app.metadataCache.getFileCache(note) || {};
-        // eslint-disable-next-line @typescript-eslint/no-explicit-any
-        const frontmatter: FrontMatterCache | Record<string, unknown> =
-            fileCachedData.frontmatter || {};
-
-        const tags = getAllTags(fileCachedData) || [];
-        if (
-            this.data.settings.noteFoldersToIgnore.some((folder) =>
-                isEqualOrSubPath(note.path, folder),
-            )
-        ) {
+    async saveNoteReviewResponse(note: TFile, response: ReviewResponse): Promise<void> {
+        const noteSrTFile: ISRFile = this.createSrTFile(note);
+
+        if (SettingsUtil.isPathInNoteIgnoreFolder(this.data.settings, note.path)) {
             new Notice(t("NOTE_IN_IGNORED_FOLDER"));
             return;
         }
 
-        let shouldIgnore = true;
-        for (const tag of tags) {
-            if (
-                this.data.settings.tagsToReview.some(
-                    (tagToReview) => tag === tagToReview || tag.startsWith(tagToReview + "/"),
-                )
-            ) {
-                shouldIgnore = false;
-                break;
-            }
-        }
-
-        if (shouldIgnore) {
+        const tags = noteSrTFile.getAllTagsFromCache();
+        if (!SettingsUtil.isAnyTagANoteReviewTag(this.data.settings, tags)) {
             new Notice(t("PLEASE_TAG_NOTE"));
             return;
         }
 
-        let fileText: string = await this.app.vault.read(note);
-        let ease: number, interval: number, delayBeforeReview: number;
-        const now: number = Date.now();
-        // new note
-        if (
-            !(
-                Object.prototype.hasOwnProperty.call(frontmatter, "sr-due") &&
-                Object.prototype.hasOwnProperty.call(frontmatter, "sr-interval") &&
-                Object.prototype.hasOwnProperty.call(frontmatter, "sr-ease")
-            )
-        ) {
-            let linkTotal = 0,
-                linkPGTotal = 0,
-                totalLinkCount = 0;
-
-            for (const statObj of this.incomingLinks[note.path] || []) {
-                const ease: number = this.easeByPath.getEaseByPath(statObj.sourcePath);
-                if (ease) {
-                    linkTotal += statObj.linkCount * this.pageranks[statObj.sourcePath] * ease;
-                    linkPGTotal += this.pageranks[statObj.sourcePath] * statObj.linkCount;
-                    totalLinkCount += statObj.linkCount;
-                }
-            }
-
-            const outgoingLinks = this.app.metadataCache.resolvedLinks[note.path] || {};
-            for (const linkedFilePath in outgoingLinks) {
-                const ease: number = this.easeByPath.getEaseByPath(linkedFilePath);
-                if (ease) {
-                    linkTotal +=
-                        outgoingLinks[linkedFilePath] * this.pageranks[linkedFilePath] * ease;
-                    linkPGTotal += this.pageranks[linkedFilePath] * outgoingLinks[linkedFilePath];
-                    totalLinkCount += outgoingLinks[linkedFilePath];
-                }
-            }
-
-            const linkContribution: number =
-                this.data.settings.maxLinkFactor *
-                Math.min(1.0, Math.log(totalLinkCount + 0.5) / Math.log(64));
-            ease =
-                (1.0 - linkContribution) * this.data.settings.baseEase +
-                (totalLinkCount > 0
-                    ? (linkContribution * linkTotal) / linkPGTotal
-                    : linkContribution * this.data.settings.baseEase);
-            // add note's average flashcard ease if available
-            if (this.easeByPath.hasEaseForPath(note.path)) {
-                ease = (ease + this.easeByPath.getEaseByPath(note.path)) / 2;
-            }
-            ease = Math.round(ease);
-            interval = 1.0;
-            delayBeforeReview = 0;
-        } else {
-            interval = frontmatter["sr-interval"];
-            ease = frontmatter["sr-ease"];
-            delayBeforeReview =
-                now -
-                window
-                    .moment(frontmatter["sr-due"], ["YYYY-MM-DD", "DD-MM-YYYY", "ddd MMM DD YYYY"])
-                    .valueOf();
-        }
-
-        const schedObj: Record<string, number> = schedule(
-            response,
-            interval,
-            ease,
-            delayBeforeReview,
-            this.data.settings,
-            this.dueDatesNotes,
-        );
-        interval = schedObj.interval;
-        ease = schedObj.ease;
-
-        const due = window.moment(now + interval * 24 * 3600 * 1000);
-        const dueString: string = due.format("YYYY-MM-DD");
-
-        // check if scheduling info exists
-        if (SCHEDULING_INFO_REGEX.test(fileText)) {
-            const schedulingInfo = SCHEDULING_INFO_REGEX.exec(fileText);
-            fileText = fileText.replace(
-                SCHEDULING_INFO_REGEX,
-                `---\n${schedulingInfo[1]}sr-due: ${dueString}\n` +
-                    `sr-interval: ${interval}\nsr-ease: ${ease}\n` +
-                    `${schedulingInfo[5]}---`,
-            );
-        } else if (YAML_FRONT_MATTER_REGEX.test(fileText)) {
-            // new note with existing YAML front matter
-            const existingYaml = YAML_FRONT_MATTER_REGEX.exec(fileText);
-            fileText = fileText.replace(
-                YAML_FRONT_MATTER_REGEX,
-                `---\n${existingYaml[1]}sr-due: ${dueString}\n` +
-                    `sr-interval: ${interval}\nsr-ease: ${ease}\n---`,
-            );
-        } else {
-            fileText =
-                `---\nsr-due: ${dueString}\nsr-interval: ${interval}\n` +
-                `sr-ease: ${ease}\n---\n\n${fileText}`;
-        }
-
-        if (this.data.settings.burySiblingCards) {
-            const noteX: Note = await this.loadNote(note);
-            for (const question of noteX.questionList) {
-                this.data.buryList.push(question.questionText.textHash);
-            }
-            await this.savePluginData();
-        }
-        await this.app.vault.modify(note, fileText);
-
-        // Update note's properties to update our due notes.
-        this.easeByPath.setEaseForPath(note.path, ease);
-
-        Object.values(this.reviewDecks).forEach((reviewDeck: ReviewDeck) => {
-            let wasDueInDeck = false;
-            for (const scheduledNote of reviewDeck.scheduledNotes) {
-                if (scheduledNote.note.path === note.path) {
-                    scheduledNote.dueUnix = due.valueOf();
-                    wasDueInDeck = true;
-                    break;
-                }
-            }
-
-            // It was a new note, remove it from the new notes and schedule it.
-            if (!wasDueInDeck) {
-                reviewDeck.newNotes.splice(
-                    reviewDeck.newNotes.findIndex((newNote: TFile) => newNote.path === note.path),
-                    1,
-                );
-                reviewDeck.scheduledNotes.push({ note, dueUnix: due.valueOf() });
-            }
-        });
-
-        this.updateAndSortDueNotes();
+        //
+        await this.osrAppCore.saveNoteReviewResponse(noteSrTFile, response, this.data.settings);
 
         new Notice(t("RESPONSE_RECEIVED"));
 
         if (this.data.settings.autoNextNote) {
-            if (!this.lastSelectedReviewDeck) {
-                const reviewDeckKeys: string[] = Object.keys(this.reviewDecks);
-                if (reviewDeckKeys.length > 0) this.lastSelectedReviewDeck = reviewDeckKeys[0];
-                else {
-                    new Notice(t("ALL_CAUGHT_UP"));
-                    return;
-                }
-            }
-            this.reviewNextNote(this.lastSelectedReviewDeck);
-        }
-    }
-
-    async reviewNextNoteModal(): Promise<void> {
-        const reviewDeckNames: string[] = Object.keys(this.reviewDecks);
-
-        if (reviewDeckNames.length === 1) {
-            this.reviewNextNote(reviewDeckNames[0]);
-        } else {
-            const deckSelectionModal = new ReviewDeckSelectionModal(this.app, reviewDeckNames);
-            deckSelectionModal.submitCallback = (deckKey: string) => this.reviewNextNote(deckKey);
-            deckSelectionModal.open();
-        }
-    }
-
-    async reviewNextNote(deckKey: string): Promise<void> {
-        if (!Object.prototype.hasOwnProperty.call(this.reviewDecks, deckKey)) {
-            new Notice(t("NO_DECK_EXISTS", { deckName: deckKey }));
-            return;
-        }
-
-        this.lastSelectedReviewDeck = deckKey;
-        const deck = this.reviewDecks[deckKey];
-
-        const nowUnix = Date.now();
-        const dueNotes = deck.scheduledNotes.filter((note) => note.dueUnix <= nowUnix);
-        if (dueNotes.length > 0) {
-            const index = this.data.settings.openRandomNote
-                ? Math.floor(Math.random() * dueNotes.length)
-                : 0;
-            await this.app.workspace.getLeaf().openFile(dueNotes[index].note);
-            return;
-        }
-
-        if (deck.newNotes.length > 0) {
-            const index = this.data.settings.openRandomNote
-                ? Math.floor(Math.random() * deck.newNotes.length)
-                : 0;
-            this.app.workspace.getLeaf().openFile(deck.newNotes[index]);
-            return;
+            this.nextNoteReviewHandler.autoReviewNextNote();
         }
-
-        new Notice(t("ALL_CAUGHT_UP"));
     }
 
     createSrTFile(note: TFile): SrTFile {
@@ -810,6 +445,15 @@ export default class SRPlugin extends Plugin {
         this.data = Object.assign({}, DEFAULT_DATA, loadedData);
         this.data.settings = Object.assign({}, DEFAULT_SETTINGS, this.data.settings);
         setDebugParser(this.data.settings.showPaserDebugMessages);
+
+        this.setupDataStoreAndAlgorithmInstances(this.data.settings);
+    }
+
+    setupDataStoreAndAlgorithmInstances(settings: SRSettings) {
+        // For now we can hardcoded as we only support the one data store and one algorithm
+        DataStore.instance = new DataStore_StoreInNote(settings);
+        SrsAlgorithm.instance = new SrsAlgorithm_Osr(settings);
+        DataStoreAlgorithm.instance = new DataStoreInNote_AlgorithmOsr(settings);
     }
 
     async savePluginData(): Promise<void> {
@@ -836,47 +480,4 @@ export default class SRPlugin extends Plugin {
             this.debouncedGenerateParserTimeout = null;
         }, timeout_ms);
     }
-
-    private getActiveLeaf(type: string): WorkspaceLeaf | null {
-        const leaves = this.app.workspace.getLeavesOfType(type);
-        if (leaves.length == 0) {
-            return null;
-        }
-
-        return leaves[0];
-    }
-
-    private async initReviewQueueView() {
-        this.registerView(
-            REVIEW_QUEUE_VIEW_TYPE,
-            (leaf) => (this.reviewQueueView = new ReviewQueueListView(leaf, this)),
-        );
-
-        if (
-            this.data.settings.enableNoteReviewPaneOnStartup &&
-            this.getActiveLeaf(REVIEW_QUEUE_VIEW_TYPE) == null
-        ) {
-            await this.activateReviewQueueViewPanel();
-        }
-    }
-
-    private async activateReviewQueueViewPanel() {
-        await this.app.workspace.getRightLeaf(false).setViewState({
-            type: REVIEW_QUEUE_VIEW_TYPE,
-            active: true,
-        });
-    }
-
-    private async openReviewQueueView() {
-        let reviewQueueLeaf = this.getActiveLeaf(REVIEW_QUEUE_VIEW_TYPE);
-        if (reviewQueueLeaf == null) {
-            await this.activateReviewQueueViewPanel();
-            reviewQueueLeaf = this.getActiveLeaf(REVIEW_QUEUE_VIEW_TYPE);
-        }
-
-        if (reviewQueueLeaf !== null) {
-            this.app.workspace.revealLeaf(reviewQueueLeaf);
-            this.updateAndSortDueNotes();
-        }
-    }
 }
diff --git a/src/parser.ts b/src/parser.ts
index d1de9ae..a40187d 100644
--- a/src/parser.ts
+++ b/src/parser.ts
@@ -6,14 +6,14 @@ import { generateParser } from "./generateParser";
 export let debugParser = false;
 
 export interface ParserOptions {
-    singleLineCardSeparator: string,
-    singleLineReversedCardSeparator: string,
-    multilineCardSeparator: string,
-    multilineReversedCardSeparator: string,
-    multilineCardEndMarker: string,
-    convertHighlightsToClozes: boolean,
-    convertBoldTextToClozes: boolean,
-    convertCurlyBracketsToClozes: boolean,
+    singleLineCardSeparator: string;
+    singleLineReversedCardSeparator: string;
+    multilineCardSeparator: string;
+    multilineReversedCardSeparator: string;
+    multilineCardEndMarker: string;
+    convertHighlightsToClozes: boolean;
+    convertBoldTextToClozes: boolean;
+    convertCurlyBracketsToClozes: boolean;
 }
 
 export function areParserOptionsEqual(options1: ParserOptions, options2: ParserOptions): boolean {
@@ -31,14 +31,14 @@ export function areParserOptionsEqual(options1: ParserOptions, options2: ParserO
 
 export function copyParserOptions(src: ParserOptions): ParserOptions {
     return {
-        singleLineCardSeparator:src.singleLineCardSeparator,
-        singleLineReversedCardSeparator:src.singleLineReversedCardSeparator,
-        multilineCardSeparator:src.multilineCardSeparator,
-        multilineReversedCardSeparator:src.multilineReversedCardSeparator,
-        multilineCardEndMarker:src.multilineCardEndMarker,
-        convertHighlightsToClozes:src.convertHighlightsToClozes,
-        convertBoldTextToClozes:src.convertBoldTextToClozes,
-        convertCurlyBracketsToClozes:src.convertCurlyBracketsToClozes,
+        singleLineCardSeparator: src.singleLineCardSeparator,
+        singleLineReversedCardSeparator: src.singleLineReversedCardSeparator,
+        multilineCardSeparator: src.multilineCardSeparator,
+        multilineReversedCardSeparator: src.multilineReversedCardSeparator,
+        multilineCardEndMarker: src.multilineCardEndMarker,
+        convertHighlightsToClozes: src.convertHighlightsToClozes,
+        convertBoldTextToClozes: src.convertBoldTextToClozes,
+        convertCurlyBracketsToClozes: src.convertCurlyBracketsToClozes,
     };
 }
 
@@ -47,30 +47,30 @@ export function setDebugParser(value: boolean) {
 }
 
 export class ParsedQuestionInfo {
-	cardType: CardType;
-	text: string;
+    cardType: CardType;
+    text: string;
 
-	// Line numbers start at 0
-	firstLineNum: number;
-	lastLineNum: number;
+    // Line numbers start at 0
+    firstLineNum: number;
+    lastLineNum: number;
 
-	constructor(cardType: CardType, text: string, firstLineNum: number, lastLineNum: number) {
+    constructor(cardType: CardType, text: string, firstLineNum: number, lastLineNum: number) {
         this.cardType = cardType;
-		this.text = text; // text.replace(/\s*$/gm, ""); // reproduce the same old behavior as when adding new lines with trimEnd. It is not clear why we need it in real life. However, it is needed to pass the tests.
-		this.firstLineNum = firstLineNum;
-		this.lastLineNum = lastLineNum;
-	}
-
-	isQuestionLineNum(lineNum: number): boolean {
-		return lineNum >= this.firstLineNum && lineNum <= this.lastLineNum;
-	}
+        this.text = text; // text.replace(/\s*$/gm, ""); // reproduce the same old behavior as when adding new lines with trimEnd. It is not clear why we need it in real life. However, it is needed to pass the tests.
+        this.firstLineNum = firstLineNum;
+        this.lastLineNum = lastLineNum;
+    }
+
+    isQuestionLineNum(lineNum: number): boolean {
+        return lineNum >= this.firstLineNum && lineNum <= this.lastLineNum;
+    }
 }
 
 /**
  * Returns flashcards found in `text`
  *
  * It is best that the text does not contain frontmatter, see extractFrontmatter for reasoning
- * 
+ *
  * EXCEPTIONS: The underlying peggy parser can throw an exception if the input it receives does
  * not conform to the grammar it was built with. However, the grammar used in generating this
  * parser, see generateParser(), intentionally matches all input text and therefore
@@ -80,42 +80,43 @@ export class ParsedQuestionInfo {
  * @param options - Plugin's settings
  * @returns An array of [CardType, card text, line number] tuples
  */
-export function parseEx(
-	text: string,
-    options: ParserOptions,
-): ParsedQuestionInfo[] {
-
-    if(debugParser) {
-      console.log("Text to parse:\n<<<" + text + ">>>");  
+export function parseEx(text: string, options: ParserOptions): ParsedQuestionInfo[] {
+    if (debugParser) {
+        console.log("Text to parse:\n<<<" + text + ">>>");
     }
 
-	let cards: ParsedQuestionInfo[] = [];
-	try {
-		if(!options) throw Error("No parser options provided.");
+    let cards: ParsedQuestionInfo[] = [];
+    try {
+        if (!options) throw Error("No parser options provided.");
 
-		const parser: Parser = generateParser(options);
+        const parser: Parser = generateParser(options);
 
-		// Use this function when you call the parse method
+        // Use this function when you call the parse method
         //
         // The few extra lines empty lines appended to the end of the text "\n\n\n"
         // is a trick to avoid unnecessarily complex grammar rules for the parer,
         // which differen between the case when the last line ends with "\n" or not.
         //
         // Prusamably a single "\n" would be sufficient, but a few more do not bother.
-		cards = parser.parse(text + "\n\n\n",  {
-			CardType,
-			createParsedQuestionInfo: (cardType: CardType, text: string, firstLineNum: number, lastLineNum: number) => {
-				return new ParsedQuestionInfo(cardType, text, firstLineNum, lastLineNum);
-			}
-		});
-	} catch (error) {
-		console.error("Unexpected error:", error);
-	}
-
-    if(debugParser) {
+        cards = parser.parse(text + "\n\n\n", {
+            CardType,
+            createParsedQuestionInfo: (
+                cardType: CardType,
+                text: string,
+                firstLineNum: number,
+                lastLineNum: number,
+            ) => {
+                return new ParsedQuestionInfo(cardType, text, firstLineNum, lastLineNum);
+            },
+        });
+    } catch (error) {
+        console.error("Unexpected error:", error);
+    }
+
+    if (debugParser) {
         console.log("Parsed cards:");
         console.log(cards);
     }
 
-	return cards;
+    return cards;
 }
diff --git a/src/scheduling.ts b/src/scheduling.ts
deleted file mode 100644
index 677f107..0000000
--- a/src/scheduling.ts
+++ /dev/null
@@ -1,89 +0,0 @@
-import { SRSettings } from "src/settings";
-import { t } from "src/lang/helpers";
-
-export enum ReviewResponse {
-    Easy,
-    Good,
-    Hard,
-    Reset,
-}
-
-// Flashcards
-
-export function schedule(
-    response: ReviewResponse,
-    interval: number,
-    ease: number,
-    delayBeforeReview: number,
-    settingsObj: SRSettings,
-    dueDates?: Record<number, number>,
-): Record<string, number> {
-    delayBeforeReview = Math.max(0, Math.floor(delayBeforeReview / (24 * 3600 * 1000)));
-
-    if (response === ReviewResponse.Easy) {
-        ease += 20;
-        interval = ((interval + delayBeforeReview) * ease) / 100;
-        interval *= settingsObj.easyBonus;
-    } else if (response === ReviewResponse.Good) {
-        interval = ((interval + delayBeforeReview / 2) * ease) / 100;
-    } else if (response === ReviewResponse.Hard) {
-        ease = Math.max(130, ease - 20);
-        interval = Math.max(
-            1,
-            (interval + delayBeforeReview / 4) * settingsObj.lapsesIntervalChange,
-        );
-    }
-
-    // replaces random fuzz with load balancing over the fuzz interval
-    if (dueDates !== undefined) {
-        interval = Math.round(interval);
-        if (!Object.prototype.hasOwnProperty.call(dueDates, interval)) {
-            dueDates[interval] = 0;
-        } else {
-            // disable fuzzing for small intervals
-            if (interval > 4) {
-                let fuzz = 0;
-                if (interval < 7) fuzz = 1;
-                else if (interval < 30) fuzz = Math.max(2, Math.floor(interval * 0.15));
-                else fuzz = Math.max(4, Math.floor(interval * 0.05));
-
-                const originalInterval = interval;
-                outer: for (let i = 1; i <= fuzz; i++) {
-                    for (const ivl of [originalInterval - i, originalInterval + i]) {
-                        if (!Object.prototype.hasOwnProperty.call(dueDates, ivl)) {
-                            dueDates[ivl] = 0;
-                            interval = ivl;
-                            break outer;
-                        }
-                        if (dueDates[ivl] < dueDates[interval]) interval = ivl;
-                    }
-                }
-            }
-        }
-
-        dueDates[interval]++;
-    }
-
-    interval = Math.min(interval, settingsObj.maximumInterval);
-
-    return { interval: Math.round(interval * 10) / 10, ease };
-}
-
-export function textInterval(interval: number, isMobile: boolean): string {
-    if (interval === undefined) {
-        return t("NEW");
-    }
-
-    const m: number = Math.round(interval / 3.04375) / 10,
-        y: number = Math.round(interval / 36.525) / 10;
-
-    if (isMobile) {
-        if (m < 1.0) return t("DAYS_STR_IVL_MOBILE", { interval });
-        else if (y < 1.0) return t("MONTHS_STR_IVL_MOBILE", { interval: m });
-        else return t("YEARS_STR_IVL_MOBILE", { interval: y });
-    } else {
-        if (m < 1.0) return t("DAYS_STR_IVL", { interval });
-        else if (y < 1.0) return t("MONTHS_STR_IVL", { interval: m });
-        else return t("YEARS_STR_IVL", { interval: y });
-    }
-}
diff --git a/src/settings.ts b/src/settings.ts
index c2790ac..2a0786b 100644
--- a/src/settings.ts
+++ b/src/settings.ts
@@ -117,6 +117,34 @@ export class SettingsUtil {
         return SettingsUtil.isTagInList(settings.flashcardTags, tag);
     }
 
+    static isPathInNoteIgnoreFolder(settings: SRSettings, path: string): boolean {
+        return settings.noteFoldersToIgnore.some((folder) => path.startsWith(folder));
+    }
+
+    static isAnyTagANoteReviewTag(settings: SRSettings, tags: string[]): boolean {
+        for (const tag of tags) {
+            if (
+                settings.tagsToReview.some(
+                    (tagToReview) => tag === tagToReview || tag.startsWith(tagToReview + "/"),
+                )
+            ) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    // Given a list of tags, return the subset that is in settings.tagsToReview
+    static filterForNoteReviewTag(settings: SRSettings, tags: string[]): string[] {
+        const result: string[] = [];
+        for (const tagToReview of settings.tagsToReview) {
+            if (tags.some((tag) => tag === tagToReview || tag.startsWith(tagToReview + "/"))) {
+                result.push(tagToReview);
+            }
+        }
+        return result;
+    }
+
     private static isTagInList(tagList: string[], tag: string): boolean {
         for (const tagFromList of tagList) {
             if (tag === tagFromList || tag.startsWith(tagFromList + "/")) {
@@ -143,7 +171,13 @@ export class SRSettingTab extends PluginSettingTab {
         this.plugin = plugin;
     }
 
+    // eslint-disable-next-line @typescript-eslint/no-explicit-any
+    hide(): any {
+        console.log("SRSettingTab: hide()");
+    }
+
     display(): void {
+        console.log("SRSettingTab: display()");
         const { containerEl } = this;
 
         containerEl.empty();
diff --git a/src/util/DateProvider.ts b/src/util/DateProvider.ts
index b995818..89e853a 100644
--- a/src/util/DateProvider.ts
+++ b/src/util/DateProvider.ts
@@ -3,10 +3,15 @@ import { Moment } from "moment";
 import { ALLOWED_DATE_FORMATS } from "src/constants";
 
 export interface IDateProvider {
+    get now(): Moment;
     get today(): Moment;
 }
 
 export class LiveDateProvider implements IDateProvider {
+    get now(): Moment {
+        return moment();
+    }
+
     get today(): Moment {
         return moment().startOf("day");
     }
@@ -19,10 +24,14 @@ export class StaticDateProvider implements IDateProvider {
         this.moment = moment;
     }
 
-    get today(): Moment {
+    get now(): Moment {
         return this.moment.clone();
     }
 
+    get today(): Moment {
+        return this.moment.clone().startOf("day");
+    }
+
     static fromDateStr(str: string): StaticDateProvider {
         return new StaticDateProvider(DateUtil.dateStrToMoment(str));
     }
diff --git a/src/util/utils.ts b/src/util/utils.ts
index 404ea69..7605c16 100644
--- a/src/util/utils.ts
+++ b/src/util/utils.ts
@@ -106,19 +106,6 @@ export function convertToStringOrEmpty(v: any): string {
     return result;
 }
 
-//
-// This returns [frontmatter, content]
-//
-// The returned content has the same number of lines as the supplied str string, but with the
-// frontmatter lines (if present) blanked out.
-//
-// 1. We don't want the parser to see the frontmatter, as it would deem it to be part of a multi-line question
-// if one started on the line immediately after the "---" closing marker.
-//
-// 2. The lines are blanked out rather than deleted so that line numbers are not affected
-// e.g. for calls to getQuestionContext(cardLine: number)
-//
-
 /**
  * Determines if a given path (`toCheck`) is either equal to or a sub-path of a specified root path (`rootPath`).
  * The function compares the paths in a case-insensitive manner and normalizes the directory separators for consistency across different platforms.
@@ -169,17 +156,19 @@ export function isEqualOrSubPath(toCheck: string, rootPath: string): boolean {
     return true;
 }
 
-/**
- * The returned content has the same number of lines as the supplied string, but with the frontmatter lines (if present) blanked out.
- *
- * 1. We don't want the parser to see the frontmatter, as it would deem it to be part of a multi-line question if one started on the line immediately after the "---" closing marker.
- *
- * 2. The lines are blanked out rather than deleted so that line numbers are not affected e.g. for calls to getQuestionContext(cardLine: number)
- *
- * @param str The file content as string
- * @returns [frontmatter, content]
- */
-export function extractFrontmatter(str: string): [string, string] {
+//
+// This returns [frontmatter, content]
+//
+// The returned content has the same number of lines as the supplied str string, but with the
+// frontmatter lines (if present) blanked out.
+//
+// 1. We don't want the parser to see the frontmatter, as it would deem it to be part of a multi-line question
+// if one started on the line immediately after the "---" closing marker.
+//
+// 2. The lines are blanked out rather than deleted so that line numbers are not affected
+// e.g. for calls to getQuestionContext(cardLine: number)
+//
+export function splitNoteIntoFrontmatterAndContent(str: string): [string, string] {
     const lines = splitTextIntoLineArray(str);
     let lineIndex = 0;
     let hasFrontmatter = false;
@@ -229,6 +218,10 @@ export function findLineIndexOfSearchStringIgnoringWs(
     return result;
 }
 
+export function isSupportedFileType(path: string): boolean {
+    return path.split(".").pop().toLowerCase() === "md";
+}
+
 /* 
 Prompted by flashcards being missed, here are some "experiments" with different frontmatter,
 showing the difference in the value of CachedMetadata.frontmatter["tags"]


Test patch:
diff --git a/tests/unit/DeckTreeIterator.test.ts b/tests/unit/DeckTreeIterator.test.ts
index 742bb41..eb738a3 100644
--- a/tests/unit/DeckTreeIterator.test.ts
+++ b/tests/unit/DeckTreeIterator.test.ts
@@ -13,6 +13,8 @@ import {
     setupNextRandomNumber,
     setupStaticRandomNumberProvider,
 } from "src/util/RandomNumberProvider";
+import { NoteEaseList } from "src/NoteEaseList";
+import { unitTestSetup_StandardDataStoreAlgorithm } from "./helpers/UnitTestSetup";
 
 let order_DueFirst_Sequential: IIteratorOrder = {
     cardOrder: CardOrder.DueFirstSequential,
@@ -24,6 +26,7 @@ var iterator: DeckTreeIterator;
 beforeAll(() => {
     setupStaticDateProvider_20230906();
     setupStaticRandomNumberProvider();
+    unitTestSetup_StandardDataStoreAlgorithm(DEFAULT_SETTINGS);
 });
 
 describe("setDeck", () => {
diff --git a/tests/unit/FlashcardReviewSequencer.test.ts b/tests/unit/FlashcardReviewSequencer.test.ts
index f19f15c..fb2b3dd 100644
--- a/tests/unit/FlashcardReviewSequencer.test.ts
+++ b/tests/unit/FlashcardReviewSequencer.test.ts
@@ -1,4 +1,3 @@
-import { CardScheduleCalculator } from "src/CardSchedule";
 import {
     CardOrder,
     DeckOrder,
@@ -16,7 +15,6 @@ import { TopicPath } from "src/TopicPath";
 import { CardListType, Deck, DeckTreeFilter } from "src/Deck";
 import { DEFAULT_SETTINGS, SRSettings } from "src/settings";
 import { SampleItemDecks } from "./SampleItems";
-import { ReviewResponse } from "src/scheduling";
 import {
     setupStaticDateProvider,
     setupStaticDateProvider_20230906,
@@ -26,6 +24,10 @@ import moment from "moment";
 import { INoteEaseList, NoteEaseList } from "src/NoteEaseList";
 import { QuestionPostponementList, IQuestionPostponementList } from "src/QuestionPostponementList";
 import { UnitTestSRFile } from "./helpers/UnitTestSRFile";
+import { ReviewResponse } from "src/algorithms/base/RepetitionItem";
+import { unitTestSetup_StandardDataStoreAlgorithm } from "./helpers/UnitTestSetup";
+import { SrsAlgorithm } from "src/algorithms/base/SrsAlgorithm";
+import { CardDueDateHistogram } from "src/DueDateHistogram";
 
 let order_DueFirst_Sequential: IIteratorOrder = {
     cardOrder: CardOrder.DueFirstSequential,
@@ -42,10 +44,9 @@ class TestContext {
     reviewMode: FlashcardReviewMode;
     iteratorOrder: IIteratorOrder;
     cardSequencer: IDeckTreeIterator;
-    noteEaseList: INoteEaseList;
-    cardScheduleCalculator: CardScheduleCalculator;
-    reviewSequencer: FlashcardReviewSequencer;
+    reviewSequencer: IFlashcardReviewSequencer;
     questionPostponementList: QuestionPostponementList;
+    dueDateFlashcardHistogram: CardDueDateHistogram;
     file: UnitTestSRFile;
     originalText: string;
     fakeFilePath: string;
@@ -62,8 +63,9 @@ class TestContext {
             this.reviewMode,
             cardSequencer,
             this.settings,
-            this.cardScheduleCalculator,
+            SrsAlgorithm.getInstance(),
             this.questionPostponementList,
+            this.dueDateFlashcardHistogram,
         );
         setupStaticDateProvider_OriginDatePlusDays(daysAfterOrigin);
 
@@ -101,33 +103,30 @@ class TestContext {
         text: string,
         fakeFilePath?: string,
     ): TestContext {
+        const settingsClone: SRSettings = { ...settings };
         let cardSequencer: IDeckTreeIterator = new DeckTreeIterator(iteratorOrder, null);
-        let noteEaseList = new NoteEaseList(settings);
-        let cardScheduleCalculator: CardScheduleCalculator = new CardScheduleCalculator(
-            settings,
-            noteEaseList,
-        );
+        unitTestSetup_StandardDataStoreAlgorithm(settingsClone);
         let cardPostponementList: QuestionPostponementList = new QuestionPostponementList(
             null,
-            settings,
+            settingsClone,
             [],
         );
+        let dueDateFlashcardHistogram: CardDueDateHistogram = new CardDueDateHistogram();
         let reviewSequencer: FlashcardReviewSequencer = new FlashcardReviewSequencer(
             reviewMode,
             cardSequencer,
-            settings,
-            cardScheduleCalculator,
+            settingsClone,
+            SrsAlgorithm.getInstance(),
             cardPostponementList,
+            dueDateFlashcardHistogram,
         );
         var file: UnitTestSRFile = new UnitTestSRFile(text, fakeFilePath);
 
         let result: TestContext = new TestContext({
-            settings,
+            settings: settingsClone,
             reviewMode,
             iteratorOrder,
             cardSequencer,
-            noteEaseList,
-            cardScheduleCalculator,
             reviewSequencer,
             questionPostponementList: cardPostponementList,
             file,
@@ -169,7 +168,7 @@ async function checkReviewResponse_ReviewMode(
     let card = c.reviewSequencer.currentCard;
     expect(card.front).toEqual("Q2");
     expect(card.scheduleInfo).toMatchObject({
-        ease: 270,
+        latestEase: 270,
         interval: 4,
     });
 
@@ -178,7 +177,7 @@ async function checkReviewResponse_ReviewMode(
     expect(c.reviewSequencer.currentCard.front).toEqual("Q1");
 
     // Schedule for the reviewed card has been updated
-    expect(card.scheduleInfo.ease).toEqual(info.cardQ2_PostReviewEase);
+    expect(card.scheduleInfo.latestEase).toEqual(info.cardQ2_PostReviewEase);
     expect(card.scheduleInfo.interval).toEqual(info.cardQ2_PostReviewInterval);
     expect(card.scheduleInfo.dueDate.unix).toEqual(moment(info.cardQ2_PostReviewDueDate).unix);
 
@@ -211,7 +210,7 @@ async function checkReviewResponse_CramMode(reviewResponse: ReviewResponse): Pro
     let card = c.reviewSequencer.currentCard;
     expect(card.front).toEqual("Q1");
     let expectInfo = {
-        ease: 270,
+        latestEase: 270,
         interval: 4,
     };
     expect(card.scheduleInfo).toMatchObject(expectInfo);
@@ -453,7 +452,7 @@ describe("processReview", () => {
                 let card = c.reviewSequencer.currentCard;
                 expect(card.front).toEqual("Q1");
                 expect(card.scheduleInfo).toMatchObject({
-                    ease: 270,
+                    latestEase: 270,
                     interval: 4,
                 });
 
@@ -463,7 +462,7 @@ describe("processReview", () => {
                 card = c.reviewSequencer.currentCard;
                 expect(card.front).toEqual("Q2");
                 expect(card.scheduleInfo).toMatchObject({
-                    ease: 270,
+                    latestEase: 270,
                     interval: 5,
                 });
 
@@ -471,7 +470,7 @@ describe("processReview", () => {
                 card = c.reviewSequencer.currentCard;
                 expect(card.front).toEqual("Q3");
                 expect(card.scheduleInfo).toMatchObject({
-                    ease: 270,
+                    latestEase: 270,
                     interval: 6,
                 });
 
@@ -480,7 +479,7 @@ describe("processReview", () => {
                 card = c.reviewSequencer.currentCard;
                 expect(card.front).toEqual("Q1");
                 expect(card.scheduleInfo).toMatchObject({
-                    ease: DEFAULT_SETTINGS.baseEase,
+                    latestEase: DEFAULT_SETTINGS.baseEase,
                     interval: 1,
                 });
             });
diff --git a/tests/unit/Note.test.ts b/tests/unit/Note.test.ts
index f21b503..05c9bde 100644
--- a/tests/unit/Note.test.ts
+++ b/tests/unit/Note.test.ts
@@ -6,10 +6,16 @@ import { DEFAULT_SETTINGS } from "src/settings";
 import { NoteFileLoader } from "src/NoteFileLoader";
 import { TextDirection } from "src/util/TextDirection";
 import { UnitTestSRFile } from "./helpers/UnitTestSRFile";
+import { NoteEaseList } from "src/NoteEaseList";
+import { unitTestSetup_StandardDataStoreAlgorithm } from "./helpers/UnitTestSetup";
 
 let parser: NoteParser = new NoteParser(DEFAULT_SETTINGS);
 var noteFileLoader: NoteFileLoader = new NoteFileLoader(DEFAULT_SETTINGS);
 
+beforeAll(() => {
+    unitTestSetup_StandardDataStoreAlgorithm(DEFAULT_SETTINGS);
+});
+
 describe("appendCardsToDeck", () => {
     test("Multiple questions, single card per question", async () => {
         let noteText: string = `#flashcards/test
diff --git a/tests/unit/NoteCardScheduleParser.test.ts b/tests/unit/NoteCardScheduleParser.test.ts
index 49f37df..6824f4a 100644
--- a/tests/unit/NoteCardScheduleParser.test.ts
+++ b/tests/unit/NoteCardScheduleParser.test.ts
@@ -1,43 +1,56 @@
-import { CardScheduleInfo, NoteCardScheduleParser } from "src/CardSchedule";
+import { NoteEaseList } from "src/NoteEaseList";
 import { TICKS_PER_DAY } from "src/constants";
+import { DataStore } from "src/dataStore/base/DataStore";
+import { DEFAULT_SETTINGS } from "src/settings";
 import { setupStaticDateProvider_20230906 } from "src/util/DateProvider";
+import { unitTestSetup_StandardDataStoreAlgorithm } from "./helpers/UnitTestSetup";
+import { RepItemScheduleInfo_Osr } from "src/algorithms/osr/RepItemScheduleInfo_Osr";
+import { RepItemScheduleInfo } from "src/algorithms/base/RepItemScheduleInfo";
 
 beforeAll(() => {
     setupStaticDateProvider_20230906();
+    unitTestSetup_StandardDataStoreAlgorithm(DEFAULT_SETTINGS);
 });
 
 test("No schedule info for question", () => {
-    expect(NoteCardScheduleParser.createCardScheduleInfoList("A::B")).toEqual([]);
+    /* let cardScheduleInfoList: RepItemScheduleInfo[] =
+    DataStore.getInstance().questionCreateSchedule("A::B", null); */
+    expect(DataStore.getInstance().questionCreateSchedule("A::B", null)).toEqual([]);
 });
 
 test("Single schedule info for question (on separate line)", () => {
-    let actual: CardScheduleInfo[] =
-        NoteCardScheduleParser.createCardScheduleInfoList(`What symbol represents an electric field:: $\\large \\vec E$
-<!--SR:!2023-09-02,4,270-->`);
+    let actual: RepItemScheduleInfo[] = DataStore.getInstance().questionCreateSchedule(
+        `What symbol represents an electric field:: $\\large \\vec E$
+<!--SR:!2023-09-02,4,270-->`,
+        null,
+    );
 
     expect(actual).toEqual([
-        CardScheduleInfo.fromDueDateStr("2023-09-02", 4, 270, -4 * TICKS_PER_DAY),
+        RepItemScheduleInfo_Osr.fromDueDateStr("2023-09-02", 4, 270, -4 * TICKS_PER_DAY),
     ]);
 });
 
 test("Single schedule info for question (on same line)", () => {
-    let actual: CardScheduleInfo[] = NoteCardScheduleParser.createCardScheduleInfoList(
+    let actual: RepItemScheduleInfo[] = DataStore.getInstance().questionCreateSchedule(
         `What symbol represents an electric field:: $\\large \\vec E$<!--SR:!2023-09-02,4,270-->`,
+        null,
     );
 
     expect(actual).toEqual([
-        CardScheduleInfo.fromDueDateStr("2023-09-02", 4, 270, -4 * TICKS_PER_DAY),
+        RepItemScheduleInfo_Osr.fromDueDateStr("2023-09-02", 4, 270, -4 * TICKS_PER_DAY),
     ]);
 });
 
 test("Multiple schedule info for question (on separate line)", () => {
-    let actual: CardScheduleInfo[] =
-        NoteCardScheduleParser.createCardScheduleInfoList(`This is a really very ==interesting== and ==fascinating== and ==great== test
-    <!--SR:!2023-09-03,1,230!2023-09-05,3,250!2023-09-06,4,270-->`);
+    let actual: RepItemScheduleInfo[] = DataStore.getInstance().questionCreateSchedule(
+        `This is a really very ==interesting== and ==fascinating== and ==great== test
+    <!--SR:!2023-09-03,1,230!2023-09-05,3,250!2023-09-06,4,270-->`,
+        null,
+    );
 
     expect(actual).toEqual([
-        CardScheduleInfo.fromDueDateStr("2023-09-03", 1, 230, -3 * TICKS_PER_DAY),
-        CardScheduleInfo.fromDueDateStr("2023-09-05", 3, 250, -1 * TICKS_PER_DAY),
-        CardScheduleInfo.fromDueDateStr("2023-09-06", 4, 270, 0),
+        RepItemScheduleInfo_Osr.fromDueDateStr("2023-09-03", 1, 230, -3 * TICKS_PER_DAY),
+        RepItemScheduleInfo_Osr.fromDueDateStr("2023-09-05", 3, 250, -1 * TICKS_PER_DAY),
+        RepItemScheduleInfo_Osr.fromDueDateStr("2023-09-06", 4, 270, 0),
     ]);
 });
diff --git a/tests/unit/NoteFileLoader.test.ts b/tests/unit/NoteFileLoader.test.ts
index c60db47..83dbd2d 100644
--- a/tests/unit/NoteFileLoader.test.ts
+++ b/tests/unit/NoteFileLoader.test.ts
@@ -4,9 +4,15 @@ import { TopicPath } from "src/TopicPath";
 import { DEFAULT_SETTINGS } from "src/settings";
 import { TextDirection } from "src/util/TextDirection";
 import { UnitTestSRFile } from "./helpers/UnitTestSRFile";
+import { NoteEaseList } from "src/NoteEaseList";
+import { unitTestSetup_StandardDataStoreAlgorithm } from "./helpers/UnitTestSetup";
 
 var noteFileLoader: NoteFileLoader = new NoteFileLoader(DEFAULT_SETTINGS);
 
+beforeAll(() => {
+    unitTestSetup_StandardDataStoreAlgorithm(DEFAULT_SETTINGS);
+});
+
 describe("load", () => {
     test("Multiple questions, none with too many schedule details", async () => {
         let noteText: string = `#flashcards/test
diff --git a/tests/unit/NoteParser.test.ts b/tests/unit/NoteParser.test.ts
index 3f62bbc..5808e83 100644
--- a/tests/unit/NoteParser.test.ts
+++ b/tests/unit/NoteParser.test.ts
@@ -6,11 +6,14 @@ import { DEFAULT_SETTINGS } from "src/settings";
 import { setupStaticDateProvider_20230906 } from "src/util/DateProvider";
 import { TextDirection } from "src/util/TextDirection";
 import { UnitTestSRFile } from "./helpers/UnitTestSRFile";
+import { unitTestSetup_StandardDataStoreAlgorithm } from "./helpers/UnitTestSetup";
+import { NoteEaseList } from "src/NoteEaseList";
 
 let parser: NoteParser = new NoteParser(DEFAULT_SETTINGS);
 
 beforeAll(() => {
     setupStaticDateProvider_20230906();
+    unitTestSetup_StandardDataStoreAlgorithm(DEFAULT_SETTINGS);
 });
 
 describe("Multiple questions in the text", () => {
diff --git a/tests/unit/NoteQuestionParser.test.ts b/tests/unit/NoteQuestionParser.test.ts
index f4ebf0f..ba5663c 100644
--- a/tests/unit/NoteQuestionParser.test.ts
+++ b/tests/unit/NoteQuestionParser.test.ts
@@ -1,5 +1,4 @@
 import { NoteQuestionParser } from "src/NoteQuestionParser";
-import { CardScheduleInfo } from "src/CardSchedule";
 import { TICKS_PER_DAY } from "src/constants";
 import { CardType, Question } from "src/Question";
 import { DEFAULT_SETTINGS, SRSettings } from "src/settings";
@@ -9,6 +8,10 @@ import { ISRFile, frontmatterTagPseudoLineNum } from "src/SRFile";
 import { setupStaticDateProvider_20230906 } from "src/util/DateProvider";
 import { TextDirection } from "src/util/TextDirection";
 import { UnitTestSRFile } from "./helpers/UnitTestSRFile";
+import { RepItemScheduleInfo } from "src/algorithms/base/RepItemScheduleInfo";
+import { RepItemScheduleInfo_Osr } from "src/algorithms/osr/RepItemScheduleInfo_Osr";
+import { NoteEaseList } from "src/NoteEaseList";
+import { unitTestSetup_StandardDataStoreAlgorithm } from "./helpers/UnitTestSetup";
 import { Card } from "src/Card";
 
 let parserWithDefaultSettings: NoteQuestionParser = createTest_NoteQuestionParser(DEFAULT_SETTINGS);
@@ -20,6 +23,7 @@ let parser_ConvertFoldersToDecks: NoteQuestionParser = createTest_NoteQuestionPa
 
 beforeAll(() => {
     setupStaticDateProvider_20230906();
+    unitTestSetup_StandardDataStoreAlgorithm(DEFAULT_SETTINGS);
 });
 
 describe("No flashcard questions", () => {
@@ -64,7 +68,7 @@ A::B
         let folderTopicPath: TopicPath = TopicPath.emptyPath;
         let card1 = {
             cardIdx: 0,
-            scheduleInfo: null as CardScheduleInfo,
+            scheduleInfo: null as RepItemScheduleInfo,
         };
         let expected = [
             {
@@ -99,14 +103,11 @@ A::B
 
         let folderTopicPath: TopicPath = TopicPath.emptyPath;
         let delayDays = 3 - 6;
+        let scheduleInfo = RepItemScheduleInfo_Osr.fromDueDateStr("2023-09-03", 1, 230);
+        scheduleInfo.delayedBeforeReviewTicks = delayDays * TICKS_PER_DAY;
         let card1 = {
             cardIdx: 0,
-            scheduleInfo: CardScheduleInfo.fromDueDateStr(
-                "2023-09-03",
-                1,
-                230,
-                delayDays * TICKS_PER_DAY,
-            ),
+            scheduleInfo,
         };
         let expected = [
             {
@@ -206,7 +207,7 @@ A::B ^d7cee0
         let folderTopicPath: TopicPath = TopicPath.emptyPath;
         let card1 = {
             cardIdx: 0,
-            scheduleInfo: null as CardScheduleInfo,
+            scheduleInfo: null as RepItemScheduleInfo_Osr,
         };
         let expected = [
             {
@@ -248,14 +249,12 @@ A::B ^d7cee0
 
         let folderTopicPath: TopicPath = TopicPath.emptyPath;
         let delayDays = 3 - 6;
+        let scheduleInfo = RepItemScheduleInfo_Osr.fromDueDateStr("2023-09-03", 1, 230);
+        scheduleInfo.delayedBeforeReviewTicks = delayDays * TICKS_PER_DAY;
+
         let card1 = {
             cardIdx: 0,
-            scheduleInfo: CardScheduleInfo.fromDueDateStr(
-                "2023-09-03",
-                1,
-                230,
-                delayDays * TICKS_PER_DAY,
-            ),
+            scheduleInfo,
         };
         let expected = [
             {
@@ -297,14 +296,11 @@ A::B <!--SR:!2023-09-03,1,230--> ^d7cee0
 
         let folderTopicPath: TopicPath = TopicPath.emptyPath;
         let delayDays = 3 - 6;
+        let scheduleInfo = RepItemScheduleInfo_Osr.fromDueDateStr("2023-09-03", 1, 230);
+        scheduleInfo.delayedBeforeReviewTicks = delayDays * TICKS_PER_DAY;
         let card1 = {
             cardIdx: 0,
-            scheduleInfo: CardScheduleInfo.fromDueDateStr(
-                "2023-09-03",
-                1,
-                230,
-                delayDays * TICKS_PER_DAY,
-            ),
+            scheduleInfo,
         };
         let expected = [
             {
@@ -345,14 +341,11 @@ A::B <!--SR:!2023-09-03,1,230--> ^d7cee0
 
         let folderTopicPath: TopicPath = TopicPath.emptyPath;
         let delayDays = 3 - 6;
+        let scheduleInfo = RepItemScheduleInfo_Osr.fromDueDateStr("2023-09-03", 1, 230);
+        scheduleInfo.delayedBeforeReviewTicks = delayDays * TICKS_PER_DAY;
         let card1 = {
             cardIdx: 0,
-            scheduleInfo: CardScheduleInfo.fromDueDateStr(
-                "2023-09-03",
-                1,
-                230,
-                delayDays * TICKS_PER_DAY,
-            ),
+            scheduleInfo,
         };
         let expected = [
             {
@@ -893,7 +886,7 @@ function checkQuestion1(question: Question) {
         isDue: false,
         front: "Q1",
         back: "A1",
-        scheduleInfo: null as CardScheduleInfo,
+        scheduleInfo: null as RepItemScheduleInfo_Osr,
     };
     let expected = {
         questionType: CardType.SingleLineBasic,
@@ -917,7 +910,7 @@ function checkQuestion2(question: Question) {
         isDue: false,
         front: "Q2",
         back: "A2",
-        scheduleInfo: null as CardScheduleInfo,
+        scheduleInfo: null as RepItemScheduleInfo_Osr,
     };
     let expected = {
         questionType: CardType.SingleLineBasic,
diff --git a/tests/unit/NoteReviewQueue.test.ts b/tests/unit/NoteReviewQueue.test.ts
new file mode 100644
index 0000000..94fb988
--- /dev/null
+++ b/tests/unit/NoteReviewQueue.test.ts
@@ -0,0 +1,58 @@
+import { DEFAULT_SETTINGS } from "src/settings";
+import { UnitTestOsrCore } from "./helpers/UnitTestOsrCore";
+import {
+    DateUtil,
+    globalDateProvider,
+    setupStaticDateProvider,
+    setupStaticDateProvider_20230906,
+} from "src/util/DateProvider";
+import { unitTestSetup_StandardDataStoreAlgorithm } from "./helpers/UnitTestSetup";
+import { DueDateHistogram } from "src/DueDateHistogram";
+import { NoteReviewQueue } from "src/NoteReviewQueue";
+
+beforeAll(() => {
+    setupStaticDateProvider_20230906();
+    unitTestSetup_StandardDataStoreAlgorithm(DEFAULT_SETTINGS);
+});
+
+function checkHistogramValue(histogram: DueDateHistogram, nDays: number, expectedValue: number) {
+    expect(histogram.hasEntryForDays(nDays)).toEqual(true);
+    expect(histogram.get(nDays)).toEqual(expectedValue);
+}
+
+function checkHistogramDueCardCount(histogram: DueDateHistogram, expectedValue: number) {
+    checkHistogramValue(histogram, DueDateHistogram.dueNowNDays, expectedValue);
+}
+
+describe("determineScheduleInfo", () => {
+    test("No notes due", async () => {
+        const osrCore: UnitTestOsrCore = new UnitTestOsrCore(DEFAULT_SETTINGS);
+
+        // A.md due 2023-09-10 (in 4 days time)
+        await osrCore.loadTestVault("notes4");
+        const histogram: DueDateHistogram = osrCore.dueDateNoteHistogram;
+        expect(histogram.hasEntryForDays(DueDateHistogram.dueNowNDays)).toEqual(false);
+    });
+
+    test("Note A.md due today", async () => {
+        const osrCore: UnitTestOsrCore = new UnitTestOsrCore(DEFAULT_SETTINGS);
+
+        // A.md due 2023-09-10, so it should be due
+        setupStaticDateProvider("2023-09-10");
+
+        await osrCore.loadTestVault("notes4");
+        const noteReviewQueue: NoteReviewQueue = osrCore.noteReviewQueue;
+        const histogram: DueDateHistogram = osrCore.dueDateNoteHistogram;
+        checkHistogramDueCardCount(histogram, 1);
+    });
+});
+
+describe("dueNotesCount", () => {
+    test("No notes due", async () => {
+        const osrCore: UnitTestOsrCore = new UnitTestOsrCore(DEFAULT_SETTINGS);
+
+        // A.md due 2023-09-10 (in 4 days time)
+        await osrCore.loadTestVault("notes4");
+        expect(osrCore.noteReviewQueue.dueNotesCount).toEqual(1);
+    });
+});
diff --git a/tests/unit/OsrCore.test.ts b/tests/unit/OsrCore.test.ts
new file mode 100644
index 0000000..1b06da4
--- /dev/null
+++ b/tests/unit/OsrCore.test.ts
@@ -0,0 +1,369 @@
+import { OsrCore } from "src/OsrCore";
+import { UnitTestOsrCore } from "./helpers/UnitTestOsrCore";
+import { DEFAULT_SETTINGS, SRSettings } from "src/settings";
+import { CardListType } from "src/Deck";
+import { unitTestSetup_StandardDataStoreAlgorithm } from "./helpers/UnitTestSetup";
+import { NoteReviewDeck, SchedNote } from "src/NoteReviewDeck";
+import { DateUtil, setupStaticDateProvider_20230906 } from "src/util/DateProvider";
+import { formatDate_YYYY_MM_DD } from "src/util/utils";
+import moment from "moment";
+import { ReviewResponse } from "src/algorithms/base/RepetitionItem";
+import { unitTest_CheckNoteFrontmatter } from "./helpers/UnitTestHelper";
+import { DueDateHistogram, NoteDueDateHistogram } from "src/DueDateHistogram";
+import { ISRFile } from "src/SRFile";
+
+function checkDeckTreeCounts(
+    osrCore: UnitTestOsrCore,
+    expectedReviewableCount: number,
+    expectedRemainingCount: number,
+): void {
+    expect(osrCore.reviewableDeckTree.getCardCount(CardListType.All, true)).toEqual(
+        expectedReviewableCount,
+    );
+    expect(osrCore.remainingDeckTree.getCardCount(CardListType.All, true)).toEqual(
+        expectedRemainingCount,
+    );
+}
+
+function checkNoteReviewDeck_Basic(actual: NoteReviewDeck, expected: any): void {
+    expect(actual.deckName).toEqual(expected.deckName);
+    expect(actual.dueNotesCount).toEqual(expected.dueNotesCount);
+    expect(actual.newNotes.length).toEqual(expected.newNotesLength);
+    expect(actual.scheduledNotes.length).toEqual(expected.scheduledNotesLength);
+}
+
+function checkScheduledNote(actual: SchedNote, expected: any): void {
+    expect(actual.note.path.endsWith(expected.filename)).toBeTruthy();
+    expect(formatDate_YYYY_MM_DD(moment(actual.dueUnix))).toEqual(expected.dueDate);
+}
+
+beforeAll(() => {
+    setupStaticDateProvider_20230906();
+    unitTestSetup_StandardDataStoreAlgorithm(DEFAULT_SETTINGS);
+});
+
+test("No questions in the text; no files tagged as notes", async () => {
+    const osrCore: UnitTestOsrCore = new UnitTestOsrCore(DEFAULT_SETTINGS);
+    await osrCore.loadTestVault("filesButNoQuestions");
+    expect(osrCore.dueDateNoteHistogram.dueNotesCount).toEqual(0);
+    expect(osrCore.noteReviewQueue.reviewDecks.size).toEqual(0);
+    checkDeckTreeCounts(osrCore, 0, 0);
+    expect(osrCore.questionPostponementList.list.length).toEqual(0);
+});
+
+describe("Notes", () => {
+    describe("Testing code that loads from test vault", () => {
+        test("Tagged as note, but no OSR frontmatter", async () => {
+            const osrCore: UnitTestOsrCore = new UnitTestOsrCore(DEFAULT_SETTINGS);
+            await osrCore.loadTestVault("notes1");
+
+            expect(osrCore.dueDateNoteHistogram.dueNotesCount).toEqual(0);
+            expect(osrCore.noteReviewQueue.reviewDecks.size).toEqual(1);
+
+            // Single deck "#review", with single new note "Computation Graph.md"
+            const actual: NoteReviewDeck = osrCore.noteReviewQueue.reviewDecks.get("#review");
+            checkNoteReviewDeck_Basic(actual, {
+                deckName: "#review",
+                dueNotesCount: 0,
+                newNotesLength: 1,
+                scheduledNotesLength: 0,
+            });
+            expect(actual.newNotes[0].path.endsWith("Computation Graph.md")).toBeTruthy();
+        });
+
+        test("Tagged as note, and includes OSR frontmatter", async () => {
+            const osrCore: UnitTestOsrCore = new UnitTestOsrCore(DEFAULT_SETTINGS);
+            await osrCore.loadTestVault("notes2");
+
+            expect(osrCore.dueDateNoteHistogram.dueNotesCount).toEqual(0);
+            expect(osrCore.noteReviewQueue.reviewDecks.size).toEqual(1);
+
+            // Single deck "#review", with single scheduled note "Triboelectric Effect.md",
+            const actual: NoteReviewDeck = osrCore.noteReviewQueue.reviewDecks.get("#review");
+            checkNoteReviewDeck_Basic(actual, {
+                deckName: "#review",
+                dueNotesCount: 0,
+                newNotesLength: 0,
+                scheduledNotesLength: 1,
+            });
+            checkScheduledNote(actual.scheduledNotes[0], {
+                filename: "Triboelectric Effect.md",
+                dueDate: "2025-02-21",
+            });
+        });
+    });
+
+    describe("Review New note (i.e. not previously reviewed); no questions present", () => {
+        test("New note without any backlinks", async () => {
+            const settings: SRSettings = { ...DEFAULT_SETTINGS };
+            const osrCore: UnitTestOsrCore = new UnitTestOsrCore(settings);
+            await osrCore.loadTestVault("notes1");
+
+            // Review the note
+            const file = osrCore.getFileByNoteName("Computation Graph");
+            await osrCore.saveNoteReviewResponse(file, ReviewResponse.Easy, settings);
+
+            // Check note frontmatter - 4 days after the simulated test date of 2023-09-06
+            const expectedDueDate: string = "2023-09-10";
+            unitTest_CheckNoteFrontmatter(file.content, expectedDueDate, 4, 270);
+        });
+
+        // The notes that have links to [[A]] themselves haven't been reviewed,
+        // So the expected post-review schedule is the same as if no files had links to [[A]]
+        test("Review note with some backlinks (source files without reviews)", async () => {
+            const settings: SRSettings = { ...DEFAULT_SETTINGS };
+            const osrCore: UnitTestOsrCore = new UnitTestOsrCore(settings);
+            await osrCore.loadTestVault("notes3");
+
+            // Review the note
+            const file = osrCore.getFileByNoteName("A");
+            await osrCore.saveNoteReviewResponse(file, ReviewResponse.Easy, settings);
+
+            // Check note frontmatter - 4 days after the simulated test date of 2023-09-06
+            const expectedDueDate: string = "2023-09-10";
+            unitTest_CheckNoteFrontmatter(file.content, expectedDueDate, 4, 270);
+        });
+
+        test("Review note with a backlink (one source file already reviewed)", async () => {
+            const settings: SRSettings = { ...DEFAULT_SETTINGS };
+            const osrCore: UnitTestOsrCore = new UnitTestOsrCore(settings);
+
+            // See: tests\vaults\readme.md
+            await osrCore.loadTestVault("notes4");
+
+            // Review note B
+            const file = osrCore.getFileByNoteName("B");
+            await osrCore.saveNoteReviewResponse(file, ReviewResponse.Easy, settings);
+
+            // Check note frontmatter - 4 days after the simulated test date of 2023-09-06
+            const expectedDueDate: string = "2023-09-10";
+            unitTest_CheckNoteFrontmatter(file.content, expectedDueDate, 4, 272);
+        });
+    });
+
+    describe("Review Old note (i.e. previously reviewed); no questions present", () => {
+        test("Review note with a backlink - Good", async () => {
+            const settings: SRSettings = { ...DEFAULT_SETTINGS };
+            const osrCore: UnitTestOsrCore = new UnitTestOsrCore(settings);
+
+            // See: tests/vaults/readme.md
+            // See: tests/vaults/notes4/readme.md
+            await osrCore.loadTestVault("notes4");
+
+            // Initial histogram
+            expect(osrCore.dueDateNoteHistogram.dueNotesCount).toEqual(0);
+            let expectedHistogram: NoteDueDateHistogram = new NoteDueDateHistogram({
+                4: 1,
+            });
+            expect(osrCore.dueDateNoteHistogram).toEqual(expectedHistogram);
+
+            // Review note A
+            const file = osrCore.getFileByNoteName("A");
+            await osrCore.saveNoteReviewResponse(file, ReviewResponse.Good, settings);
+
+            // Check note frontmatter - 11 days after the simulated test date of 2023-09-06
+            const expectedDueDate: string = "2023-09-17";
+            unitTest_CheckNoteFrontmatter(file.content, expectedDueDate, 11, 270);
+
+            expect(osrCore.dueDateNoteHistogram.dueNotesCount).toEqual(0);
+            expectedHistogram = new NoteDueDateHistogram({
+                11: 1,
+            });
+            expect(osrCore.dueDateNoteHistogram).toEqual(expectedHistogram);
+        });
+
+        test("Review note with a backlink - Hard", async () => {
+            const settings: SRSettings = { ...DEFAULT_SETTINGS };
+            const osrCore: UnitTestOsrCore = new UnitTestOsrCore(settings);
+
+            // See: tests/vaults/readme.md
+            // See: tests/vaults/notes4/readme.md
+            await osrCore.loadTestVault("notes4");
+
+            // Review note A
+            const file = osrCore.getFileByNoteName("A");
+            await osrCore.saveNoteReviewResponse(file, ReviewResponse.Hard, settings);
+
+            // Check note frontmatter - 2 days after the simulated test date of 2023-09-06
+            const expectedDueDate: string = "2023-09-08";
+            unitTest_CheckNoteFrontmatter(file.content, expectedDueDate, 2, 250);
+        });
+    });
+
+    describe("Review New note (i.e. not previously reviewed); questions present and some previously reviewed", () => {
+        test("New note without any backlinks", async () => {
+            const settings: SRSettings = { ...DEFAULT_SETTINGS };
+            const osrCore: UnitTestOsrCore = new UnitTestOsrCore(settings);
+            await osrCore.loadTestVault("notes1");
+
+            // Review the note
+            const file = osrCore.getFileByNoteName("Computation Graph");
+            await osrCore.saveNoteReviewResponse(file, ReviewResponse.Easy, settings);
+
+            // Check note frontmatter - 4 days after the simulated test date of 2023-09-06
+            const expectedDueDate: string = "2023-09-10";
+            unitTest_CheckNoteFrontmatter(file.content, expectedDueDate, 4, 270);
+        });
+
+        // The notes that have links to [[A]] themselves haven't been reviewed,
+        // So the expected post-review schedule is the same as if no files had links to [[A]]
+        test("Review note with some backlinks (source files without reviews)", async () => {
+            const settings: SRSettings = { ...DEFAULT_SETTINGS };
+            const osrCore: UnitTestOsrCore = new UnitTestOsrCore(settings);
+            await osrCore.loadTestVault("notes3");
+
+            // Review the note
+            const file = osrCore.getFileByNoteName("A");
+            await osrCore.saveNoteReviewResponse(file, ReviewResponse.Easy, settings);
+
+            // Check note frontmatter - 4 days after the simulated test date of 2023-09-06
+            const expectedDueDate: string = "2023-09-10";
+            unitTest_CheckNoteFrontmatter(file.content, expectedDueDate, 4, 270);
+        });
+
+        test("Review note with a backlink (one source file already reviewed)", async () => {
+            const settings: SRSettings = { ...DEFAULT_SETTINGS };
+            const osrCore: UnitTestOsrCore = new UnitTestOsrCore(settings);
+
+            // See: tests\vaults\readme.md
+            await osrCore.loadTestVault("notes4");
+
+            // Review note B
+            const file = osrCore.getFileByNoteName("B");
+            await osrCore.saveNoteReviewResponse(file, ReviewResponse.Easy, settings);
+
+            // Check note frontmatter - 4 days after the simulated test date of 2023-09-06
+            const expectedDueDate: string = "2023-09-10";
+            unitTest_CheckNoteFrontmatter(file.content, expectedDueDate, 4, 272);
+        });
+    });
+
+    describe("loadNote", () => {
+        test("There is schedule info for 3 cards, but only 2 cards in the question", async () => {
+            const settings: SRSettings = { ...DEFAULT_SETTINGS };
+            settings.cardCommentOnSameLine = true;
+            settings.convertCurlyBracketsToClozes = true;
+            const osrCore: UnitTestOsrCore = new UnitTestOsrCore(settings);
+            await osrCore.loadTestVault("notes6");
+
+            /* 
+A {{question}} with multiple parts {{Navevo part}}
+<!--SR:!2024-05-22,1,230!2024-05-25,4,270!2033-03-03,3,333-->
+The final schedule info "!2033-03-03,3,333" has been deleted
+             */
+            const file = osrCore.getFileByNoteName("A");
+            expect(file.content).toContain("<!--SR:!2024-05-22,1,230!2024-05-25,4,270-->");
+        });
+    });
+});
+
+describe("Note Due Date Histogram", () => {
+    test("New note", async () => {
+        const settings: SRSettings = { ...DEFAULT_SETTINGS };
+        const osrCore: UnitTestOsrCore = new UnitTestOsrCore(settings);
+        await osrCore.loadTestVault("notes1");
+
+        // Initial status
+        expect(osrCore.dueDateNoteHistogram.dueNotesCount).toEqual(0);
+
+        // Review the note
+        const file = osrCore.getFileByNoteName("Computation Graph");
+        await osrCore.saveNoteReviewResponse(file, ReviewResponse.Easy, settings);
+
+        // Check histogram - in 4 days there is one card due
+        expect(osrCore.dueDateNoteHistogram.dueNotesCount).toEqual(0);
+        const expectedHistogram: NoteDueDateHistogram = new NoteDueDateHistogram({
+            4: 1,
+        });
+        expect(osrCore.dueDateNoteHistogram).toEqual(expectedHistogram);
+    });
+
+    test("Review old note - Good", async () => {
+        const settings: SRSettings = { ...DEFAULT_SETTINGS };
+        const osrCore: UnitTestOsrCore = new UnitTestOsrCore(settings);
+
+        // See: tests/vaults/readme.md
+        // See: tests/vaults/notes4/readme.md
+        await osrCore.loadTestVault("notes4");
+
+        // Initial histogram
+        expect(osrCore.dueDateNoteHistogram.dueNotesCount).toEqual(0);
+        let expectedHistogram: NoteDueDateHistogram = new NoteDueDateHistogram({
+            4: 1,
+        });
+        expect(osrCore.dueDateNoteHistogram).toEqual(expectedHistogram);
+
+        // Review note A
+        const file = osrCore.getFileByNoteName("A");
+        await osrCore.saveNoteReviewResponse(file, ReviewResponse.Good, settings);
+
+        expect(osrCore.dueDateNoteHistogram.dueNotesCount).toEqual(0);
+        expectedHistogram = new NoteDueDateHistogram({
+            11: 1,
+        });
+        expect(osrCore.dueDateNoteHistogram).toEqual(expectedHistogram);
+    });
+
+    test("Review multiple notes", async () => {
+        const settings: SRSettings = { ...DEFAULT_SETTINGS };
+        const osrCore: UnitTestOsrCore = new UnitTestOsrCore(settings);
+
+        // See: tests/vaults/readme.md
+        // See: tests/vaults/notes4/readme.md
+        await osrCore.loadTestVault("notes4");
+
+        // Review all the notes
+        let file: ISRFile = osrCore.getFileByNoteName("A");
+        await osrCore.saveNoteReviewResponse(file, ReviewResponse.Good, settings);
+        file = osrCore.getFileByNoteName("B");
+        await osrCore.saveNoteReviewResponse(file, ReviewResponse.Hard, settings);
+        file = osrCore.getFileByNoteName("C");
+        await osrCore.saveNoteReviewResponse(file, ReviewResponse.Hard, settings);
+        file = osrCore.getFileByNoteName("D");
+        await osrCore.saveNoteReviewResponse(file, ReviewResponse.Good, settings);
+
+        expect(osrCore.dueDateNoteHistogram.dueNotesCount).toEqual(0);
+        let expectedHistogram: NoteDueDateHistogram = new NoteDueDateHistogram({
+            1: 2,
+            3: 1,
+            11: 1,
+        });
+        expect(osrCore.dueDateNoteHistogram).toEqual(expectedHistogram);
+    });
+});
+
+describe("Note review - bury all flashcards", () => {
+    test("burySiblingCards - false", async () => {
+        const settings: SRSettings = { ...DEFAULT_SETTINGS };
+        settings.burySiblingCards = false;
+        const osrCore: UnitTestOsrCore = new UnitTestOsrCore(settings);
+        await osrCore.loadTestVault("notes5");
+
+        // Nothing initially on the postponement list
+        expect(osrCore.questionPostponementList.list.length).toEqual(0);
+
+        // Review the note
+        const file = osrCore.getFileByNoteName("D");
+        await osrCore.saveNoteReviewResponse(file, ReviewResponse.Easy, settings);
+
+        // Because burySiblingCards is false, nothing has been added to the postponement list
+        expect(osrCore.questionPostponementList.list.length).toEqual(0);
+    });
+
+    test("burySiblingCards - true", async () => {
+        const settings: SRSettings = { ...DEFAULT_SETTINGS };
+        settings.burySiblingCards = true;
+        const osrCore: UnitTestOsrCore = new UnitTestOsrCore(settings);
+        await osrCore.loadTestVault("notes5");
+
+        // Nothing initially on the postponement list
+        expect(osrCore.questionPostponementList.list.length).toEqual(0);
+
+        // Review the note
+        const file = osrCore.getFileByNoteName("D");
+        await osrCore.saveNoteReviewResponse(file, ReviewResponse.Easy, settings);
+
+        // The two cards in note D have been added to the postponement list
+        expect(osrCore.questionPostponementList.list.length).toEqual(2);
+    });
+});
diff --git a/tests/unit/TopicPath.test.ts b/tests/unit/TopicPath.test.ts
index af17047..89d10b7 100644
--- a/tests/unit/TopicPath.test.ts
+++ b/tests/unit/TopicPath.test.ts
@@ -1,7 +1,7 @@
-import { ISRFile } from "src/SRFile";
 import { TopicPath, TopicPathList } from "src/TopicPath";
 import { DEFAULT_SETTINGS, SRSettings } from "src/settings";
 import { UnitTestSRFile } from "./helpers/UnitTestSRFile";
+import { ISRFile } from "src/SRFile";
 
 describe("Constructor exception handling", () => {
     test("Constructor rejects null path", () => {
diff --git a/tests/unit/algorithms/osr/RepItemScheduleInfo_Osr.test.ts b/tests/unit/algorithms/osr/RepItemScheduleInfo_Osr.test.ts
new file mode 100644
index 0000000..f28f35f
--- /dev/null
+++ b/tests/unit/algorithms/osr/RepItemScheduleInfo_Osr.test.ts
@@ -0,0 +1,29 @@
+import moment from "moment";
+import { RepItemScheduleInfo_Osr } from "src/algorithms/osr/RepItemScheduleInfo_Osr";
+import { TICKS_PER_DAY } from "src/constants";
+import { DEFAULT_SETTINGS } from "src/settings";
+
+describe("formatCardScheduleForHtmlComment", () => {
+    test("With due date", () => {
+        const repItem: RepItemScheduleInfo_Osr = RepItemScheduleInfo_Osr.fromDueDateStr(
+            "2023-09-02",
+            4,
+            270,
+            null,
+        );
+        expect(repItem.formatCardScheduleForHtmlComment()).toEqual("!2023-09-02,4,270");
+    });
+
+    test("Without due date", () => {
+        const repItem: RepItemScheduleInfo_Osr = new RepItemScheduleInfo_Osr(null, 5, 290, null);
+        expect(repItem.formatCardScheduleForHtmlComment()).toEqual("!2000-01-01,5,290");
+    });
+});
+
+test("getDummyScheduleForNewCard", () => {
+    const repItem: RepItemScheduleInfo_Osr =
+        RepItemScheduleInfo_Osr.getDummyScheduleForNewCard(DEFAULT_SETTINGS);
+    expect(repItem.interval).toEqual(1);
+    expect(repItem.latestEase).toEqual(250);
+    expect(repItem.dueDate.valueOf).toEqual(moment("2000-01-01").valueOf);
+});
diff --git a/tests/unit/dataStore/RepItemStorageInfo.test.ts b/tests/unit/dataStore/RepItemStorageInfo.test.ts
new file mode 100644
index 0000000..988beb3
--- /dev/null
+++ b/tests/unit/dataStore/RepItemStorageInfo.test.ts
@@ -0,0 +1,5 @@
+import { RepItemStorageInfo } from "src/dataStore/base/RepItemStorageInfo";
+
+test("Just to make code coverage analysis happy", () => {
+    const item = new RepItemStorageInfo();
+});
diff --git a/tests/unit/dataStore/dataStore.test.ts b/tests/unit/dataStore/dataStore.test.ts
new file mode 100644
index 0000000..dfcfb75
--- /dev/null
+++ b/tests/unit/dataStore/dataStore.test.ts
@@ -0,0 +1,8 @@
+import { DataStore } from "src/dataStore/base/DataStore";
+
+test("getInstance() not initialised exception", () => {
+    const t = () => {
+        DataStore.getInstance();
+    };
+    expect(t).toThrow(Error);
+});
diff --git a/tests/unit/dataStoreAlgorithm/DataStoreInNote_AlgorithmOsr.test.ts b/tests/unit/dataStoreAlgorithm/DataStoreInNote_AlgorithmOsr.test.ts
new file mode 100644
index 0000000..9133440
--- /dev/null
+++ b/tests/unit/dataStoreAlgorithm/DataStoreInNote_AlgorithmOsr.test.ts
@@ -0,0 +1,74 @@
+import { DataStoreInNote_AlgorithmOsr } from "src/dataStoreAlgorithm/DataStoreInNote_AlgorithmOsr";
+import { DEFAULT_SETTINGS, SRSettings } from "src/settings";
+import { UnitTestSRFile } from "../helpers/UnitTestSRFile";
+import { RepItemScheduleInfo } from "src/algorithms/base/RepItemScheduleInfo";
+import { RepItemScheduleInfo_Osr } from "src/algorithms/osr/RepItemScheduleInfo_Osr";
+import { setupStaticDateProvider_20230906 } from "src/util/DateProvider";
+import { Card } from "src/Card";
+
+beforeAll(() => {
+    setupStaticDateProvider_20230906();
+});
+
+describe("noteSetSchedule", () => {
+    test("File originally has frontmatter (but not OSR note scheduling frontmatter)", async () => {
+        const settings: SRSettings = { ...DEFAULT_SETTINGS };
+        const instance: DataStoreInNote_AlgorithmOsr = new DataStoreInNote_AlgorithmOsr(settings);
+
+        let noteText: string = `---
+created: 2024-01-17
+---
+A very interesting note
+`;
+        let file: UnitTestSRFile = new UnitTestSRFile(noteText);
+        const scheduleInfo: RepItemScheduleInfo_Osr = RepItemScheduleInfo_Osr.fromDueDateStr(
+            "2023-10-06",
+            25,
+            263,
+        );
+        await instance.noteSetSchedule(file, scheduleInfo);
+
+        const expectedText: string = `---
+created: 2024-01-17
+sr-due: 2023-10-06
+sr-interval: 25
+sr-ease: 263
+---
+A very interesting note
+`;
+        expect(file.content).toEqual(expectedText);
+    });
+});
+
+describe("formatCardSchedule", () => {
+    test("Has schedule, with due date", async () => {
+        const settings: SRSettings = { ...DEFAULT_SETTINGS };
+        const instance: DataStoreInNote_AlgorithmOsr = new DataStoreInNote_AlgorithmOsr(settings);
+
+        const scheduleInfo: RepItemScheduleInfo_Osr = RepItemScheduleInfo_Osr.fromDueDateStr(
+            "2023-10-06",
+            25,
+            263,
+        );
+        const card: Card = new Card({
+            scheduleInfo,
+        });
+        expect(instance.formatCardSchedule(card)).toEqual("!2023-10-06,25,263");
+    });
+
+    test("Has schedule, but no due date", async () => {
+        const settings: SRSettings = { ...DEFAULT_SETTINGS };
+        const instance: DataStoreInNote_AlgorithmOsr = new DataStoreInNote_AlgorithmOsr(settings);
+
+        const scheduleInfo: RepItemScheduleInfo_Osr = new RepItemScheduleInfo_Osr(
+            null,
+            25,
+            303,
+            null,
+        );
+        const card: Card = new Card({
+            scheduleInfo,
+        });
+        expect(instance.formatCardSchedule(card)).toEqual("!2000-01-01,25,303");
+    });
+});
diff --git a/tests/unit/deck.test.ts b/tests/unit/deck.test.ts
index 25e305c..011e5f2 100644
--- a/tests/unit/deck.test.ts
+++ b/tests/unit/deck.test.ts
@@ -2,6 +2,13 @@ import { CardListType, Deck } from "src/Deck";
 import { TopicPath } from "src/TopicPath";
 import { SampleItemDecks } from "./SampleItems";
 import { Card } from "src/Card";
+import { unitTestSetup_StandardDataStoreAlgorithm } from "./helpers/UnitTestSetup";
+import { NoteEaseList } from "src/NoteEaseList";
+import { DEFAULT_SETTINGS } from "src/settings";
+
+beforeAll(() => {
+    unitTestSetup_StandardDataStoreAlgorithm(DEFAULT_SETTINGS);
+});
 
 describe("constructor", () => {
     test("Deck name", () => {
diff --git a/tests/unit/helpers/UnitTestHelper.test.ts b/tests/unit/helpers/UnitTestHelper.test.ts
index 519c501..c487bec 100644
--- a/tests/unit/helpers/UnitTestHelper.test.ts
+++ b/tests/unit/helpers/UnitTestHelper.test.ts
@@ -1,5 +1,15 @@
 import { TagCache } from "obsidian";
-import { unitTest_GetAllTagsFromTextEx } from "./UnitTestHelper";
+import { unitTest_GetAllTagsFromTextEx, unitTest_ParseForOutgoingLinks } from "./UnitTestHelper";
+import { UnitTestOsrCore } from "./UnitTestOsrCore";
+import { DEFAULT_SETTINGS } from "src/settings";
+import { UnitTestLinkInfoFinder } from "./UnitTestLinkInfoFinder";
+import { unitTestSetup_StandardDataStoreAlgorithm } from "./UnitTestSetup";
+
+let linkInfoFinder: UnitTestLinkInfoFinder;
+
+beforeAll(() => {
+    unitTestSetup_StandardDataStoreAlgorithm(DEFAULT_SETTINGS);
+});
 
 describe("unitTest_GetAllTagsFromTextEx", () => {
     describe("Without frontmatter", () => {
@@ -54,6 +64,88 @@ This single {{question}} turns into {{3 separate}} {{cards}}
     });
 });
 
+describe("unitTest_ParseForOutgoingLinks", () => {
+    test("No outgoing links", () => {
+        const text: string = `
+The triboelectric effect describes electric charge transfer between two objects when they contact or slide against each other. 
+
+It can occur with different materials, such as:
+- the sole of a shoe on a carpet
+- balloon rubbing against sweater
+
+(also known as triboelectricity, triboelectric charging, triboelectrification, or tribocharging)
+`;
+        const links: string[] = unitTest_ParseForOutgoingLinks(text);
+        expect(links.length).toEqual(0);
+    });
+
+    test("Multiple outgoing links on different lines", () => {
+        const text: string = `
+The triboelectric effect describes electric charge [[transfer between]] two objects when they contact or slide against each other. 
+
+It can occur with different materials, such as:
+- the sole of a shoe on a carpet
+- balloon rubbing against sweater
+
+(also known as triboelectricity, triboelectric charging, [[triboelectrification]], or tribocharging)
+`;
+        const links: string[] = unitTest_ParseForOutgoingLinks(text);
+        const expected: string[] = ["transfer between", "triboelectrification"];
+        expect(links).toEqual(expected);
+    });
+
+    test("Multiple outgoing links on the one line", () => {
+        const text: string = `
+The triboelectric effect describes electric charge [[triboelectrification]], or [[tribocharging]])
+`;
+        const links: string[] = unitTest_ParseForOutgoingLinks(text);
+        const expected: string[] = ["triboelectrification", "tribocharging"];
+        expect(links).toEqual(expected);
+    });
+});
+
+function check_getResolvedLinks(linkName: string, expected: Map<string, number>): void {
+    let e: Record<string, number> = {};
+    expected.forEach((n, linkName) => {
+        const filename: string = linkInfoFinder.getFilenameForLink(linkName);
+        e[filename] = n;
+    });
+    expect(linkInfoFinder.getResolvedTargetLinksForNoteLink(linkName)).toEqual(e);
+}
+
+describe("UnitTestLinkInfoFinder", () => {
+    test("No outgoing links", async () => {
+        const osrCore: UnitTestOsrCore = new UnitTestOsrCore(DEFAULT_SETTINGS);
+        await osrCore.loadTestVault("notes3");
+        linkInfoFinder = new UnitTestLinkInfoFinder();
+        linkInfoFinder.init(osrCore.getFileMap());
+
+        // One link from A to each of B, C, D
+        check_getResolvedLinks(
+            "A",
+            new Map([
+                ["B", 1],
+                ["C", 1],
+                ["D", 1],
+            ]),
+        );
+
+        // No links from B
+        check_getResolvedLinks("B", new Map([]));
+
+        // One link from C to D
+        check_getResolvedLinks("C", new Map([["D", 1]]));
+
+        check_getResolvedLinks(
+            "D",
+            new Map([
+                ["A", 1],
+                ["B", 2],
+            ]),
+        );
+    });
+});
+
 function createTagCacheObj(tag: string, line: number): any {
     return {
         tag: tag,
diff --git a/tests/unit/helpers/UnitTestHelper.ts b/tests/unit/helpers/UnitTestHelper.ts
index 52f2bf0..1af05da 100644
--- a/tests/unit/helpers/UnitTestHelper.ts
+++ b/tests/unit/helpers/UnitTestHelper.ts
@@ -1,6 +1,6 @@
 import { TagCache } from "obsidian";
 import { frontmatterTagPseudoLineNum } from "src/SRFile";
-import { extractFrontmatter, splitTextIntoLineArray } from "src/util/utils";
+import { splitNoteIntoFrontmatterAndContent, splitTextIntoLineArray } from "src/util/utils";
 
 export function unitTest_CreateTagCache(tag: string, lineNum: number): TagCache {
     return {
@@ -13,10 +13,11 @@ export function unitTest_CreateTagCache(tag: string, lineNum: number): TagCache
 }
 
 export function unitTest_GetAllTagsFromTextEx(text: string): TagCache[] {
-    const [frontmatter, content] = extractFrontmatter(text);
+    const [frontmatter, content] = splitNoteIntoFrontmatterAndContent(text);
     const result = [] as TagCache[];
     let lines: string[];
 
+    const map: Map<string, string[]> = unitTest_BasicFrontmatterParserEx(text);
     if (frontmatter) {
         const dataPrefix: string = "  - ";
         lines = splitTextIntoLineArray(frontmatter);
@@ -56,3 +57,87 @@ export function unitTest_GetAllTagsFromTextEx(text: string): TagCache[] {
     }
     return result;
 }
+
+export function unitTest_GetAllTagsFromText(text: string): string[] {
+    const tagRegex = /#[^\s#]+/gi;
+    const result: RegExpMatchArray = text.match(tagRegex);
+    if (!result) return [];
+    return result;
+}
+
+export function unitTest_BasicFrontmatterParser(text: string): Map<string, string> {
+    const result = new Map<string, string>();
+    const map: Map<string, string[]> = unitTest_BasicFrontmatterParserEx(text);
+    map.forEach((value, key) => {
+        result.set(key, value.pop());
+    });
+    return result;
+}
+
+export function unitTest_BasicFrontmatterParserEx(text: string): Map<string, string[]> {
+    const [frontmatter, _] = splitNoteIntoFrontmatterAndContent(text);
+    const result = new Map<string, string[]>();
+
+    if (!frontmatter) return result;
+
+    const keyRegex = /^([A-Za-z0-9_-]+):(.*)$/;
+    const dataRegex = /^(\s+)-\s+(.+)$/;
+    const lines: string[] = splitTextIntoLineArray(frontmatter);
+    let keyName: string = null;
+    let valueList: string[] = [] as string[];
+
+    for (let i = 0; i < lines.length; i++) {
+        const line: string = lines[i];
+
+        // Is there a key, and optional value?
+        const keyMatch: RegExpMatchArray = line.match(keyRegex);
+        if (keyMatch) {
+            if (keyName) {
+                result.set(keyName, valueList);
+            }
+            keyName = keyMatch[1];
+            valueList = [] as string[];
+            const value = keyMatch[2].trim();
+            if (value) {
+                valueList.push(value);
+            }
+        } else {
+            // Just a value, related to the last key
+            const dataMatch: RegExpMatchArray = line.match(dataRegex);
+            if (keyName && dataMatch) {
+                const value = dataMatch[1].trim();
+                if (value) {
+                    valueList.push(value);
+                }
+            }
+        }
+    }
+    if (keyName) {
+        result.set(keyName, valueList);
+    }
+    return result;
+}
+
+export function unitTest_ParseForOutgoingLinks(text: string): string[] {
+    const linkRegex = /\[\[([\w\s]+)\]\]+/gi;
+    const matches = text.matchAll(linkRegex);
+    const result: string[] = [] as string[];
+    for (const m of matches) {
+        result.push(m[1]);
+    }
+    return result;
+}
+
+export function unitTest_CheckNoteFrontmatter(
+    text: string,
+    expectedDueDate: string,
+    expectedInterval: number,
+    expectedEase: number,
+): void {
+    const frontmatter: Map<string, string> = unitTest_BasicFrontmatterParser(text);
+
+    expect(frontmatter).toBeTruthy();
+    expect(frontmatter.get("sr-due")).toEqual(expectedDueDate);
+    expect(frontmatter.get("sr-interval")).toEqual(expectedInterval + "");
+    expect(frontmatter.get("sr-ease")).toEqual(expectedEase + "");
+}
diff --git a/tests/unit/helpers/UnitTestLinkInfoFinder.ts b/tests/unit/helpers/UnitTestLinkInfoFinder.ts
new file mode 100644
index 0000000..733649c
--- /dev/null
+++ b/tests/unit/helpers/UnitTestLinkInfoFinder.ts
@@ -0,0 +1,68 @@
+import { UnitTestSRFile } from "./UnitTestSRFile";
+import { unitTest_ParseForOutgoingLinks } from "./UnitTestHelper";
+import path from "path";
+import { IOsrVaultNoteLinkInfoFinder } from "src/algorithms/osr/ObsidianVaultNoteLinkInfoFinder";
+
+export class UnitTestLinkInfoFinder implements IOsrVaultNoteLinkInfoFinder {
+    private linkPathMap: Map<string, string>;
+    // Key: sourceFilename
+    // Value: Map<targetFilename, linkCount>
+    //      This is the number of links from sourceFilename to targetFilename
+    //      For simplicity, we just store the filename without the directory or filename extension
+    private outgoingLinks: Map<string, Map<string, number>>;
+
+    init(fileMap: Map<string, UnitTestSRFile>) {
+        // We first need to generate a map between the link names (e.g. the "A" in "[[A]]"), and it's file path)
+        this.linkPathMap = new Map<string, string>();
+        fileMap.forEach((_, filePath) => {
+            this.linkPathMap.set(path.parse(filePath).name, filePath);
+        });
+
+        //
+        this.outgoingLinks = new Map<string, Map<string, number>>();
+        fileMap.forEach((file, sourceFilename) => {
+            // Find all the (outgoing) links present in the file
+            const outgoingLinks2: string[] = unitTest_ParseForOutgoingLinks(file.content);
+
+            for (const targetLink of outgoingLinks2) {
+                const targetFilename: string = this.linkPathMap.get(targetLink);
+                this.incrementOutgoingLinksCount(sourceFilename, targetFilename);
+            }
+        });
+    }
+
+    private incrementOutgoingLinksCount(sourceFilename: string, targetFilename: string): void {
+        // Just the filename without the directory or filename extension
+        // sourceFilename = ;
+
+        if (!this.outgoingLinks.has(sourceFilename)) {
+            this.outgoingLinks.set(sourceFilename, new Map<string, number>());
+        }
+        const rec = this.outgoingLinks.get(sourceFilename);
+        if (!rec.has(targetFilename)) {
+            rec.set(targetFilename, 0);
+        }
+
+        rec.set(targetFilename, rec.get(targetFilename) + 1);
+    }
+
+    getFilenameForLink(linkName: string): string {
+        return this.linkPathMap.get(linkName);
+    }
+
+    getResolvedTargetLinksForNoteLink(linkName: string): Record<string, number> {
+        const filename = this.linkPathMap.get(linkName);
+        return this.getResolvedTargetLinksForNotePath(filename);
+    }
+
+    getResolvedTargetLinksForNotePath(sourcePath: string): Record<string, number> {
+        let result: Record<string, number> = {};
+        if (this.outgoingLinks.has(sourcePath)) {
+            const rec = this.outgoingLinks.get(sourcePath);
+            rec.forEach((n, filename) => {
+                result[filename] = n;
+            });
+        }
+        return result;
+    }
+}
diff --git a/tests/unit/helpers/UnitTestOsrCore.ts b/tests/unit/helpers/UnitTestOsrCore.ts
new file mode 100644
index 0000000..2481932
--- /dev/null
+++ b/tests/unit/helpers/UnitTestOsrCore.ts
@@ -0,0 +1,69 @@
+import * as fs from "fs";
+import * as path from "path";
+import { OsrCore } from "src/OsrCore";
+import { QuestionPostponementList } from "src/QuestionPostponementList";
+import { SRSettings } from "src/settings";
+import { UnitTestSRFile } from "./UnitTestSRFile";
+import { UnitTestLinkInfoFinder } from "./UnitTestLinkInfoFinder";
+
+export class UnitTestOsrCore extends OsrCore {
+    private buryList: string[];
+    // Key: Path
+    // Value: File content
+    private fileMap: Map<string, UnitTestSRFile>;
+    private infoFinder: UnitTestLinkInfoFinder;
+
+    constructor(settings: SRSettings) {
+        super();
+        this.buryList = [] as string[];
+        this.infoFinder = new UnitTestLinkInfoFinder();
+        const questionPostponementList = new QuestionPostponementList(
+            null,
+            settings,
+            this.buryList,
+        );
+        this.init(questionPostponementList, this.infoFinder, settings, () => {});
+    }
+
+    // Needed for unit testing: Setup fileMap and the link "info finder"
+    initializeFileMap(dir: string, files: string[]): void {
+        this.fileMap = new Map<string, UnitTestSRFile>();
+
+        for (const filename of files) {
+            const fullPath: string = path.join(dir, filename);
+            const f: UnitTestSRFile = UnitTestSRFile.CreateFromFsFile(fullPath);
+            this.fileMap.set(fullPath, f);
+        }
+
+        // Analyse the links between the notes before calling  processFile() finaliseLoad()
+        this.infoFinder.init(this.fileMap);
+    }
+
+    async loadTestVault(vaultSubfolder: string): Promise<void> {
+        this.loadInit();
+
+        const dir: string = path.join(__dirname, "..", "..", "vaults", vaultSubfolder);
+        const files: string[] = fs.readdirSync(dir).filter((f) => f != ".obsidian");
+
+        // Pass 1
+        this.initializeFileMap(dir, files);
+
+        // Pass 2: Process all files
+        for (const filename of files) {
+            const fullPath: string = path.join(dir, filename);
+            const f: UnitTestSRFile = this.fileMap.get(fullPath);
+            await this.processFile(f);
+        }
+
+        this.finaliseLoad();
+    }
+
+    getFileByNoteName(noteName: string): UnitTestSRFile {
+        const filename: string = this.infoFinder.getFilenameForLink(noteName);
+        return this.fileMap.get(filename);
+    }
+
+    getFileMap(): Map<string, UnitTestSRFile> {
+        return this.fileMap;
+    }
+}
diff --git a/tests/unit/helpers/UnitTestSRFile.ts b/tests/unit/helpers/UnitTestSRFile.ts
index 4c49849..306d8bc 100644
--- a/tests/unit/helpers/UnitTestSRFile.ts
+++ b/tests/unit/helpers/UnitTestSRFile.ts
@@ -1,6 +1,7 @@
-import { TagCache } from "obsidian";
+import * as fs from "fs";
+import { unitTest_BasicFrontmatterParser, unitTest_GetAllTagsFromTextEx } from "./UnitTestHelper";
+import { TFile, TagCache } from "obsidian";
 import { ISRFile } from "src/SRFile";
-import { unitTest_GetAllTagsFromTextEx } from "./UnitTestHelper";
 import { TextDirection } from "src/util/TextDirection";
 
 export class UnitTestSRFile implements ISRFile {
@@ -20,6 +21,14 @@ export class UnitTestSRFile implements ISRFile {
         return "";
     }
 
+    get tfile(): TFile {
+        throw "Not supported";
+    }
+
+    async getFrontmatter(): Promise<Map<string, string>> {
+        return unitTest_BasicFrontmatterParser(await this.read());
+    }
+
     getAllTagsFromCache(): string[] {
         return unitTest_GetAllTagsFromTextEx(this.content).map((item) => item.tag);
     }
@@ -44,4 +53,9 @@ export class UnitTestSRFile implements ISRFile {
     async write(content: string): Promise<void> {
         this.content = content;
     }
+
+    static CreateFromFsFile(path: string): UnitTestSRFile {
+        const content: string = fs.readFileSync(path, "utf8");
+        return new UnitTestSRFile(content, path);
+    }
 }
diff --git a/tests/unit/helpers/UnitTestSetup.ts b/tests/unit/helpers/UnitTestSetup.ts
new file mode 100644
index 0000000..8931871
--- /dev/null
+++ b/tests/unit/helpers/UnitTestSetup.ts
@@ -0,0 +1,13 @@
+import { SrsAlgorithm } from "src/algorithms/base/SrsAlgorithm";
+import { SrsAlgorithm_Osr } from "src/algorithms/osr/SrsAlgorithm_Osr";
+import { DataStore } from "src/dataStore/base/DataStore";
+import { DataStore_StoreInNote } from "src/dataStore/storeInNote/DataStore_StoreInNote";
+import { DataStoreAlgorithm } from "src/dataStoreAlgorithm/DataStoreAlgorithm";
+import { DataStoreInNote_AlgorithmOsr } from "src/dataStoreAlgorithm/DataStoreInNote_AlgorithmOsr";
+import { SRSettings } from "src/settings";
+
+export function unitTestSetup_StandardDataStoreAlgorithm(settings: SRSettings) {
+    DataStore.instance = new DataStore_StoreInNote(settings);
+    SrsAlgorithm.instance = new SrsAlgorithm_Osr(settings);
+    DataStoreAlgorithm.instance = new DataStoreInNote_AlgorithmOsr(settings);
+}
diff --git a/tests/unit/parser.test.ts b/tests/unit/parser.test.ts
index 3e7d1ca..3f32bca 100644
--- a/tests/unit/parser.test.ts
+++ b/tests/unit/parser.test.ts
@@ -18,10 +18,7 @@ const parserOptions: ParserOptions = {
  * Created when the actual parser changed from returning [CardType, string, number, number] to ParsedQuestionInfo.
  * It's purpose is to minimise changes to all the test cases here during the parser()->parserEx() change.
  */
-function parse(
-    text: string,
-    options: ParserOptions,
-): [CardType, string, number, number][] {
+function parse(text: string, options: ParserOptions): [CardType, string, number, number][] {
     // for testing purposes, generate parser each time, overwriting the default one
 
     const list: ParsedQuestionInfo[] = parseEx(text, options);
@@ -98,19 +95,8 @@ test("Test parsing of multi line basic cards", () => {
     expect(parse("#flashcards/tag-on-previous-line\nQuestion\n?\nAnswer", parserOptions)).toEqual([
         [CardType.MultiLineBasic, "#flashcards/tag-on-previous-line\nQuestion\n?\nAnswer", 0, 3],
     ]);
-    expect(parse("Question\n?\nAnswer line 1\nAnswer line 2\n\n---", {
-            singleLineCardSeparator: "::",
-            singleLineReversedCardSeparator: ":::",
-            multilineCardSeparator: "?",
-            multilineReversedCardSeparator: "??",
-            multilineCardEndMarker: "---",
-            convertHighlightsToClozes: false,
-            convertBoldTextToClozes: true,
-            convertCurlyBracketsToClozes: false,
-        })).toEqual([
-        [CardType.MultiLineBasic, "Question\n?\nAnswer line 1\nAnswer line 2", 0, 4],
-    ]);
-    expect(parse("Question 1\n?\nAnswer line 1\nAnswer line 2\n\n---\nQuestion 2\n?\nAnswer line 1\nAnswer line 2\n---\n", {
+    expect(
+        parse("Question\n?\nAnswer line 1\nAnswer line 2\n\n---", {
             singleLineCardSeparator: "::",
             singleLineReversedCardSeparator: ":::",
             multilineCardSeparator: "?",
@@ -119,22 +105,48 @@ test("Test parsing of multi line basic cards", () => {
             convertHighlightsToClozes: false,
             convertBoldTextToClozes: true,
             convertCurlyBracketsToClozes: false,
-        })).toEqual([
+        }),
+    ).toEqual([[CardType.MultiLineBasic, "Question\n?\nAnswer line 1\nAnswer line 2", 0, 4]]);
+    expect(
+        parse(
+            "Question 1\n?\nAnswer line 1\nAnswer line 2\n\n---\nQuestion 2\n?\nAnswer line 1\nAnswer line 2\n---\n",
+            {
+                singleLineCardSeparator: "::",
+                singleLineReversedCardSeparator: ":::",
+                multilineCardSeparator: "?",
+                multilineReversedCardSeparator: "??",
+                multilineCardEndMarker: "---",
+                convertHighlightsToClozes: false,
+                convertBoldTextToClozes: true,
+                convertCurlyBracketsToClozes: false,
+            },
+        ),
+    ).toEqual([
         [CardType.MultiLineBasic, "Question 1\n?\nAnswer line 1\nAnswer line 2", 0, 4],
-        [CardType.MultiLineBasic, "Question 2\n?\nAnswer line 1\nAnswer line 2", 6, 9]
+        [CardType.MultiLineBasic, "Question 2\n?\nAnswer line 1\nAnswer line 2", 6, 9],
     ]);
-    expect(parse("Question 1\n?\nAnswer line 1\nAnswer line 2\n\n---\nQuestion with empty line after question mark\n?\n\nAnswer line 1\nAnswer line 2\n---\n", {
-            singleLineCardSeparator: "::",
-            singleLineReversedCardSeparator: ":::",
-            multilineCardSeparator: "?",
-            multilineReversedCardSeparator: "??",
-            multilineCardEndMarker: "---",
-            convertHighlightsToClozes: false,
-            convertBoldTextToClozes: true,
-            convertCurlyBracketsToClozes: false,
-        })).toEqual([
+    expect(
+        parse(
+            "Question 1\n?\nAnswer line 1\nAnswer line 2\n\n---\nQuestion with empty line after question mark\n?\n\nAnswer line 1\nAnswer line 2\n---\n",
+            {
+                singleLineCardSeparator: "::",
+                singleLineReversedCardSeparator: ":::",
+                multilineCardSeparator: "?",
+                multilineReversedCardSeparator: "??",
+                multilineCardEndMarker: "---",
+                convertHighlightsToClozes: false,
+                convertBoldTextToClozes: true,
+                convertCurlyBracketsToClozes: false,
+            },
+        ),
+    ).toEqual([
         [CardType.MultiLineBasic, "Question 1\n?\nAnswer line 1\nAnswer line 2", 0, 4],
-        [CardType.MultiLineBasic, "Question with empty line after question mark\n?\n\nAnswer line 1\nAnswer line 2", 6, 10]
+        [
+            CardType.MultiLineBasic,
+            "Question with empty line after question mark\n?\n\nAnswer line 1\nAnswer line 2",
+            6,
+            10,
+        ],
     ]);
 });
 
@@ -148,8 +160,7 @@ test("Test parsing of multi line reversed cards", () => {
     expect(parse("Question\n??\nAnswer line 1\nAnswer line 2", parserOptions)).toEqual([
         [CardType.MultiLineReversed, "Question\n??\nAnswer line 1\nAnswer line 2", 0, 3],
     ]);
-    expect(parse("#Title\n\nLine0\nQ1\n??\nA1\nAnswerExtra\n\nQ2\n??\nA2", parserOptions)).toEqual(
-        [
+    expect(parse("#Title\n\nLine0\nQ1\n??\nA1\nAnswerExtra\n\nQ2\n??\nA2", parserOptions)).toEqual([
         [
             CardType.MultiLineReversed,
             "Line0\nQ1\n??\nA1\nAnswerExtra",
@@ -157,21 +168,9 @@ test("Test parsing of multi line reversed cards", () => {
             /* AnswerExtra */ 6,
         ],
         [CardType.MultiLineReversed, "Q2\n??\nA2", 8, 10],
-        ],
-    );
-    expect(parse("Question\n??\nAnswer line 1\nAnswer line 2\n\n---", {
-            singleLineCardSeparator: "::",
-            singleLineReversedCardSeparator: ":::",
-            multilineCardSeparator: "?",
-            multilineReversedCardSeparator: "??",
-            multilineCardEndMarker: "---",
-            convertHighlightsToClozes: true,
-            convertBoldTextToClozes: true,
-            convertCurlyBracketsToClozes: true,
-        })).toEqual([
-        [CardType.MultiLineReversed, "Question\n??\nAnswer line 1\nAnswer line 2", 0, 4],
     ]);
-    expect(parse("Question 1\n?\nAnswer line 1\nAnswer line 2\n\n---\nQuestion 2\n??\nAnswer line 1\nAnswer line 2\n---\n", {
+    expect(
+        parse("Question\n??\nAnswer line 1\nAnswer line 2\n\n---", {
             singleLineCardSeparator: "::",
             singleLineReversedCardSeparator: ":::",
             multilineCardSeparator: "?",
@@ -180,9 +179,25 @@ test("Test parsing of multi line reversed cards", () => {
             convertHighlightsToClozes: true,
             convertBoldTextToClozes: true,
             convertCurlyBracketsToClozes: true,
-        })).toEqual([
+        }),
+    ).toEqual([[CardType.MultiLineReversed, "Question\n??\nAnswer line 1\nAnswer line 2", 0, 4]]);
+    expect(
+        parse(
+            "Question 1\n?\nAnswer line 1\nAnswer line 2\n\n---\nQuestion 2\n??\nAnswer line 1\nAnswer line 2\n---\n",
+            {
+                singleLineCardSeparator: "::",
+                singleLineReversedCardSeparator: ":::",
+                multilineCardSeparator: "?",
+                multilineReversedCardSeparator: "??",
+                multilineCardEndMarker: "---",
+                convertHighlightsToClozes: true,
+                convertBoldTextToClozes: true,
+                convertCurlyBracketsToClozes: true,
+            },
+        ),
+    ).toEqual([
         [CardType.MultiLineBasic, "Question 1\n?\nAnswer line 1\nAnswer line 2", 0, 4],
-        [CardType.MultiLineReversed, "Question 2\n??\nAnswer line 1\nAnswer line 2", 6, 9]
+        [CardType.MultiLineReversed, "Question 2\n??\nAnswer line 1\nAnswer line 2", 6, 9],
     ]);
 });
 
@@ -214,7 +229,8 @@ test("Test parsing of cloze cards", () => {
     expect(parse("lorem ipsum ==p\ndolor won==", parserOptions)).toEqual([]);
     expect(parse("lorem ipsum ==dolor won=", parserOptions)).toEqual([]);
     // ==highlights== turned off
-    expect(parse("cloze ==deletion== test",{
+    expect(
+        parse("cloze ==deletion== test", {
             singleLineCardSeparator: "::",
             singleLineReversedCardSeparator: ":::",
             multilineCardSeparator: "?",
@@ -223,9 +239,8 @@ test("Test parsing of cloze cards", () => {
             convertHighlightsToClozes: false,
             convertBoldTextToClozes: true,
             convertCurlyBracketsToClozes: false,
-        })).toEqual(
-        [],
-    );
+        }),
+    ).toEqual([]);
 
     // **bolded**
     expect(parse("cloze **deletion** test", parserOptions)).toEqual([
@@ -254,7 +269,8 @@ test("Test parsing of cloze cards", () => {
     expect(parse("lorem ipsum **p\ndolor won**", parserOptions)).toEqual([]);
     expect(parse("lorem ipsum **dolor won*", parserOptions)).toEqual([]);
     // **bolded** turned off
-    expect(parse("cloze **deletion** test", {
+    expect(
+        parse("cloze **deletion** test", {
             singleLineCardSeparator: "::",
             singleLineReversedCardSeparator: ":::",
             multilineCardSeparator: "?",
@@ -263,27 +279,41 @@ test("Test parsing of cloze cards", () => {
             convertHighlightsToClozes: true,
             convertBoldTextToClozes: false,
             convertCurlyBracketsToClozes: false,
-        })).toEqual(
-        [],
-    );
+        }),
+    ).toEqual([]);
 
     // both
     expect(parse("cloze **deletion** test ==another deletion==!", parserOptions)).toEqual([
         [CardType.Cloze, "cloze **deletion** test ==another deletion==!", 0, 0],
     ]);
 
-    expect(parse("Test 1\nTest 2\nThis is a close with ===secret=== text.\nWith this extra lines\n\nAnd more here.\nAnd even more.\n\n---\n\nTest 3\nTest 4\nThis is a close with ===super secret=== text.\nWith this extra lines\n\nAnd more here.\nAnd even more.\n\n---\n\nHere is some more text.", {
-            singleLineCardSeparator: "::",
-            singleLineReversedCardSeparator: ":::",
-            multilineCardSeparator: "?",
-            multilineReversedCardSeparator: "??",
-            multilineCardEndMarker: "---",
-            convertHighlightsToClozes: true,
-            convertBoldTextToClozes: false,
-            convertCurlyBracketsToClozes: false,
-        })).toEqual([
-        [CardType.Cloze, "Test 1\nTest 2\nThis is a close with ===secret=== text.\nWith this extra lines\n\nAnd more here.\nAnd even more.", 0, 7],
-        [CardType.Cloze, "Test 3\nTest 4\nThis is a close with ===super secret=== text.\nWith this extra lines\n\nAnd more here.\nAnd even more.", 10, 17]
+    expect(
+        parse(
+            "Test 1\nTest 2\nThis is a close with ===secret=== text.\nWith this extra lines\n\nAnd more here.\nAnd even more.\n\n---\n\nTest 3\nTest 4\nThis is a close with ===super secret=== text.\nWith this extra lines\n\nAnd more here.\nAnd even more.\n\n---\n\nHere is some more text.",
+            {
+                singleLineCardSeparator: "::",
+                singleLineReversedCardSeparator: ":::",
+                multilineCardSeparator: "?",
+                multilineReversedCardSeparator: "??",
+                multilineCardEndMarker: "---",
+                convertHighlightsToClozes: true,
+                convertBoldTextToClozes: false,
+                convertCurlyBracketsToClozes: false,
+            },
+        ),
+    ).toEqual([
+        [
+            CardType.Cloze,
+            "Test 1\nTest 2\nThis is a close with ===secret=== text.\nWith this extra lines\n\nAnd more here.\nAnd even more.",
+            0,
+            7,
+        ],
+        [
+            CardType.Cloze,
+            "Test 3\nTest 4\nThis is a close with ===super secret=== text.\nWith this extra lines\n\nAnd more here.\nAnd even more.",
+            10,
+            17,
+        ],
     ]);
 });
 
diff --git a/tests/unit/scheduling.test.ts b/tests/unit/scheduling.test.ts
index 36152e3..6cd9f47 100644
--- a/tests/unit/scheduling.test.ts
+++ b/tests/unit/scheduling.test.ts
@@ -1,23 +1,48 @@
-import { schedule, ReviewResponse, textInterval } from "src/scheduling";
+import { DueDateHistogram } from "src/DueDateHistogram";
+import { ReviewResponse } from "src/algorithms/base/RepetitionItem";
+import { osrSchedule, textInterval } from "src/algorithms/osr/NoteScheduling";
 import { DEFAULT_SETTINGS } from "src/settings";
 
+const emptyHistogram = new DueDateHistogram();
+
 test("Test reviewing with default settings", () => {
     expect(
-        schedule(ReviewResponse.Easy, 1, DEFAULT_SETTINGS.baseEase, 0, DEFAULT_SETTINGS, {}),
+        osrSchedule(
+            ReviewResponse.Easy,
+            1,
+            DEFAULT_SETTINGS.baseEase,
+            0,
+            DEFAULT_SETTINGS,
+            emptyHistogram,
+        ),
     ).toEqual({
         ease: DEFAULT_SETTINGS.baseEase + 20,
         interval: 4,
     });
 
     expect(
-        schedule(ReviewResponse.Good, 1, DEFAULT_SETTINGS.baseEase, 0, DEFAULT_SETTINGS, {}),
+        osrSchedule(
+            ReviewResponse.Good,
+            1,
+            DEFAULT_SETTINGS.baseEase,
+            0,
+            DEFAULT_SETTINGS,
+            emptyHistogram,
+        ),
     ).toEqual({
         ease: DEFAULT_SETTINGS.baseEase,
         interval: 3,
     });
 
     expect(
-        schedule(ReviewResponse.Hard, 1, DEFAULT_SETTINGS.baseEase, 0, DEFAULT_SETTINGS, {}),
+        osrSchedule(
+            ReviewResponse.Hard,
+            1,
+            DEFAULT_SETTINGS.baseEase,
+            0,
+            DEFAULT_SETTINGS,
+            emptyHistogram,
+        ),
     ).toEqual({
         ease: DEFAULT_SETTINGS.baseEase - 20,
         interval: 1,
@@ -27,21 +52,42 @@ test("Test reviewing with default settings", () => {
 test("Test reviewing with default settings & delay", () => {
     const delay = 2 * 24 * 3600 * 1000; // two day delay
     expect(
-        schedule(ReviewResponse.Easy, 10, DEFAULT_SETTINGS.baseEase, delay, DEFAULT_SETTINGS, {}),
+        osrSchedule(
+            ReviewResponse.Easy,
+            10,
+            DEFAULT_SETTINGS.baseEase,
+            delay,
+            DEFAULT_SETTINGS,
+            emptyHistogram,
+        ),
     ).toEqual({
         ease: DEFAULT_SETTINGS.baseEase + 20,
         interval: 42,
     });
 
     expect(
-        schedule(ReviewResponse.Good, 10, DEFAULT_SETTINGS.baseEase, delay, DEFAULT_SETTINGS, {}),
+        osrSchedule(
+            ReviewResponse.Good,
+            10,
+            DEFAULT_SETTINGS.baseEase,
+            delay,
+            DEFAULT_SETTINGS,
+            emptyHistogram,
+        ),
     ).toEqual({
         ease: DEFAULT_SETTINGS.baseEase,
         interval: 28,
     });
 
     expect(
-        schedule(ReviewResponse.Hard, 10, DEFAULT_SETTINGS.baseEase, delay, DEFAULT_SETTINGS, {}),
+        osrSchedule(
+            ReviewResponse.Hard,
+            10,
+            DEFAULT_SETTINGS.baseEase,
+            delay,
+            DEFAULT_SETTINGS,
+            emptyHistogram,
+        ),
     ).toEqual({
         ease: DEFAULT_SETTINGS.baseEase - 20,
         interval: 5,
@@ -49,59 +95,78 @@ test("Test reviewing with default settings & delay", () => {
 });
 
 test("Test load balancing, small interval (load balancing disabled)", () => {
-    const dueDates = {
+    const originalInterval: number = 1;
+    const newInterval: number = 3;
+    const dueDates = new DueDateHistogram({
         0: 1,
-        1: 1,
+        1: 1, // key = originalInterval
         2: 1,
         3: 4,
-    };
+    });
     expect(
-        schedule(ReviewResponse.Good, 1, DEFAULT_SETTINGS.baseEase, 0, DEFAULT_SETTINGS, dueDates),
+        osrSchedule(
+            ReviewResponse.Good,
+            1,
+            DEFAULT_SETTINGS.baseEase,
+            0,
+            DEFAULT_SETTINGS,
+            dueDates,
+        ),
     ).toEqual({
         ease: DEFAULT_SETTINGS.baseEase,
-        interval: 3,
-    });
-    expect(dueDates).toEqual({
-        0: 1,
-        1: 1,
-        2: 1,
-        3: 5,
+        interval: newInterval,
     });
+    dueDates.decrement(originalInterval);
+    dueDates.increment(newInterval);
+    expect(dueDates).toEqual(
+        new DueDateHistogram({
+            0: 1,
+            1: 0, // One less than before
+            2: 1,
+            3: 5, // One more than before
+        }),
+    );
 });
 
 test("Test load balancing", () => {
     // interval < 7
-    let dueDates: Record<number, number> = {
+    let dueDates = new DueDateHistogram({
         5: 2,
-    };
+    });
     expect(
-        schedule(ReviewResponse.Good, 2, DEFAULT_SETTINGS.baseEase, 0, DEFAULT_SETTINGS, dueDates),
+        osrSchedule(
+            ReviewResponse.Good,
+            2,
+            DEFAULT_SETTINGS.baseEase,
+            0,
+            DEFAULT_SETTINGS,
+            dueDates,
+        ),
     ).toEqual({
         ease: DEFAULT_SETTINGS.baseEase,
         interval: 4,
     });
-    expect(dueDates).toEqual({
-        4: 1,
-        5: 2,
-    });
 
     // 7 <= interval < 30
-    dueDates = {
+    dueDates = new DueDateHistogram({
         25: 2,
-    };
+    });
     expect(
-        schedule(ReviewResponse.Good, 10, DEFAULT_SETTINGS.baseEase, 0, DEFAULT_SETTINGS, dueDates),
+        osrSchedule(
+            ReviewResponse.Good,
+            10,
+            DEFAULT_SETTINGS.baseEase,
+            0,
+            DEFAULT_SETTINGS,
+            dueDates,
+        ),
     ).toEqual({
         ease: DEFAULT_SETTINGS.baseEase,
         interval: 24,
     });
-    expect(dueDates).toEqual({
-        24: 1,
-        25: 2,
-    });
 
     // interval >= 30
-    dueDates = {
+    dueDates = new DueDateHistogram({
         2: 5,
         59: 8,
         60: 9,
@@ -112,25 +177,20 @@ test("Test load balancing", () => {
         65: 8,
         66: 2,
         67: 10,
-    };
+    });
     expect(
-        schedule(ReviewResponse.Good, 25, DEFAULT_SETTINGS.baseEase, 0, DEFAULT_SETTINGS, dueDates),
+        osrSchedule(
+            ReviewResponse.Good,
+            25,
+            DEFAULT_SETTINGS.baseEase,
+            0,
+            DEFAULT_SETTINGS,
+            dueDates,
+        ),
     ).toEqual({
         ease: DEFAULT_SETTINGS.baseEase,
         interval: 66,
     });
-    expect(dueDates).toEqual({
-        2: 5,
-        59: 8,
-        60: 9,
-        61: 3,
-        62: 5,
-        63: 4,
-        64: 4,
-        65: 8,
-        66: 3,
-        67: 10,
-    });
 });
 
 test("Test textInterval - desktop", () => {
diff --git a/tests/unit/util/utils.test.ts b/tests/unit/util/utils.test.ts
index c2c3de2..b8d37d0 100644
--- a/tests/unit/util/utils.test.ts
+++ b/tests/unit/util/utils.test.ts
@@ -1,10 +1,10 @@
 import { YAML_FRONT_MATTER_REGEX } from "src/constants";
 import {
     convertToStringOrEmpty,
-    extractFrontmatter,
     findLineIndexOfSearchStringIgnoringWs,
     isEqualOrSubPath,
     literalStringReplace,
+    splitNoteIntoFrontmatterAndContent,
 } from "src/util/utils";
 
 describe("literalStringReplace", () => {
@@ -120,19 +120,19 @@ describe("YAML_FRONT_MATTER_REGEX", () => {
     });
 });
 
-describe("extractFrontmatter", () => {
+describe("splitNoteIntoFrontmatterAndContent", () => {
     test("No frontmatter", () => {
         let text: string = `Hello
 Goodbye`;
         let frontmatter: string;
         let content: string;
-        [frontmatter, content] = extractFrontmatter(text);
+        [frontmatter, content] = splitNoteIntoFrontmatterAndContent(text);
         expect(frontmatter).toEqual("");
         expect(content).toEqual(text);
 
         text = `---
 Goodbye`;
-        [frontmatter, content] = extractFrontmatter(text);
+        [frontmatter, content] = splitNoteIntoFrontmatterAndContent(text);
         expect(frontmatter).toEqual("");
         expect(content).toEqual(text);
     });
@@ -148,7 +148,7 @@ tags:
 ---`;
         const text: string = frontmatter;
         let content: string;
-        [frontmatter, content] = extractFrontmatter(text);
+        [frontmatter, content] = splitNoteIntoFrontmatterAndContent(text);
         expect(frontmatter).toEqual(text);
         const frontmatterBlankedOut: string = `
 
@@ -187,7 +187,7 @@ This single {{question}} turns into {{3 separate}} {{cards}}
         const text: string = `${frontmatter}
 ${content}`;
 
-        const [f, c] = extractFrontmatter(text);
+        const [f, c] = splitNoteIntoFrontmatterAndContent(text);
         expect(f).toEqual(frontmatter);
         const frontmatterBlankedOut: string = `
 
@@ -252,7 +252,7 @@ ${content}`;
         const expectedContent: string = `${frontmatterBlankedOut}
 ${content}`;
 
-        const [f, c] = extractFrontmatter(text);
+        const [f, c] = splitNoteIntoFrontmatterAndContent(text);
         expect(f).toEqual(frontmatter);
         expect(c).toEqual(expectedContent);
     });
@@ -308,7 +308,7 @@ ${content}`;
         const expectedContent: string = `${frontmatterBlankedOut}
 ${content}`;
 
-        const [f, c] = extractFrontmatter(text);
+        const [f, c] = splitNoteIntoFrontmatterAndContent(text);
         expect(f).toEqual(frontmatter);
         expect(c).toEqual(expectedContent);
     });
@@ -367,7 +367,7 @@ ${content}`;
         const expectedContent: string = `${frontmatterBlankedOut}
 ${content}`;
 
-        const [f, c] = extractFrontmatter(text);
+        const [f, c] = splitNoteIntoFrontmatterAndContent(text);
         expect(f).toEqual(frontmatter);
         expect(c).toEqual(expectedContent);
     });
diff --git a/tests/vaults/filesButNoQuestions/Newton thought that light was composed of particles.md b/tests/vaults/filesButNoQuestions/Newton thought that light was composed of particles.md
new file mode 100644
index 0000000..e69de29
diff --git a/tests/vaults/filesButNoQuestions/The nature of light.md b/tests/vaults/filesButNoQuestions/The nature of light.md
new file mode 100644
index 0000000..72f3a33
--- /dev/null
+++ b/tests/vaults/filesButNoQuestions/The nature of light.md
@@ -0,0 +1,2 @@
+[[Scientist question things that I wouldn't]], For example [[The nature of light]].
+I don't think the question would ever occur to me… There is "something" at a distance from myself and I have information about its "state" in real time. How does that information get to me?
diff --git a/tests/vaults/notes1/Computation Graph.md b/tests/vaults/notes1/Computation Graph.md
new file mode 100644
index 0000000..7b1eeb4
--- /dev/null
+++ b/tests/vaults/notes1/Computation Graph.md
@@ -0,0 +1,10 @@
+#review
+
+https://www.coursera.org/learn/advanced-learning-algorithms/lecture/rhcTZ/computation-graph-optional
+https://www.coursera.org/learn/advanced-learning-algorithms/lecture/qqczh/larger-neural-network-example-optional
+
+Computation graph for very simple [[Forward Propagation]]
+
+![[Pasted image 20230419183000.png]]
+
+Details about [[Backpropagation]] and algorithm efficiency included in the above Coursera links, but not fleshed out here.
diff --git a/tests/vaults/notes2/Triboelectric Effect.md b/tests/vaults/notes2/Triboelectric Effect.md
new file mode 100644
index 0000000..8475afc
--- /dev/null
+++ b/tests/vaults/notes2/Triboelectric Effect.md
@@ -0,0 +1,31 @@
+---
+sr-due: 2025-02-21
+sr-interval: 421
+sr-ease: 270
+---
+
+#review
+
+The triboelectric effect describes electric charge transfer between two objects when they contact or slide against each other.
+
+It can occur with different materials, such as:
+
+-   the sole of a shoe on a carpet
+-   balloon rubbing against sweater
+
+(also known as triboelectricity, triboelectric charging, triboelectrification, or tribocharging)
+
+# See Also
+
+[[Triboelectric Effect Examples]]
+[[Triboelectric Series]]
+
+---
+
+#flashcards/science/physics
+
+# Questions
+
+What is the phenomenon called when electric charge is transferred between two objects when they contact or slide against each other::Triboelectric effect
+
+<!--SR:!2024-03-20,124,270-->
diff --git a/tests/vaults/notes3/A.md b/tests/vaults/notes3/A.md
new file mode 100644
index 0000000..ffd606c
--- /dev/null
+++ b/tests/vaults/notes3/A.md
@@ -0,0 +1,3 @@
+#review
+
+Really worth reading [[B]], [[C]] and [[D]]
diff --git a/tests/vaults/notes3/B.md b/tests/vaults/notes3/B.md
new file mode 100644
index 0000000..53cac1c
--- /dev/null
+++ b/tests/vaults/notes3/B.md
@@ -0,0 +1,10 @@
+#review
+
+Very interesting but doesn't reference any other notes
+
+# Frontmatter Determination
+
+-   Initially no frontmatter
+-   OSR 1.10.0
+-   this note reviewed as easy
+-   Plugin determined interval 4, ease 270
diff --git a/tests/vaults/notes3/C.md b/tests/vaults/notes3/C.md
new file mode 100644
index 0000000..ef0410b
--- /dev/null
+++ b/tests/vaults/notes3/C.md
@@ -0,0 +1,3 @@
+#review
+
+Definitely check out [[D]]
diff --git a/tests/vaults/notes3/D.md b/tests/vaults/notes3/D.md
new file mode 100644
index 0000000..9e472ba
--- /dev/null
+++ b/tests/vaults/notes3/D.md
@@ -0,0 +1,5 @@
+#review
+
+I recently read very positive reviews of [[A]] and [[B]].
+
+Even people on the bus was saying great things about [[B]]
diff --git a/tests/vaults/notes4/A.md b/tests/vaults/notes4/A.md
new file mode 100644
index 0000000..d9f6c3f
--- /dev/null
+++ b/tests/vaults/notes4/A.md
@@ -0,0 +1,16 @@
+---
+sr-due: 2023-09-10
+sr-interval: 4
+sr-ease: 270
+---
+
+#review
+
+Really worth reading [[B]], [[C]] and [[D]]
+
+# Frontmatter Determination
+
+-   Initially no frontmatter
+-   OSR 1.10.0
+-   this note reviewed as easy
+-   Plugin determined interval 4, ease 270
diff --git a/tests/vaults/notes4/B.md b/tests/vaults/notes4/B.md
new file mode 100644
index 0000000..b249b0c
--- /dev/null
+++ b/tests/vaults/notes4/B.md
@@ -0,0 +1,10 @@
+#review
+
+Very interesting but doesn't reference any other notes
+
+# Frontmatter Determination
+
+-   Initially no frontmatter
+-   OSR 1.10.0
+-   this note reviewed as easy
+-   Plugin determined interval 4, ease 272 (recognizing this has a link from A with an ease of 270)
diff --git a/tests/vaults/notes4/C.md b/tests/vaults/notes4/C.md
new file mode 100644
index 0000000..ef0410b
--- /dev/null
+++ b/tests/vaults/notes4/C.md
@@ -0,0 +1,3 @@
+#review
+
+Definitely check out [[D]]
diff --git a/tests/vaults/notes4/D.md b/tests/vaults/notes4/D.md
new file mode 100644
index 0000000..3bced96
--- /dev/null
+++ b/tests/vaults/notes4/D.md
@@ -0,0 +1,5 @@
+#review
+
+I recently read very positive reviews of [[A]] and [[B]].
+
+Even people on the bus were saying great things about [[B]]
diff --git a/tests/vaults/notes4/notes4_readme.md b/tests/vaults/notes4/notes4_readme.md
new file mode 100644
index 0000000..eeafd43
--- /dev/null
+++ b/tests/vaults/notes4/notes4_readme.md
@@ -0,0 +1,18 @@
+# "A.md" contains:
+
+-   frontmatter (note review of EASY)
+-   A link to "B.md", C.md
+
+# "B.md" contains:
+
+-   No frontmatter
+
+# "C.md" contains:
+
+-   No link to "B.md"
+
+# "D.md" contains:
+
+-   No frontmatter
+-   A link to "A.md"
+-   2 links to "B.md"
diff --git a/tests/vaults/notes5/A.md b/tests/vaults/notes5/A.md
new file mode 100644
index 0000000..d9f6c3f
--- /dev/null
+++ b/tests/vaults/notes5/A.md
@@ -0,0 +1,16 @@
+---
+sr-due: 2023-09-10
+sr-interval: 4
+sr-ease: 270
+---
+
+#review
+
+Really worth reading [[B]], [[C]] and [[D]]
+
+# Frontmatter Determination
+
+-   Initially no frontmatter
+-   OSR 1.10.0
+-   this note reviewed as easy
+-   Plugin determined interval 4, ease 270
diff --git a/tests/vaults/notes5/B.md b/tests/vaults/notes5/B.md
new file mode 100644
index 0000000..b249b0c
--- /dev/null
+++ b/tests/vaults/notes5/B.md
@@ -0,0 +1,10 @@
+#review
+
+Very interesting but doesn't reference any other notes
+
+# Frontmatter Determination
+
+-   Initially no frontmatter
+-   OSR 1.10.0
+-   this note reviewed as easy
+-   Plugin determined interval 4, ease 272 (recognizing this has a link from A with an ease of 270)
diff --git a/tests/vaults/notes5/C.md b/tests/vaults/notes5/C.md
new file mode 100644
index 0000000..ef0410b
--- /dev/null
+++ b/tests/vaults/notes5/C.md
@@ -0,0 +1,3 @@
+#review
+
+Definitely check out [[D]]
diff --git a/tests/vaults/notes5/D.md b/tests/vaults/notes5/D.md
new file mode 100644
index 0000000..b4d019b
--- /dev/null
+++ b/tests/vaults/notes5/D.md
@@ -0,0 +1,10 @@
+#review
+
+I recently read very positive reviews of [[A]] and [[B]].
+
+Even people on the bus were saying great things about [[B]]
+
+#flashcards
+
+This is question 1::This is answer 1
+This is question 2::This is answer 2
diff --git a/tests/vaults/notes5/notes5_readme.md b/tests/vaults/notes5/notes5_readme.md
new file mode 100644
index 0000000..9dd1834
--- /dev/null
+++ b/tests/vaults/notes5/notes5_readme.md
@@ -0,0 +1,19 @@
+# "A.md" contains:
+
+-   frontmatter (note review of EASY)
+-   A link to "B.md", C.md
+-   3 questions already reviewed
+
+# "B.md" contains:
+
+-   No frontmatter
+
+# "C.md" contains:
+
+-   No link to "B.md"
+
+# "D.md" contains:
+
+-   No frontmatter
+-   A link to "A.md"
+-   2 links to "B.md"
diff --git a/tests/vaults/notes6/A.md b/tests/vaults/notes6/A.md
new file mode 100644
index 0000000..7f6959f
--- /dev/null
+++ b/tests/vaults/notes6/A.md
@@ -0,0 +1,9 @@
+#flashcards
+
+There is schedule info for 3 cards, but only 2 cards in the question
+
+Note: The scheduling comment is on the same line as the question itself.
+If it's on the subsequent line lint will complain and require a blank line before the comment
+Which isn't recognized by the note parser
+
+A {{question}} with multiple parts {{Navevo part}} <!--SR:!2024-05-22,1,230!2024-05-25,4,270!2033-03-03,3,333-->
diff --git a/tests/vaults/readme.md b/tests/vaults/readme.md
new file mode 100644
index 0000000..3bf6f0f
--- /dev/null
+++ b/tests/vaults/readme.md
@@ -0,0 +1,20 @@
+These vaults serve are used by the unit test cases.
+
+# Test Vaults
+
+## filesButNoQuestions
+
+## notes1
+
+## notes2
+
+## notes3
+
+-   Some note files, with links between them
+-   No questions in any of the notes
+-   No notes already reviewed
+
+## notes4
+
+-   Same as notes3, except
+-   A.md note already reviewed as easy