npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

adaptive-learning-core

v1.0.0

Published

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

Downloads

2

Readme

Adaptive Learning - JavaScript Library

A comprehensive spaced repetition library with three approaches:

  1. Time-Based FSRS - Traditional spaced repetition with time decay
  2. Count-Based - Problem-count intervals for intensive practice
  3. 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:

  1. Prerequisite Check (highest priority)
    • Detours if prerequisite < 60% mastery
  2. Domain Balancing (every N skills)
    • Routes to weakest skill in weakest domain if < 50% avg
  3. 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:

| Parameter | Default | Range | Description | |-----------|---------|-------|-------------| | interferenceRate | 0.020 | 0.010-0.030 | Decay per interfering skill (2%) | | muscleMemoryFloor | 40.0 | 30-50 | Minimum retention % | | protectedRecentCount | 3 | - | Recent skills protected from interference | | defaultStability | 7.0 | 3-14 | Default FSRS stability (days) | | prerequisiteThreshold | 60.0 | 50-70 | % mastery needed to advance | | weakDomainThreshold | 50.0 | 40-60 | Triggers domain balancing | | domainCheckInterval | 5 | 3-10 | Check 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

  1. Zero Dependencies: No runtime dependencies, only dev dependencies for testing
  2. Flexible Models: Adapter methods support different attribute names (id/slug, domain/category, etc.)
  3. Predictable: Pure functions with no hidden state
  4. Well-Tested: Comprehensive test coverage with edge cases
  5. 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:

| Feature | Ruby Gem | JS 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:

  1. Count-based (primary): After solving N other problems
  2. 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

| Approach | Use Case | Example Platform | |----------|----------|------------------| | Time-Based FSRS | Self-paced courses, real-world skills | Docker DCA, Language learning | | Count-Based | Intensive daily practice | LeetCode daily grind | | Hybrid | Mixed practice patterns | Interview 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