@patch-adams/core 1.5.3 → 1.5.5

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
@@ -4164,7 +4164,11 @@ function generateLrsBridgeCode(options) {
4164
4164
  if (!TRACK_QUIZZES) return;
4165
4165
 
4166
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
4167
4169
  document.addEventListener('click', function(e) {
4170
+ if (scormTrackerActive) return; // SCORM tracker handles this
4171
+
4168
4172
  var submitBtn = e.target.closest('.quiz-card__button');
4169
4173
  if (!submitBtn) return;
4170
4174
 
@@ -4320,6 +4324,219 @@ function generateLrsBridgeCode(options) {
4320
4324
  return answerText;
4321
4325
  }
4322
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
+ // IMPORTANT: This finds and wraps the ACTUAL SCORM API that Rise uses
4334
+ // (window.API, parent.API, or global functions), NOT the bridge's copy.
4335
+ // It runs unconditionally at init, not gated by actor resolution.
4336
+ // ========================================================================
4337
+ var scormInteractions = {}; // Pending interactions keyed by index N
4338
+ var scormInteractionsSent = {}; // Track which interactions were already sent
4339
+ var scormTrackerActive = false; // Set true when SCORM tracker wraps SetValue \u2014 KC handler defers
4340
+
4341
+ function interceptScormSetValue(key, value) {
4342
+ if (typeof key !== 'string') return;
4343
+ var interactionPattern = /^cmi\\.interactions\\.(\\d+)\\.(.+)$/;
4344
+ var match = key.match(interactionPattern);
4345
+ if (!match) return;
4346
+
4347
+ var idx = match[1];
4348
+ var field = match[2];
4349
+
4350
+ // Initialize interaction tracking for this index
4351
+ if (!scormInteractions[idx]) {
4352
+ scormInteractions[idx] = {};
4353
+ }
4354
+
4355
+ // Store the field value
4356
+ scormInteractions[idx][field] = String(value);
4357
+
4358
+ log('SCORM Interaction [' + idx + '].' + field + ' = ' + String(value).substring(0, 100));
4359
+
4360
+ // When 'result' is set, the interaction is complete \u2014 fire xAPI statement
4361
+ if (field === 'result' && !scormInteractionsSent[idx]) {
4362
+ scormInteractionsSent[idx] = true;
4363
+ var interaction = scormInteractions[idx];
4364
+
4365
+ // Map SCORM interaction type to readable type
4366
+ var typeMap = {
4367
+ 'choice': 'multiple-choice',
4368
+ 'true-false': 'true-false',
4369
+ 'fill-in': 'fill-in-blank',
4370
+ 'matching': 'matching',
4371
+ 'performance': 'performance',
4372
+ 'sequencing': 'sequencing',
4373
+ 'likert': 'likert',
4374
+ 'numeric': 'numeric'
4375
+ };
4376
+
4377
+ var scormType = interaction.type || 'unknown';
4378
+ var questionType = typeMap[scormType] || scormType;
4379
+
4380
+ // Determine correctness from SCORM result value
4381
+ // SCORM 1.2: 'correct', 'wrong', 'unanticipated', 'neutral'
4382
+ // SCORM 2004: 'correct', 'incorrect', 'unanticipated', 'neutral'
4383
+ var isCorrect = String(value) === 'correct';
4384
+
4385
+ // Get student response
4386
+ var studentResponse = interaction.student_response || '';
4387
+
4388
+ // Get correct response pattern
4389
+ var correctResponse = '';
4390
+ Object.keys(interaction).forEach(function(k) {
4391
+ if (k.indexOf('correct_responses') > -1 && k.indexOf('pattern') > -1) {
4392
+ correctResponse = interaction[k];
4393
+ }
4394
+ });
4395
+
4396
+ // Rise interaction IDs encode the question text (underscored)
4397
+ // e.g. "Development_Week_Qui_I_want_a_clear_picture_of_..."
4398
+ var interactionId = interaction.id || ('interaction-' + idx);
4399
+
4400
+ // Try to make a readable question text from the interaction ID
4401
+ var questionText = interactionId
4402
+ .replace(/_\\d+$/, '') // remove trailing _0
4403
+ .replace(/_/g, ' ') // underscores to spaces
4404
+ .substring(0, 200);
4405
+
4406
+ // Get lesson context
4407
+ var lessonInfo = getCachedLessonInfo();
4408
+
4409
+ // Increment question counter
4410
+ kcQuestionCounter++;
4411
+
4412
+ log('SCORM Interaction complete [' + idx + ']:', {
4413
+ questionNumber: kcQuestionCounter,
4414
+ questionText: questionText.substring(0, 60) + '...',
4415
+ type: questionType,
4416
+ studentResponse: studentResponse,
4417
+ correctResponse: correctResponse,
4418
+ result: String(value),
4419
+ correct: isCorrect
4420
+ });
4421
+
4422
+ // Send xAPI answered statement
4423
+ LRS.questionAnswered({
4424
+ questionId: interactionId,
4425
+ questionGuid: interactionId,
4426
+ questionNumber: kcQuestionCounter,
4427
+ questionText: questionText,
4428
+ questionType: questionType,
4429
+ answer: studentResponse,
4430
+ correctAnswer: correctResponse,
4431
+ correct: isCorrect,
4432
+ result: isCorrect ? 'correct' : 'incorrect',
4433
+ assessmentName: lessonInfo.name || 'Quiz',
4434
+ lessonName: lessonInfo.name,
4435
+ sectionName: lessonInfo.sectionName
4436
+ });
4437
+ }
4438
+ }
4439
+
4440
+ function setupScormInteractionTracker() {
4441
+ if (!TRACK_QUIZZES) return;
4442
+
4443
+ // Find the ACTUAL SCORM API that Rise uses \u2014 NOT our bridge's copy.
4444
+ // Rise discovers the API via standard SCORM lookup (window.API, parent chain).
4445
+ // The Bravais CDS player's ProxyApi.injectLmsApi() sets this up.
4446
+ var wrapped = false;
4447
+
4448
+ // Try 1: window.API (SCORM 1.2) or window.API_1484_11 (SCORM 2004)
4449
+ try {
4450
+ if (window.API && typeof window.API.LMSSetValue === 'function') {
4451
+ var origSetValue = window.API.LMSSetValue;
4452
+ window.API.LMSSetValue = function(key, value) {
4453
+ var result = origSetValue.apply(window.API, arguments);
4454
+ interceptScormSetValue(key, value);
4455
+ return result;
4456
+ };
4457
+ wrapped = true;
4458
+ log('SCORM Interaction Tracker: Wrapped window.API.LMSSetValue');
4459
+ }
4460
+ } catch (e) { log('SCORM Tracker: Cannot access window.API:', e.message); }
4461
+
4462
+ if (!wrapped) {
4463
+ try {
4464
+ if (window.API_1484_11 && typeof window.API_1484_11.SetValue === 'function') {
4465
+ var origSetValue2004 = window.API_1484_11.SetValue;
4466
+ window.API_1484_11.SetValue = function(key, value) {
4467
+ var result = origSetValue2004.apply(window.API_1484_11, arguments);
4468
+ interceptScormSetValue(key, value);
4469
+ return result;
4470
+ };
4471
+ wrapped = true;
4472
+ log('SCORM Interaction Tracker: Wrapped window.API_1484_11.SetValue');
4473
+ }
4474
+ } catch (e) { log('SCORM Tracker: Cannot access window.API_1484_11:', e.message); }
4475
+ }
4476
+
4477
+ // Try 2: Parent frame API
4478
+ if (!wrapped) {
4479
+ try {
4480
+ if (window.parent && window.parent !== window) {
4481
+ if (window.parent.API && typeof window.parent.API.LMSSetValue === 'function') {
4482
+ var origParentSetValue = window.parent.API.LMSSetValue;
4483
+ window.parent.API.LMSSetValue = function(key, value) {
4484
+ var result = origParentSetValue.apply(window.parent.API, arguments);
4485
+ interceptScormSetValue(key, value);
4486
+ return result;
4487
+ };
4488
+ wrapped = true;
4489
+ log('SCORM Interaction Tracker: Wrapped window.parent.API.LMSSetValue');
4490
+ }
4491
+ }
4492
+ } catch (e) { log('SCORM Tracker: Cannot access parent API (cross-origin)'); }
4493
+ }
4494
+
4495
+ // Try 3: Global LMS functions (Bravais/Xyleme mock API)
4496
+ if (!wrapped) {
4497
+ try {
4498
+ if (typeof window.LMSSetValue === 'function') {
4499
+ var origGlobalSetValue = window.LMSSetValue;
4500
+ window.LMSSetValue = function(key, value) {
4501
+ var result = origGlobalSetValue.apply(window, arguments);
4502
+ interceptScormSetValue(key, value);
4503
+ return result;
4504
+ };
4505
+ wrapped = true;
4506
+ log('SCORM Interaction Tracker: Wrapped window.LMSSetValue (global)');
4507
+ }
4508
+ } catch (e) { log('SCORM Tracker: Cannot wrap global LMSSetValue:', e.message); }
4509
+ }
4510
+
4511
+ // Try 4: Fall back to bridge's copy (least likely to work but worth trying)
4512
+ if (!wrapped && LRS.scormApi) {
4513
+ var setValueFn = LRS.scormApiType === '2004' ? 'SetValue' : 'LMSSetValue';
4514
+ if (typeof LRS.scormApi[setValueFn] === 'function') {
4515
+ var origBridgeSetValue = LRS.scormApi[setValueFn];
4516
+ LRS.scormApi[setValueFn] = function(key, value) {
4517
+ var result = origBridgeSetValue.apply(LRS.scormApi, arguments);
4518
+ interceptScormSetValue(key, value);
4519
+ return result;
4520
+ };
4521
+ wrapped = true;
4522
+ log('SCORM Interaction Tracker: Wrapped LRS.scormApi.' + setValueFn + ' (bridge copy)');
4523
+ }
4524
+ }
4525
+
4526
+ if (wrapped) {
4527
+ scormTrackerActive = true;
4528
+ log('SCORM Interaction Tracker active \u2014 KC DOM handler will defer');
4529
+ } else {
4530
+ // API not available yet \u2014 retry in 2 seconds (Bravais proxy may not be ready)
4531
+ log('SCORM Interaction Tracker: No SCORM API found yet, retrying in 2s...');
4532
+ setTimeout(function() {
4533
+ if (!scormTrackerActive) {
4534
+ setupScormInteractionTracker();
4535
+ }
4536
+ }, 2000);
4537
+ }
4538
+ }
4539
+
4323
4540
  function setupInteractionInterceptors() {
4324
4541
  if (!TRACK_INTERACTIONS) return;
4325
4542
 
@@ -4620,6 +4837,10 @@ function generateLrsBridgeCode(options) {
4620
4837
  setupQuizInterceptors();
4621
4838
  setupInteractionInterceptors();
4622
4839
 
4840
+ // Intercept SCORM cmi.interactions to capture quiz answers as xAPI statements
4841
+ // This wraps the ACTUAL SCORM API (window.API etc.) that Rise calls
4842
+ setupScormInteractionTracker();
4843
+
4623
4844
  // Fetch document metadata from API to get GUIDs (async)
4624
4845
  // Then send course launched event
4625
4846
  var sharedLinkToken = LRS.courseInfo ? LRS.courseInfo.sharedLinkToken : null;