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,
|
||||
} from '@/lib/curriculum'
|
||||
import type { ProblemGenerationMode } from '@/lib/curriculum/config'
|
||||
import type { SessionMode } from '@/lib/curriculum/session-mode'
|
||||
|
||||
interface RouteParams {
|
||||
params: Promise<{ playerId: string }>
|
||||
|
|
@ -74,6 +75,7 @@ export async function POST(request: NextRequest, { params }: RouteParams) {
|
|||
enabledParts,
|
||||
problemGenerationMode,
|
||||
confidenceThreshold,
|
||||
sessionMode,
|
||||
} = body
|
||||
|
||||
if (!durationMinutes || typeof durationMinutes !== 'number') {
|
||||
|
|
@ -102,6 +104,8 @@ export async function POST(request: NextRequest, { params }: RouteParams) {
|
|||
// Pass BKT confidence threshold if specified
|
||||
confidenceThreshold:
|
||||
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
|
||||
...(abacusTermCount && {
|
||||
config: {
|
||||
|
|
|
|||
|
|
@ -211,6 +211,8 @@ export function StartPracticeModal({
|
|||
abacusTermCount: { min: 3, max: abacusMaxTerms },
|
||||
enabledParts,
|
||||
problemGenerationMode: 'adaptive-bkt',
|
||||
// Pass sessionMode for single source of truth targeting (no rug-pulling)
|
||||
sessionMode,
|
||||
})
|
||||
} catch (err) {
|
||||
if (err instanceof ActiveSessionExistsClientError) {
|
||||
|
|
@ -235,6 +237,7 @@ export function StartPracticeModal({
|
|||
abacusMaxTerms,
|
||||
enabledParts,
|
||||
existingPlan,
|
||||
sessionMode,
|
||||
generatePlan,
|
||||
approvePlan,
|
||||
startPlan,
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
import { useMutation, useQuery, useQueryClient, useSuspenseQuery } from '@tanstack/react-query'
|
||||
import type { SessionPlan, SlotResult } from '@/db/schema/session-plans'
|
||||
import type { ProblemGenerationMode } from '@/lib/curriculum/config'
|
||||
import type { SessionMode } from '@/lib/curriculum/session-mode'
|
||||
import { api } from '@/lib/queryClient'
|
||||
import { sessionPlanKeys } from '@/lib/queryKeys'
|
||||
|
||||
|
|
@ -57,6 +58,7 @@ async function generateSessionPlan({
|
|||
enabledParts,
|
||||
problemGenerationMode,
|
||||
confidenceThreshold,
|
||||
sessionMode,
|
||||
}: {
|
||||
playerId: string
|
||||
durationMinutes: number
|
||||
|
|
@ -68,6 +70,8 @@ async function generateSessionPlan({
|
|||
problemGenerationMode?: ProblemGenerationMode
|
||||
/** BKT confidence threshold for identifying struggling skills */
|
||||
confidenceThreshold?: number
|
||||
/** Pre-computed session mode for targeting consistency */
|
||||
sessionMode?: SessionMode
|
||||
}): Promise<SessionPlan> {
|
||||
const res = await api(`curriculum/${playerId}/sessions/plans`, {
|
||||
method: 'POST',
|
||||
|
|
@ -78,6 +82,7 @@ async function generateSessionPlan({
|
|||
enabledParts,
|
||||
problemGenerationMode,
|
||||
confidenceThreshold,
|
||||
sessionMode,
|
||||
}),
|
||||
})
|
||||
if (!res.ok) {
|
||||
|
|
|
|||
|
|
@ -36,18 +36,14 @@ export {
|
|||
// Skill mastery
|
||||
calculateMasteryPercent,
|
||||
// Practice sessions
|
||||
completePracticeSession,
|
||||
getAllSkillMastery,
|
||||
getPlayerCurriculum,
|
||||
getPlayerProgressSummary,
|
||||
getRecentSessions,
|
||||
getSessionsForPhase,
|
||||
getSkillMastery,
|
||||
initializeStudent,
|
||||
recordSkillAttempt,
|
||||
recordSkillAttempts,
|
||||
startPracticeSession,
|
||||
updatePracticeSession,
|
||||
upsertPlayerCurriculum,
|
||||
} from './progress-manager'
|
||||
|
||||
|
|
@ -66,3 +62,18 @@ export {
|
|||
recordSlotResult,
|
||||
startSessionPlan,
|
||||
} 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,
|
||||
recordSkillAttemptsWithHelp,
|
||||
} from './progress-manager'
|
||||
import { getWeakSkillIds, type SessionMode } from './session-mode'
|
||||
|
||||
// ============================================================================
|
||||
// Plan Generation
|
||||
|
|
@ -90,6 +91,12 @@ export interface GenerateSessionPlanOptions {
|
|||
* - 'classic': Fluency-based discrete states
|
||||
*/
|
||||
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,
|
||||
enabledParts,
|
||||
problemGenerationMode = DEFAULT_PROBLEM_GENERATION_MODE,
|
||||
sessionMode,
|
||||
} = options
|
||||
|
||||
const config = { ...DEFAULT_PLAN_CONFIG, ...configOverrides }
|
||||
|
|
@ -248,11 +256,25 @@ export async function generateSessionPlan(
|
|||
const struggling = findStrugglingSkills(skillMastery)
|
||||
const needsReview = findSkillsNeedingReview(skillMastery, config.reviewIntervalDays)
|
||||
|
||||
// Identify weak skills from BKT for targeting (adaptive modes only)
|
||||
const weakSkills = usesBktTargeting ? identifyWeakSkills(bktResults) : []
|
||||
// Identify weak skills for targeting
|
||||
// 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) {
|
||||
console.log(`[SessionPlanner] Identified ${weakSkills.length} weak skills for targeting:`)
|
||||
console.log(`[SessionPlanner] Targeting ${weakSkills.length} weak skills:`)
|
||||
for (const skillId of weakSkills) {
|
||||
const bkt = bktResults?.get(skillId)
|
||||
console.log(` ${skillId}: pKnown=${(bkt?.pKnown ?? 0 * 100).toFixed(0)}%`)
|
||||
|
|
|
|||
Loading…
Reference in New Issue