tessera-learn 0.0.10 → 0.0.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. package/README.md +1 -0
  2. package/dist/audit-BBJpQGqb.js +204 -0
  3. package/dist/audit-BBJpQGqb.js.map +1 -0
  4. package/dist/plugin/a11y-cli.d.ts +1 -0
  5. package/dist/plugin/a11y-cli.js +36 -0
  6. package/dist/plugin/a11y-cli.js.map +1 -0
  7. package/dist/plugin/cli.js +6 -3
  8. package/dist/plugin/cli.js.map +1 -1
  9. package/dist/plugin/index.d.ts +16 -1
  10. package/dist/plugin/index.d.ts.map +1 -1
  11. package/dist/plugin/index.js +171 -140
  12. package/dist/plugin/index.js.map +1 -1
  13. package/dist/{validation-BxWAMMnJ.js → validation-B-xTvM9B.js} +417 -81
  14. package/dist/validation-B-xTvM9B.js.map +1 -0
  15. package/package.json +17 -2
  16. package/src/components/Accordion.svelte +3 -1
  17. package/src/components/AccordionItem.svelte +1 -5
  18. package/src/components/Audio.svelte +22 -5
  19. package/src/components/Callout.svelte +5 -1
  20. package/src/components/Carousel.svelte +24 -8
  21. package/src/components/DefaultLayout.svelte +41 -12
  22. package/src/components/FillInTheBlank.svelte +75 -103
  23. package/src/components/Image.svelte +14 -10
  24. package/src/components/LockedBanner.svelte +5 -5
  25. package/src/components/Matching.svelte +48 -19
  26. package/src/components/MediaTracks.svelte +21 -0
  27. package/src/components/MultipleChoice.svelte +81 -102
  28. package/src/components/Quiz.svelte +63 -21
  29. package/src/components/ResultIcon.svelte +20 -4
  30. package/src/components/RevealModal.svelte +25 -22
  31. package/src/components/Sorting.svelte +61 -26
  32. package/src/components/Transcript.svelte +37 -0
  33. package/src/components/Video.svelte +25 -20
  34. package/src/components/util.ts +4 -1
  35. package/src/components/video-embed.ts +25 -0
  36. package/src/index.ts +2 -7
  37. package/src/plugin/a11y/audit.ts +299 -0
  38. package/src/plugin/a11y/contrast.ts +67 -0
  39. package/src/plugin/a11y-cli.ts +35 -0
  40. package/src/plugin/cli.ts +6 -8
  41. package/src/plugin/export.ts +60 -50
  42. package/src/plugin/index.ts +244 -101
  43. package/src/plugin/layout.ts +6 -51
  44. package/src/plugin/manifest.ts +90 -24
  45. package/src/plugin/override-plugin.ts +68 -0
  46. package/src/plugin/quiz.ts +9 -54
  47. package/src/plugin/validation.ts +768 -183
  48. package/src/runtime/App.svelte +128 -64
  49. package/src/runtime/LoadingBar.svelte +12 -3
  50. package/src/runtime/Sidebar.svelte +24 -8
  51. package/src/runtime/access.ts +15 -3
  52. package/src/runtime/adapters/cmi5.ts +68 -116
  53. package/src/runtime/adapters/format.ts +67 -0
  54. package/src/runtime/adapters/index.ts +45 -34
  55. package/src/runtime/adapters/retry.ts +25 -84
  56. package/src/runtime/adapters/scorm-base.ts +19 -15
  57. package/src/runtime/adapters/scorm12.ts +8 -9
  58. package/src/runtime/adapters/scorm2004.ts +22 -30
  59. package/src/runtime/adapters/web.ts +1 -1
  60. package/src/runtime/hooks.svelte.ts +152 -328
  61. package/src/runtime/interaction-format.ts +30 -12
  62. package/src/runtime/interaction.ts +44 -11
  63. package/src/runtime/navigation.svelte.ts +29 -40
  64. package/src/runtime/persistence.ts +2 -2
  65. package/src/runtime/progress.svelte.ts +22 -9
  66. package/src/runtime/quiz-engine.svelte.ts +361 -0
  67. package/src/runtime/quiz-policy.ts +28 -179
  68. package/src/runtime/types.ts +24 -2
  69. package/src/runtime/xapi/agent-rules.ts +11 -3
  70. package/src/runtime/xapi/client.ts +5 -5
  71. package/src/runtime/xapi/derive-actor.ts +2 -2
  72. package/src/runtime/xapi/publisher.ts +33 -40
  73. package/src/runtime/xapi/setup.ts +18 -15
  74. package/src/runtime/xapi/validation.ts +15 -6
  75. package/src/virtual.d.ts +4 -1
  76. package/styles/base.css +32 -11
  77. package/styles/layout.css +39 -18
  78. package/styles/theme.css +15 -3
  79. package/dist/validation-BxWAMMnJ.js.map +0 -1
@@ -43,7 +43,7 @@ export class XAPIClient {
43
43
  */
44
44
  sendStatement(
45
45
  partial: PartialStatement,
46
- options?: SendStatementOptions
46
+ options?: SendStatementOptions,
47
47
  ): Promise<SendStatementResult> {
48
48
  try {
49
49
  validatePartialStatement(partial);
@@ -59,8 +59,8 @@ export class XAPIClient {
59
59
  if (this.#publishers.every(blocked)) {
60
60
  return Promise.reject(
61
61
  new XAPIConfigError(
62
- 'XAPIClient.sendStatement: page is unloading; author statements queued during unload are dropped to keep Terminated last (cmi5 §9.3.6).'
63
- )
62
+ 'XAPIClient.sendStatement: page is unloading; author statements queued during unload are dropped to keep Terminated last (cmi5 §9.3.6).',
63
+ ),
64
64
  );
65
65
  }
66
66
  const id = uuidv4();
@@ -80,9 +80,9 @@ export class XAPIClient {
80
80
  endpoint: pub.getEndpoint(),
81
81
  ok: false,
82
82
  error: new XAPIConfigError(
83
- 'destination skipped: cmi5 publisher is unloading; statement dropped to keep Terminated last (cmi5 §9.3.6).'
83
+ 'destination skipped: cmi5 publisher is unloading; statement dropped to keep Terminated last (cmi5 §9.3.6).',
84
84
  ),
85
- })
85
+ }),
86
86
  );
87
87
  continue;
88
88
  }
@@ -39,7 +39,7 @@ export function defaultAccountHomePage(activityId: string): string | null {
39
39
  export function synthesizeSCORM12Actor(
40
40
  api: SCORM12API,
41
41
  activityId: string,
42
- actorAccountHomePage?: string
42
+ actorAccountHomePage?: string,
43
43
  ): XAPIAgent | null {
44
44
  let id = '';
45
45
  let name = '';
@@ -68,7 +68,7 @@ export function synthesizeSCORM12Actor(
68
68
  export function synthesizeSCORM2004Actor(
69
69
  api: SCORM2004API,
70
70
  activityId: string,
71
- actorAccountHomePage?: string
71
+ actorAccountHomePage?: string,
72
72
  ): XAPIAgent | null {
73
73
  let id = '';
74
74
  let name = '';
@@ -12,8 +12,8 @@ import {
12
12
  validatePartialStatement,
13
13
  validateAgent,
14
14
  validateAuthCredential,
15
+ joinFieldError,
15
16
  XAPIConfigError,
16
- XAPIStatementError,
17
17
  } from './validation.js';
18
18
  import { RETRY_ATTEMPTS, backoffMs } from '../adapters/retry.js';
19
19
 
@@ -21,16 +21,6 @@ import { RETRY_ATTEMPTS, backoffMs } from '../adapters/retry.js';
21
21
  const CMI5_SESSIONID_EXT =
22
22
  'https://w3id.org/xapi/cmi5/context/extensions/sessionid';
23
23
 
24
- /**
25
- * Combine a field label (e.g. `xapi.actor`) with the prefix-friendly suffix
26
- * returned by `validateAgent`. Sub-field suffixes start with `.` and chain
27
- * directly (`xapi.actor.mbox …`); top-level messages get a `: ` separator
28
- * (`xapi.actor: must be an object`).
29
- */
30
- function joinFieldError(label: string, suffix: string): string {
31
- return suffix.startsWith('.') ? `${label}${suffix}` : `${label}: ${suffix}`;
32
- }
33
-
34
24
  export interface XAPIPublisherOptions {
35
25
  /** Resolved http(s) endpoint URL. The 'lms' sentinel is a config-layer concept and never reaches the publisher. */
36
26
  endpoint: string;
@@ -137,7 +127,7 @@ export class XAPIPublisher {
137
127
  }
138
128
  if (!/^https?:\/\//i.test(opts.endpoint)) {
139
129
  throw new XAPIConfigError(
140
- 'XAPIPublisher: endpoint must be an absolute http(s) URL'
130
+ 'XAPIPublisher: endpoint must be an absolute http(s) URL',
141
131
  );
142
132
  }
143
133
  if (!opts.activityId) {
@@ -183,7 +173,7 @@ export class XAPIPublisher {
183
173
  resolved = await this.#actorValue();
184
174
  } catch (err) {
185
175
  throw new XAPIConfigError(
186
- `xapi.actor resolver threw: ${err instanceof Error ? err.message : String(err)}`
176
+ `xapi.actor resolver threw: ${err instanceof Error ? err.message : String(err)}`,
187
177
  );
188
178
  }
189
179
  const err = validateAgent(resolved);
@@ -209,7 +199,7 @@ export class XAPIPublisher {
209
199
  getActor(): XAPIAgent {
210
200
  if (!this.#cachedActor) {
211
201
  throw new XAPIConfigError(
212
- 'XAPIPublisher.getActor() called before init() resolved. Await publisher.init() before reading the actor.'
202
+ 'XAPIPublisher.getActor() called before init() resolved. Await publisher.init() before reading the actor.',
213
203
  );
214
204
  }
215
205
  return this.#cachedActor;
@@ -241,7 +231,7 @@ export class XAPIPublisher {
241
231
  buildStatement(partial: PartialStatement, opts?: { id?: string }): Statement {
242
232
  if (!this.#cachedActor) {
243
233
  throw new XAPIConfigError(
244
- 'XAPIPublisher.buildStatement() called before init() resolved.'
234
+ 'XAPIPublisher.buildStatement() called before init() resolved.',
245
235
  );
246
236
  }
247
237
  const userCtx = partial.context ?? {};
@@ -284,7 +274,8 @@ export class XAPIPublisher {
284
274
  timestamp: new Date().toISOString(),
285
275
  };
286
276
  if (partial.result !== undefined) statement.result = partial.result;
287
- if (partial.attachments !== undefined) statement.attachments = partial.attachments;
277
+ if (partial.attachments !== undefined)
278
+ statement.attachments = partial.attachments;
288
279
  return statement;
289
280
  }
290
281
 
@@ -299,7 +290,7 @@ export class XAPIPublisher {
299
290
  */
300
291
  sendStatement(
301
292
  partial: PartialStatement,
302
- options?: SendStatementOptions & { id?: string }
293
+ options?: SendStatementOptions & { id?: string },
303
294
  ): Promise<SendStatementResult> {
304
295
  if (this.#unavailableReason) {
305
296
  return Promise.reject(this.#unavailableReason());
@@ -316,8 +307,8 @@ export class XAPIPublisher {
316
307
  if (!this.#cachedActor) {
317
308
  return Promise.reject(
318
309
  new XAPIConfigError(
319
- 'XAPIPublisher.sendStatement() called before init() resolved.'
320
- )
310
+ 'XAPIPublisher.sendStatement() called before init() resolved.',
311
+ ),
321
312
  );
322
313
  }
323
314
  const statement = this.buildStatement(partial, { id: options?.id });
@@ -336,7 +327,7 @@ export class XAPIPublisher {
336
327
  */
337
328
  enqueueBuilt(
338
329
  statementOrBatch: Statement | Statement[],
339
- options?: SendStatementOptions
330
+ options?: SendStatementOptions,
340
331
  ): Promise<DestinationOutcome> {
341
332
  if (this.#unavailableReason) {
342
333
  return Promise.reject(this.#unavailableReason());
@@ -346,7 +337,7 @@ export class XAPIPublisher {
346
337
  endpoint: this.#endpoint,
347
338
  ok: false,
348
339
  error: new XAPIConfigError(
349
- `XAPIPublisher queue saturated (${this.#queueDepth} in-flight); refusing further sends until the LRS catches up.`
340
+ `XAPIPublisher queue saturated (${this.#queueDepth} in-flight); refusing further sends until the LRS catches up.`,
350
341
  ),
351
342
  });
352
343
  }
@@ -355,7 +346,7 @@ export class XAPIPublisher {
355
346
  console.warn(
356
347
  `Tessera: xAPI publisher queue depth ${this.#queueDepth} (>= ${QUEUE_DEPTH_WARN}). ` +
357
348
  `Each pending statement is retained in the promise chain's closure until it drains; ` +
358
- `consider rate-limiting authoring sends or batching before sendStatement.`
349
+ `consider rate-limiting authoring sends or batching before sendStatement.`,
359
350
  );
360
351
  }
361
352
  this.#queueDepth++;
@@ -377,7 +368,7 @@ export class XAPIPublisher {
377
368
  status: outcome.status,
378
369
  error: outcome.error,
379
370
  });
380
- })
371
+ }),
381
372
  );
382
373
  return outcomePromise;
383
374
  }
@@ -395,7 +386,9 @@ export class XAPIPublisher {
395
386
  resolveTask = r;
396
387
  });
397
388
  this.#queue = this.#queue.then(() =>
398
- fn().catch(() => {}).then(() => resolveTask())
389
+ fn()
390
+ .catch(() => {})
391
+ .then(() => resolveTask()),
399
392
  );
400
393
  return taskPromise;
401
394
  }
@@ -423,7 +416,7 @@ export class XAPIPublisher {
423
416
 
424
417
  #sendWithRetry(
425
418
  statementOrBatch: Statement | Statement[],
426
- options?: SendStatementOptions
419
+ options?: SendStatementOptions,
427
420
  ): Promise<SendOutcome> {
428
421
  const body = JSON.stringify(statementOrBatch);
429
422
  const retry = options?.retry !== false; // default: retry enabled
@@ -442,7 +435,7 @@ export class XAPIPublisher {
442
435
  `Tessera: xAPI ${count}-statement batch is ${body.length} bytes, ` +
443
436
  `over the 64 KiB keepalive cap. The browser may silently drop this ` +
444
437
  `request during unload. Reduce per-statement size or split sends ` +
445
- `before terminate.`
438
+ `before terminate.`,
446
439
  );
447
440
  }
448
441
  return this.#sendOnce(body, keepalive).then((outcome) => {
@@ -457,9 +450,9 @@ export class XAPIPublisher {
457
450
  return outcome;
458
451
  }
459
452
  if (isFinal) return outcome;
460
- return new Promise<void>((r) =>
461
- setTimeout(r, backoffMs(n))
462
- ).then(() => attempt(n + 1));
453
+ return new Promise<void>((r) => setTimeout(r, backoffMs(n))).then(() =>
454
+ attempt(n + 1),
455
+ );
463
456
  });
464
457
  };
465
458
 
@@ -471,7 +464,7 @@ export class XAPIPublisher {
471
464
  return Promise.resolve({
472
465
  ok: false,
473
466
  error: new XAPIConfigError(
474
- 'xapi.auth was rejected by the LRS twice in a row; refusing further sends for the publisher lifetime. Reload the runtime to retry.'
467
+ 'xapi.auth was rejected by the LRS twice in a row; refusing further sends for the publisher lifetime. Reload the runtime to retry.',
475
468
  ),
476
469
  });
477
470
  }
@@ -492,7 +485,7 @@ export class XAPIPublisher {
492
485
  #fetchWithToken(
493
486
  token: string,
494
487
  body: string,
495
- keepalive: boolean
488
+ keepalive: boolean,
496
489
  ): Promise<SendOutcome> {
497
490
  const headers = new Headers();
498
491
  if (token) headers.set('Authorization', `Basic ${token}`);
@@ -514,7 +507,7 @@ export class XAPIPublisher {
514
507
  #handleResponse(
515
508
  resp: Response,
516
509
  body: string,
517
- keepalive: boolean
510
+ keepalive: boolean,
518
511
  ): Promise<SendOutcome> | SendOutcome {
519
512
  if (resp.ok || resp.status === 409) {
520
513
  return { ok: true, status: resp.status };
@@ -550,7 +543,7 @@ export class XAPIPublisher {
550
543
  ok: false,
551
544
  status: 401,
552
545
  error: new Error(
553
- 'LRS rejected re-resolved auth (consecutive 401s); auth resolver marked dead'
546
+ 'LRS rejected re-resolved auth (consecutive 401s); auth resolver marked dead',
554
547
  ),
555
548
  };
556
549
  }
@@ -565,7 +558,7 @@ export class XAPIPublisher {
565
558
  ok: false,
566
559
  status: 401,
567
560
  error: err instanceof Error ? err : new Error(String(err)),
568
- })
561
+ }),
569
562
  );
570
563
  }
571
564
  // Append the LRS body to the error message so callers see the
@@ -583,14 +576,14 @@ export class XAPIPublisher {
583
576
  ok: false,
584
577
  status: resp.status,
585
578
  error: new Error(
586
- `LRS responded ${resp.status}${respBody ? `: ${respBody.slice(0, 500)}` : ''}`
579
+ `LRS responded ${resp.status}${respBody ? `: ${respBody.slice(0, 500)}` : ''}`,
587
580
  ),
588
581
  }),
589
582
  (): SendOutcome => ({
590
583
  ok: false,
591
584
  status: resp.status,
592
585
  error: new Error(`LRS responded ${resp.status}`),
593
- })
586
+ }),
594
587
  );
595
588
  }
596
589
 
@@ -603,7 +596,8 @@ export class XAPIPublisher {
603
596
  // where the LMS fetch URL produced no token); otherwise revalidate.
604
597
  if (this.#authValue.length > 0) {
605
598
  const err = validateAuthCredential(this.#authValue);
606
- if (err) return Promise.reject(new XAPIConfigError(`xapi.auth: ${err}`));
599
+ if (err)
600
+ return Promise.reject(new XAPIConfigError(`xapi.auth: ${err}`));
607
601
  }
608
602
  this.#cachedAuth = this.#authValue;
609
603
  return Promise.resolve(this.#cachedAuth);
@@ -613,7 +607,7 @@ export class XAPIPublisher {
613
607
  .then((resolved) => {
614
608
  if (typeof resolved !== 'string' || !resolved) {
615
609
  throw new XAPIConfigError(
616
- 'xapi.auth resolver must return a non-empty string'
610
+ 'xapi.auth resolver must return a non-empty string',
617
611
  );
618
612
  }
619
613
  const err = validateAuthCredential(resolved);
@@ -624,9 +618,8 @@ export class XAPIPublisher {
624
618
  .catch((err) => {
625
619
  if (err instanceof XAPIConfigError) throw err;
626
620
  throw new XAPIConfigError(
627
- `xapi.auth resolver threw: ${err instanceof Error ? err.message : String(err)}`
621
+ `xapi.auth resolver threw: ${err instanceof Error ? err.message : String(err)}`,
628
622
  );
629
623
  });
630
624
  }
631
625
  }
632
-
@@ -34,8 +34,8 @@ class XAPIDevFallbackError extends Error {
34
34
  "Tessera xAPI: xapi.endpoint is 'lms' but no cmi5 launch parameters " +
35
35
  '(fetch / endpoint / activityId / actor) were present on the URL. ' +
36
36
  'Either launch this course from a real LMS / SCORM Cloud, or ' +
37
- "temporarily change xapi.endpoint to an explicit URL pointed at a " +
38
- 'local LRS (e.g. http://localhost:8080/data/xAPI/) for dev work.'
37
+ 'temporarily change xapi.endpoint to an explicit URL pointed at a ' +
38
+ 'local LRS (e.g. http://localhost:8080/data/xAPI/) for dev work.',
39
39
  );
40
40
  this.name = 'XAPIDevFallbackError';
41
41
  }
@@ -72,14 +72,14 @@ class XAPISCORMDevFallbackError extends Error {
72
72
  'destination. Either supply xapi.actor explicitly in course.config.js, or launch from ' +
73
73
  'a real LMS / SCORM Cloud where ' +
74
74
  (standard === 'scorm12' ? 'cmi.core.student_id' : 'cmi.learner_id') +
75
- ' is populated.'
75
+ ' is populated.',
76
76
  );
77
77
  this.name = 'XAPISCORMDevFallbackError';
78
78
  }
79
79
  }
80
80
 
81
81
  function makeSCORMDevFallbackPublisher(
82
- standard: 'scorm12' | 'scorm2004'
82
+ standard: 'scorm12' | 'scorm2004',
83
83
  ): XAPIPublisher {
84
84
  return makeRejectingPublisher(() => new XAPISCORMDevFallbackError(standard));
85
85
  }
@@ -93,13 +93,13 @@ function makeSCORMDevFallbackPublisher(
93
93
  function resolveDestination(
94
94
  entry: XAPIConfig,
95
95
  config: CourseConfig,
96
- adapter: PersistenceAdapter | null
96
+ adapter: PersistenceAdapter | null,
97
97
  ): DestinationSource | null {
98
98
  if (entry.endpoint === 'lms') {
99
99
  if (config.export?.standard !== 'cmi5') {
100
100
  // Build-time validator should reject this; defense in depth at runtime.
101
101
  console.warn(
102
- "Tessera xAPI: ignoring xapi entry with endpoint: 'lms' under non-cmi5 export."
102
+ "Tessera xAPI: ignoring xapi entry with endpoint: 'lms' under non-cmi5 export.",
103
103
  );
104
104
  return null;
105
105
  }
@@ -121,14 +121,17 @@ function resolveDestination(
121
121
  (actorOrResolver as { __scormDevFallback?: 'scorm12' | 'scorm2004' })
122
122
  .__scormDevFallback
123
123
  ) {
124
- const std = (actorOrResolver as { __scormDevFallback: 'scorm12' | 'scorm2004' })
125
- .__scormDevFallback;
124
+ const std = (
125
+ actorOrResolver as { __scormDevFallback: 'scorm12' | 'scorm2004' }
126
+ ).__scormDevFallback;
126
127
  return { kind: 'explicit', publisher: makeSCORMDevFallbackPublisher(std) };
127
128
  }
128
129
  const publisher = new XAPIPublisher({
129
130
  endpoint: explicit.endpoint,
130
131
  auth: explicit.auth,
131
- actor: actorOrResolver as XAPIAgent | (() => XAPIAgent | Promise<XAPIAgent>),
132
+ actor: actorOrResolver as
133
+ | XAPIAgent
134
+ | (() => XAPIAgent | Promise<XAPIAgent>),
132
135
  activityId: explicit.activityId,
133
136
  registration: explicit.registration,
134
137
  });
@@ -145,7 +148,7 @@ function resolveDestination(
145
148
  function resolveExplicitActor(
146
149
  explicit: XAPIExplicitConfig,
147
150
  config: CourseConfig,
148
- adapter: PersistenceAdapter | null
151
+ adapter: PersistenceAdapter | null,
149
152
  ):
150
153
  | XAPIAgent
151
154
  | (() => XAPIAgent | Promise<XAPIAgent>)
@@ -170,7 +173,7 @@ function resolveExplicitActor(
170
173
  return synthesizeSCORM12Actor(
171
174
  adapter.getAPI(),
172
175
  explicit.activityId,
173
- explicit.actorAccountHomePage
176
+ explicit.actorAccountHomePage,
174
177
  );
175
178
  }
176
179
  // Adapter is the WebAdapter dev fallback. Mirror the cmi5 'lms'
@@ -184,14 +187,14 @@ function resolveExplicitActor(
184
187
  return synthesizeSCORM2004Actor(
185
188
  adapter.getAPI(),
186
189
  explicit.activityId,
187
- explicit.actorAccountHomePage
190
+ explicit.actorAccountHomePage,
188
191
  );
189
192
  }
190
193
  return { __scormDevFallback: 'scorm2004' };
191
194
  }
192
195
  // Web export with no actor — build-time validator should have errored.
193
196
  console.warn(
194
- 'Tessera xAPI: explicit destination has no actor and no derivation source — skipping.'
197
+ 'Tessera xAPI: explicit destination has no actor and no derivation source — skipping.',
195
198
  );
196
199
  return null;
197
200
  }
@@ -206,7 +209,7 @@ function resolveExplicitActor(
206
209
  */
207
210
  export async function buildXAPIClient(
208
211
  config: CourseConfig,
209
- adapter: PersistenceAdapter | null
212
+ adapter: PersistenceAdapter | null,
210
213
  ): Promise<XAPIClient | null> {
211
214
  const raw = config.xapi;
212
215
  if (raw === undefined || raw === null) return null;
@@ -233,7 +236,7 @@ export async function buildXAPIClient(
233
236
  } catch (err) {
234
237
  console.warn(
235
238
  'Tessera xAPI: failed to initialize an explicit destination — skipping.',
236
- err
239
+ err,
237
240
  );
238
241
  }
239
242
  }
@@ -1,5 +1,9 @@
1
1
  import type { PartialStatement } from './types.js';
2
- export { validateAgent, validateAuthCredential } from './agent-rules.js';
2
+ export {
3
+ validateAgent,
4
+ validateAuthCredential,
5
+ joinFieldError,
6
+ } from './agent-rules.js';
3
7
 
4
8
  /** Thrown for runtime-validation failures (auth/actor resolver misuse). */
5
9
  export class XAPIConfigError extends Error {
@@ -32,7 +36,7 @@ export function validatePartialStatement(partial: PartialStatement): void {
32
36
  if (!partial || typeof partial !== 'object') {
33
37
  throw new XAPIStatementError(
34
38
  'sendStatement: partial statement must be an object',
35
- partial
39
+ partial,
36
40
  );
37
41
  }
38
42
  if (
@@ -43,7 +47,7 @@ export function validatePartialStatement(partial: PartialStatement): void {
43
47
  ) {
44
48
  throw new XAPIStatementError(
45
49
  'sendStatement: verb.id is required and must be a non-empty string',
46
- partial
50
+ partial,
47
51
  );
48
52
  }
49
53
  if (partial.object !== undefined) {
@@ -55,16 +59,21 @@ export function validatePartialStatement(partial: PartialStatement): void {
55
59
  ) {
56
60
  throw new XAPIStatementError(
57
61
  'sendStatement: object.id must be a non-empty string when object is supplied',
58
- partial
62
+ partial,
59
63
  );
60
64
  }
61
65
  }
62
66
  const scaled = partial.result?.score?.scaled;
63
67
  if (scaled !== undefined) {
64
- if (typeof scaled !== 'number' || !Number.isFinite(scaled) || scaled < -1 || scaled > 1) {
68
+ if (
69
+ typeof scaled !== 'number' ||
70
+ !Number.isFinite(scaled) ||
71
+ scaled < -1 ||
72
+ scaled > 1
73
+ ) {
65
74
  throw new XAPIStatementError(
66
75
  `sendStatement: result.score.scaled must be a number in [-1, 1], got ${scaled}`,
67
- partial
76
+ partial,
68
77
  );
69
78
  }
70
79
  }
package/src/virtual.d.ts CHANGED
@@ -14,7 +14,10 @@ declare module 'virtual:tessera-xapi-setup' {
14
14
  import type { CourseConfig } from 'tessera-learn/runtime/types.js';
15
15
  import type { PersistenceAdapter } from 'tessera-learn/runtime/persistence.js';
16
16
  import type { XAPIClient } from 'tessera-learn/runtime/xapi/client.js';
17
- export function buildXAPIClient(config: CourseConfig, adapter: PersistenceAdapter): Promise<XAPIClient | null>;
17
+ export function buildXAPIClient(
18
+ config: CourseConfig,
19
+ adapter: PersistenceAdapter,
20
+ ): Promise<XAPIClient | null>;
18
21
  }
19
22
 
20
23
  interface ImportMetaEnv {
package/styles/base.css CHANGED
@@ -47,7 +47,12 @@ object {
47
47
  }
48
48
 
49
49
  /* ---- Typography ---- */
50
- h1, h2, h3, h4, h5, h6 {
50
+ h1,
51
+ h2,
52
+ h3,
53
+ h4,
54
+ h5,
55
+ h6 {
51
56
  font-weight: 700;
52
57
  line-height: 1.25;
53
58
  color: var(--tessera-text);
@@ -55,12 +60,24 @@ h1, h2, h3, h4, h5, h6 {
55
60
  margin-bottom: var(--tessera-spacing-md);
56
61
  }
57
62
 
58
- h1 { font-size: 2rem; }
59
- h2 { font-size: 1.625rem; }
60
- h3 { font-size: 1.375rem; }
61
- h4 { font-size: 1.125rem; }
62
- h5 { font-size: 1rem; }
63
- h6 { font-size: 0.875rem; }
63
+ h1 {
64
+ font-size: 2rem;
65
+ }
66
+ h2 {
67
+ font-size: 1.625rem;
68
+ }
69
+ h3 {
70
+ font-size: 1.375rem;
71
+ }
72
+ h4 {
73
+ font-size: 1.125rem;
74
+ }
75
+ h5 {
76
+ font-size: 1rem;
77
+ }
78
+ h6 {
79
+ font-size: 0.875rem;
80
+ }
64
81
 
65
82
  h1:first-child,
66
83
  h2:first-child,
@@ -90,16 +107,19 @@ a:focus-visible {
90
107
  border-radius: 2px;
91
108
  }
92
109
 
93
- strong, b {
110
+ strong,
111
+ b {
94
112
  font-weight: 700;
95
113
  }
96
114
 
97
- em, i {
115
+ em,
116
+ i {
98
117
  font-style: italic;
99
118
  }
100
119
 
101
120
  /* ---- Lists ---- */
102
- ul, ol {
121
+ ul,
122
+ ol {
103
123
  margin-bottom: var(--tessera-spacing-md);
104
124
  padding-left: var(--tessera-spacing-xl);
105
125
  }
@@ -170,7 +190,8 @@ table {
170
190
  font-size: 0.9375rem;
171
191
  }
172
192
 
173
- th, td {
193
+ th,
194
+ td {
174
195
  padding: var(--tessera-spacing-sm) var(--tessera-spacing-md);
175
196
  border: 1px solid var(--tessera-border);
176
197
  text-align: left;