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
@@ -197,12 +197,18 @@ function stringMatches(str, tester) {
197
197
  }
198
198
  return new RegExp(tester).test(str);
199
199
  }
200
- function memoize(fn, keyFn) {
200
+ function memoize(fn, keyFn, options) {
201
201
  const cache = /* @__PURE__ */ new Map();
202
202
  return ((...args) => {
203
203
  const key = keyFn ? keyFn(...args) : JSON.stringify(args);
204
+ if (options?.maxKeyLength !== void 0 && key.length > options.maxKeyLength) {
205
+ return fn(...args);
206
+ }
204
207
  return cache.has(key) ? cache.get(key) : (() => {
205
208
  const result = fn(...args);
209
+ if (options?.maxEntries !== void 0 && cache.size >= options.maxEntries) {
210
+ cache.delete(cache.keys().next().value);
211
+ }
206
212
  cache.set(key, result);
207
213
  return result;
208
214
  })();
@@ -1103,6 +1109,8 @@ const scorm2004_regex = {
1103
1109
  CMILang: "^([a-zA-Z]{1,8}|i|x)(-[a-zA-Z0-9-]{2,8})?$|^$",
1104
1110
  /** CMILangString250 - String with optional language tag, max 250 chars (RTE C.1.3) */
1105
1111
  CMILangString250: "^({lang=([a-zA-Z]{1,8}|i|x)(-[a-zA-Z0-9-]{2,8})?})?((?!{.*$).{0,250}$)?$",
1112
+ /** CMILangString - Optional language tag, no length cap; RTE C.1.3 SPM is a floor and is deliberately not enforced */
1113
+ CMILangString: "^({lang=([a-zA-Z]{1,8}|i|x)(-[a-zA-Z0-9-]{2,8})?})?((?!{.*$).*$)?$",
1106
1114
  /** CMILangcr - Language tag pattern with content */
1107
1115
  CMILangcr: "^(({lang=([a-zA-Z]{1,8}|i|x)?(-[a-zA-Z0-9-]{2,8})?}))(.*?)$",
1108
1116
  /** CMILangString250cr - String with optional language tag (carriage return variant) */
@@ -1768,12 +1776,6 @@ const HIDE_LMS_UI_TOKENS = [
1768
1776
  "suspendAll"
1769
1777
  ];
1770
1778
 
1771
- var RuleConditionOperator = /* @__PURE__ */ ((RuleConditionOperator2) => {
1772
- RuleConditionOperator2["NOT"] = "not";
1773
- RuleConditionOperator2["AND"] = "and";
1774
- RuleConditionOperator2["OR"] = "or";
1775
- return RuleConditionOperator2;
1776
- })(RuleConditionOperator || {});
1777
1779
  var RuleActionType = /* @__PURE__ */ ((RuleActionType2) => {
1778
1780
  RuleActionType2["SKIP"] = "skip";
1779
1781
  RuleActionType2["DISABLED"] = "disabled";
@@ -1788,6 +1790,48 @@ var RuleActionType = /* @__PURE__ */ ((RuleActionType2) => {
1788
1790
  RuleActionType2["EXIT"] = "exit";
1789
1791
  return RuleActionType2;
1790
1792
  })(RuleActionType || {});
1793
+ function kleeneNot(value) {
1794
+ if (value === "unknown") {
1795
+ return "unknown";
1796
+ }
1797
+ return !value;
1798
+ }
1799
+ function kleeneAnd(values) {
1800
+ let hasUnknown = false;
1801
+ for (const value of values) {
1802
+ if (value === false) {
1803
+ return false;
1804
+ }
1805
+ if (value === "unknown") {
1806
+ hasUnknown = true;
1807
+ }
1808
+ }
1809
+ return hasUnknown ? "unknown" : true;
1810
+ }
1811
+ function kleeneOr(values) {
1812
+ let hasUnknown = false;
1813
+ for (const value of values) {
1814
+ if (value === true) {
1815
+ return true;
1816
+ }
1817
+ if (value === "unknown") {
1818
+ hasUnknown = true;
1819
+ }
1820
+ }
1821
+ return hasUnknown ? "unknown" : false;
1822
+ }
1823
+ function combineRuleConditionResults(values, conditionCombination) {
1824
+ if (values.length === 0) {
1825
+ return false;
1826
+ }
1827
+ if (conditionCombination === "all" || conditionCombination === "and" /* AND */) {
1828
+ return kleeneAnd(values);
1829
+ }
1830
+ if (conditionCombination === "any" || conditionCombination === "or" /* OR */) {
1831
+ return kleeneOr(values);
1832
+ }
1833
+ return false;
1834
+ }
1791
1835
  class RuleCondition extends BaseCMI {
1792
1836
  _condition = "always" /* ALWAYS */;
1793
1837
  _operator = null;
@@ -1893,51 +1937,72 @@ class RuleCondition extends BaseCMI {
1893
1937
  /**
1894
1938
  * Evaluate the condition for an activity
1895
1939
  * @param {Activity} activity - The activity to evaluate the condition for
1896
- * @return {boolean} - True if the condition is met, false otherwise
1940
+ * @return {RuleConditionEvaluation} - True, false, or unknown per SCORM 2004 4th Ed.
1897
1941
  */
1898
1942
  evaluate(activity) {
1899
1943
  let result;
1944
+ const hasReferencedObjective = this._referencedObjective !== null;
1900
1945
  const referencedObjective = this.resolveReferencedObjective(activity);
1901
1946
  switch (this._condition) {
1902
1947
  case "satisfied" /* SATISFIED */:
1903
1948
  case "objectiveSatisfied" /* OBJECTIVE_SATISFIED */:
1904
- if (referencedObjective) {
1905
- result = referencedObjective.satisfiedStatus === true;
1949
+ if (hasReferencedObjective && !referencedObjective) {
1950
+ result = false;
1951
+ } else if (referencedObjective) {
1952
+ result = referencedObjective.satisfiedStatusKnown || referencedObjective.progressStatus ? referencedObjective.satisfiedStatus === true : "unknown";
1953
+ } else if (activity.objectiveSatisfiedStatusKnown) {
1954
+ result = activity.objectiveSatisfiedStatus === true;
1955
+ } else if (activity.successStatus !== SuccessStatus.UNKNOWN) {
1956
+ result = activity.successStatus === SuccessStatus.PASSED;
1906
1957
  } else {
1907
- result = activity.successStatus === SuccessStatus.PASSED || activity.objectiveSatisfiedStatus === true;
1958
+ result = "unknown";
1908
1959
  }
1909
1960
  break;
1910
1961
  case "objectiveStatusKnown" /* OBJECTIVE_STATUS_KNOWN */:
1911
- result = referencedObjective ? !!referencedObjective.measureStatus : !!activity.objectiveMeasureStatus;
1962
+ result = hasReferencedObjective && !referencedObjective ? false : referencedObjective ? !!referencedObjective.satisfiedStatusKnown : !!activity.objectiveSatisfiedStatusKnown;
1912
1963
  break;
1913
1964
  case "objectiveMeasureKnown" /* OBJECTIVE_MEASURE_KNOWN */:
1914
- result = referencedObjective ? !!referencedObjective.measureStatus : !!activity.objectiveMeasureStatus;
1965
+ result = hasReferencedObjective && !referencedObjective ? false : referencedObjective ? !!referencedObjective.measureStatus : !!activity.objectiveMeasureStatus;
1915
1966
  break;
1916
1967
  case "objectiveMeasureGreaterThan" /* OBJECTIVE_MEASURE_GREATER_THAN */: {
1968
+ if (hasReferencedObjective && !referencedObjective) {
1969
+ result = false;
1970
+ break;
1971
+ }
1917
1972
  const greaterThanValue = this._parameters.get("threshold") || 0;
1918
1973
  const measureStatus = referencedObjective ? referencedObjective.measureStatus : activity.objectiveMeasureStatus;
1919
1974
  const measureValue = referencedObjective ? referencedObjective.normalizedMeasure : activity.objectiveNormalizedMeasure;
1920
- result = !!measureStatus && measureValue > greaterThanValue;
1975
+ result = measureStatus ? measureValue > greaterThanValue : "unknown";
1921
1976
  break;
1922
1977
  }
1923
1978
  case "objectiveMeasureLessThan" /* OBJECTIVE_MEASURE_LESS_THAN */: {
1979
+ if (hasReferencedObjective && !referencedObjective) {
1980
+ result = false;
1981
+ break;
1982
+ }
1924
1983
  const lessThanValue = this._parameters.get("threshold") || 0;
1925
1984
  const measureStatus = referencedObjective ? referencedObjective.measureStatus : activity.objectiveMeasureStatus;
1926
1985
  const measureValue = referencedObjective ? referencedObjective.normalizedMeasure : activity.objectiveNormalizedMeasure;
1927
- result = !!measureStatus && measureValue < lessThanValue;
1986
+ result = measureStatus ? measureValue < lessThanValue : "unknown";
1928
1987
  break;
1929
1988
  }
1930
1989
  case "completed" /* COMPLETED */:
1931
1990
  case "activityCompleted" /* ACTIVITY_COMPLETED */:
1932
- if (referencedObjective) {
1933
- result = referencedObjective.completionStatus === CompletionStatus.COMPLETED;
1991
+ if (hasReferencedObjective && !referencedObjective) {
1992
+ result = false;
1993
+ } else if (referencedObjective) {
1994
+ result = referencedObjective.completionStatus === CompletionStatus.UNKNOWN ? "unknown" : referencedObjective.completionStatus === CompletionStatus.COMPLETED;
1995
+ } else if (activity.completionStatus === CompletionStatus.UNKNOWN) {
1996
+ result = "unknown";
1934
1997
  } else {
1935
- result = activity.isCompleted;
1998
+ result = activity.completionStatus === CompletionStatus.COMPLETED;
1936
1999
  }
1937
2000
  break;
1938
2001
  case "progressKnown" /* PROGRESS_KNOWN */:
1939
2002
  case "activityProgressKnown" /* ACTIVITY_PROGRESS_KNOWN */:
1940
- if (referencedObjective) {
2003
+ if (hasReferencedObjective && !referencedObjective) {
2004
+ result = false;
2005
+ } else if (referencedObjective) {
1941
2006
  result = referencedObjective.completionStatus !== CompletionStatus.UNKNOWN;
1942
2007
  } else {
1943
2008
  result = activity.completionStatus !== "unknown";
@@ -1966,7 +2031,7 @@ class RuleCondition extends BaseCMI {
1966
2031
  break;
1967
2032
  }
1968
2033
  if (this._operator === "not" /* NOT */) {
1969
- result = !result;
2034
+ result = kleeneNot(result);
1970
2035
  }
1971
2036
  return result;
1972
2037
  }
@@ -2173,15 +2238,10 @@ class SequencingRule extends BaseCMI {
2173
2238
  * @return {boolean} - True if the rule conditions are met, false otherwise
2174
2239
  */
2175
2240
  evaluate(activity) {
2176
- if (this._conditions.length === 0) {
2177
- return true;
2178
- }
2179
- if (this._conditionCombination === "all" || this._conditionCombination === "and" /* AND */) {
2180
- return this._conditions.every((condition) => condition.evaluate(activity));
2181
- } else if (this._conditionCombination === "any" || this._conditionCombination === "or" /* OR */) {
2182
- return this._conditions.some((condition) => condition.evaluate(activity));
2183
- }
2184
- return false;
2241
+ return combineRuleConditionResults(
2242
+ this._conditions.map((condition) => condition.evaluate(activity)),
2243
+ this._conditionCombination
2244
+ ) === true;
2185
2245
  }
2186
2246
  /**
2187
2247
  * toJSON for SequencingRule
@@ -3014,19 +3074,10 @@ class RuleEvaluationEngine {
3014
3074
  * Evaluates individual sequencing rule conditions
3015
3075
  * @param {Activity} activity - The activity to evaluate the rule for
3016
3076
  * @param {SequencingRule} rule - The rule to evaluate
3017
- * @return {boolean} - True if all rule conditions are met
3077
+ * @return {boolean} - True only when the rule evaluates to definite true
3018
3078
  */
3019
3079
  checkRuleSubprocess(activity, rule) {
3020
- if (rule.conditions.length === 0) {
3021
- return true;
3022
- }
3023
- const conditionCombination = rule.conditionCombination;
3024
- if (conditionCombination === "all" || conditionCombination === RuleConditionOperator.AND) {
3025
- return rule.conditions.every((condition) => condition.evaluate(activity));
3026
- } else if (conditionCombination === "any" || conditionCombination === RuleConditionOperator.OR) {
3027
- return rule.conditions.some((condition) => condition.evaluate(activity));
3028
- }
3029
- return false;
3080
+ return rule.evaluate(activity);
3030
3081
  }
3031
3082
  /**
3032
3083
  * Exit Action Rules Subprocess (TB.2.1)
@@ -3898,16 +3949,21 @@ class FlowTraversalService {
3898
3949
  * @param {Activity} fromActivity - The activity to flow from
3899
3950
  * @param {FlowSubprocessMode} direction - The flow direction
3900
3951
  * @return {FlowSubprocessResult} - Result containing the deliverable activity
3952
+ * @spec SN Book: SB.2.3 (Flow Subprocess) - preserves the SB.2.1 effective traversal direction for SB.2.2.
3953
+ * @spec SN Book: SB.2.2 (Flow Activity Traversal Subprocess) - evaluates candidates using the effective direction returned by SB.2.1.
3901
3954
  */
3902
3955
  flowSubprocess(fromActivity, direction) {
3903
3956
  let candidateActivity = fromActivity;
3904
3957
  let firstIteration = true;
3905
3958
  let lastCandidateHadNoChildren = false;
3959
+ let currentDirection = direction;
3960
+ let forwardOnlyCluster = null;
3906
3961
  while (candidateActivity) {
3907
3962
  const traversalResult = this.flowTreeTraversalSubprocess(
3908
3963
  candidateActivity,
3909
- direction,
3910
- firstIteration
3964
+ currentDirection,
3965
+ firstIteration,
3966
+ forwardOnlyCluster
3911
3967
  );
3912
3968
  if (!traversalResult.activity) {
3913
3969
  let exceptionCode = null;
@@ -3925,17 +3981,22 @@ class FlowTraversalService {
3925
3981
  traversalResult.endSequencingSession
3926
3982
  );
3927
3983
  }
3984
+ const effectiveDirection = traversalResult.direction || currentDirection;
3985
+ if (traversalResult.forwardOnlyCluster) {
3986
+ forwardOnlyCluster = traversalResult.forwardOnlyCluster;
3987
+ }
3928
3988
  lastCandidateHadNoChildren = traversalResult.activity.children.length > 0 && traversalResult.activity.getAvailableChildren().length === 0;
3929
3989
  const deliverable = this.flowActivityTraversalSubprocess(
3930
3990
  traversalResult.activity,
3931
- direction === FlowSubprocessMode.FORWARD,
3991
+ effectiveDirection === FlowSubprocessMode.FORWARD,
3932
3992
  true,
3933
- direction
3993
+ effectiveDirection
3934
3994
  );
3935
3995
  if (deliverable) {
3936
3996
  return new FlowSubprocessResult(deliverable, true, null, false);
3937
3997
  }
3938
3998
  candidateActivity = traversalResult.activity;
3999
+ currentDirection = effectiveDirection;
3939
4000
  firstIteration = false;
3940
4001
  }
3941
4002
  return new FlowSubprocessResult(null, false, null, false);
@@ -3946,11 +4007,13 @@ class FlowTraversalService {
3946
4007
  * @param {Activity} fromActivity - The activity to traverse from
3947
4008
  * @param {FlowSubprocessMode} direction - The traversal direction
3948
4009
  * @param {boolean} skipChildren - Whether to skip checking children
4010
+ * @param {Activity | null} forwardTraversalBoundary - Cluster boundary for an SB.2.1 forwardOnly direction reversal
3949
4011
  * @return {FlowTreeTraversalResult} - The next activity and flags
4012
+ * @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.
3950
4013
  */
3951
- flowTreeTraversalSubprocess(fromActivity, direction, skipChildren = false) {
4014
+ flowTreeTraversalSubprocess(fromActivity, direction, skipChildren = false, forwardTraversalBoundary = null) {
3952
4015
  if (direction === FlowSubprocessMode.FORWARD) {
3953
- return this.traverseForward(fromActivity, skipChildren);
4016
+ return this.traverseForward(fromActivity, skipChildren, forwardTraversalBoundary);
3954
4017
  } else {
3955
4018
  return this.traverseBackward(fromActivity);
3956
4019
  }
@@ -3959,10 +4022,15 @@ class FlowTraversalService {
3959
4022
  * Traverse forward in the activity tree
3960
4023
  * @param {Activity} fromActivity - Starting activity
3961
4024
  * @param {boolean} skipChildren - Whether to skip children
4025
+ * @param {Activity | null} forwardTraversalBoundary - Cluster boundary for an SB.2.1 forwardOnly direction reversal
3962
4026
  * @return {FlowTreeTraversalResult}
4027
+ * @spec SN Book: SB.2.1 (Flow Tree Traversal Subprocess) - a reversed Forward traversal from a forwardOnly cluster remains within that cluster.
3963
4028
  */
3964
- traverseForward(fromActivity, skipChildren) {
3965
- if (skipChildren && this.isActivityLastOverall(fromActivity)) {
4029
+ traverseForward(fromActivity, skipChildren, forwardTraversalBoundary = null) {
4030
+ if (forwardTraversalBoundary && !this.isDescendantOfOrSelf(fromActivity, forwardTraversalBoundary)) {
4031
+ return { activity: null, endSequencingSession: false };
4032
+ }
4033
+ if (!forwardTraversalBoundary && skipChildren && this.isActivityLastOverall(fromActivity)) {
3966
4034
  if (this.activityTree.root) {
3967
4035
  this.terminateDescendentAttempts(this.activityTree.root);
3968
4036
  }
@@ -3981,6 +4049,9 @@ class FlowTraversalService {
3981
4049
  if (nextSibling) {
3982
4050
  return { activity: nextSibling, endSequencingSession: false };
3983
4051
  }
4052
+ if (forwardTraversalBoundary && (current === forwardTraversalBoundary || current.parent === forwardTraversalBoundary)) {
4053
+ return { activity: null, endSequencingSession: false };
4054
+ }
3984
4055
  current = current.parent;
3985
4056
  }
3986
4057
  if (this.activityTree.root) {
@@ -3992,6 +4063,7 @@ class FlowTraversalService {
3992
4063
  * Traverse backward in the activity tree
3993
4064
  * @param {Activity} fromActivity - Starting activity
3994
4065
  * @return {FlowTreeTraversalResult}
4066
+ * @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.
3995
4067
  */
3996
4068
  traverseBackward(fromActivity) {
3997
4069
  if (fromActivity.parent && fromActivity.parent.sequencingControls.forwardOnly) {
@@ -3999,10 +4071,7 @@ class FlowTraversalService {
3999
4071
  }
4000
4072
  const previousSibling = this.activityTree.getPreviousSibling(fromActivity);
4001
4073
  if (previousSibling) {
4002
- return {
4003
- activity: this.getLastDescendant(previousSibling),
4004
- endSequencingSession: false
4005
- };
4074
+ return this.getBackwardTraversalEntry(previousSibling);
4006
4075
  }
4007
4076
  let current = fromActivity;
4008
4077
  let ancestorIterations = 0;
@@ -4013,38 +4082,64 @@ class FlowTraversalService {
4013
4082
  }
4014
4083
  const parentPreviousSibling = this.activityTree.getPreviousSibling(current.parent);
4015
4084
  if (parentPreviousSibling) {
4016
- return {
4017
- activity: this.getLastDescendant(parentPreviousSibling),
4018
- endSequencingSession: false
4019
- };
4085
+ return this.getBackwardTraversalEntry(parentPreviousSibling);
4020
4086
  }
4021
4087
  current = current.parent;
4022
4088
  }
4023
4089
  return { activity: null, endSequencingSession: false };
4024
4090
  }
4025
4091
  /**
4026
- * Get the last descendant of an activity
4092
+ * Get the activity entered by backward traversal.
4027
4093
  * @param {Activity} activity - The activity
4028
- * @return {Activity} - The last descendant
4094
+ * @return {FlowTreeTraversalResult} - The entered activity and effective direction
4095
+ * @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.
4029
4096
  */
4030
- getLastDescendant(activity) {
4031
- let lastDescendant = activity;
4097
+ getBackwardTraversalEntry(activity) {
4098
+ let enteredActivity = activity;
4032
4099
  let iterations = 0;
4033
4100
  const maxIterations = 1e4;
4034
4101
  while (true) {
4035
4102
  if (++iterations > maxIterations) {
4036
- throw new Error("Infinite loop detected while getting last descendant");
4103
+ throw new Error("Infinite loop detected while getting backward traversal entry");
4037
4104
  }
4038
- this.ensureSelectionAndRandomization(lastDescendant);
4039
- const children = lastDescendant.getAvailableChildren();
4105
+ this.ensureSelectionAndRandomization(enteredActivity);
4106
+ const children = enteredActivity.getAvailableChildren();
4040
4107
  if (children.length === 0) {
4041
4108
  break;
4042
4109
  }
4110
+ if (enteredActivity.sequencingControls.forwardOnly) {
4111
+ return {
4112
+ activity: children[0] || null,
4113
+ endSequencingSession: false,
4114
+ direction: FlowSubprocessMode.FORWARD,
4115
+ forwardOnlyCluster: enteredActivity
4116
+ };
4117
+ }
4043
4118
  const lastChild = children[children.length - 1];
4044
4119
  if (!lastChild) break;
4045
- lastDescendant = lastChild;
4120
+ enteredActivity = lastChild;
4046
4121
  }
4047
- return lastDescendant;
4122
+ return {
4123
+ activity: enteredActivity,
4124
+ endSequencingSession: false
4125
+ };
4126
+ }
4127
+ /**
4128
+ * Check whether an activity is the same as or beneath an ancestor.
4129
+ * @param {Activity} activity - The activity to check
4130
+ * @param {Activity} ancestor - The expected ancestor
4131
+ * @return {boolean} - True when activity is within ancestor
4132
+ * @spec SN Book: SB.2.1 (Flow Tree Traversal Subprocess) - bounds Forward traversal after a forwardOnly direction reversal to the entered cluster.
4133
+ */
4134
+ isDescendantOfOrSelf(activity, ancestor) {
4135
+ let current = activity;
4136
+ while (current) {
4137
+ if (current === ancestor) {
4138
+ return true;
4139
+ }
4140
+ current = current.parent;
4141
+ }
4142
+ return false;
4048
4143
  }
4049
4144
  /**
4050
4145
  * Flow Activity Traversal Subprocess (SB.2.2)
@@ -6594,7 +6689,9 @@ const checkValidFormat = memoize(
6594
6689
  (CMIElement, value, regexPattern, errorCode, _errorClass, allowEmptyString) => {
6595
6690
  const valueKey = typeof value === "string" ? value : `[${typeof value}]`;
6596
6691
  return `${CMIElement}:${valueKey}:${regexPattern}:${errorCode}:${allowEmptyString || false}`;
6597
- }
6692
+ },
6693
+ // Normal capped CMI values and regexes fit within 2000 characters; large uncapped values bypass caching.
6694
+ { maxEntries: 1e3, maxKeyLength: 2e3 }
6598
6695
  );
6599
6696
  const checkValidRange = memoize(
6600
6697
  (CMIElement, value, rangePattern, errorCode, errorClass) => {
@@ -6617,7 +6714,8 @@ const checkValidRange = memoize(
6617
6714
  },
6618
6715
  // Custom key function that excludes the error class from the cache key
6619
6716
  // since it can't be stringified and doesn't affect the validation result
6620
- (CMIElement, value, rangePattern, errorCode, _errorClass) => `${CMIElement}:${value}:${rangePattern}:${errorCode}`
6717
+ (CMIElement, value, rangePattern, errorCode, _errorClass) => `${CMIElement}:${value}:${rangePattern}:${errorCode}`,
6718
+ { maxEntries: 1e3 }
6621
6719
  );
6622
6720
 
6623
6721
  function check2004ValidFormat(CMIElement, value, regexPattern, allowEmptyString) {
@@ -7126,6 +7224,12 @@ class ActivityObjective {
7126
7224
  // objectives. It serves as a validity gate for other synced properties.
7127
7225
  _measureStatus = false;
7128
7226
  _normalizedMeasure = 0;
7227
+ _rawScore = "";
7228
+ _rawScoreKnown = false;
7229
+ _minScore = "";
7230
+ _minScoreKnown = false;
7231
+ _maxScore = "";
7232
+ _maxScoreKnown = false;
7129
7233
  _progressMeasure = 0;
7130
7234
  _progressMeasureStatus = false;
7131
7235
  _completionStatus = CompletionStatus.UNKNOWN;
@@ -7135,6 +7239,9 @@ class ActivityObjective {
7135
7239
  _normalizedMeasureDirty = false;
7136
7240
  _completionStatusDirty = false;
7137
7241
  _progressMeasureDirty = false;
7242
+ _rawScoreDirty = false;
7243
+ _minScoreDirty = false;
7244
+ _maxScoreDirty = false;
7138
7245
  constructor(id, options = {}) {
7139
7246
  this._id = id;
7140
7247
  this._description = options.description ?? null;
@@ -7180,6 +7287,8 @@ class ActivityObjective {
7180
7287
  if (this._satisfiedStatus !== value) {
7181
7288
  this._satisfiedStatus = value;
7182
7289
  this._satisfiedStatusDirty = true;
7290
+ this._satisfiedStatusKnown = true;
7291
+ this._progressStatus = true;
7183
7292
  }
7184
7293
  }
7185
7294
  get satisfiedStatusKnown() {
@@ -7203,6 +7312,114 @@ class ActivityObjective {
7203
7312
  this._normalizedMeasureDirty = true;
7204
7313
  }
7205
7314
  }
7315
+ /**
7316
+ * Return the known raw score value held for ADLSEQ objective score mapping.
7317
+ *
7318
+ * @spec SCORM 2004 4th Ed. ADLSEQ objectives extension - raw score mapInfo
7319
+ */
7320
+ get rawScore() {
7321
+ return this._rawScore;
7322
+ }
7323
+ /**
7324
+ * Store the RTE raw score associated with this objective.
7325
+ *
7326
+ * @spec SCORM 2004 4th Ed. SN 3.10.3 / ADLSEQ objectives extension - raw score mapInfo
7327
+ */
7328
+ set rawScore(value) {
7329
+ if (this._rawScore !== value) {
7330
+ this._rawScore = value;
7331
+ this._rawScoreDirty = true;
7332
+ }
7333
+ this._rawScoreKnown = value !== "";
7334
+ }
7335
+ /**
7336
+ * Return whether this objective's raw score is known.
7337
+ *
7338
+ * @spec SCORM 2004 4th Ed. ADLSEQ objectives extension - raw score known state
7339
+ */
7340
+ get rawScoreKnown() {
7341
+ return this._rawScoreKnown;
7342
+ }
7343
+ /**
7344
+ * Set whether this objective's raw score is known.
7345
+ *
7346
+ * @spec SCORM 2004 4th Ed. ADLSEQ objectives extension - raw score known state
7347
+ */
7348
+ set rawScoreKnown(value) {
7349
+ this._rawScoreKnown = value;
7350
+ }
7351
+ /**
7352
+ * Return the known minimum score value held for ADLSEQ objective score mapping.
7353
+ *
7354
+ * @spec SCORM 2004 4th Ed. ADLSEQ objectives extension - min score mapInfo
7355
+ */
7356
+ get minScore() {
7357
+ return this._minScore;
7358
+ }
7359
+ /**
7360
+ * Store the RTE minimum score associated with this objective.
7361
+ *
7362
+ * @spec SCORM 2004 4th Ed. SN 3.10.3 / ADLSEQ objectives extension - min score mapInfo
7363
+ */
7364
+ set minScore(value) {
7365
+ if (this._minScore !== value) {
7366
+ this._minScore = value;
7367
+ this._minScoreDirty = true;
7368
+ }
7369
+ this._minScoreKnown = value !== "";
7370
+ }
7371
+ /**
7372
+ * Return whether this objective's minimum score is known.
7373
+ *
7374
+ * @spec SCORM 2004 4th Ed. ADLSEQ objectives extension - min score known state
7375
+ */
7376
+ get minScoreKnown() {
7377
+ return this._minScoreKnown;
7378
+ }
7379
+ /**
7380
+ * Set whether this objective's minimum score is known.
7381
+ *
7382
+ * @spec SCORM 2004 4th Ed. ADLSEQ objectives extension - min score known state
7383
+ */
7384
+ set minScoreKnown(value) {
7385
+ this._minScoreKnown = value;
7386
+ }
7387
+ /**
7388
+ * Return the known maximum score value held for ADLSEQ objective score mapping.
7389
+ *
7390
+ * @spec SCORM 2004 4th Ed. ADLSEQ objectives extension - max score mapInfo
7391
+ */
7392
+ get maxScore() {
7393
+ return this._maxScore;
7394
+ }
7395
+ /**
7396
+ * Store the RTE maximum score associated with this objective.
7397
+ *
7398
+ * @spec SCORM 2004 4th Ed. SN 3.10.3 / ADLSEQ objectives extension - max score mapInfo
7399
+ */
7400
+ set maxScore(value) {
7401
+ if (this._maxScore !== value) {
7402
+ this._maxScore = value;
7403
+ this._maxScoreDirty = true;
7404
+ }
7405
+ this._maxScoreKnown = value !== "";
7406
+ }
7407
+ /**
7408
+ * Return whether this objective's maximum score is known.
7409
+ *
7410
+ * @spec SCORM 2004 4th Ed. ADLSEQ objectives extension - max score known state
7411
+ */
7412
+ get maxScoreKnown() {
7413
+ return this._maxScoreKnown;
7414
+ }
7415
+ /**
7416
+ * Set whether this objective's maximum score is known.
7417
+ *
7418
+ * @spec SCORM 2004 4th Ed. ADLSEQ objectives extension - max score known state
7419
+ */
7420
+ set maxScoreKnown(value) {
7421
+ this._maxScoreKnown = value;
7422
+ }
7206
7423
  get progressMeasure() {
7207
7424
  return this._progressMeasure;
7208
7425
  }
@@ -7233,6 +7450,11 @@ class ActivityObjective {
7233
7450
  set progressStatus(value) {
7234
7451
  this._progressStatus = value;
7235
7452
  }
7453
+ /**
7454
+ * Report whether a local objective field has changed since the last global write.
7455
+ *
7456
+ * @spec SCORM 2004 4th Ed. SN 3.10.3 / ADLSEQ objectives extension - write maps use known local objective data
7457
+ */
7236
7458
  isDirty(property) {
7237
7459
  switch (property) {
7238
7460
  case "satisfiedStatus":
@@ -7243,8 +7465,19 @@ class ActivityObjective {
7243
7465
  return this._completionStatusDirty;
7244
7466
  case "progressMeasure":
7245
7467
  return this._progressMeasureDirty;
7468
+ case "rawScore":
7469
+ return this._rawScoreDirty;
7470
+ case "minScore":
7471
+ return this._minScoreDirty;
7472
+ case "maxScore":
7473
+ return this._maxScoreDirty;
7246
7474
  }
7247
7475
  }
7476
+ /**
7477
+ * Clear a local objective dirty flag after a successful global write.
7478
+ *
7479
+ * @spec SCORM 2004 4th Ed. SN 3.10.3 / ADLSEQ objectives extension - write maps update global objective state
7480
+ */
7248
7481
  clearDirty(property) {
7249
7482
  switch (property) {
7250
7483
  case "satisfiedStatus":
@@ -7259,13 +7492,31 @@ class ActivityObjective {
7259
7492
  case "progressMeasure":
7260
7493
  this._progressMeasureDirty = false;
7261
7494
  break;
7495
+ case "rawScore":
7496
+ this._rawScoreDirty = false;
7497
+ break;
7498
+ case "minScore":
7499
+ this._minScoreDirty = false;
7500
+ break;
7501
+ case "maxScore":
7502
+ this._maxScoreDirty = false;
7503
+ break;
7262
7504
  }
7263
7505
  }
7506
+ /**
7507
+ * Clear all write-map dirty flags for this objective.
7508
+ *
7509
+ * @spec SCORM 2004 4th Ed. SN 3.10.3 - objective mapInfo writes are field-specific
7510
+ * @spec SCORM 2004 4th Ed. ADLSEQ objectives extension - score mapInfo writes are field-specific
7511
+ */
7264
7512
  clearAllDirty() {
7265
7513
  this._satisfiedStatusDirty = false;
7266
7514
  this._normalizedMeasureDirty = false;
7267
7515
  this._completionStatusDirty = false;
7268
7516
  this._progressMeasureDirty = false;
7517
+ this._rawScoreDirty = false;
7518
+ this._minScoreDirty = false;
7519
+ this._maxScoreDirty = false;
7269
7520
  }
7270
7521
  /**
7271
7522
  * Initialize objective values from CMI data transfer
@@ -7275,6 +7526,8 @@ class ActivityObjective {
7275
7526
  * @param satisfiedStatus - The satisfied status from CMI
7276
7527
  * @param normalizedMeasure - The normalized measure from CMI
7277
7528
  * @param measureStatus - Whether measure is valid
7529
+ *
7530
+ * @spec SCORM 2004 4th Ed. RTE-to-SN Data Transfer - objective satisfaction and measure transfer
7278
7531
  */
7279
7532
  initializeFromCMI(satisfiedStatus, normalizedMeasure, measureStatus) {
7280
7533
  this._satisfiedStatus = satisfiedStatus;
@@ -7283,17 +7536,93 @@ class ActivityObjective {
7283
7536
  this._normalizedMeasureDirty = true;
7284
7537
  this._measureStatus = measureStatus;
7285
7538
  }
7539
+ /**
7540
+ * Initialize raw/min/max objective score values from RTE data transfer.
7541
+ *
7542
+ * @spec SCORM 2004 4th Ed. RTE-to-SN Data Transfer - objective score transfer
7543
+ * @spec SCORM 2004 4th Ed. ADLSEQ objectives extension - raw/min/max score mapInfo writes
7544
+ */
7545
+ initializeScoreFromCMI(score) {
7546
+ if (score.rawScore !== void 0 && score.rawScore !== "") {
7547
+ this._rawScore = score.rawScore;
7548
+ this._rawScoreKnown = true;
7549
+ this._rawScoreDirty = true;
7550
+ }
7551
+ if (score.minScore !== void 0 && score.minScore !== "") {
7552
+ this._minScore = score.minScore;
7553
+ this._minScoreKnown = true;
7554
+ this._minScoreDirty = true;
7555
+ }
7556
+ if (score.maxScore !== void 0 && score.maxScore !== "") {
7557
+ this._maxScore = score.maxScore;
7558
+ this._maxScoreKnown = true;
7559
+ this._maxScoreDirty = true;
7560
+ }
7561
+ }
7562
+ /**
7563
+ * Apply read-mapped global objective state without marking the values dirty.
7564
+ *
7565
+ * @spec SCORM 2004 4th Ed. SN 3.10.3 - read maps provide access to global objective state
7566
+ * @spec SCORM 2004 4th Ed. ADLSEQ objectives extension - score read maps are access-only
7567
+ */
7568
+ applyReadMappedState(state) {
7569
+ if (state.satisfiedStatus !== void 0) {
7570
+ this._satisfiedStatus = state.satisfiedStatus;
7571
+ this._satisfiedStatusKnown = true;
7572
+ this._progressStatus = true;
7573
+ }
7574
+ if (state.normalizedMeasure !== void 0) {
7575
+ this._normalizedMeasure = state.normalizedMeasure;
7576
+ this._measureStatus = true;
7577
+ }
7578
+ if (state.completionStatus !== void 0) {
7579
+ this._completionStatus = state.completionStatus;
7580
+ }
7581
+ if (state.progressMeasure !== void 0) {
7582
+ this._progressMeasure = state.progressMeasure;
7583
+ this._progressMeasureStatus = true;
7584
+ }
7585
+ if (state.rawScore !== void 0) {
7586
+ this._rawScore = state.rawScore;
7587
+ this._rawScoreKnown = true;
7588
+ }
7589
+ if (state.minScore !== void 0) {
7590
+ this._minScore = state.minScore;
7591
+ this._minScoreKnown = true;
7592
+ }
7593
+ if (state.maxScore !== void 0) {
7594
+ this._maxScore = state.maxScore;
7595
+ this._maxScoreKnown = true;
7596
+ }
7597
+ }
7598
+ /**
7599
+ * Reset local objective state for a fresh activity attempt.
7600
+ *
7601
+ * @spec SCORM 2004 4th Ed. SN 3.10 Objective Description - unknown objective state before tracking data exists
7602
+ * @spec SCORM 2004 4th Ed. ADLSEQ objectives extension - score map fields are unknown until transferred or read
7603
+ */
7286
7604
  resetState() {
7287
7605
  this._satisfiedStatus = false;
7288
7606
  this._satisfiedStatusKnown = false;
7289
7607
  this._measureStatus = false;
7290
7608
  this._normalizedMeasure = 0;
7609
+ this._rawScore = "";
7610
+ this._rawScoreKnown = false;
7611
+ this._minScore = "";
7612
+ this._minScoreKnown = false;
7613
+ this._maxScore = "";
7614
+ this._maxScoreKnown = false;
7291
7615
  this._progressMeasure = 0;
7292
7616
  this._progressMeasureStatus = false;
7293
7617
  this._completionStatus = CompletionStatus.UNKNOWN;
7294
7618
  this._progressStatus = false;
7295
7619
  this.clearAllDirty();
7296
7620
  }
7621
+ /**
7622
+ * Copy primary activity objective state back into the primary objective model.
7623
+ *
7624
+ * @spec SCORM 2004 4th Ed. RTE-to-SN Data Transfer - primary objective state is available to sequencing
7625
+ */
7297
7626
  updateFromActivity(activity) {
7298
7627
  if (this._satisfiedStatus !== activity.objectiveSatisfiedStatus) {
7299
7628
  this._satisfiedStatus = activity.objectiveSatisfiedStatus;
@@ -7315,6 +7644,11 @@ class ActivityObjective {
7315
7644
  this._completionStatusDirty = true;
7316
7645
  }
7317
7646
  }
7647
+ /**
7648
+ * Apply primary objective state to the owning activity for sequencing rules and rollup.
7649
+ *
7650
+ * @spec SCORM 2004 4th Ed. SN 3.10 Objective Description - primary objective contributes activity state
7651
+ */
7318
7652
  applyToActivity(activity) {
7319
7653
  if (!this._isPrimary) {
7320
7654
  return;
@@ -7325,7 +7659,8 @@ class ActivityObjective {
7325
7659
  this._normalizedMeasure,
7326
7660
  this._progressMeasure,
7327
7661
  this._progressMeasureStatus,
7328
- this._completionStatus
7662
+ this._completionStatus,
7663
+ this._progressStatus || this._satisfiedStatusKnown
7329
7664
  );
7330
7665
  }
7331
7666
  }
@@ -8395,7 +8730,7 @@ class Activity extends BaseCMI {
8395
8730
  /**
8396
8731
  * Setter for primary objective
8397
8732
  * @param {ActivityObjective | null} objective
8398
- */
8733
+ */
8399
8734
  set primaryObjective(objective) {
8400
8735
  this._primaryObjective = objective;
8401
8736
  if (this._primaryObjective) {
@@ -8417,7 +8752,7 @@ class Activity extends BaseCMI {
8417
8752
  /**
8418
8753
  * Replace objectives collection
8419
8754
  * @param {ActivityObjective[]} objectives
8420
- */
8755
+ */
8421
8756
  set objectives(objectives) {
8422
8757
  this._objectives = [...objectives];
8423
8758
  this.syncPrimaryObjectiveCollection();
@@ -8510,12 +8845,12 @@ class Activity extends BaseCMI {
8510
8845
  this._objectiveNormalizedMeasureDirty = false;
8511
8846
  this._objectiveMeasureStatusDirty = false;
8512
8847
  }
8513
- setPrimaryObjectiveState(satisfiedStatus, measureStatus, normalizedMeasure, progressMeasure, progressMeasureStatus, completionStatus) {
8848
+ setPrimaryObjectiveState(satisfiedStatus, measureStatus, normalizedMeasure, progressMeasure, progressMeasureStatus, completionStatus, objectiveProgressStatus = true) {
8514
8849
  if (this._objectiveSatisfiedStatus !== satisfiedStatus) {
8515
8850
  this._objectiveSatisfiedStatus = satisfiedStatus;
8516
8851
  this._objectiveSatisfiedStatusDirty = true;
8517
8852
  }
8518
- this._objectiveSatisfiedStatusKnown = true;
8853
+ this._objectiveSatisfiedStatusKnown = objectiveProgressStatus;
8519
8854
  if (this._objectiveMeasureStatus !== measureStatus) {
8520
8855
  this._objectiveMeasureStatus = measureStatus;
8521
8856
  this._objectiveMeasureStatusDirty = true;
@@ -8534,14 +8869,28 @@ class Activity extends BaseCMI {
8534
8869
  this._primaryObjective.progressMeasure = progressMeasure;
8535
8870
  this._primaryObjective.progressMeasureStatus = progressMeasureStatus;
8536
8871
  this._primaryObjective.completionStatus = completionStatus;
8872
+ this._primaryObjective.satisfiedStatusKnown = objectiveProgressStatus;
8873
+ this._primaryObjective.progressStatus = objectiveProgressStatus;
8537
8874
  }
8538
8875
  }
8876
+ /**
8877
+ * Snapshot objective state for sequencing persistence.
8878
+ *
8879
+ * @spec SCORM 2004 4th Ed. SN 3.10 Objective Description - objective state persists across attempts
8880
+ * @spec SCORM 2004 4th Ed. ADLSEQ objectives extension - score-map state persists with objective state
8881
+ */
8539
8882
  getObjectiveStateSnapshot() {
8540
8883
  const primarySnapshot = this._primaryObjective ? {
8541
8884
  id: this._primaryObjective.id,
8542
8885
  satisfiedStatus: this.objectiveSatisfiedStatus,
8543
8886
  measureStatus: this.objectiveMeasureStatus,
8544
8887
  normalizedMeasure: this.objectiveNormalizedMeasure,
8888
+ rawScore: this._primaryObjective.rawScore,
8889
+ rawScoreKnown: this._primaryObjective.rawScoreKnown,
8890
+ minScore: this._primaryObjective.minScore,
8891
+ minScoreKnown: this._primaryObjective.minScoreKnown,
8892
+ maxScore: this._primaryObjective.maxScore,
8893
+ maxScoreKnown: this._primaryObjective.maxScoreKnown,
8545
8894
  progressMeasure: this.progressMeasure ?? 0,
8546
8895
  progressMeasureStatus: this.progressMeasureStatus,
8547
8896
  progressStatus: this._primaryObjective.progressStatus,
@@ -8554,6 +8903,12 @@ class Activity extends BaseCMI {
8554
8903
  satisfiedStatus: objective.satisfiedStatus,
8555
8904
  measureStatus: objective.measureStatus,
8556
8905
  normalizedMeasure: objective.normalizedMeasure,
8906
+ rawScore: objective.rawScore,
8907
+ rawScoreKnown: objective.rawScoreKnown,
8908
+ minScore: objective.minScore,
8909
+ minScoreKnown: objective.minScoreKnown,
8910
+ maxScore: objective.maxScore,
8911
+ maxScoreKnown: objective.maxScoreKnown,
8557
8912
  progressMeasure: objective.progressMeasure,
8558
8913
  progressMeasureStatus: objective.progressMeasureStatus,
8559
8914
  progressStatus: objective.progressStatus,
@@ -8566,6 +8921,12 @@ class Activity extends BaseCMI {
8566
8921
  objectives: additionalSnapshots
8567
8922
  };
8568
8923
  }
8924
+ /**
8925
+ * Restore objective state from a sequencing persistence snapshot.
8926
+ *
8927
+ * @spec SCORM 2004 4th Ed. SN 3.10 Objective Description - persisted objective state restores sequencing state
8928
+ * @spec SCORM 2004 4th Ed. ADLSEQ objectives extension - persisted score-map state restores objective score fields
8929
+ */
8569
8930
  applyObjectiveStateSnapshot(snapshot) {
8570
8931
  if (snapshot.primary) {
8571
8932
  const primary = this.getObjectiveById(snapshot.primary.id);
@@ -8579,8 +8940,10 @@ class Activity extends BaseCMI {
8579
8940
  state.normalizedMeasure,
8580
8941
  state.progressMeasure,
8581
8942
  state.progressMeasureStatus,
8582
- state.completionStatus
8943
+ state.completionStatus,
8944
+ state.progressStatus
8583
8945
  );
8946
+ this.applyObjectiveScoreSnapshot(primary.objective, state);
8584
8947
  }
8585
8948
  }
8586
8949
  for (const state of snapshot.objectives) {
@@ -8593,11 +8956,30 @@ class Activity extends BaseCMI {
8593
8956
  objective.progressMeasure = state.progressMeasure;
8594
8957
  objective.progressMeasureStatus = state.progressMeasureStatus;
8595
8958
  objective.completionStatus = state.completionStatus;
8959
+ this.applyObjectiveScoreSnapshot(objective, state);
8596
8960
  objective.satisfiedByMeasure = state.satisfiedByMeasure ?? objective.satisfiedByMeasure;
8597
8961
  objective.minNormalizedMeasure = state.minNormalizedMeasure !== void 0 ? state.minNormalizedMeasure : objective.minNormalizedMeasure;
8598
8962
  }
8599
8963
  }
8600
8964
  }
8965
+ /**
8966
+ * Restore known raw/min/max score fields from an objective state snapshot.
8967
+ *
8968
+ * @spec SCORM 2004 4th Ed. ADLSEQ objectives extension - raw/min/max score known flags restore independently
8969
+ */
8970
+ applyObjectiveScoreSnapshot(objective, state) {
8971
+ const scoreState = {};
8972
+ if (state.rawScoreKnown) {
8973
+ scoreState.rawScore = state.rawScore;
8974
+ }
8975
+ if (state.minScoreKnown) {
8976
+ scoreState.minScore = state.minScore;
8977
+ }
8978
+ if (state.maxScoreKnown) {
8979
+ scoreState.maxScore = state.maxScore;
8980
+ }
8981
+ objective.applyReadMappedState(scoreState);
8982
+ }
8601
8983
  /**
8602
8984
  * Get available children with selection and randomization applied
8603
8985
  * @return {Activity[]}
@@ -9988,11 +10370,18 @@ class GlobalObjectiveSynchronizer {
9988
10370
  *
9989
10371
  * @param activity - The activity to process
9990
10372
  * @param globalObjectives - Global objective map
10373
+ *
10374
+ * @spec SCORM 2004 4th Ed. SN 3.10.3 - write mapInfo transfers local objective state to global objectives
10375
+ * @spec SCORM 2004 4th Ed. ADLSEQ objectives extension - raw/min/max score write maps
9991
10376
  */
9992
10377
  syncGlobalObjectivesWritePhase(activity, globalObjectives) {
10378
+ if (!this.canWriteGlobalObjectives(activity)) {
10379
+ return;
10380
+ }
9993
10381
  const objectives = activity.getAllObjectives();
9994
10382
  for (const objective of objectives) {
9995
10383
  const mapInfos = objective.mapInfo.length > 0 ? objective.mapInfo : [this.createDefaultMapInfo(objective)];
10384
+ const dirtyFieldsToClear = /* @__PURE__ */ new Set();
9996
10385
  for (const mapInfo of mapInfos) {
9997
10386
  const targetId = mapInfo.targetObjectiveID || objective.id;
9998
10387
  const globalObjective = this.ensureGlobalObjectiveEntry(
@@ -10000,36 +10389,54 @@ class GlobalObjectiveSynchronizer {
10000
10389
  targetId,
10001
10390
  objective
10002
10391
  );
10003
- if (mapInfo.writeSatisfiedStatus && objective.measureStatus && objective.isDirty("satisfiedStatus")) {
10392
+ if (mapInfo.writeSatisfiedStatus && this.hasKnownSatisfiedStatus(objective) && objective.isDirty("satisfiedStatus")) {
10004
10393
  globalObjective.satisfiedStatus = objective.satisfiedStatus;
10005
10394
  globalObjective.satisfiedStatusKnown = true;
10006
- objective.clearDirty("satisfiedStatus");
10395
+ dirtyFieldsToClear.add("satisfiedStatus");
10007
10396
  }
10008
10397
  if (mapInfo.writeNormalizedMeasure && objective.measureStatus && objective.isDirty("normalizedMeasure")) {
10009
10398
  globalObjective.normalizedMeasure = objective.normalizedMeasure;
10010
10399
  globalObjective.normalizedMeasureKnown = true;
10011
- objective.clearDirty("normalizedMeasure");
10012
- if (globalObjective.satisfiedByMeasure || objective.satisfiedByMeasure) {
10400
+ dirtyFieldsToClear.add("normalizedMeasure");
10401
+ if (mapInfo.writeSatisfiedStatus && objective.satisfiedByMeasure) {
10013
10402
  const threshold = objective.minNormalizedMeasure ?? activity.scaledPassingScore ?? 0.7;
10014
10403
  globalObjective.satisfiedStatus = objective.normalizedMeasure >= threshold;
10015
10404
  globalObjective.satisfiedStatusKnown = true;
10016
- objective.clearDirty("satisfiedStatus");
10405
+ dirtyFieldsToClear.add("satisfiedStatus");
10017
10406
  }
10018
10407
  }
10019
10408
  if (mapInfo.writeCompletionStatus && objective.completionStatus !== CompletionStatus.UNKNOWN && objective.isDirty("completionStatus")) {
10020
10409
  globalObjective.completionStatus = objective.completionStatus;
10021
10410
  globalObjective.completionStatusKnown = true;
10022
- objective.clearDirty("completionStatus");
10411
+ dirtyFieldsToClear.add("completionStatus");
10412
+ }
10413
+ if (mapInfo.writeRawScore && objective.rawScoreKnown && objective.isDirty("rawScore")) {
10414
+ globalObjective.rawScore = objective.rawScore;
10415
+ globalObjective.rawScoreKnown = true;
10416
+ dirtyFieldsToClear.add("rawScore");
10417
+ }
10418
+ if (mapInfo.writeMinScore && objective.minScoreKnown && objective.isDirty("minScore")) {
10419
+ globalObjective.minScore = objective.minScore;
10420
+ globalObjective.minScoreKnown = true;
10421
+ dirtyFieldsToClear.add("minScore");
10422
+ }
10423
+ if (mapInfo.writeMaxScore && objective.maxScoreKnown && objective.isDirty("maxScore")) {
10424
+ globalObjective.maxScore = objective.maxScore;
10425
+ globalObjective.maxScoreKnown = true;
10426
+ dirtyFieldsToClear.add("maxScore");
10023
10427
  }
10024
10428
  if (mapInfo.writeProgressMeasure && objective.progressMeasureStatus && objective.isDirty("progressMeasure")) {
10025
10429
  globalObjective.progressMeasure = objective.progressMeasure;
10026
10430
  globalObjective.progressMeasureKnown = true;
10027
- objective.clearDirty("progressMeasure");
10431
+ dirtyFieldsToClear.add("progressMeasure");
10028
10432
  }
10029
10433
  if (mapInfo.updateAttemptData) {
10030
10434
  this.updateActivityAttemptData(activity, globalObjective, objective);
10031
10435
  }
10032
10436
  }
10437
+ for (const property of dirtyFieldsToClear) {
10438
+ objective.clearDirty(property);
10439
+ }
10033
10440
  }
10034
10441
  }
10035
10442
  /**
@@ -10037,6 +10444,9 @@ class GlobalObjectiveSynchronizer {
10037
10444
  *
10038
10445
  * @param activity - The activity to process
10039
10446
  * @param globalObjectives - Global objective map
10447
+ *
10448
+ * @spec SCORM 2004 4th Ed. SN 3.10.3 - read mapInfo transfers global objective state into the local view
10449
+ * @spec SCORM 2004 4th Ed. ADLSEQ objectives extension - raw/min/max score read maps
10040
10450
  */
10041
10451
  syncGlobalObjectivesReadPhase(activity, globalObjectives) {
10042
10452
  const objectives = activity.getAllObjectives();
@@ -10047,25 +10457,13 @@ class GlobalObjectiveSynchronizer {
10047
10457
  const globalObjective = globalObjectives.get(targetId);
10048
10458
  if (!globalObjective) continue;
10049
10459
  const isPrimary = objective.isPrimary;
10050
- if (mapInfo.readSatisfiedStatus && globalObjective.satisfiedStatusKnown) {
10051
- objective.satisfiedStatus = globalObjective.satisfiedStatus;
10052
- objective.measureStatus = true;
10053
- }
10054
- if (mapInfo.readNormalizedMeasure && globalObjective.normalizedMeasureKnown) {
10055
- objective.normalizedMeasure = globalObjective.normalizedMeasure;
10056
- objective.measureStatus = true;
10057
- if (globalObjective.satisfiedByMeasure || objective.satisfiedByMeasure) {
10058
- const threshold = objective.minNormalizedMeasure ?? activity.scaledPassingScore ?? 0.7;
10059
- objective.satisfiedStatus = globalObjective.normalizedMeasure >= threshold;
10060
- }
10061
- }
10062
- if (mapInfo.readProgressMeasure && globalObjective.progressMeasureKnown) {
10063
- objective.progressMeasure = globalObjective.progressMeasure;
10064
- objective.progressMeasureStatus = true;
10065
- }
10066
- if (mapInfo.readCompletionStatus && globalObjective.completionStatusKnown) {
10067
- objective.completionStatus = globalObjective.completionStatus;
10068
- }
10460
+ const readState = GlobalObjectiveSynchronizer.getGlobalObjectiveReadState(
10461
+ activity,
10462
+ objective,
10463
+ mapInfo,
10464
+ globalObjective
10465
+ );
10466
+ this.applyGlobalObjectiveReadState(objective, readState);
10069
10467
  if (isPrimary) {
10070
10468
  objective.applyToActivity(activity);
10071
10469
  }
@@ -10108,56 +10506,61 @@ class GlobalObjectiveSynchronizer {
10108
10506
  * @param objective - The objective to sync
10109
10507
  * @param mapInfo - Map info for this objective
10110
10508
  * @param globalObjective - The global objective
10509
+ *
10510
+ * @spec SCORM 2004 4th Ed. SN 3.10.3 - objective mapInfo read/write synchronization
10511
+ * @spec SCORM 2004 4th Ed. ADLSEQ objectives extension - score mapInfo synchronization
10111
10512
  */
10112
10513
  syncObjectiveState(activity, objective, mapInfo, globalObjective) {
10113
10514
  try {
10114
10515
  const isPrimary = objective.isPrimary;
10115
10516
  const localObjective = this.getLocalObjectiveState(activity, objective, isPrimary);
10116
- if (mapInfo.readSatisfiedStatus && globalObjective.satisfiedStatusKnown) {
10117
- objective.satisfiedStatus = globalObjective.satisfiedStatus;
10118
- objective.measureStatus = true;
10119
- }
10120
- if (mapInfo.readNormalizedMeasure && globalObjective.normalizedMeasureKnown) {
10121
- objective.normalizedMeasure = globalObjective.normalizedMeasure;
10122
- objective.measureStatus = true;
10123
- if (globalObjective.satisfiedByMeasure || objective.satisfiedByMeasure) {
10124
- const threshold = objective.minNormalizedMeasure ?? activity.scaledPassingScore ?? 0.7;
10125
- objective.satisfiedStatus = globalObjective.normalizedMeasure >= threshold;
10126
- }
10127
- }
10128
- if (mapInfo.readProgressMeasure && globalObjective.progressMeasureKnown) {
10129
- objective.progressMeasure = globalObjective.progressMeasure;
10130
- objective.progressMeasureStatus = true;
10131
- }
10132
- if (mapInfo.readCompletionStatus && globalObjective.completionStatusKnown) {
10133
- objective.completionStatus = globalObjective.completionStatus;
10134
- }
10517
+ const readState = GlobalObjectiveSynchronizer.getGlobalObjectiveReadState(
10518
+ activity,
10519
+ objective,
10520
+ mapInfo,
10521
+ globalObjective
10522
+ );
10523
+ this.applyGlobalObjectiveReadState(objective, readState);
10135
10524
  if (objective.isPrimary) {
10136
10525
  objective.applyToActivity(activity);
10137
10526
  }
10138
- if (mapInfo.writeSatisfiedStatus && objective.measureStatus) {
10139
- globalObjective.satisfiedStatus = objective.satisfiedStatus;
10140
- globalObjective.satisfiedStatusKnown = true;
10141
- }
10142
- if (mapInfo.writeNormalizedMeasure && objective.measureStatus) {
10143
- globalObjective.normalizedMeasure = objective.normalizedMeasure;
10144
- globalObjective.normalizedMeasureKnown = true;
10145
- if (globalObjective.satisfiedByMeasure || objective.satisfiedByMeasure) {
10146
- const threshold = objective.minNormalizedMeasure ?? activity.scaledPassingScore ?? 0.7;
10147
- globalObjective.satisfiedStatus = objective.normalizedMeasure >= threshold;
10527
+ if (this.canWriteGlobalObjectives(activity)) {
10528
+ if (mapInfo.writeSatisfiedStatus && this.hasKnownSatisfiedStatus(objective)) {
10529
+ globalObjective.satisfiedStatus = objective.satisfiedStatus;
10148
10530
  globalObjective.satisfiedStatusKnown = true;
10149
10531
  }
10150
- }
10151
- if (mapInfo.writeCompletionStatus && objective.completionStatus !== CompletionStatus.UNKNOWN) {
10152
- globalObjective.completionStatus = objective.completionStatus;
10153
- globalObjective.completionStatusKnown = true;
10154
- }
10155
- if (mapInfo.writeProgressMeasure && objective.progressMeasureStatus) {
10156
- globalObjective.progressMeasure = objective.progressMeasure;
10157
- globalObjective.progressMeasureKnown = true;
10158
- }
10159
- if (mapInfo.updateAttemptData) {
10160
- this.updateActivityAttemptData(activity, globalObjective, objective);
10532
+ if (mapInfo.writeNormalizedMeasure && objective.measureStatus) {
10533
+ globalObjective.normalizedMeasure = objective.normalizedMeasure;
10534
+ globalObjective.normalizedMeasureKnown = true;
10535
+ if (mapInfo.writeSatisfiedStatus && objective.satisfiedByMeasure) {
10536
+ const threshold = objective.minNormalizedMeasure ?? activity.scaledPassingScore ?? 0.7;
10537
+ globalObjective.satisfiedStatus = objective.normalizedMeasure >= threshold;
10538
+ globalObjective.satisfiedStatusKnown = true;
10539
+ }
10540
+ }
10541
+ if (mapInfo.writeCompletionStatus && objective.completionStatus !== CompletionStatus.UNKNOWN) {
10542
+ globalObjective.completionStatus = objective.completionStatus;
10543
+ globalObjective.completionStatusKnown = true;
10544
+ }
10545
+ if (mapInfo.writeRawScore && objective.rawScoreKnown) {
10546
+ globalObjective.rawScore = objective.rawScore;
10547
+ globalObjective.rawScoreKnown = true;
10548
+ }
10549
+ if (mapInfo.writeMinScore && objective.minScoreKnown) {
10550
+ globalObjective.minScore = objective.minScore;
10551
+ globalObjective.minScoreKnown = true;
10552
+ }
10553
+ if (mapInfo.writeMaxScore && objective.maxScoreKnown) {
10554
+ globalObjective.maxScore = objective.maxScore;
10555
+ globalObjective.maxScoreKnown = true;
10556
+ }
10557
+ if (mapInfo.writeProgressMeasure && objective.progressMeasureStatus) {
10558
+ globalObjective.progressMeasure = objective.progressMeasure;
10559
+ globalObjective.progressMeasureKnown = true;
10560
+ }
10561
+ if (mapInfo.updateAttemptData) {
10562
+ this.updateActivityAttemptData(activity, globalObjective, objective);
10563
+ }
10161
10564
  }
10162
10565
  this.eventCallback?.("objective_synchronized", {
10163
10566
  activityId: activity.id,
@@ -10175,6 +10578,50 @@ class GlobalObjectiveSynchronizer {
10175
10578
  });
10176
10579
  }
10177
10580
  }
10581
+ /**
10582
+ * Project a global objective through one local objective's read mapInfo.
10583
+ *
10584
+ * @spec SCORM 2004 4th Ed. SN 3.10.3 - read maps provide access to mapped global objective fields
10585
+ * @spec SCORM 2004 4th Ed. ADLSEQ objectives extension - raw/min/max score read maps are independent fields
10586
+ */
10587
+ static getGlobalObjectiveReadState(activity, objective, mapInfo, globalObjective) {
10588
+ const readState = {};
10589
+ if (mapInfo.readSatisfiedStatus && globalObjective.satisfiedStatusKnown) {
10590
+ readState.satisfiedStatus = globalObjective.satisfiedStatus;
10591
+ }
10592
+ if (mapInfo.readNormalizedMeasure && globalObjective.normalizedMeasureKnown) {
10593
+ readState.normalizedMeasure = globalObjective.normalizedMeasure;
10594
+ if (objective.satisfiedByMeasure) {
10595
+ const threshold = objective.minNormalizedMeasure ?? activity.scaledPassingScore ?? 0.7;
10596
+ readState.satisfiedStatus = globalObjective.normalizedMeasure >= threshold;
10597
+ }
10598
+ }
10599
+ if (mapInfo.readCompletionStatus && globalObjective.completionStatusKnown) {
10600
+ readState.completionStatus = globalObjective.completionStatus;
10601
+ }
10602
+ if (mapInfo.readProgressMeasure && globalObjective.progressMeasureKnown) {
10603
+ readState.progressMeasure = globalObjective.progressMeasure;
10604
+ }
10605
+ if (mapInfo.readRawScore && globalObjective.rawScoreKnown) {
10606
+ readState.rawScore = globalObjective.rawScore;
10607
+ }
10608
+ if (mapInfo.readMinScore && globalObjective.minScoreKnown) {
10609
+ readState.minScore = globalObjective.minScore;
10610
+ }
10611
+ if (mapInfo.readMaxScore && globalObjective.maxScoreKnown) {
10612
+ readState.maxScore = globalObjective.maxScore;
10613
+ }
10614
+ return readState;
10615
+ }
10616
+ /**
10617
+ * Apply read-mapped state to an objective without marking those fields dirty.
10618
+ *
10619
+ * @spec SCORM 2004 4th Ed. SN 3.10.3 - read maps are access to global state, not local writes
10620
+ * @spec SCORM 2004 4th Ed. ADLSEQ objectives extension - score read maps do not imply score writes
10621
+ */
10622
+ applyGlobalObjectiveReadState(objective, readState) {
10623
+ objective.applyReadMappedState(readState);
10624
+ }
10178
10625
  /**
10179
10626
  * Ensure global objective entry exists
10180
10627
  *
@@ -10182,19 +10629,30 @@ class GlobalObjectiveSynchronizer {
10182
10629
  * @param targetId - Target objective ID
10183
10630
  * @param objective - Source objective
10184
10631
  * @returns The global objective entry
10632
+ *
10633
+ * @spec SCORM 2004 4th Ed. SN 3.10.3 - global objective entries hold mapped objective state
10634
+ * @spec SCORM 2004 4th Ed. ADLSEQ objectives extension - global entries hold score-map state
10185
10635
  */
10186
10636
  ensureGlobalObjectiveEntry(globalObjectives, targetId, objective) {
10187
10637
  if (!globalObjectives.has(targetId)) {
10188
10638
  globalObjectives.set(targetId, {
10189
10639
  id: targetId,
10190
- satisfiedStatus: objective.satisfiedStatus,
10191
- satisfiedStatusKnown: objective.satisfiedStatusKnown,
10192
- normalizedMeasure: objective.normalizedMeasure,
10193
- normalizedMeasureKnown: objective.measureStatus,
10194
- progressMeasure: objective.progressMeasure,
10195
- progressMeasureKnown: objective.progressMeasureStatus,
10196
- completionStatus: objective.completionStatus,
10197
- completionStatusKnown: objective.completionStatus !== CompletionStatus.UNKNOWN,
10640
+ satisfiedStatus: false,
10641
+ satisfiedStatusKnown: false,
10642
+ normalizedMeasure: 0,
10643
+ normalizedMeasureKnown: false,
10644
+ // @spec SCORM 2004 4th Ed. ADLSEQ objectives extension - score fields
10645
+ // remain unknown until their corresponding write map explicitly writes them.
10646
+ rawScore: "",
10647
+ rawScoreKnown: false,
10648
+ minScore: "",
10649
+ minScoreKnown: false,
10650
+ maxScore: "",
10651
+ maxScoreKnown: false,
10652
+ progressMeasure: 0,
10653
+ progressMeasureKnown: false,
10654
+ completionStatus: CompletionStatus.UNKNOWN,
10655
+ completionStatusKnown: false,
10198
10656
  satisfiedByMeasure: objective.satisfiedByMeasure,
10199
10657
  minNormalizedMeasure: objective.minNormalizedMeasure
10200
10658
  });
@@ -10209,6 +10667,9 @@ class GlobalObjectiveSynchronizer {
10209
10667
  *
10210
10668
  * @param objective - The objective to create default map info for
10211
10669
  * @returns Default map info
10670
+ *
10671
+ * @spec SCORM 2004 4th Ed. SN 3.10.3 - mapInfo defaults are applied before objective synchronization
10672
+ * @spec SCORM 2004 4th Ed. ADLSEQ objectives extension - score maps require explicit read/write flags
10212
10673
  */
10213
10674
  createDefaultMapInfo(objective) {
10214
10675
  return {
@@ -10221,9 +10682,34 @@ class GlobalObjectiveSynchronizer {
10221
10682
  writeCompletionStatus: true,
10222
10683
  readProgressMeasure: false,
10223
10684
  writeProgressMeasure: true,
10685
+ readRawScore: false,
10686
+ writeRawScore: false,
10687
+ readMinScore: false,
10688
+ writeMinScore: false,
10689
+ readMaxScore: false,
10690
+ writeMaxScore: false,
10224
10691
  updateAttemptData: objective.isPrimary
10225
10692
  };
10226
10693
  }
10694
+ /**
10695
+ * Return whether the local objective has known satisfaction state to write.
10696
+ *
10697
+ * @spec SCORM 2004 4th Ed. SN 4.2.1 Tracking Model - Objective Progress
10698
+ * Status identifies whether Objective Satisfied Status is known; Objective
10699
+ * Measure Status is independent measure knowledge.
10700
+ */
10701
+ hasKnownSatisfiedStatus(objective) {
10702
+ return objective.progressStatus || objective.satisfiedStatusKnown;
10703
+ }
10704
+ /**
10705
+ * Return whether this activity is allowed to write tracked state to globals.
10706
+ *
10707
+ * @spec SCORM 2004 4th Ed. SN 3.13.1 Tracked - when False, the LMS
10708
+ * "does not initialize, manage or access any tracking status information".
10709
+ */
10710
+ canWriteGlobalObjectives(activity) {
10711
+ return activity.sequencingControls.tracked !== false;
10712
+ }
10227
10713
  /**
10228
10714
  * Get local objective state
10229
10715
  *
@@ -10231,6 +10717,9 @@ class GlobalObjectiveSynchronizer {
10231
10717
  * @param objective - The objective
10232
10718
  * @param isPrimary - Whether this is the primary objective
10233
10719
  * @returns Local objective state
10720
+ *
10721
+ * @spec SCORM 2004 4th Ed. SN 3.10 Objective Description - local objective state used for synchronization
10722
+ * @spec SCORM 2004 4th Ed. ADLSEQ objectives extension - local score fields are part of mapped objective state
10234
10723
  */
10235
10724
  getLocalObjectiveState(activity, objective, isPrimary) {
10236
10725
  if (isPrimary) {
@@ -10239,6 +10728,12 @@ class GlobalObjectiveSynchronizer {
10239
10728
  satisfiedStatus: activity.objectiveSatisfiedStatus,
10240
10729
  measureStatus: activity.objectiveMeasureStatus,
10241
10730
  normalizedMeasure: activity.objectiveNormalizedMeasure,
10731
+ rawScore: objective.rawScore,
10732
+ rawScoreKnown: objective.rawScoreKnown,
10733
+ minScore: objective.minScore,
10734
+ minScoreKnown: objective.minScoreKnown,
10735
+ maxScore: objective.maxScore,
10736
+ maxScoreKnown: objective.maxScoreKnown,
10242
10737
  progressMeasure: activity.progressMeasure,
10243
10738
  progressMeasureStatus: activity.progressMeasureStatus,
10244
10739
  completionStatus: activity.completionStatus,
@@ -10250,6 +10745,12 @@ class GlobalObjectiveSynchronizer {
10250
10745
  satisfiedStatus: objective.satisfiedStatus,
10251
10746
  measureStatus: objective.measureStatus,
10252
10747
  normalizedMeasure: objective.normalizedMeasure,
10748
+ rawScore: objective.rawScore,
10749
+ rawScoreKnown: objective.rawScoreKnown,
10750
+ minScore: objective.minScore,
10751
+ minScoreKnown: objective.minScoreKnown,
10752
+ maxScore: objective.maxScore,
10753
+ maxScoreKnown: objective.maxScoreKnown,
10253
10754
  progressMeasure: objective.progressMeasure,
10254
10755
  progressMeasureStatus: objective.progressMeasureStatus,
10255
10756
  completionStatus: objective.completionStatus,
@@ -10272,7 +10773,7 @@ class GlobalObjectiveSynchronizer {
10272
10773
  (rule) => rule.action === "completed" || rule.action === "incomplete"
10273
10774
  );
10274
10775
  if (globalObjective.satisfiedStatusKnown && globalObjective.satisfiedStatus) {
10275
- if (!hasCompletionRollupRules && (activity.completionStatus === CompletionStatus.UNKNOWN || activity.completionStatus === CompletionStatus.INCOMPLETE)) {
10776
+ if (!hasCompletionRollupRules && !activity.sequencingControls.completionSetByContent && !activity.attemptProgressStatus && (activity.completionStatus === CompletionStatus.UNKNOWN || activity.completionStatus === CompletionStatus.INCOMPLETE)) {
10276
10777
  activity.completionStatus = CompletionStatus.COMPLETED;
10277
10778
  }
10278
10779
  if (activity.successStatus === "unknown") {
@@ -10646,6 +11147,26 @@ class RollupProcess {
10646
11147
  }
10647
11148
  }
10648
11149
 
11150
+ function evaluateCompletionStatusFromThreshold({
11151
+ completionThreshold,
11152
+ progressMeasure,
11153
+ storedCompletionStatus
11154
+ }) {
11155
+ if (completionThreshold !== "" && completionThreshold !== null && completionThreshold !== void 0) {
11156
+ const thresholdValue = parseFloat(String(completionThreshold));
11157
+ if (!isNaN(thresholdValue)) {
11158
+ if (progressMeasure !== "" && progressMeasure !== null && progressMeasure !== void 0) {
11159
+ const progressValue = parseFloat(String(progressMeasure));
11160
+ if (!isNaN(progressValue)) {
11161
+ return progressValue >= thresholdValue ? CompletionStatus.COMPLETED : CompletionStatus.INCOMPLETE;
11162
+ }
11163
+ }
11164
+ return CompletionStatus.UNKNOWN;
11165
+ }
11166
+ }
11167
+ return storedCompletionStatus || CompletionStatus.UNKNOWN;
11168
+ }
11169
+
10649
11170
  const VALID_COMPLETION_STATUSES = [
10650
11171
  CompletionStatus.COMPLETED,
10651
11172
  CompletionStatus.INCOMPLETE,
@@ -10698,12 +11219,43 @@ class RteDataTransferService {
10698
11219
  * Transfer primary objective data from CMI to activity
10699
11220
  * @param {Activity} activity - The activity to transfer data to
10700
11221
  * @param {CMIDataForTransfer} cmiData - CMI data from runtime
11222
+ *
11223
+ * @spec SCORM 2004 4th Ed. RTE-to-SN Data Transfer - primary objective status and score data
11224
+ * @spec SCORM 2004 4th Ed. ADLSEQ objectives extension - raw/min/max score write-map source data
10701
11225
  */
10702
11226
  transferPrimaryObjective(activity, cmiData) {
10703
- const validatedCompletionStatus = validateCompletionStatus(cmiData.completion_status);
10704
- if (validatedCompletionStatus && validatedCompletionStatus !== CompletionStatus.UNKNOWN) {
10705
- activity.completionStatus = validatedCompletionStatus;
10706
- activity.attemptProgressStatus = true;
11227
+ let hasProgressMeasure = false;
11228
+ if (cmiData.progress_measure && cmiData.progress_measure !== "") {
11229
+ const progressMeasure = parseFloat(cmiData.progress_measure);
11230
+ if (!isNaN(progressMeasure)) {
11231
+ hasProgressMeasure = true;
11232
+ activity.progressMeasure = progressMeasure;
11233
+ activity.progressMeasureStatus = true;
11234
+ activity.attemptCompletionAmount = progressMeasure;
11235
+ activity.attemptCompletionAmountStatus = true;
11236
+ if (activity.primaryObjective) {
11237
+ activity.primaryObjective.progressMeasure = progressMeasure;
11238
+ activity.primaryObjective.progressMeasureStatus = true;
11239
+ }
11240
+ }
11241
+ }
11242
+ if (!hasProgressMeasure) {
11243
+ activity.attemptCompletionAmountStatus = false;
11244
+ }
11245
+ if (activity.completedByMeasure) {
11246
+ const completionStatus = evaluateCompletionStatusFromThreshold({
11247
+ completionThreshold: activity.minProgressMeasure,
11248
+ progressMeasure: cmiData.progress_measure,
11249
+ storedCompletionStatus: CompletionStatus.UNKNOWN
11250
+ });
11251
+ activity.completionStatus = completionStatus;
11252
+ activity.attemptProgressStatus = completionStatus !== CompletionStatus.UNKNOWN;
11253
+ } else {
11254
+ const validatedCompletionStatus = validateCompletionStatus(cmiData.completion_status);
11255
+ if (validatedCompletionStatus && validatedCompletionStatus !== CompletionStatus.UNKNOWN) {
11256
+ activity.completionStatus = validatedCompletionStatus;
11257
+ activity.attemptProgressStatus = true;
11258
+ }
10707
11259
  }
10708
11260
  let hasSuccessStatus = false;
10709
11261
  let successStatus = false;
@@ -10716,9 +11268,9 @@ class RteDataTransferService {
10716
11268
  activity.objectiveSatisfiedStatus = successStatus;
10717
11269
  activity.objectiveSatisfiedStatusKnown = true;
10718
11270
  activity.successStatus = validatedSuccessStatus;
10719
- activity.objectiveMeasureStatus = true;
10720
11271
  }
10721
11272
  if (cmiData.score) {
11273
+ activity.primaryObjective?.initializeScoreFromCMI(this.getObjectiveScoreState(cmiData.score));
10722
11274
  const normalized = this.normalizeScore(cmiData.score);
10723
11275
  if (normalized !== null) {
10724
11276
  normalizedScore = normalized;
@@ -10730,30 +11282,25 @@ class RteDataTransferService {
10730
11282
  if (activity.primaryObjective && (hasSuccessStatus || hasNormalizedMeasure)) {
10731
11283
  const finalStatus = hasSuccessStatus ? successStatus : activity.primaryObjective.satisfiedStatus;
10732
11284
  const finalMeasure = hasNormalizedMeasure ? normalizedScore : activity.primaryObjective.normalizedMeasure;
10733
- const measureStatus = hasSuccessStatus || hasNormalizedMeasure;
11285
+ const measureStatus = hasNormalizedMeasure;
10734
11286
  activity.primaryObjective.initializeFromCMI(finalStatus, finalMeasure, measureStatus);
10735
11287
  if (hasSuccessStatus) {
10736
11288
  activity.primaryObjective.satisfiedStatusKnown = true;
10737
11289
  activity.primaryObjective.progressStatus = true;
10738
11290
  }
10739
11291
  }
10740
- if (cmiData.progress_measure && cmiData.progress_measure !== "") {
10741
- const progressMeasure = parseFloat(cmiData.progress_measure);
10742
- if (!isNaN(progressMeasure)) {
10743
- activity.progressMeasure = progressMeasure;
10744
- activity.progressMeasureStatus = true;
10745
- if (activity.primaryObjective) {
10746
- activity.primaryObjective.progressMeasure = progressMeasure;
10747
- activity.primaryObjective.progressMeasureStatus = true;
10748
- }
10749
- }
10750
- }
10751
11292
  }
10752
11293
  /**
10753
- * Transfer non-primary objective data from CMI to activity objectives
10754
- * Only transfers changed values to protect global objectives
11294
+ * Transfer objective-array data from CMI to matching activity objectives.
11295
+ * Only transfers changed values to protect global objectives.
10755
11296
  * @param {Activity} activity - The activity to transfer data to
10756
11297
  * @param {CMIDataForTransfer} cmiData - CMI data from runtime
11298
+ *
11299
+ * @spec SCORM 2004 4th Ed. RTE 4.2.17 - cmi.objectives.n data, including
11300
+ * the primary objective entry, is part of the RTE objective data model.
11301
+ * @spec SCORM 2004 4th Ed. SN 3.10.3 and ADLSEQ objectives extension -
11302
+ * mapped objective status and raw/min/max score data transfer through
11303
+ * objective maps to sequencing state.
10757
11304
  */
10758
11305
  transferNonPrimaryObjectives(activity, cmiData) {
10759
11306
  if (!cmiData.objectives || cmiData.objectives.length === 0) {
@@ -10764,14 +11311,17 @@ class RteDataTransferService {
10764
11311
  continue;
10765
11312
  }
10766
11313
  const activityObjectiveMatch = activity.getObjectiveById(cmiObjective.id);
10767
- if (!activityObjectiveMatch || activityObjectiveMatch.isPrimary) {
11314
+ if (!activityObjectiveMatch) {
10768
11315
  continue;
10769
11316
  }
10770
11317
  const activityObjective = activityObjectiveMatch.objective;
11318
+ const isPrimaryObjective = activityObjectiveMatch.isPrimary;
10771
11319
  let hasSuccessStatus = false;
10772
11320
  let successStatus = false;
10773
11321
  let hasNormalizedMeasure = false;
10774
11322
  let normalizedScore = 0;
11323
+ let hasCompletionStatus = false;
11324
+ let hasProgressMeasure = false;
10775
11325
  const validatedObjSuccessStatus = validateSuccessStatus(cmiObjective.success_status);
10776
11326
  if (validatedObjSuccessStatus && validatedObjSuccessStatus !== SuccessStatus.UNKNOWN) {
10777
11327
  successStatus = validatedObjSuccessStatus === SuccessStatus.PASSED;
@@ -10781,8 +11331,10 @@ class RteDataTransferService {
10781
11331
  const validatedObjCompletionStatus = validateCompletionStatus(cmiObjective.completion_status);
10782
11332
  if (validatedObjCompletionStatus && validatedObjCompletionStatus !== CompletionStatus.UNKNOWN) {
10783
11333
  activityObjective.completionStatus = validatedObjCompletionStatus;
11334
+ hasCompletionStatus = true;
10784
11335
  }
10785
11336
  if (cmiObjective.score) {
11337
+ activityObjective.initializeScoreFromCMI(this.getObjectiveScoreState(cmiObjective.score));
10786
11338
  const normalized = this.normalizeScore(cmiObjective.score);
10787
11339
  if (normalized !== null) {
10788
11340
  normalizedScore = normalized;
@@ -10794,12 +11346,29 @@ class RteDataTransferService {
10794
11346
  const finalMeasure = hasNormalizedMeasure ? normalizedScore : activityObjective.normalizedMeasure;
10795
11347
  const measureStatus = hasNormalizedMeasure;
10796
11348
  activityObjective.initializeFromCMI(finalStatus, finalMeasure, measureStatus);
11349
+ if (hasSuccessStatus) {
11350
+ activityObjective.satisfiedStatusKnown = true;
11351
+ }
10797
11352
  }
10798
11353
  if (cmiObjective.progress_measure && cmiObjective.progress_measure !== "") {
10799
11354
  const progressMeasure = parseFloat(cmiObjective.progress_measure);
10800
11355
  if (!isNaN(progressMeasure)) {
10801
11356
  activityObjective.progressMeasure = progressMeasure;
10802
11357
  activityObjective.progressMeasureStatus = true;
11358
+ hasProgressMeasure = true;
11359
+ }
11360
+ }
11361
+ if (isPrimaryObjective && (hasSuccessStatus || hasNormalizedMeasure || hasCompletionStatus || hasProgressMeasure)) {
11362
+ activityObjective.applyToActivity(activity);
11363
+ if (validatedObjSuccessStatus && validatedObjSuccessStatus !== SuccessStatus.UNKNOWN) {
11364
+ activity.successStatus = validatedObjSuccessStatus;
11365
+ }
11366
+ if (hasCompletionStatus) {
11367
+ activity.attemptProgressStatus = true;
11368
+ }
11369
+ if (hasProgressMeasure) {
11370
+ activity.attemptCompletionAmount = activityObjective.progressMeasure;
11371
+ activity.attemptCompletionAmountStatus = true;
10803
11372
  }
10804
11373
  }
10805
11374
  }
@@ -10828,6 +11397,25 @@ class RteDataTransferService {
10828
11397
  }
10829
11398
  return null;
10830
11399
  }
11400
+ /**
11401
+ * Convert RTE score data into objective score-map state without numeric reformatting.
11402
+ *
11403
+ * @spec SCORM 2004 4th Ed. RTE-to-SN Data Transfer - score values transfer from RTE to sequencing state
11404
+ * @spec SCORM 2004 4th Ed. ADLSEQ objectives extension - raw/min/max score map fields are independent
11405
+ */
11406
+ getObjectiveScoreState(score) {
11407
+ const scoreState = {};
11408
+ if (score.raw !== void 0) {
11409
+ scoreState.rawScore = score.raw;
11410
+ }
11411
+ if (score.min !== void 0) {
11412
+ scoreState.minScore = score.min;
11413
+ }
11414
+ if (score.max !== void 0) {
11415
+ scoreState.maxScore = score.max;
11416
+ }
11417
+ return scoreState;
11418
+ }
10831
11419
  }
10832
11420
 
10833
11421
  class TerminationHandler {
@@ -10952,6 +11540,18 @@ class TerminationHandler {
10952
11540
  this.endAttempt(this.activityTree.currentActivity);
10953
11541
  }
10954
11542
  }
11543
+ const ancestorExitResult = this.applyAncestorExitActionRules(currentActivity);
11544
+ if (ancestorExitResult.action === "EXIT_ALL") {
11545
+ return this.handleExitAll(ancestorExitResult.activity || currentActivity);
11546
+ }
11547
+ if (ancestorExitResult.exception) {
11548
+ return {
11549
+ terminationRequest: SequencingRequestType.EXIT,
11550
+ sequencingRequest: null,
11551
+ exception: ancestorExitResult.exception,
11552
+ valid: false
11553
+ };
11554
+ }
10955
11555
  let processedExit;
10956
11556
  let postConditionResult;
10957
11557
  do {
@@ -11177,18 +11777,10 @@ class TerminationHandler {
11177
11777
  recursionDepth++;
11178
11778
  const exitRules = activity.sequencingRules.exitConditionRules;
11179
11779
  for (const rule of exitRules) {
11180
- let conditionsMet;
11181
- if (rule.conditionCombination === "all") {
11182
- conditionsMet = rule.conditions.every(
11183
- (condition) => condition.evaluate(activity)
11184
- );
11185
- } else {
11186
- conditionsMet = rule.conditions.some(
11187
- (condition) => condition.evaluate(activity)
11188
- );
11189
- }
11190
- if (conditionsMet) {
11191
- if (rule.action === RuleActionType.EXIT_PARENT) {
11780
+ if (rule.evaluate(activity)) {
11781
+ if (rule.action === RuleActionType.EXIT) {
11782
+ return { action: "EXIT", recursionDepth };
11783
+ } else if (rule.action === RuleActionType.EXIT_PARENT) {
11192
11784
  return { action: "EXIT_PARENT", recursionDepth };
11193
11785
  } else if (rule.action === RuleActionType.EXIT_ALL) {
11194
11786
  return { action: "EXIT_ALL", recursionDepth };
@@ -11222,6 +11814,55 @@ class TerminationHandler {
11222
11814
  this.processExitAtLevel(rootActivity, 0);
11223
11815
  this.terminateAll(rootActivity);
11224
11816
  }
11817
+ /**
11818
+ * Sequencing Exit Action Rules Subprocess (TB.2.1) ancestor walk
11819
+ * @spec SN Book: TB.2.1 (Sequencing Exit Action Rules Subprocess)
11820
+ * @param {Activity} terminatingActivity - The activity whose attempt just ended
11821
+ * @return {{action: string | null, activity?: Activity, exception?: string}}
11822
+ */
11823
+ applyAncestorExitActionRules(terminatingActivity) {
11824
+ const activityPath = [];
11825
+ let current = terminatingActivity.parent;
11826
+ while (current) {
11827
+ activityPath.unshift(current);
11828
+ current = current.parent;
11829
+ }
11830
+ for (const activity of activityPath) {
11831
+ const exitAction = this.exitActionRulesSubprocess(activity);
11832
+ if (exitAction === "EXIT_ALL") {
11833
+ this.fireEvent("onAncestorExitAction", {
11834
+ activity: activity.id,
11835
+ action: exitAction
11836
+ });
11837
+ return { action: "EXIT_ALL", activity };
11838
+ }
11839
+ if (exitAction === "EXIT_PARENT") {
11840
+ if (!activity.parent) {
11841
+ return { action: "EXIT_PARENT", activity, exception: "TB.2.3-4" };
11842
+ }
11843
+ this.terminateDescendants(activity.parent);
11844
+ this.activityTree.currentActivity = activity.parent;
11845
+ this.endAttempt(activity.parent);
11846
+ this.fireEvent("onAncestorExitAction", {
11847
+ activity: activity.id,
11848
+ action: exitAction,
11849
+ currentActivity: activity.parent.id
11850
+ });
11851
+ return { action: "EXIT_PARENT", activity: activity.parent };
11852
+ }
11853
+ if (exitAction === "EXIT") {
11854
+ this.terminateDescendants(activity);
11855
+ this.activityTree.currentActivity = activity;
11856
+ this.endAttempt(activity);
11857
+ this.fireEvent("onAncestorExitAction", {
11858
+ activity: activity.id,
11859
+ action: exitAction
11860
+ });
11861
+ return { action: "EXIT", activity };
11862
+ }
11863
+ }
11864
+ return { action: null };
11865
+ }
11225
11866
  /**
11226
11867
  * Process exit actions at specific level
11227
11868
  * @param {Activity} activity - Activity to process
@@ -11302,18 +11943,10 @@ class TerminationHandler {
11302
11943
  exitActionRulesSubprocess(activity) {
11303
11944
  const exitRules = activity.sequencingRules.exitConditionRules;
11304
11945
  for (const rule of exitRules) {
11305
- let conditionsMet;
11306
- if (rule.conditionCombination === "all") {
11307
- conditionsMet = rule.conditions.every(
11308
- (condition) => condition.evaluate(activity)
11309
- );
11310
- } else {
11311
- conditionsMet = rule.conditions.some(
11312
- (condition) => condition.evaluate(activity)
11313
- );
11314
- }
11315
- if (conditionsMet) {
11316
- if (rule.action === RuleActionType.EXIT_PARENT) {
11946
+ if (rule.evaluate(activity)) {
11947
+ if (rule.action === RuleActionType.EXIT) {
11948
+ return "EXIT";
11949
+ } else if (rule.action === RuleActionType.EXIT_PARENT) {
11317
11950
  return "EXIT_PARENT";
11318
11951
  } else if (rule.action === RuleActionType.EXIT_ALL) {
11319
11952
  return "EXIT_ALL";
@@ -12829,6 +13462,9 @@ class GlobalObjectiveService {
12829
13462
  * Collect Global Objectives
12830
13463
  * Recursively collects global objectives from the activity tree
12831
13464
  * @param {Activity} activity - Activity to collect objectives from
13465
+ *
13466
+ * @spec SCORM 2004 4th Ed. SN 3.10.3 - global objective map contains mapped objective state
13467
+ * @spec SCORM 2004 4th Ed. ADLSEQ objectives extension - raw/min/max score fields are unknown until written
12832
13468
  */
12833
13469
  collectObjectives(activity) {
12834
13470
  const objectives = activity.getAllObjectives();
@@ -12838,9 +13474,15 @@ class GlobalObjectiveService {
12838
13474
  this.globalObjectiveMap.set(defaultId, {
12839
13475
  id: defaultId,
12840
13476
  satisfiedStatus: activity.objectiveSatisfiedStatus,
12841
- satisfiedStatusKnown: activity.objectiveMeasureStatus,
13477
+ satisfiedStatusKnown: activity.objectiveSatisfiedStatusKnown,
12842
13478
  normalizedMeasure: activity.objectiveNormalizedMeasure,
12843
13479
  normalizedMeasureKnown: activity.objectiveMeasureStatus,
13480
+ rawScore: "",
13481
+ rawScoreKnown: false,
13482
+ minScore: "",
13483
+ minScoreKnown: false,
13484
+ maxScore: "",
13485
+ maxScoreKnown: false,
12844
13486
  progressMeasure: activity.progressMeasure,
12845
13487
  progressMeasureKnown: activity.progressMeasureStatus,
12846
13488
  completionStatus: activity.completionStatus,
@@ -12853,6 +13495,12 @@ class GlobalObjectiveService {
12853
13495
  writeCompletionStatus: true,
12854
13496
  readProgressMeasure: true,
12855
13497
  writeProgressMeasure: true,
13498
+ readRawScore: false,
13499
+ writeRawScore: false,
13500
+ readMinScore: false,
13501
+ writeMinScore: false,
13502
+ readMaxScore: false,
13503
+ writeMaxScore: false,
12856
13504
  satisfiedByMeasure: activity.scaledPassingScore !== null,
12857
13505
  minNormalizedMeasure: activity.scaledPassingScore,
12858
13506
  updateAttemptData: true
@@ -12880,9 +13528,15 @@ class GlobalObjectiveService {
12880
13528
  this.globalObjectiveMap.set(targetId, {
12881
13529
  id: targetId,
12882
13530
  satisfiedStatus: objective.satisfiedStatus,
12883
- satisfiedStatusKnown: objective.measureStatus,
13531
+ satisfiedStatusKnown: objective.satisfiedStatusKnown || objective.progressStatus,
12884
13532
  normalizedMeasure: objective.normalizedMeasure,
12885
13533
  normalizedMeasureKnown: objective.measureStatus,
13534
+ rawScore: "",
13535
+ rawScoreKnown: false,
13536
+ minScore: "",
13537
+ minScoreKnown: false,
13538
+ maxScore: "",
13539
+ maxScoreKnown: false,
12886
13540
  progressMeasure: objective.progressMeasure,
12887
13541
  progressMeasureKnown: objective.progressMeasureStatus,
12888
13542
  completionStatus: objective.completionStatus,
@@ -14865,27 +15519,44 @@ class SequencingService {
14865
15519
  }
14866
15520
  /**
14867
15521
  * Update activity properties from current CMI values
15522
+ *
15523
+ * @spec SCORM 2004 4th Ed. RTE-to-SN Data Transfer - current CMI values update activity objective state
15524
+ * @spec SCORM 2004 4th Ed. ADLSEQ objectives extension - raw/min/max scores are available to objective write maps
14868
15525
  */
14869
15526
  updateActivityFromCMI(activity) {
14870
- if (this.cmi.completion_status !== "unknown") {
15527
+ let hasProgressMeasure = false;
15528
+ if (this.cmi.progress_measure !== "") {
15529
+ const progressMeasure = parseFloat(this.cmi.progress_measure);
15530
+ if (!isNaN(progressMeasure)) {
15531
+ hasProgressMeasure = true;
15532
+ activity.progressMeasure = progressMeasure;
15533
+ activity.progressMeasureStatus = true;
15534
+ activity.attemptCompletionAmount = progressMeasure;
15535
+ activity.attemptCompletionAmountStatus = true;
15536
+ }
15537
+ }
15538
+ if (!hasProgressMeasure) {
15539
+ activity.attemptCompletionAmountStatus = false;
15540
+ }
15541
+ if (activity.completedByMeasure) {
15542
+ const completionStatus = evaluateCompletionStatusFromThreshold({
15543
+ completionThreshold: activity.minProgressMeasure,
15544
+ progressMeasure: this.cmi.progress_measure,
15545
+ storedCompletionStatus: CompletionStatus.UNKNOWN
15546
+ });
15547
+ activity.completionStatus = completionStatus;
15548
+ activity.attemptProgressStatus = completionStatus !== CompletionStatus.UNKNOWN;
15549
+ } else if (this.cmi.completion_status !== "unknown") {
14871
15550
  activity.completionStatus = this.cmi.completion_status;
14872
15551
  activity.attemptProgressStatus = true;
14873
15552
  }
14874
15553
  if (this.cmi.success_status !== "unknown") {
14875
15554
  activity.successStatus = this.cmi.success_status;
14876
15555
  activity.objectiveSatisfiedStatus = this.cmi.success_status === "passed";
14877
- activity.objectiveMeasureStatus = true;
14878
15556
  if (activity.primaryObjective) {
14879
15557
  activity.primaryObjective.progressStatus = true;
14880
15558
  }
14881
15559
  }
14882
- if (this.cmi.progress_measure !== "") {
14883
- const progressMeasure = parseFloat(this.cmi.progress_measure);
14884
- if (!isNaN(progressMeasure)) {
14885
- activity.progressMeasure = progressMeasure;
14886
- activity.progressMeasureStatus = true;
14887
- }
14888
- }
14889
15560
  if (this.cmi.score && this.cmi.score.scaled !== "") {
14890
15561
  const scaledScore = parseFloat(this.cmi.score.scaled);
14891
15562
  if (!isNaN(scaledScore)) {
@@ -14896,6 +15567,17 @@ class SequencingService {
14896
15567
  }
14897
15568
  }
14898
15569
  }
15570
+ if (activity.primaryObjective && this.cmi.score) {
15571
+ if (this.cmi.score.raw !== "") {
15572
+ activity.primaryObjective.rawScore = this.cmi.score.raw;
15573
+ }
15574
+ if (this.cmi.score.min !== "") {
15575
+ activity.primaryObjective.minScore = this.cmi.score.min;
15576
+ }
15577
+ if (this.cmi.score.max !== "") {
15578
+ activity.primaryObjective.maxScore = this.cmi.score.max;
15579
+ }
15580
+ }
14899
15581
  if (activity.primaryObjective) {
14900
15582
  activity.primaryObjective.updateFromActivity(activity);
14901
15583
  }
@@ -19573,7 +20255,7 @@ class CMIInteractionsObject extends BaseCMI {
19573
20255
  if (check2004ValidFormat(
19574
20256
  this._cmi_element + ".description",
19575
20257
  description,
19576
- scorm2004_regex.CMILangString250,
20258
+ scorm2004_regex.CMILangString,
19577
20259
  true
19578
20260
  )) {
19579
20261
  this._description = description;
@@ -20283,7 +20965,7 @@ class CMIObjectivesObject extends BaseCMI {
20283
20965
  if (check2004ValidFormat(
20284
20966
  this._cmi_element + ".description",
20285
20967
  description,
20286
- scorm2004_regex.CMILangString250,
20968
+ scorm2004_regex.CMILangString,
20287
20969
  true
20288
20970
  )) {
20289
20971
  this._description = description;
@@ -23014,22 +23696,11 @@ class Scorm2004CMIHandler {
23014
23696
  * @returns {string} The evaluated completion status
23015
23697
  */
23016
23698
  evaluateCompletionStatus() {
23017
- const threshold = this.context.cmi.completion_threshold;
23018
- const progressMeasure = this.context.cmi.progress_measure;
23019
- const storedStatus = this.context.cmi.completion_status;
23020
- if (threshold !== "" && threshold !== null && threshold !== void 0) {
23021
- const thresholdValue = parseFloat(String(threshold));
23022
- if (!isNaN(thresholdValue)) {
23023
- if (progressMeasure !== "" && progressMeasure !== null && progressMeasure !== void 0) {
23024
- const progressValue = parseFloat(String(progressMeasure));
23025
- if (!isNaN(progressValue)) {
23026
- return progressValue >= thresholdValue ? CompletionStatus.COMPLETED : CompletionStatus.INCOMPLETE;
23027
- }
23028
- }
23029
- return CompletionStatus.UNKNOWN;
23030
- }
23031
- }
23032
- return storedStatus || CompletionStatus.UNKNOWN;
23699
+ return evaluateCompletionStatusFromThreshold({
23700
+ completionThreshold: this.context.cmi.completion_threshold,
23701
+ progressMeasure: this.context.cmi.progress_measure,
23702
+ storedCompletionStatus: this.context.cmi.completion_status
23703
+ });
23033
23704
  }
23034
23705
  /**
23035
23706
  * Evaluates success_status per SCORM 2004 RTE Table 4.2.21.1a
@@ -23137,6 +23808,9 @@ class SequencingConfigurationBuilder {
23137
23808
  if (settings.objectiveSetByContent !== void 0) {
23138
23809
  target.objectiveSetByContent = settings.objectiveSetByContent;
23139
23810
  }
23811
+ if (settings.tracked !== void 0) {
23812
+ target.tracked = settings.tracked;
23813
+ }
23140
23814
  }
23141
23815
  /**
23142
23816
  * Applies the sequencing rules settings to the specified target object.
@@ -23674,6 +24348,12 @@ class ActivityTreeBuilder {
23674
24348
  activitySettings.sequencingControls
23675
24349
  );
23676
24350
  }
24351
+ if (activitySettings.deliveryControls) {
24352
+ this.sequencingConfigBuilder.applySequencingControlsSettings(
24353
+ activity.sequencingControls,
24354
+ activitySettings.deliveryControls
24355
+ );
24356
+ }
23677
24357
  if (activitySettings.sequencingRules) {
23678
24358
  this.sequencingConfigBuilder.applySequencingRulesSettings(
23679
24359
  activity.sequencingRules,
@@ -23689,6 +24369,21 @@ class ActivityTreeBuilder {
23689
24369
  if (activitySettings.rollupConsiderations) {
23690
24370
  activity.applyRollupConsiderations(activitySettings.rollupConsiderations);
23691
24371
  }
24372
+ if (activitySettings.completionThreshold) {
24373
+ const threshold = activitySettings.completionThreshold;
24374
+ if (threshold.completedByMeasure !== void 0) {
24375
+ activity.completedByMeasure = threshold.completedByMeasure;
24376
+ }
24377
+ if (threshold.minProgressMeasure !== void 0) {
24378
+ activity.minProgressMeasure = threshold.minProgressMeasure;
24379
+ activity.completionThreshold = threshold.minProgressMeasure.toString();
24380
+ } else if (threshold.completedByMeasure) {
24381
+ activity.completionThreshold = activity.minProgressMeasure.toString();
24382
+ }
24383
+ if (threshold.progressWeight !== void 0) {
24384
+ activity.progressWeight = threshold.progressWeight;
24385
+ }
24386
+ }
23692
24387
  if (activitySettings.hideLmsUi) {
23693
24388
  const mergedHide = this.sequencingConfigBuilder.mergeHideLmsUi(
23694
24389
  activity.hideLmsUi,
@@ -23748,19 +24443,19 @@ class ActivityTreeBuilder {
23748
24443
  createActivityObjectiveFromSettings(objectiveSettings, isPrimary) {
23749
24444
  const mapInfo = (objectiveSettings.mapInfo || []).map((info) => ({
23750
24445
  targetObjectiveID: info.targetObjectiveID,
23751
- readSatisfiedStatus: info.readSatisfiedStatus ?? false,
23752
- readNormalizedMeasure: info.readNormalizedMeasure ?? false,
24446
+ readSatisfiedStatus: info.readSatisfiedStatus ?? true,
24447
+ readNormalizedMeasure: info.readNormalizedMeasure ?? true,
23753
24448
  writeSatisfiedStatus: info.writeSatisfiedStatus ?? false,
23754
24449
  writeNormalizedMeasure: info.writeNormalizedMeasure ?? false,
23755
- readCompletionStatus: info.readCompletionStatus ?? false,
24450
+ readCompletionStatus: info.readCompletionStatus ?? true,
23756
24451
  writeCompletionStatus: info.writeCompletionStatus ?? false,
23757
- readProgressMeasure: info.readProgressMeasure ?? false,
24452
+ readProgressMeasure: info.readProgressMeasure ?? true,
23758
24453
  writeProgressMeasure: info.writeProgressMeasure ?? false,
23759
- readRawScore: info.readRawScore ?? false,
24454
+ readRawScore: info.readRawScore ?? true,
23760
24455
  writeRawScore: info.writeRawScore ?? false,
23761
- readMinScore: info.readMinScore ?? false,
24456
+ readMinScore: info.readMinScore ?? true,
23762
24457
  writeMinScore: info.writeMinScore ?? false,
23763
- readMaxScore: info.readMaxScore ?? false,
24458
+ readMaxScore: info.readMaxScore ?? true,
23764
24459
  writeMaxScore: info.writeMaxScore ?? false,
23765
24460
  updateAttemptData: info.updateAttemptData ?? false
23766
24461
  }));
@@ -23942,6 +24637,9 @@ class GlobalObjectiveManager {
23942
24637
  *
23943
24638
  * @param {string} objectiveId - The global objective ID
23944
24639
  * @param {CMIObjectivesObject} objective - The CMI objective object with updated values
24640
+ *
24641
+ * @spec SCORM 2004 4th Ed. SN 3.10.3 - global objectives synchronize CMI objective data
24642
+ * @spec SCORM 2004 4th Ed. ADLSEQ objectives extension - raw/min/max score fields synchronize independently
23945
24643
  */
23946
24644
  updateGlobalObjectiveFromCMI(objectiveId, objective) {
23947
24645
  if (!objectiveId || !this.context.sequencingService) {
@@ -23967,6 +24665,18 @@ class GlobalObjectiveManager {
23967
24665
  updatePayload.normalizedMeasure = normalizedMeasure;
23968
24666
  updatePayload.normalizedMeasureKnown = true;
23969
24667
  }
24668
+ if (objective.score?.raw !== void 0 && objective.score.raw !== "") {
24669
+ updatePayload.rawScore = objective.score.raw;
24670
+ updatePayload.rawScoreKnown = true;
24671
+ }
24672
+ if (objective.score?.min !== void 0 && objective.score.min !== "") {
24673
+ updatePayload.minScore = objective.score.min;
24674
+ updatePayload.minScoreKnown = true;
24675
+ }
24676
+ if (objective.score?.max !== void 0 && objective.score.max !== "") {
24677
+ updatePayload.maxScore = objective.score.max;
24678
+ updatePayload.maxScoreKnown = true;
24679
+ }
23970
24680
  const progressMeasure = this.parseObjectiveNumber(objective.progress_measure);
23971
24681
  if (progressMeasure !== null) {
23972
24682
  updatePayload.progressMeasure = progressMeasure;
@@ -23986,12 +24696,21 @@ class GlobalObjectiveManager {
23986
24696
  *
23987
24697
  * @param {CMIObjectivesObject} objective - The CMI objectives object containing data about a specific learning objective.
23988
24698
  * @return {GlobalObjectiveMapEntry} An object containing mapped properties and their values based on the provided objective.
24699
+ *
24700
+ * @spec SCORM 2004 4th Ed. SN 3.10.3 - global objective map entries capture objective state
24701
+ * @spec SCORM 2004 4th Ed. ADLSEQ objectives extension - raw/min/max score entries carry per-field known flags
23989
24702
  */
23990
24703
  buildObjectiveMapEntryFromCMI(objective) {
23991
24704
  const entry = {
23992
24705
  id: objective.id,
23993
24706
  satisfiedStatusKnown: false,
23994
24707
  normalizedMeasureKnown: false,
24708
+ rawScore: "",
24709
+ rawScoreKnown: false,
24710
+ minScore: "",
24711
+ minScoreKnown: false,
24712
+ maxScore: "",
24713
+ maxScoreKnown: false,
23995
24714
  progressMeasureKnown: false,
23996
24715
  completionStatusKnown: false,
23997
24716
  readSatisfiedStatus: true,
@@ -24001,7 +24720,13 @@ class GlobalObjectiveManager {
24001
24720
  readCompletionStatus: true,
24002
24721
  writeCompletionStatus: true,
24003
24722
  readProgressMeasure: true,
24004
- writeProgressMeasure: true
24723
+ writeProgressMeasure: true,
24724
+ readRawScore: true,
24725
+ writeRawScore: true,
24726
+ readMinScore: true,
24727
+ writeMinScore: true,
24728
+ readMaxScore: true,
24729
+ writeMaxScore: true
24005
24730
  };
24006
24731
  if (objective.success_status && objective.success_status !== SuccessStatus.UNKNOWN) {
24007
24732
  entry.satisfiedStatus = objective.success_status === SuccessStatus.PASSED;
@@ -24012,6 +24737,18 @@ class GlobalObjectiveManager {
24012
24737
  entry.normalizedMeasure = normalizedMeasure;
24013
24738
  entry.normalizedMeasureKnown = true;
24014
24739
  }
24740
+ if (objective.score?.raw !== void 0 && objective.score.raw !== "") {
24741
+ entry.rawScore = objective.score.raw;
24742
+ entry.rawScoreKnown = true;
24743
+ }
24744
+ if (objective.score?.min !== void 0 && objective.score.min !== "") {
24745
+ entry.minScore = objective.score.min;
24746
+ entry.minScoreKnown = true;
24747
+ }
24748
+ if (objective.score?.max !== void 0 && objective.score.max !== "") {
24749
+ entry.maxScore = objective.score.max;
24750
+ entry.maxScoreKnown = true;
24751
+ }
24015
24752
  const progressMeasure = this.parseObjectiveNumber(objective.progress_measure);
24016
24753
  if (progressMeasure !== null) {
24017
24754
  entry.progressMeasure = progressMeasure;
@@ -24032,6 +24769,9 @@ class GlobalObjectiveManager {
24032
24769
  * @return {CMIObjectivesObject[]} An array of `CMIObjectivesObject` instances built
24033
24770
  * from the provided snapshot map. Returns an empty array
24034
24771
  * if the snapshot is invalid or no valid objectives can be created.
24772
+ *
24773
+ * @spec SCORM 2004 4th Ed. SN 3.10.3 - global objective snapshots restore CMI objective state
24774
+ * @spec SCORM 2004 4th Ed. ADLSEQ objectives extension - raw/min/max score known fields restore independently
24035
24775
  */
24036
24776
  buildCMIObjectivesFromMap(snapshot) {
24037
24777
  const objectives = [];
@@ -24051,6 +24791,15 @@ class GlobalObjectiveManager {
24051
24791
  if (entry.normalizedMeasureKnown === true && normalizedMeasure !== null) {
24052
24792
  objective.score.scaled = String(normalizedMeasure);
24053
24793
  }
24794
+ if (entry.rawScoreKnown === true) {
24795
+ objective.score.raw = String(entry.rawScore ?? "");
24796
+ }
24797
+ if (entry.minScoreKnown === true) {
24798
+ objective.score.min = String(entry.minScore ?? "");
24799
+ }
24800
+ if (entry.maxScoreKnown === true) {
24801
+ objective.score.max = String(entry.maxScore ?? "");
24802
+ }
24054
24803
  const progressMeasure = this.parseObjectiveNumber(entry.progressMeasure);
24055
24804
  if (entry.progressMeasureKnown === true && progressMeasure !== null) {
24056
24805
  objective.progress_measure = String(progressMeasure);
@@ -24161,6 +24910,9 @@ class GlobalObjectiveManager {
24161
24910
  * @param {CompletionStatus} completionStatus
24162
24911
  * @param {SuccessStatus} successStatus
24163
24912
  * @param {ScoreObject} scoreObject
24913
+ *
24914
+ * @spec SCORM 2004 4th Ed. RTE-to-SN Data Transfer - CMI score data updates the current primary objective
24915
+ * @spec SCORM 2004 4th Ed. ADLSEQ objectives extension - raw/min/max score data is available for write maps
24164
24916
  */
24165
24917
  syncCmiToSequencingActivity(completionStatus, successStatus, scoreObject) {
24166
24918
  if (!this.context.sequencing) {
@@ -24186,6 +24938,15 @@ class GlobalObjectiveManager {
24186
24938
  primaryObjective.normalizedMeasure = scoreObject.scaled;
24187
24939
  primaryObjective.measureStatus = true;
24188
24940
  }
24941
+ if (scoreObject?.raw !== void 0 && scoreObject.raw !== null) {
24942
+ primaryObjective.rawScore = String(scoreObject.raw);
24943
+ }
24944
+ if (scoreObject?.min !== void 0 && scoreObject.min !== null) {
24945
+ primaryObjective.minScore = String(scoreObject.min);
24946
+ }
24947
+ if (scoreObject?.max !== void 0 && scoreObject.max !== null) {
24948
+ primaryObjective.maxScore = String(scoreObject.max);
24949
+ }
24189
24950
  }
24190
24951
  /**
24191
24952
  * Find or create a global objective by ID
@@ -24732,8 +25493,239 @@ class Scorm2004API extends BaseAPI {
24732
25493
  reset(settings) {
24733
25494
  this.commonReset(settings);
24734
25495
  this.cmi?.reset();
25496
+ this.applyCurrentActivityLaunchData();
24735
25497
  this.adl?.reset();
24736
25498
  }
25499
+ /**
25500
+ * Apply launch-static activity data to CMI while the new SCO is pre-initialize.
25501
+ *
25502
+ * @spec SCORM 2004 4th Ed. RTE 4.2.5, Table 4.2.5a - cmi.completion_threshold
25503
+ * @spec SCORM 2004 4th Ed. RTE 4.2.17, Table 4.2.17a - cmi.objectives
25504
+ */
25505
+ applyCurrentActivityLaunchData() {
25506
+ const currentActivity = this._sequencing?.getCurrentActivity();
25507
+ if (!currentActivity) {
25508
+ return;
25509
+ }
25510
+ this.applyActivityLaunchData(currentActivity);
25511
+ }
25512
+ /**
25513
+ * Apply launch-static CMI data when sequencing delivers a new activity before SCO Initialize.
25514
+ *
25515
+ * @spec SCORM 2004 4th Ed. SN DB.2 - Content Delivery Environment Process establishes the delivered activity.
25516
+ * @spec SCORM 2004 4th Ed. RTE 4.2.5, Table 4.2.5a - cmi.completion_threshold is initialized before SCO access.
25517
+ * @spec SCORM 2004 4th Ed. RTE 4.2.17, Table 4.2.17a - cmi.objectives is initialized before SCO access.
25518
+ */
25519
+ applyDeliveredActivityLaunchData(activity) {
25520
+ if (!this.isNotInitialized()) {
25521
+ return;
25522
+ }
25523
+ this.applyActivityLaunchData(activity);
25524
+ }
25525
+ /**
25526
+ * Copy the delivered activity's static launch data into the fresh CMI model.
25527
+ *
25528
+ * @spec SCORM 2004 4th Ed. RTE 4.2.5, Table 4.2.5a - cmi.completion_threshold
25529
+ * @spec SCORM 2004 4th Ed. RTE 4.2.17, Table 4.2.17a - cmi.objectives
25530
+ */
25531
+ applyActivityLaunchData(currentActivity) {
25532
+ if (!this.cmi) {
25533
+ return;
25534
+ }
25535
+ const contentActivityData = this._sequencing.overallSequencingProcess?.getContentActivityData(currentActivity);
25536
+ const completionThreshold = contentActivityData?.completionThreshold ?? currentActivity.completionThreshold?.toString() ?? "";
25537
+ this.cmi.completion_threshold = completionThreshold;
25538
+ this.seedCurrentActivityObjectives(currentActivity);
25539
+ }
25540
+ /**
25541
+ * Seed CMI objective ids after Initialize when automatic sequencing starts during Initialize.
25542
+ *
25543
+ * @spec SCORM 2004 4th Ed. RTE 4.2.17, Table 4.2.17a - cmi.objectives
25544
+ */
25545
+ applyCurrentActivityObjectiveData() {
25546
+ const currentActivity = this._sequencing?.getCurrentActivity();
25547
+ if (!this.cmi || !currentActivity) {
25548
+ return;
25549
+ }
25550
+ this.seedCurrentActivityObjectives(currentActivity);
25551
+ }
25552
+ /**
25553
+ * Seed CMI objectives from primary and secondary activity objectives.
25554
+ *
25555
+ * @spec SCORM 2004 4th Ed. RTE 4.2.17 - Initialization of Run-Time Objectives from Sequencing Information
25556
+ * @spec SCORM 2004 4th Ed. RTE 4.2.17, Table 4.2.17a - cmi.objectives.n.id and success_status
25557
+ * @spec SCORM 2004 4th Ed. SN 3.10.3 / ADLSEQ objectives extension - objective read maps seed the RTE view
25558
+ */
25559
+ seedCurrentActivityObjectives(currentActivity) {
25560
+ const activityObjectives = [];
25561
+ if (currentActivity.primaryObjective) {
25562
+ activityObjectives.push(currentActivity.primaryObjective);
25563
+ }
25564
+ activityObjectives.push(...currentActivity.objectives);
25565
+ const seededObjectiveIds = /* @__PURE__ */ new Set();
25566
+ for (const activityObjective of activityObjectives) {
25567
+ const objectiveId = this.getSeedableObjectiveId(activityObjective);
25568
+ if (!objectiveId || seededObjectiveIds.has(objectiveId)) {
25569
+ continue;
25570
+ }
25571
+ seededObjectiveIds.add(objectiveId);
25572
+ const index = this.findOrSeedCMIObjective(objectiveId);
25573
+ if (index === null) {
25574
+ continue;
25575
+ }
25576
+ const cmiObjective = this.cmi.objectives.findObjectiveByIndex(index);
25577
+ if (cmiObjective && this.cmi.objectives.initialized && !cmiObjective.initialized) {
25578
+ cmiObjective.initialize();
25579
+ }
25580
+ const successStatus = this.getActivityObjectiveSuccessStatus(activityObjective);
25581
+ if (successStatus) {
25582
+ this._commonSetCMIValue(
25583
+ "SeedActivityObjective",
25584
+ true,
25585
+ `cmi.objectives.${index}.success_status`,
25586
+ successStatus
25587
+ );
25588
+ }
25589
+ this.seedObjectiveReadMapValues(currentActivity, activityObjective, index);
25590
+ }
25591
+ }
25592
+ /**
25593
+ * Seed CMI objective fields from this objective's read-mapped global objectives.
25594
+ *
25595
+ * @spec SCORM 2004 4th Ed. RTE 4.2.17 - Run-Time Objectives are initialized from sequencing information
25596
+ * @spec SCORM 2004 4th Ed. SN 3.10.3 - read mapInfo grants access to mapped global objective state
25597
+ * @spec SCORM 2004 4th Ed. ADLSEQ objectives extension - raw/min/max score read maps seed RTE score fields
25598
+ */
25599
+ seedObjectiveReadMapValues(currentActivity, activityObjective, objectiveIndex) {
25600
+ const globalObjectiveMap = this._sequencing.overallSequencingProcess?.getGlobalObjectiveMap();
25601
+ if (!globalObjectiveMap || activityObjective.mapInfo.length === 0) {
25602
+ return;
25603
+ }
25604
+ for (const mapInfo of activityObjective.mapInfo) {
25605
+ const targetObjectiveId = mapInfo.targetObjectiveID || activityObjective.id;
25606
+ const globalObjective = globalObjectiveMap.get(targetObjectiveId);
25607
+ if (!globalObjective) {
25608
+ continue;
25609
+ }
25610
+ const readState = GlobalObjectiveSynchronizer.getGlobalObjectiveReadState(
25611
+ currentActivity,
25612
+ activityObjective,
25613
+ mapInfo,
25614
+ globalObjective
25615
+ );
25616
+ this.applyObjectiveReadStateToCMI(objectiveIndex, readState);
25617
+ }
25618
+ }
25619
+ /**
25620
+ * Copy mapped global objective state into the seeded CMI objective entry.
25621
+ *
25622
+ * @spec SCORM 2004 4th Ed. RTE 4.2.17, Table 4.2.17a - cmi.objectives launch-time initialization
25623
+ * @spec SCORM 2004 4th Ed. SN 3.10.3 - read maps populate the RTE view without creating local writes
25624
+ * @spec SCORM 2004 4th Ed. ADLSEQ objectives extension - raw/min/max score read maps populate CMI objective scores
25625
+ */
25626
+ applyObjectiveReadStateToCMI(objectiveIndex, readState) {
25627
+ if (readState.satisfiedStatus !== void 0) {
25628
+ this._commonSetCMIValue(
25629
+ "SeedActivityObjectiveReadMap",
25630
+ true,
25631
+ `cmi.objectives.${objectiveIndex}.success_status`,
25632
+ readState.satisfiedStatus ? SuccessStatus.PASSED : SuccessStatus.FAILED
25633
+ );
25634
+ }
25635
+ if (readState.normalizedMeasure !== void 0) {
25636
+ this._commonSetCMIValue(
25637
+ "SeedActivityObjectiveReadMap",
25638
+ true,
25639
+ `cmi.objectives.${objectiveIndex}.score.scaled`,
25640
+ String(readState.normalizedMeasure)
25641
+ );
25642
+ }
25643
+ if (readState.completionStatus !== void 0) {
25644
+ this._commonSetCMIValue(
25645
+ "SeedActivityObjectiveReadMap",
25646
+ true,
25647
+ `cmi.objectives.${objectiveIndex}.completion_status`,
25648
+ readState.completionStatus
25649
+ );
25650
+ }
25651
+ if (readState.progressMeasure !== void 0) {
25652
+ this._commonSetCMIValue(
25653
+ "SeedActivityObjectiveReadMap",
25654
+ true,
25655
+ `cmi.objectives.${objectiveIndex}.progress_measure`,
25656
+ String(readState.progressMeasure)
25657
+ );
25658
+ }
25659
+ if (readState.rawScore !== void 0) {
25660
+ this._commonSetCMIValue(
25661
+ "SeedActivityObjectiveReadMap",
25662
+ true,
25663
+ `cmi.objectives.${objectiveIndex}.score.raw`,
25664
+ readState.rawScore
25665
+ );
25666
+ }
25667
+ if (readState.minScore !== void 0) {
25668
+ this._commonSetCMIValue(
25669
+ "SeedActivityObjectiveReadMap",
25670
+ true,
25671
+ `cmi.objectives.${objectiveIndex}.score.min`,
25672
+ readState.minScore
25673
+ );
25674
+ }
25675
+ if (readState.maxScore !== void 0) {
25676
+ this._commonSetCMIValue(
25677
+ "SeedActivityObjectiveReadMap",
25678
+ true,
25679
+ `cmi.objectives.${objectiveIndex}.score.max`,
25680
+ readState.maxScore
25681
+ );
25682
+ }
25683
+ }
25684
+ /**
25685
+ * Return a manifest-defined objective id that can initialize cmi.objectives.n.id.
25686
+ *
25687
+ * @spec SCORM 2004 4th Ed. RTE 4.2.17, Table 4.2.17a - cmi.objectives.n.id
25688
+ */
25689
+ getSeedableObjectiveId(activityObjective) {
25690
+ const objectiveId = activityObjective.id;
25691
+ if (typeof objectiveId !== "string" || objectiveId.trim() === "") {
25692
+ return null;
25693
+ }
25694
+ return objectiveId;
25695
+ }
25696
+ /**
25697
+ * Find an existing CMI objective id or create the next CMI objective array entry.
25698
+ *
25699
+ * @spec SCORM 2004 4th Ed. RTE 4.2.17, Table 4.2.17a - cmi.objectives._count
25700
+ * @spec SCORM 2004 4th Ed. RTE 4.2.17, Table 4.2.17a - cmi.objectives.n.id
25701
+ */
25702
+ findOrSeedCMIObjective(objectiveId) {
25703
+ const existingIndex = this.cmi.objectives.childArray.findIndex((objective) => {
25704
+ return objective.id === objectiveId;
25705
+ });
25706
+ if (existingIndex >= 0) {
25707
+ return existingIndex;
25708
+ }
25709
+ const index = this.cmi.objectives.childArray.length;
25710
+ const result = this._commonSetCMIValue(
25711
+ "SeedActivityObjective",
25712
+ true,
25713
+ `cmi.objectives.${index}.id`,
25714
+ objectiveId
25715
+ );
25716
+ return result === global_constants.SCORM_TRUE ? index : null;
25717
+ }
25718
+ /**
25719
+ * Translate a known activity objective satisfied status to cmi.objectives.n.success_status.
25720
+ *
25721
+ * @spec SCORM 2004 4th Ed. RTE 4.2.17, Table 4.2.17a - cmi.objectives.n.success_status
25722
+ */
25723
+ getActivityObjectiveSuccessStatus(activityObjective) {
25724
+ if (activityObjective.progressStatus || activityObjective.satisfiedStatusKnown) {
25725
+ return activityObjective.satisfiedStatus ? SuccessStatus.PASSED : SuccessStatus.FAILED;
25726
+ }
25727
+ return null;
25728
+ }
24737
25729
  /**
24738
25730
  * Getter for _version
24739
25731
  * @return {string}
@@ -24813,6 +25805,7 @@ class Scorm2004API extends BaseAPI {
24813
25805
  );
24814
25806
  if (result === global_constants.SCORM_TRUE && this._sequencingService) {
24815
25807
  this._sequencingService.initialize();
25808
+ this.applyCurrentActivityObjectiveData();
24816
25809
  }
24817
25810
  if (result === global_constants.SCORM_TRUE) {
24818
25811
  this._globalObjectiveManager.restoreGlobalObjectivesToCMI();
@@ -25067,7 +26060,7 @@ class Scorm2004API extends BaseAPI {
25067
26060
  objective_id = objective ? objective.id : void 0;
25068
26061
  }
25069
26062
  const is_global = objective_id && this.settings.globalObjectiveIds?.includes(objective_id);
25070
- if (is_global) {
26063
+ if (is_global && this.currentActivityAllowsGlobalObjectiveWrites()) {
25071
26064
  const { index: global_index } = this._globalObjectiveManager.findOrCreateGlobalObjective(objective_id);
25072
26065
  const global_element = CMIElement.replace(
25073
26066
  element_base,
@@ -25082,6 +26075,15 @@ class Scorm2004API extends BaseAPI {
25082
26075
  }
25083
26076
  return this._commonSetCMIValue("SetValue", true, CMIElement, value);
25084
26077
  }
26078
+ /**
26079
+ * Return whether the current activity can update shared global objectives.
26080
+ *
26081
+ * @spec SCORM 2004 4th Ed. SN 3.13.1 Tracked - when False, the LMS
26082
+ * "does not initialize, manage or access any tracking status information".
26083
+ */
26084
+ currentActivityAllowsGlobalObjectiveWrites() {
26085
+ return this._sequencing?.getCurrentActivity()?.sequencingControls.tracked !== false;
26086
+ }
25085
26087
  /**
25086
26088
  * Gets or builds a new child element to add to the array
25087
26089
  * @param {string} CMIElement
@@ -25244,14 +26246,14 @@ class Scorm2004API extends BaseAPI {
25244
26246
  if (this.cmi.mode === "normal") {
25245
26247
  if (this.cmi.credit === "credit") {
25246
26248
  if (this.cmi.completion_threshold && this.cmi.progress_measure) {
25247
- if (this.cmi.progress_measure >= this.cmi.completion_threshold) {
26249
+ if (parseFloat(this.cmi.progress_measure) >= parseFloat(this.cmi.completion_threshold)) {
25248
26250
  this.cmi.completion_status = "completed";
25249
26251
  } else {
25250
26252
  this.cmi.completion_status = "incomplete";
25251
26253
  }
25252
26254
  }
25253
26255
  if (this.cmi.scaled_passing_score && this.cmi.score.scaled) {
25254
- if (this.cmi.score.scaled >= this.cmi.scaled_passing_score) {
26256
+ if (parseFloat(this.cmi.score.scaled) >= parseFloat(this.cmi.scaled_passing_score)) {
25255
26257
  this.cmi.success_status = "passed";
25256
26258
  } else {
25257
26259
  this.cmi.success_status = "failed";
@@ -25426,9 +26428,9 @@ class Scorm2004API extends BaseAPI {
25426
26428
  this.loggingService,
25427
26429
  sequencingConfig
25428
26430
  );
25429
- if (settings?.sequencing?.eventListeners) {
25430
- this._sequencingService.setEventListeners(settings.sequencing.eventListeners);
25431
- }
26431
+ this._sequencingService.setEventListeners(
26432
+ this.buildSequencingEventListeners(settings?.sequencing?.eventListeners)
26433
+ );
25432
26434
  this._globalObjectiveManager.updateSequencingService(this._sequencingService);
25433
26435
  this._dataSerializer.updateSequencingService(this._sequencingService);
25434
26436
  if (settings?.sequencingStatePersistence) {
@@ -25451,6 +26453,22 @@ class Scorm2004API extends BaseAPI {
25451
26453
  this._sequencingService = null;
25452
26454
  }
25453
26455
  }
26456
+ /**
26457
+ * Wrap LMS-provided sequencing listeners with API-owned delivery bookkeeping.
26458
+ *
26459
+ * @spec SCORM 2004 4th Ed. SN DB.2 - Content Delivery Environment Process
26460
+ * @spec SCORM 2004 4th Ed. RTE 4.2.5, Table 4.2.5a - cmi.completion_threshold
26461
+ * @spec SCORM 2004 4th Ed. RTE 4.2.17, Table 4.2.17a - cmi.objectives
26462
+ */
26463
+ buildSequencingEventListeners(listeners) {
26464
+ return {
26465
+ ...listeners,
26466
+ onActivityDelivery: (activity) => {
26467
+ this.applyDeliveredActivityLaunchData(activity);
26468
+ listeners?.onActivityDelivery?.(activity);
26469
+ }
26470
+ };
26471
+ }
25454
26472
  /**
25455
26473
  * Get the sequencing service
25456
26474
  * @return {SequencingService | null}
@@ -25461,10 +26479,13 @@ class Scorm2004API extends BaseAPI {
25461
26479
  /**
25462
26480
  * Set sequencing event listeners
25463
26481
  * @param {SequencingEventListeners} listeners
26482
+ *
26483
+ * @spec SCORM 2004 4th Ed. SN DB.2 - Content Delivery Environment Process
26484
+ * @spec SCORM 2004 4th Ed. RTE 4.2.5 / 4.2.17 - launch-static CMI data
25464
26485
  */
25465
26486
  setSequencingEventListeners(listeners) {
25466
26487
  if (this._sequencingService) {
25467
- this._sequencingService.setEventListeners(listeners);
26488
+ this._sequencingService.setEventListeners(this.buildSequencingEventListeners(listeners));
25468
26489
  }
25469
26490
  }
25470
26491
  /**