Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/vruizz22/innova-backend-serverless/llms.txt

Use this file to discover all available pages before exploring further.

After every attempt, Innova updates a per-student, per-topic mastery probability using closed-form Bayesian Knowledge Tracing (Corbett & Anderson, 1995). BKT treats a student’s knowledge of each topic as a hidden binary state — known or not known — and updates the probability of being in the “known” state based on whether the student answered correctly or incorrectly. The result, pKnown, is a single number in [0, 1] that teachers see in real time as a heat-map and trend line.

The BKT Model

BKT is parameterized by four values stored on each Topic row in Postgres:
ParameterFieldDefaultMeaning
pL0bktPL00.30Initial probability the student already knows the topic before any attempt
pTbktPTransit0.10Probability of transitioning from not-known to known after a single attempt
pSbktPSlip0.10Probability of an incorrect response despite knowing (slip)
pGbktPGuess0.20Probability of a correct response without knowing (guess)
These defaults encode a conservative prior: students are assumed to start with 30% mastery and learn slowly, which avoids over-crediting lucky guesses.

Update Formulas

After each observation (correct = 1, incorrect = 0) Innova applies the closed-form update in two steps:
# Step 1: Bayes update on the observation
P(known | obs=1) = (1 - pSlip) · pKnown
                  / [(1 - pSlip) · pKnown + pGuess · (1 - pKnown)]

P(known | obs=0) =      pSlip · pKnown
                  / [pSlip · pKnown + (1 - pGuess) · (1 - pKnown)]

# Step 2: Apply the transit probability
P(Ln) = P(known | obs) + (1 - P(known | obs)) · pTransit
The final pKnown is clamped to [0, 1] before being persisted.

The applyAttempt Implementation

The BKT update lives in MasteryService.applyAttempt. It reads the topic’s BKT parameters from Postgres, loads the student’s current pKnown (defaulting to pL0 on first attempt), applies the formulas above, and upserts the result:
// mastery.service.ts — applyAttempt (excerpt)
async applyAttempt(
  studentId: string,
  topicId: string,
  isCorrect: boolean,
): Promise<MasteryState> {
  await this.prisma.ensureConnected();

  const topic = await this.prisma.topic.findUnique({
    where: { id: topicId },
  });

  const pL0 = topic?.bktPL0 ?? 0.3;
  const pT  = topic?.bktPTransit ?? 0.1;
  const pS  = topic?.bktPSlip ?? 0.1;
  const pG  = topic?.bktPGuess ?? 0.2;

  const existing = await this.prisma.studentTopicMastery.findUnique({
    where: { studentId_topicId: { studentId, topicId } },
  });

  const prior = existing?.pKnown ?? pL0;

  const posteriorGivenObs = isCorrect
    ? ((1 - pS) * prior) / ((1 - pS) * prior + pG * (1 - prior))
    : (pS * prior) / (pS * prior + (1 - pG) * (1 - prior));

  const pKnown = Math.min(
    1,
    Math.max(0, posteriorGivenObs + (1 - posteriorGivenObs) * pT),
  );

  await this.prisma.studentTopicMastery.upsert({
    where: { studentId_topicId: { studentId, topicId } },
    create: {
      studentId,
      topicId,
      pKnown,
      attemptsCount: 1,
      lastAttemptAt: new Date(),
    },
    update: {
      pKnown,
      attemptsCount: { increment: 1 },
      lastAttemptAt: new Date(),
    },
  });

  return { studentId, topicCode: topic?.code ?? topicId, pKnown };
}

Storage

BKT state is stored in the StudentTopicMastery table in Postgres. The key columns are:
ColumnTypeDescription
studentIdStringFK to Student
topicIdStringFK to Topic
pKnownFloatCurrent mastery probability after the latest attempt
attemptsCountIntTotal attempts processed for this (student, topic) pair
lastAttemptAtDateTimeTimestamp of the most recent attempt
The row is upserted after every attempt, so there is exactly one record per (student, topic) pair. Historical trajectories are reconstructed from the Attempt table when needed.

Nightly Recalibration

BKT is only as good as its parameters. A separate service — innova-ai-engine — runs a nightly batch calibrator that analyses the full history of student responses for each topic. It fits new pL0, pT, pS, and pG values using expectation-maximization and writes them back to the Topic table in Postgres. On the next attempt, applyAttempt picks up the updated parameters automatically — no deploy required.

Teacher Visibility

Mastery data is exposed through three read paths:

Per-student mastery

GET /mastery/:studentId returns the current pKnown for every topic the student has attempted, ordered by topic code.

Course heatmap

GET /mastery/course/:courseId/heatmap returns a student × unit matrix. Each cell is the mean pKnown across the unit’s topics for that student.

IRT recommendation

GET /mastery/recommend/:courseId/:studentId uses the BKT pKnown as the input to the IRT Fisher-information item picker — see the IRT Practice page.

Teacher alerts

The dashboard surfaces an alert when a student’s pKnown for a topic drops below a configurable threshold, signalling recent regression.
The heatmap axes are driven by the live taxonomy (Domain → Subdomain). A subdomain appears on the heatmap only when it has real signal — at least one classified attempt or one BKT record — so new guides automatically add new columns without manual configuration.
pKnown is always clamped to [0, 1] by the Math.min(1, Math.max(0, ...)) guard in applyAttempt. Unit tests in mastery.service.spec.ts verify this property for all four boundary combinations of correct/incorrect answers and extreme parameter values.

Build docs developers (and LLMs) love