feat(session-planner): integrate SessionMode for single source of truth targeting
The session planner now accepts an optional sessionMode parameter that: - Uses pre-computed weak skills from SessionMode (remediation mode) - Eliminates duplicate BKT computation between UI and problem generation - Ensures "no rug-pulling" - what the modal shows is what configures problems Changes: - session-planner.ts: Accept sessionMode, use getWeakSkillIds() when provided - useSessionPlan.ts: Accept sessionMode in generateSessionPlan function - plans/route.ts: Pass sessionMode from request body to planner - StartPracticeModal.tsx: Pass sessionMode when generating plan - index.ts: Export SessionMode types from curriculum module 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
b345baf3c4
commit
9851c01026
|
|
@ -9,6 +9,7 @@ import {
|
||||||
NoSkillsEnabledError,
|
NoSkillsEnabledError,
|
||||||
} from '@/lib/curriculum'
|
} from '@/lib/curriculum'
|
||||||
import type { ProblemGenerationMode } from '@/lib/curriculum/config'
|
import type { ProblemGenerationMode } from '@/lib/curriculum/config'
|
||||||
|
import type { SessionMode } from '@/lib/curriculum/session-mode'
|
||||||
|
|
||||||
interface RouteParams {
|
interface RouteParams {
|
||||||
params: Promise<{ playerId: string }>
|
params: Promise<{ playerId: string }>
|
||||||
|
|
@ -74,6 +75,7 @@ export async function POST(request: NextRequest, { params }: RouteParams) {
|
||||||
enabledParts,
|
enabledParts,
|
||||||
problemGenerationMode,
|
problemGenerationMode,
|
||||||
confidenceThreshold,
|
confidenceThreshold,
|
||||||
|
sessionMode,
|
||||||
} = body
|
} = body
|
||||||
|
|
||||||
if (!durationMinutes || typeof durationMinutes !== 'number') {
|
if (!durationMinutes || typeof durationMinutes !== 'number') {
|
||||||
|
|
@ -102,6 +104,8 @@ export async function POST(request: NextRequest, { params }: RouteParams) {
|
||||||
// Pass BKT confidence threshold if specified
|
// Pass BKT confidence threshold if specified
|
||||||
confidenceThreshold:
|
confidenceThreshold:
|
||||||
typeof confidenceThreshold === 'number' ? confidenceThreshold : undefined,
|
typeof confidenceThreshold === 'number' ? confidenceThreshold : undefined,
|
||||||
|
// Pass session mode for single source of truth targeting
|
||||||
|
sessionMode: sessionMode as SessionMode | undefined,
|
||||||
// Pass config overrides if abacusTermCount is specified
|
// Pass config overrides if abacusTermCount is specified
|
||||||
...(abacusTermCount && {
|
...(abacusTermCount && {
|
||||||
config: {
|
config: {
|
||||||
|
|
|
||||||
|
|
@ -211,6 +211,8 @@ export function StartPracticeModal({
|
||||||
abacusTermCount: { min: 3, max: abacusMaxTerms },
|
abacusTermCount: { min: 3, max: abacusMaxTerms },
|
||||||
enabledParts,
|
enabledParts,
|
||||||
problemGenerationMode: 'adaptive-bkt',
|
problemGenerationMode: 'adaptive-bkt',
|
||||||
|
// Pass sessionMode for single source of truth targeting (no rug-pulling)
|
||||||
|
sessionMode,
|
||||||
})
|
})
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (err instanceof ActiveSessionExistsClientError) {
|
if (err instanceof ActiveSessionExistsClientError) {
|
||||||
|
|
@ -235,6 +237,7 @@ export function StartPracticeModal({
|
||||||
abacusMaxTerms,
|
abacusMaxTerms,
|
||||||
enabledParts,
|
enabledParts,
|
||||||
existingPlan,
|
existingPlan,
|
||||||
|
sessionMode,
|
||||||
generatePlan,
|
generatePlan,
|
||||||
approvePlan,
|
approvePlan,
|
||||||
startPlan,
|
startPlan,
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
import { useMutation, useQuery, useQueryClient, useSuspenseQuery } from '@tanstack/react-query'
|
import { useMutation, useQuery, useQueryClient, useSuspenseQuery } from '@tanstack/react-query'
|
||||||
import type { SessionPlan, SlotResult } from '@/db/schema/session-plans'
|
import type { SessionPlan, SlotResult } from '@/db/schema/session-plans'
|
||||||
import type { ProblemGenerationMode } from '@/lib/curriculum/config'
|
import type { ProblemGenerationMode } from '@/lib/curriculum/config'
|
||||||
|
import type { SessionMode } from '@/lib/curriculum/session-mode'
|
||||||
import { api } from '@/lib/queryClient'
|
import { api } from '@/lib/queryClient'
|
||||||
import { sessionPlanKeys } from '@/lib/queryKeys'
|
import { sessionPlanKeys } from '@/lib/queryKeys'
|
||||||
|
|
||||||
|
|
@ -57,6 +58,7 @@ async function generateSessionPlan({
|
||||||
enabledParts,
|
enabledParts,
|
||||||
problemGenerationMode,
|
problemGenerationMode,
|
||||||
confidenceThreshold,
|
confidenceThreshold,
|
||||||
|
sessionMode,
|
||||||
}: {
|
}: {
|
||||||
playerId: string
|
playerId: string
|
||||||
durationMinutes: number
|
durationMinutes: number
|
||||||
|
|
@ -68,6 +70,8 @@ async function generateSessionPlan({
|
||||||
problemGenerationMode?: ProblemGenerationMode
|
problemGenerationMode?: ProblemGenerationMode
|
||||||
/** BKT confidence threshold for identifying struggling skills */
|
/** BKT confidence threshold for identifying struggling skills */
|
||||||
confidenceThreshold?: number
|
confidenceThreshold?: number
|
||||||
|
/** Pre-computed session mode for targeting consistency */
|
||||||
|
sessionMode?: SessionMode
|
||||||
}): Promise<SessionPlan> {
|
}): Promise<SessionPlan> {
|
||||||
const res = await api(`curriculum/${playerId}/sessions/plans`, {
|
const res = await api(`curriculum/${playerId}/sessions/plans`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
|
|
@ -78,6 +82,7 @@ async function generateSessionPlan({
|
||||||
enabledParts,
|
enabledParts,
|
||||||
problemGenerationMode,
|
problemGenerationMode,
|
||||||
confidenceThreshold,
|
confidenceThreshold,
|
||||||
|
sessionMode,
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
|
|
|
||||||
|
|
@ -36,18 +36,14 @@ export {
|
||||||
// Skill mastery
|
// Skill mastery
|
||||||
calculateMasteryPercent,
|
calculateMasteryPercent,
|
||||||
// Practice sessions
|
// Practice sessions
|
||||||
completePracticeSession,
|
|
||||||
getAllSkillMastery,
|
getAllSkillMastery,
|
||||||
getPlayerCurriculum,
|
getPlayerCurriculum,
|
||||||
getPlayerProgressSummary,
|
getPlayerProgressSummary,
|
||||||
getRecentSessions,
|
getRecentSessions,
|
||||||
getSessionsForPhase,
|
|
||||||
getSkillMastery,
|
getSkillMastery,
|
||||||
initializeStudent,
|
initializeStudent,
|
||||||
recordSkillAttempt,
|
recordSkillAttempt,
|
||||||
recordSkillAttempts,
|
recordSkillAttempts,
|
||||||
startPracticeSession,
|
|
||||||
updatePracticeSession,
|
|
||||||
upsertPlayerCurriculum,
|
upsertPlayerCurriculum,
|
||||||
} from './progress-manager'
|
} from './progress-manager'
|
||||||
|
|
||||||
|
|
@ -66,3 +62,18 @@ export {
|
||||||
recordSlotResult,
|
recordSlotResult,
|
||||||
startSessionPlan,
|
startSessionPlan,
|
||||||
} from './session-planner'
|
} from './session-planner'
|
||||||
|
|
||||||
|
// Session mode - unified session state computation
|
||||||
|
export {
|
||||||
|
getSessionMode,
|
||||||
|
getWeakSkillIds,
|
||||||
|
isMaintenanceMode,
|
||||||
|
isProgressionMode,
|
||||||
|
isRemediationMode,
|
||||||
|
type BlockedPromotion,
|
||||||
|
type MaintenanceMode,
|
||||||
|
type ProgressionMode,
|
||||||
|
type RemediationMode,
|
||||||
|
type SessionMode,
|
||||||
|
type SkillInfo,
|
||||||
|
} from './session-mode'
|
||||||
|
|
|
||||||
|
|
@ -58,6 +58,7 @@ import {
|
||||||
getRecentSessions,
|
getRecentSessions,
|
||||||
recordSkillAttemptsWithHelp,
|
recordSkillAttemptsWithHelp,
|
||||||
} from './progress-manager'
|
} from './progress-manager'
|
||||||
|
import { getWeakSkillIds, type SessionMode } from './session-mode'
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// Plan Generation
|
// Plan Generation
|
||||||
|
|
@ -90,6 +91,12 @@ export interface GenerateSessionPlanOptions {
|
||||||
* - 'classic': Fluency-based discrete states
|
* - 'classic': Fluency-based discrete states
|
||||||
*/
|
*/
|
||||||
problemGenerationMode?: ProblemGenerationMode
|
problemGenerationMode?: ProblemGenerationMode
|
||||||
|
/**
|
||||||
|
* Pre-computed session mode from getSessionMode().
|
||||||
|
* When provided, skips duplicate BKT computation and uses the mode's weak skills directly.
|
||||||
|
* This ensures the session targets exactly what was shown in the UI (no "rug-pulling").
|
||||||
|
*/
|
||||||
|
sessionMode?: SessionMode
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -136,6 +143,7 @@ export async function generateSessionPlan(
|
||||||
config: configOverrides,
|
config: configOverrides,
|
||||||
enabledParts,
|
enabledParts,
|
||||||
problemGenerationMode = DEFAULT_PROBLEM_GENERATION_MODE,
|
problemGenerationMode = DEFAULT_PROBLEM_GENERATION_MODE,
|
||||||
|
sessionMode,
|
||||||
} = options
|
} = options
|
||||||
|
|
||||||
const config = { ...DEFAULT_PLAN_CONFIG, ...configOverrides }
|
const config = { ...DEFAULT_PLAN_CONFIG, ...configOverrides }
|
||||||
|
|
@ -248,11 +256,25 @@ export async function generateSessionPlan(
|
||||||
const struggling = findStrugglingSkills(skillMastery)
|
const struggling = findStrugglingSkills(skillMastery)
|
||||||
const needsReview = findSkillsNeedingReview(skillMastery, config.reviewIntervalDays)
|
const needsReview = findSkillsNeedingReview(skillMastery, config.reviewIntervalDays)
|
||||||
|
|
||||||
// Identify weak skills from BKT for targeting (adaptive modes only)
|
// Identify weak skills for targeting
|
||||||
const weakSkills = usesBktTargeting ? identifyWeakSkills(bktResults) : []
|
// When sessionMode is provided, use its pre-computed weak skills (single source of truth)
|
||||||
|
// This ensures the session targets exactly what was shown in the UI (no "rug-pulling")
|
||||||
|
let weakSkills: string[]
|
||||||
|
if (sessionMode) {
|
||||||
|
// Use pre-computed weak skills from sessionMode
|
||||||
|
weakSkills = getWeakSkillIds(sessionMode)
|
||||||
|
if (process.env.DEBUG_SESSION_PLANNER === 'true') {
|
||||||
|
console.log(
|
||||||
|
`[SessionPlanner] Using weak skills from sessionMode (${sessionMode.type}): ${weakSkills.length}`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Fallback: compute locally (for backwards compatibility)
|
||||||
|
weakSkills = usesBktTargeting ? identifyWeakSkills(bktResults) : []
|
||||||
|
}
|
||||||
|
|
||||||
if (process.env.DEBUG_SESSION_PLANNER === 'true' && weakSkills.length > 0) {
|
if (process.env.DEBUG_SESSION_PLANNER === 'true' && weakSkills.length > 0) {
|
||||||
console.log(`[SessionPlanner] Identified ${weakSkills.length} weak skills for targeting:`)
|
console.log(`[SessionPlanner] Targeting ${weakSkills.length} weak skills:`)
|
||||||
for (const skillId of weakSkills) {
|
for (const skillId of weakSkills) {
|
||||||
const bkt = bktResults?.get(skillId)
|
const bkt = bktResults?.get(skillId)
|
||||||
console.log(` ${skillId}: pKnown=${(bkt?.pKnown ?? 0 * 100).toFixed(0)}%`)
|
console.log(` ${skillId}: pKnown=${(bkt?.pKnown ?? 0 * 100).toFixed(0)}%`)
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue