All files / web/src/lib/curriculum part-gating.ts

100% Statements 70/70
93.33% Branches 14/15
100% Functions 1/1
100% Lines 70/70

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 712x 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 5x 5x 5x 5x 5x 5x 5x 5x 5x 5x 5x 5x 15x 15x 5x 5x 5x 15x 4x 1x 4x 4x 4x 15x 1x 1x 1x 1x 1x 15x 1x 15x 1x 1x 1x 1x 15x 5x 5x 5x  
/**
 * Part gating for session plans — the pure decision of which requested part
 * types survive the readiness gates, and WHY the others were dropped.
 *
 * Kept free of DB imports so it is unit-testable and so the modal can show the
 * same reasons the planner acted on (`skippedParts` on the plan summary and in
 * the `plan_structure_ready` progress event).
 */
 
import type { SessionPartType, SkippedPart } from '@/db/schema/session-plans'
 
export interface ResolveEnabledPartTypesInput {
  /** Parts the caller asked for */
  partsToInclude: Record<SessionPartType, boolean>
  /** At least one skill is at the visual practice level */
  hasVisualSkills: boolean
  /** The L3 derived-readiness flag */
  linearReadinessEnabled: boolean
  /** Linear-ready skills after teacher vetoes (flag on only) */
  linearReadyCount: number
  /** Linear-ready skills ignoring vetoes (flag on only) — separates "vetoed" from "not ready" */
  linearReadyBeforeVetoCount: number
}
 
export interface ResolvedPartTypes {
  enabledPartTypes: SessionPartType[]
  skippedParts: SkippedPart[]
}
 
export function resolveEnabledPartTypes(input: ResolveEnabledPartTypesInput): ResolvedPartTypes {
  const {
    partsToInclude,
    hasVisualSkills,
    linearReadinessEnabled,
    linearReadyCount,
    linearReadyBeforeVetoCount,
  } = input
 
  const enabledPartTypes: SessionPartType[] = []
  const skippedParts: SkippedPart[] = []
 
  for (const type of ['abacus', 'visualization', 'linear'] as const) {
    if (!partsToInclude[type]) continue
    if (type === 'abacus') {
      enabledPartTypes.push(type)
      continue
    }
    if (type === 'visualization') {
      if (hasVisualSkills) enabledPartTypes.push(type)
      else skippedParts.push({ type, reason: 'no-visual-skills' })
      continue
    }
    // linear
    if (!linearReadinessEnabled) {
      // Flag off: linear rides on the visual coupling (legacy behaviour)
      if (hasVisualSkills) enabledPartTypes.push(type)
      else skippedParts.push({ type, reason: 'no-visual-skills' })
      continue
    }
    if (linearReadyCount > 0) {
      enabledPartTypes.push(type)
    } else if (linearReadyBeforeVetoCount > 0) {
      skippedParts.push({ type, reason: 'vetoed' })
    } else {
      skippedParts.push({ type, reason: 'not-ready' })
    }
  }
 
  return { enabledPartTypes, skippedParts }
}