@patch-adams/core 1.5.11 → 1.5.14

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
@@ -399,7 +399,9 @@ var LrsBridgeConfigSchema = zod.z.object({
399
399
  /** Document GUID for xAPI statement aggregation - baked into package at wrap time */
400
400
  documentGuid: zod.z.string().optional(),
401
401
  /** Version GUID for xAPI statement aggregation - unique per export */
402
- versionGuid: zod.z.string().optional()
402
+ versionGuid: zod.z.string().optional(),
403
+ /** Original SCORM package filename - baked in at wrap time for LRS searchability */
404
+ packageName: zod.z.string().optional()
403
405
  });
404
406
  var BlockingAssetConfigSchema = zod.z.object({
405
407
  /** Filename for the asset */
@@ -748,6 +750,7 @@ function generateLrsBridgeCode(options) {
748
750
  const lrsProxyEndpoint = options.lrsProxyEndpoint || "";
749
751
  const documentGuid = options.documentGuid || "";
750
752
  const versionGuid = options.versionGuid || "";
753
+ const packageName = options.packageName || "";
751
754
  return `
752
755
  // ==========================================================================
753
756
  // PATCH-ADAMS LRS BRIDGE v2.2.0
@@ -780,6 +783,7 @@ function generateLrsBridgeCode(options) {
780
783
  var LRS_PROXY_ENDPOINT = '${lrsProxyEndpoint}';
781
784
  var DOCUMENT_GUID = '${documentGuid}';
782
785
  var VERSION_GUID = '${versionGuid}';
786
+ var PACKAGE_NAME = '${packageName}';
783
787
 
784
788
  // Bravais LRS endpoint pattern: https://lrs-{tenant}.bravais.com/XAPI/statements
785
789
  // Example: core-acme.bravais.com -> lrs-acme.bravais.com
@@ -2463,7 +2467,9 @@ function generateLrsBridgeCode(options) {
2463
2467
  sharedLinkName: null,
2464
2468
  // Tenant info
2465
2469
  homepage: COURSE_HOMEPAGE || window.location.origin,
2466
- tenantHomepage: null // https://{tenant}.bravais.com format
2470
+ tenantHomepage: null, // https://{tenant}.bravais.com format
2471
+ // Package info
2472
+ packageName: null // Original SCORM zip filename
2467
2473
  };
2468
2474
 
2469
2475
  // 0. Use baked-in GUIDs from PA-Patcher config (highest priority - set at wrap time)
@@ -2476,6 +2482,10 @@ function generateLrsBridgeCode(options) {
2476
2482
  info.versionGuid = VERSION_GUID;
2477
2483
  log('Version GUID from baked-in config:', VERSION_GUID);
2478
2484
  }
2485
+ if (PACKAGE_NAME) {
2486
+ info.packageName = PACKAGE_NAME;
2487
+ log('Package name from baked-in config:', PACKAGE_NAME);
2488
+ }
2479
2489
 
2480
2490
  // 1. Extract shared link token and document ID from URL
2481
2491
  info.sharedLinkToken = extractSharedLinkToken();
@@ -2906,6 +2916,11 @@ function generateLrsBridgeCode(options) {
2906
2916
  // Resource type
2907
2917
  obj.definition.extensions['resourceType'] = LRS.courseInfo.resourceType || 'Course';
2908
2918
 
2919
+ // Package name (original SCORM zip filename)
2920
+ if (LRS.courseInfo.packageName) {
2921
+ obj.definition.extensions['packageName'] = LRS.courseInfo.packageName;
2922
+ }
2923
+
2909
2924
  return obj;
2910
2925
  }
2911
2926
 
@@ -3739,11 +3754,11 @@ function generateLrsBridgeCode(options) {
3739
3754
  // Send a course completion statement \u2014 called by skin or auto-detected via SCORM
3740
3755
  LRS.sendCompletionStatement = function(data) {
3741
3756
  data = data || {};
3742
- if (scormCompletionSent) {
3743
- log('Completion statement already sent (SCORM auto-detected). Skipping duplicate.');
3744
- return;
3757
+ // Cancel any pending SCORM auto-detection to avoid double-fire for same attempt
3758
+ if (scormCompletionDebounce) {
3759
+ clearTimeout(scormCompletionDebounce);
3760
+ scormCompletionDebounce = null;
3745
3761
  }
3746
- scormCompletionSent = true;
3747
3762
 
3748
3763
  var status = data.status || 'completed';
3749
3764
  var courseTitle = data.courseTitle || (LRS.courseInfo && LRS.courseInfo.title ? decodeEntities(LRS.courseInfo.title) : decodeEntities(document.title) || 'Course');
@@ -3773,11 +3788,58 @@ function generateLrsBridgeCode(options) {
3773
3788
  };
3774
3789
  if (data.employeeName) activityDetails.employeeName = data.employeeName;
3775
3790
 
3776
- log('Sending completion statement:', verbKey, 'score:', result.score, 'title:', courseTitle);
3777
- var statement = buildStatement(verbKey, ACTIVITY_TYPES.course, activityDetails, result, null);
3791
+ // 1. Always send "completed" statement
3792
+ log('Sending completed statement, score:', result.score, 'title:', courseTitle);
3793
+ var completedStatement = buildStatement('completed', ACTIVITY_TYPES.course, activityDetails, result, null);
3794
+ sendStatement(completedStatement);
3795
+
3796
+ // 2. Send "passed" or "failed" statement (assessment outcome)
3797
+ if (status === 'passed' || status === 'failed') {
3798
+ log('Sending', status, 'statement');
3799
+ var outcomeStatement = buildStatement(status, ACTIVITY_TYPES.course, activityDetails, result, null);
3800
+ sendStatement(outcomeStatement);
3801
+ }
3802
+ };
3803
+
3804
+ // Send a "terminated" statement \u2014 called by skin close button or auto on page unload
3805
+ var exitStatementSent = false;
3806
+ LRS.sendExitStatement = function(data) {
3807
+ if (exitStatementSent) return;
3808
+ exitStatementSent = true;
3809
+ data = data || {};
3810
+
3811
+ var courseTitle = (LRS.courseInfo && LRS.courseInfo.title) ? decodeEntities(LRS.courseInfo.title) : decodeEntities(document.title) || 'Course';
3812
+ var duration = null;
3813
+ if (LRS.launchTime) {
3814
+ var ms = new Date().getTime() - new Date(LRS.launchTime).getTime();
3815
+ var secs = Math.floor(ms / 1000);
3816
+ var mins = Math.floor(secs / 60);
3817
+ var hrs = Math.floor(mins / 60);
3818
+ duration = 'PT' + (hrs > 0 ? hrs + 'H' : '') + (mins % 60) + 'M' + (secs % 60) + 'S';
3819
+ }
3820
+
3821
+ var activityDetails = {
3822
+ courseTitle: courseTitle,
3823
+ exitSource: data.source || 'page-unload',
3824
+ employeeEmail: LRS.actor && LRS.actor.mbox ? LRS.actor.mbox.replace('mailto:', '') : undefined,
3825
+ employeeId: LRS.employeeId || undefined,
3826
+ sessionDuration: duration,
3827
+ statementsSent: LRS.stats.statementsSent
3828
+ };
3829
+
3830
+ var result = duration ? { duration: duration } : null;
3831
+ log('Sending exit statement, duration:', duration);
3832
+ var statement = buildStatement('terminated', ACTIVITY_TYPES.course, activityDetails, result, null);
3778
3833
  sendStatement(statement);
3779
3834
  };
3780
3835
 
3836
+ // Auto-send exit statement on page unload
3837
+ window.addEventListener('beforeunload', function() {
3838
+ if (!exitStatementSent && LRS.actor) {
3839
+ LRS.sendExitStatement({ source: 'page-unload' });
3840
+ }
3841
+ });
3842
+
3781
3843
  /**
3782
3844
  * Re-extract actor from SCORM after LMSInitialize has been called.
3783
3845
  * Call this from the SCORM wrapper after scormInit() succeeds,
@@ -3884,6 +3946,23 @@ function generateLrsBridgeCode(options) {
3884
3946
  LRS.interacted({ type: 'choice-branch', id: data.id, name: 'Choice Branch Discarded' });
3885
3947
  };
3886
3948
 
3949
+ // ========================================================================
3950
+ // PUBLIC API \u2014 expose core internals for skin scripts
3951
+ // Usage: window.pa_patcher.lrs.api.sendStatement(stmt)
3952
+ // ========================================================================
3953
+ LRS.api = {
3954
+ sendStatement: sendStatement,
3955
+ buildStatement: buildStatement,
3956
+ buildCourseActivityObject: buildCourseActivityObject,
3957
+ buildXylemeContext: buildXylemeContext,
3958
+ generateUUID: generateUUID,
3959
+ decodeEntities: decodeEntities,
3960
+ extractActor: extractActor,
3961
+ getCachedLessonInfo: getCachedLessonInfo,
3962
+ VERBS: VERBS,
3963
+ ACTIVITY_TYPES: ACTIVITY_TYPES
3964
+ };
3965
+
3887
3966
  // ========================================================================
3888
3967
  // 8. RISE EVENT INTERCEPTORS
3889
3968
  // ========================================================================
@@ -4449,7 +4528,7 @@ function generateLrsBridgeCode(options) {
4449
4528
  var scormInteractionsSent = {}; // Track which interactions were already sent
4450
4529
  var scormTrackerActive = false; // Set true when SCORM tracker wraps SetValue \u2014 KC handler defers
4451
4530
  var scormCourseData = {}; // Accumulates course-level SCORM data (score, status)
4452
- var scormCompletionSent = false; // Prevent duplicate completion statements
4531
+ var scormCompletionDebounce = null; // Debounce timer \u2014 prevents duplicate fires within same completion event
4453
4532
 
4454
4533
  function interceptScormSetValue(key, value) {
4455
4534
  if (typeof key !== 'string') return;
@@ -4469,12 +4548,15 @@ function generateLrsBridgeCode(options) {
4469
4548
  if (key === 'cmi.success_status') { scormCourseData.successStatus = String(value).toLowerCase(); }
4470
4549
 
4471
4550
  // Fire course completion statement when status indicates pass/fail/complete
4472
- if ((key === 'cmi.core.lesson_status' || key === 'cmi.success_status' || key === 'cmi.completion_status') && !scormCompletionSent) {
4551
+ // Uses debounce (not one-time flag) so retries/new attempts are captured
4552
+ if (key === 'cmi.core.lesson_status' || key === 'cmi.success_status' || key === 'cmi.completion_status') {
4473
4553
  var status = String(value).toLowerCase();
4474
4554
  if (status === 'passed' || status === 'failed' || status === 'completed') {
4475
- scormCompletionSent = true;
4476
- // Slight delay to let score values arrive (Rise often sets score just before status)
4477
- setTimeout(function() { sendCourseCompletionStatement(status); }, 200);
4555
+ if (scormCompletionDebounce) clearTimeout(scormCompletionDebounce);
4556
+ scormCompletionDebounce = setTimeout(function() {
4557
+ scormCompletionDebounce = null;
4558
+ sendCourseCompletionStatement(status);
4559
+ }, 500);
4478
4560
  }
4479
4561
  }
4480
4562
 
@@ -4585,11 +4667,6 @@ function generateLrsBridgeCode(options) {
4585
4667
 
4586
4668
  var courseTitle = (LRS.courseInfo && LRS.courseInfo.title) ? decodeEntities(LRS.courseInfo.title) : decodeEntities(document.title) || 'Course';
4587
4669
 
4588
- // Determine verb
4589
- var verbKey = 'completed';
4590
- if (status === 'passed') verbKey = 'passed';
4591
- if (status === 'failed') verbKey = 'failed';
4592
-
4593
4670
  // Build result with score
4594
4671
  var result = { completion: true };
4595
4672
 
@@ -4615,10 +4692,17 @@ function generateLrsBridgeCode(options) {
4615
4692
  employeeId: LRS.employeeId || undefined
4616
4693
  };
4617
4694
 
4618
- log('Sending course completion:', verbKey, 'score:', result.score, 'title:', courseTitle);
4695
+ // 1. Send "completed" statement (course was finished)
4696
+ log('Sending completed statement, score:', result.score, 'title:', courseTitle);
4697
+ var completedStatement = buildStatement('completed', ACTIVITY_TYPES.course, activityDetails, result, null);
4698
+ sendStatement(completedStatement);
4619
4699
 
4620
- var statement = buildStatement(verbKey, ACTIVITY_TYPES.course, activityDetails, result, null);
4621
- sendStatement(statement);
4700
+ // 2. Send "passed" or "failed" statement (assessment outcome)
4701
+ if (status === 'passed' || status === 'failed') {
4702
+ log('Sending', status, 'statement');
4703
+ var outcomeStatement = buildStatement(status, ACTIVITY_TYPES.course, activityDetails, result, null);
4704
+ sendStatement(outcomeStatement);
4705
+ }
4622
4706
 
4623
4707
  logGroupEnd();
4624
4708
  }
@@ -5302,7 +5386,8 @@ function buildJsBeforeOptions(config, metadata) {
5302
5386
  lrsProxyEndpoint: lrsBridgeConfig.lrsProxyEndpoint,
5303
5387
  employeeApiEndpoint: lrsBridgeConfig.employeeApiEndpoint,
5304
5388
  documentGuid: lrsBridgeConfig.documentGuid,
5305
- versionGuid: lrsBridgeConfig.versionGuid
5389
+ versionGuid: lrsBridgeConfig.versionGuid,
5390
+ packageName: lrsBridgeConfig.packageName
5306
5391
  };
5307
5392
  return {
5308
5393
  remoteUrl: `${config.remoteDomain}/${config.localFolders.js}/${config.jsBefore.filename}`,
@@ -6682,6 +6767,11 @@ var Patcher = class {
6682
6767
  if (effectiveSkin) {
6683
6768
  console.log(`[Patcher] Skin: ${effectiveSkin}`);
6684
6769
  }
6770
+ if (options.packageName) {
6771
+ this.config.lrsBridge = this.config.lrsBridge ?? {};
6772
+ this.config.lrsBridge.packageName = options.packageName;
6773
+ console.log(`[Patcher] Package name: ${options.packageName}`);
6774
+ }
6685
6775
  const htmlInjector = this.getHtmlInjector(toolInfo.tool);
6686
6776
  htmlInjector.setMetadata(metadata);
6687
6777
  let fetchedFallbacks = {};
@@ -6975,6 +7065,7 @@ program.command("patch <input>").description("Patch a Rise course ZIP file").opt
6975
7065
  if (options.jsAfter) {
6976
7066
  patchOptions.jsAfterContent = fs.readFileSync(path.resolve(options.jsAfter), "utf-8");
6977
7067
  }
7068
+ patchOptions.packageName = path.basename(inputPath);
6978
7069
  spinner.text = "Patching package...";
6979
7070
  const patcher = new Patcher(validatedConfig);
6980
7071
  const { buffer, result } = await patcher.patch(inputBuffer, patchOptions);