@lessonkit/lxpack 1.0.2 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -61,8 +61,187 @@ __export(index_exports, {
61
61
  });
62
62
  module.exports = __toCommonJS(index_exports);
63
63
 
64
- // src/validateDescriptor.ts
64
+ // src/descriptor/normalize.ts
65
65
  var import_core = require("@lessonkit/core");
66
+ function normalizeDescriptor(input) {
67
+ const course = (0, import_core.validateId)(input.courseId, "courseId");
68
+ if (!course.ok) throw new Error("normalizeDescriptor called with invalid courseId");
69
+ return {
70
+ ...input,
71
+ courseId: course.id,
72
+ title: input.title.trim(),
73
+ version: input.version?.trim() || void 0,
74
+ spaLessonId: input.spaLessonId?.trim() || void 0,
75
+ lessons: input.lessons.map((lesson) => {
76
+ const idResult = (0, import_core.validateId)(lesson.id, "lessonId");
77
+ if (!idResult.ok) throw new Error("normalizeDescriptor called with invalid lesson id");
78
+ return {
79
+ ...lesson,
80
+ id: idResult.id,
81
+ title: lesson.title.trim(),
82
+ spaPath: lesson.spaPath?.trim() || void 0
83
+ };
84
+ }),
85
+ assessments: input.assessments?.map((assessment) => {
86
+ const check = (0, import_core.validateId)(assessment.checkId, "checkId");
87
+ if (!check.ok) throw new Error("normalizeDescriptor called with invalid checkId");
88
+ const question = assessment.question.trim();
89
+ if (assessment.kind === "trueFalse") {
90
+ return { ...assessment, checkId: check.id, question };
91
+ }
92
+ if (assessment.kind === "fillInBlanks") {
93
+ return {
94
+ ...assessment,
95
+ checkId: check.id,
96
+ question,
97
+ template: assessment.template.trim(),
98
+ blanks: assessment.blanks?.map((b) => ({
99
+ id: b.id.trim(),
100
+ answer: b.answer.trim()
101
+ }))
102
+ };
103
+ }
104
+ if (assessment.kind === "findHotspot") {
105
+ return {
106
+ ...assessment,
107
+ checkId: check.id,
108
+ question,
109
+ src: assessment.src.trim(),
110
+ alt: assessment.alt.trim(),
111
+ correctTargetId: assessment.correctTargetId.trim()
112
+ };
113
+ }
114
+ if (assessment.kind === "findMultipleHotspots") {
115
+ return {
116
+ ...assessment,
117
+ checkId: check.id,
118
+ question,
119
+ src: assessment.src.trim(),
120
+ alt: assessment.alt.trim(),
121
+ correctTargetIds: assessment.correctTargetIds.map((id) => id.trim()).filter((id) => id.length > 0)
122
+ };
123
+ }
124
+ const mcq = assessment;
125
+ return {
126
+ ...mcq,
127
+ checkId: check.id,
128
+ question,
129
+ choices: mcq.choices.map((c) => c.trim()).filter((c) => c.length > 0),
130
+ answer: mcq.answer.trim()
131
+ };
132
+ })
133
+ };
134
+ }
135
+
136
+ // src/descriptor/parseInput.ts
137
+ function isRecord(value) {
138
+ return typeof value === "object" && value !== null && !Array.isArray(value);
139
+ }
140
+ function parseLessonDescriptor(raw) {
141
+ if (!isRecord(raw)) {
142
+ return { id: "", title: "" };
143
+ }
144
+ return {
145
+ id: typeof raw.id === "string" ? raw.id : "",
146
+ title: typeof raw.title === "string" ? raw.title : "",
147
+ spaPath: typeof raw.spaPath === "string" ? raw.spaPath : void 0
148
+ };
149
+ }
150
+ function parseAssessmentDescriptor(raw) {
151
+ if (!isRecord(raw)) {
152
+ return { checkId: "", question: "", choices: [], answer: "" };
153
+ }
154
+ const base = {
155
+ checkId: typeof raw.checkId === "string" ? raw.checkId : "",
156
+ question: typeof raw.question === "string" ? raw.question : "",
157
+ passingScore: typeof raw.passingScore === "number" ? raw.passingScore : void 0
158
+ };
159
+ const kind = raw.kind;
160
+ if (kind === "trueFalse") {
161
+ return {
162
+ kind: "trueFalse",
163
+ ...base,
164
+ answer: typeof raw.answer === "boolean" ? raw.answer : raw.answer === "true"
165
+ };
166
+ }
167
+ if (kind === "fillInBlanks") {
168
+ return {
169
+ kind: "fillInBlanks",
170
+ ...base,
171
+ template: typeof raw.template === "string" ? raw.template : "",
172
+ blanks: Array.isArray(raw.blanks) ? raw.blanks.filter((b) => isRecord(b)).map((b) => ({
173
+ id: typeof b.id === "string" ? b.id : "",
174
+ answer: typeof b.answer === "string" ? b.answer : ""
175
+ })) : void 0
176
+ };
177
+ }
178
+ if (kind === "findHotspot") {
179
+ return {
180
+ kind: "findHotspot",
181
+ ...base,
182
+ src: typeof raw.src === "string" ? raw.src : "",
183
+ alt: typeof raw.alt === "string" ? raw.alt : "",
184
+ correctTargetId: typeof raw.correctTargetId === "string" ? raw.correctTargetId : ""
185
+ };
186
+ }
187
+ if (kind === "findMultipleHotspots") {
188
+ return {
189
+ kind: "findMultipleHotspots",
190
+ ...base,
191
+ src: typeof raw.src === "string" ? raw.src : "",
192
+ alt: typeof raw.alt === "string" ? raw.alt : "",
193
+ correctTargetIds: Array.isArray(raw.correctTargetIds) ? raw.correctTargetIds.filter((id) => typeof id === "string") : []
194
+ };
195
+ }
196
+ return {
197
+ kind: kind === "mcq" ? "mcq" : void 0,
198
+ ...base,
199
+ choices: Array.isArray(raw.choices) ? raw.choices.filter((c) => typeof c === "string") : [],
200
+ answer: typeof raw.answer === "string" ? raw.answer : ""
201
+ };
202
+ }
203
+ function parseCourseDescriptorInput(input) {
204
+ if (!isRecord(input)) return null;
205
+ const trackingRaw = input.tracking;
206
+ let tracking;
207
+ if (isRecord(trackingRaw)) {
208
+ const completionRaw = trackingRaw.completion;
209
+ const xapiRaw = trackingRaw.xapi;
210
+ tracking = {
211
+ completion: isRecord(completionRaw) ? {
212
+ threshold: typeof completionRaw.threshold === "number" ? completionRaw.threshold : void 0
213
+ } : void 0,
214
+ xapi: isRecord(xapiRaw) ? {
215
+ activityIri: typeof xapiRaw.activityIri === "string" ? xapiRaw.activityIri : void 0
216
+ } : void 0
217
+ };
218
+ }
219
+ const themeRaw = input.theme;
220
+ let theme;
221
+ if (isRecord(themeRaw)) {
222
+ theme = {
223
+ preset: typeof themeRaw.preset === "string" ? themeRaw.preset : void 0
224
+ };
225
+ if (isRecord(themeRaw.theme)) {
226
+ theme.theme = themeRaw.theme;
227
+ }
228
+ }
229
+ return {
230
+ courseId: typeof input.courseId === "string" ? input.courseId : "",
231
+ title: typeof input.title === "string" ? input.title : "",
232
+ version: typeof input.version === "string" ? input.version : void 0,
233
+ layout: typeof input.layout === "string" ? input.layout : void 0,
234
+ lessons: Array.isArray(input.lessons) ? input.lessons.map(parseLessonDescriptor) : [],
235
+ assessments: Array.isArray(input.assessments) ? input.assessments.map(parseAssessmentDescriptor) : void 0,
236
+ theme,
237
+ tracking,
238
+ spaDistDir: typeof input.spaDistDir === "string" ? input.spaDistDir : void 0,
239
+ spaLessonId: typeof input.spaLessonId === "string" ? input.spaLessonId : void 0
240
+ };
241
+ }
242
+
243
+ // src/descriptor/validateCourse.ts
244
+ var import_core3 = require("@lessonkit/core");
66
245
 
67
246
  // src/spaPath.ts
68
247
  var import_node_fs = require("fs");
@@ -87,7 +266,8 @@ function assertResolvedPathUnderRoot(root, target) {
87
266
  const targetResolved = resolveComparablePath(target);
88
267
  const prefix = rootResolved.endsWith(import_node_path.sep) ? rootResolved : rootResolved + import_node_path.sep;
89
268
  const win32Prefix = rootResolved.endsWith(import_node_path.win32.sep) ? rootResolved : rootResolved + import_node_path.win32.sep;
90
- if (targetResolved !== rootResolved && !targetResolved.startsWith(prefix) && !targetResolved.startsWith(win32Prefix)) {
269
+ if (targetResolved !== rootResolved && !targetResolved.startsWith(prefix) && /* v8 ignore next */
270
+ !targetResolved.startsWith(win32Prefix)) {
91
271
  throw new Error(`unsafe path escapes project root: ${target}`);
92
272
  }
93
273
  }
@@ -148,133 +328,69 @@ function themeToLxpackRuntime(input) {
148
328
  };
149
329
  }
150
330
 
151
- // src/validateDescriptor.ts
152
- var VALID_LAYOUTS = ["single-spa", "per-lesson-spa"];
153
- var VALID_THEME_PRESETS = ["default", "light", "dark", "brand"];
154
- function isRecord(value) {
155
- return typeof value === "object" && value !== null && !Array.isArray(value);
156
- }
157
- function parseLessonDescriptor(raw) {
158
- if (!isRecord(raw)) {
159
- return { id: "", title: "" };
331
+ // src/descriptor/validateAssessments.ts
332
+ var import_core2 = require("@lessonkit/core");
333
+ var validateMcqLike = (assessment, path, issues) => {
334
+ if (!("choices" in assessment) || !("answer" in assessment) || typeof assessment.answer !== "string") {
335
+ return;
160
336
  }
161
- return {
162
- id: typeof raw.id === "string" ? raw.id : "",
163
- title: typeof raw.title === "string" ? raw.title : "",
164
- spaPath: typeof raw.spaPath === "string" ? raw.spaPath : void 0
165
- };
166
- }
167
- function parseAssessmentDescriptor(raw) {
168
- if (!isRecord(raw)) {
169
- return { checkId: "", question: "", choices: [], answer: "" };
337
+ const trimmedChoices = assessment.choices.map((c) => c.trim()).filter((c) => c.length > 0);
338
+ if (!trimmedChoices.length) {
339
+ issues.push({ path: `${path}.choices`, message: "at least one non-empty choice is required" });
170
340
  }
171
- return {
172
- checkId: typeof raw.checkId === "string" ? raw.checkId : "",
173
- question: typeof raw.question === "string" ? raw.question : "",
174
- choices: Array.isArray(raw.choices) ? raw.choices.filter((c) => typeof c === "string") : [],
175
- answer: typeof raw.answer === "string" ? raw.answer : "",
176
- passingScore: typeof raw.passingScore === "number" ? raw.passingScore : void 0
177
- };
178
- }
179
- function parseCourseDescriptorInput(input) {
180
- if (!isRecord(input)) return null;
181
- const trackingRaw = input.tracking;
182
- let tracking;
183
- if (isRecord(trackingRaw)) {
184
- const completionRaw = trackingRaw.completion;
185
- const xapiRaw = trackingRaw.xapi;
186
- tracking = {
187
- completion: isRecord(completionRaw) ? {
188
- threshold: typeof completionRaw.threshold === "number" ? completionRaw.threshold : void 0
189
- } : void 0,
190
- xapi: isRecord(xapiRaw) ? {
191
- activityIri: typeof xapiRaw.activityIri === "string" ? xapiRaw.activityIri : void 0
192
- } : void 0
193
- };
341
+ if (!assessment.answer.trim()) {
342
+ issues.push({ path: `${path}.answer`, message: "answer is required" });
343
+ } else if (trimmedChoices.length && !trimmedChoices.includes(assessment.answer.trim())) {
344
+ issues.push({ path: `${path}.answer`, message: "answer must match a choice" });
194
345
  }
195
- const themeRaw = input.theme;
196
- let theme;
197
- if (isRecord(themeRaw)) {
198
- theme = {
199
- preset: typeof themeRaw.preset === "string" ? themeRaw.preset : void 0
200
- };
201
- if (isRecord(themeRaw.theme)) {
202
- theme.theme = themeRaw.theme;
346
+ };
347
+ var ASSESSMENT_VALIDATORS = {
348
+ mcq: validateMcqLike,
349
+ trueFalse: (assessment, path, issues) => {
350
+ if (assessment.kind === "trueFalse" && typeof assessment.answer !== "boolean") {
351
+ issues.push({ path: `${path}.answer`, message: "answer must be a boolean for trueFalse" });
203
352
  }
353
+ },
354
+ fillInBlanks: (assessment, path, issues) => {
355
+ if (assessment.kind === "fillInBlanks" && !assessment.template?.trim()) {
356
+ issues.push({ path: `${path}.template`, message: "template is required for fillInBlanks" });
357
+ }
358
+ },
359
+ findHotspot: () => {
360
+ },
361
+ findMultipleHotspots: () => {
204
362
  }
205
- return {
206
- courseId: typeof input.courseId === "string" ? input.courseId : "",
207
- title: typeof input.title === "string" ? input.title : "",
208
- version: typeof input.version === "string" ? input.version : void 0,
209
- layout: typeof input.layout === "string" ? input.layout : void 0,
210
- lessons: Array.isArray(input.lessons) ? input.lessons.map(parseLessonDescriptor) : [],
211
- assessments: Array.isArray(input.assessments) ? input.assessments.map(parseAssessmentDescriptor) : void 0,
212
- theme,
213
- tracking,
214
- spaDistDir: typeof input.spaDistDir === "string" ? input.spaDistDir : void 0,
215
- spaLessonId: typeof input.spaLessonId === "string" ? input.spaLessonId : void 0
216
- };
217
- }
218
- function normalizeDescriptor(input) {
219
- const course = (0, import_core.validateId)(input.courseId, "courseId");
220
- if (!course.ok) throw new Error("normalizeDescriptor called with invalid courseId");
221
- return {
222
- ...input,
223
- courseId: course.id,
224
- title: input.title.trim(),
225
- version: input.version?.trim() || void 0,
226
- spaLessonId: input.spaLessonId?.trim() || void 0,
227
- lessons: input.lessons.map((lesson) => {
228
- const idResult = (0, import_core.validateId)(lesson.id, "lessonId");
229
- if (!idResult.ok) throw new Error("normalizeDescriptor called with invalid lesson id");
230
- return {
231
- ...lesson,
232
- id: idResult.id,
233
- title: lesson.title.trim(),
234
- spaPath: lesson.spaPath?.trim() || void 0
235
- };
236
- }),
237
- assessments: input.assessments?.map((assessment) => {
238
- const check = (0, import_core.validateId)(assessment.checkId, "checkId");
239
- if (!check.ok) throw new Error("normalizeDescriptor called with invalid checkId");
240
- return {
241
- ...assessment,
242
- checkId: check.id,
243
- question: assessment.question.trim(),
244
- choices: assessment.choices.map((c) => c.trim()).filter((c) => c.length > 0),
245
- answer: assessment.answer.trim()
246
- };
247
- })
248
- };
249
- }
250
- function validateDescriptor(input) {
251
- const parsed = parseCourseDescriptorInput(input);
252
- if (parsed === null) {
253
- return { ok: false, issues: [{ path: "course", message: "must be an object" }] };
254
- }
255
- return validateDescriptorParsed(parsed);
256
- }
257
- function validateDescriptorForTarget(input, target) {
258
- const result = validateDescriptor(input);
259
- if (!result.ok || !target) return result;
260
- if (target !== "xapi" && target !== "cmi5") return result;
261
- const activityIri = result.descriptor.tracking?.xapi?.activityIri?.trim();
262
- if (!activityIri) {
263
- return {
264
- ok: false,
265
- issues: [
266
- {
267
- path: "course.tracking.xapi.activityIri",
268
- message: "tracking.xapi.activityIri is required for xapi and cmi5 export targets"
269
- }
270
- ]
271
- };
363
+ };
364
+ function validateAssessmentEntry(assessment, index, issues, checkIds) {
365
+ const path = `assessments[${index}]`;
366
+ const check = (0, import_core2.validateId)(assessment.checkId, `${path}.checkId`);
367
+ if (!check.ok) {
368
+ issues.push(...check.issues.map((i) => ({ path: i.path, message: i.message })));
369
+ } else if (checkIds.has(check.id)) {
370
+ issues.push({ path: `${path}.checkId`, message: "duplicate checkId" });
371
+ } else {
372
+ checkIds.add(check.id);
373
+ }
374
+ if (!assessment.question?.trim()) {
375
+ issues.push({ path: `${path}.question`, message: "question is required" });
376
+ }
377
+ const kind = assessment.kind ?? "mcq";
378
+ ASSESSMENT_VALIDATORS[kind](assessment, path, issues);
379
+ const passingScore = assessment.passingScore;
380
+ if (passingScore !== void 0 && !(Number.isFinite(passingScore) && passingScore > 0)) {
381
+ issues.push({
382
+ path: `${path}.passingScore`,
383
+ message: "passingScore must be greater than 0 (absolute point threshold)"
384
+ });
272
385
  }
273
- return result;
274
386
  }
275
- function validateDescriptorParsed(input) {
387
+
388
+ // src/descriptor/validateCourse.ts
389
+ var VALID_LAYOUTS = ["single-spa", "per-lesson-spa"];
390
+ var VALID_THEME_PRESETS = ["default", "light", "dark", "brand"];
391
+ function validateCourseDescriptor(input) {
276
392
  const issues = [];
277
- const course = (0, import_core.validateId)(input.courseId, "courseId");
393
+ const course = (0, import_core3.validateId)(input.courseId, "courseId");
278
394
  if (!course.ok) issues.push(...course.issues.map((i) => ({ path: i.path, message: i.message })));
279
395
  if (!input.title?.trim()) {
280
396
  issues.push({ path: "title", message: "title is required" });
@@ -327,7 +443,7 @@ function validateDescriptorParsed(input) {
327
443
  const spaPaths = /* @__PURE__ */ new Set();
328
444
  for (const [index, lesson] of (input.lessons ?? []).entries()) {
329
445
  const path = `lessons[${index}]`;
330
- const lessonResult = (0, import_core.validateId)(lesson.id, `${path}.id`);
446
+ const lessonResult = (0, import_core3.validateId)(lesson.id, `${path}.id`);
331
447
  if (!lessonResult.ok) {
332
448
  issues.push(...lessonResult.issues.map((i) => ({ path: i.path, message: i.message })));
333
449
  } else if (lessonIds.has(lessonResult.id)) {
@@ -359,7 +475,7 @@ function validateDescriptorParsed(input) {
359
475
  }
360
476
  if (layout === "single-spa" && input.spaLessonId?.trim()) {
361
477
  const spaId = input.spaLessonId.trim();
362
- const spaResult = (0, import_core.validateId)(spaId, "spaLessonId");
478
+ const spaResult = (0, import_core3.validateId)(spaId, "spaLessonId");
363
479
  if (!spaResult.ok) {
364
480
  issues.push(...spaResult.issues.map((i) => ({ path: i.path, message: i.message })));
365
481
  } else if (!lessonIds.has(spaResult.id)) {
@@ -371,41 +487,48 @@ function validateDescriptorParsed(input) {
371
487
  }
372
488
  const checkIds = /* @__PURE__ */ new Set();
373
489
  for (const [index, assessment] of (input.assessments ?? []).entries()) {
374
- const path = `assessments[${index}]`;
375
- const check = (0, import_core.validateId)(assessment.checkId, `${path}.checkId`);
376
- if (!check.ok) {
377
- issues.push(...check.issues.map((i) => ({ path: i.path, message: i.message })));
378
- } else if (checkIds.has(check.id)) {
379
- issues.push({ path: `${path}.checkId`, message: "duplicate checkId" });
380
- } else {
381
- checkIds.add(check.id);
382
- }
383
- if (!assessment.question?.trim()) {
384
- issues.push({ path: `${path}.question`, message: "question is required" });
385
- }
386
- const trimmedChoices = (assessment.choices ?? []).map((c) => c.trim()).filter((c) => c.length > 0);
387
- if (!trimmedChoices.length) {
388
- issues.push({
389
- path: `${path}.choices`,
390
- message: "at least one non-empty choice is required"
391
- });
392
- }
393
- if (!assessment.answer?.trim()) {
394
- issues.push({ path: `${path}.answer`, message: "answer is required" });
395
- } else if (trimmedChoices.length && !trimmedChoices.includes(assessment.answer.trim())) {
396
- issues.push({ path: `${path}.answer`, message: "answer must match a choice" });
397
- }
398
- const passingScore = assessment.passingScore;
399
- if (passingScore !== void 0 && !(Number.isFinite(passingScore) && passingScore > 0)) {
400
- issues.push({
401
- path: `${path}.passingScore`,
402
- message: "passingScore must be greater than 0 (absolute point threshold)"
403
- });
404
- }
490
+ validateAssessmentEntry(assessment, index, issues, checkIds);
405
491
  }
492
+ return issues;
493
+ }
494
+
495
+ // src/descriptor/validateForTarget.ts
496
+ function validateDescriptorForExportTarget(descriptor, target) {
497
+ if (target !== "xapi" && target !== "cmi5") return [];
498
+ const activityIri = descriptor.tracking?.xapi?.activityIri?.trim();
499
+ if (!activityIri) {
500
+ return [
501
+ {
502
+ path: "course.tracking.xapi.activityIri",
503
+ message: "tracking.xapi.activityIri is required for xapi and cmi5 export targets"
504
+ }
505
+ ];
506
+ }
507
+ return [];
508
+ }
509
+
510
+ // src/validateDescriptor.ts
511
+ function validateDescriptorParsed(input) {
512
+ const issues = validateCourseDescriptor(input);
406
513
  if (issues.length) return { ok: false, issues };
407
514
  return { ok: true, descriptor: normalizeDescriptor(input) };
408
515
  }
516
+ function validateDescriptor(input) {
517
+ const parsed = parseCourseDescriptorInput(input);
518
+ if (parsed === null) {
519
+ return { ok: false, issues: [{ path: "course", message: "must be an object" }] };
520
+ }
521
+ return validateDescriptorParsed(parsed);
522
+ }
523
+ function validateDescriptorForTarget(input, target) {
524
+ const result = validateDescriptor(input);
525
+ if (!result.ok || !target) return result;
526
+ const targetIssues = validateDescriptorForExportTarget(result.descriptor, target);
527
+ if (targetIssues.length) {
528
+ return { ok: false, issues: targetIssues };
529
+ }
530
+ return result;
531
+ }
409
532
 
410
533
  // src/validateProjectPaths.ts
411
534
  var import_node_path2 = require("path");
@@ -460,12 +583,12 @@ function resolveSafePackageOutputOverride(projectRoot, override) {
460
583
  }
461
584
 
462
585
  // src/mapIds.ts
463
- var import_core2 = require("@lessonkit/core");
586
+ var import_core4 = require("@lessonkit/core");
464
587
  function mapLessonkitIds(descriptor) {
465
- const courseId = (0, import_core2.assertValidId)(descriptor.courseId, "courseId");
466
- const lessonIds = descriptor.lessons.map((l) => (0, import_core2.assertValidId)(l.id, "lessonId"));
588
+ const courseId = (0, import_core4.assertValidId)(descriptor.courseId, "courseId");
589
+ const lessonIds = descriptor.lessons.map((l) => (0, import_core4.assertValidId)(l.id, "lessonId"));
467
590
  const checkIds = (descriptor.assessments ?? []).map(
468
- (a) => (0, import_core2.assertValidId)(a.checkId, "checkId")
591
+ (a) => (0, import_core4.assertValidId)(a.checkId, "checkId")
469
592
  );
470
593
  return { courseId, lessonIds, checkIds };
471
594
  }
@@ -476,7 +599,7 @@ function slugChoiceId(text, index) {
476
599
  const stem = base.length ? base : "choice";
477
600
  return `${stem}-${index + 1}`;
478
601
  }
479
- function assessmentDescriptorToLxpack(assessment) {
602
+ function mcqToLxpack(assessment) {
480
603
  const choices = assessment.choices.map((text, index) => {
481
604
  const id = slugChoiceId(text, index);
482
605
  return {
@@ -497,8 +620,43 @@ function assessmentDescriptorToLxpack(assessment) {
497
620
  ]
498
621
  };
499
622
  }
623
+ function assessmentDescriptorToLxpack(assessment) {
624
+ const kind = assessment.kind ?? "mcq";
625
+ if (kind === "trueFalse" && assessment.kind === "trueFalse") {
626
+ const choices = ["True", "False"];
627
+ const answerText = assessment.answer ? "True" : "False";
628
+ return mcqToLxpack({
629
+ kind: "mcq",
630
+ checkId: assessment.checkId,
631
+ question: assessment.question,
632
+ choices,
633
+ answer: answerText,
634
+ passingScore: assessment.passingScore
635
+ });
636
+ }
637
+ if (kind === "fillInBlanks") {
638
+ return null;
639
+ }
640
+ if (kind === "findHotspot" && assessment.kind === "findHotspot") {
641
+ return mcqToLxpack({
642
+ kind: "mcq",
643
+ checkId: assessment.checkId,
644
+ question: assessment.question,
645
+ choices: [assessment.correctTargetId, "other"],
646
+ answer: assessment.correctTargetId,
647
+ passingScore: assessment.passingScore
648
+ });
649
+ }
650
+ if (kind === "findMultipleHotspots") {
651
+ return null;
652
+ }
653
+ if ("choices" in assessment && "answer" in assessment && typeof assessment.answer === "string") {
654
+ return mcqToLxpack(assessment);
655
+ }
656
+ return null;
657
+ }
500
658
  function extractAssessments(descriptor) {
501
- return (descriptor.assessments ?? []).map(assessmentDescriptorToLxpack);
659
+ return (descriptor.assessments ?? []).map(assessmentDescriptorToLxpack).filter((a) => a !== null);
502
660
  }
503
661
 
504
662
  // src/interchange.ts
@@ -571,7 +729,8 @@ async function resolveSpaDirs(options) {
571
729
  const { descriptor, spaDistDir, lessonSpaDirs, projectRoot } = options;
572
730
  const spaLessons = resolveSpaLessons(descriptor);
573
731
  if (descriptor.layout === "single-spa") {
574
- const spaDistRelative = spaDistDir ?? descriptor.spaDistDir ?? "dist";
732
+ const spaDistRelative = spaDistDir ?? descriptor.spaDistDir ?? /* v8 ignore next */
733
+ "dist";
575
734
  const srcDist = projectRoot ? (0, import_node_path3.resolve)(projectRoot, spaDistRelative) : (0, import_node_path3.resolve)(spaDistRelative);
576
735
  if (projectRoot) {
577
736
  assertRealPathUnderRoot((0, import_node_path3.resolve)(projectRoot), srcDist);
@@ -586,7 +745,8 @@ async function resolveSpaDirs(options) {
586
745
  } catch {
587
746
  throw new Error(`spaDistDir must contain index.html: ${(0, import_node_path3.join)(srcDist, "index.html")}`);
588
747
  }
589
- const lessonId = spaLessons[0]?.id ?? "main";
748
+ const lessonId = spaLessons[0]?.id ?? /* v8 ignore next */
749
+ "main";
590
750
  return { [lessonId]: srcDist };
591
751
  }
592
752
  const dirs = {};
@@ -670,7 +830,15 @@ function validatePackageInputs(options) {
670
830
  ok: false,
671
831
  courseDir: outDir,
672
832
  target,
673
- issues: [{ path: "outDir", message: err instanceof Error ? err.message : String(err) }]
833
+ issues: [
834
+ {
835
+ path: "outDir",
836
+ message: (
837
+ /* v8 ignore next */
838
+ err instanceof Error ? err.message : String(err)
839
+ )
840
+ }
841
+ ]
674
842
  };
675
843
  }
676
844
  }
@@ -702,7 +870,10 @@ function validatePackageInputs(options) {
702
870
  issues: [
703
871
  {
704
872
  path: "outputBaseDir",
705
- message: err instanceof Error ? err.message : String(err)
873
+ message: (
874
+ /* v8 ignore next */
875
+ err instanceof Error ? err.message : String(err)
876
+ )
706
877
  }
707
878
  ]
708
879
  };
@@ -717,7 +888,15 @@ function validatePackageInputs(options) {
717
888
  ok: false,
718
889
  courseDir: outDir,
719
890
  target,
720
- issues: [{ path: "output", message: err instanceof Error ? err.message : String(err) }]
891
+ issues: [
892
+ {
893
+ path: "output",
894
+ message: (
895
+ /* v8 ignore next */
896
+ err instanceof Error ? err.message : String(err)
897
+ )
898
+ }
899
+ ]
721
900
  };
722
901
  }
723
902
  }
@@ -806,7 +985,10 @@ async function promoteStagingToOutDir(stagingDir, outDir) {
806
985
  try {
807
986
  await renameOrCopy(tmpPromote, failedPromote2);
808
987
  } catch {
809
- await fsp.rm(tmpPromote, { recursive: true, force: true }).catch(() => void 0);
988
+ await fsp.rm(tmpPromote, { recursive: true, force: true }).catch(
989
+ /* v8 ignore next */
990
+ () => void 0
991
+ );
810
992
  }
811
993
  const promoteMsg = promoteError instanceof Error ? promoteError.message : String(promoteError);
812
994
  const restoreMsg = restoreError instanceof Error ? restoreError.message : String(restoreError);
@@ -822,7 +1004,10 @@ async function promoteStagingToOutDir(stagingDir, outDir) {
822
1004
  `[lessonkit/lxpack] failed to restore ${stagingDir} after promote error:`,
823
1005
  restoreError instanceof Error ? restoreError.message : restoreError
824
1006
  );
825
- await fsp.rm(tmpPromote, { recursive: true, force: true }).catch(() => void 0);
1007
+ await fsp.rm(tmpPromote, { recursive: true, force: true }).catch(
1008
+ /* v8 ignore next */
1009
+ () => void 0
1010
+ );
826
1011
  }
827
1012
  throw promoteError;
828
1013
  }
@@ -830,12 +1015,18 @@ async function promoteStagingToOutDir(stagingDir, outDir) {
830
1015
  try {
831
1016
  await renameOrCopy(tmpPromote, failedPromote);
832
1017
  } catch {
833
- await fsp.rm(tmpPromote, { recursive: true, force: true }).catch(() => void 0);
1018
+ await fsp.rm(tmpPromote, { recursive: true, force: true }).catch(
1019
+ /* v8 ignore next */
1020
+ () => void 0
1021
+ );
834
1022
  }
835
1023
  throw promoteError;
836
1024
  }
837
1025
  if (backup) {
838
- await fsp.rm(backup, { recursive: true, force: true }).catch(() => void 0);
1026
+ await fsp.rm(backup, { recursive: true, force: true }).catch(
1027
+ /* v8 ignore next */
1028
+ () => void 0
1029
+ );
839
1030
  }
840
1031
  }
841
1032
 
@@ -898,7 +1089,10 @@ async function buildStagingPackage(options) {
898
1089
  outputDir: "outputDir" in build ? build.outputDir : void 0
899
1090
  };
900
1091
  } catch (err) {
901
- await fsp2.rm(stagingDir, { recursive: true, force: true }).catch(() => void 0);
1092
+ await fsp2.rm(stagingDir, { recursive: true, force: true }).catch(
1093
+ /* v8 ignore next */
1094
+ () => void 0
1095
+ );
902
1096
  throw err;
903
1097
  }
904
1098
  }
@@ -964,7 +1158,10 @@ async function packageLessonkitCourse(options) {
964
1158
  outputBaseDir
965
1159
  });
966
1160
  if (!staged.ok) {
967
- await fsp3.rm(staged.stagingDir, { recursive: true, force: true }).catch(() => void 0);
1161
+ await fsp3.rm(staged.stagingDir, { recursive: true, force: true }).catch(
1162
+ /* v8 ignore next */
1163
+ () => void 0
1164
+ );
968
1165
  const validation2 = staged.build ? { ok: false, issues: staged.build.issues } : void 0;
969
1166
  return {
970
1167
  ok: false,
@@ -982,7 +1179,10 @@ async function packageLessonkitCourse(options) {
982
1179
  validateArtifactInStaging(stagingRoot, staged.outputDir, "outputDir")
983
1180
  ].filter((issue) => issue != null);
984
1181
  if (artifactIssues.length > 0) {
985
- await fsp3.rm(stagingDir, { recursive: true, force: true }).catch(() => void 0);
1182
+ await fsp3.rm(stagingDir, { recursive: true, force: true }).catch(
1183
+ /* v8 ignore next */
1184
+ () => void 0
1185
+ );
986
1186
  return {
987
1187
  ok: false,
988
1188
  courseDir: outDir,
@@ -1084,7 +1284,10 @@ function parseLessonkitManifest(raw, label = "lessonkit.json", projectRoot) {
1084
1284
  if (!validation.ok) {
1085
1285
  for (const i of validation.issues) {
1086
1286
  issues.push({
1087
- path: i.path.startsWith("course.") ? i.path : `course.${i.path}`,
1287
+ path: (
1288
+ /* v8 ignore next */
1289
+ i.path.startsWith("course.") ? i.path : `course.${i.path}`
1290
+ ),
1088
1291
  message: i.message
1089
1292
  });
1090
1293
  }