@telebort/question-banks 2.2.0 → 2.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -19,22 +19,52 @@ var OptionSchema = zod.z.object({
19
19
  text: zod.z.string().min(1),
20
20
  isCorrect: zod.z.boolean(),
21
21
  // v1.1 fields - optional for backward compatibility
22
- misconceptionId: zod.z.string().optional(),
22
+ misconceptionId: zod.z.string().nullish(),
23
23
  feedback: FeedbackSchema.optional()
24
24
  });
25
25
  var QuestionTypeSchema = zod.z.enum([
26
+ // Core assessment types (original v1.0)
26
27
  "vocabulary",
27
28
  "code_understanding",
28
29
  "problem_solving",
29
30
  "application",
30
- "reflection"
31
+ "reflection",
32
+ // Code-focused types
33
+ "debugging",
34
+ "trace",
35
+ "predict",
36
+ // Higher-order thinking
37
+ "analyze",
38
+ "evaluation",
39
+ "synthesis",
40
+ "design",
41
+ "conceptual",
42
+ "explain",
43
+ // Domain-specific
44
+ "bebras",
45
+ "blockmodel",
46
+ "ethical_reasoning"
31
47
  ]);
32
48
  var QuestionArchetypeSchema = zod.z.enum([
49
+ // Original archetypes (v1.1)
33
50
  "vocabulary",
34
51
  "trace",
35
52
  "bebras",
36
53
  "blockmodel",
37
- "parsons"
54
+ "parsons",
55
+ // Assessment patterns
56
+ "application",
57
+ "problem_solving",
58
+ "debugging",
59
+ "predict",
60
+ "explain",
61
+ "evaluation",
62
+ "reflection",
63
+ "conceptual",
64
+ "analysis",
65
+ "design",
66
+ "synthesis",
67
+ "code"
38
68
  ]);
39
69
  var DifficultySchema = zod.z.enum(["easy", "medium", "hard", "challenge"]);
40
70
  var BloomsTaxonomySchema = zod.z.enum([
@@ -51,6 +81,10 @@ var QuestionMetadataSchema = zod.z.object({
51
81
  // seconds
52
82
  bloomsTaxonomy: BloomsTaxonomySchema,
53
83
  tags: zod.z.array(zod.z.string()),
84
+ conceptTags: zod.z.array(zod.z.string()).optional(),
85
+ // mastery-relevant concept tags
86
+ metaTags: zod.z.array(zod.z.string()).optional(),
87
+ // Bloom's types, course codes, internal
54
88
  source: zod.z.string().default("exit-ticket"),
55
89
  version: zod.z.string().default("1.1"),
56
90
  createdDate: zod.z.string().optional(),
@@ -58,8 +92,8 @@ var QuestionMetadataSchema = zod.z.object({
58
92
  });
59
93
  var QuestionSchema = zod.z.object({
60
94
  // Identifiers
61
- questionId: zod.z.string().regex(/^[a-z0-9-]+-l\d+-q\d+$/),
62
- globalId: zod.z.string().regex(/^exit-ticket-\d{4}$/),
95
+ questionId: zod.z.string().regex(/^[a-z0-9][-a-z0-9]*-(?:l\d+-)?q\d+$/),
96
+ globalId: zod.z.string().regex(/^exit-ticket-[a-z0-9][-a-z0-9]*$/),
63
97
  questionNumber: zod.z.number().int().positive().max(5),
64
98
  // Classification
65
99
  questionType: QuestionTypeSchema,
@@ -93,58 +127,17 @@ QuestionSchema.omit({
93
127
  })
94
128
  ).length(4)
95
129
  });
96
- var CourseDomainSchema = zod.z.enum([
97
- // AI & Data Science
98
- "ai_data_science",
99
- "ai_ml_cv",
100
- "ai_generative",
101
- "ai_advanced",
102
- // Web Development
103
- "web_development",
104
- // Mobile Development
105
- "mobile_development",
106
- "mobile",
107
- // Block-based Programming
108
- "block_based",
109
- "python_programming",
110
- "design",
111
- // Foundation
112
- "foundation",
113
- "creative_computing"
114
- ]);
115
- var CourseTierSchema = zod.z.enum([
116
- "foundation",
117
- "intermediate",
118
- "advanced"
119
- ]);
120
- var LessonSchema = zod.z.object({
121
- lessonId: zod.z.string(),
122
- // e.g., "ai-1-lesson-1"
123
- lessonNumber: zod.z.number().int().positive(),
124
- lessonTitle: zod.z.string(),
125
- lessonSlug: zod.z.string().optional(),
126
- totalQuestions: zod.z.number().int().positive().default(5),
127
- questions: zod.z.array(QuestionSchema)
128
- });
129
- var CourseSchema = zod.z.object({
130
- courseId: zod.z.string(),
131
- // e.g., "ai-1"
132
- courseName: zod.z.string(),
133
- // e.g., "AI-1 Data Analysis and Data Science"
134
- courseCode: zod.z.string(),
135
- // e.g., "AI1"
136
- domain: CourseDomainSchema,
137
- tier: CourseTierSchema,
138
- difficulty: zod.z.number().int().min(1).max(5),
139
- totalLessons: zod.z.number().int().positive(),
140
- totalQuestions: zod.z.number().int().positive(),
141
- sourceFile: zod.z.string().optional(),
142
- lessons: zod.z.array(LessonSchema)
143
- });
144
- CourseSchema.omit({ lessons: true }).extend({
145
- lessons: zod.z.array(
146
- LessonSchema.omit({ questions: true })
147
- ).optional()
130
+ var AssessmentBlueprintSchema = zod.z.object({
131
+ assessmentId: zod.z.string(),
132
+ // e.g., "cs1-quiz-1"
133
+ title: zod.z.string(),
134
+ // e.g., "Quiz 1"
135
+ description: zod.z.string(),
136
+ lessonRange: zod.z.tuple([zod.z.number().int().positive(), zod.z.number().int().positive()]).refine(([start2, end2]) => start2 <= end2, { message: "lessonRange start must be <= end" }),
137
+ questionCount: zod.z.number().int().positive(),
138
+ passingScore: zod.z.number().min(0).max(100),
139
+ timeLimitMinutes: zod.z.number().int().positive().nullable(),
140
+ tags: zod.z.array(zod.z.string()).optional()
148
141
  });
149
142
  var UserResponseSchema = zod.z.object({
150
143
  questionId: zod.z.string(),
@@ -185,7 +178,7 @@ var MisconceptionReportSchema = zod.z.object({
185
178
  description: zod.z.string().optional()
186
179
  });
187
180
  zod.z.object({
188
- assessmentId: zod.z.string().uuid(),
181
+ assessmentId: zod.z.string(),
189
182
  userId: zod.z.string().optional(),
190
183
  courseId: zod.z.string(),
191
184
  lessonId: zod.z.string().optional(),
@@ -221,6 +214,74 @@ zod.z.object({
221
214
  questionCount: zod.z.number().int(),
222
215
  answeredCount: zod.z.number().int()
223
216
  });
217
+
218
+ // src/schemas/course.ts
219
+ var CourseDomainSchema = zod.z.enum([
220
+ // AI & Data Science
221
+ "ai_data_science",
222
+ "ai_ml_cv",
223
+ "ai_generative",
224
+ "ai_advanced",
225
+ "ai",
226
+ // Prompt Engineering (general AI)
227
+ // Web Development
228
+ "web_development",
229
+ // Mobile Development
230
+ "mobile_development",
231
+ "mobile",
232
+ // Block-based Programming
233
+ "block_based",
234
+ "python_programming",
235
+ "design",
236
+ "artificial_intelligence",
237
+ // Block-based AI courses (BBAI)
238
+ // Computer Science
239
+ "cs_foundations",
240
+ // CS1-CS4 courses
241
+ // Foundation
242
+ "foundation",
243
+ "creative_computing",
244
+ // Utility / Tools
245
+ "tools"
246
+ // IDE Setup, Git, Vibe Coding
247
+ ]);
248
+ var CourseTierSchema = zod.z.enum([
249
+ "beginner",
250
+ // Block-based courses (BBW, BBP, BBD, BBAI)
251
+ "foundation",
252
+ "intermediate",
253
+ "advanced"
254
+ ]);
255
+ var LessonSchema = zod.z.object({
256
+ lessonId: zod.z.string(),
257
+ // e.g., "ai-1-lesson-1"
258
+ lessonNumber: zod.z.number().int().positive(),
259
+ lessonTitle: zod.z.string(),
260
+ lessonSlug: zod.z.string().optional(),
261
+ totalQuestions: zod.z.number().int().positive().default(5),
262
+ questions: zod.z.array(QuestionSchema)
263
+ });
264
+ var CourseSchema = zod.z.object({
265
+ courseId: zod.z.string(),
266
+ // e.g., "ai-1"
267
+ courseName: zod.z.string(),
268
+ // e.g., "AI-1 Data Analysis and Data Science"
269
+ courseCode: zod.z.string(),
270
+ // e.g., "AI1"
271
+ domain: CourseDomainSchema,
272
+ tier: CourseTierSchema,
273
+ difficulty: zod.z.number().int().min(1).max(5),
274
+ totalLessons: zod.z.number().int().positive(),
275
+ totalQuestions: zod.z.number().int().positive(),
276
+ sourceFile: zod.z.string().optional(),
277
+ lessons: zod.z.array(LessonSchema),
278
+ assessments: zod.z.array(AssessmentBlueprintSchema).optional()
279
+ });
280
+ CourseSchema.omit({ lessons: true }).extend({
281
+ lessons: zod.z.array(
282
+ LessonSchema.omit({ questions: true })
283
+ ).optional()
284
+ });
224
285
  var CertificationProjectSchema = zod.z.object({
225
286
  projectNumber: zod.z.number().int().positive(),
226
287
  projectName: zod.z.string(),
@@ -508,6 +569,138 @@ ${green(bold("Valid!"))} File passed schema validation.
508
569
  }
509
570
  }
510
571
 
572
+ // src/core/assessment.ts
573
+ var SeededRandom = class {
574
+ constructor(seed) {
575
+ this.state = seed;
576
+ }
577
+ next() {
578
+ this.state = (this.state * 1103515245 + 12345) % 2147483648;
579
+ return this.state / 2147483648;
580
+ }
581
+ /** Fisher-Yates shuffle */
582
+ shuffle(array) {
583
+ const result = [...array];
584
+ for (let i3 = result.length - 1; i3 > 0; i3--) {
585
+ const j2 = Math.floor(this.next() * (i3 + 1));
586
+ const temp = result[i3];
587
+ result[i3] = result[j2];
588
+ result[j2] = temp;
589
+ }
590
+ return result;
591
+ }
592
+ };
593
+ function hashString(str) {
594
+ let hash = 0;
595
+ for (let i3 = 0; i3 < str.length; i3++) {
596
+ const char = str.charCodeAt(i3);
597
+ hash = (hash << 5) - hash + char;
598
+ hash = hash & hash;
599
+ }
600
+ return Math.abs(hash);
601
+ }
602
+ function createAssessmentModule(getCourse) {
603
+ return {
604
+ async getAssessments(courseId) {
605
+ const course = await getCourse(courseId);
606
+ return course?.assessments ?? [];
607
+ },
608
+ async buildAssessmentQuiz(courseId, assessmentId, seed) {
609
+ const course = await getCourse(courseId);
610
+ if (!course) throw new Error(`Course not found: ${courseId}`);
611
+ const blueprint = course.assessments?.find((a2) => a2.assessmentId === assessmentId);
612
+ if (!blueprint) throw new Error(`Assessment not found: ${assessmentId}`);
613
+ const [rangeStart, rangeEnd] = blueprint.lessonRange;
614
+ const pool = [];
615
+ for (const lesson of course.lessons) {
616
+ if (lesson.lessonNumber >= rangeStart && lesson.lessonNumber <= rangeEnd) {
617
+ pool.push(...lesson.questions);
618
+ }
619
+ }
620
+ if (pool.length === 0) {
621
+ throw new Error(`No questions found for lessons ${rangeStart}-${rangeEnd} in ${courseId}`);
622
+ }
623
+ const seedValue = seed ? hashString(seed) : Date.now();
624
+ const rng = new SeededRandom(seedValue);
625
+ const shuffled = rng.shuffle(pool);
626
+ return shuffled.slice(0, Math.min(blueprint.questionCount, shuffled.length));
627
+ },
628
+ async gradeAssessment(courseId, assessmentId, responses, passingScore) {
629
+ const course = await getCourse(courseId);
630
+ if (!course) throw new Error(`Course not found: ${courseId}`);
631
+ const blueprint = course.assessments?.find((a2) => a2.assessmentId === assessmentId);
632
+ if (!blueprint) throw new Error(`Assessment not found: ${assessmentId}`);
633
+ const threshold = passingScore ?? blueprint.passingScore;
634
+ const questionMap = /* @__PURE__ */ new Map();
635
+ for (const lesson of course.lessons) {
636
+ for (const q of lesson.questions) {
637
+ questionMap.set(q.questionId, q);
638
+ }
639
+ }
640
+ const gradedResponses = [];
641
+ let correctCount = 0;
642
+ for (const response of responses) {
643
+ const question = questionMap.get(response.questionId);
644
+ if (!question) continue;
645
+ const selectedOption = question.options.find((o2) => o2.key === response.selectedAnswer);
646
+ const isCorrect = selectedOption?.isCorrect ?? false;
647
+ if (isCorrect) correctCount++;
648
+ gradedResponses.push({
649
+ questionId: response.questionId,
650
+ selectedAnswer: response.selectedAnswer,
651
+ correctAnswer: question.correctAnswer,
652
+ isCorrect,
653
+ misconceptionId: isCorrect ? null : selectedOption?.misconceptionId ?? null,
654
+ feedback: selectedOption?.feedback,
655
+ timeSpent: response.timeSpent
656
+ });
657
+ }
658
+ const totalQuestions = gradedResponses.length;
659
+ const score = totalQuestions > 0 ? Math.round(correctCount / totalQuestions * 100) : 0;
660
+ const misconceptionCounts = /* @__PURE__ */ new Map();
661
+ for (const gr of gradedResponses) {
662
+ if (gr.misconceptionId) {
663
+ const existing = misconceptionCounts.get(gr.misconceptionId);
664
+ if (existing) {
665
+ existing.count++;
666
+ existing.questionIds.push(gr.questionId);
667
+ } else {
668
+ misconceptionCounts.set(gr.misconceptionId, {
669
+ count: 1,
670
+ questionIds: [gr.questionId]
671
+ });
672
+ }
673
+ }
674
+ }
675
+ const misconceptions = Array.from(misconceptionCounts.entries()).map(([id, data]) => ({
676
+ misconceptionId: id,
677
+ count: data.count,
678
+ percentage: Math.round(data.count / totalQuestions * 100),
679
+ questionIds: data.questionIds
680
+ })).sort((a2, b) => b.count - a2.count);
681
+ if (totalQuestions === 0) {
682
+ throw new Error(`No valid questions matched for assessment ${assessmentId}. All ${responses.length} response(s) referenced unknown questionIds.`);
683
+ }
684
+ const now = (/* @__PURE__ */ new Date()).toISOString();
685
+ const generatedId = `grade-${courseId}-${assessmentId}-${hashString(now).toString(36)}`;
686
+ return {
687
+ assessmentId: generatedId,
688
+ courseId,
689
+ totalQuestions,
690
+ correctCount,
691
+ incorrectCount: totalQuestions - correctCount,
692
+ score,
693
+ passed: score >= threshold,
694
+ responses: gradedResponses,
695
+ misconceptions,
696
+ topMisconceptions: misconceptions.slice(0, 3).map((m3) => m3.misconceptionId),
697
+ submittedAt: now,
698
+ gradedAt: now
699
+ };
700
+ }
701
+ };
702
+ }
703
+
511
704
  // src/knowledge/index.ts
512
705
  function createLogger(config) {
513
706
  return {
@@ -23249,6 +23442,7 @@ function createExitTicketsSDK(config) {
23249
23442
  };
23250
23443
  const malaysiaMath = createMalaysiaMathModule(malaysiaMathConfig);
23251
23444
  const malaysiaMathTaxonomy = createMalaysiaMathTaxonomyModule(malaysiaMathConfig);
23445
+ const assessment = createAssessmentModule(data.getCourse);
23252
23446
  return {
23253
23447
  data,
23254
23448
  query,
@@ -23259,7 +23453,8 @@ function createExitTicketsSDK(config) {
23259
23453
  knowledge,
23260
23454
  taxonomy,
23261
23455
  malaysiaMath,
23262
- malaysiaMathTaxonomy
23456
+ malaysiaMathTaxonomy,
23457
+ assessment
23263
23458
  };
23264
23459
  }
23265
23460
 
@@ -23428,26 +23623,32 @@ async function loadCourseData(filePath) {
23428
23623
  }
23429
23624
  function extractQuestions(courseData) {
23430
23625
  const questions = [];
23626
+ const pushQuestion = (q) => {
23627
+ questions.push({
23628
+ questionId: q.questionId || q.globalId || `${courseData.courseId || "unknown"}-unknown`,
23629
+ prompt: q.prompt || "",
23630
+ options: (q.options || []).map((o2) => ({
23631
+ key: o2.key || "",
23632
+ text: o2.text || "",
23633
+ isCorrect: o2.isCorrect || false
23634
+ })),
23635
+ hasCodeBlock: q.hasCodeBlock || false,
23636
+ codeLanguage: q.codeLanguage || null,
23637
+ codeContent: q.codeContent || null
23638
+ });
23639
+ };
23431
23640
  if (courseData.lessons) {
23432
23641
  for (const lesson of courseData.lessons) {
23433
23642
  if (lesson.questions) {
23434
23643
  for (const q of lesson.questions) {
23435
- questions.push({
23436
- questionId: q.questionId || `${courseData.courseId}-unknown`,
23437
- prompt: q.prompt || "",
23438
- options: (q.options || []).map((o2) => ({
23439
- key: o2.key || "",
23440
- text: o2.text || "",
23441
- isCorrect: o2.isCorrect || false
23442
- })),
23443
- // CS-specific fields for enhanced analysis
23444
- hasCodeBlock: q.hasCodeBlock || false,
23445
- codeLanguage: q.codeLanguage || null,
23446
- codeContent: q.codeContent || null
23447
- });
23644
+ pushQuestion(q);
23448
23645
  }
23449
23646
  }
23450
23647
  }
23648
+ } else if (courseData.questions && Array.isArray(courseData.questions)) {
23649
+ for (const q of courseData.questions) {
23650
+ pushQuestion(q);
23651
+ }
23451
23652
  }
23452
23653
  return questions;
23453
23654
  }