All files / web/src/components/practice LinearEntryReport.tsx

89.38% Statements 101/113
78.26% Branches 18/23
100% Functions 4/4
89.38% Lines 101/113

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 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 1141x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 8x 8x 8x 1x 1x 1x 1x 1x 1x 1x 4x 4x 4x 4x 4x 4x 4x 4x 4x 4x 4x 4x 4x   4x 4x 4x 4x 4x 4x 4x   4x 4x 4x 4x                     4x 4x 1x 1x 1x 4x 3x 4x 4x 1x 1x 5x 5x 5x 5x 5x 5x 5x 4x 4x 4x 4x 4x 4x 4x 4x 4x 4x 4x 4x 4x 4x 4x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 3x 3x 3x 3x 1x 1x 1x 1x  
'use client'
 
import type { LinearEntryAssessment } from '@/lib/curriculum/linear-entry-policy'
import { css } from '../../../styled-system/css'
 
export interface LinearEntryDimension {
  key: 'mastery' | 'volume' | 'accuracy' | 'speed'
  label: string
  met: boolean
  /** Short human phrase, e.g. "73% over last 15, need 85%". */
  detail: string
}
 
function pct(x: number): string {
  return `${Math.round(x * 100)}%`
}
 
/**
 * Turn the entry policy's verdict into parent-readable dimensions. Speed only
 * appears when the policy actually checks it — a dimension that can't fail is
 * noise.
 */
export function describeLinearEntry(entry: LinearEntryAssessment): LinearEntryDimension[] {
  const dims: LinearEntryDimension[] = [
    {
      key: 'mastery',
      label: 'Mastery',
      met: entry.mastery.met,
      detail: entry.mastery.met ? 'mastered' : `${pct(entry.mastery.pKnown)} mastered`,
    },
    {
      key: 'volume',
      label: 'Practice',
      met: entry.volume.met,
      detail: entry.volume.met
        ? `${entry.volume.opportunities} tries`
        : `${entry.volume.opportunities} of ${entry.volume.minOpportunities} tries`,
    },
    {
      key: 'accuracy',
      label: 'Accuracy',
      met: entry.accuracy.met,
      detail:
        entry.accuracy.windowFilled < entry.accuracy.windowSize
          ? `${entry.accuracy.windowFilled} of ${entry.accuracy.windowSize} recent tries`
          : `${pct(entry.accuracy.recentAccuracy)} over last ${entry.accuracy.windowSize}, need ${pct(entry.accuracy.minAccuracy)}`,
    },
  ]
  if (entry.speed.rule !== 'off') {
    dims.push({
      key: 'speed',
      label: 'Speed',
      met: entry.speed.met,
      detail:
        entry.speed.secondsPerTerm == null
          ? 'not enough timed tries'
          : `${entry.speed.secondsPerTerm.toFixed(1)}s per step, need under ${entry.speed.maxSecondsPerTerm}s`,
    })
  }
  return dims
}
 
/** The one thing to say next to a skill name: "ready" or the first unmet dimension. */
export function summarizeLinearEntry(entry: LinearEntryAssessment): string {
  if (entry.ready) return 'ready'
  const blocker = describeLinearEntry(entry).find((d) => !d.met)
  return blocker ? `${blocker.label.toLowerCase()}: ${blocker.detail}` : 'ready'
}
 
export function LinearEntryReport({
  entry,
  variant = 'full',
}: {
  entry: LinearEntryAssessment
  variant?: 'full' | 'compact'
}) {
  if (variant === 'compact') {
    return (
      <span
        data-element="linear-entry-summary"
        data-ready={entry.ready}
        className={css({
          fontSize: '0.75rem',
          color: entry.ready ? 'green.600' : 'gray.500',
          _dark: { color: entry.ready ? 'green.300' : 'gray.400' },
        })}
      >
        {entry.ready ? '✓ ' : ''}
        {summarizeLinearEntry(entry)}
      </span>
    )
  }
  return (
    <ul
      data-element="linear-entry-report"
      className={css({
        display: 'flex',
        flexDirection: 'column',
        gap: '0.125rem',
        fontSize: '0.75rem',
        color: 'gray.600',
        _dark: { color: 'gray.300' },
      })}
    >
      {describeLinearEntry(entry).map((d) => (
        <li key={d.key} data-dimension={d.key} data-met={d.met}>
          <span aria-hidden>{d.met ? '✓' : '○'} </span>
          <strong>{d.label}</strong> · {d.detail}
        </li>
      ))}
    </ul>
  )
}