@patch-adams/core 1.5.2 → 1.5.4

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.
package/dist/cli.cjs CHANGED
@@ -4152,8 +4152,9 @@ function generateLrsBridgeCode(options) {
4152
4152
  });
4153
4153
  }
4154
4154
 
4155
- // Track submitted Knowledge Check blocks to avoid duplicates
4155
+ // Track submitted Knowledge Check questions to avoid duplicates
4156
4156
  var submittedKnowledgeChecks = {};
4157
+ var kcQuestionCounter = 0;
4157
4158
 
4158
4159
  /**
4159
4160
  * Set up interceptors specifically for Rise Knowledge Check blocks
@@ -4163,7 +4164,11 @@ function generateLrsBridgeCode(options) {
4163
4164
  if (!TRACK_QUIZZES) return;
4164
4165
 
4165
4166
  // Intercept submit button clicks on Knowledge Check blocks
4167
+ // NOTE: If the SCORM tracker is active, it handles quiz tracking via
4168
+ // cmi.interactions \u2014 skip DOM scraping to avoid duplicate statements
4166
4169
  document.addEventListener('click', function(e) {
4170
+ if (scormTrackerActive) return; // SCORM tracker handles this
4171
+
4167
4172
  var submitBtn = e.target.closest('.quiz-card__button');
4168
4173
  if (!submitBtn) return;
4169
4174
 
@@ -4185,16 +4190,26 @@ function generateLrsBridgeCode(options) {
4185
4190
  * Extract and send xAPI statement for a Knowledge Check submission
4186
4191
  */
4187
4192
  function extractKnowledgeCheckResult(kcBlock) {
4188
- // Get block ID for deduplication
4193
+ // Get block ID for context
4189
4194
  var blockContainer = kcBlock.closest('[data-block-id]');
4190
4195
  var blockId = blockContainer ? blockContainer.getAttribute('data-block-id') : null;
4191
4196
 
4197
+ // Get question text first \u2014 needed for per-question dedup key
4198
+ var questionText = '';
4199
+ var questionTextEl = kcBlock.querySelector('.quiz-card__title .fr-view, .quiz-card__title');
4200
+ if (questionTextEl) {
4201
+ questionText = questionTextEl.textContent.trim();
4202
+ }
4203
+
4192
4204
  // Get question ID from the title element
4193
4205
  var questionTitleEl = kcBlock.querySelector('.quiz-card__title');
4194
- var questionId = questionTitleEl ? questionTitleEl.id : (blockId ? 'q-' + blockId : 'q-' + generateUUID());
4206
+ var questionId = questionTitleEl ? questionTitleEl.id : null;
4207
+
4208
+ // Build a question-specific dedup key using question text hash
4209
+ // This ensures each question in a multi-question quiz block gets its own key
4210
+ var questionHash = questionText ? questionText.substring(0, 100) : (questionId || blockId || generateUUID());
4211
+ var submissionKey = 'kc-' + questionHash;
4195
4212
 
4196
- // Check if we already processed this submission (avoid duplicates)
4197
- var submissionKey = blockId || questionId;
4198
4213
  var feedbackLabel = kcBlock.querySelector('.quiz-card__feedback-label');
4199
4214
  if (!feedbackLabel) {
4200
4215
  log('Knowledge Check: No feedback visible yet');
@@ -4210,11 +4225,9 @@ function generateLrsBridgeCode(options) {
4210
4225
  }
4211
4226
  submittedKnowledgeChecks[submissionId] = true;
4212
4227
 
4213
- // Get question text
4214
- var questionText = '';
4215
- var questionTextEl = kcBlock.querySelector('.quiz-card__title .fr-view, .quiz-card__title');
4216
- if (questionTextEl) {
4217
- questionText = questionTextEl.textContent.trim();
4228
+ // Use question-specific ID for the statement (not the shared block ID)
4229
+ if (!questionId) {
4230
+ questionId = blockId ? 'q-' + blockId + '-' + questionHash.substring(0, 20) : 'q-' + generateUUID();
4218
4231
  }
4219
4232
 
4220
4233
  // Determine question type from aria-label
@@ -4243,11 +4256,14 @@ function generateLrsBridgeCode(options) {
4243
4256
  correct: isCorrect
4244
4257
  });
4245
4258
 
4259
+ // Increment question counter for this session
4260
+ kcQuestionCounter++;
4261
+
4246
4262
  // Send question answered statement using existing LRS method
4247
4263
  LRS.questionAnswered({
4248
4264
  questionId: questionId,
4249
4265
  questionGuid: blockId || generateUUID(),
4250
- questionNumber: 1,
4266
+ questionNumber: kcQuestionCounter,
4251
4267
  questionText: questionText.substring(0, 500),
4252
4268
  questionType: questionType,
4253
4269
  answer: answerText.substring(0, 500),
@@ -4308,6 +4324,142 @@ function generateLrsBridgeCode(options) {
4308
4324
  return answerText;
4309
4325
  }
4310
4326
 
4327
+ // ========================================================================
4328
+ // SCORM INTERACTION TRACKER
4329
+ // Intercepts cmi.interactions.N.* SetValue calls to capture quiz answers
4330
+ // directly from the SCORM data model \u2014 works regardless of Rise UI format
4331
+ // (Knowledge Check blocks, quiz lessons, etc.)
4332
+ // ========================================================================
4333
+ var scormInteractions = {}; // Pending interactions keyed by index N
4334
+ var scormInteractionsSent = {}; // Track which interactions were already sent
4335
+ var scormTrackerActive = false; // Set true when SCORM tracker wraps SetValue \u2014 KC handler defers
4336
+
4337
+ function setupScormInteractionTracker() {
4338
+ if (!TRACK_QUIZZES) return;
4339
+ if (!LRS.scormApi) {
4340
+ log('SCORM Interaction Tracker: No SCORM API found, skipping');
4341
+ return;
4342
+ }
4343
+
4344
+ var api = LRS.scormApi;
4345
+ var apiType = LRS.scormApiType;
4346
+
4347
+ // Determine which SetValue function to wrap
4348
+ var setValueFn = apiType === '2004' ? 'SetValue' : 'LMSSetValue';
4349
+ var originalSetValue = api[setValueFn];
4350
+
4351
+ if (typeof originalSetValue !== 'function') {
4352
+ log('SCORM Interaction Tracker: No ' + setValueFn + ' function found');
4353
+ return;
4354
+ }
4355
+
4356
+ // SCORM 1.2 uses cmi.interactions.N.*, SCORM 2004 uses cmi.interactions.N.*
4357
+ var interactionPattern = /^cmi\\.interactions\\.(\\d+)\\.(.+)$/;
4358
+
4359
+ api[setValueFn] = function(key, value) {
4360
+ // Always call the original first
4361
+ var result = originalSetValue.apply(api, arguments);
4362
+
4363
+ // Check if this is an interaction data element
4364
+ if (typeof key === 'string') {
4365
+ var match = key.match(interactionPattern);
4366
+ if (match) {
4367
+ var idx = match[1];
4368
+ var field = match[2];
4369
+
4370
+ // Initialize interaction tracking for this index
4371
+ if (!scormInteractions[idx]) {
4372
+ scormInteractions[idx] = {};
4373
+ }
4374
+
4375
+ // Store the field value
4376
+ scormInteractions[idx][field] = value;
4377
+
4378
+ log('SCORM Interaction [' + idx + '].' + field + ' = ' + (value ? value.substring(0, 80) : value));
4379
+
4380
+ // When 'result' is set, the interaction is complete \u2014 fire xAPI statement
4381
+ if (field === 'result' && !scormInteractionsSent[idx]) {
4382
+ scormInteractionsSent[idx] = true;
4383
+ var interaction = scormInteractions[idx];
4384
+
4385
+ // Map SCORM interaction type to readable type
4386
+ var typeMap = {
4387
+ 'choice': 'multiple-choice',
4388
+ 'true-false': 'true-false',
4389
+ 'fill-in': 'fill-in-blank',
4390
+ 'matching': 'matching',
4391
+ 'performance': 'performance',
4392
+ 'sequencing': 'sequencing',
4393
+ 'likert': 'likert',
4394
+ 'numeric': 'numeric'
4395
+ };
4396
+
4397
+ var scormType = interaction.type || 'unknown';
4398
+ var questionType = typeMap[scormType] || scormType;
4399
+
4400
+ // Determine correctness from SCORM result value
4401
+ // SCORM 1.2: 'correct', 'wrong', 'unanticipated', 'neutral'
4402
+ // SCORM 2004: 'correct', 'incorrect', 'unanticipated', 'neutral'
4403
+ var isCorrect = value === 'correct';
4404
+
4405
+ // Get student response \u2014 for 'choice' type, this might be indices like '1,3'
4406
+ var studentResponse = interaction.student_response || interaction['student_response'] || '';
4407
+
4408
+ // Get correct response pattern
4409
+ var correctResponse = '';
4410
+ // SCORM 1.2: correct_responses.0.pattern
4411
+ // Check stored fields for correct_responses
4412
+ Object.keys(interaction).forEach(function(k) {
4413
+ if (k.indexOf('correct_responses') > -1 && k.indexOf('pattern') > -1) {
4414
+ correctResponse = interaction[k];
4415
+ }
4416
+ });
4417
+
4418
+ // Get interaction ID (Rise sets this to a unique question identifier)
4419
+ var interactionId = interaction.id || ('interaction-' + idx);
4420
+
4421
+ // Get lesson context
4422
+ var lessonInfo = getCachedLessonInfo();
4423
+
4424
+ // Increment question counter
4425
+ kcQuestionCounter++;
4426
+
4427
+ log('SCORM Interaction complete [' + idx + ']:', {
4428
+ id: interactionId,
4429
+ type: questionType,
4430
+ response: studentResponse,
4431
+ correctResponse: correctResponse,
4432
+ result: value,
4433
+ correct: isCorrect,
4434
+ questionNumber: kcQuestionCounter
4435
+ });
4436
+
4437
+ // Send xAPI answered statement
4438
+ LRS.questionAnswered({
4439
+ questionId: interactionId,
4440
+ questionGuid: interactionId,
4441
+ questionNumber: kcQuestionCounter,
4442
+ questionText: 'Question ' + kcQuestionCounter + ' (' + interactionId + ')',
4443
+ questionType: questionType,
4444
+ answer: studentResponse,
4445
+ correctAnswer: correctResponse,
4446
+ correct: isCorrect,
4447
+ result: isCorrect ? 'correct' : 'incorrect',
4448
+ assessmentName: lessonInfo.name || 'Quiz',
4449
+ lessonName: lessonInfo.name,
4450
+ sectionName: lessonInfo.sectionName
4451
+ });
4452
+ }
4453
+ }
4454
+ }
4455
+
4456
+ return result;
4457
+ };
4458
+
4459
+ scormTrackerActive = true;
4460
+ log('SCORM Interaction Tracker: Wrapped ' + setValueFn + ' (apiType=' + apiType + ') \u2014 KC DOM handler will defer to SCORM tracker');
4461
+ }
4462
+
4311
4463
  function setupInteractionInterceptors() {
4312
4464
  if (!TRACK_INTERACTIONS) return;
4313
4465
 
@@ -4526,6 +4678,9 @@ function generateLrsBridgeCode(options) {
4526
4678
  LRS.scormApiFound = true;
4527
4679
  LRS.scormApiType = result.type;
4528
4680
 
4681
+ // Wrap SCORM SetValue to intercept cmi.interactions for quiz tracking
4682
+ setupScormInteractionTracker();
4683
+
4529
4684
  // Read learner_id for diagnostics
4530
4685
  var scormLearnerId = 'n/a', scormLearnerName = 'n/a';
4531
4686
  try {