scorm-again 3.1.3 → 3.1.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.
Files changed (34) hide show
  1. package/dist/esm/scorm-again.js +1257 -236
  2. package/dist/esm/scorm-again.js.map +1 -1
  3. package/dist/esm/scorm-again.min.js +1 -1
  4. package/dist/esm/scorm-again.min.js.map +1 -1
  5. package/dist/esm/scorm12.js +14 -3
  6. package/dist/esm/scorm12.js.map +1 -1
  7. package/dist/esm/scorm12.min.js +1 -1
  8. package/dist/esm/scorm12.min.js.map +1 -1
  9. package/dist/esm/scorm2004.js +1257 -236
  10. package/dist/esm/scorm2004.js.map +1 -1
  11. package/dist/esm/scorm2004.min.js +1 -1
  12. package/dist/esm/scorm2004.min.js.map +1 -1
  13. package/dist/scorm-again.js +1619 -449
  14. package/dist/scorm-again.js.map +1 -1
  15. package/dist/scorm-again.min.js +1 -1
  16. package/dist/scorm12.js +54 -14
  17. package/dist/scorm12.js.map +1 -1
  18. package/dist/scorm12.min.js +1 -1
  19. package/dist/scorm2004.js +1614 -444
  20. package/dist/scorm2004.js.map +1 -1
  21. package/dist/scorm2004.min.js +1 -1
  22. package/dist/types/Scorm2004API.d.ts +12 -0
  23. package/dist/types/cmi/scorm2004/completion_status_evaluation.d.ts +7 -0
  24. package/dist/types/cmi/scorm2004/sequencing/activity.d.ts +47 -5
  25. package/dist/types/cmi/scorm2004/sequencing/handlers/rte_data_transfer.d.ts +1 -0
  26. package/dist/types/cmi/scorm2004/sequencing/handlers/termination_handler.d.ts +1 -0
  27. package/dist/types/cmi/scorm2004/sequencing/objectives/global_objective_synchronizer.d.ts +17 -1
  28. package/dist/types/cmi/scorm2004/sequencing/sequencing_rules.d.ts +6 -1
  29. package/dist/types/cmi/scorm2004/sequencing/traversal/flow_traversal_service.d.ts +5 -2
  30. package/dist/types/constants/regex.d.ts +1 -0
  31. package/dist/types/types/api_types.d.ts +12 -0
  32. package/dist/types/types/sequencing_types.d.ts +13 -0
  33. package/dist/types/utilities/core.d.ts +4 -1
  34. package/package.json +1 -1
@@ -139,12 +139,18 @@ function stringMatches(str, tester) {
139
139
  }
140
140
  return new RegExp(tester).test(str);
141
141
  }
142
- function memoize(fn, keyFn) {
142
+ function memoize(fn, keyFn, options) {
143
143
  const cache = /* @__PURE__ */ new Map();
144
144
  return ((...args) => {
145
145
  const key = keyFn ? keyFn(...args) : JSON.stringify(args);
146
+ if (options?.maxKeyLength !== void 0 && key.length > options.maxKeyLength) {
147
+ return fn(...args);
148
+ }
146
149
  return cache.has(key) ? cache.get(key) : (() => {
147
150
  const result = fn(...args);
151
+ if (options?.maxEntries !== void 0 && cache.size >= options.maxEntries) {
152
+ cache.delete(cache.keys().next().value);
153
+ }
148
154
  cache.set(key, result);
149
155
  return result;
150
156
  })();
@@ -896,6 +902,8 @@ const scorm2004_regex = {
896
902
  CMILang: "^([a-zA-Z]{1,8}|i|x)(-[a-zA-Z0-9-]{2,8})?$|^$",
897
903
  /** CMILangString250 - String with optional language tag, max 250 chars (RTE C.1.3) */
898
904
  CMILangString250: "^({lang=([a-zA-Z]{1,8}|i|x)(-[a-zA-Z0-9-]{2,8})?})?((?!{.*$).{0,250}$)?$",
905
+ /** CMILangString - Optional language tag, no length cap; RTE C.1.3 SPM is a floor and is deliberately not enforced */
906
+ CMILangString: "^({lang=([a-zA-Z]{1,8}|i|x)(-[a-zA-Z0-9-]{2,8})?})?((?!{.*$).*$)?$",
899
907
  /** CMILangcr - Language tag pattern with content */
900
908
  CMILangcr: "^(({lang=([a-zA-Z]{1,8}|i|x)?(-[a-zA-Z0-9-]{2,8})?}))(.*?)$",
901
909
  /** CMILangString250cr - String with optional language tag (carriage return variant) */
@@ -1548,12 +1556,6 @@ const HIDE_LMS_UI_TOKENS = [
1548
1556
  "suspendAll"
1549
1557
  ];
1550
1558
 
1551
- var RuleConditionOperator = /* @__PURE__ */ ((RuleConditionOperator2) => {
1552
- RuleConditionOperator2["NOT"] = "not";
1553
- RuleConditionOperator2["AND"] = "and";
1554
- RuleConditionOperator2["OR"] = "or";
1555
- return RuleConditionOperator2;
1556
- })(RuleConditionOperator || {});
1557
1559
  var RuleActionType = /* @__PURE__ */ ((RuleActionType2) => {
1558
1560
  RuleActionType2["SKIP"] = "skip";
1559
1561
  RuleActionType2["DISABLED"] = "disabled";
@@ -1568,6 +1570,48 @@ var RuleActionType = /* @__PURE__ */ ((RuleActionType2) => {
1568
1570
  RuleActionType2["EXIT"] = "exit";
1569
1571
  return RuleActionType2;
1570
1572
  })(RuleActionType || {});
1573
+ function kleeneNot(value) {
1574
+ if (value === "unknown") {
1575
+ return "unknown";
1576
+ }
1577
+ return !value;
1578
+ }
1579
+ function kleeneAnd(values) {
1580
+ let hasUnknown = false;
1581
+ for (const value of values) {
1582
+ if (value === false) {
1583
+ return false;
1584
+ }
1585
+ if (value === "unknown") {
1586
+ hasUnknown = true;
1587
+ }
1588
+ }
1589
+ return hasUnknown ? "unknown" : true;
1590
+ }
1591
+ function kleeneOr(values) {
1592
+ let hasUnknown = false;
1593
+ for (const value of values) {
1594
+ if (value === true) {
1595
+ return true;
1596
+ }
1597
+ if (value === "unknown") {
1598
+ hasUnknown = true;
1599
+ }
1600
+ }
1601
+ return hasUnknown ? "unknown" : false;
1602
+ }
1603
+ function combineRuleConditionResults(values, conditionCombination) {
1604
+ if (values.length === 0) {
1605
+ return false;
1606
+ }
1607
+ if (conditionCombination === "all" || conditionCombination === "and" /* AND */) {
1608
+ return kleeneAnd(values);
1609
+ }
1610
+ if (conditionCombination === "any" || conditionCombination === "or" /* OR */) {
1611
+ return kleeneOr(values);
1612
+ }
1613
+ return false;
1614
+ }
1571
1615
  class RuleCondition extends BaseCMI {
1572
1616
  _condition = "always" /* ALWAYS */;
1573
1617
  _operator = null;
@@ -1673,51 +1717,72 @@ class RuleCondition extends BaseCMI {
1673
1717
  /**
1674
1718
  * Evaluate the condition for an activity
1675
1719
  * @param {Activity} activity - The activity to evaluate the condition for
1676
- * @return {boolean} - True if the condition is met, false otherwise
1720
+ * @return {RuleConditionEvaluation} - True, false, or unknown per SCORM 2004 4th Ed.
1677
1721
  */
1678
1722
  evaluate(activity) {
1679
1723
  let result;
1724
+ const hasReferencedObjective = this._referencedObjective !== null;
1680
1725
  const referencedObjective = this.resolveReferencedObjective(activity);
1681
1726
  switch (this._condition) {
1682
1727
  case "satisfied" /* SATISFIED */:
1683
1728
  case "objectiveSatisfied" /* OBJECTIVE_SATISFIED */:
1684
- if (referencedObjective) {
1685
- result = referencedObjective.satisfiedStatus === true;
1729
+ if (hasReferencedObjective && !referencedObjective) {
1730
+ result = false;
1731
+ } else if (referencedObjective) {
1732
+ result = referencedObjective.satisfiedStatusKnown || referencedObjective.progressStatus ? referencedObjective.satisfiedStatus === true : "unknown";
1733
+ } else if (activity.objectiveSatisfiedStatusKnown) {
1734
+ result = activity.objectiveSatisfiedStatus === true;
1735
+ } else if (activity.successStatus !== SuccessStatus.UNKNOWN) {
1736
+ result = activity.successStatus === SuccessStatus.PASSED;
1686
1737
  } else {
1687
- result = activity.successStatus === SuccessStatus.PASSED || activity.objectiveSatisfiedStatus === true;
1738
+ result = "unknown";
1688
1739
  }
1689
1740
  break;
1690
1741
  case "objectiveStatusKnown" /* OBJECTIVE_STATUS_KNOWN */:
1691
- result = referencedObjective ? !!referencedObjective.measureStatus : !!activity.objectiveMeasureStatus;
1742
+ result = hasReferencedObjective && !referencedObjective ? false : referencedObjective ? !!referencedObjective.satisfiedStatusKnown : !!activity.objectiveSatisfiedStatusKnown;
1692
1743
  break;
1693
1744
  case "objectiveMeasureKnown" /* OBJECTIVE_MEASURE_KNOWN */:
1694
- result = referencedObjective ? !!referencedObjective.measureStatus : !!activity.objectiveMeasureStatus;
1745
+ result = hasReferencedObjective && !referencedObjective ? false : referencedObjective ? !!referencedObjective.measureStatus : !!activity.objectiveMeasureStatus;
1695
1746
  break;
1696
1747
  case "objectiveMeasureGreaterThan" /* OBJECTIVE_MEASURE_GREATER_THAN */: {
1748
+ if (hasReferencedObjective && !referencedObjective) {
1749
+ result = false;
1750
+ break;
1751
+ }
1697
1752
  const greaterThanValue = this._parameters.get("threshold") || 0;
1698
1753
  const measureStatus = referencedObjective ? referencedObjective.measureStatus : activity.objectiveMeasureStatus;
1699
1754
  const measureValue = referencedObjective ? referencedObjective.normalizedMeasure : activity.objectiveNormalizedMeasure;
1700
- result = !!measureStatus && measureValue > greaterThanValue;
1755
+ result = measureStatus ? measureValue > greaterThanValue : "unknown";
1701
1756
  break;
1702
1757
  }
1703
1758
  case "objectiveMeasureLessThan" /* OBJECTIVE_MEASURE_LESS_THAN */: {
1759
+ if (hasReferencedObjective && !referencedObjective) {
1760
+ result = false;
1761
+ break;
1762
+ }
1704
1763
  const lessThanValue = this._parameters.get("threshold") || 0;
1705
1764
  const measureStatus = referencedObjective ? referencedObjective.measureStatus : activity.objectiveMeasureStatus;
1706
1765
  const measureValue = referencedObjective ? referencedObjective.normalizedMeasure : activity.objectiveNormalizedMeasure;
1707
- result = !!measureStatus && measureValue < lessThanValue;
1766
+ result = measureStatus ? measureValue < lessThanValue : "unknown";
1708
1767
  break;
1709
1768
  }
1710
1769
  case "completed" /* COMPLETED */:
1711
1770
  case "activityCompleted" /* ACTIVITY_COMPLETED */:
1712
- if (referencedObjective) {
1713
- result = referencedObjective.completionStatus === CompletionStatus.COMPLETED;
1771
+ if (hasReferencedObjective && !referencedObjective) {
1772
+ result = false;
1773
+ } else if (referencedObjective) {
1774
+ result = referencedObjective.completionStatus === CompletionStatus.UNKNOWN ? "unknown" : referencedObjective.completionStatus === CompletionStatus.COMPLETED;
1775
+ } else if (activity.completionStatus === CompletionStatus.UNKNOWN) {
1776
+ result = "unknown";
1714
1777
  } else {
1715
- result = activity.isCompleted;
1778
+ result = activity.completionStatus === CompletionStatus.COMPLETED;
1716
1779
  }
1717
1780
  break;
1718
1781
  case "progressKnown" /* PROGRESS_KNOWN */:
1719
1782
  case "activityProgressKnown" /* ACTIVITY_PROGRESS_KNOWN */:
1720
- if (referencedObjective) {
1783
+ if (hasReferencedObjective && !referencedObjective) {
1784
+ result = false;
1785
+ } else if (referencedObjective) {
1721
1786
  result = referencedObjective.completionStatus !== CompletionStatus.UNKNOWN;
1722
1787
  } else {
1723
1788
  result = activity.completionStatus !== "unknown";
@@ -1746,7 +1811,7 @@ class RuleCondition extends BaseCMI {
1746
1811
  break;
1747
1812
  }
1748
1813
  if (this._operator === "not" /* NOT */) {
1749
- result = !result;
1814
+ result = kleeneNot(result);
1750
1815
  }
1751
1816
  return result;
1752
1817
  }
@@ -1953,15 +2018,10 @@ class SequencingRule extends BaseCMI {
1953
2018
  * @return {boolean} - True if the rule conditions are met, false otherwise
1954
2019
  */
1955
2020
  evaluate(activity) {
1956
- if (this._conditions.length === 0) {
1957
- return true;
1958
- }
1959
- if (this._conditionCombination === "all" || this._conditionCombination === "and" /* AND */) {
1960
- return this._conditions.every((condition) => condition.evaluate(activity));
1961
- } else if (this._conditionCombination === "any" || this._conditionCombination === "or" /* OR */) {
1962
- return this._conditions.some((condition) => condition.evaluate(activity));
1963
- }
1964
- return false;
2021
+ return combineRuleConditionResults(
2022
+ this._conditions.map((condition) => condition.evaluate(activity)),
2023
+ this._conditionCombination
2024
+ ) === true;
1965
2025
  }
1966
2026
  /**
1967
2027
  * toJSON for SequencingRule
@@ -2794,19 +2854,10 @@ class RuleEvaluationEngine {
2794
2854
  * Evaluates individual sequencing rule conditions
2795
2855
  * @param {Activity} activity - The activity to evaluate the rule for
2796
2856
  * @param {SequencingRule} rule - The rule to evaluate
2797
- * @return {boolean} - True if all rule conditions are met
2857
+ * @return {boolean} - True only when the rule evaluates to definite true
2798
2858
  */
2799
2859
  checkRuleSubprocess(activity, rule) {
2800
- if (rule.conditions.length === 0) {
2801
- return true;
2802
- }
2803
- const conditionCombination = rule.conditionCombination;
2804
- if (conditionCombination === "all" || conditionCombination === RuleConditionOperator.AND) {
2805
- return rule.conditions.every((condition) => condition.evaluate(activity));
2806
- } else if (conditionCombination === "any" || conditionCombination === RuleConditionOperator.OR) {
2807
- return rule.conditions.some((condition) => condition.evaluate(activity));
2808
- }
2809
- return false;
2860
+ return rule.evaluate(activity);
2810
2861
  }
2811
2862
  /**
2812
2863
  * Exit Action Rules Subprocess (TB.2.1)
@@ -3678,16 +3729,21 @@ class FlowTraversalService {
3678
3729
  * @param {Activity} fromActivity - The activity to flow from
3679
3730
  * @param {FlowSubprocessMode} direction - The flow direction
3680
3731
  * @return {FlowSubprocessResult} - Result containing the deliverable activity
3732
+ * @spec SN Book: SB.2.3 (Flow Subprocess) - preserves the SB.2.1 effective traversal direction for SB.2.2.
3733
+ * @spec SN Book: SB.2.2 (Flow Activity Traversal Subprocess) - evaluates candidates using the effective direction returned by SB.2.1.
3681
3734
  */
3682
3735
  flowSubprocess(fromActivity, direction) {
3683
3736
  let candidateActivity = fromActivity;
3684
3737
  let firstIteration = true;
3685
3738
  let lastCandidateHadNoChildren = false;
3739
+ let currentDirection = direction;
3740
+ let forwardOnlyCluster = null;
3686
3741
  while (candidateActivity) {
3687
3742
  const traversalResult = this.flowTreeTraversalSubprocess(
3688
3743
  candidateActivity,
3689
- direction,
3690
- firstIteration
3744
+ currentDirection,
3745
+ firstIteration,
3746
+ forwardOnlyCluster
3691
3747
  );
3692
3748
  if (!traversalResult.activity) {
3693
3749
  let exceptionCode = null;
@@ -3705,17 +3761,22 @@ class FlowTraversalService {
3705
3761
  traversalResult.endSequencingSession
3706
3762
  );
3707
3763
  }
3764
+ const effectiveDirection = traversalResult.direction || currentDirection;
3765
+ if (traversalResult.forwardOnlyCluster) {
3766
+ forwardOnlyCluster = traversalResult.forwardOnlyCluster;
3767
+ }
3708
3768
  lastCandidateHadNoChildren = traversalResult.activity.children.length > 0 && traversalResult.activity.getAvailableChildren().length === 0;
3709
3769
  const deliverable = this.flowActivityTraversalSubprocess(
3710
3770
  traversalResult.activity,
3711
- direction === FlowSubprocessMode.FORWARD,
3771
+ effectiveDirection === FlowSubprocessMode.FORWARD,
3712
3772
  true,
3713
- direction
3773
+ effectiveDirection
3714
3774
  );
3715
3775
  if (deliverable) {
3716
3776
  return new FlowSubprocessResult(deliverable, true, null, false);
3717
3777
  }
3718
3778
  candidateActivity = traversalResult.activity;
3779
+ currentDirection = effectiveDirection;
3719
3780
  firstIteration = false;
3720
3781
  }
3721
3782
  return new FlowSubprocessResult(null, false, null, false);
@@ -3726,11 +3787,13 @@ class FlowTraversalService {
3726
3787
  * @param {Activity} fromActivity - The activity to traverse from
3727
3788
  * @param {FlowSubprocessMode} direction - The traversal direction
3728
3789
  * @param {boolean} skipChildren - Whether to skip checking children
3790
+ * @param {Activity | null} forwardTraversalBoundary - Cluster boundary for an SB.2.1 forwardOnly direction reversal
3729
3791
  * @return {FlowTreeTraversalResult} - The next activity and flags
3792
+ * @spec SN Book: SB.2.1 (Flow Tree Traversal Subprocess) - backward traversal into a forwardOnly cluster selects the first available child and reverses traversal direction to Forward.
3730
3793
  */
3731
- flowTreeTraversalSubprocess(fromActivity, direction, skipChildren = false) {
3794
+ flowTreeTraversalSubprocess(fromActivity, direction, skipChildren = false, forwardTraversalBoundary = null) {
3732
3795
  if (direction === FlowSubprocessMode.FORWARD) {
3733
- return this.traverseForward(fromActivity, skipChildren);
3796
+ return this.traverseForward(fromActivity, skipChildren, forwardTraversalBoundary);
3734
3797
  } else {
3735
3798
  return this.traverseBackward(fromActivity);
3736
3799
  }
@@ -3739,10 +3802,15 @@ class FlowTraversalService {
3739
3802
  * Traverse forward in the activity tree
3740
3803
  * @param {Activity} fromActivity - Starting activity
3741
3804
  * @param {boolean} skipChildren - Whether to skip children
3805
+ * @param {Activity | null} forwardTraversalBoundary - Cluster boundary for an SB.2.1 forwardOnly direction reversal
3742
3806
  * @return {FlowTreeTraversalResult}
3807
+ * @spec SN Book: SB.2.1 (Flow Tree Traversal Subprocess) - a reversed Forward traversal from a forwardOnly cluster remains within that cluster.
3743
3808
  */
3744
- traverseForward(fromActivity, skipChildren) {
3745
- if (skipChildren && this.isActivityLastOverall(fromActivity)) {
3809
+ traverseForward(fromActivity, skipChildren, forwardTraversalBoundary = null) {
3810
+ if (forwardTraversalBoundary && !this.isDescendantOfOrSelf(fromActivity, forwardTraversalBoundary)) {
3811
+ return { activity: null, endSequencingSession: false };
3812
+ }
3813
+ if (!forwardTraversalBoundary && skipChildren && this.isActivityLastOverall(fromActivity)) {
3746
3814
  if (this.activityTree.root) {
3747
3815
  this.terminateDescendentAttempts(this.activityTree.root);
3748
3816
  }
@@ -3761,6 +3829,9 @@ class FlowTraversalService {
3761
3829
  if (nextSibling) {
3762
3830
  return { activity: nextSibling, endSequencingSession: false };
3763
3831
  }
3832
+ if (forwardTraversalBoundary && (current === forwardTraversalBoundary || current.parent === forwardTraversalBoundary)) {
3833
+ return { activity: null, endSequencingSession: false };
3834
+ }
3764
3835
  current = current.parent;
3765
3836
  }
3766
3837
  if (this.activityTree.root) {
@@ -3772,6 +3843,7 @@ class FlowTraversalService {
3772
3843
  * Traverse backward in the activity tree
3773
3844
  * @param {Activity} fromActivity - Starting activity
3774
3845
  * @return {FlowTreeTraversalResult}
3846
+ * @spec SN Book: SB.2.1 (Flow Tree Traversal Subprocess) - backward traversal into a forwardOnly cluster selects the first available child and reverses traversal direction to Forward.
3775
3847
  */
3776
3848
  traverseBackward(fromActivity) {
3777
3849
  if (fromActivity.parent && fromActivity.parent.sequencingControls.forwardOnly) {
@@ -3779,10 +3851,7 @@ class FlowTraversalService {
3779
3851
  }
3780
3852
  const previousSibling = this.activityTree.getPreviousSibling(fromActivity);
3781
3853
  if (previousSibling) {
3782
- return {
3783
- activity: this.getLastDescendant(previousSibling),
3784
- endSequencingSession: false
3785
- };
3854
+ return this.getBackwardTraversalEntry(previousSibling);
3786
3855
  }
3787
3856
  let current = fromActivity;
3788
3857
  let ancestorIterations = 0;
@@ -3793,38 +3862,64 @@ class FlowTraversalService {
3793
3862
  }
3794
3863
  const parentPreviousSibling = this.activityTree.getPreviousSibling(current.parent);
3795
3864
  if (parentPreviousSibling) {
3796
- return {
3797
- activity: this.getLastDescendant(parentPreviousSibling),
3798
- endSequencingSession: false
3799
- };
3865
+ return this.getBackwardTraversalEntry(parentPreviousSibling);
3800
3866
  }
3801
3867
  current = current.parent;
3802
3868
  }
3803
3869
  return { activity: null, endSequencingSession: false };
3804
3870
  }
3805
3871
  /**
3806
- * Get the last descendant of an activity
3872
+ * Get the activity entered by backward traversal.
3807
3873
  * @param {Activity} activity - The activity
3808
- * @return {Activity} - The last descendant
3874
+ * @return {FlowTreeTraversalResult} - The entered activity and effective direction
3875
+ * @spec SN Book: SB.2.1 (Flow Tree Traversal Subprocess) - entering a forwardOnly cluster while moving Backward uses the first available child and changes direction to Forward.
3809
3876
  */
3810
- getLastDescendant(activity) {
3811
- let lastDescendant = activity;
3877
+ getBackwardTraversalEntry(activity) {
3878
+ let enteredActivity = activity;
3812
3879
  let iterations = 0;
3813
3880
  const maxIterations = 1e4;
3814
3881
  while (true) {
3815
3882
  if (++iterations > maxIterations) {
3816
- throw new Error("Infinite loop detected while getting last descendant");
3883
+ throw new Error("Infinite loop detected while getting backward traversal entry");
3817
3884
  }
3818
- this.ensureSelectionAndRandomization(lastDescendant);
3819
- const children = lastDescendant.getAvailableChildren();
3885
+ this.ensureSelectionAndRandomization(enteredActivity);
3886
+ const children = enteredActivity.getAvailableChildren();
3820
3887
  if (children.length === 0) {
3821
3888
  break;
3822
3889
  }
3890
+ if (enteredActivity.sequencingControls.forwardOnly) {
3891
+ return {
3892
+ activity: children[0] || null,
3893
+ endSequencingSession: false,
3894
+ direction: FlowSubprocessMode.FORWARD,
3895
+ forwardOnlyCluster: enteredActivity
3896
+ };
3897
+ }
3823
3898
  const lastChild = children[children.length - 1];
3824
3899
  if (!lastChild) break;
3825
- lastDescendant = lastChild;
3900
+ enteredActivity = lastChild;
3901
+ }
3902
+ return {
3903
+ activity: enteredActivity,
3904
+ endSequencingSession: false
3905
+ };
3906
+ }
3907
+ /**
3908
+ * Check whether an activity is the same as or beneath an ancestor.
3909
+ * @param {Activity} activity - The activity to check
3910
+ * @param {Activity} ancestor - The expected ancestor
3911
+ * @return {boolean} - True when activity is within ancestor
3912
+ * @spec SN Book: SB.2.1 (Flow Tree Traversal Subprocess) - bounds Forward traversal after a forwardOnly direction reversal to the entered cluster.
3913
+ */
3914
+ isDescendantOfOrSelf(activity, ancestor) {
3915
+ let current = activity;
3916
+ while (current) {
3917
+ if (current === ancestor) {
3918
+ return true;
3919
+ }
3920
+ current = current.parent;
3826
3921
  }
3827
- return lastDescendant;
3922
+ return false;
3828
3923
  }
3829
3924
  /**
3830
3925
  * Flow Activity Traversal Subprocess (SB.2.2)
@@ -6374,7 +6469,9 @@ const checkValidFormat = memoize(
6374
6469
  (CMIElement, value, regexPattern, errorCode, _errorClass, allowEmptyString) => {
6375
6470
  const valueKey = typeof value === "string" ? value : `[${typeof value}]`;
6376
6471
  return `${CMIElement}:${valueKey}:${regexPattern}:${errorCode}:${allowEmptyString || false}`;
6377
- }
6472
+ },
6473
+ // Normal capped CMI values and regexes fit within 2000 characters; large uncapped values bypass caching.
6474
+ { maxEntries: 1e3, maxKeyLength: 2e3 }
6378
6475
  );
6379
6476
  const checkValidRange = memoize(
6380
6477
  (CMIElement, value, rangePattern, errorCode, errorClass) => {
@@ -6397,7 +6494,8 @@ const checkValidRange = memoize(
6397
6494
  },
6398
6495
  // Custom key function that excludes the error class from the cache key
6399
6496
  // since it can't be stringified and doesn't affect the validation result
6400
- (CMIElement, value, rangePattern, errorCode, _errorClass) => `${CMIElement}:${value}:${rangePattern}:${errorCode}`
6497
+ (CMIElement, value, rangePattern, errorCode, _errorClass) => `${CMIElement}:${value}:${rangePattern}:${errorCode}`,
6498
+ { maxEntries: 1e3 }
6401
6499
  );
6402
6500
 
6403
6501
  function check2004ValidFormat(CMIElement, value, regexPattern, allowEmptyString) {
@@ -6906,6 +7004,12 @@ class ActivityObjective {
6906
7004
  // objectives. It serves as a validity gate for other synced properties.
6907
7005
  _measureStatus = false;
6908
7006
  _normalizedMeasure = 0;
7007
+ _rawScore = "";
7008
+ _rawScoreKnown = false;
7009
+ _minScore = "";
7010
+ _minScoreKnown = false;
7011
+ _maxScore = "";
7012
+ _maxScoreKnown = false;
6909
7013
  _progressMeasure = 0;
6910
7014
  _progressMeasureStatus = false;
6911
7015
  _completionStatus = CompletionStatus.UNKNOWN;
@@ -6915,6 +7019,9 @@ class ActivityObjective {
6915
7019
  _normalizedMeasureDirty = false;
6916
7020
  _completionStatusDirty = false;
6917
7021
  _progressMeasureDirty = false;
7022
+ _rawScoreDirty = false;
7023
+ _minScoreDirty = false;
7024
+ _maxScoreDirty = false;
6918
7025
  constructor(id, options = {}) {
6919
7026
  this._id = id;
6920
7027
  this._description = options.description ?? null;
@@ -6960,6 +7067,8 @@ class ActivityObjective {
6960
7067
  if (this._satisfiedStatus !== value) {
6961
7068
  this._satisfiedStatus = value;
6962
7069
  this._satisfiedStatusDirty = true;
7070
+ this._satisfiedStatusKnown = true;
7071
+ this._progressStatus = true;
6963
7072
  }
6964
7073
  }
6965
7074
  get satisfiedStatusKnown() {
@@ -6983,6 +7092,114 @@ class ActivityObjective {
6983
7092
  this._normalizedMeasureDirty = true;
6984
7093
  }
6985
7094
  }
7095
+ /**
7096
+ * Return the known raw score value held for ADLSEQ objective score mapping.
7097
+ *
7098
+ * @spec SCORM 2004 4th Ed. ADLSEQ objectives extension - raw score mapInfo
7099
+ */
7100
+ get rawScore() {
7101
+ return this._rawScore;
7102
+ }
7103
+ /**
7104
+ * Store the RTE raw score associated with this objective.
7105
+ *
7106
+ * @spec SCORM 2004 4th Ed. SN 3.10.3 / ADLSEQ objectives extension - raw score mapInfo
7107
+ */
7108
+ set rawScore(value) {
7109
+ if (this._rawScore !== value) {
7110
+ this._rawScore = value;
7111
+ this._rawScoreDirty = true;
7112
+ }
7113
+ this._rawScoreKnown = value !== "";
7114
+ }
7115
+ /**
7116
+ * Return whether this objective's raw score is known.
7117
+ *
7118
+ * @spec SCORM 2004 4th Ed. ADLSEQ objectives extension - raw score known state
7119
+ */
7120
+ get rawScoreKnown() {
7121
+ return this._rawScoreKnown;
7122
+ }
7123
+ /**
7124
+ * Set whether this objective's raw score is known.
7125
+ *
7126
+ * @spec SCORM 2004 4th Ed. ADLSEQ objectives extension - raw score known state
7127
+ */
7128
+ set rawScoreKnown(value) {
7129
+ this._rawScoreKnown = value;
7130
+ }
7131
+ /**
7132
+ * Return the known minimum score value held for ADLSEQ objective score mapping.
7133
+ *
7134
+ * @spec SCORM 2004 4th Ed. ADLSEQ objectives extension - min score mapInfo
7135
+ */
7136
+ get minScore() {
7137
+ return this._minScore;
7138
+ }
7139
+ /**
7140
+ * Store the RTE minimum score associated with this objective.
7141
+ *
7142
+ * @spec SCORM 2004 4th Ed. SN 3.10.3 / ADLSEQ objectives extension - min score mapInfo
7143
+ */
7144
+ set minScore(value) {
7145
+ if (this._minScore !== value) {
7146
+ this._minScore = value;
7147
+ this._minScoreDirty = true;
7148
+ }
7149
+ this._minScoreKnown = value !== "";
7150
+ }
7151
+ /**
7152
+ * Return whether this objective's minimum score is known.
7153
+ *
7154
+ * @spec SCORM 2004 4th Ed. ADLSEQ objectives extension - min score known state
7155
+ */
7156
+ get minScoreKnown() {
7157
+ return this._minScoreKnown;
7158
+ }
7159
+ /**
7160
+ * Set whether this objective's minimum score is known.
7161
+ *
7162
+ * @spec SCORM 2004 4th Ed. ADLSEQ objectives extension - min score known state
7163
+ */
7164
+ set minScoreKnown(value) {
7165
+ this._minScoreKnown = value;
7166
+ }
7167
+ /**
7168
+ * Return the known maximum score value held for ADLSEQ objective score mapping.
7169
+ *
7170
+ * @spec SCORM 2004 4th Ed. ADLSEQ objectives extension - max score mapInfo
7171
+ */
7172
+ get maxScore() {
7173
+ return this._maxScore;
7174
+ }
7175
+ /**
7176
+ * Store the RTE maximum score associated with this objective.
7177
+ *
7178
+ * @spec SCORM 2004 4th Ed. SN 3.10.3 / ADLSEQ objectives extension - max score mapInfo
7179
+ */
7180
+ set maxScore(value) {
7181
+ if (this._maxScore !== value) {
7182
+ this._maxScore = value;
7183
+ this._maxScoreDirty = true;
7184
+ }
7185
+ this._maxScoreKnown = value !== "";
7186
+ }
7187
+ /**
7188
+ * Return whether this objective's maximum score is known.
7189
+ *
7190
+ * @spec SCORM 2004 4th Ed. ADLSEQ objectives extension - max score known state
7191
+ */
7192
+ get maxScoreKnown() {
7193
+ return this._maxScoreKnown;
7194
+ }
7195
+ /**
7196
+ * Set whether this objective's maximum score is known.
7197
+ *
7198
+ * @spec SCORM 2004 4th Ed. ADLSEQ objectives extension - max score known state
7199
+ */
7200
+ set maxScoreKnown(value) {
7201
+ this._maxScoreKnown = value;
7202
+ }
6986
7203
  get progressMeasure() {
6987
7204
  return this._progressMeasure;
6988
7205
  }
@@ -7013,6 +7230,11 @@ class ActivityObjective {
7013
7230
  set progressStatus(value) {
7014
7231
  this._progressStatus = value;
7015
7232
  }
7233
+ /**
7234
+ * Report whether a local objective field has changed since the last global write.
7235
+ *
7236
+ * @spec SCORM 2004 4th Ed. SN 3.10.3 / ADLSEQ objectives extension - write maps use known local objective data
7237
+ */
7016
7238
  isDirty(property) {
7017
7239
  switch (property) {
7018
7240
  case "satisfiedStatus":
@@ -7023,8 +7245,19 @@ class ActivityObjective {
7023
7245
  return this._completionStatusDirty;
7024
7246
  case "progressMeasure":
7025
7247
  return this._progressMeasureDirty;
7248
+ case "rawScore":
7249
+ return this._rawScoreDirty;
7250
+ case "minScore":
7251
+ return this._minScoreDirty;
7252
+ case "maxScore":
7253
+ return this._maxScoreDirty;
7026
7254
  }
7027
7255
  }
7256
+ /**
7257
+ * Clear a local objective dirty flag after a successful global write.
7258
+ *
7259
+ * @spec SCORM 2004 4th Ed. SN 3.10.3 / ADLSEQ objectives extension - write maps update global objective state
7260
+ */
7028
7261
  clearDirty(property) {
7029
7262
  switch (property) {
7030
7263
  case "satisfiedStatus":
@@ -7039,13 +7272,31 @@ class ActivityObjective {
7039
7272
  case "progressMeasure":
7040
7273
  this._progressMeasureDirty = false;
7041
7274
  break;
7275
+ case "rawScore":
7276
+ this._rawScoreDirty = false;
7277
+ break;
7278
+ case "minScore":
7279
+ this._minScoreDirty = false;
7280
+ break;
7281
+ case "maxScore":
7282
+ this._maxScoreDirty = false;
7283
+ break;
7042
7284
  }
7043
7285
  }
7286
+ /**
7287
+ * Clear all write-map dirty flags for this objective.
7288
+ *
7289
+ * @spec SCORM 2004 4th Ed. SN 3.10.3 - objective mapInfo writes are field-specific
7290
+ * @spec SCORM 2004 4th Ed. ADLSEQ objectives extension - score mapInfo writes are field-specific
7291
+ */
7044
7292
  clearAllDirty() {
7045
7293
  this._satisfiedStatusDirty = false;
7046
7294
  this._normalizedMeasureDirty = false;
7047
7295
  this._completionStatusDirty = false;
7048
7296
  this._progressMeasureDirty = false;
7297
+ this._rawScoreDirty = false;
7298
+ this._minScoreDirty = false;
7299
+ this._maxScoreDirty = false;
7049
7300
  }
7050
7301
  /**
7051
7302
  * Initialize objective values from CMI data transfer
@@ -7055,6 +7306,8 @@ class ActivityObjective {
7055
7306
  * @param satisfiedStatus - The satisfied status from CMI
7056
7307
  * @param normalizedMeasure - The normalized measure from CMI
7057
7308
  * @param measureStatus - Whether measure is valid
7309
+ *
7310
+ * @spec SCORM 2004 4th Ed. RTE-to-SN Data Transfer - objective satisfaction and measure transfer
7058
7311
  */
7059
7312
  initializeFromCMI(satisfiedStatus, normalizedMeasure, measureStatus) {
7060
7313
  this._satisfiedStatus = satisfiedStatus;
@@ -7063,17 +7316,93 @@ class ActivityObjective {
7063
7316
  this._normalizedMeasureDirty = true;
7064
7317
  this._measureStatus = measureStatus;
7065
7318
  }
7319
+ /**
7320
+ * Initialize raw/min/max objective score values from RTE data transfer.
7321
+ *
7322
+ * @spec SCORM 2004 4th Ed. RTE-to-SN Data Transfer - objective score transfer
7323
+ * @spec SCORM 2004 4th Ed. ADLSEQ objectives extension - raw/min/max score mapInfo writes
7324
+ */
7325
+ initializeScoreFromCMI(score) {
7326
+ if (score.rawScore !== void 0 && score.rawScore !== "") {
7327
+ this._rawScore = score.rawScore;
7328
+ this._rawScoreKnown = true;
7329
+ this._rawScoreDirty = true;
7330
+ }
7331
+ if (score.minScore !== void 0 && score.minScore !== "") {
7332
+ this._minScore = score.minScore;
7333
+ this._minScoreKnown = true;
7334
+ this._minScoreDirty = true;
7335
+ }
7336
+ if (score.maxScore !== void 0 && score.maxScore !== "") {
7337
+ this._maxScore = score.maxScore;
7338
+ this._maxScoreKnown = true;
7339
+ this._maxScoreDirty = true;
7340
+ }
7341
+ }
7342
+ /**
7343
+ * Apply read-mapped global objective state without marking the values dirty.
7344
+ *
7345
+ * @spec SCORM 2004 4th Ed. SN 3.10.3 - read maps provide access to global objective state
7346
+ * @spec SCORM 2004 4th Ed. ADLSEQ objectives extension - score read maps are access-only
7347
+ */
7348
+ applyReadMappedState(state) {
7349
+ if (state.satisfiedStatus !== void 0) {
7350
+ this._satisfiedStatus = state.satisfiedStatus;
7351
+ this._satisfiedStatusKnown = true;
7352
+ this._progressStatus = true;
7353
+ }
7354
+ if (state.normalizedMeasure !== void 0) {
7355
+ this._normalizedMeasure = state.normalizedMeasure;
7356
+ this._measureStatus = true;
7357
+ }
7358
+ if (state.completionStatus !== void 0) {
7359
+ this._completionStatus = state.completionStatus;
7360
+ }
7361
+ if (state.progressMeasure !== void 0) {
7362
+ this._progressMeasure = state.progressMeasure;
7363
+ this._progressMeasureStatus = true;
7364
+ }
7365
+ if (state.rawScore !== void 0) {
7366
+ this._rawScore = state.rawScore;
7367
+ this._rawScoreKnown = true;
7368
+ }
7369
+ if (state.minScore !== void 0) {
7370
+ this._minScore = state.minScore;
7371
+ this._minScoreKnown = true;
7372
+ }
7373
+ if (state.maxScore !== void 0) {
7374
+ this._maxScore = state.maxScore;
7375
+ this._maxScoreKnown = true;
7376
+ }
7377
+ }
7378
+ /**
7379
+ * Reset local objective state for a fresh activity attempt.
7380
+ *
7381
+ * @spec SCORM 2004 4th Ed. SN 3.10 Objective Description - unknown objective state before tracking data exists
7382
+ * @spec SCORM 2004 4th Ed. ADLSEQ objectives extension - score map fields are unknown until transferred or read
7383
+ */
7066
7384
  resetState() {
7067
7385
  this._satisfiedStatus = false;
7068
7386
  this._satisfiedStatusKnown = false;
7069
7387
  this._measureStatus = false;
7070
7388
  this._normalizedMeasure = 0;
7389
+ this._rawScore = "";
7390
+ this._rawScoreKnown = false;
7391
+ this._minScore = "";
7392
+ this._minScoreKnown = false;
7393
+ this._maxScore = "";
7394
+ this._maxScoreKnown = false;
7071
7395
  this._progressMeasure = 0;
7072
7396
  this._progressMeasureStatus = false;
7073
7397
  this._completionStatus = CompletionStatus.UNKNOWN;
7074
7398
  this._progressStatus = false;
7075
7399
  this.clearAllDirty();
7076
7400
  }
7401
+ /**
7402
+ * Copy primary activity objective state back into the primary objective model.
7403
+ *
7404
+ * @spec SCORM 2004 4th Ed. RTE-to-SN Data Transfer - primary objective state is available to sequencing
7405
+ */
7077
7406
  updateFromActivity(activity) {
7078
7407
  if (this._satisfiedStatus !== activity.objectiveSatisfiedStatus) {
7079
7408
  this._satisfiedStatus = activity.objectiveSatisfiedStatus;
@@ -7095,6 +7424,11 @@ class ActivityObjective {
7095
7424
  this._completionStatusDirty = true;
7096
7425
  }
7097
7426
  }
7427
+ /**
7428
+ * Apply primary objective state to the owning activity for sequencing rules and rollup.
7429
+ *
7430
+ * @spec SCORM 2004 4th Ed. SN 3.10 Objective Description - primary objective contributes activity state
7431
+ */
7098
7432
  applyToActivity(activity) {
7099
7433
  if (!this._isPrimary) {
7100
7434
  return;
@@ -7105,7 +7439,8 @@ class ActivityObjective {
7105
7439
  this._normalizedMeasure,
7106
7440
  this._progressMeasure,
7107
7441
  this._progressMeasureStatus,
7108
- this._completionStatus
7442
+ this._completionStatus,
7443
+ this._progressStatus || this._satisfiedStatusKnown
7109
7444
  );
7110
7445
  }
7111
7446
  }
@@ -8175,7 +8510,7 @@ class Activity extends BaseCMI {
8175
8510
  /**
8176
8511
  * Setter for primary objective
8177
8512
  * @param {ActivityObjective | null} objective
8178
- */
8513
+ */
8179
8514
  set primaryObjective(objective) {
8180
8515
  this._primaryObjective = objective;
8181
8516
  if (this._primaryObjective) {
@@ -8197,7 +8532,7 @@ class Activity extends BaseCMI {
8197
8532
  /**
8198
8533
  * Replace objectives collection
8199
8534
  * @param {ActivityObjective[]} objectives
8200
- */
8535
+ */
8201
8536
  set objectives(objectives) {
8202
8537
  this._objectives = [...objectives];
8203
8538
  this.syncPrimaryObjectiveCollection();
@@ -8290,12 +8625,12 @@ class Activity extends BaseCMI {
8290
8625
  this._objectiveNormalizedMeasureDirty = false;
8291
8626
  this._objectiveMeasureStatusDirty = false;
8292
8627
  }
8293
- setPrimaryObjectiveState(satisfiedStatus, measureStatus, normalizedMeasure, progressMeasure, progressMeasureStatus, completionStatus) {
8628
+ setPrimaryObjectiveState(satisfiedStatus, measureStatus, normalizedMeasure, progressMeasure, progressMeasureStatus, completionStatus, objectiveProgressStatus = true) {
8294
8629
  if (this._objectiveSatisfiedStatus !== satisfiedStatus) {
8295
8630
  this._objectiveSatisfiedStatus = satisfiedStatus;
8296
8631
  this._objectiveSatisfiedStatusDirty = true;
8297
8632
  }
8298
- this._objectiveSatisfiedStatusKnown = true;
8633
+ this._objectiveSatisfiedStatusKnown = objectiveProgressStatus;
8299
8634
  if (this._objectiveMeasureStatus !== measureStatus) {
8300
8635
  this._objectiveMeasureStatus = measureStatus;
8301
8636
  this._objectiveMeasureStatusDirty = true;
@@ -8314,14 +8649,28 @@ class Activity extends BaseCMI {
8314
8649
  this._primaryObjective.progressMeasure = progressMeasure;
8315
8650
  this._primaryObjective.progressMeasureStatus = progressMeasureStatus;
8316
8651
  this._primaryObjective.completionStatus = completionStatus;
8652
+ this._primaryObjective.satisfiedStatusKnown = objectiveProgressStatus;
8653
+ this._primaryObjective.progressStatus = objectiveProgressStatus;
8317
8654
  }
8318
8655
  }
8656
+ /**
8657
+ * Snapshot objective state for sequencing persistence.
8658
+ *
8659
+ * @spec SCORM 2004 4th Ed. SN 3.10 Objective Description - objective state persists across attempts
8660
+ * @spec SCORM 2004 4th Ed. ADLSEQ objectives extension - score-map state persists with objective state
8661
+ */
8319
8662
  getObjectiveStateSnapshot() {
8320
8663
  const primarySnapshot = this._primaryObjective ? {
8321
8664
  id: this._primaryObjective.id,
8322
8665
  satisfiedStatus: this.objectiveSatisfiedStatus,
8323
8666
  measureStatus: this.objectiveMeasureStatus,
8324
8667
  normalizedMeasure: this.objectiveNormalizedMeasure,
8668
+ rawScore: this._primaryObjective.rawScore,
8669
+ rawScoreKnown: this._primaryObjective.rawScoreKnown,
8670
+ minScore: this._primaryObjective.minScore,
8671
+ minScoreKnown: this._primaryObjective.minScoreKnown,
8672
+ maxScore: this._primaryObjective.maxScore,
8673
+ maxScoreKnown: this._primaryObjective.maxScoreKnown,
8325
8674
  progressMeasure: this.progressMeasure ?? 0,
8326
8675
  progressMeasureStatus: this.progressMeasureStatus,
8327
8676
  progressStatus: this._primaryObjective.progressStatus,
@@ -8334,6 +8683,12 @@ class Activity extends BaseCMI {
8334
8683
  satisfiedStatus: objective.satisfiedStatus,
8335
8684
  measureStatus: objective.measureStatus,
8336
8685
  normalizedMeasure: objective.normalizedMeasure,
8686
+ rawScore: objective.rawScore,
8687
+ rawScoreKnown: objective.rawScoreKnown,
8688
+ minScore: objective.minScore,
8689
+ minScoreKnown: objective.minScoreKnown,
8690
+ maxScore: objective.maxScore,
8691
+ maxScoreKnown: objective.maxScoreKnown,
8337
8692
  progressMeasure: objective.progressMeasure,
8338
8693
  progressMeasureStatus: objective.progressMeasureStatus,
8339
8694
  progressStatus: objective.progressStatus,
@@ -8346,6 +8701,12 @@ class Activity extends BaseCMI {
8346
8701
  objectives: additionalSnapshots
8347
8702
  };
8348
8703
  }
8704
+ /**
8705
+ * Restore objective state from a sequencing persistence snapshot.
8706
+ *
8707
+ * @spec SCORM 2004 4th Ed. SN 3.10 Objective Description - persisted objective state restores sequencing state
8708
+ * @spec SCORM 2004 4th Ed. ADLSEQ objectives extension - persisted score-map state restores objective score fields
8709
+ */
8349
8710
  applyObjectiveStateSnapshot(snapshot) {
8350
8711
  if (snapshot.primary) {
8351
8712
  const primary = this.getObjectiveById(snapshot.primary.id);
@@ -8359,8 +8720,10 @@ class Activity extends BaseCMI {
8359
8720
  state.normalizedMeasure,
8360
8721
  state.progressMeasure,
8361
8722
  state.progressMeasureStatus,
8362
- state.completionStatus
8723
+ state.completionStatus,
8724
+ state.progressStatus
8363
8725
  );
8726
+ this.applyObjectiveScoreSnapshot(primary.objective, state);
8364
8727
  }
8365
8728
  }
8366
8729
  for (const state of snapshot.objectives) {
@@ -8373,11 +8736,30 @@ class Activity extends BaseCMI {
8373
8736
  objective.progressMeasure = state.progressMeasure;
8374
8737
  objective.progressMeasureStatus = state.progressMeasureStatus;
8375
8738
  objective.completionStatus = state.completionStatus;
8739
+ this.applyObjectiveScoreSnapshot(objective, state);
8376
8740
  objective.satisfiedByMeasure = state.satisfiedByMeasure ?? objective.satisfiedByMeasure;
8377
8741
  objective.minNormalizedMeasure = state.minNormalizedMeasure !== void 0 ? state.minNormalizedMeasure : objective.minNormalizedMeasure;
8378
8742
  }
8379
8743
  }
8380
8744
  }
8745
+ /**
8746
+ * Restore known raw/min/max score fields from an objective state snapshot.
8747
+ *
8748
+ * @spec SCORM 2004 4th Ed. ADLSEQ objectives extension - raw/min/max score known flags restore independently
8749
+ */
8750
+ applyObjectiveScoreSnapshot(objective, state) {
8751
+ const scoreState = {};
8752
+ if (state.rawScoreKnown) {
8753
+ scoreState.rawScore = state.rawScore;
8754
+ }
8755
+ if (state.minScoreKnown) {
8756
+ scoreState.minScore = state.minScore;
8757
+ }
8758
+ if (state.maxScoreKnown) {
8759
+ scoreState.maxScore = state.maxScore;
8760
+ }
8761
+ objective.applyReadMappedState(scoreState);
8762
+ }
8381
8763
  /**
8382
8764
  * Get available children with selection and randomization applied
8383
8765
  * @return {Activity[]}
@@ -9768,11 +10150,18 @@ class GlobalObjectiveSynchronizer {
9768
10150
  *
9769
10151
  * @param activity - The activity to process
9770
10152
  * @param globalObjectives - Global objective map
10153
+ *
10154
+ * @spec SCORM 2004 4th Ed. SN 3.10.3 - write mapInfo transfers local objective state to global objectives
10155
+ * @spec SCORM 2004 4th Ed. ADLSEQ objectives extension - raw/min/max score write maps
9771
10156
  */
9772
10157
  syncGlobalObjectivesWritePhase(activity, globalObjectives) {
10158
+ if (!this.canWriteGlobalObjectives(activity)) {
10159
+ return;
10160
+ }
9773
10161
  const objectives = activity.getAllObjectives();
9774
10162
  for (const objective of objectives) {
9775
10163
  const mapInfos = objective.mapInfo.length > 0 ? objective.mapInfo : [this.createDefaultMapInfo(objective)];
10164
+ const dirtyFieldsToClear = /* @__PURE__ */ new Set();
9776
10165
  for (const mapInfo of mapInfos) {
9777
10166
  const targetId = mapInfo.targetObjectiveID || objective.id;
9778
10167
  const globalObjective = this.ensureGlobalObjectiveEntry(
@@ -9780,36 +10169,54 @@ class GlobalObjectiveSynchronizer {
9780
10169
  targetId,
9781
10170
  objective
9782
10171
  );
9783
- if (mapInfo.writeSatisfiedStatus && objective.measureStatus && objective.isDirty("satisfiedStatus")) {
10172
+ if (mapInfo.writeSatisfiedStatus && this.hasKnownSatisfiedStatus(objective) && objective.isDirty("satisfiedStatus")) {
9784
10173
  globalObjective.satisfiedStatus = objective.satisfiedStatus;
9785
10174
  globalObjective.satisfiedStatusKnown = true;
9786
- objective.clearDirty("satisfiedStatus");
10175
+ dirtyFieldsToClear.add("satisfiedStatus");
9787
10176
  }
9788
10177
  if (mapInfo.writeNormalizedMeasure && objective.measureStatus && objective.isDirty("normalizedMeasure")) {
9789
10178
  globalObjective.normalizedMeasure = objective.normalizedMeasure;
9790
10179
  globalObjective.normalizedMeasureKnown = true;
9791
- objective.clearDirty("normalizedMeasure");
9792
- if (globalObjective.satisfiedByMeasure || objective.satisfiedByMeasure) {
10180
+ dirtyFieldsToClear.add("normalizedMeasure");
10181
+ if (mapInfo.writeSatisfiedStatus && objective.satisfiedByMeasure) {
9793
10182
  const threshold = objective.minNormalizedMeasure ?? activity.scaledPassingScore ?? 0.7;
9794
10183
  globalObjective.satisfiedStatus = objective.normalizedMeasure >= threshold;
9795
10184
  globalObjective.satisfiedStatusKnown = true;
9796
- objective.clearDirty("satisfiedStatus");
10185
+ dirtyFieldsToClear.add("satisfiedStatus");
9797
10186
  }
9798
10187
  }
9799
10188
  if (mapInfo.writeCompletionStatus && objective.completionStatus !== CompletionStatus.UNKNOWN && objective.isDirty("completionStatus")) {
9800
10189
  globalObjective.completionStatus = objective.completionStatus;
9801
10190
  globalObjective.completionStatusKnown = true;
9802
- objective.clearDirty("completionStatus");
10191
+ dirtyFieldsToClear.add("completionStatus");
10192
+ }
10193
+ if (mapInfo.writeRawScore && objective.rawScoreKnown && objective.isDirty("rawScore")) {
10194
+ globalObjective.rawScore = objective.rawScore;
10195
+ globalObjective.rawScoreKnown = true;
10196
+ dirtyFieldsToClear.add("rawScore");
10197
+ }
10198
+ if (mapInfo.writeMinScore && objective.minScoreKnown && objective.isDirty("minScore")) {
10199
+ globalObjective.minScore = objective.minScore;
10200
+ globalObjective.minScoreKnown = true;
10201
+ dirtyFieldsToClear.add("minScore");
10202
+ }
10203
+ if (mapInfo.writeMaxScore && objective.maxScoreKnown && objective.isDirty("maxScore")) {
10204
+ globalObjective.maxScore = objective.maxScore;
10205
+ globalObjective.maxScoreKnown = true;
10206
+ dirtyFieldsToClear.add("maxScore");
9803
10207
  }
9804
10208
  if (mapInfo.writeProgressMeasure && objective.progressMeasureStatus && objective.isDirty("progressMeasure")) {
9805
10209
  globalObjective.progressMeasure = objective.progressMeasure;
9806
10210
  globalObjective.progressMeasureKnown = true;
9807
- objective.clearDirty("progressMeasure");
10211
+ dirtyFieldsToClear.add("progressMeasure");
9808
10212
  }
9809
10213
  if (mapInfo.updateAttemptData) {
9810
10214
  this.updateActivityAttemptData(activity, globalObjective, objective);
9811
10215
  }
9812
10216
  }
10217
+ for (const property of dirtyFieldsToClear) {
10218
+ objective.clearDirty(property);
10219
+ }
9813
10220
  }
9814
10221
  }
9815
10222
  /**
@@ -9817,6 +10224,9 @@ class GlobalObjectiveSynchronizer {
9817
10224
  *
9818
10225
  * @param activity - The activity to process
9819
10226
  * @param globalObjectives - Global objective map
10227
+ *
10228
+ * @spec SCORM 2004 4th Ed. SN 3.10.3 - read mapInfo transfers global objective state into the local view
10229
+ * @spec SCORM 2004 4th Ed. ADLSEQ objectives extension - raw/min/max score read maps
9820
10230
  */
9821
10231
  syncGlobalObjectivesReadPhase(activity, globalObjectives) {
9822
10232
  const objectives = activity.getAllObjectives();
@@ -9827,25 +10237,13 @@ class GlobalObjectiveSynchronizer {
9827
10237
  const globalObjective = globalObjectives.get(targetId);
9828
10238
  if (!globalObjective) continue;
9829
10239
  const isPrimary = objective.isPrimary;
9830
- if (mapInfo.readSatisfiedStatus && globalObjective.satisfiedStatusKnown) {
9831
- objective.satisfiedStatus = globalObjective.satisfiedStatus;
9832
- objective.measureStatus = true;
9833
- }
9834
- if (mapInfo.readNormalizedMeasure && globalObjective.normalizedMeasureKnown) {
9835
- objective.normalizedMeasure = globalObjective.normalizedMeasure;
9836
- objective.measureStatus = true;
9837
- if (globalObjective.satisfiedByMeasure || objective.satisfiedByMeasure) {
9838
- const threshold = objective.minNormalizedMeasure ?? activity.scaledPassingScore ?? 0.7;
9839
- objective.satisfiedStatus = globalObjective.normalizedMeasure >= threshold;
9840
- }
9841
- }
9842
- if (mapInfo.readProgressMeasure && globalObjective.progressMeasureKnown) {
9843
- objective.progressMeasure = globalObjective.progressMeasure;
9844
- objective.progressMeasureStatus = true;
9845
- }
9846
- if (mapInfo.readCompletionStatus && globalObjective.completionStatusKnown) {
9847
- objective.completionStatus = globalObjective.completionStatus;
9848
- }
10240
+ const readState = GlobalObjectiveSynchronizer.getGlobalObjectiveReadState(
10241
+ activity,
10242
+ objective,
10243
+ mapInfo,
10244
+ globalObjective
10245
+ );
10246
+ this.applyGlobalObjectiveReadState(objective, readState);
9849
10247
  if (isPrimary) {
9850
10248
  objective.applyToActivity(activity);
9851
10249
  }
@@ -9888,56 +10286,61 @@ class GlobalObjectiveSynchronizer {
9888
10286
  * @param objective - The objective to sync
9889
10287
  * @param mapInfo - Map info for this objective
9890
10288
  * @param globalObjective - The global objective
10289
+ *
10290
+ * @spec SCORM 2004 4th Ed. SN 3.10.3 - objective mapInfo read/write synchronization
10291
+ * @spec SCORM 2004 4th Ed. ADLSEQ objectives extension - score mapInfo synchronization
9891
10292
  */
9892
10293
  syncObjectiveState(activity, objective, mapInfo, globalObjective) {
9893
10294
  try {
9894
10295
  const isPrimary = objective.isPrimary;
9895
10296
  const localObjective = this.getLocalObjectiveState(activity, objective, isPrimary);
9896
- if (mapInfo.readSatisfiedStatus && globalObjective.satisfiedStatusKnown) {
9897
- objective.satisfiedStatus = globalObjective.satisfiedStatus;
9898
- objective.measureStatus = true;
9899
- }
9900
- if (mapInfo.readNormalizedMeasure && globalObjective.normalizedMeasureKnown) {
9901
- objective.normalizedMeasure = globalObjective.normalizedMeasure;
9902
- objective.measureStatus = true;
9903
- if (globalObjective.satisfiedByMeasure || objective.satisfiedByMeasure) {
9904
- const threshold = objective.minNormalizedMeasure ?? activity.scaledPassingScore ?? 0.7;
9905
- objective.satisfiedStatus = globalObjective.normalizedMeasure >= threshold;
9906
- }
9907
- }
9908
- if (mapInfo.readProgressMeasure && globalObjective.progressMeasureKnown) {
9909
- objective.progressMeasure = globalObjective.progressMeasure;
9910
- objective.progressMeasureStatus = true;
9911
- }
9912
- if (mapInfo.readCompletionStatus && globalObjective.completionStatusKnown) {
9913
- objective.completionStatus = globalObjective.completionStatus;
9914
- }
10297
+ const readState = GlobalObjectiveSynchronizer.getGlobalObjectiveReadState(
10298
+ activity,
10299
+ objective,
10300
+ mapInfo,
10301
+ globalObjective
10302
+ );
10303
+ this.applyGlobalObjectiveReadState(objective, readState);
9915
10304
  if (objective.isPrimary) {
9916
10305
  objective.applyToActivity(activity);
9917
10306
  }
9918
- if (mapInfo.writeSatisfiedStatus && objective.measureStatus) {
9919
- globalObjective.satisfiedStatus = objective.satisfiedStatus;
9920
- globalObjective.satisfiedStatusKnown = true;
9921
- }
9922
- if (mapInfo.writeNormalizedMeasure && objective.measureStatus) {
9923
- globalObjective.normalizedMeasure = objective.normalizedMeasure;
9924
- globalObjective.normalizedMeasureKnown = true;
9925
- if (globalObjective.satisfiedByMeasure || objective.satisfiedByMeasure) {
9926
- const threshold = objective.minNormalizedMeasure ?? activity.scaledPassingScore ?? 0.7;
9927
- globalObjective.satisfiedStatus = objective.normalizedMeasure >= threshold;
10307
+ if (this.canWriteGlobalObjectives(activity)) {
10308
+ if (mapInfo.writeSatisfiedStatus && this.hasKnownSatisfiedStatus(objective)) {
10309
+ globalObjective.satisfiedStatus = objective.satisfiedStatus;
9928
10310
  globalObjective.satisfiedStatusKnown = true;
9929
10311
  }
9930
- }
9931
- if (mapInfo.writeCompletionStatus && objective.completionStatus !== CompletionStatus.UNKNOWN) {
9932
- globalObjective.completionStatus = objective.completionStatus;
9933
- globalObjective.completionStatusKnown = true;
9934
- }
9935
- if (mapInfo.writeProgressMeasure && objective.progressMeasureStatus) {
9936
- globalObjective.progressMeasure = objective.progressMeasure;
9937
- globalObjective.progressMeasureKnown = true;
9938
- }
9939
- if (mapInfo.updateAttemptData) {
9940
- this.updateActivityAttemptData(activity, globalObjective, objective);
10312
+ if (mapInfo.writeNormalizedMeasure && objective.measureStatus) {
10313
+ globalObjective.normalizedMeasure = objective.normalizedMeasure;
10314
+ globalObjective.normalizedMeasureKnown = true;
10315
+ if (mapInfo.writeSatisfiedStatus && objective.satisfiedByMeasure) {
10316
+ const threshold = objective.minNormalizedMeasure ?? activity.scaledPassingScore ?? 0.7;
10317
+ globalObjective.satisfiedStatus = objective.normalizedMeasure >= threshold;
10318
+ globalObjective.satisfiedStatusKnown = true;
10319
+ }
10320
+ }
10321
+ if (mapInfo.writeCompletionStatus && objective.completionStatus !== CompletionStatus.UNKNOWN) {
10322
+ globalObjective.completionStatus = objective.completionStatus;
10323
+ globalObjective.completionStatusKnown = true;
10324
+ }
10325
+ if (mapInfo.writeRawScore && objective.rawScoreKnown) {
10326
+ globalObjective.rawScore = objective.rawScore;
10327
+ globalObjective.rawScoreKnown = true;
10328
+ }
10329
+ if (mapInfo.writeMinScore && objective.minScoreKnown) {
10330
+ globalObjective.minScore = objective.minScore;
10331
+ globalObjective.minScoreKnown = true;
10332
+ }
10333
+ if (mapInfo.writeMaxScore && objective.maxScoreKnown) {
10334
+ globalObjective.maxScore = objective.maxScore;
10335
+ globalObjective.maxScoreKnown = true;
10336
+ }
10337
+ if (mapInfo.writeProgressMeasure && objective.progressMeasureStatus) {
10338
+ globalObjective.progressMeasure = objective.progressMeasure;
10339
+ globalObjective.progressMeasureKnown = true;
10340
+ }
10341
+ if (mapInfo.updateAttemptData) {
10342
+ this.updateActivityAttemptData(activity, globalObjective, objective);
10343
+ }
9941
10344
  }
9942
10345
  this.eventCallback?.("objective_synchronized", {
9943
10346
  activityId: activity.id,
@@ -9955,6 +10358,50 @@ class GlobalObjectiveSynchronizer {
9955
10358
  });
9956
10359
  }
9957
10360
  }
10361
+ /**
10362
+ * Project a global objective through one local objective's read mapInfo.
10363
+ *
10364
+ * @spec SCORM 2004 4th Ed. SN 3.10.3 - read maps provide access to mapped global objective fields
10365
+ * @spec SCORM 2004 4th Ed. ADLSEQ objectives extension - raw/min/max score read maps are independent fields
10366
+ */
10367
+ static getGlobalObjectiveReadState(activity, objective, mapInfo, globalObjective) {
10368
+ const readState = {};
10369
+ if (mapInfo.readSatisfiedStatus && globalObjective.satisfiedStatusKnown) {
10370
+ readState.satisfiedStatus = globalObjective.satisfiedStatus;
10371
+ }
10372
+ if (mapInfo.readNormalizedMeasure && globalObjective.normalizedMeasureKnown) {
10373
+ readState.normalizedMeasure = globalObjective.normalizedMeasure;
10374
+ if (objective.satisfiedByMeasure) {
10375
+ const threshold = objective.minNormalizedMeasure ?? activity.scaledPassingScore ?? 0.7;
10376
+ readState.satisfiedStatus = globalObjective.normalizedMeasure >= threshold;
10377
+ }
10378
+ }
10379
+ if (mapInfo.readCompletionStatus && globalObjective.completionStatusKnown) {
10380
+ readState.completionStatus = globalObjective.completionStatus;
10381
+ }
10382
+ if (mapInfo.readProgressMeasure && globalObjective.progressMeasureKnown) {
10383
+ readState.progressMeasure = globalObjective.progressMeasure;
10384
+ }
10385
+ if (mapInfo.readRawScore && globalObjective.rawScoreKnown) {
10386
+ readState.rawScore = globalObjective.rawScore;
10387
+ }
10388
+ if (mapInfo.readMinScore && globalObjective.minScoreKnown) {
10389
+ readState.minScore = globalObjective.minScore;
10390
+ }
10391
+ if (mapInfo.readMaxScore && globalObjective.maxScoreKnown) {
10392
+ readState.maxScore = globalObjective.maxScore;
10393
+ }
10394
+ return readState;
10395
+ }
10396
+ /**
10397
+ * Apply read-mapped state to an objective without marking those fields dirty.
10398
+ *
10399
+ * @spec SCORM 2004 4th Ed. SN 3.10.3 - read maps are access to global state, not local writes
10400
+ * @spec SCORM 2004 4th Ed. ADLSEQ objectives extension - score read maps do not imply score writes
10401
+ */
10402
+ applyGlobalObjectiveReadState(objective, readState) {
10403
+ objective.applyReadMappedState(readState);
10404
+ }
9958
10405
  /**
9959
10406
  * Ensure global objective entry exists
9960
10407
  *
@@ -9962,19 +10409,30 @@ class GlobalObjectiveSynchronizer {
9962
10409
  * @param targetId - Target objective ID
9963
10410
  * @param objective - Source objective
9964
10411
  * @returns The global objective entry
10412
+ *
10413
+ * @spec SCORM 2004 4th Ed. SN 3.10.3 - global objective entries hold mapped objective state
10414
+ * @spec SCORM 2004 4th Ed. ADLSEQ objectives extension - global entries hold score-map state
9965
10415
  */
9966
10416
  ensureGlobalObjectiveEntry(globalObjectives, targetId, objective) {
9967
10417
  if (!globalObjectives.has(targetId)) {
9968
10418
  globalObjectives.set(targetId, {
9969
10419
  id: targetId,
9970
- satisfiedStatus: objective.satisfiedStatus,
9971
- satisfiedStatusKnown: objective.satisfiedStatusKnown,
9972
- normalizedMeasure: objective.normalizedMeasure,
9973
- normalizedMeasureKnown: objective.measureStatus,
9974
- progressMeasure: objective.progressMeasure,
9975
- progressMeasureKnown: objective.progressMeasureStatus,
9976
- completionStatus: objective.completionStatus,
9977
- completionStatusKnown: objective.completionStatus !== CompletionStatus.UNKNOWN,
10420
+ satisfiedStatus: false,
10421
+ satisfiedStatusKnown: false,
10422
+ normalizedMeasure: 0,
10423
+ normalizedMeasureKnown: false,
10424
+ // @spec SCORM 2004 4th Ed. ADLSEQ objectives extension - score fields
10425
+ // remain unknown until their corresponding write map explicitly writes them.
10426
+ rawScore: "",
10427
+ rawScoreKnown: false,
10428
+ minScore: "",
10429
+ minScoreKnown: false,
10430
+ maxScore: "",
10431
+ maxScoreKnown: false,
10432
+ progressMeasure: 0,
10433
+ progressMeasureKnown: false,
10434
+ completionStatus: CompletionStatus.UNKNOWN,
10435
+ completionStatusKnown: false,
9978
10436
  satisfiedByMeasure: objective.satisfiedByMeasure,
9979
10437
  minNormalizedMeasure: objective.minNormalizedMeasure
9980
10438
  });
@@ -9989,6 +10447,9 @@ class GlobalObjectiveSynchronizer {
9989
10447
  *
9990
10448
  * @param objective - The objective to create default map info for
9991
10449
  * @returns Default map info
10450
+ *
10451
+ * @spec SCORM 2004 4th Ed. SN 3.10.3 - mapInfo defaults are applied before objective synchronization
10452
+ * @spec SCORM 2004 4th Ed. ADLSEQ objectives extension - score maps require explicit read/write flags
9992
10453
  */
9993
10454
  createDefaultMapInfo(objective) {
9994
10455
  return {
@@ -10001,9 +10462,34 @@ class GlobalObjectiveSynchronizer {
10001
10462
  writeCompletionStatus: true,
10002
10463
  readProgressMeasure: false,
10003
10464
  writeProgressMeasure: true,
10465
+ readRawScore: false,
10466
+ writeRawScore: false,
10467
+ readMinScore: false,
10468
+ writeMinScore: false,
10469
+ readMaxScore: false,
10470
+ writeMaxScore: false,
10004
10471
  updateAttemptData: objective.isPrimary
10005
10472
  };
10006
10473
  }
10474
+ /**
10475
+ * Return whether the local objective has known satisfaction state to write.
10476
+ *
10477
+ * @spec SCORM 2004 4th Ed. SN 4.2.1 Tracking Model - Objective Progress
10478
+ * Status identifies whether Objective Satisfied Status is known; Objective
10479
+ * Measure Status is independent measure knowledge.
10480
+ */
10481
+ hasKnownSatisfiedStatus(objective) {
10482
+ return objective.progressStatus || objective.satisfiedStatusKnown;
10483
+ }
10484
+ /**
10485
+ * Return whether this activity is allowed to write tracked state to globals.
10486
+ *
10487
+ * @spec SCORM 2004 4th Ed. SN 3.13.1 Tracked - when False, the LMS
10488
+ * "does not initialize, manage or access any tracking status information".
10489
+ */
10490
+ canWriteGlobalObjectives(activity) {
10491
+ return activity.sequencingControls.tracked !== false;
10492
+ }
10007
10493
  /**
10008
10494
  * Get local objective state
10009
10495
  *
@@ -10011,6 +10497,9 @@ class GlobalObjectiveSynchronizer {
10011
10497
  * @param objective - The objective
10012
10498
  * @param isPrimary - Whether this is the primary objective
10013
10499
  * @returns Local objective state
10500
+ *
10501
+ * @spec SCORM 2004 4th Ed. SN 3.10 Objective Description - local objective state used for synchronization
10502
+ * @spec SCORM 2004 4th Ed. ADLSEQ objectives extension - local score fields are part of mapped objective state
10014
10503
  */
10015
10504
  getLocalObjectiveState(activity, objective, isPrimary) {
10016
10505
  if (isPrimary) {
@@ -10019,6 +10508,12 @@ class GlobalObjectiveSynchronizer {
10019
10508
  satisfiedStatus: activity.objectiveSatisfiedStatus,
10020
10509
  measureStatus: activity.objectiveMeasureStatus,
10021
10510
  normalizedMeasure: activity.objectiveNormalizedMeasure,
10511
+ rawScore: objective.rawScore,
10512
+ rawScoreKnown: objective.rawScoreKnown,
10513
+ minScore: objective.minScore,
10514
+ minScoreKnown: objective.minScoreKnown,
10515
+ maxScore: objective.maxScore,
10516
+ maxScoreKnown: objective.maxScoreKnown,
10022
10517
  progressMeasure: activity.progressMeasure,
10023
10518
  progressMeasureStatus: activity.progressMeasureStatus,
10024
10519
  completionStatus: activity.completionStatus,
@@ -10030,6 +10525,12 @@ class GlobalObjectiveSynchronizer {
10030
10525
  satisfiedStatus: objective.satisfiedStatus,
10031
10526
  measureStatus: objective.measureStatus,
10032
10527
  normalizedMeasure: objective.normalizedMeasure,
10528
+ rawScore: objective.rawScore,
10529
+ rawScoreKnown: objective.rawScoreKnown,
10530
+ minScore: objective.minScore,
10531
+ minScoreKnown: objective.minScoreKnown,
10532
+ maxScore: objective.maxScore,
10533
+ maxScoreKnown: objective.maxScoreKnown,
10033
10534
  progressMeasure: objective.progressMeasure,
10034
10535
  progressMeasureStatus: objective.progressMeasureStatus,
10035
10536
  completionStatus: objective.completionStatus,
@@ -10052,7 +10553,7 @@ class GlobalObjectiveSynchronizer {
10052
10553
  (rule) => rule.action === "completed" || rule.action === "incomplete"
10053
10554
  );
10054
10555
  if (globalObjective.satisfiedStatusKnown && globalObjective.satisfiedStatus) {
10055
- if (!hasCompletionRollupRules && (activity.completionStatus === CompletionStatus.UNKNOWN || activity.completionStatus === CompletionStatus.INCOMPLETE)) {
10556
+ if (!hasCompletionRollupRules && !activity.sequencingControls.completionSetByContent && !activity.attemptProgressStatus && (activity.completionStatus === CompletionStatus.UNKNOWN || activity.completionStatus === CompletionStatus.INCOMPLETE)) {
10056
10557
  activity.completionStatus = CompletionStatus.COMPLETED;
10057
10558
  }
10058
10559
  if (activity.successStatus === "unknown") {
@@ -10426,6 +10927,26 @@ class RollupProcess {
10426
10927
  }
10427
10928
  }
10428
10929
 
10930
+ function evaluateCompletionStatusFromThreshold({
10931
+ completionThreshold,
10932
+ progressMeasure,
10933
+ storedCompletionStatus
10934
+ }) {
10935
+ if (completionThreshold !== "" && completionThreshold !== null && completionThreshold !== void 0) {
10936
+ const thresholdValue = parseFloat(String(completionThreshold));
10937
+ if (!isNaN(thresholdValue)) {
10938
+ if (progressMeasure !== "" && progressMeasure !== null && progressMeasure !== void 0) {
10939
+ const progressValue = parseFloat(String(progressMeasure));
10940
+ if (!isNaN(progressValue)) {
10941
+ return progressValue >= thresholdValue ? CompletionStatus.COMPLETED : CompletionStatus.INCOMPLETE;
10942
+ }
10943
+ }
10944
+ return CompletionStatus.UNKNOWN;
10945
+ }
10946
+ }
10947
+ return storedCompletionStatus || CompletionStatus.UNKNOWN;
10948
+ }
10949
+
10429
10950
  const VALID_COMPLETION_STATUSES = [
10430
10951
  CompletionStatus.COMPLETED,
10431
10952
  CompletionStatus.INCOMPLETE,
@@ -10478,12 +10999,43 @@ class RteDataTransferService {
10478
10999
  * Transfer primary objective data from CMI to activity
10479
11000
  * @param {Activity} activity - The activity to transfer data to
10480
11001
  * @param {CMIDataForTransfer} cmiData - CMI data from runtime
11002
+ *
11003
+ * @spec SCORM 2004 4th Ed. RTE-to-SN Data Transfer - primary objective status and score data
11004
+ * @spec SCORM 2004 4th Ed. ADLSEQ objectives extension - raw/min/max score write-map source data
10481
11005
  */
10482
11006
  transferPrimaryObjective(activity, cmiData) {
10483
- const validatedCompletionStatus = validateCompletionStatus(cmiData.completion_status);
10484
- if (validatedCompletionStatus && validatedCompletionStatus !== CompletionStatus.UNKNOWN) {
10485
- activity.completionStatus = validatedCompletionStatus;
10486
- activity.attemptProgressStatus = true;
11007
+ let hasProgressMeasure = false;
11008
+ if (cmiData.progress_measure && cmiData.progress_measure !== "") {
11009
+ const progressMeasure = parseFloat(cmiData.progress_measure);
11010
+ if (!isNaN(progressMeasure)) {
11011
+ hasProgressMeasure = true;
11012
+ activity.progressMeasure = progressMeasure;
11013
+ activity.progressMeasureStatus = true;
11014
+ activity.attemptCompletionAmount = progressMeasure;
11015
+ activity.attemptCompletionAmountStatus = true;
11016
+ if (activity.primaryObjective) {
11017
+ activity.primaryObjective.progressMeasure = progressMeasure;
11018
+ activity.primaryObjective.progressMeasureStatus = true;
11019
+ }
11020
+ }
11021
+ }
11022
+ if (!hasProgressMeasure) {
11023
+ activity.attemptCompletionAmountStatus = false;
11024
+ }
11025
+ if (activity.completedByMeasure) {
11026
+ const completionStatus = evaluateCompletionStatusFromThreshold({
11027
+ completionThreshold: activity.minProgressMeasure,
11028
+ progressMeasure: cmiData.progress_measure,
11029
+ storedCompletionStatus: CompletionStatus.UNKNOWN
11030
+ });
11031
+ activity.completionStatus = completionStatus;
11032
+ activity.attemptProgressStatus = completionStatus !== CompletionStatus.UNKNOWN;
11033
+ } else {
11034
+ const validatedCompletionStatus = validateCompletionStatus(cmiData.completion_status);
11035
+ if (validatedCompletionStatus && validatedCompletionStatus !== CompletionStatus.UNKNOWN) {
11036
+ activity.completionStatus = validatedCompletionStatus;
11037
+ activity.attemptProgressStatus = true;
11038
+ }
10487
11039
  }
10488
11040
  let hasSuccessStatus = false;
10489
11041
  let successStatus = false;
@@ -10496,9 +11048,9 @@ class RteDataTransferService {
10496
11048
  activity.objectiveSatisfiedStatus = successStatus;
10497
11049
  activity.objectiveSatisfiedStatusKnown = true;
10498
11050
  activity.successStatus = validatedSuccessStatus;
10499
- activity.objectiveMeasureStatus = true;
10500
11051
  }
10501
11052
  if (cmiData.score) {
11053
+ activity.primaryObjective?.initializeScoreFromCMI(this.getObjectiveScoreState(cmiData.score));
10502
11054
  const normalized = this.normalizeScore(cmiData.score);
10503
11055
  if (normalized !== null) {
10504
11056
  normalizedScore = normalized;
@@ -10510,30 +11062,25 @@ class RteDataTransferService {
10510
11062
  if (activity.primaryObjective && (hasSuccessStatus || hasNormalizedMeasure)) {
10511
11063
  const finalStatus = hasSuccessStatus ? successStatus : activity.primaryObjective.satisfiedStatus;
10512
11064
  const finalMeasure = hasNormalizedMeasure ? normalizedScore : activity.primaryObjective.normalizedMeasure;
10513
- const measureStatus = hasSuccessStatus || hasNormalizedMeasure;
11065
+ const measureStatus = hasNormalizedMeasure;
10514
11066
  activity.primaryObjective.initializeFromCMI(finalStatus, finalMeasure, measureStatus);
10515
11067
  if (hasSuccessStatus) {
10516
11068
  activity.primaryObjective.satisfiedStatusKnown = true;
10517
11069
  activity.primaryObjective.progressStatus = true;
10518
11070
  }
10519
11071
  }
10520
- if (cmiData.progress_measure && cmiData.progress_measure !== "") {
10521
- const progressMeasure = parseFloat(cmiData.progress_measure);
10522
- if (!isNaN(progressMeasure)) {
10523
- activity.progressMeasure = progressMeasure;
10524
- activity.progressMeasureStatus = true;
10525
- if (activity.primaryObjective) {
10526
- activity.primaryObjective.progressMeasure = progressMeasure;
10527
- activity.primaryObjective.progressMeasureStatus = true;
10528
- }
10529
- }
10530
- }
10531
11072
  }
10532
11073
  /**
10533
- * Transfer non-primary objective data from CMI to activity objectives
10534
- * Only transfers changed values to protect global objectives
11074
+ * Transfer objective-array data from CMI to matching activity objectives.
11075
+ * Only transfers changed values to protect global objectives.
10535
11076
  * @param {Activity} activity - The activity to transfer data to
10536
11077
  * @param {CMIDataForTransfer} cmiData - CMI data from runtime
11078
+ *
11079
+ * @spec SCORM 2004 4th Ed. RTE 4.2.17 - cmi.objectives.n data, including
11080
+ * the primary objective entry, is part of the RTE objective data model.
11081
+ * @spec SCORM 2004 4th Ed. SN 3.10.3 and ADLSEQ objectives extension -
11082
+ * mapped objective status and raw/min/max score data transfer through
11083
+ * objective maps to sequencing state.
10537
11084
  */
10538
11085
  transferNonPrimaryObjectives(activity, cmiData) {
10539
11086
  if (!cmiData.objectives || cmiData.objectives.length === 0) {
@@ -10544,14 +11091,17 @@ class RteDataTransferService {
10544
11091
  continue;
10545
11092
  }
10546
11093
  const activityObjectiveMatch = activity.getObjectiveById(cmiObjective.id);
10547
- if (!activityObjectiveMatch || activityObjectiveMatch.isPrimary) {
11094
+ if (!activityObjectiveMatch) {
10548
11095
  continue;
10549
11096
  }
10550
11097
  const activityObjective = activityObjectiveMatch.objective;
11098
+ const isPrimaryObjective = activityObjectiveMatch.isPrimary;
10551
11099
  let hasSuccessStatus = false;
10552
11100
  let successStatus = false;
10553
11101
  let hasNormalizedMeasure = false;
10554
11102
  let normalizedScore = 0;
11103
+ let hasCompletionStatus = false;
11104
+ let hasProgressMeasure = false;
10555
11105
  const validatedObjSuccessStatus = validateSuccessStatus(cmiObjective.success_status);
10556
11106
  if (validatedObjSuccessStatus && validatedObjSuccessStatus !== SuccessStatus.UNKNOWN) {
10557
11107
  successStatus = validatedObjSuccessStatus === SuccessStatus.PASSED;
@@ -10561,8 +11111,10 @@ class RteDataTransferService {
10561
11111
  const validatedObjCompletionStatus = validateCompletionStatus(cmiObjective.completion_status);
10562
11112
  if (validatedObjCompletionStatus && validatedObjCompletionStatus !== CompletionStatus.UNKNOWN) {
10563
11113
  activityObjective.completionStatus = validatedObjCompletionStatus;
11114
+ hasCompletionStatus = true;
10564
11115
  }
10565
11116
  if (cmiObjective.score) {
11117
+ activityObjective.initializeScoreFromCMI(this.getObjectiveScoreState(cmiObjective.score));
10566
11118
  const normalized = this.normalizeScore(cmiObjective.score);
10567
11119
  if (normalized !== null) {
10568
11120
  normalizedScore = normalized;
@@ -10574,12 +11126,29 @@ class RteDataTransferService {
10574
11126
  const finalMeasure = hasNormalizedMeasure ? normalizedScore : activityObjective.normalizedMeasure;
10575
11127
  const measureStatus = hasNormalizedMeasure;
10576
11128
  activityObjective.initializeFromCMI(finalStatus, finalMeasure, measureStatus);
11129
+ if (hasSuccessStatus) {
11130
+ activityObjective.satisfiedStatusKnown = true;
11131
+ }
10577
11132
  }
10578
11133
  if (cmiObjective.progress_measure && cmiObjective.progress_measure !== "") {
10579
11134
  const progressMeasure = parseFloat(cmiObjective.progress_measure);
10580
11135
  if (!isNaN(progressMeasure)) {
10581
11136
  activityObjective.progressMeasure = progressMeasure;
10582
11137
  activityObjective.progressMeasureStatus = true;
11138
+ hasProgressMeasure = true;
11139
+ }
11140
+ }
11141
+ if (isPrimaryObjective && (hasSuccessStatus || hasNormalizedMeasure || hasCompletionStatus || hasProgressMeasure)) {
11142
+ activityObjective.applyToActivity(activity);
11143
+ if (validatedObjSuccessStatus && validatedObjSuccessStatus !== SuccessStatus.UNKNOWN) {
11144
+ activity.successStatus = validatedObjSuccessStatus;
11145
+ }
11146
+ if (hasCompletionStatus) {
11147
+ activity.attemptProgressStatus = true;
11148
+ }
11149
+ if (hasProgressMeasure) {
11150
+ activity.attemptCompletionAmount = activityObjective.progressMeasure;
11151
+ activity.attemptCompletionAmountStatus = true;
10583
11152
  }
10584
11153
  }
10585
11154
  }
@@ -10608,6 +11177,25 @@ class RteDataTransferService {
10608
11177
  }
10609
11178
  return null;
10610
11179
  }
11180
+ /**
11181
+ * Convert RTE score data into objective score-map state without numeric reformatting.
11182
+ *
11183
+ * @spec SCORM 2004 4th Ed. RTE-to-SN Data Transfer - score values transfer from RTE to sequencing state
11184
+ * @spec SCORM 2004 4th Ed. ADLSEQ objectives extension - raw/min/max score map fields are independent
11185
+ */
11186
+ getObjectiveScoreState(score) {
11187
+ const scoreState = {};
11188
+ if (score.raw !== void 0) {
11189
+ scoreState.rawScore = score.raw;
11190
+ }
11191
+ if (score.min !== void 0) {
11192
+ scoreState.minScore = score.min;
11193
+ }
11194
+ if (score.max !== void 0) {
11195
+ scoreState.maxScore = score.max;
11196
+ }
11197
+ return scoreState;
11198
+ }
10611
11199
  }
10612
11200
 
10613
11201
  class TerminationHandler {
@@ -10732,6 +11320,18 @@ class TerminationHandler {
10732
11320
  this.endAttempt(this.activityTree.currentActivity);
10733
11321
  }
10734
11322
  }
11323
+ const ancestorExitResult = this.applyAncestorExitActionRules(currentActivity);
11324
+ if (ancestorExitResult.action === "EXIT_ALL") {
11325
+ return this.handleExitAll(ancestorExitResult.activity || currentActivity);
11326
+ }
11327
+ if (ancestorExitResult.exception) {
11328
+ return {
11329
+ terminationRequest: SequencingRequestType.EXIT,
11330
+ sequencingRequest: null,
11331
+ exception: ancestorExitResult.exception,
11332
+ valid: false
11333
+ };
11334
+ }
10735
11335
  let processedExit;
10736
11336
  let postConditionResult;
10737
11337
  do {
@@ -10957,18 +11557,10 @@ class TerminationHandler {
10957
11557
  recursionDepth++;
10958
11558
  const exitRules = activity.sequencingRules.exitConditionRules;
10959
11559
  for (const rule of exitRules) {
10960
- let conditionsMet;
10961
- if (rule.conditionCombination === "all") {
10962
- conditionsMet = rule.conditions.every(
10963
- (condition) => condition.evaluate(activity)
10964
- );
10965
- } else {
10966
- conditionsMet = rule.conditions.some(
10967
- (condition) => condition.evaluate(activity)
10968
- );
10969
- }
10970
- if (conditionsMet) {
10971
- if (rule.action === RuleActionType.EXIT_PARENT) {
11560
+ if (rule.evaluate(activity)) {
11561
+ if (rule.action === RuleActionType.EXIT) {
11562
+ return { action: "EXIT", recursionDepth };
11563
+ } else if (rule.action === RuleActionType.EXIT_PARENT) {
10972
11564
  return { action: "EXIT_PARENT", recursionDepth };
10973
11565
  } else if (rule.action === RuleActionType.EXIT_ALL) {
10974
11566
  return { action: "EXIT_ALL", recursionDepth };
@@ -11002,6 +11594,55 @@ class TerminationHandler {
11002
11594
  this.processExitAtLevel(rootActivity, 0);
11003
11595
  this.terminateAll(rootActivity);
11004
11596
  }
11597
+ /**
11598
+ * Sequencing Exit Action Rules Subprocess (TB.2.1) ancestor walk
11599
+ * @spec SN Book: TB.2.1 (Sequencing Exit Action Rules Subprocess)
11600
+ * @param {Activity} terminatingActivity - The activity whose attempt just ended
11601
+ * @return {{action: string | null, activity?: Activity, exception?: string}}
11602
+ */
11603
+ applyAncestorExitActionRules(terminatingActivity) {
11604
+ const activityPath = [];
11605
+ let current = terminatingActivity.parent;
11606
+ while (current) {
11607
+ activityPath.unshift(current);
11608
+ current = current.parent;
11609
+ }
11610
+ for (const activity of activityPath) {
11611
+ const exitAction = this.exitActionRulesSubprocess(activity);
11612
+ if (exitAction === "EXIT_ALL") {
11613
+ this.fireEvent("onAncestorExitAction", {
11614
+ activity: activity.id,
11615
+ action: exitAction
11616
+ });
11617
+ return { action: "EXIT_ALL", activity };
11618
+ }
11619
+ if (exitAction === "EXIT_PARENT") {
11620
+ if (!activity.parent) {
11621
+ return { action: "EXIT_PARENT", activity, exception: "TB.2.3-4" };
11622
+ }
11623
+ this.terminateDescendants(activity.parent);
11624
+ this.activityTree.currentActivity = activity.parent;
11625
+ this.endAttempt(activity.parent);
11626
+ this.fireEvent("onAncestorExitAction", {
11627
+ activity: activity.id,
11628
+ action: exitAction,
11629
+ currentActivity: activity.parent.id
11630
+ });
11631
+ return { action: "EXIT_PARENT", activity: activity.parent };
11632
+ }
11633
+ if (exitAction === "EXIT") {
11634
+ this.terminateDescendants(activity);
11635
+ this.activityTree.currentActivity = activity;
11636
+ this.endAttempt(activity);
11637
+ this.fireEvent("onAncestorExitAction", {
11638
+ activity: activity.id,
11639
+ action: exitAction
11640
+ });
11641
+ return { action: "EXIT", activity };
11642
+ }
11643
+ }
11644
+ return { action: null };
11645
+ }
11005
11646
  /**
11006
11647
  * Process exit actions at specific level
11007
11648
  * @param {Activity} activity - Activity to process
@@ -11082,18 +11723,10 @@ class TerminationHandler {
11082
11723
  exitActionRulesSubprocess(activity) {
11083
11724
  const exitRules = activity.sequencingRules.exitConditionRules;
11084
11725
  for (const rule of exitRules) {
11085
- let conditionsMet;
11086
- if (rule.conditionCombination === "all") {
11087
- conditionsMet = rule.conditions.every(
11088
- (condition) => condition.evaluate(activity)
11089
- );
11090
- } else {
11091
- conditionsMet = rule.conditions.some(
11092
- (condition) => condition.evaluate(activity)
11093
- );
11094
- }
11095
- if (conditionsMet) {
11096
- if (rule.action === RuleActionType.EXIT_PARENT) {
11726
+ if (rule.evaluate(activity)) {
11727
+ if (rule.action === RuleActionType.EXIT) {
11728
+ return "EXIT";
11729
+ } else if (rule.action === RuleActionType.EXIT_PARENT) {
11097
11730
  return "EXIT_PARENT";
11098
11731
  } else if (rule.action === RuleActionType.EXIT_ALL) {
11099
11732
  return "EXIT_ALL";
@@ -12609,6 +13242,9 @@ class GlobalObjectiveService {
12609
13242
  * Collect Global Objectives
12610
13243
  * Recursively collects global objectives from the activity tree
12611
13244
  * @param {Activity} activity - Activity to collect objectives from
13245
+ *
13246
+ * @spec SCORM 2004 4th Ed. SN 3.10.3 - global objective map contains mapped objective state
13247
+ * @spec SCORM 2004 4th Ed. ADLSEQ objectives extension - raw/min/max score fields are unknown until written
12612
13248
  */
12613
13249
  collectObjectives(activity) {
12614
13250
  const objectives = activity.getAllObjectives();
@@ -12618,9 +13254,15 @@ class GlobalObjectiveService {
12618
13254
  this.globalObjectiveMap.set(defaultId, {
12619
13255
  id: defaultId,
12620
13256
  satisfiedStatus: activity.objectiveSatisfiedStatus,
12621
- satisfiedStatusKnown: activity.objectiveMeasureStatus,
13257
+ satisfiedStatusKnown: activity.objectiveSatisfiedStatusKnown,
12622
13258
  normalizedMeasure: activity.objectiveNormalizedMeasure,
12623
13259
  normalizedMeasureKnown: activity.objectiveMeasureStatus,
13260
+ rawScore: "",
13261
+ rawScoreKnown: false,
13262
+ minScore: "",
13263
+ minScoreKnown: false,
13264
+ maxScore: "",
13265
+ maxScoreKnown: false,
12624
13266
  progressMeasure: activity.progressMeasure,
12625
13267
  progressMeasureKnown: activity.progressMeasureStatus,
12626
13268
  completionStatus: activity.completionStatus,
@@ -12633,6 +13275,12 @@ class GlobalObjectiveService {
12633
13275
  writeCompletionStatus: true,
12634
13276
  readProgressMeasure: true,
12635
13277
  writeProgressMeasure: true,
13278
+ readRawScore: false,
13279
+ writeRawScore: false,
13280
+ readMinScore: false,
13281
+ writeMinScore: false,
13282
+ readMaxScore: false,
13283
+ writeMaxScore: false,
12636
13284
  satisfiedByMeasure: activity.scaledPassingScore !== null,
12637
13285
  minNormalizedMeasure: activity.scaledPassingScore,
12638
13286
  updateAttemptData: true
@@ -12660,9 +13308,15 @@ class GlobalObjectiveService {
12660
13308
  this.globalObjectiveMap.set(targetId, {
12661
13309
  id: targetId,
12662
13310
  satisfiedStatus: objective.satisfiedStatus,
12663
- satisfiedStatusKnown: objective.measureStatus,
13311
+ satisfiedStatusKnown: objective.satisfiedStatusKnown || objective.progressStatus,
12664
13312
  normalizedMeasure: objective.normalizedMeasure,
12665
13313
  normalizedMeasureKnown: objective.measureStatus,
13314
+ rawScore: "",
13315
+ rawScoreKnown: false,
13316
+ minScore: "",
13317
+ minScoreKnown: false,
13318
+ maxScore: "",
13319
+ maxScoreKnown: false,
12666
13320
  progressMeasure: objective.progressMeasure,
12667
13321
  progressMeasureKnown: objective.progressMeasureStatus,
12668
13322
  completionStatus: objective.completionStatus,
@@ -14645,27 +15299,44 @@ class SequencingService {
14645
15299
  }
14646
15300
  /**
14647
15301
  * Update activity properties from current CMI values
15302
+ *
15303
+ * @spec SCORM 2004 4th Ed. RTE-to-SN Data Transfer - current CMI values update activity objective state
15304
+ * @spec SCORM 2004 4th Ed. ADLSEQ objectives extension - raw/min/max scores are available to objective write maps
14648
15305
  */
14649
15306
  updateActivityFromCMI(activity) {
14650
- if (this.cmi.completion_status !== "unknown") {
15307
+ let hasProgressMeasure = false;
15308
+ if (this.cmi.progress_measure !== "") {
15309
+ const progressMeasure = parseFloat(this.cmi.progress_measure);
15310
+ if (!isNaN(progressMeasure)) {
15311
+ hasProgressMeasure = true;
15312
+ activity.progressMeasure = progressMeasure;
15313
+ activity.progressMeasureStatus = true;
15314
+ activity.attemptCompletionAmount = progressMeasure;
15315
+ activity.attemptCompletionAmountStatus = true;
15316
+ }
15317
+ }
15318
+ if (!hasProgressMeasure) {
15319
+ activity.attemptCompletionAmountStatus = false;
15320
+ }
15321
+ if (activity.completedByMeasure) {
15322
+ const completionStatus = evaluateCompletionStatusFromThreshold({
15323
+ completionThreshold: activity.minProgressMeasure,
15324
+ progressMeasure: this.cmi.progress_measure,
15325
+ storedCompletionStatus: CompletionStatus.UNKNOWN
15326
+ });
15327
+ activity.completionStatus = completionStatus;
15328
+ activity.attemptProgressStatus = completionStatus !== CompletionStatus.UNKNOWN;
15329
+ } else if (this.cmi.completion_status !== "unknown") {
14651
15330
  activity.completionStatus = this.cmi.completion_status;
14652
15331
  activity.attemptProgressStatus = true;
14653
15332
  }
14654
15333
  if (this.cmi.success_status !== "unknown") {
14655
15334
  activity.successStatus = this.cmi.success_status;
14656
15335
  activity.objectiveSatisfiedStatus = this.cmi.success_status === "passed";
14657
- activity.objectiveMeasureStatus = true;
14658
15336
  if (activity.primaryObjective) {
14659
15337
  activity.primaryObjective.progressStatus = true;
14660
15338
  }
14661
15339
  }
14662
- if (this.cmi.progress_measure !== "") {
14663
- const progressMeasure = parseFloat(this.cmi.progress_measure);
14664
- if (!isNaN(progressMeasure)) {
14665
- activity.progressMeasure = progressMeasure;
14666
- activity.progressMeasureStatus = true;
14667
- }
14668
- }
14669
15340
  if (this.cmi.score && this.cmi.score.scaled !== "") {
14670
15341
  const scaledScore = parseFloat(this.cmi.score.scaled);
14671
15342
  if (!isNaN(scaledScore)) {
@@ -14676,6 +15347,17 @@ class SequencingService {
14676
15347
  }
14677
15348
  }
14678
15349
  }
15350
+ if (activity.primaryObjective && this.cmi.score) {
15351
+ if (this.cmi.score.raw !== "") {
15352
+ activity.primaryObjective.rawScore = this.cmi.score.raw;
15353
+ }
15354
+ if (this.cmi.score.min !== "") {
15355
+ activity.primaryObjective.minScore = this.cmi.score.min;
15356
+ }
15357
+ if (this.cmi.score.max !== "") {
15358
+ activity.primaryObjective.maxScore = this.cmi.score.max;
15359
+ }
15360
+ }
14679
15361
  if (activity.primaryObjective) {
14680
15362
  activity.primaryObjective.updateFromActivity(activity);
14681
15363
  }
@@ -17188,7 +17870,7 @@ class CMIInteractionsObject extends BaseCMI {
17188
17870
  if (check2004ValidFormat(
17189
17871
  this._cmi_element + ".description",
17190
17872
  description,
17191
- scorm2004_regex.CMILangString250,
17873
+ scorm2004_regex.CMILangString,
17192
17874
  true
17193
17875
  )) {
17194
17876
  this._description = description;
@@ -18092,7 +18774,7 @@ class CMIObjectivesObject extends BaseCMI {
18092
18774
  if (check2004ValidFormat(
18093
18775
  this._cmi_element + ".description",
18094
18776
  description,
18095
- scorm2004_regex.CMILangString250,
18777
+ scorm2004_regex.CMILangString,
18096
18778
  true
18097
18779
  )) {
18098
18780
  this._description = description;
@@ -20823,22 +21505,11 @@ class Scorm2004CMIHandler {
20823
21505
  * @returns {string} The evaluated completion status
20824
21506
  */
20825
21507
  evaluateCompletionStatus() {
20826
- const threshold = this.context.cmi.completion_threshold;
20827
- const progressMeasure = this.context.cmi.progress_measure;
20828
- const storedStatus = this.context.cmi.completion_status;
20829
- if (threshold !== "" && threshold !== null && threshold !== void 0) {
20830
- const thresholdValue = parseFloat(String(threshold));
20831
- if (!isNaN(thresholdValue)) {
20832
- if (progressMeasure !== "" && progressMeasure !== null && progressMeasure !== void 0) {
20833
- const progressValue = parseFloat(String(progressMeasure));
20834
- if (!isNaN(progressValue)) {
20835
- return progressValue >= thresholdValue ? CompletionStatus.COMPLETED : CompletionStatus.INCOMPLETE;
20836
- }
20837
- }
20838
- return CompletionStatus.UNKNOWN;
20839
- }
20840
- }
20841
- return storedStatus || CompletionStatus.UNKNOWN;
21508
+ return evaluateCompletionStatusFromThreshold({
21509
+ completionThreshold: this.context.cmi.completion_threshold,
21510
+ progressMeasure: this.context.cmi.progress_measure,
21511
+ storedCompletionStatus: this.context.cmi.completion_status
21512
+ });
20842
21513
  }
20843
21514
  /**
20844
21515
  * Evaluates success_status per SCORM 2004 RTE Table 4.2.21.1a
@@ -20946,6 +21617,9 @@ class SequencingConfigurationBuilder {
20946
21617
  if (settings.objectiveSetByContent !== void 0) {
20947
21618
  target.objectiveSetByContent = settings.objectiveSetByContent;
20948
21619
  }
21620
+ if (settings.tracked !== void 0) {
21621
+ target.tracked = settings.tracked;
21622
+ }
20949
21623
  }
20950
21624
  /**
20951
21625
  * Applies the sequencing rules settings to the specified target object.
@@ -21483,6 +22157,12 @@ class ActivityTreeBuilder {
21483
22157
  activitySettings.sequencingControls
21484
22158
  );
21485
22159
  }
22160
+ if (activitySettings.deliveryControls) {
22161
+ this.sequencingConfigBuilder.applySequencingControlsSettings(
22162
+ activity.sequencingControls,
22163
+ activitySettings.deliveryControls
22164
+ );
22165
+ }
21486
22166
  if (activitySettings.sequencingRules) {
21487
22167
  this.sequencingConfigBuilder.applySequencingRulesSettings(
21488
22168
  activity.sequencingRules,
@@ -21498,6 +22178,21 @@ class ActivityTreeBuilder {
21498
22178
  if (activitySettings.rollupConsiderations) {
21499
22179
  activity.applyRollupConsiderations(activitySettings.rollupConsiderations);
21500
22180
  }
22181
+ if (activitySettings.completionThreshold) {
22182
+ const threshold = activitySettings.completionThreshold;
22183
+ if (threshold.completedByMeasure !== void 0) {
22184
+ activity.completedByMeasure = threshold.completedByMeasure;
22185
+ }
22186
+ if (threshold.minProgressMeasure !== void 0) {
22187
+ activity.minProgressMeasure = threshold.minProgressMeasure;
22188
+ activity.completionThreshold = threshold.minProgressMeasure.toString();
22189
+ } else if (threshold.completedByMeasure) {
22190
+ activity.completionThreshold = activity.minProgressMeasure.toString();
22191
+ }
22192
+ if (threshold.progressWeight !== void 0) {
22193
+ activity.progressWeight = threshold.progressWeight;
22194
+ }
22195
+ }
21501
22196
  if (activitySettings.hideLmsUi) {
21502
22197
  const mergedHide = this.sequencingConfigBuilder.mergeHideLmsUi(
21503
22198
  activity.hideLmsUi,
@@ -21557,19 +22252,19 @@ class ActivityTreeBuilder {
21557
22252
  createActivityObjectiveFromSettings(objectiveSettings, isPrimary) {
21558
22253
  const mapInfo = (objectiveSettings.mapInfo || []).map((info) => ({
21559
22254
  targetObjectiveID: info.targetObjectiveID,
21560
- readSatisfiedStatus: info.readSatisfiedStatus ?? false,
21561
- readNormalizedMeasure: info.readNormalizedMeasure ?? false,
22255
+ readSatisfiedStatus: info.readSatisfiedStatus ?? true,
22256
+ readNormalizedMeasure: info.readNormalizedMeasure ?? true,
21562
22257
  writeSatisfiedStatus: info.writeSatisfiedStatus ?? false,
21563
22258
  writeNormalizedMeasure: info.writeNormalizedMeasure ?? false,
21564
- readCompletionStatus: info.readCompletionStatus ?? false,
22259
+ readCompletionStatus: info.readCompletionStatus ?? true,
21565
22260
  writeCompletionStatus: info.writeCompletionStatus ?? false,
21566
- readProgressMeasure: info.readProgressMeasure ?? false,
22261
+ readProgressMeasure: info.readProgressMeasure ?? true,
21567
22262
  writeProgressMeasure: info.writeProgressMeasure ?? false,
21568
- readRawScore: info.readRawScore ?? false,
22263
+ readRawScore: info.readRawScore ?? true,
21569
22264
  writeRawScore: info.writeRawScore ?? false,
21570
- readMinScore: info.readMinScore ?? false,
22265
+ readMinScore: info.readMinScore ?? true,
21571
22266
  writeMinScore: info.writeMinScore ?? false,
21572
- readMaxScore: info.readMaxScore ?? false,
22267
+ readMaxScore: info.readMaxScore ?? true,
21573
22268
  writeMaxScore: info.writeMaxScore ?? false,
21574
22269
  updateAttemptData: info.updateAttemptData ?? false
21575
22270
  }));
@@ -21751,6 +22446,9 @@ class GlobalObjectiveManager {
21751
22446
  *
21752
22447
  * @param {string} objectiveId - The global objective ID
21753
22448
  * @param {CMIObjectivesObject} objective - The CMI objective object with updated values
22449
+ *
22450
+ * @spec SCORM 2004 4th Ed. SN 3.10.3 - global objectives synchronize CMI objective data
22451
+ * @spec SCORM 2004 4th Ed. ADLSEQ objectives extension - raw/min/max score fields synchronize independently
21754
22452
  */
21755
22453
  updateGlobalObjectiveFromCMI(objectiveId, objective) {
21756
22454
  if (!objectiveId || !this.context.sequencingService) {
@@ -21776,6 +22474,18 @@ class GlobalObjectiveManager {
21776
22474
  updatePayload.normalizedMeasure = normalizedMeasure;
21777
22475
  updatePayload.normalizedMeasureKnown = true;
21778
22476
  }
22477
+ if (objective.score?.raw !== void 0 && objective.score.raw !== "") {
22478
+ updatePayload.rawScore = objective.score.raw;
22479
+ updatePayload.rawScoreKnown = true;
22480
+ }
22481
+ if (objective.score?.min !== void 0 && objective.score.min !== "") {
22482
+ updatePayload.minScore = objective.score.min;
22483
+ updatePayload.minScoreKnown = true;
22484
+ }
22485
+ if (objective.score?.max !== void 0 && objective.score.max !== "") {
22486
+ updatePayload.maxScore = objective.score.max;
22487
+ updatePayload.maxScoreKnown = true;
22488
+ }
21779
22489
  const progressMeasure = this.parseObjectiveNumber(objective.progress_measure);
21780
22490
  if (progressMeasure !== null) {
21781
22491
  updatePayload.progressMeasure = progressMeasure;
@@ -21795,12 +22505,21 @@ class GlobalObjectiveManager {
21795
22505
  *
21796
22506
  * @param {CMIObjectivesObject} objective - The CMI objectives object containing data about a specific learning objective.
21797
22507
  * @return {GlobalObjectiveMapEntry} An object containing mapped properties and their values based on the provided objective.
22508
+ *
22509
+ * @spec SCORM 2004 4th Ed. SN 3.10.3 - global objective map entries capture objective state
22510
+ * @spec SCORM 2004 4th Ed. ADLSEQ objectives extension - raw/min/max score entries carry per-field known flags
21798
22511
  */
21799
22512
  buildObjectiveMapEntryFromCMI(objective) {
21800
22513
  const entry = {
21801
22514
  id: objective.id,
21802
22515
  satisfiedStatusKnown: false,
21803
22516
  normalizedMeasureKnown: false,
22517
+ rawScore: "",
22518
+ rawScoreKnown: false,
22519
+ minScore: "",
22520
+ minScoreKnown: false,
22521
+ maxScore: "",
22522
+ maxScoreKnown: false,
21804
22523
  progressMeasureKnown: false,
21805
22524
  completionStatusKnown: false,
21806
22525
  readSatisfiedStatus: true,
@@ -21810,7 +22529,13 @@ class GlobalObjectiveManager {
21810
22529
  readCompletionStatus: true,
21811
22530
  writeCompletionStatus: true,
21812
22531
  readProgressMeasure: true,
21813
- writeProgressMeasure: true
22532
+ writeProgressMeasure: true,
22533
+ readRawScore: true,
22534
+ writeRawScore: true,
22535
+ readMinScore: true,
22536
+ writeMinScore: true,
22537
+ readMaxScore: true,
22538
+ writeMaxScore: true
21814
22539
  };
21815
22540
  if (objective.success_status && objective.success_status !== SuccessStatus.UNKNOWN) {
21816
22541
  entry.satisfiedStatus = objective.success_status === SuccessStatus.PASSED;
@@ -21821,6 +22546,18 @@ class GlobalObjectiveManager {
21821
22546
  entry.normalizedMeasure = normalizedMeasure;
21822
22547
  entry.normalizedMeasureKnown = true;
21823
22548
  }
22549
+ if (objective.score?.raw !== void 0 && objective.score.raw !== "") {
22550
+ entry.rawScore = objective.score.raw;
22551
+ entry.rawScoreKnown = true;
22552
+ }
22553
+ if (objective.score?.min !== void 0 && objective.score.min !== "") {
22554
+ entry.minScore = objective.score.min;
22555
+ entry.minScoreKnown = true;
22556
+ }
22557
+ if (objective.score?.max !== void 0 && objective.score.max !== "") {
22558
+ entry.maxScore = objective.score.max;
22559
+ entry.maxScoreKnown = true;
22560
+ }
21824
22561
  const progressMeasure = this.parseObjectiveNumber(objective.progress_measure);
21825
22562
  if (progressMeasure !== null) {
21826
22563
  entry.progressMeasure = progressMeasure;
@@ -21841,6 +22578,9 @@ class GlobalObjectiveManager {
21841
22578
  * @return {CMIObjectivesObject[]} An array of `CMIObjectivesObject` instances built
21842
22579
  * from the provided snapshot map. Returns an empty array
21843
22580
  * if the snapshot is invalid or no valid objectives can be created.
22581
+ *
22582
+ * @spec SCORM 2004 4th Ed. SN 3.10.3 - global objective snapshots restore CMI objective state
22583
+ * @spec SCORM 2004 4th Ed. ADLSEQ objectives extension - raw/min/max score known fields restore independently
21844
22584
  */
21845
22585
  buildCMIObjectivesFromMap(snapshot) {
21846
22586
  const objectives = [];
@@ -21860,6 +22600,15 @@ class GlobalObjectiveManager {
21860
22600
  if (entry.normalizedMeasureKnown === true && normalizedMeasure !== null) {
21861
22601
  objective.score.scaled = String(normalizedMeasure);
21862
22602
  }
22603
+ if (entry.rawScoreKnown === true) {
22604
+ objective.score.raw = String(entry.rawScore ?? "");
22605
+ }
22606
+ if (entry.minScoreKnown === true) {
22607
+ objective.score.min = String(entry.minScore ?? "");
22608
+ }
22609
+ if (entry.maxScoreKnown === true) {
22610
+ objective.score.max = String(entry.maxScore ?? "");
22611
+ }
21863
22612
  const progressMeasure = this.parseObjectiveNumber(entry.progressMeasure);
21864
22613
  if (entry.progressMeasureKnown === true && progressMeasure !== null) {
21865
22614
  objective.progress_measure = String(progressMeasure);
@@ -21970,6 +22719,9 @@ class GlobalObjectiveManager {
21970
22719
  * @param {CompletionStatus} completionStatus
21971
22720
  * @param {SuccessStatus} successStatus
21972
22721
  * @param {ScoreObject} scoreObject
22722
+ *
22723
+ * @spec SCORM 2004 4th Ed. RTE-to-SN Data Transfer - CMI score data updates the current primary objective
22724
+ * @spec SCORM 2004 4th Ed. ADLSEQ objectives extension - raw/min/max score data is available for write maps
21973
22725
  */
21974
22726
  syncCmiToSequencingActivity(completionStatus, successStatus, scoreObject) {
21975
22727
  if (!this.context.sequencing) {
@@ -21995,6 +22747,15 @@ class GlobalObjectiveManager {
21995
22747
  primaryObjective.normalizedMeasure = scoreObject.scaled;
21996
22748
  primaryObjective.measureStatus = true;
21997
22749
  }
22750
+ if (scoreObject?.raw !== void 0 && scoreObject.raw !== null) {
22751
+ primaryObjective.rawScore = String(scoreObject.raw);
22752
+ }
22753
+ if (scoreObject?.min !== void 0 && scoreObject.min !== null) {
22754
+ primaryObjective.minScore = String(scoreObject.min);
22755
+ }
22756
+ if (scoreObject?.max !== void 0 && scoreObject.max !== null) {
22757
+ primaryObjective.maxScore = String(scoreObject.max);
22758
+ }
21998
22759
  }
21999
22760
  /**
22000
22761
  * Find or create a global objective by ID
@@ -22541,8 +23302,239 @@ class Scorm2004API extends BaseAPI {
22541
23302
  reset(settings) {
22542
23303
  this.commonReset(settings);
22543
23304
  this.cmi?.reset();
23305
+ this.applyCurrentActivityLaunchData();
22544
23306
  this.adl?.reset();
22545
23307
  }
23308
+ /**
23309
+ * Apply launch-static activity data to CMI while the new SCO is pre-initialize.
23310
+ *
23311
+ * @spec SCORM 2004 4th Ed. RTE 4.2.5, Table 4.2.5a - cmi.completion_threshold
23312
+ * @spec SCORM 2004 4th Ed. RTE 4.2.17, Table 4.2.17a - cmi.objectives
23313
+ */
23314
+ applyCurrentActivityLaunchData() {
23315
+ const currentActivity = this._sequencing?.getCurrentActivity();
23316
+ if (!currentActivity) {
23317
+ return;
23318
+ }
23319
+ this.applyActivityLaunchData(currentActivity);
23320
+ }
23321
+ /**
23322
+ * Apply launch-static CMI data when sequencing delivers a new activity before SCO Initialize.
23323
+ *
23324
+ * @spec SCORM 2004 4th Ed. SN DB.2 - Content Delivery Environment Process establishes the delivered activity.
23325
+ * @spec SCORM 2004 4th Ed. RTE 4.2.5, Table 4.2.5a - cmi.completion_threshold is initialized before SCO access.
23326
+ * @spec SCORM 2004 4th Ed. RTE 4.2.17, Table 4.2.17a - cmi.objectives is initialized before SCO access.
23327
+ */
23328
+ applyDeliveredActivityLaunchData(activity) {
23329
+ if (!this.isNotInitialized()) {
23330
+ return;
23331
+ }
23332
+ this.applyActivityLaunchData(activity);
23333
+ }
23334
+ /**
23335
+ * Copy the delivered activity's static launch data into the fresh CMI model.
23336
+ *
23337
+ * @spec SCORM 2004 4th Ed. RTE 4.2.5, Table 4.2.5a - cmi.completion_threshold
23338
+ * @spec SCORM 2004 4th Ed. RTE 4.2.17, Table 4.2.17a - cmi.objectives
23339
+ */
23340
+ applyActivityLaunchData(currentActivity) {
23341
+ if (!this.cmi) {
23342
+ return;
23343
+ }
23344
+ const contentActivityData = this._sequencing.overallSequencingProcess?.getContentActivityData(currentActivity);
23345
+ const completionThreshold = contentActivityData?.completionThreshold ?? currentActivity.completionThreshold?.toString() ?? "";
23346
+ this.cmi.completion_threshold = completionThreshold;
23347
+ this.seedCurrentActivityObjectives(currentActivity);
23348
+ }
23349
+ /**
23350
+ * Seed CMI objective ids after Initialize when automatic sequencing starts during Initialize.
23351
+ *
23352
+ * @spec SCORM 2004 4th Ed. RTE 4.2.17, Table 4.2.17a - cmi.objectives
23353
+ */
23354
+ applyCurrentActivityObjectiveData() {
23355
+ const currentActivity = this._sequencing?.getCurrentActivity();
23356
+ if (!this.cmi || !currentActivity) {
23357
+ return;
23358
+ }
23359
+ this.seedCurrentActivityObjectives(currentActivity);
23360
+ }
23361
+ /**
23362
+ * Seed CMI objectives from primary and secondary activity objectives.
23363
+ *
23364
+ * @spec SCORM 2004 4th Ed. RTE 4.2.17 - Initialization of Run-Time Objectives from Sequencing Information
23365
+ * @spec SCORM 2004 4th Ed. RTE 4.2.17, Table 4.2.17a - cmi.objectives.n.id and success_status
23366
+ * @spec SCORM 2004 4th Ed. SN 3.10.3 / ADLSEQ objectives extension - objective read maps seed the RTE view
23367
+ */
23368
+ seedCurrentActivityObjectives(currentActivity) {
23369
+ const activityObjectives = [];
23370
+ if (currentActivity.primaryObjective) {
23371
+ activityObjectives.push(currentActivity.primaryObjective);
23372
+ }
23373
+ activityObjectives.push(...currentActivity.objectives);
23374
+ const seededObjectiveIds = /* @__PURE__ */ new Set();
23375
+ for (const activityObjective of activityObjectives) {
23376
+ const objectiveId = this.getSeedableObjectiveId(activityObjective);
23377
+ if (!objectiveId || seededObjectiveIds.has(objectiveId)) {
23378
+ continue;
23379
+ }
23380
+ seededObjectiveIds.add(objectiveId);
23381
+ const index = this.findOrSeedCMIObjective(objectiveId);
23382
+ if (index === null) {
23383
+ continue;
23384
+ }
23385
+ const cmiObjective = this.cmi.objectives.findObjectiveByIndex(index);
23386
+ if (cmiObjective && this.cmi.objectives.initialized && !cmiObjective.initialized) {
23387
+ cmiObjective.initialize();
23388
+ }
23389
+ const successStatus = this.getActivityObjectiveSuccessStatus(activityObjective);
23390
+ if (successStatus) {
23391
+ this._commonSetCMIValue(
23392
+ "SeedActivityObjective",
23393
+ true,
23394
+ `cmi.objectives.${index}.success_status`,
23395
+ successStatus
23396
+ );
23397
+ }
23398
+ this.seedObjectiveReadMapValues(currentActivity, activityObjective, index);
23399
+ }
23400
+ }
23401
+ /**
23402
+ * Seed CMI objective fields from this objective's read-mapped global objectives.
23403
+ *
23404
+ * @spec SCORM 2004 4th Ed. RTE 4.2.17 - Run-Time Objectives are initialized from sequencing information
23405
+ * @spec SCORM 2004 4th Ed. SN 3.10.3 - read mapInfo grants access to mapped global objective state
23406
+ * @spec SCORM 2004 4th Ed. ADLSEQ objectives extension - raw/min/max score read maps seed RTE score fields
23407
+ */
23408
+ seedObjectiveReadMapValues(currentActivity, activityObjective, objectiveIndex) {
23409
+ const globalObjectiveMap = this._sequencing.overallSequencingProcess?.getGlobalObjectiveMap();
23410
+ if (!globalObjectiveMap || activityObjective.mapInfo.length === 0) {
23411
+ return;
23412
+ }
23413
+ for (const mapInfo of activityObjective.mapInfo) {
23414
+ const targetObjectiveId = mapInfo.targetObjectiveID || activityObjective.id;
23415
+ const globalObjective = globalObjectiveMap.get(targetObjectiveId);
23416
+ if (!globalObjective) {
23417
+ continue;
23418
+ }
23419
+ const readState = GlobalObjectiveSynchronizer.getGlobalObjectiveReadState(
23420
+ currentActivity,
23421
+ activityObjective,
23422
+ mapInfo,
23423
+ globalObjective
23424
+ );
23425
+ this.applyObjectiveReadStateToCMI(objectiveIndex, readState);
23426
+ }
23427
+ }
23428
+ /**
23429
+ * Copy mapped global objective state into the seeded CMI objective entry.
23430
+ *
23431
+ * @spec SCORM 2004 4th Ed. RTE 4.2.17, Table 4.2.17a - cmi.objectives launch-time initialization
23432
+ * @spec SCORM 2004 4th Ed. SN 3.10.3 - read maps populate the RTE view without creating local writes
23433
+ * @spec SCORM 2004 4th Ed. ADLSEQ objectives extension - raw/min/max score read maps populate CMI objective scores
23434
+ */
23435
+ applyObjectiveReadStateToCMI(objectiveIndex, readState) {
23436
+ if (readState.satisfiedStatus !== void 0) {
23437
+ this._commonSetCMIValue(
23438
+ "SeedActivityObjectiveReadMap",
23439
+ true,
23440
+ `cmi.objectives.${objectiveIndex}.success_status`,
23441
+ readState.satisfiedStatus ? SuccessStatus.PASSED : SuccessStatus.FAILED
23442
+ );
23443
+ }
23444
+ if (readState.normalizedMeasure !== void 0) {
23445
+ this._commonSetCMIValue(
23446
+ "SeedActivityObjectiveReadMap",
23447
+ true,
23448
+ `cmi.objectives.${objectiveIndex}.score.scaled`,
23449
+ String(readState.normalizedMeasure)
23450
+ );
23451
+ }
23452
+ if (readState.completionStatus !== void 0) {
23453
+ this._commonSetCMIValue(
23454
+ "SeedActivityObjectiveReadMap",
23455
+ true,
23456
+ `cmi.objectives.${objectiveIndex}.completion_status`,
23457
+ readState.completionStatus
23458
+ );
23459
+ }
23460
+ if (readState.progressMeasure !== void 0) {
23461
+ this._commonSetCMIValue(
23462
+ "SeedActivityObjectiveReadMap",
23463
+ true,
23464
+ `cmi.objectives.${objectiveIndex}.progress_measure`,
23465
+ String(readState.progressMeasure)
23466
+ );
23467
+ }
23468
+ if (readState.rawScore !== void 0) {
23469
+ this._commonSetCMIValue(
23470
+ "SeedActivityObjectiveReadMap",
23471
+ true,
23472
+ `cmi.objectives.${objectiveIndex}.score.raw`,
23473
+ readState.rawScore
23474
+ );
23475
+ }
23476
+ if (readState.minScore !== void 0) {
23477
+ this._commonSetCMIValue(
23478
+ "SeedActivityObjectiveReadMap",
23479
+ true,
23480
+ `cmi.objectives.${objectiveIndex}.score.min`,
23481
+ readState.minScore
23482
+ );
23483
+ }
23484
+ if (readState.maxScore !== void 0) {
23485
+ this._commonSetCMIValue(
23486
+ "SeedActivityObjectiveReadMap",
23487
+ true,
23488
+ `cmi.objectives.${objectiveIndex}.score.max`,
23489
+ readState.maxScore
23490
+ );
23491
+ }
23492
+ }
23493
+ /**
23494
+ * Return a manifest-defined objective id that can initialize cmi.objectives.n.id.
23495
+ *
23496
+ * @spec SCORM 2004 4th Ed. RTE 4.2.17, Table 4.2.17a - cmi.objectives.n.id
23497
+ */
23498
+ getSeedableObjectiveId(activityObjective) {
23499
+ const objectiveId = activityObjective.id;
23500
+ if (typeof objectiveId !== "string" || objectiveId.trim() === "") {
23501
+ return null;
23502
+ }
23503
+ return objectiveId;
23504
+ }
23505
+ /**
23506
+ * Find an existing CMI objective id or create the next CMI objective array entry.
23507
+ *
23508
+ * @spec SCORM 2004 4th Ed. RTE 4.2.17, Table 4.2.17a - cmi.objectives._count
23509
+ * @spec SCORM 2004 4th Ed. RTE 4.2.17, Table 4.2.17a - cmi.objectives.n.id
23510
+ */
23511
+ findOrSeedCMIObjective(objectiveId) {
23512
+ const existingIndex = this.cmi.objectives.childArray.findIndex((objective) => {
23513
+ return objective.id === objectiveId;
23514
+ });
23515
+ if (existingIndex >= 0) {
23516
+ return existingIndex;
23517
+ }
23518
+ const index = this.cmi.objectives.childArray.length;
23519
+ const result = this._commonSetCMIValue(
23520
+ "SeedActivityObjective",
23521
+ true,
23522
+ `cmi.objectives.${index}.id`,
23523
+ objectiveId
23524
+ );
23525
+ return result === global_constants.SCORM_TRUE ? index : null;
23526
+ }
23527
+ /**
23528
+ * Translate a known activity objective satisfied status to cmi.objectives.n.success_status.
23529
+ *
23530
+ * @spec SCORM 2004 4th Ed. RTE 4.2.17, Table 4.2.17a - cmi.objectives.n.success_status
23531
+ */
23532
+ getActivityObjectiveSuccessStatus(activityObjective) {
23533
+ if (activityObjective.progressStatus || activityObjective.satisfiedStatusKnown) {
23534
+ return activityObjective.satisfiedStatus ? SuccessStatus.PASSED : SuccessStatus.FAILED;
23535
+ }
23536
+ return null;
23537
+ }
22546
23538
  /**
22547
23539
  * Getter for _version
22548
23540
  * @return {string}
@@ -22622,6 +23614,7 @@ class Scorm2004API extends BaseAPI {
22622
23614
  );
22623
23615
  if (result === global_constants.SCORM_TRUE && this._sequencingService) {
22624
23616
  this._sequencingService.initialize();
23617
+ this.applyCurrentActivityObjectiveData();
22625
23618
  }
22626
23619
  if (result === global_constants.SCORM_TRUE) {
22627
23620
  this._globalObjectiveManager.restoreGlobalObjectivesToCMI();
@@ -22876,7 +23869,7 @@ class Scorm2004API extends BaseAPI {
22876
23869
  objective_id = objective ? objective.id : void 0;
22877
23870
  }
22878
23871
  const is_global = objective_id && this.settings.globalObjectiveIds?.includes(objective_id);
22879
- if (is_global) {
23872
+ if (is_global && this.currentActivityAllowsGlobalObjectiveWrites()) {
22880
23873
  const { index: global_index } = this._globalObjectiveManager.findOrCreateGlobalObjective(objective_id);
22881
23874
  const global_element = CMIElement.replace(
22882
23875
  element_base,
@@ -22891,6 +23884,15 @@ class Scorm2004API extends BaseAPI {
22891
23884
  }
22892
23885
  return this._commonSetCMIValue("SetValue", true, CMIElement, value);
22893
23886
  }
23887
+ /**
23888
+ * Return whether the current activity can update shared global objectives.
23889
+ *
23890
+ * @spec SCORM 2004 4th Ed. SN 3.13.1 Tracked - when False, the LMS
23891
+ * "does not initialize, manage or access any tracking status information".
23892
+ */
23893
+ currentActivityAllowsGlobalObjectiveWrites() {
23894
+ return this._sequencing?.getCurrentActivity()?.sequencingControls.tracked !== false;
23895
+ }
22894
23896
  /**
22895
23897
  * Gets or builds a new child element to add to the array
22896
23898
  * @param {string} CMIElement
@@ -23053,14 +24055,14 @@ class Scorm2004API extends BaseAPI {
23053
24055
  if (this.cmi.mode === "normal") {
23054
24056
  if (this.cmi.credit === "credit") {
23055
24057
  if (this.cmi.completion_threshold && this.cmi.progress_measure) {
23056
- if (this.cmi.progress_measure >= this.cmi.completion_threshold) {
24058
+ if (parseFloat(this.cmi.progress_measure) >= parseFloat(this.cmi.completion_threshold)) {
23057
24059
  this.cmi.completion_status = "completed";
23058
24060
  } else {
23059
24061
  this.cmi.completion_status = "incomplete";
23060
24062
  }
23061
24063
  }
23062
24064
  if (this.cmi.scaled_passing_score && this.cmi.score.scaled) {
23063
- if (this.cmi.score.scaled >= this.cmi.scaled_passing_score) {
24065
+ if (parseFloat(this.cmi.score.scaled) >= parseFloat(this.cmi.scaled_passing_score)) {
23064
24066
  this.cmi.success_status = "passed";
23065
24067
  } else {
23066
24068
  this.cmi.success_status = "failed";
@@ -23235,9 +24237,9 @@ class Scorm2004API extends BaseAPI {
23235
24237
  this.loggingService,
23236
24238
  sequencingConfig
23237
24239
  );
23238
- if (settings?.sequencing?.eventListeners) {
23239
- this._sequencingService.setEventListeners(settings.sequencing.eventListeners);
23240
- }
24240
+ this._sequencingService.setEventListeners(
24241
+ this.buildSequencingEventListeners(settings?.sequencing?.eventListeners)
24242
+ );
23241
24243
  this._globalObjectiveManager.updateSequencingService(this._sequencingService);
23242
24244
  this._dataSerializer.updateSequencingService(this._sequencingService);
23243
24245
  if (settings?.sequencingStatePersistence) {
@@ -23260,6 +24262,22 @@ class Scorm2004API extends BaseAPI {
23260
24262
  this._sequencingService = null;
23261
24263
  }
23262
24264
  }
24265
+ /**
24266
+ * Wrap LMS-provided sequencing listeners with API-owned delivery bookkeeping.
24267
+ *
24268
+ * @spec SCORM 2004 4th Ed. SN DB.2 - Content Delivery Environment Process
24269
+ * @spec SCORM 2004 4th Ed. RTE 4.2.5, Table 4.2.5a - cmi.completion_threshold
24270
+ * @spec SCORM 2004 4th Ed. RTE 4.2.17, Table 4.2.17a - cmi.objectives
24271
+ */
24272
+ buildSequencingEventListeners(listeners) {
24273
+ return {
24274
+ ...listeners,
24275
+ onActivityDelivery: (activity) => {
24276
+ this.applyDeliveredActivityLaunchData(activity);
24277
+ listeners?.onActivityDelivery?.(activity);
24278
+ }
24279
+ };
24280
+ }
23263
24281
  /**
23264
24282
  * Get the sequencing service
23265
24283
  * @return {SequencingService | null}
@@ -23270,10 +24288,13 @@ class Scorm2004API extends BaseAPI {
23270
24288
  /**
23271
24289
  * Set sequencing event listeners
23272
24290
  * @param {SequencingEventListeners} listeners
24291
+ *
24292
+ * @spec SCORM 2004 4th Ed. SN DB.2 - Content Delivery Environment Process
24293
+ * @spec SCORM 2004 4th Ed. RTE 4.2.5 / 4.2.17 - launch-static CMI data
23273
24294
  */
23274
24295
  setSequencingEventListeners(listeners) {
23275
24296
  if (this._sequencingService) {
23276
- this._sequencingService.setEventListeners(listeners);
24297
+ this._sequencingService.setEventListeners(this.buildSequencingEventListeners(listeners));
23277
24298
  }
23278
24299
  }
23279
24300
  /**