All files / web/src/lib/curriculum/bkt bkt-core.ts

87.36% Statements 83/95
50% Branches 4/8
100% Functions 2/2
87.36% Lines 83/95

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 962x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 446147x 446147x 446147x 446147x       446147x       446147x 446147x 446147x 446147x 446147x 446147x 446147x 221901x 221901x 221901x 221901x 221901x 446147x 224246x 224246x 224246x 224246x 224246x 224246x 446147x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 221901x 221901x       221901x       221901x 221901x 221901x  
/**
 * Core BKT (Bayesian Knowledge Tracing) Update Equations
 *
 * Standard BKT is a Hidden Markov Model where:
 * - Hidden state: student knows the skill or not
 * - Observations: correct or incorrect answers
 *
 * Four key parameters:
 * - P(L0) = pInit: prior probability of knowing
 * - P(T) = pLearn: probability of learning on each opportunity
 * - P(S) = pSlip: probability of error despite knowing
 * - P(G) = pGuess: probability of correct despite not knowing
 */
 
import type { BktParams } from './types'
 
/**
 * Standard BKT update for a SINGLE skill given an observation.
 *
 * Uses Bayes' theorem to update P(known) based on the observation:
 *
 * For correct answer:
 *   P(known | correct) = P(correct | known) × P(known) / P(correct)
 *   where P(correct | known) = 1 - P(slip)
 *   and   P(correct | ¬known) = P(guess)
 *
 * For incorrect answer:
 *   P(known | incorrect) = P(incorrect | known) × P(known) / P(incorrect)
 *   where P(incorrect | known) = P(slip)
 *   and   P(incorrect | ¬known) = 1 - P(guess)
 *
 * @param priorPKnown - Current P(known) before this observation
 * @param isCorrect - Whether the student answered correctly
 * @param params - BKT parameters (pInit, pLearn, pSlip, pGuess)
 * @returns Updated P(known) after the observation
 */
export function bktUpdate(priorPKnown: number, isCorrect: boolean, params: BktParams): number {
  const { pSlip, pGuess } = params
 
  // Surface data issues - log warning and let NaN propagate so UI can show error state
  if (!Number.isFinite(priorPKnown)) {
    console.warn('[BKT] Invalid priorPKnown detected:', priorPKnown, '- letting NaN propagate')
    return Number.NaN // Let NaN propagate for UI error boundaries
  }
  if (!Number.isFinite(pSlip) || !Number.isFinite(pGuess)) {
    console.warn('[BKT] Invalid params detected:', { pSlip, pGuess }, '- letting NaN propagate')
    return Number.NaN // Let NaN propagate for UI error boundaries
  }
 
  // Guard against division by zero
  const safeSlip = Math.max(0.001, Math.min(0.999, pSlip))
  const safeGuess = Math.max(0.001, Math.min(0.999, pGuess))
  const safePrior = Math.max(0.001, Math.min(0.999, priorPKnown))
 
  if (isCorrect) {
    // P(correct) = P(known) × (1 - pSlip) + P(¬known) × pGuess
    const pCorrect = safePrior * (1 - safeSlip) + (1 - safePrior) * safeGuess
    // P(known | correct) via Bayes
    const pKnownGivenCorrect = (safePrior * (1 - safeSlip)) / pCorrect
    return pKnownGivenCorrect
  } else {
    // P(incorrect) = P(known) × pSlip + P(¬known) × (1 - pGuess)
    const pIncorrect = safePrior * safeSlip + (1 - safePrior) * (1 - safeGuess)
    // P(known | incorrect) via Bayes
    const pKnownGivenIncorrect = (safePrior * safeSlip) / pIncorrect
    return pKnownGivenIncorrect
  }
}
 
/**
 * Apply learning transition after observation.
 *
 * After each observation, there's a chance the student learned:
 * P(known after) = P(known) + P(¬known) × P(learn)
 *
 * This models the possibility that even if the student didn't know
 * the skill before, they may have learned it from this opportunity.
 *
 * @param pKnown - Current P(known) after Bayesian update
 * @param pLearn - P(T) probability of learning
 * @returns P(known) after learning transition
 */
export function applyLearning(pKnown: number, pLearn: number): number {
  // Surface data issues - log warning and let NaN propagate
  if (!Number.isFinite(pKnown)) {
    console.warn('[BKT] applyLearning: Invalid pKnown:', pKnown, '- letting NaN propagate')
    return Number.NaN
  }
  if (!Number.isFinite(pLearn)) {
    console.warn('[BKT] applyLearning: Invalid pLearn:', pLearn, '- letting NaN propagate')
    return Number.NaN
  }
 
  return pKnown + (1 - pKnown) * pLearn
}