#アーキテクチャ
ts-fsrs v6 のスケジューリングアーキテクチャでは、記憶モデル、時間処理、スケジューリングポリシーを Model、Chrono、Middleware に分離し、Scheduler がそれらの実行を調整します。
#責務の分担
| 要素 | 責務 |
|---|---|
| Model | step で記憶状態を更新し、nextInterval で基準の間隔を計算します。FSRS-6 の記憶状態には安定性と難易度が含まれます。 |
| Chrono | 時刻の表現と時間演算を定義します。dateChrono は JavaScript の Date を使い、時間差の計算や日時の加減算を提供します。 |
| Middleware | 目標保持率、学習ステップ、最大間隔などのポリシーを追加し、config、card、revlog の schema を拡張できます。 |
| Scheduler | schema とデフォルト値を組み合わせ、設定と入力を検証し、newCard、review、preview、rollback などを実行します。 |
安定性や難易度などの Model の記憶状態は、New や Learning などのカード状態とは異なります。カードの学習状態とスケジューリングポリシーは Scheduler と Middleware が共同で処理します。
カードの選択、返された card と revlog の保存、ユーザーとコレクションの管理はアプリケーションが担当します。スケジューラは結果を自動で永続化しません。
#スケジューラの定義とインスタンス
defineScheduler({ model, chrono }) は定義を作成します。.use(...) は追加した middleware と推論された型を持つ新しい定義を返し、元の定義も再利用できます。.create({ config }) はユーザー設定を検証し、独立したスケジューラインスタンスを作成します。
.use(...) による組み合わせを含む定義をプログラム単位のプリセットとして保持し、ユーザーやコレクションの設定が決まった時点でインスタンスを作成します。
import { const dateChrono: Chrono<{
readonly time: SRSSchema<{
input: Date;
output: Date;
}>;
readonly config: SRSSchema<{
input: {
readonly fractionalDays?: boolean | undefined;
};
output: {
readonly fractionalDays: boolean;
};
}>;
readonly fields: {
readonly card: SRSSchema<{
input: {
dueAt: Date;
lastReviewAt?: (Date | null) | undefined;
};
output: {
dueAt: Date;
lastReviewAt: Date | null;
};
}>;
readonly revlog: SRSSchema<{
input: {
dueAt: Date;
lastReviewAt?: (Date | null) | undefined;
reviewTime: Date;
};
output: {
dueAt: Date;
lastReviewAt: Date | null;
reviewTime: Date;
};
}>;
};
}>
dateChrono , function defineScheduler<const M extends AnyModel, const C extends AnyChrono>(definition: {
readonly model: M;
readonly chrono: C;
}): CustomScheduler<SchedulerNameOf<M>, SchedulerEnvFor<M, C, readonly []>, M, C>
defineScheduler , type Rating = 0 | 1 | 2 | 3 | 4
const Rating: Readonly<{
readonly Manual: 0;
readonly Again: 1;
readonly Hard: 2;
readonly Good: 3;
readonly Easy: 4;
}>
Rating } from 'ts-fsrs'
import {
const schedulerDesiredRetentionMiddleware: Middleware<symbol, {
readonly config: SRSSchema<{
input: {
readonly desiredRetention: number | Record<Grade, number>;
};
output: {
readonly desiredRetention: Record<Grade, number>;
};
}>;
}>
schedulerDesiredRetentionMiddleware ,
const schedulerMaximumIntervalMiddleware: Middleware<symbol, {
readonly config: SRSSchema<{
input: {
readonly maximumInterval?: number;
};
output: {
readonly maximumInterval: number;
};
}>;
}>
Register after learning steps so explicit steps can keep their exact delay.
schedulerMaximumIntervalMiddleware ,
} from 'ts-fsrs/middlewares'
import { const FSRS6_DEFAULT_WEIGHTS: number[] FSRS6_DEFAULT_WEIGHTS , const FSRS6Model: Model<{
readonly name: "fsrs-6";
readonly config: SRSSchema<{
input: {
readonly weights: number[];
readonly enableShortTerm: boolean;
readonly numRelearningSteps: number;
};
output: {
readonly weights: number[];
readonly enableShortTerm: boolean;
readonly numRelearningSteps: number;
};
}>;
readonly memoryState: SRSSchema<{
input: {
stability: number;
difficulty: number;
};
output: {
stability: number;
difficulty: number;
};
}>;
readonly algorithm: FSRS6Algorithm;
}>
FSRS6Model } from 'ts-fsrs/models/fsrs-6'
// Keep the model, chronology, and policies as a reusable program preset.
const const schedulerDefinition: CustomScheduler<"fsrs-6", ExtendSchedulerEnv<SchedulerEnvFor<Model<{
readonly name: "fsrs-6";
readonly config: SRSSchema<{
input: {
readonly weights: number[];
readonly enableShortTerm: boolean;
readonly numRelearningSteps: number;
};
output: {
readonly weights: number[];
readonly enableShortTerm: boolean;
readonly numRelearningSteps: number;
};
}>;
readonly memoryState: SRSSchema<{
input: {
stability: number;
difficulty: number;
};
output: {
stability: number;
difficulty: number;
};
}>;
readonly algorithm: FSRS6Algorithm;
}>, Chrono<...>, readonly []>, readonly [...]>, Model<...>, Chrono<...>>
schedulerDefinition = defineScheduler<Model<{
readonly name: "fsrs-6";
readonly config: SRSSchema<{
input: {
readonly weights: number[];
readonly enableShortTerm: boolean;
readonly numRelearningSteps: number;
};
output: {
readonly weights: number[];
readonly enableShortTerm: boolean;
readonly numRelearningSteps: number;
};
}>;
readonly memoryState: SRSSchema<{
input: {
stability: number;
difficulty: number;
};
output: {
stability: number;
difficulty: number;
};
}>;
readonly algorithm: FSRS6Algorithm;
}>, Chrono<...>>(definition: {
...;
}): CustomScheduler<...>
defineScheduler ({
model: Model<{
readonly name: "fsrs-6";
readonly config: SRSSchema<{
input: {
readonly weights: number[];
readonly enableShortTerm: boolean;
readonly numRelearningSteps: number;
};
output: {
readonly weights: number[];
readonly enableShortTerm: boolean;
readonly numRelearningSteps: number;
};
}>;
readonly memoryState: SRSSchema<{
input: {
stability: number;
difficulty: number;
};
output: {
stability: number;
difficulty: number;
};
}>;
readonly algorithm: FSRS6Algorithm;
}>
model : const FSRS6Model: Model<{
readonly name: "fsrs-6";
readonly config: SRSSchema<{
input: {
readonly weights: number[];
readonly enableShortTerm: boolean;
readonly numRelearningSteps: number;
};
output: {
readonly weights: number[];
readonly enableShortTerm: boolean;
readonly numRelearningSteps: number;
};
}>;
readonly memoryState: SRSSchema<{
input: {
stability: number;
difficulty: number;
};
output: {
stability: number;
difficulty: number;
};
}>;
readonly algorithm: FSRS6Algorithm;
}>
FSRS6Model ,
chrono: Chrono<{
readonly time: SRSSchema<{
input: Date;
output: Date;
}>;
readonly config: SRSSchema<{
input: {
readonly fractionalDays?: boolean | undefined;
};
output: {
readonly fractionalDays: boolean;
};
}>;
readonly fields: {
readonly card: SRSSchema<{
input: {
dueAt: Date;
lastReviewAt?: (Date | null) | undefined;
};
output: {
dueAt: Date;
lastReviewAt: Date | null;
};
}>;
readonly revlog: SRSSchema<{
input: {
dueAt: Date;
lastReviewAt?: (Date | null) | undefined;
reviewTime: Date;
};
output: {
dueAt: Date;
lastReviewAt: Date | null;
reviewTime: Date;
};
}>;
};
}>
chrono : const dateChrono: Chrono<{
readonly time: SRSSchema<{
input: Date;
output: Date;
}>;
readonly config: SRSSchema<{
input: {
readonly fractionalDays?: boolean | undefined;
};
output: {
readonly fractionalDays: boolean;
};
}>;
readonly fields: {
readonly card: SRSSchema<{
input: {
dueAt: Date;
lastReviewAt?: (Date | null) | undefined;
};
output: {
dueAt: Date;
lastReviewAt: Date | null;
};
}>;
readonly revlog: SRSSchema<{
input: {
dueAt: Date;
lastReviewAt?: (Date | null) | undefined;
reviewTime: Date;
};
output: {
dueAt: Date;
lastReviewAt: Date | null;
reviewTime: Date;
};
}>;
};
}>
dateChrono ,
}).CustomScheduler<"fsrs-6", SchedulerEnvFor<Model<{ readonly name: "fsrs-6"; readonly config: SRSSchema<{ input: { readonly weights: number[]; readonly enableShortTerm: boolean; readonly numRelearningSteps: number; }; output: { readonly weights: number[]; readonly enableShortTerm: boolean; readonly numRelearningSteps: number; }; }>; readonly memoryState: SRSSchema<{ input: { stability: number; difficulty: number; }; output: { stability: number; difficulty: number; }; }>; readonly algorithm: FSRS6Algorithm; }>, Chrono<...>, readonly []>, Model<...>, Chrono<...>>.use: <readonly [Middleware<symbol, {
readonly config: SRSSchema<{
input: {
readonly desiredRetention: number | Record<Grade, number>;
};
output: {
readonly desiredRetention: Record<Grade, number>;
};
}>;
}>, Middleware<symbol, {
readonly config: SRSSchema<{
input: {
readonly maximumInterval?: number;
};
output: {
readonly maximumInterval: number;
};
}>;
}>]>(middlewares_0: Middleware<symbol, {
readonly config: SRSSchema<{
input: {
readonly desiredRetention: number | Record<Grade, number>;
};
output: {
readonly desiredRetention: Record<Grade, number>;
};
}>;
}>, middlewares_1: Middleware<...>) => CustomScheduler<...>
Returns a new scheduler definition extended with the supplied middleware.
The current scheduler and any cores created from it remain unchanged.
use (const schedulerDesiredRetentionMiddleware: Middleware<symbol, {
readonly config: SRSSchema<{
input: {
readonly desiredRetention: number | Record<Grade, number>;
};
output: {
readonly desiredRetention: Record<Grade, number>;
};
}>;
}>
schedulerDesiredRetentionMiddleware , const schedulerMaximumIntervalMiddleware: Middleware<symbol, {
readonly config: SRSSchema<{
input: {
readonly maximumInterval?: number;
};
output: {
readonly maximumInterval: number;
};
}>;
}>
Register after learning steps so explicit steps can keep their exact delay.
schedulerMaximumIntervalMiddleware )
// Supply each user's configuration when creating their scheduler.
const const scheduler: SchedulerCore<SchedulerCoreEnv<ExtendSchedulerEnv<SchedulerEnvFor<Model<{
readonly name: "fsrs-6";
readonly config: SRSSchema<{
input: {
readonly weights: number[];
readonly enableShortTerm: boolean;
readonly numRelearningSteps: number;
};
output: {
readonly weights: number[];
readonly enableShortTerm: boolean;
readonly numRelearningSteps: number;
};
}>;
readonly memoryState: SRSSchema<{
input: {
stability: number;
difficulty: number;
};
output: {
stability: number;
difficulty: number;
};
}>;
readonly algorithm: FSRS6Algorithm;
}>, Chrono<...>, readonly []>, readonly [...]>>, ModelCore<...>, ChronoCore<...>>
scheduler = const schedulerDefinition: CustomScheduler<"fsrs-6", ExtendSchedulerEnv<SchedulerEnvFor<Model<{
readonly name: "fsrs-6";
readonly config: SRSSchema<{
input: {
readonly weights: number[];
readonly enableShortTerm: boolean;
readonly numRelearningSteps: number;
};
output: {
readonly weights: number[];
readonly enableShortTerm: boolean;
readonly numRelearningSteps: number;
};
}>;
readonly memoryState: SRSSchema<{
input: {
stability: number;
difficulty: number;
};
output: {
stability: number;
difficulty: number;
};
}>;
readonly algorithm: FSRS6Algorithm;
}>, Chrono<...>, readonly []>, readonly [...]>, Model<...>, Chrono<...>>
schedulerDefinition .CustomScheduler<"fsrs-6", ExtendSchedulerEnv<SchedulerEnvFor<Model<{ readonly name: "fsrs-6"; readonly config: SRSSchema<{ input: { readonly weights: number[]; readonly enableShortTerm: boolean; readonly numRelearningSteps: number; }; output: { readonly weights: number[]; readonly enableShortTerm: boolean; readonly numRelearningSteps: number; }; }>; readonly memoryState: SRSSchema<{ input: { stability: number; difficulty: number; }; output: { stability: number; difficulty: number; }; }>; readonly algorithm: FSRS6Algorithm; }>, Chrono<...>, readonly []>, readonly [...]>, Model<...>, Chrono<...>>.create: (ctx: {
readonly config: {
readonly weights: number[];
readonly enableShortTerm: boolean;
readonly numRelearningSteps: number;
} & {
readonly fractionalDays?: boolean | undefined;
} & EmptyPart & {
readonly desiredRetention: number | Record<Grade, number>;
readonly maximumInterval?: number | undefined;
};
}) => SchedulerCore<SchedulerCoreEnv<ExtendSchedulerEnv<SchedulerEnvFor<Model<{
readonly name: "fsrs-6";
readonly config: SRSSchema<{
input: {
readonly weights: number[];
readonly enableShortTerm: boolean;
readonly numRelearningSteps: number;
};
output: {
readonly weights: number[];
readonly enableShortTerm: boolean;
readonly numRelearningSteps: number;
};
}>;
readonly memoryState: SRSSchema<{
input: {
stability: number;
difficulty: number;
};
output: {
stability: number;
difficulty: number;
};
}>;
readonly algorithm: FSRS6Algorithm;
}>, Chrono<...>, readonly []>, readonly [...]>>, ModelCore<...>, ChronoCore<...>>
create ({
config: {
readonly weights: number[];
readonly enableShortTerm: boolean;
readonly numRelearningSteps: number;
} & {
readonly fractionalDays?: boolean | undefined;
} & EmptyPart & {
readonly desiredRetention: number | Record<Grade, number>;
readonly maximumInterval?: number | undefined;
}
config : {
weights: const FSRS6_DEFAULT_WEIGHTS: number[] FSRS6_DEFAULT_WEIGHTS ,
enableShortTerm: true,
numRelearningSteps: 1,
desiredRetention: 0.9,
maximumInterval: 1,
},
})
const now = new Date('2026-01-01T00:00:00.000Z')
const const card: {
stability: number;
difficulty: number;
dueAt: Date;
lastReviewAt: Date | null;
state: State;
scheduleStatus: "new" | "learning" | "review";
}
card = const scheduler: SchedulerCore<SchedulerCoreEnv<ExtendSchedulerEnv<SchedulerEnvFor<Model<{
readonly name: "fsrs-6";
readonly config: SRSSchema<{
input: {
readonly weights: number[];
readonly enableShortTerm: boolean;
readonly numRelearningSteps: number;
};
output: {
readonly weights: number[];
readonly enableShortTerm: boolean;
readonly numRelearningSteps: number;
};
}>;
readonly memoryState: SRSSchema<{
input: {
stability: number;
difficulty: number;
};
output: {
stability: number;
difficulty: number;
};
}>;
readonly algorithm: FSRS6Algorithm;
}>, Chrono<...>, readonly []>, readonly [...]>>, ModelCore<...>, ChronoCore<...>>
scheduler .SchedulerCore<SchedulerCoreEnv<ExtendSchedulerEnv<SchedulerEnvFor<Model<{ readonly name: "fsrs-6"; readonly config: SRSSchema<{ input: { readonly weights: number[]; readonly enableShortTerm: boolean; readonly numRelearningSteps: number; }; output: { readonly weights: number[]; readonly enableShortTerm: boolean; readonly numRelearningSteps: number; }; }>; readonly memoryState: SRSSchema<{ input: { stability: number; difficulty: number; }; output: { stability: number; difficulty: number; }; }>; readonly algorithm: FSRS6Algorithm; }>, Chrono<...>, readonly []>, readonly [...]>>, ModelCore<...>, ChronoCore<...>>.newCard: (options?: {
readonly now?: Date | undefined;
} | undefined) => {
stability: number;
difficulty: number;
dueAt: Date;
lastReviewAt: Date | null;
state: State;
scheduleStatus: "new" | "learning" | "review";
}
newCard ({ now })
const const result: {
card: {
stability: number;
difficulty: number;
dueAt: Date;
lastReviewAt: Date | null;
state: State;
scheduleStatus: "new" | "learning" | "review";
};
revlog: {
stability: number;
difficulty: number;
dueAt: Date;
lastReviewAt: Date | null;
reviewTime: Date;
state: State;
scheduleStatus: "new" | "learning" | "review";
rating: Grade;
};
}
result = const scheduler: SchedulerCore<SchedulerCoreEnv<ExtendSchedulerEnv<SchedulerEnvFor<Model<{
readonly name: "fsrs-6";
readonly config: SRSSchema<{
input: {
readonly weights: number[];
readonly enableShortTerm: boolean;
readonly numRelearningSteps: number;
};
output: {
readonly weights: number[];
readonly enableShortTerm: boolean;
readonly numRelearningSteps: number;
};
}>;
readonly memoryState: SRSSchema<{
input: {
stability: number;
difficulty: number;
};
output: {
stability: number;
difficulty: number;
};
}>;
readonly algorithm: FSRS6Algorithm;
}>, Chrono<...>, readonly []>, readonly [...]>>, ModelCore<...>, ChronoCore<...>>
scheduler .SchedulerCore<SchedulerCoreEnv<ExtendSchedulerEnv<SchedulerEnvFor<Model<{ readonly name: "fsrs-6"; readonly config: SRSSchema<{ input: { readonly weights: number[]; readonly enableShortTerm: boolean; readonly numRelearningSteps: number; }; output: { readonly weights: number[]; readonly enableShortTerm: boolean; readonly numRelearningSteps: number; }; }>; readonly memoryState: SRSSchema<{ input: { stability: number; difficulty: number; }; output: { stability: number; difficulty: number; }; }>; readonly algorithm: FSRS6Algorithm; }>, Chrono<...>, readonly []>, readonly [...]>>, ModelCore<...>, ChronoCore<...>>.review: (input: {
readonly card: Readonly<Omit<Readonly<Omit<Readonly<{
stability: number;
difficulty: number;
} & {
dueAt: Date;
lastReviewAt: Date | null;
} & SchedulerCoreFields$1<"new" | "learning" | "review">>, keyof SchedulerCoreFields$1<"new" | "learning" | "review">> & SchedulerCoreFields$1<"new" | "learning" | "review">>, keyof SchedulerCoreFields$1<"new" | "learning" | "review">> & SchedulerCoreFields$1<"new" | "learning" | "review">>;
readonly grade: Grade;
readonly now?: Date | undefined;
}) => {
...;
}
review ({ card, grade: const Rating: Readonly<{
readonly Manual: 0;
readonly Again: 1;
readonly Hard: 2;
readonly Good: 3;
readonly Easy: 4;
}>
Rating .Good, now })
console.log(
JSON.stringify(
{
originalState: const card: {
stability: number;
difficulty: number;
dueAt: Date;
lastReviewAt: Date | null;
state: State;
scheduleStatus: "new" | "learning" | "review";
}
card .state,
state: const result: {
card: {
stability: number;
difficulty: number;
dueAt: Date;
lastReviewAt: Date | null;
state: State;
scheduleStatus: "new" | "learning" | "review";
};
revlog: {
stability: number;
difficulty: number;
dueAt: Date;
lastReviewAt: Date | null;
reviewTime: Date;
state: State;
scheduleStatus: "new" | "learning" | "review";
rating: Grade;
};
}
result .card: {
stability: number;
difficulty: number;
dueAt: Date;
lastReviewAt: Date | null;
state: State;
scheduleStatus: "new" | "learning" | "review";
}
card .state,
dueAt: const result: {
card: {
stability: number;
difficulty: number;
dueAt: Date;
lastReviewAt: Date | null;
state: State;
scheduleStatus: "new" | "learning" | "review";
};
revlog: {
stability: number;
difficulty: number;
dueAt: Date;
lastReviewAt: Date | null;
reviewTime: Date;
state: State;
scheduleStatus: "new" | "learning" | "review";
rating: Grade;
};
}
result .card: {
stability: number;
difficulty: number;
dueAt: Date;
lastReviewAt: Date | null;
state: State;
scheduleStatus: "new" | "learning" | "review";
}
card .dueAt,
stability: const result: {
card: {
stability: number;
difficulty: number;
dueAt: Date;
lastReviewAt: Date | null;
state: State;
scheduleStatus: "new" | "learning" | "review";
};
revlog: {
stability: number;
difficulty: number;
dueAt: Date;
lastReviewAt: Date | null;
reviewTime: Date;
state: State;
scheduleStatus: "new" | "learning" | "review";
rating: Grade;
};
}
result .card: {
stability: number;
difficulty: number;
dueAt: Date;
lastReviewAt: Date | null;
state: State;
scheduleStatus: "new" | "learning" | "review";
}
card .stability,
rating: const result: {
card: {
stability: number;
difficulty: number;
dueAt: Date;
lastReviewAt: Date | null;
state: State;
scheduleStatus: "new" | "learning" | "review";
};
revlog: {
stability: number;
difficulty: number;
dueAt: Date;
lastReviewAt: Date | null;
reviewTime: Date;
state: State;
scheduleStatus: "new" | "learning" | "review";
rating: Grade;
};
}
result .revlog: {
stability: number;
difficulty: number;
dueAt: Date;
lastReviewAt: Date | null;
reviewTime: Date;
state: State;
scheduleStatus: "new" | "learning" | "review";
rating: Grade;
}
revlog .rating,
},
null,
2
)
)
この例では最大間隔を意図的に 1 日に制限します。モデルが計算する安定性は 2.3065 のままですが、最大間隔のポリシーが期限を 1 月 2 日に変更します。元のカードは New のままで、返されたカードは Review になります。この最小プリセットには学習ステップ、統計、fuzzing は含まれません。
#review のデータの流れ
card、grade、nowをreviewに渡します。スケジューラは入力を検証し、Chrono で経過時間を計算します。- Middleware がコア処理を囲んで実行されます。目標保持率の middleware は間隔の計算前に保持率を設定します。
- コア処理が次の記憶状態を計算し、必要に応じてモデルを呼び出して基準の間隔を求めます。Middleware は最終的なスケジュール間隔を指定または調整できます。
- チェーン全体が戻った後、スケジューラは Chrono を使って復習時刻と最終的な間隔から
dueAtを算出し、新しい card と revlog を返します。Card は次の記憶状態を持ち、revlog は復習前の記憶状態と今回の評価・復習時刻を保持します。
#Middleware の順序
.use(A, B) の各 review handler が next() を呼び出す場合、次の順序で実行されます。
A の next() 前 → B の next() 前 → コア処理 → B の next() 後 → A の next() 後
next() より前のコードは処理を準備し、その後のコードは下流の結果を受け取ります。このため、登録順序によって最後に適用されるポリシーが変わります。この例ではコア処理の前に目標保持率を設定し、その後で最大間隔を制限します。学習ステップを追加する場合は、最大間隔の middleware を学習ステップの middleware より後に登録します。学習ステップの middleware は戻りの段階で設定された学習間隔を復元し、日単位の間隔ポリシーによる上書きを防ぎます。