All files / web/src/lib/curriculum linear-readiness-service.ts

0% Statements 0/75
0% Branches 0/1
0% Functions 0/1
0% Lines 0/75

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                                                                                                                                                       
/**
 * Linear-readiness state service (L3)
 *
 * DB-touching orchestration around the pure `linear-readiness` derivation: loads a
 * student's mastery, history, BKT and vetoes, and reports which skill categories are
 * (or would be) linear-ready plus their veto state — the data feed for the
 * graduation banner and the per-category veto chip.
 */

import { computeBktFromHistory } from './bkt'
import { BKT_INTEGRATION_CONFIG } from './config'
import { getRecentSessionResults } from './session-planner'
import { getAllSkillMastery, getLinearReadinessVetoes } from './progress-manager'
import { deriveLinearReadySkills, groupLinearReadyByCategory } from './linear-readiness'
import { getCategoryDisplayName, type SkillCategoryKey } from '@/constants/skillCategories'
import { isEnabled } from '@/lib/feature-flags'

export interface LinearReadyCategory {
  category: SkillCategoryKey
  /** Human-readable category name (e.g. "Ten Complements (Addition)"). */
  name: string
  /** The linear-ready skill ids in this category. */
  skillIds: string[]
  /** Whether the teacher has vetoed this category off number sentences. */
  vetoed: boolean
}

export interface LinearReadinessState {
  /** Whether the `linear_readiness` feature flag is on. */
  enabled: boolean
  /** Categories that are (or would be, ignoring veto) linear-ready. */
  categories: LinearReadyCategory[]
}

/**
 * Compute the linear-readiness state for a student. Returns an inert empty state
 * when the feature flag is off (no history/BKT work done).
 *
 * The derivation runs with an EMPTY veto set so the result includes vetoed
 * categories too (annotated with `vetoed: true`); the banner/chip need to show a
 * vetoed category so the teacher can lift the veto.
 */
export async function getLinearReadinessState(playerId: string): Promise<LinearReadinessState> {
  const enabled = await isEnabled('linear_readiness.enabled', false)
  if (!enabled) return { enabled: false, categories: [] }

  const [skillMastery, problemHistory, vetoes] = await Promise.all([
    getAllSkillMastery(playerId),
    getRecentSessionResults(playerId, BKT_INTEGRATION_CONFIG.sessionHistoryDepth),
    getLinearReadinessVetoes(playerId),
  ])

  const bktResults =
    problemHistory.length > 0
      ? new Map(computeBktFromHistory(problemHistory).skills.map((s) => [s.skillId, s]))
      : undefined

  const allReady = deriveLinearReadySkills({
    skillMastery,
    problemHistory,
    bktResults,
    vetoedCategories: new Set(), // ignore vetoes here; annotate below
  })

  const categories: LinearReadyCategory[] = [...groupLinearReadyByCategory(allReady).entries()].map(
    ([category, skillIds]) => ({
      category,
      name: getCategoryDisplayName(category),
      skillIds,
      vetoed: vetoes.has(category),
    })
  )

  return { enabled: true, categories }
}