tessera-learn 0.0.5 → 0.0.7

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.
@@ -1,79 +1,125 @@
1
1
  /**
2
- * Format `Interaction` payloads for SCORM 2004 / xAPI `cmi.interactions.n.*`
3
- * writes. Delimiters follow SCORM 2004 4th Edition RTE §4.2.7 — `cmi5` (xAPI)
4
- * reuses the same encoding for `cmi.interaction` activity statements.
5
- *
6
- * ITEM delimiter [,]
7
- * PAIR delimiter [.]
8
- * RANGE delimiter [:]
2
+ * SCORM 1.2 RTE §3.4.7 vs SCORM 2004 4E RTE §4.2.7 differ in delimiter
3
+ * encoding and identifier rules; cmi5 (xAPI) reuses the 2004 encoding.
9
4
  */
10
5
 
11
6
  import type { Interaction } from './interaction.js';
12
7
 
8
+ export interface InteractionFormat {
9
+ itemDelim: string;
10
+ pairDelim: string;
11
+ rangeDelim: string;
12
+ /**
13
+ * SCORM 1.2 has no numeric range syntax — `correct_responses.n.pattern`
14
+ * is a single CMIDecimal. SCORM 2004 supports `min[:]max`.
15
+ */
16
+ supportsNumericRange: boolean;
17
+ formatBoolean(value: boolean): string;
18
+ identifier(value: string): string;
19
+ }
20
+
21
+ export const SCORM12_INTERACTION_FORMAT: InteractionFormat = {
22
+ itemDelim: ',',
23
+ pairDelim: '.',
24
+ rangeDelim: ':',
25
+ supportsNumericRange: false,
26
+ formatBoolean: (v) => (v ? 't' : 'f'),
27
+ identifier: shortIdentifier,
28
+ };
29
+
30
+ /**
31
+ * Bracketed delimiters are literal text, not regex. xAPI parses them the
32
+ * same way.
33
+ */
34
+ export const SCORM2004_INTERACTION_FORMAT: InteractionFormat = {
35
+ itemDelim: '[,]',
36
+ pairDelim: '[.]',
37
+ rangeDelim: '[:]',
38
+ supportsNumericRange: true,
39
+ formatBoolean: (v) => (v ? 'true' : 'false'),
40
+ identifier: shortIdentifier,
41
+ };
42
+
13
43
  /**
14
- * Serialize the learner response to the `learner_response` / `student_response`
15
- * field format expected by SCORM 2004 and mirrored by xAPI.
44
+ * SCORM `short_identifier_type` / `CMIIdentifier`: alphanumerics +
45
+ * underscore, max 250 chars. Strict validators (SCORM Cloud) reject raw
46
+ * option labels with spaces or punctuation with error 405/406.
16
47
  */
17
- export function formatResponse(i: Interaction): string {
48
+ function shortIdentifier(value: string): string {
49
+ const cleaned = value.replace(/[^A-Za-z0-9]+/g, '_').replace(/^_+|_+$/g, '');
50
+ const trimmed = cleaned.slice(0, 250);
51
+ return trimmed || '_';
52
+ }
53
+
54
+ export function formatResponse(
55
+ i: Interaction,
56
+ fmt: InteractionFormat = SCORM2004_INTERACTION_FORMAT
57
+ ): string {
18
58
  switch (i.type) {
19
59
  case 'choice':
20
60
  case 'sequencing':
21
- return i.response.join('[,]');
61
+ return i.response.map(fmt.identifier).join(fmt.itemDelim);
22
62
  case 'true-false':
23
- return i.response ? 'true' : 'false';
63
+ return fmt.formatBoolean(i.response);
24
64
  case 'fill-in':
25
65
  case 'long-fill-in':
26
66
  case 'likert':
27
67
  case 'other':
28
68
  return i.response;
29
69
  case 'matching':
30
- return i.response.map(([l, r]) => `${l}[.]${r}`).join('[,]');
70
+ return i.response
71
+ .map(([l, r]) => `${fmt.identifier(l)}${fmt.pairDelim}${fmt.identifier(r)}`)
72
+ .join(fmt.itemDelim);
31
73
  case 'numeric':
32
74
  return String(i.response);
33
75
  case 'performance':
34
- return i.response.map(([s, v]) => `${s}[.]${v}`).join('[,]');
76
+ return i.response
77
+ .map(([s, v]) => `${fmt.identifier(s)}${fmt.pairDelim}${fmt.identifier(String(v))}`)
78
+ .join(fmt.itemDelim);
35
79
  }
36
80
  }
37
81
 
38
- /**
39
- * Serialize the `correct_responses.0.pattern` for this interaction. Returns
40
- * `null` if no correct pattern was provided.
41
- */
42
- export function formatCorrectPattern(i: Interaction): string | null {
82
+ /** Returns null when no correct pattern was provided. */
83
+ export function formatCorrectPattern(
84
+ i: Interaction,
85
+ fmt: InteractionFormat = SCORM2004_INTERACTION_FORMAT
86
+ ): string | null {
43
87
  if (i.correct === undefined) return null;
44
88
  switch (i.type) {
45
89
  case 'choice':
46
90
  case 'sequencing':
47
- return (i.correct as string[]).join('[,]');
91
+ return (i.correct as string[]).map(fmt.identifier).join(fmt.itemDelim);
48
92
  case 'true-false':
49
- return (i.correct as boolean) ? 'true' : 'false';
93
+ return fmt.formatBoolean(i.correct as boolean);
50
94
  case 'fill-in':
51
95
  case 'long-fill-in':
52
- // SCORM 2004 accepts multiple acceptable patterns joined with `[,]`.
53
- return (i.correct as string[]).join('[,]');
96
+ return (i.correct as string[]).join(fmt.itemDelim);
54
97
  case 'matching':
55
- return (i.correct as Array<[string, string]>).map(([l, r]) => `${l}[.]${r}`).join('[,]');
98
+ return (i.correct as Array<[string, string]>)
99
+ .map(([l, r]) => `${fmt.identifier(l)}${fmt.pairDelim}${fmt.identifier(r)}`)
100
+ .join(fmt.itemDelim);
56
101
  case 'numeric': {
57
102
  const c = i.correct as { min?: number; max?: number };
58
- const min = c.min ?? '';
59
- const max = c.max ?? '';
60
- return `${min}[:]${max}`;
103
+ if (c.min !== undefined && c.max !== undefined && c.min === c.max) {
104
+ return String(c.min);
105
+ }
106
+ if (c.min !== undefined && c.max === undefined) return String(c.min);
107
+ if (c.min === undefined && c.max !== undefined) return String(c.max);
108
+ // True range — drop the pattern in 1.2 (rely on `result` for pass/fail).
109
+ if (!fmt.supportsNumericRange) return null;
110
+ return `${c.min ?? ''}${fmt.rangeDelim}${c.max ?? ''}`;
61
111
  }
62
112
  case 'likert':
63
113
  case 'other':
64
114
  return i.correct as string;
65
115
  case 'performance':
66
116
  return (i.correct as Array<[string, string | number]>)
67
- .map(([s, v]) => `${s}[.]${v}`)
68
- .join('[,]');
117
+ .map(([s, v]) => `${fmt.identifier(s)}${fmt.pairDelim}${fmt.identifier(String(v))}`)
118
+ .join(fmt.itemDelim);
69
119
  }
70
120
  }
71
121
 
72
- /**
73
- * Map Tessera interaction types to SCORM 1.2's narrower vocabulary. SCORM 1.2
74
- * does not define `long-fill-in`; fall back to `fill-in`. `other` is not in
75
- * the spec either — fall back to `fill-in` (free text).
76
- */
122
+ /** SCORM 1.2 has no `long-fill-in` or `other` — both fall back to `fill-in`. */
77
123
  export function scorm12Type(type: Interaction['type']): string {
78
124
  switch (type) {
79
125
  case 'long-fill-in':
@@ -85,26 +131,15 @@ export function scorm12Type(type: Interaction['type']): string {
85
131
  }
86
132
  }
87
133
 
88
- /**
89
- * Per-standard differences in how `cmi.interactions.n.*` is written. The
90
- * SCORM 1.2 vs 2004 deltas are: response field name, result vocabulary,
91
- * timestamp field name+format, and the type vocabulary mapping.
92
- */
93
134
  export interface ScormInteractionSpec {
94
135
  responseField: 'student_response' | 'learner_response';
95
136
  timestampField: 'time' | 'timestamp';
96
- /** Wall-clock value formatted to whichever style the standard expects. */
97
137
  timestamp: string;
98
- /** Mapped interaction type — already narrowed for SCORM 1.2 callers. */
99
138
  typeValue: string;
100
139
  resultLabels: { correct: string; incorrect: string };
140
+ format: InteractionFormat;
101
141
  }
102
142
 
103
- /**
104
- * Build the ordered list of `cmi.interactions.n.*` writes that SCORM 1.2 and
105
- * SCORM 2004 adapters share. Caller wires each pair through its own LMS
106
- * SetValue queue (the queueing semantics differ between adapters).
107
- */
108
143
  export function buildScormInteractionFields(
109
144
  prefix: string,
110
145
  questionId: string,
@@ -115,9 +150,9 @@ export function buildScormInteractionFields(
115
150
  const fields: Array<[string, string]> = [
116
151
  [`${prefix}.id`, questionId],
117
152
  [`${prefix}.type`, spec.typeValue],
118
- [`${prefix}.${spec.responseField}`, formatResponse(interaction)],
153
+ [`${prefix}.${spec.responseField}`, formatResponse(interaction, spec.format)],
119
154
  ];
120
- const pattern = formatCorrectPattern(interaction);
155
+ const pattern = formatCorrectPattern(interaction, spec.format);
121
156
  if (pattern !== null) {
122
157
  fields.push([`${prefix}.correct_responses.0.pattern`, pattern]);
123
158
  }