
Security News
pnpm 11.5 Adds Support for Recognizing npm Staged Publishes
pnpm 11.5 now recognizes npm staged publish approvals in release metadata, preventing those releases from being mistaken for lower-trust package publishes.
adaptive-learning-core
Advanced tools
Adaptive learning curriculum routing algorithm combining FSRS spaced repetition with progress interference decay
A comprehensive spaced repetition library with three approaches:
npm install @adaptive-learning/core
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
// }
Skills decay over time and with new learning:
Formula: Current Score = Base × FSRS Retention × Interference Factor
R(t) = e^(-t/S)
Interference = Skills Learned Since × Rate × Similarity
Factor = 1 - min(Interference, 0.60)
The router makes intelligent decisions based on:
Priority Order:
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 |
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)
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)
}
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']
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!
}
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);
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
});
# Run all tests
npm test
# Run with coverage
npm test:coverage
# Watch mode
npm test:watch
All tests pass:
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:
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%
Current_Score = Base × R(t) × Interference_Factor
= Base × e^(-t/S) × (1 - I)
>= muscle_memory_floor (40%)
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)
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
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 |
For coding interview prep, daily practice, or variable-frequency learning.
Time-Based Only:
Count-Based Only:
Triggers reviews when EITHER condition is met:
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)
Count Trigger (Primary):
Time Trigger (Fallback):
// 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
// 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!
// 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
// 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, ... }
See examples/hybrid-interview-prep.js for complete working example with:
| 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 |
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);
MIT
Contributions welcome! Please open an issue or PR.
Based on research in:
FAQs
Adaptive learning curriculum routing algorithm combining FSRS spaced repetition with progress interference decay
We found that adaptive-learning-core demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 1 open source maintainer collaborating on the project.
Did you know?

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.

Security News
pnpm 11.5 now recognizes npm staged publish approvals in release metadata, preventing those releases from being mistaken for lower-trust package publishes.

Security News
Federal audit finds NIST lacked a plan to clear the NVD backlog, wasted funds on duplicate work, and delayed use of CISA data.

Research
/Security News
A mini Shai-Hulud campaign compromised Red Hat Cloud Services npm packages to steal developer and CI/CD secrets during installation.