@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.js CHANGED
@@ -4142,8 +4142,9 @@ function generateLrsBridgeCode(options) {
4142
4142
  });
4143
4143
  }
4144
4144
 
4145
- // Track submitted Knowledge Check blocks to avoid duplicates
4145
+ // Track submitted Knowledge Check questions to avoid duplicates
4146
4146
  var submittedKnowledgeChecks = {};
4147
+ var kcQuestionCounter = 0;
4147
4148
 
4148
4149
  /**
4149
4150
  * Set up interceptors specifically for Rise Knowledge Check blocks
@@ -4153,7 +4154,11 @@ function generateLrsBridgeCode(options) {
4153
4154
  if (!TRACK_QUIZZES) return;
4154
4155
 
4155
4156
  // Intercept submit button clicks on Knowledge Check blocks
4157
+ // NOTE: If the SCORM tracker is active, it handles quiz tracking via
4158
+ // cmi.interactions \u2014 skip DOM scraping to avoid duplicate statements
4156
4159
  document.addEventListener('click', function(e) {
4160
+ if (scormTrackerActive) return; // SCORM tracker handles this
4161
+
4157
4162
  var submitBtn = e.target.closest('.quiz-card__button');
4158
4163
  if (!submitBtn) return;
4159
4164
 
@@ -4175,16 +4180,26 @@ function generateLrsBridgeCode(options) {
4175
4180
  * Extract and send xAPI statement for a Knowledge Check submission
4176
4181
  */
4177
4182
  function extractKnowledgeCheckResult(kcBlock) {
4178
- // Get block ID for deduplication
4183
+ // Get block ID for context
4179
4184
  var blockContainer = kcBlock.closest('[data-block-id]');
4180
4185
  var blockId = blockContainer ? blockContainer.getAttribute('data-block-id') : null;
4181
4186
 
4187
+ // Get question text first \u2014 needed for per-question dedup key
4188
+ var questionText = '';
4189
+ var questionTextEl = kcBlock.querySelector('.quiz-card__title .fr-view, .quiz-card__title');
4190
+ if (questionTextEl) {
4191
+ questionText = questionTextEl.textContent.trim();
4192
+ }
4193
+
4182
4194
  // Get question ID from the title element
4183
4195
  var questionTitleEl = kcBlock.querySelector('.quiz-card__title');
4184
- var questionId = questionTitleEl ? questionTitleEl.id : (blockId ? 'q-' + blockId : 'q-' + generateUUID());
4196
+ var questionId = questionTitleEl ? questionTitleEl.id : null;
4197
+
4198
+ // Build a question-specific dedup key using question text hash
4199
+ // This ensures each question in a multi-question quiz block gets its own key
4200
+ var questionHash = questionText ? questionText.substring(0, 100) : (questionId || blockId || generateUUID());
4201
+ var submissionKey = 'kc-' + questionHash;
4185
4202
 
4186
- // Check if we already processed this submission (avoid duplicates)
4187
- var submissionKey = blockId || questionId;
4188
4203
  var feedbackLabel = kcBlock.querySelector('.quiz-card__feedback-label');
4189
4204
  if (!feedbackLabel) {
4190
4205
  log('Knowledge Check: No feedback visible yet');
@@ -4200,11 +4215,9 @@ function generateLrsBridgeCode(options) {
4200
4215
  }
4201
4216
  submittedKnowledgeChecks[submissionId] = true;
4202
4217
 
4203
- // Get question text
4204
- var questionText = '';
4205
- var questionTextEl = kcBlock.querySelector('.quiz-card__title .fr-view, .quiz-card__title');
4206
- if (questionTextEl) {
4207
- questionText = questionTextEl.textContent.trim();
4218
+ // Use question-specific ID for the statement (not the shared block ID)
4219
+ if (!questionId) {
4220
+ questionId = blockId ? 'q-' + blockId + '-' + questionHash.substring(0, 20) : 'q-' + generateUUID();
4208
4221
  }
4209
4222
 
4210
4223
  // Determine question type from aria-label
@@ -4233,11 +4246,14 @@ function generateLrsBridgeCode(options) {
4233
4246
  correct: isCorrect
4234
4247
  });
4235
4248
 
4249
+ // Increment question counter for this session
4250
+ kcQuestionCounter++;
4251
+
4236
4252
  // Send question answered statement using existing LRS method
4237
4253
  LRS.questionAnswered({
4238
4254
  questionId: questionId,
4239
4255
  questionGuid: blockId || generateUUID(),
4240
- questionNumber: 1,
4256
+ questionNumber: kcQuestionCounter,
4241
4257
  questionText: questionText.substring(0, 500),
4242
4258
  questionType: questionType,
4243
4259
  answer: answerText.substring(0, 500),
@@ -4298,6 +4314,142 @@ function generateLrsBridgeCode(options) {
4298
4314
  return answerText;
4299
4315
  }
4300
4316
 
4317
+ // ========================================================================
4318
+ // SCORM INTERACTION TRACKER
4319
+ // Intercepts cmi.interactions.N.* SetValue calls to capture quiz answers
4320
+ // directly from the SCORM data model \u2014 works regardless of Rise UI format
4321
+ // (Knowledge Check blocks, quiz lessons, etc.)
4322
+ // ========================================================================
4323
+ var scormInteractions = {}; // Pending interactions keyed by index N
4324
+ var scormInteractionsSent = {}; // Track which interactions were already sent
4325
+ var scormTrackerActive = false; // Set true when SCORM tracker wraps SetValue \u2014 KC handler defers
4326
+
4327
+ function setupScormInteractionTracker() {
4328
+ if (!TRACK_QUIZZES) return;
4329
+ if (!LRS.scormApi) {
4330
+ log('SCORM Interaction Tracker: No SCORM API found, skipping');
4331
+ return;
4332
+ }
4333
+
4334
+ var api = LRS.scormApi;
4335
+ var apiType = LRS.scormApiType;
4336
+
4337
+ // Determine which SetValue function to wrap
4338
+ var setValueFn = apiType === '2004' ? 'SetValue' : 'LMSSetValue';
4339
+ var originalSetValue = api[setValueFn];
4340
+
4341
+ if (typeof originalSetValue !== 'function') {
4342
+ log('SCORM Interaction Tracker: No ' + setValueFn + ' function found');
4343
+ return;
4344
+ }
4345
+
4346
+ // SCORM 1.2 uses cmi.interactions.N.*, SCORM 2004 uses cmi.interactions.N.*
4347
+ var interactionPattern = /^cmi\\.interactions\\.(\\d+)\\.(.+)$/;
4348
+
4349
+ api[setValueFn] = function(key, value) {
4350
+ // Always call the original first
4351
+ var result = originalSetValue.apply(api, arguments);
4352
+
4353
+ // Check if this is an interaction data element
4354
+ if (typeof key === 'string') {
4355
+ var match = key.match(interactionPattern);
4356
+ if (match) {
4357
+ var idx = match[1];
4358
+ var field = match[2];
4359
+
4360
+ // Initialize interaction tracking for this index
4361
+ if (!scormInteractions[idx]) {
4362
+ scormInteractions[idx] = {};
4363
+ }
4364
+
4365
+ // Store the field value
4366
+ scormInteractions[idx][field] = value;
4367
+
4368
+ log('SCORM Interaction [' + idx + '].' + field + ' = ' + (value ? value.substring(0, 80) : value));
4369
+
4370
+ // When 'result' is set, the interaction is complete \u2014 fire xAPI statement
4371
+ if (field === 'result' && !scormInteractionsSent[idx]) {
4372
+ scormInteractionsSent[idx] = true;
4373
+ var interaction = scormInteractions[idx];
4374
+
4375
+ // Map SCORM interaction type to readable type
4376
+ var typeMap = {
4377
+ 'choice': 'multiple-choice',
4378
+ 'true-false': 'true-false',
4379
+ 'fill-in': 'fill-in-blank',
4380
+ 'matching': 'matching',
4381
+ 'performance': 'performance',
4382
+ 'sequencing': 'sequencing',
4383
+ 'likert': 'likert',
4384
+ 'numeric': 'numeric'
4385
+ };
4386
+
4387
+ var scormType = interaction.type || 'unknown';
4388
+ var questionType = typeMap[scormType] || scormType;
4389
+
4390
+ // Determine correctness from SCORM result value
4391
+ // SCORM 1.2: 'correct', 'wrong', 'unanticipated', 'neutral'
4392
+ // SCORM 2004: 'correct', 'incorrect', 'unanticipated', 'neutral'
4393
+ var isCorrect = value === 'correct';
4394
+
4395
+ // Get student response \u2014 for 'choice' type, this might be indices like '1,3'
4396
+ var studentResponse = interaction.student_response || interaction['student_response'] || '';
4397
+
4398
+ // Get correct response pattern
4399
+ var correctResponse = '';
4400
+ // SCORM 1.2: correct_responses.0.pattern
4401
+ // Check stored fields for correct_responses
4402
+ Object.keys(interaction).forEach(function(k) {
4403
+ if (k.indexOf('correct_responses') > -1 && k.indexOf('pattern') > -1) {
4404
+ correctResponse = interaction[k];
4405
+ }
4406
+ });
4407
+
4408
+ // Get interaction ID (Rise sets this to a unique question identifier)
4409
+ var interactionId = interaction.id || ('interaction-' + idx);
4410
+
4411
+ // Get lesson context
4412
+ var lessonInfo = getCachedLessonInfo();
4413
+
4414
+ // Increment question counter
4415
+ kcQuestionCounter++;
4416
+
4417
+ log('SCORM Interaction complete [' + idx + ']:', {
4418
+ id: interactionId,
4419
+ type: questionType,
4420
+ response: studentResponse,
4421
+ correctResponse: correctResponse,
4422
+ result: value,
4423
+ correct: isCorrect,
4424
+ questionNumber: kcQuestionCounter
4425
+ });
4426
+
4427
+ // Send xAPI answered statement
4428
+ LRS.questionAnswered({
4429
+ questionId: interactionId,
4430
+ questionGuid: interactionId,
4431
+ questionNumber: kcQuestionCounter,
4432
+ questionText: 'Question ' + kcQuestionCounter + ' (' + interactionId + ')',
4433
+ questionType: questionType,
4434
+ answer: studentResponse,
4435
+ correctAnswer: correctResponse,
4436
+ correct: isCorrect,
4437
+ result: isCorrect ? 'correct' : 'incorrect',
4438
+ assessmentName: lessonInfo.name || 'Quiz',
4439
+ lessonName: lessonInfo.name,
4440
+ sectionName: lessonInfo.sectionName
4441
+ });
4442
+ }
4443
+ }
4444
+ }
4445
+
4446
+ return result;
4447
+ };
4448
+
4449
+ scormTrackerActive = true;
4450
+ log('SCORM Interaction Tracker: Wrapped ' + setValueFn + ' (apiType=' + apiType + ') \u2014 KC DOM handler will defer to SCORM tracker');
4451
+ }
4452
+
4301
4453
  function setupInteractionInterceptors() {
4302
4454
  if (!TRACK_INTERACTIONS) return;
4303
4455
 
@@ -4516,6 +4668,9 @@ function generateLrsBridgeCode(options) {
4516
4668
  LRS.scormApiFound = true;
4517
4669
  LRS.scormApiType = result.type;
4518
4670
 
4671
+ // Wrap SCORM SetValue to intercept cmi.interactions for quiz tracking
4672
+ setupScormInteractionTracker();
4673
+
4519
4674
  // Read learner_id for diagnostics
4520
4675
  var scormLearnerId = 'n/a', scormLearnerName = 'n/a';
4521
4676
  try {