Overview
The mastery system calculates how well a learner understands each concept based on quiz performance. Mastery levels range from 0-100% and determine when concepts need reinforcement.
Mastery Calculation
Mastery level is calculated as a simple percentage:
// From server/services/mastery-service.ts:50
const newMasteryLevel = newTotalCount > 0
? Math . round (( newCorrectCount / newTotalCount ) * 100 )
: 0 ;
Mastery Level = (Correct Answers / Total Attempts) × 100This is rounded to the nearest integer for storage.
Example Calculations
High Mastery
Developing
Struggling
First Attempt
Concept : Addition
Correct: 18
Total: 20
Mastery : 90%
✅ Above threshold (70%) — concept mastered Concept : Multiplication
Correct: 8
Total: 12
Mastery : 67%
⚠️ Below threshold — needs reinforcement Concept : Fractions
Correct: 3
Total: 10
Mastery : 30%
❌ Well below threshold — priority for practice Concept : Division (new)
Correct: 1
Total: 1
Mastery : 100%
ℹ️ First attempt success, but needs more data
Mastery Threshold
// From server/services/mastery-service.ts:25
const MASTERY_THRESHOLD = 70 ; // 70% accuracy for mastery
Concepts are considered “mastered” at 70% or higher accuracy.
Threshold Rationale 70% is chosen as a balance between:
Too Low (e.g., 50%): Learner may have significant gaps
Too High (e.g., 90%): Unrealistic expectation, may discourage learners
70% indicates solid understanding while allowing room for growth.
Updating Mastery
Mastery is updated after each quiz question:
// From server/services/mastery-service.ts:30
export async function updateConceptMastery (
learnerId : number ,
conceptName : string ,
subject : string ,
isCorrect : boolean
) : Promise < void > {
try {
// Check if mastery record exists
const existing = await db . execute ( sql `
SELECT * FROM concept_mastery
WHERE learner_id = ${ learnerId }
AND concept_name = ${ conceptName }
AND subject = ${ subject }
` );
if ( existing . rows . length > 0 ) {
// Update existing record
const record = existing . rows [ 0 ] as any ;
const newCorrectCount = record . correct_count + ( isCorrect ? 1 : 0 );
const newTotalCount = record . total_count + 1 ;
const newMasteryLevel = newTotalCount > 0
? Math . round (( newCorrectCount / newTotalCount ) * 100 )
: 0 ;
const needsReinforcement = newMasteryLevel < MASTERY_THRESHOLD ;
await db . execute ( sql `
UPDATE concept_mastery
SET
correct_count = ${ newCorrectCount } ,
total_count = ${ newTotalCount } ,
mastery_level = ${ newMasteryLevel } ,
last_tested = NOW(),
needs_reinforcement = ${ needsReinforcement }
WHERE learner_id = ${ learnerId }
AND concept_name = ${ conceptName }
AND subject = ${ subject }
` );
} else {
// Create new record
const masteryLevel = isCorrect ? 100 : 0 ;
const needsReinforcement = masteryLevel < MASTERY_THRESHOLD ;
await db . execute ( sql `
INSERT INTO concept_mastery (
learner_id, concept_name, subject,
correct_count, total_count, mastery_level,
last_tested, needs_reinforcement
) VALUES (
${ learnerId } , ${ conceptName } , ${ subject } ,
${ isCorrect ? 1 : 0 } , 1, ${ masteryLevel } ,
NOW(), ${ needsReinforcement }
)
` );
}
} catch ( error ) {
console . error ( `Error updating mastery for concept ${ conceptName } :` , error );
throw error ;
}
}
Question Answered
Quiz answer is submitted with concept tags
Check Existing Mastery
Look for existing mastery record for this learner + concept + subject
Update or Create
If exists, update counts and recalculate mastery If new, create record with 100% (correct) or 0% (wrong)
Flag for Reinforcement
Set needs_reinforcement = true if mastery < 70%
Update Timestamp
Record when the concept was last tested
Bulk Updates from Quiz
After a full quiz, update mastery for all concepts at once:
// From server/services/mastery-service.ts:101
export async function updateMasteryFromQuiz (
learnerId : number ,
subject : string ,
conceptTags : string [],
isCorrect : boolean
) : Promise < void > {
for ( const concept of conceptTags ) {
await updateConceptMastery ( learnerId , concept , subject , isCorrect );
}
}
// From server/services/mastery-service.ts:267
export async function bulkUpdateMasteryFromAnswers (
learnerId : number ,
subject : string ,
conceptsAndCorrectness : Array <{ concepts : string []; isCorrect : boolean }>
) : Promise < void > {
for ( const { concepts , isCorrect } of conceptsAndCorrectness ) {
await updateMasteryFromQuiz ( learnerId , subject , concepts , isCorrect );
}
}
Retrieving Learner Mastery
Get All Mastery Records
// From server/services/mastery-service.ts:114
export async function getLearnerMastery (
learnerId : number ,
subject ?: string
) : Promise < ConceptMastery []> {
try {
let query ;
if ( subject ) {
query = sql `
SELECT * FROM concept_mastery
WHERE learner_id = ${ learnerId }
AND subject = ${ subject }
ORDER BY mastery_level ASC, last_tested DESC
` ;
} else {
query = sql `
SELECT * FROM concept_mastery
WHERE learner_id = ${ learnerId }
ORDER BY subject, mastery_level ASC
` ;
}
const results = await db . execute ( query );
return results . rows . map ( row => ({
id: ( row as any ). id ,
learnerId: ( row as any ). learner_id ,
conceptName: ( row as any ). concept_name ,
subject: ( row as any ). subject ,
correctCount: ( row as any ). correct_count ,
totalCount: ( row as any ). total_count ,
masteryLevel: parseFloat (( row as any ). mastery_level ),
lastTested: new Date (( row as any ). last_tested ),
needsReinforcement: ( row as any ). needs_reinforcement ,
createdAt: ( row as any ). created_at ? new Date (( row as any ). created_at ) : undefined
}));
} catch ( error ) {
console . error ( 'Error fetching learner mastery:' , error );
return [];
}
}
Get Concepts Needing Reinforcement
// From server/services/mastery-service.ts:158
export async function getConceptsNeedingReinforcement (
learnerId : number ,
subject ?: string ,
limit : number = 5
) : Promise < ConceptMastery []> {
try {
let query ;
if ( subject ) {
query = sql `
SELECT * FROM concept_mastery
WHERE learner_id = ${ learnerId }
AND subject = ${ subject }
AND needs_reinforcement = true
ORDER BY mastery_level ASC, last_tested ASC
LIMIT ${ limit }
` ;
} else {
query = sql `
SELECT * FROM concept_mastery
WHERE learner_id = ${ learnerId }
AND needs_reinforcement = true
ORDER BY mastery_level ASC, last_tested ASC
LIMIT ${ limit }
` ;
}
const results = await db . execute ( query );
return results . rows . map ( /* ... */ );
} catch ( error ) {
console . error ( 'Error fetching concepts needing reinforcement:' , error );
return [];
}
}
Results are sorted by:
Lowest mastery level first (most struggling)
Oldest last_tested first (hasn’t practiced recently)
This prioritizes concepts that are both low-performing and haven’t been reviewed recently.
Mastery Summary
Get an overview of all mastery data for a learner:
// From server/services/mastery-service.ts:206
export async function getMasterySummary (
learnerId : number
) : Promise <{
totalConcepts : number ;
masteredConcepts : number ;
needsReinforcementCount : number ;
averageMastery : number ;
bySubject : Record < string , { mastered : number ; total : number ; avgMastery : number }>;
}> {
try {
const allMastery = await getLearnerMastery ( learnerId );
const totalConcepts = allMastery . length ;
const masteredConcepts = allMastery . filter ( m => m . masteryLevel >= MASTERY_THRESHOLD ). length ;
const needsReinforcementCount = allMastery . filter ( m => m . needsReinforcement ). length ;
const averageMastery =
totalConcepts > 0
? allMastery . reduce (( sum , m ) => sum + m . masteryLevel , 0 ) / totalConcepts
: 0 ;
// Calculate by subject
const bySubject : Record < string , { mastered : number ; total : number ; avgMastery : number }> = {};
for ( const mastery of allMastery ) {
if ( ! bySubject [ mastery . subject ]) {
bySubject [ mastery . subject ] = { mastered: 0 , total: 0 , avgMastery: 0 };
}
bySubject [ mastery . subject ]. total ++ ;
if ( mastery . masteryLevel >= MASTERY_THRESHOLD ) {
bySubject [ mastery . subject ]. mastered ++ ;
}
bySubject [ mastery . subject ]. avgMastery += mastery . masteryLevel ;
}
// Calculate averages
for ( const subject in bySubject ) {
bySubject [ subject ]. avgMastery /= bySubject [ subject ]. total ;
}
return {
totalConcepts ,
masteredConcepts ,
needsReinforcementCount ,
averageMastery ,
bySubject
};
} catch ( error ) {
console . error ( 'Error calculating mastery summary:' , error );
return {
totalConcepts: 0 ,
masteredConcepts: 0 ,
needsReinforcementCount: 0 ,
averageMastery: 0 ,
bySubject: {}
};
}
}
Example response:
{
"totalConcepts" : 45 ,
"masteredConcepts" : 32 ,
"needsReinforcementCount" : 13 ,
"averageMastery" : 76.8 ,
"bySubject" : {
"Math" : {
"mastered" : 15 ,
"total" : 20 ,
"avgMastery" : 78.5
},
"Science" : {
"mastered" : 17 ,
"total" : 25 ,
"avgMastery" : 75.2
}
}
}
Mastery Decay (Future Enhancement)
Not yet implemented — but planned for future releases.
Mastery decay will reduce mastery levels over time if concepts aren’t practiced:
// Planned implementation
interface MasteryDecayConfig {
decayRate : number ; // Percentage lost per day
decayThreshold : number ; // Mastery level below which no decay occurs
decayInterval : number ; // Days between decay calculations
}
const DEFAULT_DECAY = {
decayRate: 0.5 , // 0.5% per day
decayThreshold: 40 , // Stop decay at 40%
decayInterval: 1 // Check daily
};
Planned Decay Algorithm
Calculate Days Since Last Test
daysSince = (NOW - last_tested) / 86400
Calculate Decay Amount
decay = daysSince * decayRate
Apply Decay
newMastery = max(decayThreshold, currentMastery - decay)
Update Record
Save new mastery level and set needs_reinforcement if below threshold
Example Decay Scenario
Concept : Multiplication
Current Mastery : 85%
Last Tested : 30 days ago
Decay : 30 days × 0.5% = 15%
New Mastery : 85% - 15% = 70%
Spaced Repetition (Future Enhancement)
Planned integration with spaced repetition algorithm:
Concepts are grouped into “boxes” based on mastery:
Box 1 (0-40%): Review daily
Box 2 (41-60%): Review every 3 days
Box 3 (61-80%): Review weekly
Box 4 (81-100%): Review monthly
When a concept is answered correctly, it moves up a box. When wrong, it moves down.
More sophisticated algorithm that calculates optimal review intervals based on:
Number of repetitions
Ease factor (how easy the concept is for this learner)
Time since last review
Answer quality
Interval = previous_interval × ease_factor
Data Structure
// From server/services/mastery-service.ts:12
export interface ConceptMastery {
id ?: string ;
learnerId : number ;
conceptName : string ;
subject : string ;
correctCount : number ;
totalCount : number ;
masteryLevel : number ; // 0-100 integer
lastTested : Date ;
needsReinforcement : boolean ;
createdAt ?: Date ;
}
Visual Mastery Indicators
0-40%: Struggling
41-69%: Developing
70-84%: Proficient
85-100%: Advanced
🔴 Red — Priority for immediate practice
🟡 Yellow — Needs reinforcement
🔵 Blue — Mastered, occasional review
🟢 Green — Strong mastery, minimal review needed
Best Practices
Regular Practice Concepts should be tested regularly, ideally every few days, to maintain mastery and provide accurate data.
Balanced Coverage Ensure learners practice both new concepts and review older ones to prevent mastery decay.
Celebrate Milestones When a concept crosses 70% threshold, celebrate! When it reaches 90%, extra recognition.
Address Struggles Early Concepts below 50% should get immediate attention before gaps widen.
API Examples
Check Mastery for a Concept
const mastery = await getLearnerMastery ( 42 , 'Math' );
const fractionMastery = mastery . find ( m => m . conceptName === 'fractions' );
if ( fractionMastery && fractionMastery . masteryLevel < 70 ) {
console . log ( 'Needs more fraction practice!' );
}
Get Top Priority Concepts
const struggling = await getConceptsNeedingReinforcement ( 42 , undefined , 5 );
struggling . forEach (( concept , index ) => {
console . log ( ` ${ index + 1 } . ${ concept . conceptName } : ${ concept . masteryLevel } %` );
});
Generate Mastery Report
const summary = await getMasterySummary ( 42 );
console . log ( `Overall Progress: ${ summary . masteredConcepts } / ${ summary . totalConcepts } concepts mastered` );
console . log ( `Average Mastery: ${ summary . averageMastery . toFixed ( 1 ) } %` );
console . log ( `Needs Practice: ${ summary . needsReinforcementCount } concepts` );