Embedded SRS Development
Also: Preact, Naming Website Features, Website House Tour
Working on embedded SRS.
Content warning: AI-written code, liberally, throughout, as desired, to speed up small tasks.
Typical Intervals
First off, want to get review intervals nice to start:
- meatier cards, so donât want a ton of reviewing
- want to do binary rating if possible (
AgainorGood)
Trying learning_steps = ["10m", "6h"] with some simulations.
Simulation code
import { fsrs, generatorParameters, createEmptyCard, Rating, Card, RecordLogItem } from 'ts-fsrs';
type ReviewRating = Rating.Again | Rating.Hard | Rating.Good | Rating.Easy;
// Setup: Binary-friendly params (1 step allows "Good" to graduate immediately)
const params = generatorParameters({
enable_fuzz: false,
enable_short_term: true,
learning_steps: ["10m", "6h"],
});
const f = fsrs(params);
function formatInterval(intervalMs: number): string {
const hours = intervalMs / (1000 * 60 * 60);
if (hours < 24) {
return `${hours.toFixed(2)}h`;
} else {
const days = hours / 24;
return `${days.toFixed(1)}d`;
}
}
function simulateScenario(scenarioName: string, initialHistory: ReviewRating[]) {
console.log(`\n--- Simulation: ${scenarioName} ---`);
let card = createEmptyCard(new Date('2024-01-01T12:00:00')); // Start Jan 1st
let now = new Date(card.due);
// 1. Replay initial history (e.g., the miss)
for (const rating of initialHistory) {
const scheduling = f.repeat(card, now);
card = scheduling[rating].card;
const interval = formatInterval(card.due.getTime() - now.getTime());
console.log(`[${now.toISOString().split('T')[0]}] Rated: ${Rating[rating]} -> Due: ${card.due.toISOString().split('T')[0]} (Interval: ${interval})`);
// Fast forward exactly to the due date
now = new Date(card.due);
}
// 2. Simulate getting it CORRECT (Good) every time for the next 5 reviews
for (let i = 0; i < 5; i++) {
const scheduling = f.repeat(card, now);
card = scheduling[Rating.Good].card; // Always choose "Good"
const interval = formatInterval(card.due.getTime() - now.getTime());
console.log(`[${now.toISOString().split('T')[0]}] Rated: ${Rating[Rating.Good]} -> Due: ${card.due.toISOString().split('T')[0]} (Interval: ${interval})`);
// Fast forward exactly to the due date
now = new Date(card.due);
}
}
// (a) Correct every time
simulateScenario("Perfect Streak", [Rating.Good]);
// (b) Miss first, then correct
simulateScenario("Miss then Perfect", [Rating.Again, Rating.Good]);
// (c)
simulateScenario("Miss, Miss, Perfect", [Rating.Again, Rating.Again, Rating.Good]);
// (d)
simulateScenario("Miss, Good, Miss, Perfect", [Rating.Again, Rating.Good, Rating.Again]);
// (x) Manual in the middle (can't use Rating.Manual lol)
// simulateScenario("Manual in the middle", [Rating.Good, Rating.Manual]);
Output:
--- Simulation: Perfect Streak ---
[2024-01-01] Rated: Good -> Due: 2024-01-01 (Interval: 6.00h)
[2024-01-01] Rated: Good -> Due: 2024-01-03 (Interval: 2.0d)
[2024-01-03] Rated: Good -> Due: 2024-01-14 (Interval: 11.0d)
[2024-01-14] Rated: Good -> Due: 2024-02-29 (Interval: 46.0d)
[2024-02-29] Rated: Good -> Due: 2024-08-10 (Interval: 163.0d)
[2024-08-10] Rated: Good -> Due: 2025-12-21 (Interval: 498.0d)
--- Simulation: Miss then Perfect ---
[2024-01-01] Rated: Again -> Due: 2024-01-01 (Interval: 0.17h)
[2024-01-01] Rated: Good -> Due: 2024-01-02 (Interval: 1.0d)
[2024-01-02] Rated: Good -> Due: 2024-01-04 (Interval: 2.0d)
[2024-01-04] Rated: Good -> Due: 2024-01-10 (Interval: 6.0d)
[2024-01-10] Rated: Good -> Due: 2024-01-28 (Interval: 18.0d)
[2024-01-28] Rated: Good -> Due: 2024-03-13 (Interval: 45.0d)
[2024-03-13] Rated: Good -> Due: 2024-06-26 (Interval: 105.0d)
--- Simulation: Miss, Miss, Perfect ---
[2024-01-01] Rated: Again -> Due: 2024-01-01 (Interval: 0.17h)
[2024-01-01] Rated: Again -> Due: 2024-01-01 (Interval: 0.17h)
[2024-01-01] Rated: Good -> Due: 2024-01-02 (Interval: 1.0d)
[2024-01-02] Rated: Good -> Due: 2024-01-04 (Interval: 2.0d)
[2024-01-04] Rated: Good -> Due: 2024-01-07 (Interval: 3.0d)
[2024-01-07] Rated: Good -> Due: 2024-01-12 (Interval: 5.0d)
[2024-01-12] Rated: Good -> Due: 2024-01-22 (Interval: 10.0d)
[2024-01-22] Rated: Good -> Due: 2024-02-09 (Interval: 18.0d)
--- Simulation: Miss, Good, Miss, Perfect ---
[2024-01-01] Rated: Again -> Due: 2024-01-01 (Interval: 0.17h)
[2024-01-01] Rated: Good -> Due: 2024-01-02 (Interval: 1.0d)
[2024-01-02] Rated: Again -> Due: 2024-01-02 (Interval: 0.17h)
[2024-01-02] Rated: Good -> Due: 2024-01-03 (Interval: 1.0d)
[2024-01-03] Rated: Good -> Due: 2024-01-05 (Interval: 2.0d)
[2024-01-05] Rated: Good -> Due: 2024-01-08 (Interval: 3.0d)
[2024-01-08] Rated: Good -> Due: 2024-01-14 (Interval: 6.0d)
[2024-01-14] Rated: Good -> Due: 2024-01-25 (Interval: 11.0d)
A bit confusing (reference again: learning_steps = ["10m", "6h"] ). Basic mechanics seem to be:
Againjumps tolearning_steps[0]Goodincrements- this will start it at
learning_steps[1] - or, if it was previously
Again, itâll jump right out into the âstandardâ algorithm but w/ more conservative growth (1d, 2d, âŠ)
- this will start it at
Fuzz probably nice in real life, but accidentally having it on made the simulation much more confusing to diagnose.
Reviewing Through History
Code snippet (Gemini) to run through a log of historical reviews:
import {
fsrs,
generatorParameters,
createEmptyCard,
Rating,
Card,
FSRS
} from 'ts-fsrs';
// 1. Setup FSRS with default parameters
const params = generatorParameters({ enable_fuzz: true });
const f = fsrs(params);
// 2. Define your history structure
// difficulty: 1 (Again), 2 (Hard), 3 (Good), 4 (Easy)
type ReviewHistory = {
datetime: Date;
rating: Rating;
};
/**
* Replays history to calculate the current state of a card.
*/
function getCardStateFromHistory(reviews: ReviewHistory[]): Card {
// Start with a fresh card (optionally set the creation date to the first review date)
let card = createEmptyCard(reviews[0]?.datetime || new Date());
// Sort reviews chronologically to be safe
const sortedReviews = reviews.sort((a, b) => a.datetime.getTime() - b.datetime.getTime());
for (const review of sortedReviews) {
// f.repeat returns an array of possible outcomes (Again, Hard, Good, Easy).
// We select the one that actually happened in history.
const scheduling_cards = f.repeat(card, review.datetime);
// Select the schedule corresponding to the rating given at that time
// Note: RecordLog is the log, card is the new card state
card = scheduling_cards[review.rating].card;
}
return card;
}
// Example Usage
const history: ReviewHistory[] = [
{ datetime: new Date('2023-10-01T10:00:00Z'), rating: Rating.Again }, // Failed first time
{ datetime: new Date('2023-10-01T10:05:00Z'), rating: Rating.Good }, // Got it right immediately after
{ datetime: new Date('2023-10-04T09:00:00Z'), rating: Rating.Good }, // Reviewed 3 days later
];
const currentCard = getCardStateFromHistory(history);
console.log("Next review due:", currentCard.due);
console.log("Current State:", currentCard.state); // 0=New, 1=Learning, 2=Review, 3=Relearning
and wildly over-annotated Card type by Claude Opus:
type Card = {
due: Date;
// The date/time when this card is next scheduled for review.
// Units: JavaScript Date object (millisecond precision)
// Range: Any valid Date
stability: number;
// The memory stability - represents the interval (in days) at which
// retrievability would be exactly 90%.
// Units: days
// Range: [0.001, 36500] (S_MIN to S_MAX constants)
// Initial values for first review: [0.1, 100] (capped by INIT_S_MAX)
// Higher = stronger memory, longer intervals before forgetting
difficulty: number;
// The inherent difficulty of the card content.
// Units: dimensionless
// Range: [1, 10] (clamped by the algorithm)
// 1 = easiest, 10 = hardest
// Affects how stability grows on successful recall
elapsed_days: number;
// @deprecated (removed in v6.0.0)
// Days since the card was last reviewed.
// Units: days (integer)
// Range: [0, â)
// 0 for new cards or same-day reviews
scheduled_days: number;
// The interval between this review and the next scheduled review.
// Units: days (integer)
// Range: [0, maximum_interval] (default max: 36500 = 100 years)
// 0 during learning/relearning steps (sub-day intervals)
learning_steps: number;
// Current step index in the learning/relearning sequence.
// Units: dimensionless (step index)
// Range: [0, n] where n = length of learning_steps array
// 0 = first step, increments as you progress through steps
reps: number;
// Total count of times the card has been reviewed.
// Units: count (integer)
// Range: [0, â)
// Increments by 1 on each review
lapses: number;
// Count of times the card was forgotten (rated "Again" after graduating).
// Units: count (integer)
// Range: [0, â)
// Only increments when a Review card lapses back to Relearning
state: State;
// Current learning state of the card.
// Units: enum
// Range: 0=New, 1=Learning, 2=Review, 3=Relearning
last_review?: Date;
// The timestamp of the most recent review.
// Units: JavaScript Date object
// Range: Any valid Date, or undefined for new cards
};
Adding Preact to The Build System
Prototyping Flash Cards
Proposal: Naming Website Features
A shower thought I had: people build software projects and name them. The names are great. It makes even a small thing feel real and weighty and possible to talk about as a first-class object.
I spend enormous amounts of time building features into my website. Though I love them, I often have this feeling after theyâre done of, was that just a waste of time? Will anybody notice? Which is silly â I donât make the site so anybody will notice, I make it for me!
Why not name them, these website features? Celebrate their completion and improvement the way you would a small software project.
My original thought that sparked this was to have a âwebsite tourâ pageâlike a home tour youâd give of your wacky Rube Goldberg mansion off in a swampâto introduce visitors to all the hidden features. I think these two ideas go hand-in-hand.
What would these be? Some ideas for names:
| name | feature |
|---|---|
| Wallace | Sidenote / endnote system |
| ??? | Image display system (thumbhash, sizes, backgrounds, performance, layouts) |
| ??? | Inscryption-inspired card layout |
| ??? | System-aware color theme selection, plus vaporwave support |
| ??? | Embedded (esbuild) system for extending website with live software (plots, soon flash cards) |
| ??? | The monstrosity of the current (eleventy++) build system. Maybe include deployment and cache |
| ??? | The (already legacy-ish) maps (static and animated) that appear in many posts |
| ??? | The graph system (providing inbound/outbound links) |
| ??? | The upcoming flash card system |
| ??? | Email digest nightmare (src â (blogpost, react email) â resend broadcast) |