workspace-v1/projects/gamilit/docs/01-fase-alcance-inicial/EAI-002-actividades/especificaciones/ET-EDU-003-taxonomia-bloom.md
Adrian Flores Cortes 967ab360bb Initial commit: Workspace v1 with 3-layer architecture
Structure:
- control-plane/: Registries, SIMCO directives, CI/CD templates
- projects/: Gamilit, ERP-Suite, Trading-Platform, Betting-Analytics
- shared/: Libs catalog, knowledge-base

Key features:
- Centralized port, domain, database, and service registries
- 23 SIMCO directives + 6 fundamental principles
- NEXUS agent profiles with delegation rules
- Validation scripts for workspace integrity
- Dockerfiles for all services
- Path aliases for quick reference

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-23 00:35:19 -06:00

46 KiB

ET-EDU-003: Especificación Técnica - Taxonomía de Bloom

ID: ET-EDU-003 Título: Implementación de Clasificación Cognitiva según Bloom Módulo: 03-contenido-educativo Tipo: Especificación Técnica Estado: Implementado Prioridad: Alta Versión: 1.0 Última actualización: 2025-11-07


📋 Resumen Ejecutivo

Esta especificación técnica define la implementación completa del sistema de clasificación cognitiva basado en la Taxonomía de Bloom Revisada (6 niveles: Remember → Create) en la plataforma Gamilit. Incluye:

  • Schema de base de datos con ENUMs, tablas de analytics, funciones y políticas RLS
  • Backend (NestJS) con servicios de analytics cognitivos, filtrado y recomendaciones
  • Frontend (React) con visualización de perfil cognitivo y progreso LOTS/HOTS
  • Sistema de gamificación con achievements específicos por nivel cognitivo
  • Integración con sistema de dificultad (ET-EDU-002) para complejidad multidimensional

🔗 Referencias

Implementa:

Relacionado con:


🗄️ 1. Base de Datos (PostgreSQL)

1.1 ENUM: bloom_level

-- Archivo: apps/database/ddl/schemas/educational_content/enums/bloom_level.sql
CREATE TYPE educational_content.bloom_level AS ENUM (
    'remember',      -- Recordar: recuperar información de la memoria
    'understand',    -- Comprender: construir significado, interpretar
    'apply',         -- Aplicar: usar información en situaciones nuevas
    'analyze',       -- Analizar: descomponer en partes, encontrar relaciones
    'evaluate',      -- Evaluar: hacer juicios basados en criterios
    'create'         -- Crear: reunir elementos para formar un todo original
);

COMMENT ON TYPE educational_content.bloom_level IS
'Los 6 niveles de la Taxonomía de Bloom Revisada (Anderson & Krathwohl, 2001)';

1.2 Modificación: Agregar columna a exercises

-- Archivo: apps/database/ddl/schemas/educational_content/tables/exercises.sql (modificación)

-- Agregar columna bloom_level a la tabla exercises
ALTER TABLE educational_content.exercises
ADD COLUMN bloom_level educational_content.bloom_level NOT NULL DEFAULT 'remember';

COMMENT ON COLUMN educational_content.exercises.bloom_level IS
'Nivel cognitivo del ejercicio según Taxonomía de Bloom';

-- Índice para búsquedas y filtrado
CREATE INDEX idx_exercises_bloom_level
ON educational_content.exercises(bloom_level);

-- Índice compuesto para búsquedas combinadas (dificultad + bloom)
CREATE INDEX idx_exercises_difficulty_bloom
ON educational_content.exercises(difficulty_level, bloom_level);

1.3 Tabla: cognitive_performance

Tracking del desempeño cognitivo por usuario y nivel de Bloom.

-- Archivo: apps/database/ddl/schemas/progress_tracking/tables/cognitive_performance.sql
CREATE TABLE progress_tracking.cognitive_performance (
    user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
    bloom_level educational_content.bloom_level NOT NULL,

    -- Métricas de desempeño
    exercises_attempted INT NOT NULL DEFAULT 0,
    exercises_correct INT NOT NULL DEFAULT 0,

    -- Tasa de éxito (calculada automáticamente)
    success_rate NUMERIC(5,2) GENERATED ALWAYS AS (
        CASE
            WHEN exercises_attempted > 0
            THEN ROUND((exercises_correct::NUMERIC / exercises_attempted) * 100, 2)
            ELSE 0
        END
    ) STORED,

    -- Tiempo de ejecución
    total_time_spent_seconds BIGINT NOT NULL DEFAULT 0,
    avg_time_per_exercise NUMERIC(10,2) GENERATED ALWAYS AS (
        CASE
            WHEN exercises_attempted > 0
            THEN ROUND(total_time_spent_seconds::NUMERIC / exercises_attempted, 2)
            ELSE 0
        END
    ) STORED,

    -- Timestamps
    last_attempt_at TIMESTAMP WITH TIME ZONE,
    created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,

    PRIMARY KEY (user_id, bloom_level)
);

-- Índices
CREATE INDEX idx_cognitive_performance_user ON progress_tracking.cognitive_performance(user_id);
CREATE INDEX idx_cognitive_performance_level ON progress_tracking.cognitive_performance(bloom_level);
CREATE INDEX idx_cognitive_performance_success_rate ON progress_tracking.cognitive_performance(bloom_level, success_rate DESC);

-- Política RLS
ALTER TABLE progress_tracking.cognitive_performance ENABLE ROW LEVEL SECURITY;

CREATE POLICY "Users can view own cognitive performance"
ON progress_tracking.cognitive_performance
FOR SELECT
USING (auth.uid() = user_id);

CREATE POLICY "Teachers can view students cognitive performance"
ON progress_tracking.cognitive_performance
FOR SELECT
USING (
    EXISTS (
        SELECT 1 FROM auth.user_roles ur
        WHERE ur.user_id = auth.uid()
        AND ur.role = 'teacher'
    )
);

CREATE POLICY "System can manage cognitive performance"
ON progress_tracking.cognitive_performance
FOR ALL
USING (TRUE)
WITH CHECK (TRUE);

-- Trigger para updated_at
CREATE TRIGGER update_cognitive_performance_updated_at
BEFORE UPDATE ON progress_tracking.cognitive_performance
FOR EACH ROW
EXECUTE FUNCTION public.update_updated_at_column();

1.4 Función: update_cognitive_performance()

Actualiza el desempeño cognitivo tras completar un ejercicio.

-- Archivo: apps/database/ddl/schemas/progress_tracking/functions/update_cognitive_performance.sql
CREATE OR REPLACE FUNCTION progress_tracking.update_cognitive_performance(
    p_user_id UUID,
    p_bloom_level educational_content.bloom_level,
    p_is_correct BOOLEAN,
    p_time_spent_seconds INT
) RETURNS VOID AS $$
BEGIN
    -- Upsert del desempeño cognitivo
    INSERT INTO progress_tracking.cognitive_performance
    (user_id, bloom_level, exercises_attempted, exercises_correct, total_time_spent_seconds, last_attempt_at)
    VALUES (
        p_user_id,
        p_bloom_level,
        1,
        CASE WHEN p_is_correct THEN 1 ELSE 0 END,
        p_time_spent_seconds,
        CURRENT_TIMESTAMP
    )
    ON CONFLICT (user_id, bloom_level)
    DO UPDATE SET
        exercises_attempted = progress_tracking.cognitive_performance.exercises_attempted + 1,
        exercises_correct = progress_tracking.cognitive_performance.exercises_correct +
            CASE WHEN p_is_correct THEN 1 ELSE 0 END,
        total_time_spent_seconds = progress_tracking.cognitive_performance.total_time_spent_seconds + p_time_spent_seconds,
        last_attempt_at = CURRENT_TIMESTAMP,
        updated_at = CURRENT_TIMESTAMP;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;

COMMENT ON FUNCTION progress_tracking.update_cognitive_performance IS
'Actualiza el desempeño cognitivo del usuario tras completar un ejercicio';

1.5 Función: get_cognitive_profile()

Obtiene el perfil cognitivo completo del usuario con análisis LOTS/HOTS.

-- Archivo: apps/database/ddl/schemas/progress_tracking/functions/get_cognitive_profile.sql
CREATE OR REPLACE FUNCTION progress_tracking.get_cognitive_profile(p_user_id UUID)
RETURNS TABLE(
    bloom_level educational_content.bloom_level,
    success_rate NUMERIC,
    attempts INT,
    correct INT,
    avg_time NUMERIC,
    status TEXT,
    category TEXT  -- 'LOTS', 'MOTS', 'HOTS'
) AS $$
BEGIN
    RETURN QUERY
    SELECT
        cp.bloom_level,
        cp.success_rate,
        cp.exercises_attempted,
        cp.exercises_correct,
        cp.avg_time_per_exercise,
        CASE
            WHEN cp.exercises_attempted < 5 THEN 'insufficient_data'
            WHEN cp.success_rate >= 80 THEN 'excellent'
            WHEN cp.success_rate >= 70 THEN 'good'
            WHEN cp.success_rate >= 60 THEN 'needs_practice'
            ELSE 'requires_attention'
        END AS status,
        CASE
            WHEN cp.bloom_level IN ('remember', 'understand') THEN 'LOTS'
            WHEN cp.bloom_level IN ('apply', 'analyze') THEN 'MOTS'
            WHEN cp.bloom_level IN ('evaluate', 'create') THEN 'HOTS'
        END AS category
    FROM progress_tracking.cognitive_performance cp
    WHERE cp.user_id = p_user_id
    ORDER BY
        CASE cp.bloom_level
            WHEN 'remember' THEN 1
            WHEN 'understand' THEN 2
            WHEN 'apply' THEN 3
            WHEN 'analyze' THEN 4
            WHEN 'evaluate' THEN 5
            WHEN 'create' THEN 6
        END;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;

COMMENT ON FUNCTION progress_tracking.get_cognitive_profile IS
'Obtiene el perfil cognitivo del usuario con análisis LOTS/MOTS/HOTS';

1.6 Función: get_weak_cognitive_levels()

Identifica niveles cognitivos débiles que requieren práctica.

-- Archivo: apps/database/ddl/schemas/progress_tracking/functions/get_weak_cognitive_levels.sql
CREATE OR REPLACE FUNCTION progress_tracking.get_weak_cognitive_levels(
    p_user_id UUID,
    p_success_threshold NUMERIC DEFAULT 70.0,
    p_min_attempts INT DEFAULT 5
)
RETURNS TABLE(
    bloom_level educational_content.bloom_level,
    success_rate NUMERIC,
    gap NUMERIC  -- Diferencia con el threshold
) AS $$
BEGIN
    RETURN QUERY
    SELECT
        cp.bloom_level,
        cp.success_rate,
        (p_success_threshold - cp.success_rate) AS gap
    FROM progress_tracking.cognitive_performance cp
    WHERE cp.user_id = p_user_id
    AND cp.exercises_attempted >= p_min_attempts
    AND cp.success_rate < p_success_threshold
    ORDER BY cp.success_rate ASC;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;

COMMENT ON FUNCTION progress_tracking.get_weak_cognitive_levels IS
'Identifica niveles cognitivos con tasa de éxito por debajo del umbral';

1.7 Vista Materializada: cognitive_level_distribution

Distribución de ejercicios por nivel cognitivo en el sistema.

-- Archivo: apps/database/ddl/schemas/educational_content/materialized-views/cognitive_level_distribution.sql
CREATE MATERIALIZED VIEW educational_content.cognitive_level_distribution AS
SELECT
    bloom_level,
    COUNT(*) AS total_exercises,
    ROUND((COUNT(*)::NUMERIC / SUM(COUNT(*)) OVER ()) * 100, 2) AS percentage,
    COUNT(CASE WHEN difficulty_level IN ('beginner', 'elementary') THEN 1 END) AS beginners_count,
    COUNT(CASE WHEN difficulty_level IN ('pre_intermediate', 'intermediate') THEN 1 END) AS intermediate_count,
    COUNT(CASE WHEN difficulty_level IN ('upper_intermediate', 'advanced', 'proficient', 'native') THEN 1 END) AS advanced_count
FROM educational_content.exercises
WHERE is_active = TRUE
GROUP BY bloom_level
ORDER BY
    CASE bloom_level
        WHEN 'remember' THEN 1
        WHEN 'understand' THEN 2
        WHEN 'apply' THEN 3
        WHEN 'analyze' THEN 4
        WHEN 'evaluate' THEN 5
        WHEN 'create' THEN 6
    END;

CREATE UNIQUE INDEX idx_cognitive_distribution_level
ON educational_content.cognitive_level_distribution(bloom_level);

-- Refresh programado (diariamente a las 3 AM)
SELECT cron.schedule('refresh-cognitive-distribution', '0 3 * * *',
    'REFRESH MATERIALIZED VIEW CONCURRENTLY educational_content.cognitive_level_distribution');

COMMENT ON MATERIALIZED VIEW educational_content.cognitive_level_distribution IS
'Distribución de ejercicios por nivel cognitivo de Bloom';

1.8 Achievements Cognitivos

-- Archivo: apps/database/ddl/schemas/gamification_system/tables/achievements.sql (inserción)

-- Insertar achievements específicos para niveles cognitivos
INSERT INTO gamification_system.achievements
(id, name, description, icon, xp_reward, coins_reward, criteria, rarity)
VALUES
('critical_thinker', 'Pensador Crítico', 'Alcanza 80% de éxito en ejercicios de Analyze', '🔍', 500, 100,
 '{"bloom_level": "analyze", "success_rate": 80, "min_attempts": 20}'::jsonb, 'rare'),

('master_creator', 'Creador Maestro', 'Completa 10 ejercicios de Create con calificación excelente', '🎨', 1000, 250,
 '{"bloom_level": "create", "excellent_count": 10}'::jsonb, 'epic'),

('lots_champion', 'Campeón LOTS', 'Domina todos los niveles básicos (Remember + Understand) con 85%+', '🧠', 300, 75,
 '{"categories": ["remember", "understand"], "success_rate": 85, "min_attempts_each": 30}'::jsonb, 'rare'),

('hots_master', 'Maestro HOTS', 'Domina todos los niveles superiores (Evaluate + Create) con 70%+', '🎓', 1500, 500,
 '{"categories": ["evaluate", "create"], "success_rate": 70, "min_attempts_each": 20}'::jsonb, 'legendary'),

('balanced_learner', 'Aprendiz Equilibrado', 'Mantén 70%+ de éxito en todos los 6 niveles cognitivos', '⚖️', 750, 200,
 '{"all_levels": true, "success_rate": 70, "min_attempts_each": 15}'::jsonb, 'epic'),

('cognitive_explorer', 'Explorador Cognitivo', 'Completa al menos 5 ejercicios de cada nivel cognitivo', '🗺️', 200, 50,
 '{"all_levels": true, "min_attempts_each": 5}'::jsonb, 'uncommon');

-- Badges por nivel cognitivo
INSERT INTO gamification_system.badges
(id, name, description, icon, unlock_criteria)
VALUES
('badge_remember', 'Badge Recordar', '100 ejercicios de Remember con 85%+ éxito', '🧠',
 '{"bloom_level": "remember", "count": 100, "success_rate": 85}'::jsonb),

('badge_understand', 'Badge Comprender', '100 ejercicios de Understand con 85%+ éxito', '💡',
 '{"bloom_level": "understand", "count": 100, "success_rate": 85}'::jsonb),

('badge_apply', 'Badge Aplicar', '75 ejercicios de Apply con 80%+ éxito', '🔧',
 '{"bloom_level": "apply", "count": 75, "success_rate": 80}'::jsonb),

('badge_analyze', 'Badge Analizar', '50 ejercicios de Analyze con 75%+ éxito', '🔍',
 '{"bloom_level": "analyze", "count": 50, "success_rate": 75}'::jsonb),

('badge_evaluate', 'Badge Evaluar', '30 ejercicios de Evaluate con 70%+ éxito', '⚖️',
 '{"bloom_level": "evaluate", "count": 30, "success_rate": 70}'::jsonb),

('badge_create', 'Badge Crear', '20 ejercicios de Create con 70%+ éxito', '🎨',
 '{"bloom_level": "create", "count": 20, "success_rate": 70}'::jsonb);

🖥️ 2. Backend (NestJS + TypeScript)

2.1 Enum: BloomLevel

// Archivo: apps/backend/src/modules/educational-content/enums/bloom-level.enum.ts
export enum BloomLevel {
    REMEMBER = 'remember',
    UNDERSTAND = 'understand',
    APPLY = 'apply',
    ANALYZE = 'analyze',
    EVALUATE = 'evaluate',
    CREATE = 'create'
}

export const BLOOM_ORDER: BloomLevel[] = [
    BloomLevel.REMEMBER,
    BloomLevel.UNDERSTAND,
    BloomLevel.APPLY,
    BloomLevel.ANALYZE,
    BloomLevel.EVALUATE,
    BloomLevel.CREATE
];

export type CognitiveCategory = 'LOTS' | 'MOTS' | 'HOTS';

export function getCognitiveCategory(level: BloomLevel): CognitiveCategory {
    if (level === BloomLevel.REMEMBER || level === BloomLevel.UNDERSTAND) {
        return 'LOTS'; // Lower Order Thinking Skills
    }
    if (level === BloomLevel.APPLY || level === BloomLevel.ANALYZE) {
        return 'MOTS'; // Middle Order Thinking Skills
    }
    return 'HOTS'; // Higher Order Thinking Skills (Evaluate, Create)
}

export const BLOOM_LABELS: Record<BloomLevel, string> = {
    [BloomLevel.REMEMBER]: 'Recordar',
    [BloomLevel.UNDERSTAND]: 'Comprender',
    [BloomLevel.APPLY]: 'Aplicar',
    [BloomLevel.ANALYZE]: 'Analizar',
    [BloomLevel.EVALUATE]: 'Evaluar',
    [BloomLevel.CREATE]: 'Crear'
};

2.2 Entity: CognitivePerformance

// Archivo: apps/backend/src/modules/progress/entities/cognitive-performance.entity.ts
import { Entity, Column, PrimaryColumn, Index } from 'typeorm';
import { BloomLevel } from '../../educational-content/enums/bloom-level.enum';

@Entity('cognitive_performance', { schema: 'progress_tracking' })
@Index(['user_id', 'bloom_level'], { unique: true })
export class CognitivePerformance {
    @PrimaryColumn('uuid')
    user_id: string;

    @PrimaryColumn({
        type: 'enum',
        enum: BloomLevel
    })
    bloom_level: BloomLevel;

    @Column({ type: 'int', default: 0 })
    exercises_attempted: number;

    @Column({ type: 'int', default: 0 })
    exercises_correct: number;

    @Column({ type: 'decimal', precision: 5, scale: 2, select: false })
    success_rate: number;

    @Column({ type: 'bigint', default: 0 })
    total_time_spent_seconds: number;

    @Column({ type: 'decimal', precision: 10, scale: 2, select: false })
    avg_time_per_exercise: number;

    @Column({ type: 'timestamp with time zone', nullable: true })
    last_attempt_at: Date;

    @Column({ type: 'timestamp with time zone', default: () => 'CURRENT_TIMESTAMP' })
    created_at: Date;

    @Column({ type: 'timestamp with time zone', default: () => 'CURRENT_TIMESTAMP' })
    updated_at: Date;
}

2.3 DTO: CognitiveProfileDto

// Archivo: apps/backend/src/modules/analytics/dto/cognitive-profile.dto.ts
import { BloomLevel, CognitiveCategory } from '../../educational-content/enums/bloom-level.enum';

export class CognitiveLevelProfileDto {
    bloom_level: BloomLevel;
    success_rate: number;
    attempts: number;
    correct: number;
    avg_time: number;
    status: 'insufficient_data' | 'excellent' | 'good' | 'needs_practice' | 'requires_attention';
    category: CognitiveCategory;
}

export class CognitiveProfileDto {
    profile: CognitiveLevelProfileDto[];
    weak_levels: BloomLevel[];
    overall_lots: number;  // Tasa de éxito promedio en LOTS
    overall_mots: number;  // Tasa de éxito promedio en MOTS
    overall_hots: number;  // Tasa de éxito promedio en HOTS
}

export class WeakCognitiveLevelDto {
    bloom_level: BloomLevel;
    success_rate: number;
    gap: number;  // Diferencia con el umbral (70%)
}

2.4 Service: CognitiveAnalyticsService

// Archivo: apps/backend/src/modules/analytics/services/cognitive-analytics.service.ts
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository, DataSource } from 'typeorm';
import { CognitivePerformance } from '../../progress/entities/cognitive-performance.entity';
import { BloomLevel, getCognitiveCategory } from '../../educational-content/enums/bloom-level.enum';
import { CognitiveProfileDto, WeakCognitiveLevelDto } from '../dto/cognitive-profile.dto';

@Injectable()
export class CognitiveAnalyticsService {
    constructor(
        @InjectRepository(CognitivePerformance)
        private cognitiveRepo: Repository<CognitivePerformance>,
        private dataSource: DataSource
    ) {}

    /**
     * Obtiene el perfil cognitivo completo del usuario
     */
    async getCognitiveProfile(userId: string): Promise<CognitiveProfileDto> {
        const result = await this.dataSource.query(
            'SELECT * FROM progress_tracking.get_cognitive_profile($1)',
            [userId]
        );

        const weakLevels = await this.getWeakCognitiveLevels(userId);

        return {
            profile: result,
            weak_levels: weakLevels.map(w => w.bloom_level),
            overall_lots: this.calculateCategoryRate(result, 'LOTS'),
            overall_mots: this.calculateCategoryRate(result, 'MOTS'),
            overall_hots: this.calculateCategoryRate(result, 'HOTS')
        };
    }

    /**
     * Calcula la tasa de éxito promedio para una categoría (LOTS/MOTS/HOTS)
     */
    private calculateCategoryRate(profile: any[], category: string): number {
        const categoryLevels = profile.filter(p => p.category === category);
        if (categoryLevels.length === 0) return 0;

        const totalAttempts = categoryLevels.reduce((sum, p) => sum + p.attempts, 0);
        const totalCorrect = categoryLevels.reduce((sum, p) => sum + p.correct, 0);

        return totalAttempts > 0 ? Math.round((totalCorrect / totalAttempts) * 100) : 0;
    }

    /**
     * Identifica niveles cognitivos débiles
     */
    async getWeakCognitiveLevels(
        userId: string,
        successThreshold: number = 70,
        minAttempts: number = 5
    ): Promise<WeakCognitiveLevelDto[]> {
        return this.dataSource.query(
            'SELECT * FROM progress_tracking.get_weak_cognitive_levels($1, $2, $3)',
            [userId, successThreshold, minAttempts]
        );
    }

    /**
     * Actualiza el desempeño cognitivo tras completar un ejercicio
     */
    async updateCognitivePerformance(
        userId: string,
        bloomLevel: BloomLevel,
        isCorrect: boolean,
        timeSpentSeconds: number
    ): Promise<void> {
        await this.dataSource.query(
            'SELECT progress_tracking.update_cognitive_performance($1, $2, $3, $4)',
            [userId, bloomLevel, isCorrect, timeSpentSeconds]
        );
    }

    /**
     * Obtiene recomendaciones de ejercicios basadas en niveles débiles
     */
    async getRecommendedExercises(userId: string, limit: number = 5): Promise<any[]> {
        const weakLevels = await this.getWeakCognitiveLevels(userId);

        if (weakLevels.length === 0) {
            // Si no hay niveles débiles, recomendar HOTS para desafío
            return this.dataSource.query(`
                SELECT e.id, e.title, e.bloom_level, e.difficulty_level
                FROM educational_content.exercises e
                WHERE e.bloom_level IN ('evaluate', 'create')
                AND e.is_active = TRUE
                ORDER BY RANDOM()
                LIMIT $1
            `, [limit]);
        }

        // Recomendar ejercicios de niveles débiles
        const weakLevelNames = weakLevels.map(w => w.bloom_level);
        return this.dataSource.query(`
            SELECT e.id, e.title, e.bloom_level, e.difficulty_level
            FROM educational_content.exercises e
            WHERE e.bloom_level = ANY($1)
            AND e.is_active = TRUE
            ORDER BY RANDOM()
            LIMIT $2
        `, [weakLevelNames, limit]);
    }

    /**
     * Obtiene estadísticas agregadas por nivel cognitivo
     */
    async getBloomLevelStatistics(level: BloomLevel): Promise<any> {
        const result = await this.dataSource.query(`
            SELECT
                COUNT(DISTINCT user_id) AS total_users,
                ROUND(AVG(success_rate), 2) AS avg_success_rate,
                ROUND(AVG(avg_time_per_exercise), 2) AS avg_time_per_exercise,
                COUNT(CASE WHEN success_rate >= 80 THEN 1 END) AS users_excellent
            FROM progress_tracking.cognitive_performance
            WHERE bloom_level = $1
            AND exercises_attempted >= 5
        `, [level]);

        return result[0];
    }

    /**
     * Verifica si el usuario ha desbloqueado un achievement cognitivo
     */
    async checkCognitiveAchievements(userId: string): Promise<string[]> {
        const profile = await this.getCognitiveProfile(userId);
        const unlockedAchievements: string[] = [];

        // Critical Thinker: 80%+ en Analyze con 20+ intentos
        const analyzePerf = profile.profile.find(p => p.bloom_level === BloomLevel.ANALYZE);
        if (analyzePerf && analyzePerf.success_rate >= 80 && analyzePerf.attempts >= 20) {
            unlockedAchievements.push('critical_thinker');
        }

        // LOTS Champion: 85%+ en Remember y Understand con 30+ cada uno
        const rememberPerf = profile.profile.find(p => p.bloom_level === BloomLevel.REMEMBER);
        const understandPerf = profile.profile.find(p => p.bloom_level === BloomLevel.UNDERSTAND);
        if (
            rememberPerf && rememberPerf.success_rate >= 85 && rememberPerf.attempts >= 30 &&
            understandPerf && understandPerf.success_rate >= 85 && understandPerf.attempts >= 30
        ) {
            unlockedAchievements.push('lots_champion');
        }

        // HOTS Master: 70%+ en Evaluate y Create con 20+ cada uno
        const evaluatePerf = profile.profile.find(p => p.bloom_level === BloomLevel.EVALUATE);
        const createPerf = profile.profile.find(p => p.bloom_level === BloomLevel.CREATE);
        if (
            evaluatePerf && evaluatePerf.success_rate >= 70 && evaluatePerf.attempts >= 20 &&
            createPerf && createPerf.success_rate >= 70 && createPerf.attempts >= 20
        ) {
            unlockedAchievements.push('hots_master');
        }

        // Balanced Learner: 70%+ en todos los niveles con 15+ cada uno
        const allAbove70 = profile.profile.every(p => p.success_rate >= 70 && p.attempts >= 15);
        if (allAbove70 && profile.profile.length === 6) {
            unlockedAchievements.push('balanced_learner');
        }

        // Cognitive Explorer: 5+ intentos en cada nivel
        const allExplored = profile.profile.every(p => p.attempts >= 5);
        if (allExplored && profile.profile.length === 6) {
            unlockedAchievements.push('cognitive_explorer');
        }

        return unlockedAchievements;
    }
}

2.5 Controller: CognitiveAnalyticsController

// Archivo: apps/backend/src/modules/analytics/controllers/cognitive-analytics.controller.ts
import { Controller, Get, UseGuards, Query, ParseIntPipe } from '@nestjs/common';
import { JwtAuthGuard } from '../../auth/guards/jwt-auth.guard';
import { CurrentUser } from '../../auth/decorators/current-user.decorator';
import { CognitiveAnalyticsService } from '../services/cognitive-analytics.service';

@Controller('analytics/cognitive')
@UseGuards(JwtAuthGuard)
export class CognitiveAnalyticsController {
    constructor(private cognitiveService: CognitiveAnalyticsService) {}

    @Get('profile')
    async getCognitiveProfile(@CurrentUser() user: any) {
        return this.cognitiveService.getCognitiveProfile(user.userId);
    }

    @Get('weak-levels')
    async getWeakLevels(
        @CurrentUser() user: any,
        @Query('threshold', new ParseIntPipe({ optional: true })) threshold?: number,
        @Query('minAttempts', new ParseIntPipe({ optional: true })) minAttempts?: number
    ) {
        return this.cognitiveService.getWeakCognitiveLevels(
            user.userId,
            threshold || 70,
            minAttempts || 5
        );
    }

    @Get('recommendations')
    async getRecommendations(
        @CurrentUser() user: any,
        @Query('limit', new ParseIntPipe({ optional: true })) limit?: number
    ) {
        const exercises = await this.cognitiveService.getRecommendedExercises(
            user.userId,
            limit || 5
        );

        const weakLevels = await this.cognitiveService.getWeakCognitiveLevels(user.userId);

        return {
            weak_levels: weakLevels,
            recommended_exercises: exercises
        };
    }

    @Get('achievements')
    async checkAchievements(@CurrentUser() user: any) {
        return this.cognitiveService.checkCognitiveAchievements(user.userId);
    }
}

2.6 Modificación: ExerciseAttemptService

Actualizar para llamar a update_cognitive_performance tras cada intento.

// Archivo: apps/backend/src/modules/exercises/services/exercise-attempt.service.ts (modificación)

import { CognitiveAnalyticsService } from '../../analytics/services/cognitive-analytics.service';

@Injectable()
export class ExerciseAttemptService {
    constructor(
        // ... otros repositorios
        private cognitiveAnalyticsService: CognitiveAnalyticsService
    ) {}

    async submitAttempt(userId: string, exerciseId: string, answer: any): Promise<any> {
        // ... lógica existente de corrección

        // NUEVO: Actualizar desempeño cognitivo
        const exercise = await this.exerciseRepo.findOne({ where: { id: exerciseId } });
        if (exercise && exercise.bloom_level) {
            await this.cognitiveAnalyticsService.updateCognitivePerformance(
                userId,
                exercise.bloom_level,
                isCorrect,
                timeSpentSeconds
            );
        }

        // ... resto de la lógica
    }
}

🎨 3. Frontend (React + TypeScript)

3.1 Hook: useCognitiveProfile

// Archivo: apps/frontend/src/hooks/useCognitiveProfile.ts
import { useQuery } from '@tanstack/react-query';
import { apiClient } from '../lib/api-client';
import { BloomLevel, CognitiveCategory } from '../types/bloom-level';

export interface CognitiveLevelProfile {
    bloom_level: BloomLevel;
    success_rate: number;
    attempts: number;
    correct: number;
    avg_time: number;
    status: 'insufficient_data' | 'excellent' | 'good' | 'needs_practice' | 'requires_attention';
    category: CognitiveCategory;
}

export interface CognitiveProfileData {
    profile: CognitiveLevelProfile[];
    weak_levels: BloomLevel[];
    overall_lots: number;
    overall_mots: number;
    overall_hots: number;
}

export function useCognitiveProfile() {
    return useQuery<CognitiveProfileData>({
        queryKey: ['cognitive-profile'],
        queryFn: async () => {
            const response = await apiClient.get('/analytics/cognitive/profile');
            return response.data;
        },
        staleTime: 5 * 60 * 1000 // 5 minutos
    });
}

export function useCognitiveRecommendations(limit: number = 5) {
    return useQuery({
        queryKey: ['cognitive-recommendations', limit],
        queryFn: async () => {
            const response = await apiClient.get('/analytics/cognitive/recommendations', {
                params: { limit }
            });
            return response.data;
        },
        staleTime: 10 * 60 * 1000 // 10 minutos
    });
}

3.2 Componente: CognitiveProfileDashboard

// Archivo: apps/frontend/src/components/analytics/CognitiveProfileDashboard.tsx
import React from 'react';
import { useCognitiveProfile } from '../../hooks/useCognitiveProfile';
import { Progress } from '../ui/Progress';
import { Badge } from '../ui/Badge';

const BLOOM_LABELS = {
    remember: 'Recordar',
    understand: 'Comprender',
    apply: 'Aplicar',
    analyze: 'Analizar',
    evaluate: 'Evaluar',
    create: 'Crear'
};

const STATUS_COLORS = {
    excellent: 'green',
    good: 'blue',
    needs_practice: 'yellow',
    requires_attention: 'red',
    insufficient_data: 'gray'
};

const CATEGORY_LABELS = {
    LOTS: 'Habilidades Básicas (LOTS)',
    MOTS: 'Pensamiento Medio (MOTS)',
    HOTS: 'Pensamiento Superior (HOTS)'
};

export function CognitiveProfileDashboard() {
    const { data, isLoading, error } = useCognitiveProfile();

    if (isLoading) return <div>Cargando perfil cognitivo...</div>;
    if (error) return <div>Error al cargar perfil cognitivo</div>;
    if (!data) return null;

    return (
        <div className="cognitive-profile">
            <h2>Tu Perfil Cognitivo</h2>

            {/* Resumen LOTS/MOTS/HOTS */}
            <div className="category-summary">
                <div className="category-item">
                    <span className="category-label">{CATEGORY_LABELS.LOTS}:</span>
                    <strong className="category-value">{data.overall_lots}%</strong>
                    <Progress value={data.overall_lots} max={100} color="green" />
                </div>
                <div className="category-item">
                    <span className="category-label">{CATEGORY_LABELS.MOTS}:</span>
                    <strong className="category-value">{data.overall_mots}%</strong>
                    <Progress value={data.overall_mots} max={100} color="blue" />
                </div>
                <div className="category-item">
                    <span className="category-label">{CATEGORY_LABELS.HOTS}:</span>
                    <strong className="category-value">{data.overall_hots}%</strong>
                    <Progress value={data.overall_hots} max={100} color="purple" />
                </div>
            </div>

            {/* Desglose por nivel de Bloom */}
            <div className="levels-breakdown">
                <h3>Desempeño por Nivel Cognitivo</h3>
                {data.profile.map(level => (
                    <div key={level.bloom_level} className="level-item">
                        <div className="level-header">
                            <span className="level-name">
                                {BLOOM_LABELS[level.bloom_level]}
                            </span>
                            <Badge color={STATUS_COLORS[level.status]}>
                                {level.success_rate}%
                            </Badge>
                            <Badge color="gray">{level.category}</Badge>
                        </div>
                        <Progress value={level.success_rate} max={100} />
                        <div className="level-stats">
                            <span>{level.correct}/{level.attempts} correctos</span>
                            <span>Tiempo promedio: {Math.round(level.avg_time)}s</span>
                        </div>
                    </div>
                ))}
            </div>

            {/* Recomendaciones */}
            {data.weak_levels.length > 0 && (
                <div className="recommendations">
                    <h3>Áreas de Mejora</h3>
                    <p>Practica más ejercicios de:</p>
                    <ul>
                        {data.weak_levels.map(level => (
                            <li key={level}>
                                <strong>{BLOOM_LABELS[level]}</strong>
                            </li>
                        ))}
                    </ul>
                </div>
            )}
        </div>
    );
}

3.3 Componente: BloomLevelFilter

Filtro de ejercicios por nivel cognitivo para maestros.

// Archivo: apps/frontend/src/components/exercises/BloomLevelFilter.tsx
import React from 'react';
import { BloomLevel } from '../../types/bloom-level';

const BLOOM_OPTIONS = [
    { value: BloomLevel.REMEMBER, label: 'Recordar', icon: '🧠' },
    { value: BloomLevel.UNDERSTAND, label: 'Comprender', icon: '💡' },
    { value: BloomLevel.APPLY, label: 'Aplicar', icon: '🔧' },
    { value: BloomLevel.ANALYZE, label: 'Analizar', icon: '🔍' },
    { value: BloomLevel.EVALUATE, label: 'Evaluar', icon: '⚖️' },
    { value: BloomLevel.CREATE, label: 'Crear', icon: '🎨' }
];

interface BloomLevelFilterProps {
    selected: BloomLevel[];
    onChange: (levels: BloomLevel[]) => void;
}

export function BloomLevelFilter({ selected, onChange }: BloomLevelFilterProps) {
    const handleToggle = (level: BloomLevel) => {
        if (selected.includes(level)) {
            onChange(selected.filter(l => l !== level));
        } else {
            onChange([...selected, level]);
        }
    };

    return (
        <div className="bloom-filter">
            <h4>Nivel Cognitivo</h4>
            <div className="bloom-options">
                {BLOOM_OPTIONS.map(option => (
                    <label key={option.value} className="bloom-option">
                        <input
                            type="checkbox"
                            checked={selected.includes(option.value)}
                            onChange={() => handleToggle(option.value)}
                        />
                        <span className="icon">{option.icon}</span>
                        <span className="label">{option.label}</span>
                    </label>
                ))}
            </div>
        </div>
    );
}

🧪 4. Tests

4.1 Test: cognitive-analytics.service.spec.ts

// Archivo: apps/backend/src/modules/analytics/services/cognitive-analytics.service.spec.ts
import { Test, TestingModule } from '@nestjs/testing';
import { CognitiveAnalyticsService } from './cognitive-analytics.service';
import { getRepositoryToken } from '@nestjs/typeorm';
import { CognitivePerformance } from '../../progress/entities/cognitive-performance.entity';
import { DataSource } from 'typeorm';
import { BloomLevel } from '../../educational-content/enums/bloom-level.enum';

describe('CognitiveAnalyticsService', () => {
    let service: CognitiveAnalyticsService;
    let mockCognitiveRepo: any;
    let mockDataSource: any;

    beforeEach(async () => {
        mockCognitiveRepo = {
            find: jest.fn(),
            findOne: jest.fn()
        };

        mockDataSource = {
            query: jest.fn()
        };

        const module: TestingModule = await Test.createTestingModule({
            providers: [
                CognitiveAnalyticsService,
                {
                    provide: getRepositoryToken(CognitivePerformance),
                    useValue: mockCognitiveRepo
                },
                {
                    provide: DataSource,
                    useValue: mockDataSource
                }
            ]
        }).compile();

        service = module.get<CognitiveAnalyticsService>(CognitiveAnalyticsService);
    });

    describe('getCognitiveProfile', () => {
        it('should return complete cognitive profile with LOTS/MOTS/HOTS rates', async () => {
            const mockProfileData = [
                { bloom_level: 'remember', success_rate: 85, attempts: 50, correct: 42, category: 'LOTS' },
                { bloom_level: 'understand', success_rate: 80, attempts: 40, correct: 32, category: 'LOTS' },
                { bloom_level: 'apply', success_rate: 75, attempts: 30, correct: 22, category: 'MOTS' },
                { bloom_level: 'analyze', success_rate: 65, attempts: 20, correct: 13, category: 'MOTS' },
                { bloom_level: 'evaluate', success_rate: 60, attempts: 15, correct: 9, category: 'HOTS' },
                { bloom_level: 'create', success_rate: 55, attempts: 10, correct: 5, category: 'HOTS' }
            ];

            mockDataSource.query
                .mockResolvedValueOnce(mockProfileData) // get_cognitive_profile
                .mockResolvedValueOnce([]); // get_weak_cognitive_levels

            const result = await service.getCognitiveProfile('user-123');

            expect(result.profile).toHaveLength(6);
            expect(result.overall_lots).toBeGreaterThan(0);
            expect(result.overall_mots).toBeGreaterThan(0);
            expect(result.overall_hots).toBeGreaterThan(0);
        });
    });

    describe('getWeakCognitiveLevels', () => {
        it('should return levels below success threshold', async () => {
            const mockWeakLevels = [
                { bloom_level: 'evaluate', success_rate: 60, gap: 10 },
                { bloom_level: 'create', success_rate: 55, gap: 15 }
            ];

            mockDataSource.query.mockResolvedValue(mockWeakLevels);

            const result = await service.getWeakCognitiveLevels('user-123', 70, 5);

            expect(result).toEqual(mockWeakLevels);
            expect(mockDataSource.query).toHaveBeenCalledWith(
                expect.stringContaining('get_weak_cognitive_levels'),
                ['user-123', 70, 5]
            );
        });
    });

    describe('checkCognitiveAchievements', () => {
        it('should return critical_thinker achievement when criteria met', async () => {
            const mockProfile = {
                profile: [
                    { bloom_level: BloomLevel.ANALYZE, success_rate: 85, attempts: 25, correct: 21 }
                ],
                weak_levels: [],
                overall_lots: 80,
                overall_mots: 75,
                overall_hots: 70
            };

            mockDataSource.query.mockResolvedValueOnce(mockProfile.profile);
            mockDataSource.query.mockResolvedValueOnce([]);

            jest.spyOn(service, 'getCognitiveProfile').mockResolvedValue(mockProfile as any);

            const achievements = await service.checkCognitiveAchievements('user-123');

            expect(achievements).toContain('critical_thinker');
        });

        it('should return lots_champion when both Remember and Understand exceed 85%', async () => {
            const mockProfile = {
                profile: [
                    { bloom_level: BloomLevel.REMEMBER, success_rate: 90, attempts: 40, correct: 36 },
                    { bloom_level: BloomLevel.UNDERSTAND, success_rate: 88, attempts: 35, correct: 31 }
                ],
                weak_levels: [],
                overall_lots: 89,
                overall_mots: 70,
                overall_hots: 65
            };

            jest.spyOn(service, 'getCognitiveProfile').mockResolvedValue(mockProfile as any);

            const achievements = await service.checkCognitiveAchievements('user-123');

            expect(achievements).toContain('lots_champion');
        });
    });
});

4.2 Test de Integración: Función SQL

-- Archivo: apps/database/tests/cognitive_performance_test.sql
-- Test de la función update_cognitive_performance

BEGIN;

-- Setup: Crear usuario de prueba
INSERT INTO auth.users (id, email, created_at)
VALUES ('test-user-cognitive', 'cognitive@test.com', CURRENT_TIMESTAMP)
ON CONFLICT (id) DO NOTHING;

-- Test 1: Primera actualización crea la fila
SELECT progress_tracking.update_cognitive_performance(
    'test-user-cognitive',
    'remember',
    TRUE,
    30
);

SELECT
    exercises_attempted,
    exercises_correct,
    success_rate
FROM progress_tracking.cognitive_performance
WHERE user_id = 'test-user-cognitive' AND bloom_level = 'remember';

-- Expected: attempts=1, correct=1, success_rate=100.00

-- Test 2: Segunda actualización incrementa correctamente
SELECT progress_tracking.update_cognitive_performance(
    'test-user-cognitive',
    'remember',
    FALSE,
    25
);

SELECT
    exercises_attempted,
    exercises_correct,
    success_rate
FROM progress_tracking.cognitive_performance
WHERE user_id = 'test-user-cognitive' AND bloom_level = 'remember';

-- Expected: attempts=2, correct=1, success_rate=50.00

-- Test 3: get_cognitive_profile retorna datos correctos
SELECT * FROM progress_tracking.get_cognitive_profile('test-user-cognitive');

-- Expected: Debe retornar al menos la fila de 'remember' con status calculado

-- Cleanup
DELETE FROM progress_tracking.cognitive_performance WHERE user_id = 'test-user-cognitive';
DELETE FROM auth.users WHERE id = 'test-user-cognitive';

ROLLBACK;

Criterios de Aceptación

CA-001: ENUMs y Schema Base

  • Existe ENUM bloom_level con 6 valores
  • Columna bloom_level agregada a tabla exercises
  • Índices creados para búsquedas eficientes

CA-002: Tracking de Desempeño Cognitivo

  • Tabla cognitive_performance registra intentos por nivel cognitivo
  • Campos calculados (success_rate, avg_time_per_exercise) funcionan
  • Función update_cognitive_performance() actualiza correctamente

CA-003: Análisis Cognitivo

  • Función get_cognitive_profile() retorna perfil completo con categoría (LOTS/MOTS/HOTS)
  • Función get_weak_cognitive_levels() identifica niveles débiles
  • Backend calcula tasas promedio por categoría

CA-004: Filtrado de Ejercicios

  • Maestros pueden filtrar ejercicios por nivel cognitivo
  • Índice compuesto (difficulty_level, bloom_level) optimiza búsquedas
  • API retorna ejercicios filtrados correctamente

CA-005: Recomendaciones

  • Sistema sugiere ejercicios de niveles débiles
  • Si no hay niveles débiles, sugiere HOTS para desafío
  • Recomendaciones son aleatorias pero relevantes

CA-006: Achievements Cognitivos

  • Existe achievement "Critical Thinker" (Analyze 80%+)
  • Existe achievement "LOTS Champion" (Remember + Understand 85%+)
  • Existe achievement "HOTS Master" (Evaluate + Create 70%+)
  • Service checkCognitiveAchievements() valida criterios correctamente

CA-007: Frontend UI

  • Componente CognitiveProfileDashboard muestra perfil visual
  • Resumen LOTS/MOTS/HOTS con gráficos de progreso
  • Indicación de áreas de mejora
  • Componente BloomLevelFilter para maestros

CA-008: Tests

  • Tests unitarios para CognitiveAnalyticsService
  • Tests de integración para funciones SQL
  • Tests de frontend para componentes

📊 5. Análisis y Reportes

5.1 Dashboard de Maestros: Distribución Cognitiva

// Archivo: apps/backend/src/modules/analytics/services/teacher-analytics.service.ts (extracto)

async getClassroomCognitiveDistribution(classroomId: string): Promise<any> {
    const result = await this.dataSource.query(`
        SELECT
            cp.bloom_level,
            COUNT(DISTINCT cp.user_id) AS students_count,
            ROUND(AVG(cp.success_rate), 2) AS avg_success_rate,
            COUNT(CASE WHEN cp.success_rate >= 80 THEN 1 END) AS students_excellent,
            COUNT(CASE WHEN cp.success_rate < 60 THEN 1 END) AS students_struggling
        FROM progress_tracking.cognitive_performance cp
        JOIN classroom_members cm ON cm.user_id = cp.user_id
        WHERE cm.classroom_id = $1
        AND cp.exercises_attempted >= 5
        GROUP BY cp.bloom_level
        ORDER BY
            CASE cp.bloom_level
                WHEN 'remember' THEN 1
                WHEN 'understand' THEN 2
                WHEN 'apply' THEN 3
                WHEN 'analyze' THEN 4
                WHEN 'evaluate' THEN 5
                WHEN 'create' THEN 6
            END
    `, [classroomId]);

    return result;
}

📚 Referencias Técnicas

Database

  • Schema: educational_content - Ejercicios con clasificación cognitiva
  • Schema: progress_tracking - Desempeño cognitivo
  • Función: update_cognitive_performance() - Actualización de métricas
  • Función: get_cognitive_profile() - Perfil cognitivo completo
  • Función: get_weak_cognitive_levels() - Identificación de niveles débiles

Backend

  • Service: apps/backend/src/modules/analytics/services/cognitive-analytics.service.ts
  • Controller: apps/backend/src/modules/analytics/controllers/cognitive-analytics.controller.ts
  • Enum: apps/backend/src/modules/educational-content/enums/bloom-level.enum.ts

Frontend

  • Hook: apps/frontend/src/hooks/useCognitiveProfile.ts
  • Component: apps/frontend/src/components/analytics/CognitiveProfileDashboard.tsx
  • Component: apps/frontend/src/components/exercises/BloomLevelFilter.tsx

Documentación Relacionada


Última revisión: 2025-11-07 Revisores: Equipo Backend, Frontend, Database, Pedagógico Próxima revisión: 2025-12-07