Dec 6, 2025

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:

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:

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

It works!

Prototyping Flash Cards

Happening here.

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)

post info


Published Dec 6, 2025
Tags blog recurse daily
Outbound