Big News: Socket raises $60M Series C at a $1B valuation to secure software supply chains for AI-driven development.Announcement
Sign In

adaptive-learning-core

Package Overview
Dependencies
Maintainers
1
Versions
1
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

adaptive-learning-core

Adaptive learning curriculum routing algorithm combining FSRS spaced repetition with progress interference decay

latest
npmnpm
Version
1.0.0
Version published
Maintainers
1
Created
Source

Adaptive Learning - JavaScript Library

A comprehensive spaced repetition library with three approaches:

  • Time-Based FSRS - Traditional spaced repetition with time decay
  • Count-Based - Problem-count intervals for intensive practice
  • Hybrid - Combines both for optimal results

Features

  • Time-Based FSRS: Exponential decay with progress interference
  • Count-Based Scheduling: Discrete intervals [2, 5, 10, 20, 40] problems
  • Hybrid Scheduler: Best of both worlds - count for active practice, time for breaks
  • Adaptive Routing: Prerequisite checking and domain balancing
  • A/B Testing: Built-in cohort assignment for experimentation
  • Zero Dependencies: Pure JavaScript with no external runtime dependencies
  • Fully Tested: 121 comprehensive tests with Jest

Installation

npm install @adaptive-learning/core

Quick Start

import { Router, DecayCalculator, configure } from '@adaptive-learning/core';

// Optional: Configure algorithm parameters
configure({
  interferenceRate: 0.020,        // 2% decay per interfering skill
  muscleMemoryFloor: 40.0,        // Minimum retention percentage
  prerequisiteThreshold: 60.0,    // 60% mastery needed to advance
  weakDomainThreshold: 50.0,      // Trigger balancing below 50%
  domainCheckInterval: 5          // Check domains every 5 skills
});

// Your data models
const user = { id: 1, learningCohort: null }; // Cohort assigned automatically
const chapters = [
  { id: 'ch-1', title: 'Docker Basics', domain: 'containers', prerequisiteSkillIds: [] },
  { id: 'ch-2', title: 'Volumes', domain: 'storage', prerequisiteSkillIds: ['ch-1'] }
];
const masteries = [
  {
    canonicalCommand: 'ch-1',
    proficiencyScore: 75,
    stability: 7.0,
    lastUsedAt: new Date(),
    chaptersAtMastery: 0
  }
];

// Determine next chapter to learn
const router = new Router({
  user,
  currentSkill: chapters[0],
  allSkills: chapters,
  masteries
});

const result = router.nextSkill();
console.log(result);
// {
//   nextSkill: { id: 'ch-2', ... },
//   reason: 'linear',           // or 'prerequisite_gap', 'weak_domain'
//   message: null,              // User-facing message if detour
//   detour: false               // True if prerequisite detour
// }

Core Concepts

1. Hybrid Decay Model

Skills decay over time and with new learning:

Formula: Current Score = Base × FSRS Retention × Interference Factor

FSRS (Time-based Decay)

R(t) = e^(-t/S)
  • t: Days since last practice
  • S: Stability in days (increases with successful reviews)
  • R: Retention factor (0-1)

Progress Interference

Interference = Skills Learned Since × Rate × Similarity
Factor = 1 - min(Interference, 0.60)
  • Recent 3 skills are protected (no interference)
  • Maximum 60% interference cap
  • Default 15% similarity between skills

2. Adaptive Routing

The router makes intelligent decisions based on:

Priority Order:

  • Prerequisite Check (highest priority)
    • Detours if prerequisite < 60% mastery
  • Domain Balancing (every N skills)
    • Routes to weakest skill in weakest domain if < 50% avg
  • Linear Progression (default)
    • Continue with next skill in sequence

3. A/B Testing Cohorts

  • Linear Cohort: Traditional sequential learning (control group)
  • Adaptive Cohort: Smart routing with prerequisites + domain balancing (experimental)

API Reference

Configuration

import { configure, getConfiguration, resetConfiguration } from '@adaptive-learning/core';

// Modify shared configuration
configure({
  interferenceRate: 0.025,
  defaultStability: 10.0
});

// Get current configuration
const config = getConfiguration();

// Reset to defaults
resetConfiguration();

Available Parameters:

ParameterDefaultRangeDescription
interferenceRate0.0200.010-0.030Decay per interfering skill (2%)
muscleMemoryFloor40.030-50Minimum retention %
protectedRecentCount3-Recent skills protected from interference
defaultStability7.03-14Default FSRS stability (days)
prerequisiteThreshold60.050-70% mastery needed to advance
weakDomainThreshold50.040-60Triggers domain balancing
domainCheckInterval53-10Check domains every N skills

DecayCalculator

import { DecayCalculator } from '@adaptive-learning/core';

const calculator = new DecayCalculator();

// Calculate current decayed score
const currentScore = calculator.calculateCurrentScore({
  baseScore: 80,
  stability: 7.0,
  lastUsedAt: new Date('2024-01-01'),
  skillsLearnedSince: 10,
  similarities: { 'skill-2': 0.75, 'skill-3': 0.60 }
});
// Returns: ~45-55 (decayed due to time + interference)

// Calculate FSRS retention only
const retention = calculator.calculateFSRSRetention(
  new Date('2024-01-01'), // lastUsedAt
  7.0                     // stability
);
// Returns: ~0.368 after 7 days (e^(-7/7))

// Calculate interference only
const interference = calculator.calculateInterference(
  10,                                      // skillsLearnedSince
  { 'skill-2': 0.75, 'skill-3': 0.60 }    // similarities
);
// Returns: 0.40-1.0 (interference factor)

// Update stability after review
const newStability = calculator.updateStability(
  7.0,   // currentStability
  true,  // reviewSuccess
  1      // difficulty (1=very easy, 5=very hard)
);
// Returns: 17.5 (2.5x increase for very easy)

Router

import { Router } from '@adaptive-learning/core';

const router = new Router({
  user,           // { learningCohort: 'linear' | 'adaptive' }
  currentSkill,   // Currently completed skill
  allSkills,      // Array of all skills/chapters
  masteries,      // Array of mastery records
  config          // Optional custom configuration
});

const result = router.nextSkill();
// {
//   nextSkill: <Skill object or null>,
//   reason: 'linear' | 'prerequisite_gap' | 'weak_domain',
//   message: 'Let\'s review X first...' | null,
//   detour: true | false
// }

Model Requirements:

User:

{
  learningCohort: 'linear' | 'adaptive' | null  // Auto-assigned if null
}

Skill/Chapter:

{
  id: string,                        // or slug
  title: string,
  domain: string,                    // or category (optional)
  prerequisiteSkillIds: string[]     // or prerequisites (optional)
}

Mastery:

{
  canonicalCommand: string,    // or skillId - links to skill.id
  proficiencyScore: number,    // 0-100
  stability: number,           // FSRS stability in days
  lastUsedAt: Date,           // When last practiced
  chaptersAtMastery: number   // Position when mastered (for interference)
}

CohortAssigner

import { CohortAssigner } from '@adaptive-learning/core';

// Assign random cohort (50/50 split)
const cohort = CohortAssigner.assign();
// Returns: 'linear' or 'adaptive'

// Validate cohort
CohortAssigner.valid('linear');    // true
CohortAssigner.valid('invalid');   // false

// Get all cohorts
CohortAssigner.all();
// Returns: ['linear', 'adaptive']

Usage Examples

Complete Learning Flow

import { Router, DecayCalculator } from '@adaptive-learning/core';

// 1. User completes a chapter
const completedChapter = chapters.find(c => c.id === 'ch-1');
const userScore = 85;      // User's score on the chapter
const difficulty = 2;      // User-rated difficulty (1-5)

// 2. Find or create mastery record
let mastery = masteries.find(m => m.canonicalCommand === completedChapter.id);
if (!mastery) {
  mastery = {
    canonicalCommand: completedChapter.id,
    proficiencyScore: 0,
    stability: 7.0,
    lastUsedAt: new Date(),
    chaptersAtMastery: masteries.length
  };
  masteries.push(mastery);
}

// 3. Update stability based on performance
const calculator = new DecayCalculator();
const success = userScore >= 70;
mastery.stability = calculator.updateStability(
  mastery.stability,
  success,
  difficulty
);

// 4. Update mastery score and timestamp
mastery.proficiencyScore = userScore;
mastery.lastUsedAt = new Date();

// 5. Route to next chapter
const router = new Router({
  user,
  currentSkill: completedChapter,
  allSkills: chapters,
  masteries
});

const result = router.nextSkill();

// 6. Show message if detour
if (result.detour && result.message) {
  showFlashMessage(result.message);
  // "Let's review Docker Networks first - it's a prerequisite for what's next."
}

// 7. Navigate to next chapter
if (result.nextSkill) {
  navigateTo(`/chapters/${result.nextSkill.id}`);
} else {
  showCompletionCelebration(); // End of course!
}

Calculate Decayed Scores for Review Dashboard

import { DecayCalculator } from '@adaptive-learning/core';

const calculator = new DecayCalculator();

// Get all skills that need review (decayed below 70%)
const skillsNeedingReview = masteries
  .map(mastery => {
    const skill = chapters.find(c => c.id === mastery.canonicalCommand);
    const skillsLearnedSince = masteries.length - mastery.chaptersAtMastery - 1;

    const currentScore = calculator.calculateCurrentScore({
      baseScore: mastery.proficiencyScore,
      stability: mastery.stability,
      lastUsedAt: mastery.lastUsedAt,
      skillsLearnedSince,
      similarities: {} // Can add similarity data if available
    });

    return {
      skill,
      originalScore: mastery.proficiencyScore,
      currentScore,
      decayAmount: mastery.proficiencyScore - currentScore
    };
  })
  .filter(item => item.currentScore < 70)
  .sort((a, b) => a.currentScore - b.currentScore); // Weakest first

console.log('Skills needing review:', skillsNeedingReview);

Custom Configuration Per Course

import { Configuration, Router } from '@adaptive-learning/core';

// Docker DCA course (fast-paced, practical skills)
const dockerConfig = Configuration.create({
  interferenceRate: 0.020,
  muscleMemoryFloor: 40.0,
  defaultStability: 7.0,
  prerequisiteThreshold: 60.0
});

// Academic course (slower, deeper mastery)
const academicConfig = Configuration.create({
  interferenceRate: 0.010,
  muscleMemoryFloor: 50.0,
  defaultStability: 14.0,
  prerequisiteThreshold: 75.0
});

// Use course-specific config
const router = new Router({
  user,
  currentSkill,
  allSkills,
  masteries,
  config: dockerConfig  // Pass custom config
});

Testing

# Run all tests
npm test

# Run with coverage
npm test:coverage

# Watch mode
npm test:watch

All tests pass:

  • ✅ 69 comprehensive tests
  • ✅ DecayCalculator: FSRS formulas, interference, stability updates
  • ✅ Router: Linear routing, prerequisite checking, domain balancing
  • ✅ Configuration: Defaults, customization, singleton
  • ✅ CohortAssigner: Random assignment, validation

Mathematical Formulas

FSRS Retention (Time-based Decay)

R(t) = e^(-t/S)

Where:
- R = retention factor (0-1)
- t = time elapsed (days)
- S = stability (days)
- e = Euler's number (≈2.71828)

Examples:

  • After 1 day with 7-day stability: e^(-1/7) ≈ 0.867 (86.7%)
  • After 7 days with 7-day stability: e^(-7/7) ≈ 0.368 (36.8%)
  • After 14 days with 7-day stability: e^(-14/7) ≈ 0.135 (13.5%)

Progress Interference

I = Σ (rate × similarity × count)
  = (skills_learned - protected_count) × interference_rate × avg_similarity

Interference_Factor = 1 - min(I, 0.60)

Where:
- rate = 0.020 (2% default)
- protected_count = 3 (default)
- avg_similarity = 0.15 (default if not specified)
- max interference = 60%

Combined Score

Current_Score = Base × R(t) × Interference_Factor
              = Base × e^(-t/S) × (1 - I)
              >= muscle_memory_floor (40%)

Stability Update

Success:
  S_new = S_old × factor
  factor = 2.5 - (difficulty - 1) × 0.325
  - difficulty 1 (very easy): 2.5x
  - difficulty 2 (easy): 2.175x
  - difficulty 3 (medium): 1.85x
  - difficulty 4 (hard): 1.525x
  - difficulty 5 (very hard): 1.2x
  S_new = min(S_new, 180 days)

Failure:
  S_new = S_old × 0.5
  S_new = max(S_new, 1 day)

Architecture

adaptive-learning-js/
├── src/
│   ├── Configuration.js      # Algorithm parameters
│   ├── DecayCalculator.js    # FSRS + Interference calculations
│   ├── Router.js             # Adaptive routing logic
│   ├── CohortAssigner.js     # A/B testing
│   └── index.js              # Main exports
├── test/
│   ├── Configuration.test.js
│   ├── DecayCalculator.test.js
│   ├── Router.test.js
│   └── CohortAssigner.test.js
├── examples/
│   └── integration.js        # Complete integration example
└── package.json

Design Principles

  • Zero Dependencies: No runtime dependencies, only dev dependencies for testing
  • Flexible Models: Adapter methods support different attribute names (id/slug, domain/category, etc.)
  • Predictable: Pure functions with no hidden state
  • Well-Tested: Comprehensive test coverage with edge cases
  • Configurable: All algorithm parameters can be tuned per use case

Comparison with Ruby Gem

This JavaScript library is a functionally identical port of the Ruby gem adaptive-learning-gem:

FeatureRuby GemJS Library
FSRS Decay
Progress Interference
Adaptive Routing
Prerequisite Checking
Domain Balancing
A/B Testing
Zero Dependencies
Test Coverage✅ 40+ tests✅ 69 tests

Use Cases

  • E-learning Platforms: Adaptive course sequencing
  • Coding Bootcamps: Personalized curriculum paths
  • Certification Training: Prerequisite-aware learning (Docker DCA, AWS, etc.)
  • Language Learning: Spaced repetition with contextual interference
  • Corporate Training: Domain-balanced skill development
  • MOOCs: A/B testing different pedagogical approaches

Hybrid Scheduler (NEW!)

For coding interview prep, daily practice, or variable-frequency learning.

The Problem with Pure Approaches

Time-Based Only:

  • Active users (10+ problems/day) don't benefit from time decay
  • Reviews trigger too slowly for intensive practice
  • Doesn't leverage interleaving benefits

Count-Based Only:

  • Users who take breaks don't review (forgotten but count not met)
  • Casual users wait forever for count thresholds
  • Ignores real forgetting over time

The Hybrid Solution

Triggers reviews when EITHER condition is met:

  • Count-based (primary): After solving N other problems
  • Time-based (fallback): When retention drops below 70%

Quick Example

import { HybridScheduler } from '@adaptive-learning/core';

const scheduler = new HybridScheduler({
  retentionThreshold: 0.70,  // Review when retention < 70%
  timeWeight: 0.5,           // Balance both priorities
  countWeight: 0.5
});

// Create problem history
const history = scheduler.createProblemHistory('two-pointers-1', 7.0);

// User solves a problem
scheduler.completeProblem(problemHistories, 'two-pointers-1', true, 2);

// Check if review needed
const decision = scheduler.shouldReview({
  problemsSolvedSince: 10,      // Solved 10 other problems
  consecutiveCorrect: 1,        // Need 5 for count trigger
  lastPracticedAt: threeDaysAgo, // 3 days ago
  stability: 7.0
});

console.log(decision.shouldReview);  // true
console.log(decision.reason);        // 'count' (primary trigger)

When Each Trigger Fires

Count Trigger (Primary):

  • Active daily users solving many problems
  • Provides optimal interleaving
  • Intervals: [2, 5, 10, 20, 40] problems

Time Trigger (Fallback):

  • Users who take breaks (vacation, busy week)
  • Casual users with low problem volume
  • Prevents "review starvation"

Example Scenarios

Scenario 1: Active Daily User

// User solves 15 problems/day
// Problem A last seen 1 day ago, 10 problems solved since

scheduler.shouldReview({
  problemsSolvedSince: 10,      // OVER interval (need 5)
  consecutiveCorrect: 1,
  lastPracticedAt: oneDayAgo,   // Only 1 day (high retention)
  stability: 7.0
});
// → shouldReview: true
// → reason: 'count'  ← Count triggered, time didn't

Scenario 2: User with Break

// User on vacation for 2 weeks, no problems solved

scheduler.shouldReview({
  problemsSolvedSince: 0,       // No problems (count won't trigger)
  consecutiveCorrect: 2,        // Need 10 problems
  lastPracticedAt: fourteenDaysAgo, // 2 weeks!
  stability: 7.0
});
// → shouldReview: true
// → reason: 'time'  ← Time caught the gap!

Scenario 3: Casual User

// User solves 2 problems/week (would take 5 weeks to reach count threshold)

scheduler.shouldReview({
  problemsSolvedSince: 2,       // Only 2 problems (need 10)
  consecutiveCorrect: 2,
  lastPracticedAt: sevenDaysAgo, // 1 week ago
  stability: 7.0
});
// → shouldReview: true
// → reason: 'time'  ← Time prevents starvation

API

// Create scheduler
const scheduler = new HybridScheduler({
  retentionThreshold: 0.70,  // Review when < 70% retention
  timeWeight: 0.5,           // Weight for time priority
  countWeight: 0.5           // Weight for count priority
});

// Check if review needed
const decision = scheduler.shouldReview({
  problemsSolvedSince,
  consecutiveCorrect,
  lastPracticedAt,
  stability
});
// Returns: { shouldReview, reason, countPriority, timePriority, retention, ... }

// Select most urgent problem
const next = scheduler.selectNextProblem(problems);
// Returns: { problem, decision, priority }

// Complete a problem (updates ALL state)
scheduler.completeProblem(problemHistories, problemId, wasCorrect, difficulty);

// Get statistics
const stats = scheduler.getReviewStatistics(problems);
// Returns: { total, countTriggered, timeTriggered, averagePriority, ... }

Integration Example

See examples/hybrid-interview-prep.js for complete working example with:

  • Active daily user scenario
  • User with breaks scenario
  • Casual user scenario
  • Statistics comparison

When to Use What

ApproachUse CaseExample Platform
Time-Based FSRSSelf-paced courses, real-world skillsDocker DCA, Language learning
Count-BasedIntensive daily practiceLeetCode daily grind
HybridMixed practice patternsInterview prep courses

Count-Based Scheduler (Standalone)

Can also use count-based scheduling independently:

import { CountBasedScheduler } from '@adaptive-learning/core';

const scheduler = new CountBasedScheduler();

// Get interval for mastery level
const interval = scheduler.getReviewInterval(consecutiveCorrect);
// 0 → 2, 1 → 5, 2 → 10, 3 → 20, 4+ → 40

// Check if due
const due = scheduler.shouldReview(problemsSolvedSince, consecutiveCorrect);

// Calculate overdueness
const priority = scheduler.calculatePriority(problemsSolvedSince, consecutiveCorrect);

// Complete problem (auto-updates all counters)
scheduler.completeProblem(problemHistories, problemId, wasCorrect);

License

MIT

Contributing

Contributions welcome! Please open an issue or PR.

Acknowledgments

Based on research in:

  • FSRS (Free Spaced Repetition Scheduler)
  • Spaced repetition algorithms
  • Count-based interleaving for problem-solving
  • Adaptive learning systems
  • Curriculum sequencing theory

Keywords

adaptive-learning

FAQs

Package last updated on 03 Nov 2025

Did you know?

Socket

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Install

Related posts