@lessonkit/lxpack 1.0.2 → 1.1.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/bridge.cjs CHANGED
@@ -155,10 +155,31 @@ function dispatchBridgeActionInner(bridge, action) {
155
155
  return;
156
156
  }
157
157
  }
158
+ function forwardAssessmentCompletedToBridge(bridge, event) {
159
+ const data = event.data;
160
+ const scaled = normalizeAssessmentScore({
161
+ score: data.score,
162
+ maxScore: data.maxScore
163
+ });
164
+ if (scaled === null) return;
165
+ bridge.submitAssessment?.({
166
+ id: data.checkId,
167
+ score: scaled,
168
+ passingScore: normalizeAssessmentPassingScore({
169
+ passingScore: data.passingScore,
170
+ maxScore: data.maxScore
171
+ }),
172
+ maxScore: data.maxScore
173
+ });
174
+ }
158
175
  function forwardTelemetryToBridge(event, mode = "auto", parentWindow) {
159
176
  if (mode === "off") return;
160
177
  const bridge = getBridge(parentWindow);
161
178
  if (!bridge) return;
179
+ if (event.name === "assessment_completed") {
180
+ forwardAssessmentCompletedToBridge(bridge, event);
181
+ return;
182
+ }
162
183
  const lessonkitEvent = telemetryEventToLessonkit(event);
163
184
  if (!lessonkitEvent) return;
164
185
  const action = (0, import_tracking_schema3.mapLessonkitTelemetryToBridgeAction)(lessonkitEvent);
package/dist/bridge.d.cts CHANGED
@@ -24,7 +24,6 @@ declare function normalizeAssessmentPassingScore(opts?: {
24
24
  type LxpackBridgeMode = "auto" | "off";
25
25
  /** Apply a mapped bridge action to an LXPack bridge instance. */
26
26
  declare function dispatchBridgeAction(bridge: LxpackBridgeV1, action: ReturnType<typeof mapLessonkitTelemetryToBridgeAction>): void;
27
- /** Resolve bridge and dispatch a telemetry-derived action. */
28
27
  declare function forwardTelemetryToBridge(event: TelemetryEvent, mode?: LxpackBridgeMode, parentWindow?: Window): void;
29
28
  declare function createLxpackBridge(): LxpackBridgeV1 | null;
30
29
  declare function notifyLxpackLessonComplete(lessonId: LessonId): boolean;
package/dist/bridge.d.ts CHANGED
@@ -24,7 +24,6 @@ declare function normalizeAssessmentPassingScore(opts?: {
24
24
  type LxpackBridgeMode = "auto" | "off";
25
25
  /** Apply a mapped bridge action to an LXPack bridge instance. */
26
26
  declare function dispatchBridgeAction(bridge: LxpackBridgeV1, action: ReturnType<typeof mapLessonkitTelemetryToBridgeAction>): void;
27
- /** Resolve bridge and dispatch a telemetry-derived action. */
28
27
  declare function forwardTelemetryToBridge(event: TelemetryEvent, mode?: LxpackBridgeMode, parentWindow?: Window): void;
29
28
  declare function createLxpackBridge(): LxpackBridgeV1 | null;
30
29
  declare function notifyLxpackLessonComplete(lessonId: LessonId): boolean;
package/dist/bridge.js CHANGED
@@ -93,10 +93,31 @@ function dispatchBridgeActionInner(bridge, action) {
93
93
  return;
94
94
  }
95
95
  }
96
+ function forwardAssessmentCompletedToBridge(bridge, event) {
97
+ const data = event.data;
98
+ const scaled = normalizeAssessmentScore({
99
+ score: data.score,
100
+ maxScore: data.maxScore
101
+ });
102
+ if (scaled === null) return;
103
+ bridge.submitAssessment?.({
104
+ id: data.checkId,
105
+ score: scaled,
106
+ passingScore: normalizeAssessmentPassingScore({
107
+ passingScore: data.passingScore,
108
+ maxScore: data.maxScore
109
+ }),
110
+ maxScore: data.maxScore
111
+ });
112
+ }
96
113
  function forwardTelemetryToBridge(event, mode = "auto", parentWindow) {
97
114
  if (mode === "off") return;
98
115
  const bridge = getBridge(parentWindow);
99
116
  if (!bridge) return;
117
+ if (event.name === "assessment_completed") {
118
+ forwardAssessmentCompletedToBridge(bridge, event);
119
+ return;
120
+ }
100
121
  const lessonkitEvent = telemetryEventToLessonkit(event);
101
122
  if (!lessonkitEvent) return;
102
123
  const action = mapLessonkitTelemetryToBridgeAction2(lessonkitEvent);
package/dist/index.cjs CHANGED
@@ -87,7 +87,8 @@ function assertResolvedPathUnderRoot(root, target) {
87
87
  const targetResolved = resolveComparablePath(target);
88
88
  const prefix = rootResolved.endsWith(import_node_path.sep) ? rootResolved : rootResolved + import_node_path.sep;
89
89
  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)) {
90
+ if (targetResolved !== rootResolved && !targetResolved.startsWith(prefix) && /* v8 ignore next */
91
+ !targetResolved.startsWith(win32Prefix)) {
91
92
  throw new Error(`unsafe path escapes project root: ${target}`);
92
93
  }
93
94
  }
@@ -168,13 +169,36 @@ function parseAssessmentDescriptor(raw) {
168
169
  if (!isRecord(raw)) {
169
170
  return { checkId: "", question: "", choices: [], answer: "" };
170
171
  }
171
- return {
172
+ const base = {
172
173
  checkId: typeof raw.checkId === "string" ? raw.checkId : "",
173
174
  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
175
  passingScore: typeof raw.passingScore === "number" ? raw.passingScore : void 0
177
176
  };
177
+ const kind = raw.kind;
178
+ if (kind === "trueFalse") {
179
+ return {
180
+ kind: "trueFalse",
181
+ ...base,
182
+ answer: typeof raw.answer === "boolean" ? raw.answer : raw.answer === "true"
183
+ };
184
+ }
185
+ if (kind === "fillInBlanks") {
186
+ return {
187
+ kind: "fillInBlanks",
188
+ ...base,
189
+ template: typeof raw.template === "string" ? raw.template : "",
190
+ blanks: Array.isArray(raw.blanks) ? raw.blanks.filter((b) => isRecord(b)).map((b) => ({
191
+ id: typeof b.id === "string" ? b.id : "",
192
+ answer: typeof b.answer === "string" ? b.answer : ""
193
+ })) : void 0
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
+ };
178
202
  }
179
203
  function parseCourseDescriptorInput(input) {
180
204
  if (!isRecord(input)) return null;
@@ -237,10 +261,26 @@ function normalizeDescriptor(input) {
237
261
  assessments: input.assessments?.map((assessment) => {
238
262
  const check = (0, import_core.validateId)(assessment.checkId, "checkId");
239
263
  if (!check.ok) throw new Error("normalizeDescriptor called with invalid checkId");
264
+ const question = assessment.question.trim();
265
+ if (assessment.kind === "trueFalse") {
266
+ return { ...assessment, checkId: check.id, question };
267
+ }
268
+ if (assessment.kind === "fillInBlanks") {
269
+ return {
270
+ ...assessment,
271
+ checkId: check.id,
272
+ question,
273
+ template: assessment.template.trim(),
274
+ blanks: assessment.blanks?.map((b) => ({
275
+ id: b.id.trim(),
276
+ answer: b.answer.trim()
277
+ }))
278
+ };
279
+ }
240
280
  return {
241
281
  ...assessment,
242
282
  checkId: check.id,
243
- question: assessment.question.trim(),
283
+ question,
244
284
  choices: assessment.choices.map((c) => c.trim()).filter((c) => c.length > 0),
245
285
  answer: assessment.answer.trim()
246
286
  };
@@ -383,17 +423,28 @@ function validateDescriptorParsed(input) {
383
423
  if (!assessment.question?.trim()) {
384
424
  issues.push({ path: `${path}.question`, message: "question is required" });
385
425
  }
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" });
426
+ const kind = assessment.kind ?? "mcq";
427
+ if (kind === "trueFalse" && assessment.kind === "trueFalse") {
428
+ if (typeof assessment.answer !== "boolean") {
429
+ issues.push({ path: `${path}.answer`, message: "answer must be a boolean for trueFalse" });
430
+ }
431
+ } else if (kind === "fillInBlanks" && assessment.kind === "fillInBlanks") {
432
+ if (!assessment.template?.trim()) {
433
+ issues.push({ path: `${path}.template`, message: "template is required for fillInBlanks" });
434
+ }
435
+ } else if ("choices" in assessment && "answer" in assessment && typeof assessment.answer === "string") {
436
+ const trimmedChoices = assessment.choices.map((c) => c.trim()).filter((c) => c.length > 0);
437
+ if (!trimmedChoices.length) {
438
+ issues.push({
439
+ path: `${path}.choices`,
440
+ message: "at least one non-empty choice is required"
441
+ });
442
+ }
443
+ if (!assessment.answer.trim()) {
444
+ issues.push({ path: `${path}.answer`, message: "answer is required" });
445
+ } else if (trimmedChoices.length && !trimmedChoices.includes(assessment.answer.trim())) {
446
+ issues.push({ path: `${path}.answer`, message: "answer must match a choice" });
447
+ }
397
448
  }
398
449
  const passingScore = assessment.passingScore;
399
450
  if (passingScore !== void 0 && !(Number.isFinite(passingScore) && passingScore > 0)) {
@@ -476,7 +527,7 @@ function slugChoiceId(text, index) {
476
527
  const stem = base.length ? base : "choice";
477
528
  return `${stem}-${index + 1}`;
478
529
  }
479
- function assessmentDescriptorToLxpack(assessment) {
530
+ function mcqToLxpack(assessment) {
480
531
  const choices = assessment.choices.map((text, index) => {
481
532
  const id = slugChoiceId(text, index);
482
533
  return {
@@ -497,8 +548,30 @@ function assessmentDescriptorToLxpack(assessment) {
497
548
  ]
498
549
  };
499
550
  }
551
+ function assessmentDescriptorToLxpack(assessment) {
552
+ const kind = assessment.kind ?? "mcq";
553
+ if (kind === "trueFalse" && assessment.kind === "trueFalse") {
554
+ const choices = ["True", "False"];
555
+ const answerText = assessment.answer ? "True" : "False";
556
+ return mcqToLxpack({
557
+ kind: "mcq",
558
+ checkId: assessment.checkId,
559
+ question: assessment.question,
560
+ choices,
561
+ answer: answerText,
562
+ passingScore: assessment.passingScore
563
+ });
564
+ }
565
+ if (kind === "fillInBlanks") {
566
+ return null;
567
+ }
568
+ if ("choices" in assessment && "answer" in assessment && typeof assessment.answer === "string") {
569
+ return mcqToLxpack(assessment);
570
+ }
571
+ return null;
572
+ }
500
573
  function extractAssessments(descriptor) {
501
- return (descriptor.assessments ?? []).map(assessmentDescriptorToLxpack);
574
+ return (descriptor.assessments ?? []).map(assessmentDescriptorToLxpack).filter((a) => a !== null);
502
575
  }
503
576
 
504
577
  // src/interchange.ts
@@ -571,7 +644,8 @@ async function resolveSpaDirs(options) {
571
644
  const { descriptor, spaDistDir, lessonSpaDirs, projectRoot } = options;
572
645
  const spaLessons = resolveSpaLessons(descriptor);
573
646
  if (descriptor.layout === "single-spa") {
574
- const spaDistRelative = spaDistDir ?? descriptor.spaDistDir ?? "dist";
647
+ const spaDistRelative = spaDistDir ?? descriptor.spaDistDir ?? /* v8 ignore next */
648
+ "dist";
575
649
  const srcDist = projectRoot ? (0, import_node_path3.resolve)(projectRoot, spaDistRelative) : (0, import_node_path3.resolve)(spaDistRelative);
576
650
  if (projectRoot) {
577
651
  assertRealPathUnderRoot((0, import_node_path3.resolve)(projectRoot), srcDist);
@@ -586,7 +660,8 @@ async function resolveSpaDirs(options) {
586
660
  } catch {
587
661
  throw new Error(`spaDistDir must contain index.html: ${(0, import_node_path3.join)(srcDist, "index.html")}`);
588
662
  }
589
- const lessonId = spaLessons[0]?.id ?? "main";
663
+ const lessonId = spaLessons[0]?.id ?? /* v8 ignore next */
664
+ "main";
590
665
  return { [lessonId]: srcDist };
591
666
  }
592
667
  const dirs = {};
@@ -670,7 +745,15 @@ function validatePackageInputs(options) {
670
745
  ok: false,
671
746
  courseDir: outDir,
672
747
  target,
673
- issues: [{ path: "outDir", message: err instanceof Error ? err.message : String(err) }]
748
+ issues: [
749
+ {
750
+ path: "outDir",
751
+ message: (
752
+ /* v8 ignore next */
753
+ err instanceof Error ? err.message : String(err)
754
+ )
755
+ }
756
+ ]
674
757
  };
675
758
  }
676
759
  }
@@ -702,7 +785,10 @@ function validatePackageInputs(options) {
702
785
  issues: [
703
786
  {
704
787
  path: "outputBaseDir",
705
- message: err instanceof Error ? err.message : String(err)
788
+ message: (
789
+ /* v8 ignore next */
790
+ err instanceof Error ? err.message : String(err)
791
+ )
706
792
  }
707
793
  ]
708
794
  };
@@ -717,7 +803,15 @@ function validatePackageInputs(options) {
717
803
  ok: false,
718
804
  courseDir: outDir,
719
805
  target,
720
- issues: [{ path: "output", message: err instanceof Error ? err.message : String(err) }]
806
+ issues: [
807
+ {
808
+ path: "output",
809
+ message: (
810
+ /* v8 ignore next */
811
+ err instanceof Error ? err.message : String(err)
812
+ )
813
+ }
814
+ ]
721
815
  };
722
816
  }
723
817
  }
@@ -806,7 +900,10 @@ async function promoteStagingToOutDir(stagingDir, outDir) {
806
900
  try {
807
901
  await renameOrCopy(tmpPromote, failedPromote2);
808
902
  } catch {
809
- await fsp.rm(tmpPromote, { recursive: true, force: true }).catch(() => void 0);
903
+ await fsp.rm(tmpPromote, { recursive: true, force: true }).catch(
904
+ /* v8 ignore next */
905
+ () => void 0
906
+ );
810
907
  }
811
908
  const promoteMsg = promoteError instanceof Error ? promoteError.message : String(promoteError);
812
909
  const restoreMsg = restoreError instanceof Error ? restoreError.message : String(restoreError);
@@ -822,7 +919,10 @@ async function promoteStagingToOutDir(stagingDir, outDir) {
822
919
  `[lessonkit/lxpack] failed to restore ${stagingDir} after promote error:`,
823
920
  restoreError instanceof Error ? restoreError.message : restoreError
824
921
  );
825
- await fsp.rm(tmpPromote, { recursive: true, force: true }).catch(() => void 0);
922
+ await fsp.rm(tmpPromote, { recursive: true, force: true }).catch(
923
+ /* v8 ignore next */
924
+ () => void 0
925
+ );
826
926
  }
827
927
  throw promoteError;
828
928
  }
@@ -830,12 +930,18 @@ async function promoteStagingToOutDir(stagingDir, outDir) {
830
930
  try {
831
931
  await renameOrCopy(tmpPromote, failedPromote);
832
932
  } catch {
833
- await fsp.rm(tmpPromote, { recursive: true, force: true }).catch(() => void 0);
933
+ await fsp.rm(tmpPromote, { recursive: true, force: true }).catch(
934
+ /* v8 ignore next */
935
+ () => void 0
936
+ );
834
937
  }
835
938
  throw promoteError;
836
939
  }
837
940
  if (backup) {
838
- await fsp.rm(backup, { recursive: true, force: true }).catch(() => void 0);
941
+ await fsp.rm(backup, { recursive: true, force: true }).catch(
942
+ /* v8 ignore next */
943
+ () => void 0
944
+ );
839
945
  }
840
946
  }
841
947
 
@@ -898,7 +1004,10 @@ async function buildStagingPackage(options) {
898
1004
  outputDir: "outputDir" in build ? build.outputDir : void 0
899
1005
  };
900
1006
  } catch (err) {
901
- await fsp2.rm(stagingDir, { recursive: true, force: true }).catch(() => void 0);
1007
+ await fsp2.rm(stagingDir, { recursive: true, force: true }).catch(
1008
+ /* v8 ignore next */
1009
+ () => void 0
1010
+ );
902
1011
  throw err;
903
1012
  }
904
1013
  }
@@ -964,7 +1073,10 @@ async function packageLessonkitCourse(options) {
964
1073
  outputBaseDir
965
1074
  });
966
1075
  if (!staged.ok) {
967
- await fsp3.rm(staged.stagingDir, { recursive: true, force: true }).catch(() => void 0);
1076
+ await fsp3.rm(staged.stagingDir, { recursive: true, force: true }).catch(
1077
+ /* v8 ignore next */
1078
+ () => void 0
1079
+ );
968
1080
  const validation2 = staged.build ? { ok: false, issues: staged.build.issues } : void 0;
969
1081
  return {
970
1082
  ok: false,
@@ -982,7 +1094,10 @@ async function packageLessonkitCourse(options) {
982
1094
  validateArtifactInStaging(stagingRoot, staged.outputDir, "outputDir")
983
1095
  ].filter((issue) => issue != null);
984
1096
  if (artifactIssues.length > 0) {
985
- await fsp3.rm(stagingDir, { recursive: true, force: true }).catch(() => void 0);
1097
+ await fsp3.rm(stagingDir, { recursive: true, force: true }).catch(
1098
+ /* v8 ignore next */
1099
+ () => void 0
1100
+ );
986
1101
  return {
987
1102
  ok: false,
988
1103
  courseDir: outDir,
@@ -1084,7 +1199,10 @@ function parseLessonkitManifest(raw, label = "lessonkit.json", projectRoot) {
1084
1199
  if (!validation.ok) {
1085
1200
  for (const i of validation.issues) {
1086
1201
  issues.push({
1087
- path: i.path.startsWith("course.") ? i.path : `course.${i.path}`,
1202
+ path: (
1203
+ /* v8 ignore next */
1204
+ i.path.startsWith("course.") ? i.path : `course.${i.path}`
1205
+ ),
1088
1206
  message: i.message
1089
1207
  });
1090
1208
  }
package/dist/index.d.cts CHANGED
@@ -14,13 +14,34 @@ type LessonDescriptor = {
14
14
  /** Built SPA folder relative to the LXPack project root (`per-lesson-spa` only). */
15
15
  spaPath?: string;
16
16
  };
17
- type AssessmentDescriptor = {
17
+ type McqAssessmentDescriptor = {
18
+ kind?: "mcq";
18
19
  checkId: CheckId;
19
20
  question: string;
20
21
  choices: string[];
21
22
  answer: string;
22
23
  passingScore?: number;
23
24
  };
25
+ type TrueFalseAssessmentDescriptor = {
26
+ kind: "trueFalse";
27
+ checkId: CheckId;
28
+ question: string;
29
+ answer: boolean;
30
+ passingScore?: number;
31
+ };
32
+ type FillInBlanksAssessmentDescriptor = {
33
+ kind: "fillInBlanks";
34
+ checkId: CheckId;
35
+ question: string;
36
+ template: string;
37
+ blanks?: Array<{
38
+ id: string;
39
+ answer: string;
40
+ }>;
41
+ passingScore?: number;
42
+ };
43
+ /** Discriminated assessment entries in lessonkit.json (defaults to MCQ when kind omitted). */
44
+ type AssessmentDescriptor = McqAssessmentDescriptor | TrueFalseAssessmentDescriptor | FillInBlanksAssessmentDescriptor;
24
45
  type LessonkitCourseDescriptor = {
25
46
  courseId: CourseId;
26
47
  title: string;
@@ -120,7 +141,7 @@ type LxpackInjectedAssessment = {
120
141
  }>;
121
142
  }>;
122
143
  };
123
- declare function assessmentDescriptorToLxpack(assessment: AssessmentDescriptor): LxpackInjectedAssessment;
144
+ declare function assessmentDescriptorToLxpack(assessment: AssessmentDescriptor): LxpackInjectedAssessment | null;
124
145
  declare function extractAssessments(descriptor: LessonkitCourseDescriptor): LxpackInjectedAssessment[];
125
146
 
126
147
  type WriteLxpackProjectOptions = {
@@ -149,10 +170,6 @@ type WriteLxpackProjectResult = {
149
170
  */
150
171
  declare function writeLxpackProject(options: WriteLxpackProjectOptions): Promise<WriteLxpackProjectResult>;
151
172
 
152
- /**
153
- * Atomically replace `outDir` with the packaged tree at `stagingDir`.
154
- * Restores the previous `outDir` when promote fails after a backup rename.
155
- */
156
173
  declare function promoteStagingToOutDir(stagingDir: string, outDir: string): Promise<void>;
157
174
 
158
175
  type BuildStagingPackageOptions = WriteLxpackProjectOptions & {
@@ -273,4 +290,4 @@ type ParseManifestResult = {
273
290
  declare function parseLessonkitManifest(raw: unknown, label?: string, projectRoot?: string): ParseManifestResult;
274
291
  declare function loadLessonkitManifestFromFile(readJson: () => Promise<unknown>, label?: string, projectRoot?: string): Promise<ParseManifestResult>;
275
292
 
276
- export { type AssessmentDescriptor, type BuildLessonkitProjectOptions, type BuildStagingPackageOptions, type BuildStagingPackageResult, type DescriptorValidationIssue, type DescriptorValidationResult, type LessonDescriptor, type LessonkitCourseDescriptor, type LessonkitManifest, type LessonkitManifestPaths, type LxpackInjectedAssessment, type LxpackRuntimeTheme, type ManifestParseIssue, type MappedLessonkitIds, type PackageLessonkitCourseOptions, type PackageLessonkitCourseResult, type PackageValidationIssue, type ParseManifestResult, type ProjectPathsInput, type SpaLayout, type SpaLessonEntry, type ValidateLessonkitProjectOptions, type ValidatePackageInputsResult, type ValidationIssue, type WriteLxpackProjectOptions, type WriteLxpackProjectResult, assessmentDescriptorToLxpack, buildLessonkitProject, buildStagingPackage, descriptorToInterchange, ensureOutDirParent, extractAssessments, loadLessonkitManifestFromFile, mapLessonkitIds, packageLessonkitCourse, parseLessonkitManifest, promoteStagingToOutDir, remapArtifactPaths, resolveSafePackageOutputOverride, resolveSpaLessons, themeToLxpackRuntime, validateDescriptor, validateDescriptorForTarget, validateLessonkitProject, validatePackageInputs, validateProjectPaths, writeLxpackProject };
293
+ export { type AssessmentDescriptor, type BuildLessonkitProjectOptions, type BuildStagingPackageOptions, type BuildStagingPackageResult, type DescriptorValidationIssue, type DescriptorValidationResult, type FillInBlanksAssessmentDescriptor, type LessonDescriptor, type LessonkitCourseDescriptor, type LessonkitManifest, type LessonkitManifestPaths, type LxpackInjectedAssessment, type LxpackRuntimeTheme, type ManifestParseIssue, type MappedLessonkitIds, type McqAssessmentDescriptor, type PackageLessonkitCourseOptions, type PackageLessonkitCourseResult, type PackageValidationIssue, type ParseManifestResult, type ProjectPathsInput, type SpaLayout, type SpaLessonEntry, type TrueFalseAssessmentDescriptor, type ValidateLessonkitProjectOptions, type ValidatePackageInputsResult, type ValidationIssue, type WriteLxpackProjectOptions, type WriteLxpackProjectResult, assessmentDescriptorToLxpack, buildLessonkitProject, buildStagingPackage, descriptorToInterchange, ensureOutDirParent, extractAssessments, loadLessonkitManifestFromFile, mapLessonkitIds, packageLessonkitCourse, parseLessonkitManifest, promoteStagingToOutDir, remapArtifactPaths, resolveSafePackageOutputOverride, resolveSpaLessons, themeToLxpackRuntime, validateDescriptor, validateDescriptorForTarget, validateLessonkitProject, validatePackageInputs, validateProjectPaths, writeLxpackProject };
package/dist/index.d.ts CHANGED
@@ -14,13 +14,34 @@ type LessonDescriptor = {
14
14
  /** Built SPA folder relative to the LXPack project root (`per-lesson-spa` only). */
15
15
  spaPath?: string;
16
16
  };
17
- type AssessmentDescriptor = {
17
+ type McqAssessmentDescriptor = {
18
+ kind?: "mcq";
18
19
  checkId: CheckId;
19
20
  question: string;
20
21
  choices: string[];
21
22
  answer: string;
22
23
  passingScore?: number;
23
24
  };
25
+ type TrueFalseAssessmentDescriptor = {
26
+ kind: "trueFalse";
27
+ checkId: CheckId;
28
+ question: string;
29
+ answer: boolean;
30
+ passingScore?: number;
31
+ };
32
+ type FillInBlanksAssessmentDescriptor = {
33
+ kind: "fillInBlanks";
34
+ checkId: CheckId;
35
+ question: string;
36
+ template: string;
37
+ blanks?: Array<{
38
+ id: string;
39
+ answer: string;
40
+ }>;
41
+ passingScore?: number;
42
+ };
43
+ /** Discriminated assessment entries in lessonkit.json (defaults to MCQ when kind omitted). */
44
+ type AssessmentDescriptor = McqAssessmentDescriptor | TrueFalseAssessmentDescriptor | FillInBlanksAssessmentDescriptor;
24
45
  type LessonkitCourseDescriptor = {
25
46
  courseId: CourseId;
26
47
  title: string;
@@ -120,7 +141,7 @@ type LxpackInjectedAssessment = {
120
141
  }>;
121
142
  }>;
122
143
  };
123
- declare function assessmentDescriptorToLxpack(assessment: AssessmentDescriptor): LxpackInjectedAssessment;
144
+ declare function assessmentDescriptorToLxpack(assessment: AssessmentDescriptor): LxpackInjectedAssessment | null;
124
145
  declare function extractAssessments(descriptor: LessonkitCourseDescriptor): LxpackInjectedAssessment[];
125
146
 
126
147
  type WriteLxpackProjectOptions = {
@@ -149,10 +170,6 @@ type WriteLxpackProjectResult = {
149
170
  */
150
171
  declare function writeLxpackProject(options: WriteLxpackProjectOptions): Promise<WriteLxpackProjectResult>;
151
172
 
152
- /**
153
- * Atomically replace `outDir` with the packaged tree at `stagingDir`.
154
- * Restores the previous `outDir` when promote fails after a backup rename.
155
- */
156
173
  declare function promoteStagingToOutDir(stagingDir: string, outDir: string): Promise<void>;
157
174
 
158
175
  type BuildStagingPackageOptions = WriteLxpackProjectOptions & {
@@ -273,4 +290,4 @@ type ParseManifestResult = {
273
290
  declare function parseLessonkitManifest(raw: unknown, label?: string, projectRoot?: string): ParseManifestResult;
274
291
  declare function loadLessonkitManifestFromFile(readJson: () => Promise<unknown>, label?: string, projectRoot?: string): Promise<ParseManifestResult>;
275
292
 
276
- export { type AssessmentDescriptor, type BuildLessonkitProjectOptions, type BuildStagingPackageOptions, type BuildStagingPackageResult, type DescriptorValidationIssue, type DescriptorValidationResult, type LessonDescriptor, type LessonkitCourseDescriptor, type LessonkitManifest, type LessonkitManifestPaths, type LxpackInjectedAssessment, type LxpackRuntimeTheme, type ManifestParseIssue, type MappedLessonkitIds, type PackageLessonkitCourseOptions, type PackageLessonkitCourseResult, type PackageValidationIssue, type ParseManifestResult, type ProjectPathsInput, type SpaLayout, type SpaLessonEntry, type ValidateLessonkitProjectOptions, type ValidatePackageInputsResult, type ValidationIssue, type WriteLxpackProjectOptions, type WriteLxpackProjectResult, assessmentDescriptorToLxpack, buildLessonkitProject, buildStagingPackage, descriptorToInterchange, ensureOutDirParent, extractAssessments, loadLessonkitManifestFromFile, mapLessonkitIds, packageLessonkitCourse, parseLessonkitManifest, promoteStagingToOutDir, remapArtifactPaths, resolveSafePackageOutputOverride, resolveSpaLessons, themeToLxpackRuntime, validateDescriptor, validateDescriptorForTarget, validateLessonkitProject, validatePackageInputs, validateProjectPaths, writeLxpackProject };
293
+ export { type AssessmentDescriptor, type BuildLessonkitProjectOptions, type BuildStagingPackageOptions, type BuildStagingPackageResult, type DescriptorValidationIssue, type DescriptorValidationResult, type FillInBlanksAssessmentDescriptor, type LessonDescriptor, type LessonkitCourseDescriptor, type LessonkitManifest, type LessonkitManifestPaths, type LxpackInjectedAssessment, type LxpackRuntimeTheme, type ManifestParseIssue, type MappedLessonkitIds, type McqAssessmentDescriptor, type PackageLessonkitCourseOptions, type PackageLessonkitCourseResult, type PackageValidationIssue, type ParseManifestResult, type ProjectPathsInput, type SpaLayout, type SpaLessonEntry, type TrueFalseAssessmentDescriptor, type ValidateLessonkitProjectOptions, type ValidatePackageInputsResult, type ValidationIssue, type WriteLxpackProjectOptions, type WriteLxpackProjectResult, assessmentDescriptorToLxpack, buildLessonkitProject, buildStagingPackage, descriptorToInterchange, ensureOutDirParent, extractAssessments, loadLessonkitManifestFromFile, mapLessonkitIds, packageLessonkitCourse, parseLessonkitManifest, promoteStagingToOutDir, remapArtifactPaths, resolveSafePackageOutputOverride, resolveSpaLessons, themeToLxpackRuntime, validateDescriptor, validateDescriptorForTarget, validateLessonkitProject, validatePackageInputs, validateProjectPaths, writeLxpackProject };
package/dist/index.js CHANGED
@@ -28,7 +28,8 @@ function assertResolvedPathUnderRoot(root, target) {
28
28
  const targetResolved = resolveComparablePath(target);
29
29
  const prefix = rootResolved.endsWith(sep) ? rootResolved : rootResolved + sep;
30
30
  const win32Prefix = rootResolved.endsWith(win32.sep) ? rootResolved : rootResolved + win32.sep;
31
- if (targetResolved !== rootResolved && !targetResolved.startsWith(prefix) && !targetResolved.startsWith(win32Prefix)) {
31
+ if (targetResolved !== rootResolved && !targetResolved.startsWith(prefix) && /* v8 ignore next */
32
+ !targetResolved.startsWith(win32Prefix)) {
32
33
  throw new Error(`unsafe path escapes project root: ${target}`);
33
34
  }
34
35
  }
@@ -109,13 +110,36 @@ function parseAssessmentDescriptor(raw) {
109
110
  if (!isRecord(raw)) {
110
111
  return { checkId: "", question: "", choices: [], answer: "" };
111
112
  }
112
- return {
113
+ const base = {
113
114
  checkId: typeof raw.checkId === "string" ? raw.checkId : "",
114
115
  question: typeof raw.question === "string" ? raw.question : "",
115
- choices: Array.isArray(raw.choices) ? raw.choices.filter((c) => typeof c === "string") : [],
116
- answer: typeof raw.answer === "string" ? raw.answer : "",
117
116
  passingScore: typeof raw.passingScore === "number" ? raw.passingScore : void 0
118
117
  };
118
+ const kind = raw.kind;
119
+ if (kind === "trueFalse") {
120
+ return {
121
+ kind: "trueFalse",
122
+ ...base,
123
+ answer: typeof raw.answer === "boolean" ? raw.answer : raw.answer === "true"
124
+ };
125
+ }
126
+ if (kind === "fillInBlanks") {
127
+ return {
128
+ kind: "fillInBlanks",
129
+ ...base,
130
+ template: typeof raw.template === "string" ? raw.template : "",
131
+ blanks: Array.isArray(raw.blanks) ? raw.blanks.filter((b) => isRecord(b)).map((b) => ({
132
+ id: typeof b.id === "string" ? b.id : "",
133
+ answer: typeof b.answer === "string" ? b.answer : ""
134
+ })) : void 0
135
+ };
136
+ }
137
+ return {
138
+ kind: kind === "mcq" ? "mcq" : void 0,
139
+ ...base,
140
+ choices: Array.isArray(raw.choices) ? raw.choices.filter((c) => typeof c === "string") : [],
141
+ answer: typeof raw.answer === "string" ? raw.answer : ""
142
+ };
119
143
  }
120
144
  function parseCourseDescriptorInput(input) {
121
145
  if (!isRecord(input)) return null;
@@ -178,10 +202,26 @@ function normalizeDescriptor(input) {
178
202
  assessments: input.assessments?.map((assessment) => {
179
203
  const check = validateId(assessment.checkId, "checkId");
180
204
  if (!check.ok) throw new Error("normalizeDescriptor called with invalid checkId");
205
+ const question = assessment.question.trim();
206
+ if (assessment.kind === "trueFalse") {
207
+ return { ...assessment, checkId: check.id, question };
208
+ }
209
+ if (assessment.kind === "fillInBlanks") {
210
+ return {
211
+ ...assessment,
212
+ checkId: check.id,
213
+ question,
214
+ template: assessment.template.trim(),
215
+ blanks: assessment.blanks?.map((b) => ({
216
+ id: b.id.trim(),
217
+ answer: b.answer.trim()
218
+ }))
219
+ };
220
+ }
181
221
  return {
182
222
  ...assessment,
183
223
  checkId: check.id,
184
- question: assessment.question.trim(),
224
+ question,
185
225
  choices: assessment.choices.map((c) => c.trim()).filter((c) => c.length > 0),
186
226
  answer: assessment.answer.trim()
187
227
  };
@@ -324,17 +364,28 @@ function validateDescriptorParsed(input) {
324
364
  if (!assessment.question?.trim()) {
325
365
  issues.push({ path: `${path}.question`, message: "question is required" });
326
366
  }
327
- const trimmedChoices = (assessment.choices ?? []).map((c) => c.trim()).filter((c) => c.length > 0);
328
- if (!trimmedChoices.length) {
329
- issues.push({
330
- path: `${path}.choices`,
331
- message: "at least one non-empty choice is required"
332
- });
333
- }
334
- if (!assessment.answer?.trim()) {
335
- issues.push({ path: `${path}.answer`, message: "answer is required" });
336
- } else if (trimmedChoices.length && !trimmedChoices.includes(assessment.answer.trim())) {
337
- issues.push({ path: `${path}.answer`, message: "answer must match a choice" });
367
+ const kind = assessment.kind ?? "mcq";
368
+ if (kind === "trueFalse" && assessment.kind === "trueFalse") {
369
+ if (typeof assessment.answer !== "boolean") {
370
+ issues.push({ path: `${path}.answer`, message: "answer must be a boolean for trueFalse" });
371
+ }
372
+ } else if (kind === "fillInBlanks" && assessment.kind === "fillInBlanks") {
373
+ if (!assessment.template?.trim()) {
374
+ issues.push({ path: `${path}.template`, message: "template is required for fillInBlanks" });
375
+ }
376
+ } else if ("choices" in assessment && "answer" in assessment && typeof assessment.answer === "string") {
377
+ const trimmedChoices = assessment.choices.map((c) => c.trim()).filter((c) => c.length > 0);
378
+ if (!trimmedChoices.length) {
379
+ issues.push({
380
+ path: `${path}.choices`,
381
+ message: "at least one non-empty choice is required"
382
+ });
383
+ }
384
+ if (!assessment.answer.trim()) {
385
+ issues.push({ path: `${path}.answer`, message: "answer is required" });
386
+ } else if (trimmedChoices.length && !trimmedChoices.includes(assessment.answer.trim())) {
387
+ issues.push({ path: `${path}.answer`, message: "answer must match a choice" });
388
+ }
338
389
  }
339
390
  const passingScore = assessment.passingScore;
340
391
  if (passingScore !== void 0 && !(Number.isFinite(passingScore) && passingScore > 0)) {
@@ -417,7 +468,7 @@ function slugChoiceId(text, index) {
417
468
  const stem = base.length ? base : "choice";
418
469
  return `${stem}-${index + 1}`;
419
470
  }
420
- function assessmentDescriptorToLxpack(assessment) {
471
+ function mcqToLxpack(assessment) {
421
472
  const choices = assessment.choices.map((text, index) => {
422
473
  const id = slugChoiceId(text, index);
423
474
  return {
@@ -438,8 +489,30 @@ function assessmentDescriptorToLxpack(assessment) {
438
489
  ]
439
490
  };
440
491
  }
492
+ function assessmentDescriptorToLxpack(assessment) {
493
+ const kind = assessment.kind ?? "mcq";
494
+ if (kind === "trueFalse" && assessment.kind === "trueFalse") {
495
+ const choices = ["True", "False"];
496
+ const answerText = assessment.answer ? "True" : "False";
497
+ return mcqToLxpack({
498
+ kind: "mcq",
499
+ checkId: assessment.checkId,
500
+ question: assessment.question,
501
+ choices,
502
+ answer: answerText,
503
+ passingScore: assessment.passingScore
504
+ });
505
+ }
506
+ if (kind === "fillInBlanks") {
507
+ return null;
508
+ }
509
+ if ("choices" in assessment && "answer" in assessment && typeof assessment.answer === "string") {
510
+ return mcqToLxpack(assessment);
511
+ }
512
+ return null;
513
+ }
441
514
  function extractAssessments(descriptor) {
442
- return (descriptor.assessments ?? []).map(assessmentDescriptorToLxpack);
515
+ return (descriptor.assessments ?? []).map(assessmentDescriptorToLxpack).filter((a) => a !== null);
443
516
  }
444
517
 
445
518
  // src/interchange.ts
@@ -512,7 +585,8 @@ async function resolveSpaDirs(options) {
512
585
  const { descriptor, spaDistDir, lessonSpaDirs, projectRoot } = options;
513
586
  const spaLessons = resolveSpaLessons(descriptor);
514
587
  if (descriptor.layout === "single-spa") {
515
- const spaDistRelative = spaDistDir ?? descriptor.spaDistDir ?? "dist";
588
+ const spaDistRelative = spaDistDir ?? descriptor.spaDistDir ?? /* v8 ignore next */
589
+ "dist";
516
590
  const srcDist = projectRoot ? resolve3(projectRoot, spaDistRelative) : resolve3(spaDistRelative);
517
591
  if (projectRoot) {
518
592
  assertRealPathUnderRoot(resolve3(projectRoot), srcDist);
@@ -527,7 +601,8 @@ async function resolveSpaDirs(options) {
527
601
  } catch {
528
602
  throw new Error(`spaDistDir must contain index.html: ${join(srcDist, "index.html")}`);
529
603
  }
530
- const lessonId = spaLessons[0]?.id ?? "main";
604
+ const lessonId = spaLessons[0]?.id ?? /* v8 ignore next */
605
+ "main";
531
606
  return { [lessonId]: srcDist };
532
607
  }
533
608
  const dirs = {};
@@ -614,7 +689,15 @@ function validatePackageInputs(options) {
614
689
  ok: false,
615
690
  courseDir: outDir,
616
691
  target,
617
- issues: [{ path: "outDir", message: err instanceof Error ? err.message : String(err) }]
692
+ issues: [
693
+ {
694
+ path: "outDir",
695
+ message: (
696
+ /* v8 ignore next */
697
+ err instanceof Error ? err.message : String(err)
698
+ )
699
+ }
700
+ ]
618
701
  };
619
702
  }
620
703
  }
@@ -646,7 +729,10 @@ function validatePackageInputs(options) {
646
729
  issues: [
647
730
  {
648
731
  path: "outputBaseDir",
649
- message: err instanceof Error ? err.message : String(err)
732
+ message: (
733
+ /* v8 ignore next */
734
+ err instanceof Error ? err.message : String(err)
735
+ )
650
736
  }
651
737
  ]
652
738
  };
@@ -661,7 +747,15 @@ function validatePackageInputs(options) {
661
747
  ok: false,
662
748
  courseDir: outDir,
663
749
  target,
664
- issues: [{ path: "output", message: err instanceof Error ? err.message : String(err) }]
750
+ issues: [
751
+ {
752
+ path: "output",
753
+ message: (
754
+ /* v8 ignore next */
755
+ err instanceof Error ? err.message : String(err)
756
+ )
757
+ }
758
+ ]
665
759
  };
666
760
  }
667
761
  }
@@ -750,7 +844,10 @@ async function promoteStagingToOutDir(stagingDir, outDir) {
750
844
  try {
751
845
  await renameOrCopy(tmpPromote, failedPromote2);
752
846
  } catch {
753
- await fsp.rm(tmpPromote, { recursive: true, force: true }).catch(() => void 0);
847
+ await fsp.rm(tmpPromote, { recursive: true, force: true }).catch(
848
+ /* v8 ignore next */
849
+ () => void 0
850
+ );
754
851
  }
755
852
  const promoteMsg = promoteError instanceof Error ? promoteError.message : String(promoteError);
756
853
  const restoreMsg = restoreError instanceof Error ? restoreError.message : String(restoreError);
@@ -766,7 +863,10 @@ async function promoteStagingToOutDir(stagingDir, outDir) {
766
863
  `[lessonkit/lxpack] failed to restore ${stagingDir} after promote error:`,
767
864
  restoreError instanceof Error ? restoreError.message : restoreError
768
865
  );
769
- await fsp.rm(tmpPromote, { recursive: true, force: true }).catch(() => void 0);
866
+ await fsp.rm(tmpPromote, { recursive: true, force: true }).catch(
867
+ /* v8 ignore next */
868
+ () => void 0
869
+ );
770
870
  }
771
871
  throw promoteError;
772
872
  }
@@ -774,12 +874,18 @@ async function promoteStagingToOutDir(stagingDir, outDir) {
774
874
  try {
775
875
  await renameOrCopy(tmpPromote, failedPromote);
776
876
  } catch {
777
- await fsp.rm(tmpPromote, { recursive: true, force: true }).catch(() => void 0);
877
+ await fsp.rm(tmpPromote, { recursive: true, force: true }).catch(
878
+ /* v8 ignore next */
879
+ () => void 0
880
+ );
778
881
  }
779
882
  throw promoteError;
780
883
  }
781
884
  if (backup) {
782
- await fsp.rm(backup, { recursive: true, force: true }).catch(() => void 0);
885
+ await fsp.rm(backup, { recursive: true, force: true }).catch(
886
+ /* v8 ignore next */
887
+ () => void 0
888
+ );
783
889
  }
784
890
  }
785
891
 
@@ -842,7 +948,10 @@ async function buildStagingPackage(options) {
842
948
  outputDir: "outputDir" in build ? build.outputDir : void 0
843
949
  };
844
950
  } catch (err) {
845
- await fsp2.rm(stagingDir, { recursive: true, force: true }).catch(() => void 0);
951
+ await fsp2.rm(stagingDir, { recursive: true, force: true }).catch(
952
+ /* v8 ignore next */
953
+ () => void 0
954
+ );
846
955
  throw err;
847
956
  }
848
957
  }
@@ -908,7 +1017,10 @@ async function packageLessonkitCourse(options) {
908
1017
  outputBaseDir
909
1018
  });
910
1019
  if (!staged.ok) {
911
- await fsp3.rm(staged.stagingDir, { recursive: true, force: true }).catch(() => void 0);
1020
+ await fsp3.rm(staged.stagingDir, { recursive: true, force: true }).catch(
1021
+ /* v8 ignore next */
1022
+ () => void 0
1023
+ );
912
1024
  const validation2 = staged.build ? { ok: false, issues: staged.build.issues } : void 0;
913
1025
  return {
914
1026
  ok: false,
@@ -926,7 +1038,10 @@ async function packageLessonkitCourse(options) {
926
1038
  validateArtifactInStaging(stagingRoot, staged.outputDir, "outputDir")
927
1039
  ].filter((issue) => issue != null);
928
1040
  if (artifactIssues.length > 0) {
929
- await fsp3.rm(stagingDir, { recursive: true, force: true }).catch(() => void 0);
1041
+ await fsp3.rm(stagingDir, { recursive: true, force: true }).catch(
1042
+ /* v8 ignore next */
1043
+ () => void 0
1044
+ );
930
1045
  return {
931
1046
  ok: false,
932
1047
  courseDir: outDir,
@@ -1028,7 +1143,10 @@ function parseLessonkitManifest(raw, label = "lessonkit.json", projectRoot) {
1028
1143
  if (!validation.ok) {
1029
1144
  for (const i of validation.issues) {
1030
1145
  issues.push({
1031
- path: i.path.startsWith("course.") ? i.path : `course.${i.path}`,
1146
+ path: (
1147
+ /* v8 ignore next */
1148
+ i.path.startsWith("course.") ? i.path : `course.${i.path}`
1149
+ ),
1032
1150
  message: i.message
1033
1151
  });
1034
1152
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lessonkit/lxpack",
3
- "version": "1.0.2",
3
+ "version": "1.1.0",
4
4
  "private": false,
5
5
  "description": "LXPack export adapter for LessonKit courses (SCORM, standalone, xAPI, cmi5).",
6
6
  "license": "Apache-2.0",
@@ -55,8 +55,8 @@
55
55
  "lint": "echo \"(no lint configured yet)\""
56
56
  },
57
57
  "dependencies": {
58
- "@lessonkit/core": "1.0.2",
59
- "@lessonkit/themes": "1.0.2",
58
+ "@lessonkit/core": "1.1.0",
59
+ "@lessonkit/themes": "1.1.0",
60
60
  "@lxpack/api": "^0.6.2",
61
61
  "@lxpack/spa-bridge": "^0.6.2",
62
62
  "@lxpack/tracking-schema": "^0.6.2",