@lxpack/validators 0.1.1 → 0.2.1

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/README.md CHANGED
@@ -5,7 +5,7 @@
5
5
  [![License](https://img.shields.io/github/license/eddiethedean/lxpack)](https://github.com/eddiethedean/lxpack/blob/main/LICENSE)
6
6
  [![Node.js](https://img.shields.io/badge/node-%3E%3D20-brightgreen)](https://nodejs.org/)
7
7
 
8
- Zod schemas and filesystem validation for LXPack course manifests.
8
+ Zod schemas and filesystem validation for LXPack course manifests — including flow, variables, and component lessons (v0.2.0).
9
9
 
10
10
  Part of [LXPack](https://github.com/eddiethedean/lxpack) — an AI-native learning experience compiler and runtime.
11
11
 
@@ -14,6 +14,7 @@ Part of [LXPack](https://github.com/eddiethedean/lxpack) — an AI-native learni
14
14
  | CLI | [`@lxpack/cli`](../cli/README.md) |
15
15
  | Packaging | [`@lxpack/scorm`](../scorm/README.md) |
16
16
  | Runtime | [`@lxpack/runtime`](../runtime/README.md) |
17
+ | Components | [`@lxpack/components`](../components/README.md) |
17
18
 
18
19
  ## Install
19
20
 
@@ -44,7 +45,9 @@ if (!result.valid) {
44
45
  const { manifest } = result;
45
46
  const bundle = await buildRuntimeAssessmentBundle("/path/to/my-course", manifest);
46
47
  // bundle.assessments — learner-facing questions (no correct flags)
47
- // bundle.answerKeys — scoring keys for the runtime (embedded at build time)
48
+ // bundle.answerKeys — scoring keys for the runtime
49
+ // bundle.configs — maxAttempts, shuffleChoices, showFeedback per assessment
50
+ // bundle.feedback — questionId → explanation text for feedback modes
48
51
  }
49
52
  ```
50
53
 
@@ -65,6 +68,14 @@ if (Array.isArray(loaded)) {
65
68
  const parsed = courseManifestSchema.safeParse(manifestObject);
66
69
  ```
67
70
 
71
+ ### Flow validation
72
+
73
+ ```ts
74
+ import { validateFlow, detectFlowCycles, collectActivityIds } from "@lxpack/validators";
75
+ ```
76
+
77
+ `validateCourse` runs flow checks when `manifest.flow` is present: valid `goto` targets, known condition shapes, and cycle detection.
78
+
68
79
  ### Safe path resolution
69
80
 
70
81
  ```ts
@@ -78,34 +89,43 @@ isPathContained(courseDir, abs); // true if inside course root
78
89
 
79
90
  | Export | Description |
80
91
  |--------|-------------|
81
- | `validateCourse(dir)` | Parse `course.yaml`, validate schema, check files, symlink containment |
92
+ | `validateCourse(dir)` | Parse `course.yaml`, validate schema, flow, files, symlink containment |
82
93
  | `loadManifest(courseDir)` | Load and parse `course.yaml` |
83
- | `buildRuntimeAssessmentBundle(dir, manifest)` | Load assessments; split learner view vs answer keys |
84
- | `toLearnerAssessment(assessment)` | Strip `correct` / `explanation` from one assessment |
94
+ | `buildRuntimeAssessmentBundle(dir, manifest)` | Load assessments; split learner view, keys, configs, feedback |
95
+ | `toLearnerAssessment(assessment)` | Strip `correct` from choices; extract config and feedback maps |
96
+ | `validateFlow(manifest)` | Flow rule and target validation |
97
+ | `detectFlowCycles(flow)` | Cycle detection for branching graphs |
98
+ | `collectActivityIds(manifest)` | Lesson and assessment IDs for flow targets |
99
+ | `conditionSchema`, `flowRuleSchema` | Zod schemas for flow conditions and rules |
100
+ | `BUILTIN_COMPONENT_IDS`, `isBuiltinComponentId` | Allowed built-in component lesson IDs |
85
101
  | `resolveCoursePath(dir, relativePath)` | Resolve a path safely inside the course directory |
86
102
  | `isPathContained(root, target)` | Whether `target` stays under `root` |
87
103
  | `courseManifestSchema` | Zod schema for the full course manifest |
88
- | `lessonSchema`, `assessmentSchema`, … | Strict sub-schemas for manifest sections |
89
- | `CourseManifest`, `Lesson`, `Assessment`, `LearnerAssessment`, `RuntimeAssessmentBundle` | TypeScript types |
104
+ | `lessonSchema`, `assessmentSchema`, `variableDefSchema`, … | Strict sub-schemas |
105
+ | `CourseManifest`, `Lesson`, `Assessment`, `FlowRule`, `VariableDef`, `RuntimeAssessmentBundle` | TypeScript types |
90
106
 
91
107
  ## What gets validated
92
108
 
93
- - Manifest shape (lessons, assessments, tracking rules)
94
- - Lesson types: `markdown` (`file`) and `html` (`path`)
95
- - Assessment YAML: strict MCQ schemas (`correct` on exactly one choice per question)
109
+ - Manifest shape: lessons, assessments, optional `variables` and `flow`, tracking rules
110
+ - Lesson types: `markdown` (`file`), `html` (`path`), `component` (`component` + optional `props`)
111
+ - Component IDs: built-in IDs or course overrides under `components/<id>/`
112
+ - Flow rules: condition grammar, `goto` targets that reference known activity IDs, acyclic flow (warnings for cycles)
113
+ - Assessment YAML: strict MCQ schemas; optional `maxAttempts`, `shuffleChoices`, `showFeedback`; `explanation` per question
96
114
  - Duplicate lesson IDs
97
115
  - Path containment — referenced files must stay inside the course directory (including via symlinks)
98
- - On-disk assets: files exist and assessments paths are regular files
116
+ - On-disk assets: files exist and assessment paths are regular files
99
117
 
100
118
  ## Assessment packaging
101
119
 
102
120
  Author assessments live as YAML under `assessments/` in the course repo. At build/preview time:
103
121
 
104
122
  1. `buildRuntimeAssessmentBundle()` reads each assessment file.
105
- 2. **Learner payload** — questions and choices without `correct` or `explanation`.
123
+ 2. **Learner payload** — questions and choices without `correct` flags.
106
124
  3. **Answer keys** — `questionId → choiceId` map for scoring.
125
+ 4. **Configs** — per-assessment quiz behavior (`maxAttempts`, `shuffleChoices`, `showFeedback`).
126
+ 5. **Feedback** — `questionId → explanation` for immediate/end feedback (not shipped as separate files).
107
127
 
108
- The CLI and [`@lxpack/scorm`](../scorm/README.md) embed both in the HTML config JSON. Exported ZIPs **do not** include `assessments/` files, so answer keys are not fetchable as static assets.
128
+ The CLI and [`@lxpack/scorm`](../scorm/README.md) embed all of this in the HTML config JSON. Exported ZIPs **do not** include `assessments/` files, so answer keys are not fetchable as static assets.
109
129
 
110
130
  ## Development
111
131
 
@@ -120,6 +140,8 @@ pnpm --filter @lxpack/validators typecheck
120
140
  ## Links
121
141
 
122
142
  - [LXPack repository](https://github.com/eddiethedean/lxpack)
143
+ - [Documentation index](https://github.com/eddiethedean/lxpack/blob/main/docs/README.md)
144
+ - [Technical specification](https://github.com/eddiethedean/lxpack/blob/main/docs/SPEC.md)
123
145
  - [Changelog](https://github.com/eddiethedean/lxpack/blob/main/CHANGELOG.md)
124
146
 
125
147
  ## License
package/dist/index.d.ts CHANGED
@@ -1,5 +1,35 @@
1
1
  import { z } from 'zod';
2
2
 
3
+ type Condition = {
4
+ variable: {
5
+ eq: [string, string | number | boolean];
6
+ };
7
+ } | {
8
+ assessment: {
9
+ passed: string;
10
+ };
11
+ } | {
12
+ interaction: {
13
+ done: string;
14
+ };
15
+ } | {
16
+ all: Condition[];
17
+ } | {
18
+ any: Condition[];
19
+ };
20
+ declare const conditionSchema: z.ZodType<Condition>;
21
+ declare const flowRuleSchema: z.ZodObject<{
22
+ when: z.ZodType<Condition, z.ZodTypeDef, Condition>;
23
+ goto: z.ZodString;
24
+ }, "strict", z.ZodTypeAny, {
25
+ when: Condition;
26
+ goto: string;
27
+ }, {
28
+ when: Condition;
29
+ goto: string;
30
+ }>;
31
+ type FlowRule = z.infer<typeof flowRuleSchema>;
32
+
3
33
  declare const assessmentQuestionSchema: z.ZodEffects<z.ZodObject<{
4
34
  id: z.ZodString;
5
35
  prompt: z.ZodString;
@@ -60,13 +90,13 @@ declare const markdownLessonSchema: z.ZodObject<{
60
90
  file: z.ZodString;
61
91
  title: z.ZodOptional<z.ZodString>;
62
92
  }, "strict", z.ZodTypeAny, {
63
- id: string;
64
93
  type: "markdown";
94
+ id: string;
65
95
  file: string;
66
96
  title?: string | undefined;
67
97
  }, {
68
- id: string;
69
98
  type: "markdown";
99
+ id: string;
70
100
  file: string;
71
101
  title?: string | undefined;
72
102
  }>;
@@ -76,15 +106,34 @@ declare const htmlLessonSchema: z.ZodObject<{
76
106
  path: z.ZodString;
77
107
  title: z.ZodOptional<z.ZodString>;
78
108
  }, "strict", z.ZodTypeAny, {
79
- id: string;
80
109
  path: string;
81
110
  type: "html";
111
+ id: string;
82
112
  title?: string | undefined;
83
113
  }, {
84
- id: string;
85
114
  path: string;
86
115
  type: "html";
116
+ id: string;
117
+ title?: string | undefined;
118
+ }>;
119
+ declare const componentLessonSchema: z.ZodObject<{
120
+ id: z.ZodString;
121
+ type: z.ZodLiteral<"component">;
122
+ component: z.ZodString;
123
+ props: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
124
+ title: z.ZodOptional<z.ZodString>;
125
+ }, "strict", z.ZodTypeAny, {
126
+ type: "component";
127
+ id: string;
128
+ component: string;
129
+ title?: string | undefined;
130
+ props?: Record<string, unknown> | undefined;
131
+ }, {
132
+ type: "component";
133
+ id: string;
134
+ component: string;
87
135
  title?: string | undefined;
136
+ props?: Record<string, unknown> | undefined;
88
137
  }>;
89
138
  declare const lessonSchema: z.ZodDiscriminatedUnion<"type", [z.ZodObject<{
90
139
  id: z.ZodString;
@@ -92,13 +141,13 @@ declare const lessonSchema: z.ZodDiscriminatedUnion<"type", [z.ZodObject<{
92
141
  file: z.ZodString;
93
142
  title: z.ZodOptional<z.ZodString>;
94
143
  }, "strict", z.ZodTypeAny, {
95
- id: string;
96
144
  type: "markdown";
145
+ id: string;
97
146
  file: string;
98
147
  title?: string | undefined;
99
148
  }, {
100
- id: string;
101
149
  type: "markdown";
150
+ id: string;
102
151
  file: string;
103
152
  title?: string | undefined;
104
153
  }>, z.ZodObject<{
@@ -107,20 +156,42 @@ declare const lessonSchema: z.ZodDiscriminatedUnion<"type", [z.ZodObject<{
107
156
  path: z.ZodString;
108
157
  title: z.ZodOptional<z.ZodString>;
109
158
  }, "strict", z.ZodTypeAny, {
110
- id: string;
111
159
  path: string;
112
160
  type: "html";
161
+ id: string;
113
162
  title?: string | undefined;
114
163
  }, {
115
- id: string;
116
164
  path: string;
117
165
  type: "html";
166
+ id: string;
167
+ title?: string | undefined;
168
+ }>, z.ZodObject<{
169
+ id: z.ZodString;
170
+ type: z.ZodLiteral<"component">;
171
+ component: z.ZodString;
172
+ props: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
173
+ title: z.ZodOptional<z.ZodString>;
174
+ }, "strict", z.ZodTypeAny, {
175
+ type: "component";
176
+ id: string;
177
+ component: string;
118
178
  title?: string | undefined;
179
+ props?: Record<string, unknown> | undefined;
180
+ }, {
181
+ type: "component";
182
+ id: string;
183
+ component: string;
184
+ title?: string | undefined;
185
+ props?: Record<string, unknown> | undefined;
119
186
  }>]>;
187
+ declare const showFeedbackSchema: z.ZodDefault<z.ZodEnum<["immediate", "end", "never"]>>;
120
188
  declare const assessmentSchema: z.ZodObject<{
121
189
  id: z.ZodString;
122
190
  title: z.ZodOptional<z.ZodString>;
123
191
  passingScore: z.ZodDefault<z.ZodNumber>;
192
+ maxAttempts: z.ZodOptional<z.ZodNumber>;
193
+ shuffleChoices: z.ZodOptional<z.ZodBoolean>;
194
+ showFeedback: z.ZodOptional<z.ZodDefault<z.ZodEnum<["immediate", "end", "never"]>>>;
124
195
  questions: z.ZodArray<z.ZodEffects<z.ZodObject<{
125
196
  id: z.ZodString;
126
197
  prompt: z.ZodString;
@@ -189,6 +260,9 @@ declare const assessmentSchema: z.ZodObject<{
189
260
  explanation?: string | undefined;
190
261
  }[];
191
262
  title?: string | undefined;
263
+ maxAttempts?: number | undefined;
264
+ shuffleChoices?: boolean | undefined;
265
+ showFeedback?: "never" | "immediate" | "end" | undefined;
192
266
  }, {
193
267
  id: string;
194
268
  questions: {
@@ -203,6 +277,9 @@ declare const assessmentSchema: z.ZodObject<{
203
277
  }[];
204
278
  title?: string | undefined;
205
279
  passingScore?: number | undefined;
280
+ maxAttempts?: number | undefined;
281
+ shuffleChoices?: boolean | undefined;
282
+ showFeedback?: "never" | "immediate" | "end" | undefined;
206
283
  }>;
207
284
  declare const assessmentRefSchema: z.ZodObject<{
208
285
  id: z.ZodString;
@@ -214,6 +291,22 @@ declare const assessmentRefSchema: z.ZodObject<{
214
291
  id: string;
215
292
  file: string;
216
293
  }>;
294
+ declare const variableDefSchema: z.ZodEffects<z.ZodObject<{
295
+ default: z.ZodUnion<[z.ZodString, z.ZodNumber, z.ZodBoolean]>;
296
+ type: z.ZodOptional<z.ZodEnum<["string", "number", "boolean"]>>;
297
+ }, "strict", z.ZodTypeAny, {
298
+ default: string | number | boolean;
299
+ type?: "string" | "number" | "boolean" | undefined;
300
+ }, {
301
+ default: string | number | boolean;
302
+ type?: "string" | "number" | "boolean" | undefined;
303
+ }>, {
304
+ default: string | number | boolean;
305
+ type?: "string" | "number" | "boolean" | undefined;
306
+ }, {
307
+ default: string | number | boolean;
308
+ type?: "string" | "number" | "boolean" | undefined;
309
+ }>;
217
310
  declare const trackingSchema: z.ZodOptional<z.ZodObject<{
218
311
  completion: z.ZodOptional<z.ZodObject<{
219
312
  threshold: z.ZodDefault<z.ZodNumber>;
@@ -238,6 +331,7 @@ declare const runtimeConfigSchema: z.ZodOptional<z.ZodObject<{
238
331
  }, {
239
332
  theme?: string | undefined;
240
333
  }>>;
334
+
241
335
  declare const courseManifestSchema: z.ZodObject<{
242
336
  title: z.ZodString;
243
337
  version: z.ZodString;
@@ -266,19 +360,45 @@ declare const courseManifestSchema: z.ZodObject<{
266
360
  threshold?: number | undefined;
267
361
  } | undefined;
268
362
  }>>;
363
+ variables: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodEffects<z.ZodObject<{
364
+ default: z.ZodUnion<[z.ZodString, z.ZodNumber, z.ZodBoolean]>;
365
+ type: z.ZodOptional<z.ZodEnum<["string", "number", "boolean"]>>;
366
+ }, "strict", z.ZodTypeAny, {
367
+ default: string | number | boolean;
368
+ type?: "string" | "number" | "boolean" | undefined;
369
+ }, {
370
+ default: string | number | boolean;
371
+ type?: "string" | "number" | "boolean" | undefined;
372
+ }>, {
373
+ default: string | number | boolean;
374
+ type?: "string" | "number" | "boolean" | undefined;
375
+ }, {
376
+ default: string | number | boolean;
377
+ type?: "string" | "number" | "boolean" | undefined;
378
+ }>>>;
379
+ flow: z.ZodOptional<z.ZodArray<z.ZodObject<{
380
+ when: z.ZodType<Condition, z.ZodTypeDef, Condition>;
381
+ goto: z.ZodString;
382
+ }, "strict", z.ZodTypeAny, {
383
+ when: Condition;
384
+ goto: string;
385
+ }, {
386
+ when: Condition;
387
+ goto: string;
388
+ }>, "many">>;
269
389
  lessons: z.ZodArray<z.ZodDiscriminatedUnion<"type", [z.ZodObject<{
270
390
  id: z.ZodString;
271
391
  type: z.ZodLiteral<"markdown">;
272
392
  file: z.ZodString;
273
393
  title: z.ZodOptional<z.ZodString>;
274
394
  }, "strict", z.ZodTypeAny, {
275
- id: string;
276
395
  type: "markdown";
396
+ id: string;
277
397
  file: string;
278
398
  title?: string | undefined;
279
399
  }, {
280
- id: string;
281
400
  type: "markdown";
401
+ id: string;
282
402
  file: string;
283
403
  title?: string | undefined;
284
404
  }>, z.ZodObject<{
@@ -287,15 +407,33 @@ declare const courseManifestSchema: z.ZodObject<{
287
407
  path: z.ZodString;
288
408
  title: z.ZodOptional<z.ZodString>;
289
409
  }, "strict", z.ZodTypeAny, {
290
- id: string;
291
410
  path: string;
292
411
  type: "html";
412
+ id: string;
293
413
  title?: string | undefined;
294
414
  }, {
295
- id: string;
296
415
  path: string;
297
416
  type: "html";
417
+ id: string;
418
+ title?: string | undefined;
419
+ }>, z.ZodObject<{
420
+ id: z.ZodString;
421
+ type: z.ZodLiteral<"component">;
422
+ component: z.ZodString;
423
+ props: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
424
+ title: z.ZodOptional<z.ZodString>;
425
+ }, "strict", z.ZodTypeAny, {
426
+ type: "component";
427
+ id: string;
428
+ component: string;
429
+ title?: string | undefined;
430
+ props?: Record<string, unknown> | undefined;
431
+ }, {
432
+ type: "component";
433
+ id: string;
434
+ component: string;
298
435
  title?: string | undefined;
436
+ props?: Record<string, unknown> | undefined;
299
437
  }>]>, "many">;
300
438
  assessments: z.ZodOptional<z.ZodArray<z.ZodObject<{
301
439
  id: z.ZodString;
@@ -311,15 +449,21 @@ declare const courseManifestSchema: z.ZodObject<{
311
449
  title: string;
312
450
  version: string;
313
451
  lessons: ({
314
- id: string;
315
452
  type: "markdown";
453
+ id: string;
316
454
  file: string;
317
455
  title?: string | undefined;
318
456
  } | {
319
- id: string;
320
457
  path: string;
321
458
  type: "html";
459
+ id: string;
460
+ title?: string | undefined;
461
+ } | {
462
+ type: "component";
463
+ id: string;
464
+ component: string;
322
465
  title?: string | undefined;
466
+ props?: Record<string, unknown> | undefined;
323
467
  })[];
324
468
  description?: string | undefined;
325
469
  runtime?: {
@@ -330,6 +474,14 @@ declare const courseManifestSchema: z.ZodObject<{
330
474
  threshold: number;
331
475
  } | undefined;
332
476
  } | undefined;
477
+ variables?: Record<string, {
478
+ default: string | number | boolean;
479
+ type?: "string" | "number" | "boolean" | undefined;
480
+ }> | undefined;
481
+ flow?: {
482
+ when: Condition;
483
+ goto: string;
484
+ }[] | undefined;
333
485
  assessments?: {
334
486
  id: string;
335
487
  file: string;
@@ -338,15 +490,21 @@ declare const courseManifestSchema: z.ZodObject<{
338
490
  title: string;
339
491
  version: string;
340
492
  lessons: ({
341
- id: string;
342
493
  type: "markdown";
494
+ id: string;
343
495
  file: string;
344
496
  title?: string | undefined;
345
497
  } | {
346
- id: string;
347
498
  path: string;
348
499
  type: "html";
500
+ id: string;
501
+ title?: string | undefined;
502
+ } | {
503
+ type: "component";
504
+ id: string;
505
+ component: string;
349
506
  title?: string | undefined;
507
+ props?: Record<string, unknown> | undefined;
350
508
  })[];
351
509
  description?: string | undefined;
352
510
  runtime?: {
@@ -357,6 +515,14 @@ declare const courseManifestSchema: z.ZodObject<{
357
515
  threshold?: number | undefined;
358
516
  } | undefined;
359
517
  } | undefined;
518
+ variables?: Record<string, {
519
+ default: string | number | boolean;
520
+ type?: "string" | "number" | "boolean" | undefined;
521
+ }> | undefined;
522
+ flow?: {
523
+ when: Condition;
524
+ goto: string;
525
+ }[] | undefined;
360
526
  assessments?: {
361
527
  id: string;
362
528
  file: string;
@@ -366,19 +532,15 @@ type CourseManifest = z.infer<typeof courseManifestSchema>;
366
532
  type Lesson = z.infer<typeof lessonSchema>;
367
533
  type Assessment = z.infer<typeof assessmentSchema>;
368
534
  type AssessmentRef = z.infer<typeof assessmentRefSchema>;
535
+ type VariableDef = z.infer<typeof variableDefSchema>;
536
+ type ComponentLesson = z.infer<typeof componentLessonSchema>;
537
+ type ShowFeedback = z.infer<typeof showFeedbackSchema>;
538
+
539
+ /** Built-in component ids shipped in @lxpack/components */
540
+ declare const BUILTIN_COMPONENT_IDS: readonly ["callout", "image-card", "checklist"];
541
+ type BuiltinComponentId = (typeof BUILTIN_COMPONENT_IDS)[number];
542
+ declare function isBuiltinComponentId(id: string): id is BuiltinComponentId;
369
543
 
370
- declare function formatErrorMessage(err: unknown): string;
371
- declare function formatIssuePath(path: PropertyKey[]): string;
372
- interface ValidationIssue {
373
- path: string;
374
- message: string;
375
- severity: "error" | "warning";
376
- }
377
- interface ValidationResult {
378
- valid: boolean;
379
- manifest?: CourseManifest;
380
- issues: ValidationIssue[];
381
- }
382
544
  declare function isPathContained(rootDir: string, candidatePath: string): boolean;
383
545
  declare function resolveCoursePath(courseDir: string, relativePath: string): {
384
546
  ok: true;
@@ -393,12 +555,33 @@ declare function assertResolvedPathContained(courseDir: string, resolvedPath: st
393
555
  ok: false;
394
556
  message: string;
395
557
  };
558
+
559
+ declare function formatErrorMessage(err: unknown): string;
560
+ declare function formatIssuePath(path: PropertyKey[]): string;
561
+ interface ValidationIssue {
562
+ path: string;
563
+ message: string;
564
+ severity: "error" | "warning";
565
+ }
566
+ interface ValidationResult {
567
+ valid: boolean;
568
+ manifest?: CourseManifest;
569
+ issues: ValidationIssue[];
570
+ /** Populated when assessment files parse successfully. */
571
+ parsedAssessments?: Map<string, Assessment>;
572
+ }
396
573
  declare function loadManifest(courseDir: string): Promise<{
397
574
  manifest: CourseManifest;
398
575
  raw: unknown;
399
576
  } | ValidationIssue[]>;
400
577
  declare function validateCourse(courseDir: string): Promise<ValidationResult>;
401
578
 
579
+ declare function collectActivityIds(manifest: CourseManifest): Set<string>;
580
+ declare function collectAssessmentIds(manifest: CourseManifest): Set<string>;
581
+ declare function validateFlow(manifest: CourseManifest): ValidationIssue[];
582
+ /** Detect simple goto cycles (same-target chains). */
583
+ declare function detectFlowCycles(flow: FlowRule[]): string[];
584
+
402
585
  interface LearnerChoice {
403
586
  id: string;
404
587
  text: string;
@@ -414,14 +597,42 @@ interface LearnerAssessment {
414
597
  passingScore: number;
415
598
  questions: LearnerQuestion[];
416
599
  }
600
+ interface AssessmentRuntimeConfig {
601
+ maxAttempts: number;
602
+ shuffleChoices: boolean;
603
+ showFeedback: ShowFeedback;
604
+ }
605
+ interface QuestionFeedback {
606
+ [questionId: string]: string | undefined;
607
+ }
417
608
  interface RuntimeAssessmentBundle {
418
609
  assessments: Record<string, LearnerAssessment>;
419
610
  answerKeys: Record<string, Record<string, string>>;
611
+ configs: Record<string, AssessmentRuntimeConfig>;
612
+ feedback: Record<string, QuestionFeedback>;
420
613
  }
421
614
  declare function toLearnerAssessment(assessment: Assessment): {
422
615
  learner: LearnerAssessment;
423
616
  answerKey: Record<string, string>;
617
+ config: AssessmentRuntimeConfig;
618
+ feedback: QuestionFeedback;
424
619
  };
620
+
621
+ interface ParsedAssessmentsResult {
622
+ parsed: Map<string, Assessment>;
623
+ issues: ValidationIssue[];
624
+ }
625
+ declare function loadParsedAssessments(courseDir: string, manifest: CourseManifest): Promise<ParsedAssessmentsResult>;
626
+ declare function buildRuntimeAssessmentBundleFromParsed(parsed: Map<string, Assessment>): RuntimeAssessmentBundle;
425
627
  declare function buildRuntimeAssessmentBundle(courseDir: string, manifest: CourseManifest): Promise<RuntimeAssessmentBundle>;
426
628
 
427
- export { type Assessment, type AssessmentRef, type CourseManifest, type LearnerAssessment, type LearnerChoice, type LearnerQuestion, type Lesson, type RuntimeAssessmentBundle, type ValidationIssue, type ValidationResult, assertResolvedPathContained, assessmentQuestionSchema, assessmentRefSchema, assessmentSchema, buildRuntimeAssessmentBundle, courseManifestSchema, formatErrorMessage, formatIssuePath, htmlLessonSchema, isPathContained, lessonSchema, loadManifest, markdownLessonSchema, resolveCoursePath, runtimeConfigSchema, toLearnerAssessment, trackingSchema, validateCourse };
629
+ interface CourseActivity {
630
+ id: string;
631
+ title: string;
632
+ kind: "lesson" | "assessment";
633
+ }
634
+ declare function enumerateActivities(manifest: CourseManifest): CourseActivity[];
635
+
636
+ declare function escapeHtml(text: string): string;
637
+
638
+ export { type Assessment, type AssessmentRef, type AssessmentRuntimeConfig, BUILTIN_COMPONENT_IDS, type BuiltinComponentId, type ComponentLesson, type Condition, type CourseActivity, type CourseManifest, type FlowRule, type LearnerAssessment, type LearnerChoice, type LearnerQuestion, type Lesson, type QuestionFeedback, type RuntimeAssessmentBundle, type ShowFeedback, type ValidationIssue, type ValidationResult, type VariableDef, assertResolvedPathContained, assessmentQuestionSchema, assessmentRefSchema, assessmentSchema, buildRuntimeAssessmentBundle, buildRuntimeAssessmentBundleFromParsed, collectActivityIds, collectAssessmentIds, componentLessonSchema, conditionSchema, courseManifestSchema, detectFlowCycles, enumerateActivities, escapeHtml, flowRuleSchema, formatErrorMessage, formatIssuePath, htmlLessonSchema, isBuiltinComponentId, isPathContained, lessonSchema, loadManifest, loadParsedAssessments, markdownLessonSchema, resolveCoursePath, runtimeConfigSchema, showFeedbackSchema, toLearnerAssessment, trackingSchema, validateCourse, validateFlow, variableDefSchema };