@patch-adams/core 1.5.10 → 1.5.13

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
@@ -3736,6 +3736,95 @@ function generateLrsBridgeCode(options) {
3736
3736
  sendStatement(statement);
3737
3737
  };
3738
3738
 
3739
+ // Send a course completion statement \u2014 called by skin or auto-detected via SCORM
3740
+ LRS.sendCompletionStatement = function(data) {
3741
+ data = data || {};
3742
+ // Cancel any pending SCORM auto-detection to avoid double-fire for same attempt
3743
+ if (scormCompletionDebounce) {
3744
+ clearTimeout(scormCompletionDebounce);
3745
+ scormCompletionDebounce = null;
3746
+ }
3747
+
3748
+ var status = data.status || 'completed';
3749
+ var courseTitle = data.courseTitle || (LRS.courseInfo && LRS.courseInfo.title ? decodeEntities(LRS.courseInfo.title) : decodeEntities(document.title) || 'Course');
3750
+
3751
+ var verbKey = 'completed';
3752
+ if (status === 'passed') verbKey = 'passed';
3753
+ if (status === 'failed') verbKey = 'failed';
3754
+
3755
+ var result = { completion: true, success: (status === 'passed') };
3756
+
3757
+ if (typeof data.score === 'number') {
3758
+ var max = data.scoreMax || 100;
3759
+ var min = data.scoreMin || 0;
3760
+ result.score = {
3761
+ raw: data.score,
3762
+ max: max,
3763
+ min: min,
3764
+ scaled: max > min ? (data.score - min) / (max - min) : 0
3765
+ };
3766
+ }
3767
+
3768
+ var activityDetails = {
3769
+ courseTitle: courseTitle,
3770
+ completionStatus: status,
3771
+ employeeEmail: data.email || (LRS.actor && LRS.actor.mbox ? LRS.actor.mbox.replace('mailto:', '') : undefined),
3772
+ employeeId: data.employeeId || LRS.employeeId || undefined
3773
+ };
3774
+ if (data.employeeName) activityDetails.employeeName = data.employeeName;
3775
+
3776
+ // 1. Always send "completed" statement
3777
+ log('Sending completed statement, score:', result.score, 'title:', courseTitle);
3778
+ var completedStatement = buildStatement('completed', ACTIVITY_TYPES.course, activityDetails, result, null);
3779
+ sendStatement(completedStatement);
3780
+
3781
+ // 2. Send "passed" or "failed" statement (assessment outcome)
3782
+ if (status === 'passed' || status === 'failed') {
3783
+ log('Sending', status, 'statement');
3784
+ var outcomeStatement = buildStatement(status, ACTIVITY_TYPES.course, activityDetails, result, null);
3785
+ sendStatement(outcomeStatement);
3786
+ }
3787
+ };
3788
+
3789
+ // Send a "terminated" statement \u2014 called by skin close button or auto on page unload
3790
+ var exitStatementSent = false;
3791
+ LRS.sendExitStatement = function(data) {
3792
+ if (exitStatementSent) return;
3793
+ exitStatementSent = true;
3794
+ data = data || {};
3795
+
3796
+ var courseTitle = (LRS.courseInfo && LRS.courseInfo.title) ? decodeEntities(LRS.courseInfo.title) : decodeEntities(document.title) || 'Course';
3797
+ var duration = null;
3798
+ if (LRS.launchTime) {
3799
+ var ms = new Date().getTime() - new Date(LRS.launchTime).getTime();
3800
+ var secs = Math.floor(ms / 1000);
3801
+ var mins = Math.floor(secs / 60);
3802
+ var hrs = Math.floor(mins / 60);
3803
+ duration = 'PT' + (hrs > 0 ? hrs + 'H' : '') + (mins % 60) + 'M' + (secs % 60) + 'S';
3804
+ }
3805
+
3806
+ var activityDetails = {
3807
+ courseTitle: courseTitle,
3808
+ exitSource: data.source || 'page-unload',
3809
+ employeeEmail: LRS.actor && LRS.actor.mbox ? LRS.actor.mbox.replace('mailto:', '') : undefined,
3810
+ employeeId: LRS.employeeId || undefined,
3811
+ sessionDuration: duration,
3812
+ statementsSent: LRS.stats.statementsSent
3813
+ };
3814
+
3815
+ var result = duration ? { duration: duration } : null;
3816
+ log('Sending exit statement, duration:', duration);
3817
+ var statement = buildStatement('terminated', ACTIVITY_TYPES.course, activityDetails, result, null);
3818
+ sendStatement(statement);
3819
+ };
3820
+
3821
+ // Auto-send exit statement on page unload
3822
+ window.addEventListener('beforeunload', function() {
3823
+ if (!exitStatementSent && LRS.actor) {
3824
+ LRS.sendExitStatement({ source: 'page-unload' });
3825
+ }
3826
+ });
3827
+
3739
3828
  /**
3740
3829
  * Re-extract actor from SCORM after LMSInitialize has been called.
3741
3830
  * Call this from the SCORM wrapper after scormInit() succeeds,
@@ -3842,6 +3931,23 @@ function generateLrsBridgeCode(options) {
3842
3931
  LRS.interacted({ type: 'choice-branch', id: data.id, name: 'Choice Branch Discarded' });
3843
3932
  };
3844
3933
 
3934
+ // ========================================================================
3935
+ // PUBLIC API \u2014 expose core internals for skin scripts
3936
+ // Usage: window.pa_patcher.lrs.api.sendStatement(stmt)
3937
+ // ========================================================================
3938
+ LRS.api = {
3939
+ sendStatement: sendStatement,
3940
+ buildStatement: buildStatement,
3941
+ buildCourseActivityObject: buildCourseActivityObject,
3942
+ buildXylemeContext: buildXylemeContext,
3943
+ generateUUID: generateUUID,
3944
+ decodeEntities: decodeEntities,
3945
+ extractActor: extractActor,
3946
+ getCachedLessonInfo: getCachedLessonInfo,
3947
+ VERBS: VERBS,
3948
+ ACTIVITY_TYPES: ACTIVITY_TYPES
3949
+ };
3950
+
3845
3951
  // ========================================================================
3846
3952
  // 8. RISE EVENT INTERCEPTORS
3847
3953
  // ========================================================================
@@ -4406,9 +4512,40 @@ function generateLrsBridgeCode(options) {
4406
4512
  var scormInteractions = {}; // Pending interactions keyed by index N
4407
4513
  var scormInteractionsSent = {}; // Track which interactions were already sent
4408
4514
  var scormTrackerActive = false; // Set true when SCORM tracker wraps SetValue \u2014 KC handler defers
4515
+ var scormCourseData = {}; // Accumulates course-level SCORM data (score, status)
4516
+ var scormCompletionDebounce = null; // Debounce timer \u2014 prevents duplicate fires within same completion event
4409
4517
 
4410
4518
  function interceptScormSetValue(key, value) {
4411
4519
  if (typeof key !== 'string') return;
4520
+
4521
+ // ---- Course-level SCORM data (score, status, completion) ----
4522
+ // SCORM 1.2
4523
+ if (key === 'cmi.core.score.raw') { scormCourseData.scoreRaw = parseFloat(value); }
4524
+ if (key === 'cmi.core.score.max') { scormCourseData.scoreMax = parseFloat(value); }
4525
+ if (key === 'cmi.core.score.min') { scormCourseData.scoreMin = parseFloat(value); }
4526
+ if (key === 'cmi.core.lesson_status') { scormCourseData.status = String(value).toLowerCase(); }
4527
+ // SCORM 2004
4528
+ if (key === 'cmi.score.raw') { scormCourseData.scoreRaw = parseFloat(value); }
4529
+ if (key === 'cmi.score.max') { scormCourseData.scoreMax = parseFloat(value); }
4530
+ if (key === 'cmi.score.min') { scormCourseData.scoreMin = parseFloat(value); }
4531
+ if (key === 'cmi.score.scaled') { scormCourseData.scoreScaled = parseFloat(value); }
4532
+ if (key === 'cmi.completion_status') { scormCourseData.completionStatus = String(value).toLowerCase(); }
4533
+ if (key === 'cmi.success_status') { scormCourseData.successStatus = String(value).toLowerCase(); }
4534
+
4535
+ // Fire course completion statement when status indicates pass/fail/complete
4536
+ // Uses debounce (not one-time flag) so retries/new attempts are captured
4537
+ if (key === 'cmi.core.lesson_status' || key === 'cmi.success_status' || key === 'cmi.completion_status') {
4538
+ var status = String(value).toLowerCase();
4539
+ if (status === 'passed' || status === 'failed' || status === 'completed') {
4540
+ if (scormCompletionDebounce) clearTimeout(scormCompletionDebounce);
4541
+ scormCompletionDebounce = setTimeout(function() {
4542
+ scormCompletionDebounce = null;
4543
+ sendCourseCompletionStatement(status);
4544
+ }, 500);
4545
+ }
4546
+ }
4547
+
4548
+ // ---- Interaction-level tracking (existing) ----
4412
4549
  var interactionPattern = /^cmi\\.interactions\\.(\\d+)\\.(.+)$/;
4413
4550
  var match = key.match(interactionPattern);
4414
4551
  if (!match) return;
@@ -4508,6 +4645,53 @@ function generateLrsBridgeCode(options) {
4508
4645
  }
4509
4646
  }
4510
4647
 
4648
+ function sendCourseCompletionStatement(status) {
4649
+ logGroup('Course Completion');
4650
+ log('Status:', status);
4651
+ log('SCORM course data:', scormCourseData);
4652
+
4653
+ var courseTitle = (LRS.courseInfo && LRS.courseInfo.title) ? decodeEntities(LRS.courseInfo.title) : decodeEntities(document.title) || 'Course';
4654
+
4655
+ // Build result with score
4656
+ var result = { completion: true };
4657
+
4658
+ var raw = scormCourseData.scoreRaw;
4659
+ var max = scormCourseData.scoreMax || 100;
4660
+ var min = scormCourseData.scoreMin || 0;
4661
+
4662
+ if (typeof raw === 'number' && !isNaN(raw)) {
4663
+ result.score = {
4664
+ raw: raw,
4665
+ max: max,
4666
+ min: min,
4667
+ scaled: scormCourseData.scoreScaled != null ? scormCourseData.scoreScaled : (max > min ? (raw - min) / (max - min) : 0)
4668
+ };
4669
+ }
4670
+
4671
+ result.success = (status === 'passed');
4672
+
4673
+ var activityDetails = {
4674
+ courseTitle: courseTitle,
4675
+ completionStatus: status,
4676
+ employeeEmail: LRS.actor && LRS.actor.mbox ? LRS.actor.mbox.replace('mailto:', '') : undefined,
4677
+ employeeId: LRS.employeeId || undefined
4678
+ };
4679
+
4680
+ // 1. Send "completed" statement (course was finished)
4681
+ log('Sending completed statement, score:', result.score, 'title:', courseTitle);
4682
+ var completedStatement = buildStatement('completed', ACTIVITY_TYPES.course, activityDetails, result, null);
4683
+ sendStatement(completedStatement);
4684
+
4685
+ // 2. Send "passed" or "failed" statement (assessment outcome)
4686
+ if (status === 'passed' || status === 'failed') {
4687
+ log('Sending', status, 'statement');
4688
+ var outcomeStatement = buildStatement(status, ACTIVITY_TYPES.course, activityDetails, result, null);
4689
+ sendStatement(outcomeStatement);
4690
+ }
4691
+
4692
+ logGroupEnd();
4693
+ }
4694
+
4511
4695
  function setupScormInteractionTracker() {
4512
4696
  if (!TRACK_QUIZZES) return;
4513
4697