diff --git a/apps/web/.claude/BKT_DESIGN_SPEC.md b/apps/web/.claude/BKT_DESIGN_SPEC.md index 6f178758..4102af1e 100644 --- a/apps/web/.claude/BKT_DESIGN_SPEC.md +++ b/apps/web/.claude/BKT_DESIGN_SPEC.md @@ -68,7 +68,7 @@ export interface SlotResult { timestamp: number; responseTimeMs: number; userAnswer: number | null; - helpLevel: 0 | 1; // Boolean: 0 = no help, 1 = used help + hadHelp: boolean; // Whether student used help during this problem } ``` @@ -223,13 +223,13 @@ export function updateOnIncorrect( * Adjust observation weight based on whether help was used. * Using help = less confident the student really knows it. * - * Note: Help is binary (0 = no help, 1 = used help). + * Note: Help is a boolean (hadHelp: true = used help, false = no help). * We can't determine which skill needed help for multi-skill problems, * so we apply the discount uniformly and let conjunctive BKT identify * weak skills from aggregated evidence. */ -export function helpLevelWeight(helpLevel: 0 | 1): number { - return helpLevel === 0 ? 1.0 : 0.5; // 50% weight for helped answers +export function helpWeight(hadHelp: boolean): number { + return hadHelp ? 0.5 : 1.0; // 50% weight for helped answers } /** @@ -345,7 +345,7 @@ export function getUncertaintyRange( import type { ProblemResultWithContext } from "../session-planner"; import { getDefaultParams, type BktParams } from "./skill-priors"; import { updateOnCorrect, updateOnIncorrect } from "./conjunctive-bkt"; -import { helpLevelWeight, responseTimeWeight } from "./evidence-quality"; +import { helpWeight, responseTimeWeight } from "./evidence-quality"; import { calculateConfidence, getUncertaintyRange } from "./confidence"; export interface BktComputeOptions { @@ -428,12 +428,12 @@ export function computeBktFromHistory( }); // Calculate evidence weight - const helpWeight = helpLevelWeight(result.helpLevel); + const helpW = helpWeight(result.hadHelp); const rtWeight = responseTimeWeight( result.responseTimeMs, result.isCorrect, ); - const evidenceWeight = helpWeight * rtWeight; + const evidenceWeight = helpW * rtWeight; // Compute updates const updates = result.isCorrect diff --git a/apps/web/drizzle/0040_rename_last_help_level_to_last_had_help.sql b/apps/web/drizzle/0040_rename_last_help_level_to_last_had_help.sql new file mode 100644 index 00000000..a0b3243b --- /dev/null +++ b/apps/web/drizzle/0040_rename_last_help_level_to_last_had_help.sql @@ -0,0 +1,3 @@ +-- Custom SQL migration file, put your code below! -- +-- Rename last_help_level to last_had_help (terminology change: "help level" is no longer accurate since it's a boolean) +ALTER TABLE `player_skill_mastery` RENAME COLUMN `last_help_level` TO `last_had_help`; diff --git a/apps/web/drizzle/meta/0040_snapshot.json b/apps/web/drizzle/meta/0040_snapshot.json new file mode 100644 index 00000000..960a2b52 --- /dev/null +++ b/apps/web/drizzle/meta/0040_snapshot.json @@ -0,0 +1,1094 @@ +{ + "id": "a669f209-bec0-49f5-bc97-05a2c8cc47e7", + "prevId": "785dd0f6-d547-426c-87ce-0220203b660b", + "version": "6", + "dialect": "sqlite", + "tables": { + "abacus_settings": { + "name": "abacus_settings", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "color_scheme": { + "name": "color_scheme", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'place-value'" + }, + "bead_shape": { + "name": "bead_shape", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'diamond'" + }, + "color_palette": { + "name": "color_palette", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'default'" + }, + "hide_inactive_beads": { + "name": "hide_inactive_beads", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "colored_numerals": { + "name": "colored_numerals", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "scale_factor": { + "name": "scale_factor", + "type": "real", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 1 + }, + "show_numbers": { + "name": "show_numbers", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "animated": { + "name": "animated", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "interactive": { + "name": "interactive", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "gestures": { + "name": "gestures", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "sound_enabled": { + "name": "sound_enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "sound_volume": { + "name": "sound_volume", + "type": "real", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0.8 + } + }, + "indexes": {}, + "foreignKeys": { + "abacus_settings_user_id_users_id_fk": { + "name": "abacus_settings_user_id_users_id_fk", + "tableFrom": "abacus_settings", + "columnsFrom": [ + "user_id" + ], + "tableTo": "users", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "arcade_rooms": { + "name": "arcade_rooms", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "code": { + "name": "code", + "type": "text(6)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text(50)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "creator_name": { + "name": "creator_name", + "type": "text(50)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_activity": { + "name": "last_activity", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ttl_minutes": { + "name": "ttl_minutes", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 60 + }, + "is_locked": { + "name": "is_locked", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "game_name": { + "name": "game_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "game_config": { + "name": "game_config", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'lobby'" + }, + "current_session_id": { + "name": "current_session_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "total_games_played": { + "name": "total_games_played", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + } + }, + "indexes": { + "arcade_rooms_code_unique": { + "name": "arcade_rooms_code_unique", + "columns": [ + "code" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "arcade_sessions": { + "name": "arcade_sessions", + "columns": { + "room_id": { + "name": "room_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "current_game": { + "name": "current_game", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "game_url": { + "name": "game_url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "game_state": { + "name": "game_state", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "active_players": { + "name": "active_players", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "started_at": { + "name": "started_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_activity_at": { + "name": "last_activity_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "is_active": { + "name": "is_active", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 1 + } + }, + "indexes": {}, + "foreignKeys": { + "arcade_sessions_room_id_arcade_rooms_id_fk": { + "name": "arcade_sessions_room_id_arcade_rooms_id_fk", + "tableFrom": "arcade_sessions", + "columnsFrom": [ + "room_id" + ], + "tableTo": "arcade_rooms", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "arcade_sessions_user_id_users_id_fk": { + "name": "arcade_sessions_user_id_users_id_fk", + "tableFrom": "arcade_sessions", + "columnsFrom": [ + "user_id" + ], + "tableTo": "users", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "players": { + "name": "players", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "emoji": { + "name": "emoji", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "is_active": { + "name": "is_active", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "players_user_id_idx": { + "name": "players_user_id_idx", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "players_user_id_users_id_fk": { + "name": "players_user_id_users_id_fk", + "tableFrom": "players", + "columnsFrom": [ + "user_id" + ], + "tableTo": "users", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "room_members": { + "name": "room_members", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "room_id": { + "name": "room_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "display_name": { + "name": "display_name", + "type": "text(50)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "is_creator": { + "name": "is_creator", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "joined_at": { + "name": "joined_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_seen": { + "name": "last_seen", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "is_online": { + "name": "is_online", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + } + }, + "indexes": { + "idx_room_members_user_id_unique": { + "name": "idx_room_members_user_id_unique", + "columns": [ + "user_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "room_members_room_id_arcade_rooms_id_fk": { + "name": "room_members_room_id_arcade_rooms_id_fk", + "tableFrom": "room_members", + "columnsFrom": [ + "room_id" + ], + "tableTo": "arcade_rooms", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "room_member_history": { + "name": "room_member_history", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "room_id": { + "name": "room_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "display_name": { + "name": "display_name", + "type": "text(50)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "first_joined_at": { + "name": "first_joined_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_seen_at": { + "name": "last_seen_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_action": { + "name": "last_action", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'active'" + }, + "last_action_at": { + "name": "last_action_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "room_member_history_room_id_arcade_rooms_id_fk": { + "name": "room_member_history_room_id_arcade_rooms_id_fk", + "tableFrom": "room_member_history", + "columnsFrom": [ + "room_id" + ], + "tableTo": "arcade_rooms", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "room_invitations": { + "name": "room_invitations", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "room_id": { + "name": "room_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_name": { + "name": "user_name", + "type": "text(50)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "invited_by": { + "name": "invited_by", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "invited_by_name": { + "name": "invited_by_name", + "type": "text(50)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'pending'" + }, + "invitation_type": { + "name": "invitation_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'manual'" + }, + "message": { + "name": "message", + "type": "text(500)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "responded_at": { + "name": "responded_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "idx_room_invitations_user_room": { + "name": "idx_room_invitations_user_room", + "columns": [ + "user_id", + "room_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "room_invitations_room_id_arcade_rooms_id_fk": { + "name": "room_invitations_room_id_arcade_rooms_id_fk", + "tableFrom": "room_invitations", + "columnsFrom": [ + "room_id" + ], + "tableTo": "arcade_rooms", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "room_reports": { + "name": "room_reports", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "room_id": { + "name": "room_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "reporter_id": { + "name": "reporter_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "reporter_name": { + "name": "reporter_name", + "type": "text(50)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "reported_user_id": { + "name": "reported_user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "reported_user_name": { + "name": "reported_user_name", + "type": "text(50)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "details": { + "name": "details", + "type": "text(500)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'pending'" + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "reviewed_at": { + "name": "reviewed_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "reviewed_by": { + "name": "reviewed_by", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "room_reports_room_id_arcade_rooms_id_fk": { + "name": "room_reports_room_id_arcade_rooms_id_fk", + "tableFrom": "room_reports", + "columnsFrom": [ + "room_id" + ], + "tableTo": "arcade_rooms", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "room_bans": { + "name": "room_bans", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "room_id": { + "name": "room_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_name": { + "name": "user_name", + "type": "text(50)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "banned_by": { + "name": "banned_by", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "banned_by_name": { + "name": "banned_by_name", + "type": "text(50)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "notes": { + "name": "notes", + "type": "text(500)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "idx_room_bans_user_room": { + "name": "idx_room_bans_user_room", + "columns": [ + "user_id", + "room_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "room_bans_room_id_arcade_rooms_id_fk": { + "name": "room_bans_room_id_arcade_rooms_id_fk", + "tableFrom": "room_bans", + "columnsFrom": [ + "room_id" + ], + "tableTo": "arcade_rooms", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "user_stats": { + "name": "user_stats", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "games_played": { + "name": "games_played", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "total_wins": { + "name": "total_wins", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "favorite_game_type": { + "name": "favorite_game_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "best_time": { + "name": "best_time", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "highest_accuracy": { + "name": "highest_accuracy", + "type": "real", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + } + }, + "indexes": {}, + "foreignKeys": { + "user_stats_user_id_users_id_fk": { + "name": "user_stats_user_id_users_id_fk", + "tableFrom": "user_stats", + "columnsFrom": [ + "user_id" + ], + "tableTo": "users", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "users": { + "name": "users", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "guest_id": { + "name": "guest_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "upgraded_at": { + "name": "upgraded_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "users_guest_id_unique": { + "name": "users_guest_id_unique", + "columns": [ + "guest_id" + ], + "isUnique": true + }, + "users_email_unique": { + "name": "users_email_unique", + "columns": [ + "email" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + }, + "internal": { + "indexes": {} + } +} \ No newline at end of file diff --git a/apps/web/drizzle/meta/_journal.json b/apps/web/drizzle/meta/_journal.json index c5017427..e1113afc 100644 --- a/apps/web/drizzle/meta/_journal.json +++ b/apps/web/drizzle/meta/_journal.json @@ -281,6 +281,13 @@ "when": 1766275200000, "tag": "0039_add_player_archived", "breakpoints": true + }, + { + "idx": 40, + "version": "6", + "when": 1766320890578, + "tag": "0040_rename_last_help_level_to_last_had_help", + "breakpoints": true } ] -} +} \ No newline at end of file diff --git a/apps/web/src/components/practice/ActiveSession.stories.tsx b/apps/web/src/components/practice/ActiveSession.stories.tsx index 08088145..74ac9170 100644 --- a/apps/web/src/components/practice/ActiveSession.stories.tsx +++ b/apps/web/src/components/practice/ActiveSession.stories.tsx @@ -388,7 +388,7 @@ function InteractiveSessionDemo() { partNumber: (plan.currentPartIndex + 1) as 1 | 2 | 3, timestamp: new Date(), // Default help tracking fields if not provided - helpLevelUsed: result.helpLevelUsed ?? 0, + hadHelp: result.hadHelp ?? false, incorrectAttempts: result.incorrectAttempts ?? 0, helpTrigger: result.helpTrigger ?? 'none', } diff --git a/apps/web/src/components/practice/ActiveSession.tsx b/apps/web/src/components/practice/ActiveSession.tsx index d300546d..df9db7f1 100644 --- a/apps/web/src/components/practice/ActiveSession.tsx +++ b/apps/web/src/components/practice/ActiveSession.tsx @@ -899,7 +899,7 @@ export function ActiveSession({ skillsExercised: attemptData.problem.skillsRequired, usedOnScreenAbacus: phase.phase === 'helpMode', incorrectAttempts: 0, // TODO: track this properly - helpLevelUsed: phase.phase === 'helpMode' ? 1 : 0, + hadHelp: phase.phase === 'helpMode', } await onAnswer(result) diff --git a/apps/web/src/components/practice/AllProblemsSection.tsx b/apps/web/src/components/practice/AllProblemsSection.tsx index d3cf9367..388e8ed5 100644 --- a/apps/web/src/components/practice/AllProblemsSection.tsx +++ b/apps/web/src/components/practice/AllProblemsSection.tsx @@ -463,14 +463,14 @@ function ProblemDetailPopover({ - {/* Help level */} - {result && result.helpLevelUsed > 0 && ( + {/* Help used */} + {result?.hadHelp && (
Help used: - Level {result.helpLevelUsed} + Yes
)} diff --git a/apps/web/src/components/practice/DetailedProblemCard.tsx b/apps/web/src/components/practice/DetailedProblemCard.tsx index 11ea8919..494e6627 100644 --- a/apps/web/src/components/practice/DetailedProblemCard.tsx +++ b/apps/web/src/components/practice/DetailedProblemCard.tsx @@ -1136,7 +1136,7 @@ export function DetailedProblemCard({ result.responseTimeMs > autoPauseStats.thresholdMs && ' (over threshold)'} - {result.helpLevelUsed > 0 && Help level: {result.helpLevelUsed}} + {result.hadHelp && Used help} )} diff --git a/apps/web/src/components/practice/PracticeSubNav.stories.tsx b/apps/web/src/components/practice/PracticeSubNav.stories.tsx index b6fc9d14..a22f516f 100644 --- a/apps/web/src/components/practice/PracticeSubNav.stories.tsx +++ b/apps/web/src/components/practice/PracticeSubNav.stories.tsx @@ -90,7 +90,7 @@ function createMockResults( skillsExercised: ['basic.directAddition'], usedOnScreenAbacus: partType === 'abacus', timestamp: new Date(Date.now() - (count - i) * 30000), - helpLevelUsed: 0, + hadHelp: false, incorrectAttempts: 0, })) } @@ -141,7 +141,7 @@ function createSessionHud(config: { skillsExercised: ['basic.directAddition'], usedOnScreenAbacus: pIdx === 0, timestamp: new Date(), - helpLevelUsed: 0, + hadHelp: false, incorrectAttempts: 0, }) } diff --git a/apps/web/src/components/practice/ProblemToReview.tsx b/apps/web/src/components/practice/ProblemToReview.tsx index e6a86b74..ecfe0eb0 100644 --- a/apps/web/src/components/practice/ProblemToReview.tsx +++ b/apps/web/src/components/practice/ProblemToReview.tsx @@ -420,13 +420,13 @@ export function ProblemToReview({ 🧮 Used on-screen abacus )} - {result.helpLevelUsed > 0 && ( + {result.hadHelp && ( - 💡 Help level: {result.helpLevelUsed} + 💡 Used help )} diff --git a/apps/web/src/components/practice/ProgressDashboard.tsx b/apps/web/src/components/practice/ProgressDashboard.tsx index 107e314c..286ba099 100644 --- a/apps/web/src/components/practice/ProgressDashboard.tsx +++ b/apps/web/src/components/practice/ProgressDashboard.tsx @@ -18,8 +18,8 @@ export interface SkillProgress { attempts: number correct: number consecutiveCorrect: number - /** Last help level used on this skill (0 or 1) */ - lastHelpLevel?: number + /** Whether help was used when this skill was last practiced */ + lastHadHelp?: boolean } /** diff --git a/apps/web/src/components/practice/SessionPausedModal.stories.tsx b/apps/web/src/components/practice/SessionPausedModal.stories.tsx index 08925dba..f11ca32a 100644 --- a/apps/web/src/components/practice/SessionPausedModal.stories.tsx +++ b/apps/web/src/components/practice/SessionPausedModal.stories.tsx @@ -103,7 +103,7 @@ function createMockSessionPlan(config: { skillsExercised: ['basic.directAddition'], usedOnScreenAbacus: i < 5, timestamp: new Date(Date.now() - (completedCount - i) * 30000), - helpLevelUsed: 0, + hadHelp: false, incorrectAttempts: 0, })) diff --git a/apps/web/src/components/practice/sessionSummaryUtils.ts b/apps/web/src/components/practice/sessionSummaryUtils.ts index f4c8ec02..51922458 100644 --- a/apps/web/src/components/practice/sessionSummaryUtils.ts +++ b/apps/web/src/components/practice/sessionSummaryUtils.ts @@ -104,8 +104,8 @@ export function filterProblemsNeedingAttention( reasons.push('slow') } - // Check if used help (helpLevelUsed is binary: 0 = no help, 1 = help used) - if (problem.result.helpLevelUsed >= 1) { + // Check if used help + if (problem.result.hadHelp) { reasons.push('help-used') } diff --git a/apps/web/src/db/schema/player-skill-mastery.ts b/apps/web/src/db/schema/player-skill-mastery.ts index 20650f24..b6bf03ad 100644 --- a/apps/web/src/db/schema/player-skill-mastery.ts +++ b/apps/web/src/db/schema/player-skill-mastery.ts @@ -57,9 +57,9 @@ export const playerSkillMastery = sqliteTable( .$defaultFn(() => new Date()), /** - * Last help level used on this skill (0 = no help, 1 = used help) + * Whether help was used the last time this skill was practiced */ - lastHelpLevel: integer('last_help_level').notNull().default(0), + lastHadHelp: integer('last_had_help', { mode: 'boolean' }).notNull().default(false), }, (table) => ({ /** Index for fast lookups by playerId */ diff --git a/apps/web/src/db/schema/session-plans.ts b/apps/web/src/db/schema/session-plans.ts index 33101ee3..b113f7b5 100644 --- a/apps/web/src/db/schema/session-plans.ts +++ b/apps/web/src/db/schema/session-plans.ts @@ -213,16 +213,6 @@ export interface SessionAdjustment { previousHealth: SessionHealth } -/** - * Help level used during a problem (boolean) - * - 0: No help requested - * - 1: Help was used (interactive abacus overlay shown) - * - * Note: The system previously defined levels 0-3, but only 0/1 are ever recorded. - * BKT uses conjunctive blame attribution to identify weak skills. - */ -export type HelpLevel = 0 | 1 - /** * Result of a single problem slot */ @@ -241,8 +231,8 @@ export interface SlotResult { // ---- Help Tracking (for feedback loop) ---- - /** Maximum help level used during this problem (0 = no help) */ - helpLevelUsed: HelpLevel + /** Whether the student used help during this problem */ + hadHelp: boolean /** Number of incorrect attempts before getting the right answer */ incorrectAttempts: number diff --git a/apps/web/src/lib/curriculum/bkt/compute-bkt.ts b/apps/web/src/lib/curriculum/bkt/compute-bkt.ts index df9e308d..7bcf5d6f 100644 --- a/apps/web/src/lib/curriculum/bkt/compute-bkt.ts +++ b/apps/web/src/lib/curriculum/bkt/compute-bkt.ts @@ -10,7 +10,7 @@ import { BKT_THRESHOLDS } from '../config/bkt-integration' import type { ProblemResultWithContext } from '../session-planner' import { calculateConfidence, getUncertaintyRange } from './confidence' import { type BlameMethod, updateOnCorrect, updateOnIncorrectWithMethod } from './conjunctive-bkt' -import { helpLevelWeight, responseTimeWeight } from './evidence-quality' +import { helpWeight, responseTimeWeight } from './evidence-quality' import { getDefaultParams } from './skill-priors' import type { BktComputeOptions, @@ -125,10 +125,10 @@ export function computeBktFromHistory( } }) - // Calculate evidence weight based on help level and response time - const helpWeight = helpLevelWeight(result.helpLevelUsed) + // Calculate evidence weight based on help usage and response time + const helpW = helpWeight(result.hadHelp) const rtWeight = responseTimeWeight(result.responseTimeMs, result.isCorrect) - const evidenceWeight = helpWeight * rtWeight + const evidenceWeight = helpW * rtWeight // Compute BKT updates (conjunctive model) const blameMethod = opts.blameMethod ?? 'heuristic' diff --git a/apps/web/src/lib/curriculum/bkt/evidence-quality.ts b/apps/web/src/lib/curriculum/bkt/evidence-quality.ts index 4aedf9b4..32accd64 100644 --- a/apps/web/src/lib/curriculum/bkt/evidence-quality.ts +++ b/apps/web/src/lib/curriculum/bkt/evidence-quality.ts @@ -3,26 +3,19 @@ * * Not all observations are equally informative. We adjust the weight * of evidence based on: - * - Help level: Using help = less confident the student really knows it + * - Help usage: Using help = less confident the student really knows it * - Response time: Fast correct = strong mastery, slow correct = struggled */ -import type { HelpLevel } from '@/db/schema/session-plans' - /** * Adjust observation weight based on whether help was used. * Using help = less confident the student really knows it. * - * @param helpLevel - 0 = no help, 1 = help used + * @param hadHelp - true if help was used, false otherwise * @returns Weight multiplier [0.5, 1.0] */ -export function helpLevelWeight(helpLevel: HelpLevel): number { - // Guard against unexpected values (legacy data, JSON parsing issues) - if (helpLevel !== 0 && helpLevel !== 1) { - return 1.0 - } - // 0 = no help (full evidence), 1 = used help (50% evidence) - return helpLevel === 0 ? 1.0 : 0.5 +export function helpWeight(hadHelp: boolean): number { + return hadHelp ? 0.5 : 1.0 } /** @@ -67,13 +60,13 @@ export function responseTimeWeight( } /** - * Combined evidence weight from help and response time. + * Combined evidence weight from help usage and response time. */ export function combinedEvidenceWeight( - helpLevel: HelpLevel, + hadHelp: boolean, responseTimeMs: number, isCorrect: boolean, expectedTimeMs: number = 5000 ): number { - return helpLevelWeight(helpLevel) * responseTimeWeight(responseTimeMs, isCorrect, expectedTimeMs) + return helpWeight(hadHelp) * responseTimeWeight(responseTimeMs, isCorrect, expectedTimeMs) } diff --git a/apps/web/src/lib/curriculum/bkt/index.ts b/apps/web/src/lib/curriculum/bkt/index.ts index e550470c..67c01de5 100644 --- a/apps/web/src/lib/curriculum/bkt/index.ts +++ b/apps/web/src/lib/curriculum/bkt/index.ts @@ -58,7 +58,7 @@ export { getDefaultParams, getSkillCategory } from './skill-priors' // Evidence quality (for advanced use cases) export { combinedEvidenceWeight, - helpLevelWeight, + helpWeight, responseTimeWeight, } from './evidence-quality' diff --git a/apps/web/src/lib/curriculum/progress-manager.ts b/apps/web/src/lib/curriculum/progress-manager.ts index 6897f9e9..515ded8f 100644 --- a/apps/web/src/lib/curriculum/progress-manager.ts +++ b/apps/web/src/lib/curriculum/progress-manager.ts @@ -8,7 +8,6 @@ import { db, schema } from '@/db' import type { NewPlayerCurriculum, PlayerCurriculum } from '@/db/schema/player-curriculum' import type { NewPlayerSkillMastery, PlayerSkillMastery } from '@/db/schema/player-skill-mastery' import type { PracticeSession } from '@/db/schema/practice-sessions' -import type { HelpLevel } from '@/db/schema/session-plans' import { isTutorialSatisfied, type NewSkillTutorialProgress, @@ -261,20 +260,18 @@ export async function recordSkillAttempt( } /** - * Record a skill attempt with help level tracking + * Record a skill attempt with help tracking * * Updates the lastPracticedAt timestamp and tracks whether help was used. * BKT handles mastery estimation via evidence weighting (helped answers get 0.5x weight). * - * NOTE: The old reinforcement system (based on help levels 2+) has been removed. - * Only boolean help (0 or 1) is recorded. BKT's conjunctive blame attribution - * identifies weak skills from multi-skill problems. + * NOTE: BKT's conjunctive blame attribution identifies weak skills from multi-skill problems. */ export async function recordSkillAttemptWithHelp( playerId: string, skillId: string, _isCorrect: boolean, - helpLevel: HelpLevel, + hadHelp: boolean, _responseTimeMs?: number ): Promise { const existing = await getSkillMastery(playerId, skillId) @@ -286,7 +283,7 @@ export async function recordSkillAttemptWithHelp( .set({ lastPracticedAt: now, updatedAt: now, - lastHelpLevel: helpLevel, + lastHadHelp: hadHelp, }) .where(eq(schema.playerSkillMastery.id, existing.id)) @@ -299,7 +296,7 @@ export async function recordSkillAttemptWithHelp( skillId, isPracticing: true, // skill is being practiced lastPracticedAt: now, - lastHelpLevel: helpLevel, + lastHadHelp: hadHelp, } await db.insert(schema.playerSkillMastery).values(newRecord) @@ -314,7 +311,7 @@ export async function recordSkillAttemptWithHelp( export async function recordSkillAttemptsWithHelp( playerId: string, skillResults: Array<{ skillId: string; isCorrect: boolean }>, - helpLevel: HelpLevel, + hadHelp: boolean, responseTimeMs?: number ): Promise { const results: PlayerSkillMastery[] = [] @@ -324,7 +321,7 @@ export async function recordSkillAttemptsWithHelp( playerId, skillId, isCorrect, - helpLevel, + hadHelp, responseTimeMs ) results.push(result) diff --git a/apps/web/src/lib/curriculum/session-planner.ts b/apps/web/src/lib/curriculum/session-planner.ts index 69551800..a11df306 100644 --- a/apps/web/src/lib/curriculum/session-planner.ts +++ b/apps/web/src/lib/curriculum/session-planner.ts @@ -805,7 +805,7 @@ export async function recordSlotResult( await recordSkillAttemptsWithHelp( plan.playerId, skillResults, - result.helpLevelUsed, + result.hadHelp, result.responseTimeMs ) } catch (skillError) { diff --git a/apps/web/src/test/bkt-comprehensive-skill-test.test.ts b/apps/web/src/test/bkt-comprehensive-skill-test.test.ts index 7d0df473..63a5b35b 100644 --- a/apps/web/src/test/bkt-comprehensive-skill-test.test.ts +++ b/apps/web/src/test/bkt-comprehensive-skill-test.test.ts @@ -169,7 +169,7 @@ function createResultFromProblem( skillsExercised: problem.skillsUsed, usedOnScreenAbacus: false, timestamp, - helpLevelUsed: 0, + hadHelp: false, incorrectAttempts: 0, sessionCompletedAt: timestamp, partType: 'abacus', diff --git a/apps/web/src/test/bkt-skill-identification.test.ts b/apps/web/src/test/bkt-skill-identification.test.ts index 9d0ab6be..66bf55c5 100644 --- a/apps/web/src/test/bkt-skill-identification.test.ts +++ b/apps/web/src/test/bkt-skill-identification.test.ts @@ -49,7 +49,7 @@ function createResult( skillsExercised, usedOnScreenAbacus: false, timestamp, - helpLevelUsed: 0, + hadHelp: false, incorrectAttempts: 0, sessionCompletedAt: timestamp, partType: 'abacus', @@ -174,7 +174,7 @@ function generateSyntheticResults( isCorrect, responseTimeMs: containsWeakSkill ? 8000 : 3000, // Slower on weak skill usedOnScreenAbacus: false, - helpLevelUsed: 0, + hadHelp: false, incorrectAttempts: 0, sessionCompletedAt: new Date(baseTime + i * 5000), partType: 'abacus', diff --git a/apps/web/src/test/journey-simulator/EphemeralDatabase.ts b/apps/web/src/test/journey-simulator/EphemeralDatabase.ts index 6ff77437..c3481c29 100644 --- a/apps/web/src/test/journey-simulator/EphemeralDatabase.ts +++ b/apps/web/src/test/journey-simulator/EphemeralDatabase.ts @@ -142,9 +142,7 @@ export async function initializeSkillMastery( playerId, skillId, isPracticing, - needsReinforcement: false, - lastHelpLevel: 0, - reinforcementStreak: 0, + lastHadHelp: false, createdAt: now, lastPracticedAt: null, }) diff --git a/apps/web/src/test/journey-simulator/JourneyRunner.ts b/apps/web/src/test/journey-simulator/JourneyRunner.ts index b23006b1..1ed6d18c 100644 --- a/apps/web/src/test/journey-simulator/JourneyRunner.ts +++ b/apps/web/src/test/journey-simulator/JourneyRunner.ts @@ -158,9 +158,9 @@ export class JourneyRunner { responseTimeMs: answer.responseTimeMs, skillsExercised: answer.skillsChallenged, usedOnScreenAbacus: false, - helpLevelUsed: answer.helpLevelUsed, + hadHelp: answer.hadHelp, incorrectAttempts: answer.isCorrect ? 0 : 1, - helpTrigger: answer.helpLevelUsed > 0 ? 'manual' : 'none', + helpTrigger: answer.hadHelp ? 'manual' : 'none', }) } } diff --git a/apps/web/src/test/journey-simulator/SimulatedStudent.ts b/apps/web/src/test/journey-simulator/SimulatedStudent.ts index cb1f05ce..e5f2e81a 100644 --- a/apps/web/src/test/journey-simulator/SimulatedStudent.ts +++ b/apps/web/src/test/journey-simulator/SimulatedStudent.ts @@ -20,7 +20,7 @@ * - Help provides additive bonus to probability */ -import type { GeneratedProblem, HelpLevel } from '@/db/schema/session-plans' +import type { GeneratedProblem } from '@/db/schema/session-plans' import type { SeededRandom } from './SeededRandom' import type { SimulatedAnswer, StudentProfile } from './types' @@ -175,20 +175,20 @@ export class SimulatedStudent { } // Determine if student uses help (binary) - const helpLevelUsed = this.selectHelpLevel() + const hadHelp = this.selectHelpUsage() // Calculate answer probability using Hill function + conjunctive model - const answerProbability = this.calculateAnswerProbability(skillsChallenged, helpLevelUsed) + const answerProbability = this.calculateAnswerProbability(skillsChallenged, hadHelp) const isCorrect = this.rng.chance(answerProbability) // Calculate response time - const responseTimeMs = this.calculateResponseTime(skillsChallenged, helpLevelUsed, isCorrect) + const responseTimeMs = this.calculateResponseTime(skillsChallenged, hadHelp, isCorrect) return { isCorrect, responseTimeMs, - helpLevelUsed, + hadHelp, skillsChallenged, fatigue, } @@ -202,7 +202,7 @@ export class SimulatedStudent { * * Help bonus is additive (applied after the product). */ - private calculateAnswerProbability(skillIds: string[], helpLevel: HelpLevel): number { + private calculateAnswerProbability(skillIds: string[], hadHelp: boolean): number { if (skillIds.length === 0) { // Basic problems (no special skills) almost always correct return 0.95 @@ -220,7 +220,8 @@ export class SimulatedStudent { } // Add help bonus (additive, not multiplicative) - const helpBonus = this.profile.helpBonuses[helpLevel] + // helpBonuses[0] = no help, helpBonuses[1] = with help + const helpBonus = this.profile.helpBonuses[hadHelp ? 1 : 0] probability += helpBonus // Clamp to valid probability range @@ -229,12 +230,12 @@ export class SimulatedStudent { } /** - * Select whether student uses help (binary). + * Select whether student uses help. * Based on profile's helpUsageProbabilities [P(no help), P(help)]. */ - private selectHelpLevel(): HelpLevel { + private selectHelpUsage(): boolean { const [pNoHelp] = this.profile.helpUsageProbabilities - return this.rng.next() < pNoHelp ? 0 : 1 + return this.rng.next() >= pNoHelp } /** @@ -242,7 +243,7 @@ export class SimulatedStudent { */ private calculateResponseTime( skillIds: string[], - helpLevel: HelpLevel, + hadHelp: boolean, isCorrect: boolean ): number { const base = this.profile.baseResponseTimeMs @@ -258,7 +259,7 @@ export class SimulatedStudent { const exposureFactor = 2.0 - Math.min(1.0, avgExposure / (this.profile.halfMaxExposure * 2)) // Help usage adds time (reading hints, etc.) - const helpFactor = 1.0 + helpLevel * 0.25 + const helpFactor = hadHelp ? 1.25 : 1.0 // Incorrect answers: sometimes faster (gave up), sometimes slower (struggled) const correctnessFactor = isCorrect ? 1.0 : this.rng.chance(0.5) ? 0.7 : 1.4 diff --git a/apps/web/src/test/journey-simulator/types.ts b/apps/web/src/test/journey-simulator/types.ts index 1bddd795..5ed2ac48 100644 --- a/apps/web/src/test/journey-simulator/types.ts +++ b/apps/web/src/test/journey-simulator/types.ts @@ -4,7 +4,6 @@ * Type definitions for the BKT validation test infrastructure. */ -import type { HelpLevel } from '@/db/schema/session-plans' import type { BlameMethod } from '@/lib/curriculum/bkt' import type { ProblemGenerationMode } from '@/lib/curriculum/config/bkt-integration' @@ -114,8 +113,8 @@ export interface SimulatedAnswer { isCorrect: boolean /** Time taken to answer in milliseconds */ responseTimeMs: number - /** Help level used (0 = no help, 1 = used help) */ - helpLevelUsed: HelpLevel + /** Whether help was used during this problem */ + hadHelp: boolean /** Skills that were actually challenged by this problem */ skillsChallenged: string[] /** diff --git a/apps/web/src/test/session-targeting-trace.test.ts b/apps/web/src/test/session-targeting-trace.test.ts index d6fc95c9..d915efff 100644 --- a/apps/web/src/test/session-targeting-trace.test.ts +++ b/apps/web/src/test/session-targeting-trace.test.ts @@ -104,7 +104,7 @@ describe('Session Targeting Trace', () => { await recordSlotResult(plan1.id, part.partNumber, slot.index, { isCorrect: answer.isCorrect, responseTimeMs: answer.responseTimeMs, - helpLevelUsed: answer.helpLevelUsed, + hadHelp: answer.hadHelp, skillsExercised: answer.skillsChallenged, }) } @@ -189,7 +189,7 @@ describe('Session Targeting Trace', () => { await recordSlotResult(classicPlan1.id, part.partNumber, slot.index, { isCorrect: answer.isCorrect, responseTimeMs: answer.responseTimeMs, - helpLevelUsed: answer.helpLevelUsed, + hadHelp: answer.hadHelp, skillsExercised: answer.skillsChallenged, }) }