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 | 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 8x 8x 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 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 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 39x 39x 39x 39x 39x 39x 39x 39x 39x 2x 2x 2x 2x | import { createId } from '@paralleldrive/cuid2'
import { index, integer, sqliteTable, text, uniqueIndex } from 'drizzle-orm/sqlite-core'
import { players } from './players'
// ============================================================================
// Practice Level Types & Helpers
// ============================================================================
/**
* Practice level for a skill:
* - 'none': Skill is inactive (not in practice rotation)
* - 'abacus': Skill appears in "Use Abacus" parts only
* - 'visual': Skill appears in all parts (abacus + visualization + linear)
*
* Progression: none → abacus → visual
*/
export type PracticeLevel = 'none' | 'abacus' | 'visual'
/** Check if a skill is active at any level (not 'none') */
export function isActive(level: PracticeLevel): boolean {
return level !== 'none'
}
/** Check if a skill is ready for visualization/linear parts */
export function isVisualReady(level: PracticeLevel): boolean {
return level === 'visual'
}
/** Cycle to the next practice level: none → abacus → visual → none */
export function nextPracticeLevel(level: PracticeLevel): PracticeLevel {
switch (level) {
case 'none':
return 'abacus'
case 'abacus':
return 'visual'
case 'visual':
return 'none'
}
}
/**
* Player skill mastery table - tracks per-skill progress for each player
*
* Each row represents a player's progress with a specific abacus skill.
* Skills are identified by their path (e.g., "fiveComplements.4=5-1").
*/
export const playerSkillMastery = sqliteTable(
'player_skill_mastery',
{
/** Primary key */
id: text('id')
.primaryKey()
.$defaultFn(() => createId()),
/** Foreign key to players table */
playerId: text('player_id')
.notNull()
.references(() => players.id, { onDelete: 'cascade' }),
/**
* Skill identifier - matches the skill paths from SkillSet type
* Examples:
* - "basic.directAddition"
* - "fiveComplements.4=5-1"
* - "tenComplements.9=10-1"
* - "fiveComplementsSub.-4=-5+1"
* - "tenComplementsSub.-9=+1-10"
*/
skillId: text('skill_id').notNull(),
// NOTE: attempts, correct, consecutiveCorrect columns REMOVED
// These are now computed on-the-fly from session results (single source of truth)
// See: getRecentSessionResults() in session-planner.ts
/**
* @deprecated Use practiceLevel instead. Kept in DB for migration safety.
* Whether this skill is in the student's active practice rotation.
*/
isPracticing: integer('is_practicing', { mode: 'boolean' }).notNull().default(false),
/**
* Practice level controlling which session parts this skill appears in:
* - 'none': Skill is inactive
* - 'abacus': Skill appears in abacus parts only
* - 'visual': Skill appears in all parts (abacus + visualization + linear)
*
* Set by teacher via ManualSkillSelector 3-state toggle.
* Mastery is tracked via BKT (Bayesian Knowledge Tracing) using session history.
*/
practiceLevel: text('practice_level').$type<PracticeLevel>().notNull().default('none'),
/** When this skill was last practiced */
lastPracticedAt: integer('last_practiced_at', { mode: 'timestamp' }),
/** When this record was created */
createdAt: integer('created_at', { mode: 'timestamp' })
.notNull()
.$defaultFn(() => new Date()),
/** When this record was last updated */
updatedAt: integer('updated_at', { mode: 'timestamp' })
.notNull()
.$defaultFn(() => new Date()),
/**
* Whether help was used the last time this skill was practiced
*/
lastHadHelp: integer('last_had_help', { mode: 'boolean' }).notNull().default(false),
},
(table) => ({
/** Index for fast lookups by playerId */
playerIdIdx: index('player_skill_mastery_player_id_idx').on(table.playerId),
/** Unique constraint: one record per player per skill */
playerSkillUnique: uniqueIndex('player_skill_mastery_player_skill_unique').on(
table.playerId,
table.skillId
),
})
)
export type PlayerSkillMastery = typeof playerSkillMastery.$inferSelect
export type NewPlayerSkillMastery = typeof playerSkillMastery.$inferInsert
|