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 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 | 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 5x 5x 5x 4x 4x 4x 4x 4x 4x 4x 4x 4x 4x 4x 5x 5x 5x 5x 5x 5x 5x 5x 5x 5x 5x 5x 2x 2x 2x 2x 2x 2x 2x 2x 5x 5x 5x 5x 4x 4x 4x 4x 4x 4x 4x 4x 4x 4x 4x 4x 4x 12x 12x 12x 12x 12x 12x 4x 4x 4x 4x 4x | /**
* Linear readiness state for a player — the single contract behind the
* start-practice modal's locked segment, the dashboard's "Number sentences"
* panel and the graduation banner.
*
* Server-only (reads the DB and feature flags). The client reads it through
* `GET /api/curriculum/[playerId]/linear-veto` and `useLinearReadiness`.
*/
import { getCategoryDisplayName, type SkillCategoryKey } from '@/constants/skillCategories'
import {
type LinearEntryAssessment,
resolveLinearEntryPolicy,
} from '@/lib/curriculum/linear-entry-policy'
import { getFlag } from '@/lib/feature-flags'
import { getSkillDisplayName } from '@/utils/skillDisplay'
import { computeBktFromHistory } from './bkt'
import { BKT_INTEGRATION_CONFIG } from './config'
import {
explainLinearReadiness,
groupLinearReadyByCategory,
type LinearReadinessFrontier,
type LinearReadinessSkillDetail,
} from './linear-readiness'
import { getAllSkillMastery, getLinearReadinessVetoes } from './progress-manager'
import { getRecentSessionResults } from './session-planner'
import type { SkillReadinessResult } from './skill-readiness'
export type { LinearReadinessFrontier }
/**
* - `ready` — graduated past the frontier and not vetoed; feeds number sentences.
* - `vetoed` — graduated, but the teacher said "not yet" for this category.
* - `locked` — the frontier stage itself: still being consolidated.
*/
export type LinearCategoryStatus = 'ready' | 'vetoed' | 'locked'
export interface LinearReadyCategory {
category: SkillCategoryKey
/** Display name, e.g. "Basic Skills" */
name: string
skillIds: string[]
/** Kept for existing consumers; equals `status === 'vetoed'` for graduated categories. */
vetoed: boolean
status: LinearCategoryStatus
}
/** Per-skill readiness for the frontier stage — what the student is working toward. */
export interface LinearReadinessSkillState {
skillId: string
/** Human-readable skill name */
name: string
/** Stage rank in the linear-readiness ladder */
stage: number
/** Generic four-dimension assessment (dashboard badge semantics). */
readiness: SkillReadinessResult
/** The entry policy's verdict — what actually decides number sentences. */
entry?: LinearEntryAssessment
}
export interface LinearReadinessState {
/** Whether the derived-readiness flag is on */
enabled: boolean
/** The stage holding number sentences back; `null` when the flag is off or every stage is solid. */
frontier: LinearReadinessFrontier | null
/** Graduated categories (ready / vetoed) plus the frontier category (locked). */
categories: LinearReadyCategory[]
/** Readiness detail for each skill in the frontier stage. */
skills: LinearReadinessSkillState[]
/** Skills the frontier has moved past that still miss the entry policy (accuracy / speed). */
pending: LinearReadinessSkillState[]
}
function flagOffState(): LinearReadinessState {
return {
enabled: false,
frontier: null,
categories: [],
skills: [],
pending: [],
}
}
export async function getLinearReadinessState(playerId: string): Promise<LinearReadinessState> {
const flag = await getFlag('linear_readiness.enabled')
if (!flag?.enabled) return flagOffState()
const policy = resolveLinearEntryPolicy(flag.config)
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 explanation = explainLinearReadiness({
skillMastery,
problemHistory,
bktResults,
vetoedCategories: vetoes,
policy,
})
const categories: LinearReadyCategory[] = [
...groupLinearReadyByCategory(explanation.readyBeforeVetoSkillIds).entries(),
].map(([category, skillIds]) => {
const vetoed = vetoes.has(category)
return {
category,
name: getCategoryDisplayName(category),
skillIds,
vetoed,
status: vetoed ? 'vetoed' : 'ready',
}
})
const { frontier } = explanation
if (frontier && !categories.some((c) => c.category === frontier.category)) {
categories.push({
category: frontier.category,
name: frontier.name,
skillIds: frontier.skillIds,
// A veto is the teacher's explicit call and re-suppresses the category the
// moment it re-graduates, so it must stay visible (and liftable) even while
// the category is also the frontier.
vetoed: vetoes.has(frontier.category),
status: vetoes.has(frontier.category) ? 'vetoed' : 'locked',
})
}
const toState = (detail: LinearReadinessSkillDetail): LinearReadinessSkillState => ({
skillId: detail.skillId,
name: getSkillDisplayName(detail.skillId),
stage: detail.stageRank,
readiness: detail.readiness,
entry: detail.entry,
})
const skills = explanation.frontierSkills.map(toState)
const pending = explanation.pendingSkills.map(toState)
return { enabled: true, frontier, categories, skills, pending }
}
|